mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-14 09:52:09 +03:00
commit
e6fe1c672c
150
core/client/app/components/gh-file-uploader.js
Normal file
150
core/client/app/components/gh-file-uploader.js
Normal file
@ -0,0 +1,150 @@
|
||||
import Ember from 'ember';
|
||||
import { invoke, invokeAction } from 'ember-invoke-action';
|
||||
import {
|
||||
RequestEntityTooLargeError,
|
||||
UnsupportedMediaTypeError
|
||||
} from 'ghost/services/ajax';
|
||||
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
inject: {service},
|
||||
isBlank,
|
||||
run
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'section',
|
||||
classNames: ['gh-image-uploader'],
|
||||
classNameBindings: ['dragClass'],
|
||||
|
||||
labelText: 'Select or drag-and-drop a file',
|
||||
url: null,
|
||||
paramName: 'file',
|
||||
|
||||
file: null,
|
||||
response: null,
|
||||
|
||||
dragClass: null,
|
||||
failureMessage: null,
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: service(),
|
||||
|
||||
formData: computed('file', function () {
|
||||
let paramName = this.get('paramName');
|
||||
let file = this.get('file');
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append(paramName, file);
|
||||
|
||||
return formData;
|
||||
}),
|
||||
|
||||
progressStyle: computed('uploadPercentage', function () {
|
||||
let percentage = this.get('uploadPercentage');
|
||||
let width = '';
|
||||
|
||||
if (percentage > 0) {
|
||||
width = `${percentage}%`;
|
||||
} else {
|
||||
width = '0';
|
||||
}
|
||||
|
||||
return Ember.String.htmlSafe(`width: ${width}`);
|
||||
}),
|
||||
|
||||
dragOver(event) {
|
||||
event.preventDefault();
|
||||
this.set('dragClass', '--drag-over');
|
||||
},
|
||||
|
||||
dragLeave(event) {
|
||||
event.preventDefault();
|
||||
this.set('dragClass', null);
|
||||
},
|
||||
|
||||
drop(event) {
|
||||
event.preventDefault();
|
||||
this.set('dragClass', null);
|
||||
if (event.dataTransfer.files) {
|
||||
invoke(this, 'fileSelected', event.dataTransfer.files);
|
||||
}
|
||||
},
|
||||
|
||||
generateRequest() {
|
||||
let ajax = this.get('ajax');
|
||||
let formData = this.get('formData');
|
||||
let url = this.get('url');
|
||||
|
||||
invokeAction(this, 'uploadStarted');
|
||||
|
||||
ajax.post(url, {
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'text',
|
||||
xhr: () => {
|
||||
let xhr = new window.XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
this._uploadProgress(event);
|
||||
}, false);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
}).then((response) => {
|
||||
this._uploadSuccess(JSON.parse(response));
|
||||
}).catch((error) => {
|
||||
this._uploadFailed(error);
|
||||
}).finally(() => {
|
||||
invokeAction(this, 'uploadFinished');
|
||||
});
|
||||
},
|
||||
|
||||
_uploadProgress(event) {
|
||||
if (event.lengthComputable) {
|
||||
run(() => {
|
||||
let percentage = Math.round((event.loaded / event.total) * 100);
|
||||
this.set('uploadPercentage', percentage);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_uploadSuccess(response) {
|
||||
invokeAction(this, 'uploadSuccess', response);
|
||||
invoke(this, 'reset');
|
||||
},
|
||||
|
||||
_uploadFailed(error) {
|
||||
let message;
|
||||
|
||||
if (error instanceof UnsupportedMediaTypeError) {
|
||||
message = 'The file type you uploaded is not supported.';
|
||||
} else if (error instanceof RequestEntityTooLargeError) {
|
||||
message = 'The file you uploaded was larger than the maximum file size your server allows.';
|
||||
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
||||
message = error.errors[0].message;
|
||||
} else {
|
||||
message = 'Something went wrong :(';
|
||||
}
|
||||
|
||||
this.set('failureMessage', message);
|
||||
invokeAction(this, 'uploadFailed', error);
|
||||
},
|
||||
|
||||
actions: {
|
||||
fileSelected(fileList) {
|
||||
this.set('file', fileList[0]);
|
||||
run.schedule('actions', this, function () {
|
||||
this.generateRequest();
|
||||
});
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
this.set('failureMessage', null);
|
||||
}
|
||||
}
|
||||
});
|
@ -28,7 +28,6 @@ export default Component.extend({
|
||||
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
session: service(),
|
||||
|
||||
// TODO: this wouldn't be necessary if the server could accept direct
|
||||
// file uploads
|
||||
|
27
core/client/app/components/gh-light-table.js
Normal file
27
core/client/app/components/gh-light-table.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Ember from 'ember';
|
||||
import LightTable from 'ember-light-table/components/light-table';
|
||||
|
||||
const {$, run} = Ember;
|
||||
|
||||
export default LightTable.extend({
|
||||
|
||||
// HACK: infinite pagination was not triggering when scrolling very fast
|
||||
// as the throttle triggers before scrolling into the buffer area but
|
||||
// the scroll finishes before the throttle timeout. Adding a debounce that
|
||||
// does the same thing means that we are guaranteed a final trigger when
|
||||
// scrolling stops
|
||||
//
|
||||
// An issue has been opened upstream, this can be removed if it gets fixed
|
||||
// https://github.com/offirgolan/ember-light-table/issues/15
|
||||
|
||||
_setupScrollEvents() {
|
||||
$(this.get('touchMoveContainer')).on('touchmove.light-table', run.bind(this, this._scrollHandler, '_touchmoveTimer'));
|
||||
$(this.get('scrollContainer')).on('scroll.light-table', run.bind(this, this._scrollHandler, '_scrollTimer'));
|
||||
$(this.get('scrollContainer')).on('scroll.light-table', run.bind(this, this._scrollHandler, '_scrollDebounce'));
|
||||
},
|
||||
|
||||
_scrollHandler(timer) {
|
||||
this.set(timer, run.debounce(this, this._onScroll, 100));
|
||||
this.set(timer, run.throttle(this, this._onScroll, 100));
|
||||
}
|
||||
});
|
@ -22,6 +22,7 @@ export default Component.extend({
|
||||
config: service(),
|
||||
session: service(),
|
||||
ghostPaths: service(),
|
||||
feature: service(),
|
||||
|
||||
mouseEnter() {
|
||||
this.sendAction('onMouseEnter');
|
||||
|
17
core/client/app/components/gh-subscribers-table.js
Normal file
17
core/client/app/components/gh-subscribers-table.js
Normal file
@ -0,0 +1,17 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['subscribers-table'],
|
||||
|
||||
table: null,
|
||||
|
||||
actions: {
|
||||
onScrolledToBottom() {
|
||||
let loadNextPage = this.get('loadNextPage');
|
||||
|
||||
if (!this.get('isLoading')) {
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
23
core/client/app/components/modals/delete-subscriber.js
Normal file
23
core/client/app/components/modals/delete-subscriber.js
Normal file
@ -0,0 +1,23 @@
|
||||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
|
||||
const {computed} = Ember;
|
||||
const {alias} = computed;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
|
||||
submitting: false,
|
||||
|
||||
subscriber: alias('model'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.set('submitting', true);
|
||||
|
||||
invokeAction(this, 'confirm').finally(() => {
|
||||
this.set('submitting', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
43
core/client/app/components/modals/import-subscribers.js
Normal file
43
core/client/app/components/modals/import-subscribers.js
Normal file
@ -0,0 +1,43 @@
|
||||
import Ember from 'ember';
|
||||
import { invokeAction } from 'ember-invoke-action';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||
|
||||
const {computed} = Ember;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
labelText: 'Select or drag-and-drop a CSV File',
|
||||
|
||||
response: null,
|
||||
closeDisabled: false,
|
||||
|
||||
uploadUrl: computed(function () {
|
||||
return `${ghostPaths().apiRoot}/subscribers/csv/`;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
uploadStarted() {
|
||||
this.set('closeDisabled', true);
|
||||
},
|
||||
|
||||
uploadFinished() {
|
||||
this.set('closeDisabled', false);
|
||||
},
|
||||
|
||||
uploadSuccess(response) {
|
||||
this.set('response', response.meta.stats);
|
||||
// invoke the passed in confirm action
|
||||
invokeAction(this, 'confirm');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
// noop - we don't want the enter key doing anything
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
if (!this.get('closeDisabled')) {
|
||||
this._super(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
32
core/client/app/components/modals/new-subscriber.js
Normal file
32
core/client/app/components/modals/new-subscriber.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Ember from 'ember';
|
||||
import ModalComponent from 'ghost/components/modals/base';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
actions: {
|
||||
updateEmail(newEmail) {
|
||||
this.set('model.email', newEmail);
|
||||
this.set('model.hasValidated', Ember.A());
|
||||
this.get('model.errors').clear();
|
||||
},
|
||||
|
||||
confirm() {
|
||||
let confirmAction = this.get('confirm');
|
||||
|
||||
this.set('submitting', true);
|
||||
|
||||
confirmAction().then(() => {
|
||||
this.send('closeModal');
|
||||
}).catch((errors) => {
|
||||
let [error] = errors;
|
||||
if (error && error.match(/email/)) {
|
||||
this.get('model.errors').add('email', error);
|
||||
this.get('model.hasValidated').pushObject('email');
|
||||
}
|
||||
}).finally(() => {
|
||||
if (!this.get('isDestroying') && !this.get('isDestroyed')) {
|
||||
this.set('submitting', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
162
core/client/app/controllers/subscribers.js
Normal file
162
core/client/app/controllers/subscribers.js
Normal file
@ -0,0 +1,162 @@
|
||||
import Ember from 'ember';
|
||||
import Table from 'ember-light-table';
|
||||
import PaginationMixin from 'ghost/mixins/pagination';
|
||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||
|
||||
const {
|
||||
$,
|
||||
assign,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Ember.Controller.extend(PaginationMixin, {
|
||||
|
||||
queryParams: ['order', 'direction'],
|
||||
order: 'created_at',
|
||||
direction: 'desc',
|
||||
|
||||
paginationModel: 'subscriber',
|
||||
|
||||
total: 0,
|
||||
table: null,
|
||||
subscriberToDelete: null,
|
||||
|
||||
session: service(),
|
||||
|
||||
// paginationSettings is replaced by the pagination mixin so we need a
|
||||
// getter/setter CP here so that we don't lose the dynamic order param
|
||||
paginationSettings: computed('order', 'direction', {
|
||||
get() {
|
||||
let order = this.get('order');
|
||||
let direction = this.get('direction');
|
||||
|
||||
let currentSettings = this._paginationSettings || {
|
||||
limit: 30
|
||||
};
|
||||
|
||||
return assign({}, currentSettings, {
|
||||
order: `${order} ${direction}`
|
||||
});
|
||||
},
|
||||
set(key, value) {
|
||||
this._paginationSettings = value;
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
|
||||
columns: computed('order', 'direction', function () {
|
||||
let order = this.get('order');
|
||||
let direction = this.get('direction');
|
||||
|
||||
return [{
|
||||
label: 'Subscriber',
|
||||
valuePath: 'email',
|
||||
sorted: order === 'email',
|
||||
ascending: direction === 'asc'
|
||||
}, {
|
||||
label: 'Subscription Date',
|
||||
valuePath: 'createdAt',
|
||||
format(value) {
|
||||
return value.format('MMMM DD, YYYY');
|
||||
},
|
||||
sorted: order === 'created_at',
|
||||
ascending: direction === 'asc'
|
||||
}, {
|
||||
label: 'Status',
|
||||
valuePath: 'status',
|
||||
sorted: order === 'status',
|
||||
ascending: direction === 'asc'
|
||||
}, {
|
||||
label: '',
|
||||
sortable: false,
|
||||
cellComponent: 'gh-subscribers-table-delete-cell',
|
||||
align: 'right'
|
||||
}];
|
||||
}),
|
||||
|
||||
initializeTable() {
|
||||
this.set('table', new Table(this.get('columns'), this.get('subscribers')));
|
||||
},
|
||||
|
||||
// capture the total from the server any time we fetch a new page
|
||||
didReceivePaginationMeta(meta) {
|
||||
if (meta && meta.pagination) {
|
||||
this.set('total', meta.pagination.total);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadFirstPage() {
|
||||
let table = this.get('table');
|
||||
|
||||
return this._super(...arguments).then((results) => {
|
||||
table.addRows(results);
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
loadNextPage() {
|
||||
let table = this.get('table');
|
||||
|
||||
return this._super(...arguments).then((results) => {
|
||||
table.addRows(results);
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
sortByColumn(column) {
|
||||
let table = this.get('table');
|
||||
|
||||
if (column.sorted) {
|
||||
this.setProperties({
|
||||
order: column.get('valuePath').trim().underscore(),
|
||||
direction: column.ascending ? 'asc' : 'desc'
|
||||
});
|
||||
table.setRows([]);
|
||||
this.send('loadFirstPage');
|
||||
}
|
||||
},
|
||||
|
||||
addSubscriber(subscriber) {
|
||||
this.get('table').insertRowAt(0, subscriber);
|
||||
this.incrementProperty('total');
|
||||
},
|
||||
|
||||
deleteSubscriber(subscriber) {
|
||||
this.set('subscriberToDelete', subscriber);
|
||||
},
|
||||
|
||||
confirmDeleteSubscriber() {
|
||||
let subscriber = this.get('subscriberToDelete');
|
||||
|
||||
return subscriber.destroyRecord().then(() => {
|
||||
this.set('subscriberToDelete', null);
|
||||
this.get('table').removeRow(subscriber);
|
||||
this.decrementProperty('total');
|
||||
});
|
||||
},
|
||||
|
||||
cancelDeleteSubscriber() {
|
||||
this.set('subscriberToDelete', null);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.get('table').setRows([]);
|
||||
this.send('loadFirstPage');
|
||||
},
|
||||
|
||||
exportData() {
|
||||
let exportUrl = ghostPaths().url.api('subscribers/csv');
|
||||
let accessToken = this.get('session.data.authenticated.access_token');
|
||||
let downloadURL = `${exportUrl}?access_token=${accessToken}`;
|
||||
let iframe = $('#iframeDownload');
|
||||
|
||||
if (iframe.length === 0) {
|
||||
iframe = $('<iframe>', {id: 'iframeDownload'}).hide().appendTo('body');
|
||||
}
|
||||
|
||||
iframe.attr('src', downloadURL);
|
||||
}
|
||||
}
|
||||
});
|
@ -47,12 +47,78 @@ function paginatedResponse(modelName, allModels, request) {
|
||||
};
|
||||
}
|
||||
|
||||
function mockSubscribers(server) {
|
||||
server.get('/subscribers/', function (db, request) {
|
||||
let response = paginatedResponse('subscribers', db.subscribers, request);
|
||||
return response;
|
||||
});
|
||||
|
||||
server.post('/subscribers/', function (db, request) {
|
||||
let [attrs] = JSON.parse(request.requestBody).subscribers;
|
||||
let [subscriber] = db.subscribers.where({email: attrs.email});
|
||||
|
||||
if (subscriber) {
|
||||
return new Mirage.Response(422, {}, {
|
||||
errors: [{
|
||||
errorType: 'DataImportError',
|
||||
message: 'duplicate email',
|
||||
property: 'email'
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
attrs.created_at = new Date();
|
||||
attrs.created_by = 0;
|
||||
|
||||
subscriber = db.subscribers.insert(attrs);
|
||||
|
||||
return {
|
||||
subscriber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.put('/subscribers/:id/', function (db, request) {
|
||||
let {id} = request.params;
|
||||
let [attrs] = JSON.parse(request.requestBody).subscribers;
|
||||
let subscriber = db.subscribers.update(id, attrs);
|
||||
|
||||
return {
|
||||
subscriber
|
||||
};
|
||||
});
|
||||
|
||||
server.del('/subscribers/:id/', function (db, request) {
|
||||
db.subscribers.remove(request.params.id);
|
||||
|
||||
return new Mirage.Response(204, {}, {});
|
||||
});
|
||||
|
||||
server.post('/subscribers/csv/', function (/*db, request*/) {
|
||||
// NB: we get a raw FormData object with no way to inspect it in Chrome
|
||||
// until version 50 adds the additional read methods
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FormData#Browser_compatibility
|
||||
|
||||
server.createList('subscriber', 50);
|
||||
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: 50,
|
||||
duplicates: 3,
|
||||
invalid: 2
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function () {
|
||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
||||
this.namespace = 'ghost/api/v0.1'; // make this `api`, for example, if your API is namespaced
|
||||
// this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||
this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||
|
||||
// Mock endpoints here to override real API requests during development
|
||||
mockSubscribers(this);
|
||||
|
||||
// keep this line, it allows all other API requests to hit the real server
|
||||
this.passthrough();
|
||||
@ -251,6 +317,10 @@ export function testConfig() {
|
||||
};
|
||||
});
|
||||
|
||||
/* Subscribers ---------------------------------------------------------- */
|
||||
|
||||
mockSubscribers(this);
|
||||
|
||||
/* Tags ----------------------------------------------------------------- */
|
||||
|
||||
this.post('/tags/', function (db, request) {
|
||||
|
21
core/client/app/mirage/factories/subscriber.js
Normal file
21
core/client/app/mirage/factories/subscriber.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Mirage, {faker} from 'ember-cli-mirage';
|
||||
|
||||
let randomDate = function randomDate(start = moment().subtract(30, 'days').toDate(), end = new Date()) {
|
||||
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
||||
};
|
||||
|
||||
let statuses = ['pending', 'subscribed'];
|
||||
|
||||
// jscs:disable requireBlocksOnNewline
|
||||
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
||||
export default Mirage.Factory.extend({
|
||||
uuid(i) { return `subscriber-${i}`; },
|
||||
name() { return `${faker.name.firstName()} ${faker.name.lastName()}`; },
|
||||
email() { return faker.internet.email(); },
|
||||
status() { return statuses[Math.floor(Math.random() * statuses.length)]; },
|
||||
created_at() { return randomDate(); },
|
||||
updated_at: null,
|
||||
created_by: 0,
|
||||
updated_by: null,
|
||||
unsubscribed_at: null
|
||||
});
|
@ -125,7 +125,7 @@ export default [
|
||||
id: 12,
|
||||
uuid: 'd806f358-7996-4c74-b153-8876959c4b70',
|
||||
key: 'labs',
|
||||
value: '{"codeInjectionUI":true}',
|
||||
value: '{"codeInjectionUI":true,"subscribers":true}',
|
||||
type: 'blog',
|
||||
created_at: '2015-01-12T18:29:01.000Z',
|
||||
created_by: 1,
|
||||
|
@ -1,6 +1,8 @@
|
||||
export default function (/* server */) {
|
||||
export default function (server) {
|
||||
// Seed your development database using your factories. This
|
||||
// data will not be loaded in your tests.
|
||||
|
||||
// server.createList('contact', 10);
|
||||
|
||||
server.createList('subscriber', 125);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import getRequestErrorMessage from 'ghost/utils/ajax';
|
||||
const {
|
||||
Mixin,
|
||||
computed,
|
||||
RSVP,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
@ -34,7 +35,7 @@ export default Mixin.create({
|
||||
|
||||
init() {
|
||||
let paginationSettings = this.get('paginationSettings');
|
||||
let settings = Ember.$.extend({}, defaultPaginationSettings, paginationSettings);
|
||||
let settings = Ember.assign({}, defaultPaginationSettings, paginationSettings);
|
||||
|
||||
this._super(...arguments);
|
||||
this.set('paginationSettings', settings);
|
||||
@ -63,7 +64,7 @@ export default Mixin.create({
|
||||
let paginationSettings = this.get('paginationSettings');
|
||||
let modelName = this.get('paginationModel');
|
||||
|
||||
paginationSettings.page = 1;
|
||||
this.set('paginationSettings.page', 1);
|
||||
|
||||
this.set('isLoading', true);
|
||||
|
||||
@ -93,7 +94,7 @@ export default Mixin.create({
|
||||
let nextPage = metadata.pagination && metadata.pagination.next;
|
||||
let paginationSettings = this.get('paginationSettings');
|
||||
|
||||
if (nextPage) {
|
||||
if (nextPage && !this.get('isLoading')) {
|
||||
this.set('isLoading', true);
|
||||
this.set('paginationSettings.page', nextPage);
|
||||
|
||||
@ -105,6 +106,8 @@ export default Mixin.create({
|
||||
}).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
});
|
||||
} else {
|
||||
return RSVP.resolve([]);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -3,18 +3,20 @@ import DS from 'ember-data';
|
||||
import Model from 'ember-data/model';
|
||||
import getRequestErrorMessage from 'ghost/utils/ajax';
|
||||
|
||||
import ValidatorExtensions from 'ghost/utils/validator-extensions';
|
||||
import PostValidator from 'ghost/validators/post';
|
||||
import SetupValidator from 'ghost/validators/setup';
|
||||
import SignupValidator from 'ghost/validators/signup';
|
||||
import SigninValidator from 'ghost/validators/signin';
|
||||
import SettingValidator from 'ghost/validators/setting';
|
||||
import ResetValidator from 'ghost/validators/reset';
|
||||
import UserValidator from 'ghost/validators/user';
|
||||
import TagSettingsValidator from 'ghost/validators/tag-settings';
|
||||
import NavItemValidator from 'ghost/validators/nav-item';
|
||||
import InviteUserValidator from 'ghost/validators/invite-user';
|
||||
import NavItemValidator from 'ghost/validators/nav-item';
|
||||
import PostValidator from 'ghost/validators/post';
|
||||
import ResetValidator from 'ghost/validators/reset';
|
||||
import SettingValidator from 'ghost/validators/setting';
|
||||
import SetupValidator from 'ghost/validators/setup';
|
||||
import SigninValidator from 'ghost/validators/signin';
|
||||
import SignupValidator from 'ghost/validators/signup';
|
||||
import SlackIntegrationValidator from 'ghost/validators/slack-integration';
|
||||
import SubscriberValidator from 'ghost/validators/subscriber';
|
||||
import TagSettingsValidator from 'ghost/validators/tag-settings';
|
||||
import UserValidator from 'ghost/validators/user';
|
||||
|
||||
import ValidatorExtensions from 'ghost/utils/validator-extensions';
|
||||
|
||||
const {Mixin, RSVP, isArray} = Ember;
|
||||
const {Errors} = DS;
|
||||
@ -36,17 +38,18 @@ export default Mixin.create({
|
||||
// in that case the model will be the class that the ValidationEngine
|
||||
// was mixed into, i.e. the controller or Ember Data model.
|
||||
validators: {
|
||||
post: PostValidator,
|
||||
setup: SetupValidator,
|
||||
signup: SignupValidator,
|
||||
signin: SigninValidator,
|
||||
setting: SettingValidator,
|
||||
reset: ResetValidator,
|
||||
user: UserValidator,
|
||||
tag: TagSettingsValidator,
|
||||
navItem: NavItemValidator,
|
||||
inviteUser: InviteUserValidator,
|
||||
slackIntegration: SlackIntegrationValidator
|
||||
navItem: NavItemValidator,
|
||||
post: PostValidator,
|
||||
reset: ResetValidator,
|
||||
setting: SettingValidator,
|
||||
setup: SetupValidator,
|
||||
signin: SigninValidator,
|
||||
signup: SignupValidator,
|
||||
slackIntegration: SlackIntegrationValidator,
|
||||
subscriber: SubscriberValidator,
|
||||
tag: TagSettingsValidator,
|
||||
user: UserValidator
|
||||
},
|
||||
|
||||
// This adds the Errors object to the validation engine, and shouldn't affect
|
||||
|
23
core/client/app/models/subscriber.js
Normal file
23
core/client/app/models/subscriber.js
Normal file
@ -0,0 +1,23 @@
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import {belongsTo} from 'ember-data/relationships';
|
||||
import ValidationEngine from 'ghost/mixins/validation-engine';
|
||||
|
||||
export default Model.extend(ValidationEngine, {
|
||||
validationType: 'subscriber',
|
||||
|
||||
uuid: attr('string'),
|
||||
name: attr('string'),
|
||||
email: attr('string'),
|
||||
status: attr('string'),
|
||||
subscribedUrl: attr('string'),
|
||||
subscribedReferrer: attr('string'),
|
||||
unsubscribedUrl: attr('string'),
|
||||
unsubscribedAt: attr('moment-date'),
|
||||
createdAt: attr('moment-date'),
|
||||
updatedAt: attr('moment-date'),
|
||||
createdBy: attr('number'),
|
||||
updatedBy: attr('number'),
|
||||
|
||||
post: belongsTo('post')
|
||||
});
|
@ -59,6 +59,11 @@ Router.map(function () {
|
||||
this.route('slack', {path: 'slack'});
|
||||
});
|
||||
|
||||
this.route('subscribers', function() {
|
||||
this.route('new');
|
||||
this.route('import');
|
||||
});
|
||||
|
||||
this.route('error404', {path: '/*path'});
|
||||
});
|
||||
|
||||
|
54
core/client/app/routes/subscribers.js
Normal file
54
core/client/app/routes/subscribers.js
Normal file
@ -0,0 +1,54 @@
|
||||
import Ember from 'ember';
|
||||
import AuthenticatedRoute from 'ghost/routes/authenticated';
|
||||
|
||||
const {
|
||||
RSVP,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default AuthenticatedRoute.extend({
|
||||
titleToken: 'Subscribers',
|
||||
|
||||
feature: service(),
|
||||
|
||||
// redirect if subscribers is disabled or user isn't owner/admin
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
let promises = {
|
||||
user: this.get('session.user'),
|
||||
subscribers: this.get('feature.subscribers')
|
||||
};
|
||||
|
||||
return RSVP.hash(promises).then((hash) => {
|
||||
let {user, subscribers} = hash;
|
||||
|
||||
if (!subscribers || !(user.get('isOwner') || user.get('isAdmin'))) {
|
||||
return this.transitionTo('posts');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.initializeTable();
|
||||
controller.send('loadFirstPage');
|
||||
},
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
this._super(...arguments);
|
||||
if (isExiting) {
|
||||
controller.set('order', 'created_at');
|
||||
controller.set('direction', 'desc');
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
addSubscriber(subscriber) {
|
||||
this.get('controller').send('addSubscriber', subscriber);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.get('controller').send('reset');
|
||||
}
|
||||
}
|
||||
});
|
9
core/client/app/routes/subscribers/import.js
Normal file
9
core/client/app/routes/subscribers/import.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
actions: {
|
||||
cancel() {
|
||||
this.transitionTo('subscribers');
|
||||
}
|
||||
}
|
||||
});
|
37
core/client/app/routes/subscribers/new.js
Normal file
37
core/client/app/routes/subscribers/new.js
Normal file
@ -0,0 +1,37 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.get('store').createRecord('subscriber');
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
let subscriber = this.controller.get('model');
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
if (subscriber.get('isNew')) {
|
||||
this.rollbackModel();
|
||||
}
|
||||
},
|
||||
|
||||
rollbackModel() {
|
||||
let subscriber = this.controller.get('model');
|
||||
subscriber.rollbackAttributes();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
let subscriber = this.controller.get('model');
|
||||
return subscriber.save().then((saved) => {
|
||||
this.send('addSubscriber', saved);
|
||||
return saved;
|
||||
});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.rollbackModel();
|
||||
this.transitionTo('subscribers');
|
||||
}
|
||||
}
|
||||
});
|
@ -31,6 +31,7 @@ export default Service.extend({
|
||||
notifications: service(),
|
||||
|
||||
publicAPI: feature('publicAPI'),
|
||||
subscribers: feature('subscribers'),
|
||||
|
||||
_settings: null,
|
||||
|
||||
|
@ -45,3 +45,4 @@
|
||||
@import "layouts/error.css";
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
@import "layouts/subscribers.css";
|
||||
|
@ -61,10 +61,6 @@
|
||||
/* The modal
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.fullscreen-modal .gh-image-uploader {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Modal content
|
||||
/* ---------------------------------------------------------- */
|
||||
@ -143,6 +139,10 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.modal-body .gh-image-uploader {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Content Modifiers
|
||||
/* ---------------------------------------------------------- */
|
||||
|
69
core/client/app/styles/layouts/subscribers.css
Normal file
69
core/client/app/styles/layouts/subscribers.css
Normal file
@ -0,0 +1,69 @@
|
||||
/* Subscribers Management /ghost/subscribers/
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.view-subscribers .view-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* Table (left pane)
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.subscribers-table {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 12px; /* ember-light-table has 8px padding on cells */
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.subscribers-table table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subscribers-table table .btn {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.subscribers-table table tr:hover .btn {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.subscribers-table tbody td:last-of-type {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Sidebar (right pane)
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.subscribers-sidebar {
|
||||
width: 350px;
|
||||
border-left: 1px solid #dfe1e3;
|
||||
}
|
||||
|
||||
.subscribers-import-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.subscribers-import-buttons .btn {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.subscribers-import-buttons .btn:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Import modal
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.subscribers-import-results {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
@ -65,6 +65,13 @@ fieldset[disabled] .btn {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.btn-hover-green:hover,
|
||||
.btn-hover-green:active,
|
||||
.btn-hover-green:focus {
|
||||
border-color: var(--green);
|
||||
color: color(var(--green) lightness(-10%));
|
||||
}
|
||||
|
||||
|
||||
/* Blue button
|
||||
/* ---------------------------------------------------------- */
|
||||
|
@ -87,9 +87,15 @@
|
||||
.icon-idea:before {
|
||||
content: "\e00e";
|
||||
}
|
||||
.icon-arrow:before {
|
||||
.icon-arrow:before,
|
||||
.icon-ascending:before,
|
||||
.icon-descending:before {
|
||||
content: "\e00f";
|
||||
}
|
||||
.icon-ascending:before {
|
||||
display: inline-block;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.icon-pen:before {
|
||||
content: "\e010";
|
||||
}
|
||||
|
@ -59,3 +59,20 @@ table td,
|
||||
.table.plain tbody > tr:nth-child(odd) > th {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Ember Light Table
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.ember-light-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ember-light-table .lt-column .lt-sort-icon {
|
||||
float: none;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
.lt-sort-icon.icon-ascending:before,
|
||||
.lt-sort-icon.icon-descending:before {
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
20
core/client/app/templates/components/gh-file-uploader.hbs
Normal file
20
core/client/app/templates/components/gh-file-uploader.hbs
Normal file
@ -0,0 +1,20 @@
|
||||
{{#if file}}
|
||||
{{!-- Upload in progress! --}}
|
||||
{{#if failureMessage}}
|
||||
<div class="failed">{{failureMessage}}</div>
|
||||
{{/if}}
|
||||
<div class="progress-container">
|
||||
<div class="progress">
|
||||
<div class="bar {{if failureMessage "fail"}}" style={{progressStyle}}></div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if failureMessage}}
|
||||
<button class="btn btn-green" {{action "reset"}}>Try Again</button>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="upload-form">
|
||||
{{#x-file-input multiple=false alt=labelText action=(action 'fileSelected') accept="text/csv"}}
|
||||
<div class="description">{{labelText}}</div>
|
||||
{{/x-file-input}}
|
||||
</div>
|
||||
{{/if}}
|
@ -25,6 +25,11 @@
|
||||
{{!<li><a href="#"><i class="icon-user"></i>My Posts</a></li>}}
|
||||
<li>{{#link-to "team" classNames="gh-nav-main-users"}}<i class="icon-team"></i>Team{{/link-to}}</li>
|
||||
{{!<li><a href="#"><i class="icon-idea"></i>Ideas</a></li>}}
|
||||
{{#if feature.subscribers}}
|
||||
{{#if (gh-user-can-admin session.user)}}
|
||||
<li>{{#link-to "subscribers" classNames="gh-nav-main-subscribers"}}<i class="icon-mail"></i>Subscribers{{/link-to}}</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{#if (gh-user-can-admin session.user)}}
|
||||
<ul class="gh-nav-list gh-nav-settings">
|
||||
|
@ -0,0 +1 @@
|
||||
<button class="btn btn-minor btn-sm" {{action tableActions.delete row.content}}><i class="icon-trash"></i></button>
|
@ -0,0 +1,17 @@
|
||||
{{#gh-light-table table scrollContainer=".subscribers-table" scrollBuffer=100 onScrolledToBottom=(action 'onScrolledToBottom') as |t|}}
|
||||
{{t.head onColumnClick=(action sortByColumn) iconAscending="icon-ascending" iconDescending="icon-descending"}}
|
||||
|
||||
{{#t.body canSelect=false tableActions=(hash delete=(action delete)) as |body|}}
|
||||
{{#if isLoading}}
|
||||
{{#body.loader}}
|
||||
Loading...
|
||||
{{/body.loader}}
|
||||
{{else}}
|
||||
{{#if table.isEmpty}}
|
||||
{{#body.no-data}}
|
||||
No subscribers found.
|
||||
{{/body.no-data}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/t.body}}
|
||||
{{/gh-light-table}}
|
@ -0,0 +1,13 @@
|
||||
<header class="modal-header">
|
||||
<h1>Are you sure?</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<strong>WARNING:</strong> All data for this subscriber will be deleted. There is no way to recover this.
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Delete{{/gh-spin-button}}
|
||||
</div>
|
@ -0,0 +1,47 @@
|
||||
<header class="modal-header">
|
||||
<h1>
|
||||
{{#if response}}
|
||||
Import Successful
|
||||
{{else}}
|
||||
Import Subscribers
|
||||
{{/if}}
|
||||
</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#liquid-if response class="fade-transition"}}
|
||||
<table class="subscribers-import-results">
|
||||
<tr>
|
||||
<td>Imported:</td>
|
||||
<td align="left">{{response.imported}}</td>
|
||||
</tr>
|
||||
{{#if response.duplicates}}
|
||||
<tr>
|
||||
<td>Duplicates:</td>
|
||||
<td align="left">{{response.duplicates}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{#if response.invalid}}
|
||||
<tr>
|
||||
<td>Invalid:</td>
|
||||
<td align="left">{{response.invalid}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
{{else}}
|
||||
{{gh-file-uploader
|
||||
url=uploadUrl
|
||||
paramName="subscribersfile"
|
||||
labelText="Select or drag-and-drop a CSV file."
|
||||
uploadStarted=(action 'uploadStarted')
|
||||
uploadFinished=(action 'uploadFinished')
|
||||
uploadSuccess=(action 'uploadSuccess')}}
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} disabled={{closeDisabled}} class="btn btn-default btn-minor">
|
||||
{{#if response}}Close{{else}}Cancel{{/if}}
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,29 @@
|
||||
<header class="modal-header">
|
||||
<h1>Add a Subscriber</h1>
|
||||
</header>
|
||||
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="email"}}
|
||||
<label for="new-subscriber-email">Email Address</label>
|
||||
<input type="email"
|
||||
value={{model.email}}
|
||||
oninput={{action "updateEmail" value="target.value"}}
|
||||
id="new-subscriber-email"
|
||||
class="gh-input email"
|
||||
placeholder="Email Address"
|
||||
name="email"
|
||||
autofocus="autofocus"
|
||||
autocapitalize="off"
|
||||
autocorrect="off">
|
||||
{{gh-error-message errors=model.errors property="email"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||
{{#gh-spin-button action="confirm" class="btn btn-green" submitting=submitting}}Add{{/gh-spin-button}}
|
||||
</div>
|
@ -50,6 +50,9 @@
|
||||
{{#gh-feature-flag "publicAPI"}}
|
||||
Public API - For full instructions, read the <a href="http://support.ghost.org/public-api-beta/">developer guide</a>.
|
||||
{{/gh-feature-flag}}
|
||||
{{#gh-feature-flag "subscribers"}}
|
||||
Subscribers - Allow visitors to subscribe to e-mail updates of your new posts
|
||||
{{/gh-feature-flag}}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
49
core/client/app/templates/subscribers.hbs
Normal file
49
core/client/app/templates/subscribers.hbs
Normal file
@ -0,0 +1,49 @@
|
||||
<section class="gh-view view-subscribers">
|
||||
<header class="view-header">
|
||||
{{#gh-view-title openMobileMenu="openMobileMenu"}}<span>Subscribers</span>{{/gh-view-title}}
|
||||
<div class="view-actions">
|
||||
{{#link-to "subscribers.new" class="btn btn-green"}}Add Subscriber{{/link-to}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="view-container">
|
||||
{{gh-subscribers-table
|
||||
table=table
|
||||
isLoading=isLoading
|
||||
loadNextPage=(action 'loadNextPage')
|
||||
sortByColumn=(action 'sortByColumn')
|
||||
delete=(action 'deleteSubscriber')}}
|
||||
|
||||
<div class="subscribers-sidebar">
|
||||
<div class="settings-menu-header">
|
||||
<h4>Import Subscribers</h4>
|
||||
</div>
|
||||
<div class="settings-menu-content subscribers-import-buttons">
|
||||
{{#link-to "subscribers.import" class="btn btn-hover-green"}}Import CSV{{/link-to}}
|
||||
<a {{action 'exportData'}} class="btn">Export CSV</a>
|
||||
</div>
|
||||
|
||||
<div class="settings-menu-header">
|
||||
<h4>Quick Stats</h4>
|
||||
</div>
|
||||
<div class="settings-menu-content">
|
||||
<ul>
|
||||
<li>
|
||||
Total Subscribers:
|
||||
<span id="total-subscribers">{{total}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if subscriberToDelete}}
|
||||
{{gh-fullscreen-modal "delete-subscriber"
|
||||
model=subscriberToDelete
|
||||
confirm=(action "confirmDeleteSubscriber")
|
||||
close=(action "cancelDeleteSubscriber")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
3
core/client/app/templates/subscribers/import.hbs
Normal file
3
core/client/app/templates/subscribers/import.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
{{gh-fullscreen-modal "import-subscribers"
|
||||
confirm=(route-action "reset")
|
||||
close=(route-action "cancel")}}
|
4
core/client/app/templates/subscribers/new.hbs
Normal file
4
core/client/app/templates/subscribers/new.hbs
Normal file
@ -0,0 +1,4 @@
|
||||
{{gh-fullscreen-modal "new-subscriber"
|
||||
model=model
|
||||
confirm=(route-action "save")
|
||||
close=(route-action "cancel")}}
|
@ -8,4 +8,9 @@ export default function () {
|
||||
this.use('tether', ['fade', {duration: 150}], ['fade', {duration: 150}]),
|
||||
this.reverse('tether', ['fade', {duration: 80}], ['fade', {duration: 150}])
|
||||
);
|
||||
|
||||
this.transition(
|
||||
this.hasClass('fade-transition'),
|
||||
this.use('crossFade', {duration: 100})
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ import Ember from 'ember';
|
||||
|
||||
const {isArray} = Ember;
|
||||
|
||||
// TODO: this should be removed and instead have our app serializer properly
|
||||
// process the response so that errors can be tied to the model
|
||||
|
||||
// Used in API request fail handlers to parse a standard api error
|
||||
// response json for the message to display
|
||||
export default function getRequestErrorMessage(request, performConcat) {
|
||||
@ -20,12 +23,12 @@ export default function getRequestErrorMessage(request, performConcat) {
|
||||
if (request.status !== 200) {
|
||||
try {
|
||||
// Try to parse out the error, or default to 'Unknown'
|
||||
if (request.responseJSON.errors && isArray(request.responseJSON.errors)) {
|
||||
message = request.responseJSON.errors.map((errorItem) => {
|
||||
if (request.errors && isArray(request.errors)) {
|
||||
message = request.errors.map((errorItem) => {
|
||||
return errorItem.message;
|
||||
});
|
||||
} else {
|
||||
message = request.responseJSON.error || 'Unknown Error';
|
||||
message = request.error || 'Unknown Error';
|
||||
}
|
||||
} catch (e) {
|
||||
msgDetail = request.status ? `${request.status} - ${request.statusText}` : 'Server was not available';
|
||||
|
19
core/client/app/validators/subscriber.js
Normal file
19
core/client/app/validators/subscriber.js
Normal file
@ -0,0 +1,19 @@
|
||||
import BaseValidator from './base';
|
||||
|
||||
export default BaseValidator.create({
|
||||
properties: ['email'],
|
||||
|
||||
email(model) {
|
||||
let email = model.get('email');
|
||||
|
||||
if (validator.empty(email)) {
|
||||
model.get('errors').add('email', 'Please enter an email.');
|
||||
model.get('hasValidated').pushObject('email');
|
||||
this.invalidate();
|
||||
} else if (!validator.isEmail(email)) {
|
||||
model.get('errors').add('email', 'Invalid email.');
|
||||
model.get('hasValidated').pushObject('email');
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
@ -42,6 +42,7 @@
|
||||
"ember-data-filter": "1.13.0",
|
||||
"ember-export-application-global": "1.0.5",
|
||||
"ember-invoke-action": "1.3.0",
|
||||
"ember-light-table": "0.1.9",
|
||||
"ember-load-initializers": "0.5.1",
|
||||
"ember-myth": "0.1.1",
|
||||
"ember-one-way-controls": "0.6.2",
|
||||
|
256
core/client/tests/acceptance/subscribers-test.js
Normal file
256
core/client/tests/acceptance/subscribers-test.js
Normal file
@ -0,0 +1,256 @@
|
||||
/* jshint expr:true */
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import startApp from '../helpers/start-app';
|
||||
import destroyApp from '../helpers/destroy-app';
|
||||
import { invalidateSession, authenticateSession } from 'ghost/tests/helpers/ember-simple-auth';
|
||||
|
||||
describe('Acceptance: Subscribers', function() {
|
||||
let application;
|
||||
|
||||
beforeEach(function() {
|
||||
application = startApp();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
destroyApp(application);
|
||||
});
|
||||
|
||||
it('redirects to signin when not authenticated', function () {
|
||||
invalidateSession(application);
|
||||
visit('/subscribers');
|
||||
|
||||
andThen(function () {
|
||||
expect(currentURL()).to.equal('/signin');
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects editors to posts', function () {
|
||||
let role = server.create('role', {name: 'Editor'});
|
||||
let user = server.create('user', {roles: [role]});
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/subscribers');
|
||||
|
||||
andThen(function () {
|
||||
expect(currentURL()).to.equal('/');
|
||||
expect(find('.gh-nav-main a:contains("Subscribers")').length, 'sidebar link is visible')
|
||||
.to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects authors to posts', function () {
|
||||
let role = server.create('role', {name: 'Author'});
|
||||
let user = server.create('user', {roles: [role]});
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/subscribers');
|
||||
|
||||
andThen(function () {
|
||||
expect(currentURL()).to.equal('/');
|
||||
expect(find('.gh-nav-main a:contains("Subscribers")').length, 'sidebar link is visible')
|
||||
.to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('an admin', function () {
|
||||
beforeEach(function () {
|
||||
let role = server.create('role', {name: 'Administrator'});
|
||||
let user = server.create('user', {roles: [role]});
|
||||
|
||||
server.loadFixtures();
|
||||
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
||||
it('can manage subscribers', function () {
|
||||
server.createList('subscriber', 40);
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/');
|
||||
click('.gh-nav-main a:contains("Subscribers")');
|
||||
|
||||
andThen(function() {
|
||||
// it navigates to the correct page
|
||||
expect(currentPath()).to.equal('subscribers.index');
|
||||
|
||||
// it has correct page title
|
||||
expect(document.title, 'page title')
|
||||
.to.equal('Subscribers - Test Blog');
|
||||
|
||||
// it loads the first page
|
||||
expect(find('.subscribers-table .lt-body .lt-row').length, 'number of subscriber rows')
|
||||
.to.equal(30);
|
||||
|
||||
// it shows the total number of subscribers
|
||||
expect(find('#total-subscribers').text().trim(), 'displayed subscribers total')
|
||||
.to.equal('40');
|
||||
|
||||
// it defaults to sorting by created_at desc
|
||||
let [lastRequest] = server.pretender.handledRequests.slice(-1);
|
||||
expect(lastRequest.queryParams.order).to.equal('created_at desc');
|
||||
|
||||
let createdAtHeader = find('.subscribers-table th:contains("Subscription Date")');
|
||||
expect(createdAtHeader.hasClass('is-sorted'), 'createdAt column is sorted')
|
||||
.to.be.true;
|
||||
expect(createdAtHeader.find('.icon-descending').length, 'createdAt column has descending icon')
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
// click the column to re-order
|
||||
click('th:contains("Subscription Date")');
|
||||
|
||||
andThen(function () {
|
||||
// it flips the directions and re-fetches
|
||||
let [lastRequest] = server.pretender.handledRequests.slice(-1);
|
||||
expect(lastRequest.queryParams.order).to.equal('created_at asc');
|
||||
|
||||
let createdAtHeader = find('.subscribers-table th:contains("Subscription Date")');
|
||||
expect(createdAtHeader.find('.icon-ascending').length, 'createdAt column has ascending icon')
|
||||
.to.equal(1);
|
||||
|
||||
// scroll to the bottom of the table to simulate infinite scroll
|
||||
find('.subscribers-table').scrollTop(find('.subscribers-table .ember-light-table').height());
|
||||
});
|
||||
|
||||
// trigger infinite scroll
|
||||
triggerEvent('.subscribers-table', 'scroll');
|
||||
|
||||
andThen(function () {
|
||||
// it loads the next page
|
||||
expect(find('.subscribers-table .lt-body .lt-row').length, 'number of subscriber rows after infinite-scroll')
|
||||
.to.equal(40);
|
||||
});
|
||||
|
||||
// click the add subscriber button
|
||||
click('.btn:contains("Add Subscriber")');
|
||||
|
||||
andThen(function () {
|
||||
// it displays the add subscriber modal
|
||||
expect(find('.fullscreen-modal').length, 'add subscriber modal displayed')
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
// cancel the modal
|
||||
click('.fullscreen-modal .btn:contains("Cancel")');
|
||||
|
||||
andThen(function () {
|
||||
// it closes the add subscriber modal
|
||||
expect(find('.fullscreen-modal').length, 'add subscriber modal displayed after cancel')
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
// save a new subscriber
|
||||
click('.btn:contains("Add Subscriber")');
|
||||
fillIn('.fullscreen-modal input[name="email"]', 'test@example.com');
|
||||
click('.fullscreen-modal .btn:contains("Add")');
|
||||
|
||||
andThen(function () {
|
||||
// the add subscriber modal is closed
|
||||
expect(find('.fullscreen-modal').length, 'add subscriber modal displayed after save')
|
||||
.to.equal(0);
|
||||
|
||||
// the subscriber is added to the table
|
||||
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type').text().trim(), 'first email in list after addition')
|
||||
.to.equal('test@example.com');
|
||||
|
||||
// the table is scrolled to the top
|
||||
// TODO: implement scroll to new record after addition
|
||||
// expect(find('.subscribers-table').scrollTop(), 'scroll position after addition')
|
||||
// .to.equal(0);
|
||||
|
||||
// the subscriber total is updated
|
||||
expect(find('#total-subscribers').text().trim(), 'subscribers total after addition')
|
||||
.to.equal('41');
|
||||
});
|
||||
|
||||
// saving a duplicate subscriber
|
||||
click('.btn:contains("Add Subscriber")');
|
||||
fillIn('.fullscreen-modal input[name="email"]', 'test@example.com');
|
||||
click('.fullscreen-modal .btn:contains("Add")');
|
||||
|
||||
andThen(function () {
|
||||
// the validation error is displayed
|
||||
expect(find('.fullscreen-modal .error .response').text().trim(), 'duplicate email validation')
|
||||
.to.match(/duplicate/);
|
||||
|
||||
// the subscriber is not added to the table
|
||||
expect(find('.lt-cell:contains(test@example.com)').length, 'number of "test@example.com rows"')
|
||||
.to.equal(1);
|
||||
|
||||
// the subscriber total is unchanged
|
||||
expect(find('#total-subscribers').text().trim(), 'subscribers total after failed add')
|
||||
.to.equal('41');
|
||||
});
|
||||
|
||||
// deleting a subscriber
|
||||
click('.fullscreen-modal .btn:contains("Cancel")');
|
||||
click('.subscribers-table tbody tr:first-of-type button:last-of-type');
|
||||
|
||||
andThen(function () {
|
||||
// it displays the delete subscriber modal
|
||||
expect(find('.fullscreen-modal').length, 'delete subscriber modal displayed')
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
// cancel the modal
|
||||
click('.fullscreen-modal .btn:contains("Cancel")');
|
||||
|
||||
andThen(function () {
|
||||
// return pauseTest();
|
||||
// it closes the add subscriber modal
|
||||
expect(find('.fullscreen-modal').length, 'delete subscriber modal displayed after cancel')
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
click('.subscribers-table tbody tr:first-of-type button:last-of-type');
|
||||
click('.fullscreen-modal .btn:contains("Delete")');
|
||||
|
||||
andThen(function () {
|
||||
// the add subscriber modal is closed
|
||||
expect(find('.fullscreen-modal').length, 'delete subscriber modal displayed after confirm')
|
||||
.to.equal(0);
|
||||
|
||||
// the subscriber is removed from the table
|
||||
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type').text().trim(), 'first email in list after addition')
|
||||
.to.not.equal('test@example.com');
|
||||
|
||||
// the subscriber total is updated
|
||||
expect(find('#total-subscribers').text().trim(), 'subscribers total after addition')
|
||||
.to.equal('40');
|
||||
});
|
||||
|
||||
// click the import subscribers button
|
||||
click('.btn:contains("Import CSV")');
|
||||
|
||||
andThen(function () {
|
||||
// it displays the import subscribers modal
|
||||
expect(find('.fullscreen-modal').length, 'import subscribers modal displayed')
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
// cancel the modal
|
||||
click('.fullscreen-modal .btn:contains("Cancel")');
|
||||
|
||||
andThen(function () {
|
||||
// it closes the import subscribers modal
|
||||
expect(find('.fullscreen-modal').length, 'import subscribers modal displayed after cancel')
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
// TODO: how to simulate file upload?
|
||||
|
||||
// re-open import modal
|
||||
// upload a file
|
||||
// modal title changes
|
||||
// modal button changes
|
||||
// table is reset
|
||||
// close modal
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,93 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Ember from 'ember';
|
||||
import Pretender from 'pretender';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
|
||||
const {run} = Ember;
|
||||
|
||||
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"'];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||
errors: [{
|
||||
errorType: error,
|
||||
message: `Error: ${error}`
|
||||
}]
|
||||
})];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
describeComponent(
|
||||
'gh-file-uploader',
|
||||
'Integration: Component: gh-file-uploader',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function() {
|
||||
let server;
|
||||
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
this.render(hbs`{{gh-file-uploader}}`);
|
||||
|
||||
expect(this.$('label').text().trim(), 'default label')
|
||||
.to.equal('Select or drag-and-drop a file');
|
||||
});
|
||||
|
||||
it('renders form with supplied label text', function () {
|
||||
this.set('labelText', 'My label');
|
||||
this.render(hbs`{{gh-file-uploader labelText=labelText}}`);
|
||||
|
||||
expect(this.$('label').text().trim(), 'label')
|
||||
.to.equal('My label');
|
||||
});
|
||||
|
||||
it('generates request to supplied endpoint', function (done) {
|
||||
stubSuccessfulUpload(server);
|
||||
this.set('uploadUrl', '/ghost/api/v0.1/uploads/');
|
||||
|
||||
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
|
||||
this.$('input[type="file"]').trigger('change');
|
||||
|
||||
wait().then(() => {
|
||||
expect(server.handledRequests.length).to.equal(1);
|
||||
expect(server.handledRequests[0].url).to.equal('/ghost/api/v0.1/uploads/');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles drag over/leave', function () {
|
||||
this.render(hbs`{{gh-file-uploader}}`);
|
||||
|
||||
run(() => {
|
||||
this.$('.gh-image-uploader').trigger('dragover');
|
||||
});
|
||||
|
||||
expect(this.$('.gh-image-uploader').hasClass('--drag-over'), 'has drag-over class').to.be.true;
|
||||
|
||||
run(() => {
|
||||
this.$('.gh-image-uploader').trigger('dragleave');
|
||||
});
|
||||
|
||||
expect(this.$('.gh-image-uploader').hasClass('--drag-over'), 'has drag-over class').to.be.false;
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,26 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Table from 'ember-light-table';
|
||||
|
||||
describeComponent(
|
||||
'gh-subscribers-table',
|
||||
'Integration: Component: gh-subscribers-table',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function() {
|
||||
it('renders', function() {
|
||||
this.set('table', new Table([], []));
|
||||
this.set('sortByColumn', function () {});
|
||||
this.set('delete', function () {});
|
||||
|
||||
this.render(hbs`{{gh-subscribers-table table=table sortByColumn=(action sortByColumn) delete=(action delete)}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describeComponent(
|
||||
'modals/delete-subscriber',
|
||||
'Integration: Component: modals/delete-subscriber',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function() {
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#modals/delete-subscriber}}
|
||||
// template content
|
||||
// {{/modals/delete-subscriber}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{modals/delete-subscriber}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describeComponent(
|
||||
'modals/import-subscribers',
|
||||
'Integration: Component: modals/import-subscribers',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function() {
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#modals/import-subscribers}}
|
||||
// template content
|
||||
// {{/modals/import-subscribers}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{modals/import-subscribers}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
}
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describeComponent(
|
||||
'modals/new-subscriber',
|
||||
'Integration: Component: modals/new-subscriber',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function() {
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#modals/new-subscriber}}
|
||||
// template content
|
||||
// {{/modals/new-subscriber}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{modals/new-subscriber}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
}
|
||||
);
|
313
core/client/tests/unit/components/gh-file-uploader-test.js
Normal file
313
core/client/tests/unit/components/gh-file-uploader-test.js
Normal file
@ -0,0 +1,313 @@
|
||||
/* jshint expr:true */
|
||||
/* global Blob */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import Ember from 'ember';
|
||||
import sinon from 'sinon';
|
||||
import Pretender from 'pretender';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
|
||||
const {run} = Ember;
|
||||
|
||||
const createFile = function (content = ['test'], options = {}) {
|
||||
let {
|
||||
name,
|
||||
type,
|
||||
lastModifiedDate
|
||||
} = options;
|
||||
|
||||
let file = new Blob(content, {type: type ? type : 'text/plain'});
|
||||
file.name = name ? name : 'text.txt';
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"'];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||
errors: [{
|
||||
errorType: error,
|
||||
message: `Error: ${error}`
|
||||
}]
|
||||
})];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
describeComponent(
|
||||
'gh-file-uploader',
|
||||
'Unit: Component: gh-file-uploader',
|
||||
{
|
||||
needs: [
|
||||
'service:ajax',
|
||||
'service:session', // used by ajax service
|
||||
'service:feature',
|
||||
'component:x-file-input'
|
||||
],
|
||||
unit: true
|
||||
},
|
||||
function() {
|
||||
let server, url;
|
||||
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
url = '/ghost/api/v0.1/uploads/';
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
// creates the component instance
|
||||
let component = this.subject();
|
||||
// renders the component on the page
|
||||
this.render();
|
||||
expect(component).to.be.ok;
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
|
||||
it('fires uploadSuccess action on successful upload', function (done) {
|
||||
let uploadSuccess = sinon.spy();
|
||||
let component = this.subject({url, uploadSuccess});
|
||||
let file = createFile();
|
||||
|
||||
stubSuccessfulUpload(server);
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(uploadSuccess.calledOnce).to.be.true;
|
||||
expect(uploadSuccess.firstCall.args[0]).to.equal('/content/images/test.png');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fires uploadStarted action on upload start', function (done) {
|
||||
let uploadStarted = sinon.spy();
|
||||
let component = this.subject({url, uploadStarted});
|
||||
let file = createFile();
|
||||
|
||||
stubSuccessfulUpload(server);
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(uploadStarted.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fires uploadFinished action on successful upload', function (done) {
|
||||
let uploadFinished = sinon.spy();
|
||||
let component = this.subject({url, uploadFinished});
|
||||
let file = createFile();
|
||||
|
||||
stubSuccessfulUpload(server);
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(uploadFinished.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fires uploadFinished action on failed upload', function (done) {
|
||||
let uploadFinished = sinon.spy();
|
||||
let component = this.subject({url, uploadFinished});
|
||||
let file = createFile();
|
||||
|
||||
stubFailedUpload(server);
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(uploadFinished.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays invalid file type error', function (done) {
|
||||
let component = this.subject({url});
|
||||
let file = createFile();
|
||||
|
||||
stubFailedUpload(server, 415, 'UnsupportedMediaTypeError');
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||
expect(this.$('.failed').text()).to.match(/The file type you uploaded is not supported/);
|
||||
expect(this.$('.btn-green').length, 'reset button is displayed').to.equal(1);
|
||||
expect(this.$('.btn-green').text()).to.equal('Try Again');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays file too large for server error', function (done) {
|
||||
let component = this.subject({url});
|
||||
let file = createFile();
|
||||
|
||||
stubFailedUpload(server, 413, 'RequestEntityTooLargeError');
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||
expect(this.$('.failed').text()).to.match(/The file you uploaded was larger/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles file too large error directly from the web server', function (done) {
|
||||
let component = this.subject({url});
|
||||
let file = createFile();
|
||||
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
return [413, {}, ''];
|
||||
});
|
||||
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||
expect(this.$('.failed').text()).to.match(/The file you uploaded was larger/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays other server-side error with message', function (done) {
|
||||
let component = this.subject({url});
|
||||
let file = createFile();
|
||||
|
||||
stubFailedUpload(server, 400, 'UnknownError');
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||
expect(this.$('.failed').text()).to.match(/Error: UnknownError/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles unknown failure', function (done) {
|
||||
let component = this.subject({url});
|
||||
let file = createFile();
|
||||
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
return [500, {'Content-Type': 'application/json'}, ''];
|
||||
});
|
||||
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||
expect(this.$('.failed').text()).to.match(/Something went wrong/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can be reset after a failed upload', function (done) {
|
||||
let component = this.subject({url});
|
||||
let file = createFile();
|
||||
|
||||
stubFailedUpload(server, 400, 'UnknownError');
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
run(() => {
|
||||
this.$('.btn-green').click();
|
||||
});
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(this.$('input[type="file"]').length).to.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays upload progress', function (done) {
|
||||
let component = this.subject({url, uploadFinished: done});
|
||||
let file = createFile();
|
||||
|
||||
// pretender fires a progress event every 50ms
|
||||
stubSuccessfulUpload(server, 150);
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
component.send('fileSelected', [file]);
|
||||
});
|
||||
|
||||
// after 75ms we should have had one progress event
|
||||
run.later(this, function () {
|
||||
expect(this.$('.progress .bar').length).to.equal(1);
|
||||
let [_, percentageWidth] = this.$('.progress .bar').attr('style').match(/width: (\d+)%?/);
|
||||
expect(percentageWidth).to.be.above(0);
|
||||
expect(percentageWidth).to.be.below(100);
|
||||
}, 75);
|
||||
});
|
||||
|
||||
it('triggers file upload on file drop', function (done) {
|
||||
let uploadSuccess = sinon.spy();
|
||||
let component = this.subject({url, uploadSuccess});
|
||||
let file = createFile();
|
||||
let drop = Ember.$.Event('drop', {
|
||||
dataTransfer: {
|
||||
files: [file]
|
||||
}
|
||||
});
|
||||
|
||||
stubSuccessfulUpload(server);
|
||||
this.render();
|
||||
|
||||
run(() => {
|
||||
this.$().trigger(drop);
|
||||
});
|
||||
|
||||
wait().then(() => {
|
||||
expect(uploadSuccess.calledOnce).to.be.true;
|
||||
expect(uploadSuccess.firstCall.args[0]).to.equal('/content/images/test.png');
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
21
core/client/tests/unit/controllers/subscribers-test.js
Normal file
21
core/client/tests/unit/controllers/subscribers-test.js
Normal file
@ -0,0 +1,21 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeModule(
|
||||
'controller:subscribers',
|
||||
'Unit: Controller: subscribers',
|
||||
{
|
||||
needs: ['service:notifications']
|
||||
},
|
||||
function() {
|
||||
// Replace this with your real tests.
|
||||
it('exists', function() {
|
||||
let controller = this.subject();
|
||||
expect(controller).to.be.ok;
|
||||
});
|
||||
}
|
||||
);
|
20
core/client/tests/unit/models/subscriber-test.js
Normal file
20
core/client/tests/unit/models/subscriber-test.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import { describeModel, it } from 'ember-mocha';
|
||||
|
||||
describeModel(
|
||||
'subscriber',
|
||||
'Unit: Model: subscriber',
|
||||
{
|
||||
// Specify the other units that are required for this test.
|
||||
needs: ['model:post']
|
||||
},
|
||||
function() {
|
||||
// Replace this with your real tests.
|
||||
it('exists', function() {
|
||||
let model = this.subject();
|
||||
// var store = this.store();
|
||||
expect(model).to.be.ok;
|
||||
});
|
||||
}
|
||||
);
|
20
core/client/tests/unit/routes/subscribers-test.js
Normal file
20
core/client/tests/unit/routes/subscribers-test.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeModule(
|
||||
'route:subscribers',
|
||||
'Unit: Route: subscribers',
|
||||
{
|
||||
needs: ['service:notifications']
|
||||
},
|
||||
function() {
|
||||
it('exists', function() {
|
||||
let route = this.subject();
|
||||
expect(route).to.be.ok;
|
||||
});
|
||||
}
|
||||
);
|
21
core/client/tests/unit/routes/subscribers/import-test.js
Normal file
21
core/client/tests/unit/routes/subscribers/import-test.js
Normal file
@ -0,0 +1,21 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeModule(
|
||||
'route:subscribers/import',
|
||||
'SubscribersImportRoute',
|
||||
{
|
||||
// Specify the other units that are required for this test.
|
||||
needs: ['service:notifications']
|
||||
},
|
||||
function() {
|
||||
it('exists', function() {
|
||||
let route = this.subject();
|
||||
expect(route).to.be.ok;
|
||||
});
|
||||
}
|
||||
);
|
20
core/client/tests/unit/routes/subscribers/new-test.js
Normal file
20
core/client/tests/unit/routes/subscribers/new-test.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeModule(
|
||||
'route:subscribers/new',
|
||||
'Unit: Route: subscribers/new',
|
||||
{
|
||||
needs: ['service:notifications']
|
||||
},
|
||||
function() {
|
||||
it('exists', function() {
|
||||
let route = this.subject();
|
||||
expect(route).to.be.ok;
|
||||
});
|
||||
}
|
||||
);
|
77
core/client/tests/unit/validators/subscriber-test.js
Normal file
77
core/client/tests/unit/validators/subscriber-test.js
Normal file
@ -0,0 +1,77 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describe,
|
||||
it
|
||||
} from 'mocha';
|
||||
import Ember from 'ember';
|
||||
import ValidationEngine from 'ghost/mixins/validation-engine';
|
||||
|
||||
const {run} = Ember;
|
||||
|
||||
const Subscriber = Ember.Object.extend(ValidationEngine, {
|
||||
validationType: 'subscriber',
|
||||
|
||||
email: null
|
||||
});
|
||||
|
||||
describe('Unit: Validator: subscriber', function () {
|
||||
it('validates email by default', function () {
|
||||
let subscriber = Subscriber.create({});
|
||||
let properties = subscriber.get('validators.subscriber.properties');
|
||||
|
||||
console.log(subscriber);
|
||||
|
||||
expect(properties, 'properties').to.include('email');
|
||||
});
|
||||
|
||||
it('passes with a valid email', function () {
|
||||
let subscriber = Subscriber.create({email: 'test@example.com'});
|
||||
let passed = false;
|
||||
|
||||
run(() => {
|
||||
subscriber.validate({property: 'email'}).then(() => {
|
||||
passed = true;
|
||||
});
|
||||
});
|
||||
|
||||
expect(passed, 'passed').to.be.true;
|
||||
expect(subscriber.get('hasValidated'), 'hasValidated').to.include('email');
|
||||
});
|
||||
|
||||
it('validates email presence', function () {
|
||||
let subscriber = Subscriber.create({});
|
||||
let passed = false;
|
||||
|
||||
run(() => {
|
||||
subscriber.validate({property: 'email'}).then(() => {
|
||||
passed = true;
|
||||
});
|
||||
});
|
||||
|
||||
let emailErrors = subscriber.get('errors').errorsFor('email').get(0);
|
||||
expect(emailErrors.attribute, 'errors.email.attribute').to.equal('email');
|
||||
expect(emailErrors.message, 'errors.email.message').to.equal('Please enter an email.');
|
||||
|
||||
expect(passed, 'passed').to.be.false;
|
||||
expect(subscriber.get('hasValidated'), 'hasValidated').to.include('email');
|
||||
});
|
||||
|
||||
it('validates email', function () {
|
||||
let subscriber = Subscriber.create({email: 'foo'});
|
||||
let passed = false;
|
||||
|
||||
run(() => {
|
||||
subscriber.validate({property: 'email'}).then(() => {
|
||||
passed = true;
|
||||
});
|
||||
});
|
||||
|
||||
let emailErrors = subscriber.get('errors').errorsFor('email').get(0);
|
||||
expect(emailErrors.attribute, 'errors.email.attribute').to.equal('email');
|
||||
expect(emailErrors.message, 'errors.email.message').to.equal('Invalid email.');
|
||||
|
||||
expect(passed, 'passed').to.be.false;
|
||||
expect(subscriber.get('hasValidated'), 'hasValidated').to.include('email');
|
||||
});
|
||||
});
|
@ -1,4 +1,3 @@
|
||||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
|
@ -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
|
||||
|
335
core/server/api/subscribers.js
Normal file
335
core/server/api/subscribers.js
Normal file
@ -0,0 +1,335 @@
|
||||
// # Tag API
|
||||
// RESTful API for the Tag resource
|
||||
var Promise = require('bluebird'),
|
||||
_ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
dataProvider = require('../models'),
|
||||
errors = require('../errors'),
|
||||
utils = require('./utils'),
|
||||
serverUtils = 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}),
|
||||
utils.handlePermissions(docName, 'browse'),
|
||||
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}),
|
||||
utils.handlePermissions(docName, 'read'),
|
||||
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.subscribers.subscriberNotFound')));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* ## Add
|
||||
* @param {Subscriber} object the subscriber to create
|
||||
* @returns {Promise(Subscriber)} Newly created Subscriber
|
||||
*/
|
||||
add: function add(object, options) {
|
||||
var tasks;
|
||||
|
||||
/**
|
||||
* ### Model Query
|
||||
* Make the call to the Model layer
|
||||
* @param {Object} options
|
||||
* @returns {Object} options
|
||||
*/
|
||||
function doQuery(options) {
|
||||
return dataProvider.Subscriber.getByEmail(options.data.subscribers[0].email)
|
||||
.then(function (subscriber) {
|
||||
if (subscriber && options.context.external) {
|
||||
// we don't expose this information
|
||||
return Promise.resolve(subscriber);
|
||||
} else if (subscriber) {
|
||||
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.subscribers.subscriberAlreadyExist')));
|
||||
}
|
||||
|
||||
return dataProvider.Subscriber.add(options.data.subscribers[0], _.omit(options, ['data'])).catch(function (error) {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.subscribers.subscriberAlreadyExist')));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
utils.validate(docName),
|
||||
utils.handlePermissions(docName, 'add'),
|
||||
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}),
|
||||
utils.handlePermissions(docName, 'edit'),
|
||||
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.subscribers.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}),
|
||||
utils.handlePermissions(docName, 'destroy'),
|
||||
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 = [
|
||||
utils.handlePermissions(docName, 'browse'),
|
||||
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) {
|
||||
var filePath = options.path,
|
||||
fulfilled = 0,
|
||||
invalid = 0,
|
||||
duplicates = 0;
|
||||
|
||||
return serverUtils.readCSV({
|
||||
path: filePath,
|
||||
columnsToExtract: ['email']
|
||||
}).then(function (result) {
|
||||
return Promise.all(result.map(function (entry) {
|
||||
return subscribers.add(
|
||||
{subscribers: [{email: entry.email}]},
|
||||
{context: options.context}
|
||||
).reflect();
|
||||
})).each(function (inspection) {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
} else {
|
||||
if (inspection.reason() instanceof errors.ValidationError) {
|
||||
duplicates = duplicates + 1;
|
||||
} else {
|
||||
invalid = invalid + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}).then(function () {
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: fulfilled,
|
||||
duplicates: duplicates,
|
||||
invalid: invalid
|
||||
}
|
||||
}
|
||||
};
|
||||
}).finally(function () {
|
||||
// Remove uploaded file from tmp location
|
||||
return Promise.promisify(fs.unlink)(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
tasks = [
|
||||
validate,
|
||||
utils.handlePermissions(docName, 'add'),
|
||||
importCSV
|
||||
];
|
||||
|
||||
return pipeline(tasks, options);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = subscribers;
|
81
core/server/apps/subscribers/index.js
Normal file
81
core/server/apps/subscribers/index.js
Normal file
@ -0,0 +1,81 @@
|
||||
var _ = require('lodash'),
|
||||
path = require('path'),
|
||||
hbs = require('express-hbs'),
|
||||
router = require('./lib/router'),
|
||||
|
||||
// Dirty requires
|
||||
config = require('../../config'),
|
||||
errors = require('../../errors'),
|
||||
i18n = require('../../i18n'),
|
||||
labs = require('../../utils/labs'),
|
||||
template = require('../../helpers/template'),
|
||||
utils = require('../../helpers/utils'),
|
||||
|
||||
params = ['error', 'success', 'email'],
|
||||
|
||||
/**
|
||||
* This helper script sets the referrer and current location if not existent.
|
||||
*
|
||||
* document.querySelector['.location']['value'] = document.querySelector('.location')['value'] || window.location.href;
|
||||
*/
|
||||
subscribeScript =
|
||||
'<script type="text/javascript">' +
|
||||
'(function(g,h,o,s,t){' +
|
||||
'h[o](\'.location\')[s]=h[o](\'.location\')[s] || g.location.href;' +
|
||||
'h[o](\'.referrer\')[s]=h[o](\'.referrer\')[s] || h.referrer;' +
|
||||
'})(window,document,\'querySelector\',\'value\');' +
|
||||
'</script>';
|
||||
|
||||
function makeHidden(name, extras) {
|
||||
return utils.inputTemplate({
|
||||
type: 'hidden',
|
||||
name: name,
|
||||
className: name,
|
||||
extras: extras
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeFormHelper(options) {
|
||||
var root = options.data.root,
|
||||
data = _.merge({}, options.hash, _.pick(root, params), {
|
||||
action: path.join('/', config.paths.subdir, config.routeKeywords.subscribe, '/'),
|
||||
script: new hbs.handlebars.SafeString(subscribeScript),
|
||||
hidden: new hbs.handlebars.SafeString(
|
||||
makeHidden('confirm') +
|
||||
makeHidden('location', root.subscribed_url ? 'value=' + root.subscribed_url : '') +
|
||||
makeHidden('referrer', root.subscribed_referrer ? 'value=' + root.subscribed_referrer : '')
|
||||
)
|
||||
});
|
||||
|
||||
return template.execute('subscribe_form', data, options);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
activate: function activate(ghost) {
|
||||
var errorMessages = [
|
||||
i18n.t('warnings.helpers.helperNotAvailable', {helperName: 'subscribe_form'}),
|
||||
i18n.t('warnings.helpers.apiMustBeEnabled', {helperName: 'subscribe_form', flagName: 'subscribers'}),
|
||||
i18n.t('warnings.helpers.seeLink', {url: 'http://support.ghost.org/subscribers-beta/'})
|
||||
];
|
||||
|
||||
// Correct way to register a helper from an app
|
||||
ghost.helpers.register('subscribe_form', function labsEnabledHelper() {
|
||||
if (labs.isSet('subscribers') === true) {
|
||||
return subscribeFormHelper.apply(this, arguments);
|
||||
}
|
||||
|
||||
errors.logError.apply(this, errorMessages);
|
||||
return new hbs.handlebars.SafeString('<script>console.error("' + errorMessages.join(' ') + '");</script>');
|
||||
});
|
||||
},
|
||||
|
||||
setupRoutes: function setupRoutes(blogRouter) {
|
||||
blogRouter.use('/' + config.routeKeywords.subscribe + '/', function labsEnabledRouter(req, res, next) {
|
||||
if (labs.isSet('subscribers') === true) {
|
||||
return router.apply(this, arguments);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
};
|
105
core/server/apps/subscribers/lib/router.js
Normal file
105
core/server/apps/subscribers/lib/router.js
Normal file
@ -0,0 +1,105 @@
|
||||
var path = require('path'),
|
||||
express = require('express'),
|
||||
_ = require('lodash'),
|
||||
subscribeRouter = express.Router(),
|
||||
|
||||
// Dirty requires
|
||||
api = require('../../../api'),
|
||||
errors = require('../../../errors'),
|
||||
templates = require('../../../controllers/frontend/templates'),
|
||||
postlookup = require('../../../controllers/frontend/post-lookup'),
|
||||
setResponseContext = require('../../../controllers/frontend/context');
|
||||
|
||||
function controller(req, res) {
|
||||
var defaultView = path.resolve(__dirname, 'views', 'subscribe.hbs'),
|
||||
paths = templates.getActiveThemePaths(req.app.get('activeTheme')),
|
||||
data = req.body;
|
||||
|
||||
setResponseContext(req, res);
|
||||
if (paths.hasOwnProperty('subscribe.hbs')) {
|
||||
return res.render('subscribe', data);
|
||||
} else {
|
||||
return res.render(defaultView, data);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error, req, res, next) {
|
||||
/*jshint unused:false */
|
||||
|
||||
if (error.statusCode !== 404) {
|
||||
res.locals.error = error;
|
||||
return controller(req, res);
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
|
||||
function honeyPot(req, res, next) {
|
||||
if (!req.body.hasOwnProperty('confirm') || req.body.confirm !== '') {
|
||||
return next(new Error('Oops, something went wrong!'));
|
||||
}
|
||||
|
||||
// we don't need this anymore
|
||||
delete req.body.confirm;
|
||||
next();
|
||||
}
|
||||
|
||||
function handleSource(req, res, next) {
|
||||
req.body.subscribed_url = req.body.location;
|
||||
req.body.subscribed_referrer = req.body.referrer;
|
||||
delete req.body.location;
|
||||
delete req.body.referrer;
|
||||
|
||||
postlookup(req.body.subscribed_url)
|
||||
.then(function (result) {
|
||||
if (result && result.post) {
|
||||
req.body.post_id = result.post.id;
|
||||
}
|
||||
|
||||
next();
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err instanceof errors.NotFoundError) {
|
||||
return next();
|
||||
}
|
||||
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
|
||||
function storeSubscriber(req, res, next) {
|
||||
req.body.status = 'subscribed';
|
||||
|
||||
if (_.isEmpty(req.body.email)) {
|
||||
return next(new errors.ValidationError('Email cannot be blank.'));
|
||||
}
|
||||
|
||||
return api.subscribers.add({subscribers: [req.body]}, {context: {external: true}})
|
||||
.then(function () {
|
||||
res.locals.success = true;
|
||||
next();
|
||||
})
|
||||
.catch(function () {
|
||||
// we do not expose any information
|
||||
res.locals.success = true;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// subscribe frontend route
|
||||
subscribeRouter.route('/')
|
||||
.get(
|
||||
controller
|
||||
)
|
||||
.post(
|
||||
honeyPot,
|
||||
handleSource,
|
||||
storeSubscriber,
|
||||
controller
|
||||
);
|
||||
|
||||
// configure an error handler just for subscribe problems
|
||||
subscribeRouter.use(errorHandler);
|
||||
|
||||
module.exports = subscribeRouter;
|
||||
module.exports.controller = controller;
|
63
core/server/apps/subscribers/lib/views/subscribe.hbs
Normal file
63
core/server/apps/subscribers/lib/views/subscribe.hbs
Normal file
@ -0,0 +1,63 @@
|
||||
<!doctype html>
|
||||
<!--[if (IE 8)&!(IEMobile)]><html class="no-js lt-ie9" lang="en"><![endif]-->
|
||||
<!--[if (gte IE 9)| IEMobile |!(IE)]><!--><html class="no-js" lang="en"><!--<![endif]-->
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
|
||||
<title>Ghost - Subscribe</title>
|
||||
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
<meta name="MobileOptimized" content="320">
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<link rel="shortcut icon" href="{{asset "favicon.ico"}}">
|
||||
<meta http-equiv="cleartype" content="on">
|
||||
|
||||
<link rel="stylesheet" type='text/css' href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700'>
|
||||
<link rel="stylesheet" href="{{asset "ghost.css" ghost="true" minifyInProduction="true"}}" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="gh-app">
|
||||
<div class="gh-viewport">
|
||||
<main class="gh-main" role="main">
|
||||
<div class="gh-flow">
|
||||
<header class="gh-flow-head">
|
||||
<nav class="gh-flow-nav">
|
||||
<a href="{{@blog.url}}" class="gh-flow-back"><i class="icon-arrow-left"></i> Back</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="gh-flow-content-wrap">
|
||||
<section class="gh-flow-content">
|
||||
{{^if success}}
|
||||
<header>
|
||||
<h1>Subscribe to {{@blog.title}}</h1>
|
||||
</header>
|
||||
|
||||
{{subscribe_form
|
||||
form_class="gh-signin"
|
||||
input_class="gh-input"
|
||||
button_class="btn btn-blue btn-block"
|
||||
placeholder="Your email address"
|
||||
autofocus="true"
|
||||
}}
|
||||
{{else}}
|
||||
<header>
|
||||
<h1>Subscribed!</h1>
|
||||
</header>
|
||||
|
||||
<p>
|
||||
You've successfully subscribed to <em>{{@blog.title}}</em>
|
||||
with the email address <em>{{email}}</em>.
|
||||
</p>
|
||||
{{/if}}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -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
|
||||
|
16
core/server/data/migration/005/05-add-subscribers-table.js
Normal file
16
core/server/data/migration/005/05-add-subscribers-table.js
Normal file
@ -0,0 +1,16 @@
|
||||
var commands = require('../../schema').commands,
|
||||
db = require('../../db'),
|
||||
|
||||
table = 'subscribers',
|
||||
message = 'Creating table: ' + table;
|
||||
|
||||
module.exports = function addSubscribersTable(logger) {
|
||||
return db.knex.schema.hasTable(table).then(function (exists) {
|
||||
if (!exists) {
|
||||
logger.info(message);
|
||||
return commands.createTable(table);
|
||||
} else {
|
||||
logger.warn(message);
|
||||
}
|
||||
});
|
||||
};
|
@ -5,7 +5,8 @@ module.exports = [
|
||||
require('./02-add-visibility-column-to-key-tables'),
|
||||
// Add mobiledoc column to posts
|
||||
require('./03-add-mobiledoc-column-to-posts'),
|
||||
// Add social media columns to isers
|
||||
require('./04-add-social-media-columns-to-users')
|
||||
|
||||
// Add social media columns to users
|
||||
require('./04-add-social-media-columns-to-users'),
|
||||
// Add subscribers table
|
||||
require('./05-add-subscribers-table')
|
||||
];
|
||||
|
@ -1,12 +1,13 @@
|
||||
// Update the permissions & permissions_roles tables to get the new entries
|
||||
var utils = require('../utils');
|
||||
// Update the permissions & permissions_roles tables to add entries for clients
|
||||
var utils = require('../utils'),
|
||||
resource = 'client';
|
||||
|
||||
function getClientPermissions() {
|
||||
return utils.findModelFixtures('Permission', {object_type: 'client'});
|
||||
function getPermissions() {
|
||||
return utils.findModelFixtures('Permission', {object_type: resource});
|
||||
}
|
||||
|
||||
function getClientRelations() {
|
||||
return utils.findPermissionRelationsForObject('client');
|
||||
function getRelations() {
|
||||
return utils.findPermissionRelationsForObject(resource);
|
||||
}
|
||||
|
||||
function printResult(logger, result, message) {
|
||||
@ -18,13 +19,13 @@ function printResult(logger, result, message) {
|
||||
}
|
||||
|
||||
module.exports = function addClientPermissions(options, logger) {
|
||||
var modelToAdd = getClientPermissions(),
|
||||
relationToAdd = getClientRelations();
|
||||
var modelToAdd = getPermissions(),
|
||||
relationToAdd = getRelations();
|
||||
|
||||
return utils.addFixturesForModel(modelToAdd).then(function (result) {
|
||||
printResult(logger, result, 'Adding permissions fixtures for clients');
|
||||
printResult(logger, result, 'Adding permissions fixtures for ' + resource + 's');
|
||||
return utils.addFixturesForRelation(relationToAdd);
|
||||
}).then(function (result) {
|
||||
printResult(logger, result, 'Adding permissions_roles fixtures for clients');
|
||||
printResult(logger, result, 'Adding permissions_roles fixtures for ' + resource + 's');
|
||||
});
|
||||
};
|
||||
|
@ -0,0 +1,31 @@
|
||||
// Update the permissions & permissions_roles tables to add entries for subscribers
|
||||
var utils = require('../utils'),
|
||||
resource = 'subscriber';
|
||||
|
||||
function getPermissions() {
|
||||
return utils.findModelFixtures('Permission', {object_type: resource});
|
||||
}
|
||||
|
||||
function getRelations() {
|
||||
return utils.findPermissionRelationsForObject(resource);
|
||||
}
|
||||
|
||||
function printResult(logger, result, message) {
|
||||
if (result.done === result.expected) {
|
||||
logger.info(message);
|
||||
} else {
|
||||
logger.warn('(' + result.done + '/' + result.expected + ') ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function addSubscriberPermissions(options, logger) {
|
||||
var modelToAdd = getPermissions(),
|
||||
relationToAdd = getRelations();
|
||||
|
||||
return utils.addFixturesForModel(modelToAdd).then(function (result) {
|
||||
printResult(logger, result, 'Adding permissions fixtures for ' + resource + 's');
|
||||
return utils.addFixturesForRelation(relationToAdd);
|
||||
}).then(function (result) {
|
||||
printResult(logger, result, 'Adding permissions_roles fixtures for ' + resource + 's');
|
||||
});
|
||||
};
|
@ -4,5 +4,7 @@ module.exports = [
|
||||
// add ghost-scheduler client
|
||||
require('./02-add-ghost-scheduler-client'),
|
||||
// add client permissions and permission_role relations
|
||||
require('./03-add-client-permissions')
|
||||
require('./03-add-client-permissions'),
|
||||
// add subscriber permissions and permission_role relations
|
||||
require('./04-add-subscriber-permissions')
|
||||
];
|
||||
|
@ -249,6 +249,31 @@
|
||||
"name": "Delete clients",
|
||||
"action_type": "destroy",
|
||||
"object_type": "client"
|
||||
},
|
||||
{
|
||||
"name": "Browse subscribers",
|
||||
"action_type": "browse",
|
||||
"object_type": "subscriber"
|
||||
},
|
||||
{
|
||||
"name": "Read subscribers",
|
||||
"action_type": "read",
|
||||
"object_type": "subscriber"
|
||||
},
|
||||
{
|
||||
"name": "Edit subscribers",
|
||||
"action_type": "edit",
|
||||
"object_type": "subscriber"
|
||||
},
|
||||
{
|
||||
"name": "Add subscribers",
|
||||
"action_type": "add",
|
||||
"object_type": "subscriber"
|
||||
},
|
||||
{
|
||||
"name": "Delete subscribers",
|
||||
"action_type": "destroy",
|
||||
"object_type": "subscriber"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -277,7 +302,8 @@
|
||||
"theme": "all",
|
||||
"user": "all",
|
||||
"role": "all",
|
||||
"client": "all"
|
||||
"client": "all",
|
||||
"subscriber": "all"
|
||||
},
|
||||
"Editor": {
|
||||
"post": "all",
|
||||
@ -286,7 +312,8 @@
|
||||
"tag": "all",
|
||||
"user": "all",
|
||||
"role": "all",
|
||||
"client": "all"
|
||||
"client": "all",
|
||||
"subscriber": ["add"]
|
||||
},
|
||||
"Author": {
|
||||
"post": ["browse", "read", "add"],
|
||||
@ -295,7 +322,8 @@
|
||||
"tag": ["browse", "read", "add"],
|
||||
"user": ["browse", "read"],
|
||||
"role": ["browse"],
|
||||
"client": "all"
|
||||
"client": "all",
|
||||
"subscriber": ["add"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -198,5 +198,21 @@ module.exports = {
|
||||
user_id: {type: 'integer', nullable: false, unsigned: true, references: 'users.id'},
|
||||
client_id: {type: 'integer', nullable: false, unsigned: true, references: 'clients.id'},
|
||||
expires: {type: 'bigInteger', nullable: false}
|
||||
},
|
||||
subscribers: {
|
||||
id: {type: 'increments', nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {isUUID: true}},
|
||||
name: {type: 'string', maxlength: 150, nullable: true},
|
||||
email: {type: 'string', maxlength: 254, nullable: false, unique: true, validations: {isEmail: true}},
|
||||
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'pending', validations: {isIn: [['subscribed', 'pending', 'unsubscribed']]}},
|
||||
post_id: {type: 'integer', nullable: true, unsigned: true, references: 'posts.id'},
|
||||
subscribed_url: {type: 'text', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},
|
||||
subscribed_referrer: {type: 'text', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},
|
||||
unsubscribed_url: {type: 'text', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},
|
||||
unsubscribed_at: {type: 'dateTime', nullable: true},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'integer', nullable: false},
|
||||
updated_at: {type: 'dateTime', nullable: true},
|
||||
updated_by: {type: 'integer', nullable: true}
|
||||
}
|
||||
};
|
||||
|
@ -152,6 +152,7 @@ errors = {
|
||||
context = i18n.t('errors.errors.databaseIsReadOnly');
|
||||
help = i18n.t('errors.errors.checkDatabase');
|
||||
}
|
||||
|
||||
// TODO: Logging framework hookup
|
||||
// Eventually we'll have better logging which will know about envs
|
||||
if ((process.env.NODE_ENV === 'development' ||
|
||||
|
@ -38,6 +38,7 @@ coreHelpers.url = require('./url');
|
||||
|
||||
// Specialist helpers for certain templates
|
||||
coreHelpers.input_password = require('./input_password');
|
||||
coreHelpers.input_email = require('./input_email');
|
||||
coreHelpers.page_url = require('./page_url');
|
||||
coreHelpers.pageUrl = require('./page_url').deprecated;
|
||||
|
||||
@ -97,6 +98,7 @@ registerHelpers = function (adminHbs) {
|
||||
registerThemeHelper('has', coreHelpers.has);
|
||||
registerThemeHelper('is', coreHelpers.is);
|
||||
registerThemeHelper('image', coreHelpers.image);
|
||||
registerThemeHelper('input_email', coreHelpers.input_email);
|
||||
registerThemeHelper('input_password', coreHelpers.input_password);
|
||||
registerThemeHelper('meta_description', coreHelpers.meta_description);
|
||||
registerThemeHelper('meta_title', coreHelpers.meta_title);
|
||||
|
43
core/server/helpers/input_email.js
Normal file
43
core/server/helpers/input_email.js
Normal file
@ -0,0 +1,43 @@
|
||||
// # Input Email Helper
|
||||
// Usage: `{{input_email}}`
|
||||
//
|
||||
// Password input used on private.hbs for password-protected blogs
|
||||
//
|
||||
// We use the name meta_title to match the helper for consistency:
|
||||
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
||||
|
||||
var hbs = require('express-hbs'),
|
||||
utils = require('./utils'),
|
||||
input_email;
|
||||
|
||||
input_email = function (options) {
|
||||
options = options || {};
|
||||
options.hash = options.hash || {};
|
||||
|
||||
var className = (options.hash.class) ? options.hash.class : 'subscribe-email',
|
||||
extras = '',
|
||||
output;
|
||||
|
||||
if (options.hash.autofocus) {
|
||||
extras += 'autofocus="autofocus"';
|
||||
}
|
||||
|
||||
if (options.hash.placeholder) {
|
||||
extras += ' placeholder="' + options.hash.placeholder + '"';
|
||||
}
|
||||
|
||||
if (options.hash.value) {
|
||||
extras += ' value="' + options.hash.value + '"';
|
||||
}
|
||||
|
||||
output = utils.inputTemplate({
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
className: className,
|
||||
extras: extras
|
||||
});
|
||||
|
||||
return new hbs.handlebars.SafeString(output);
|
||||
};
|
||||
|
||||
module.exports = input_email;
|
15
core/server/helpers/tpl/subscribe_form.hbs
Normal file
15
core/server/helpers/tpl/subscribe_form.hbs
Normal file
@ -0,0 +1,15 @@
|
||||
<form method="post" action="{{action}}" class="{{form_class}}">
|
||||
{{! This is required for the form to work correctly }}
|
||||
{{hidden}}
|
||||
|
||||
<div class="form-group{{#if error}} error{{/if}}">
|
||||
{{input_email class=input_class placeholder=placeholder value=email autofocus=autofocus}}
|
||||
</div>
|
||||
<button class="{{button_class}}" type="submit">Subscribe</button>
|
||||
{{! This is used to get extra info about where this subscriber came from }}
|
||||
{{script}}
|
||||
</form>
|
||||
|
||||
{{#if error}}
|
||||
<p class="main-error">{{{error.message}}}</p>
|
||||
{{/if}}
|
@ -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;
|
@ -27,6 +27,7 @@ themeHandler = {
|
||||
// Setup handlebars for the current context (admin or theme)
|
||||
configHbsForContext: function configHbsForContext(req, res, next) {
|
||||
var themeData = _.cloneDeep(config.theme),
|
||||
labsData = _.cloneDeep(config.labs),
|
||||
blogApp = req.app;
|
||||
|
||||
if (req.secure && config.urlSSL) {
|
||||
@ -38,7 +39,7 @@ themeHandler = {
|
||||
themeData.posts_per_page = themeData.postsPerPage;
|
||||
delete themeData.postsPerPage;
|
||||
|
||||
hbs.updateTemplateOptions({data: {blog: themeData}});
|
||||
hbs.updateTemplateOptions({data: {blog: themeData, labs: labsData}});
|
||||
|
||||
if (config.paths.themePath && blogApp.get('activeTheme')) {
|
||||
blogApp.set('views', path.join(config.paths.themePath, blogApp.get('activeTheme')));
|
||||
|
@ -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'
|
||||
];
|
||||
|
102
core/server/models/subscriber.js
Normal file
102
core/server/models/subscriber.js
Normal file
@ -0,0 +1,102 @@
|
||||
var ghostBookshelf = require('./base'),
|
||||
errors = require('../errors'),
|
||||
events = require('../events'),
|
||||
i18n = require('../i18n'),
|
||||
Promise = require('bluebird'),
|
||||
uuid = require('node-uuid'),
|
||||
Subscriber,
|
||||
Subscribers;
|
||||
|
||||
Subscriber = ghostBookshelf.Model.extend({
|
||||
tableName: 'subscribers',
|
||||
|
||||
emitChange: function emitChange(event) {
|
||||
events.emit('subscriber' + '.' + event, this);
|
||||
},
|
||||
defaults: function defaults() {
|
||||
return {
|
||||
uuid: uuid.v4(),
|
||||
status: 'subscribed'
|
||||
};
|
||||
},
|
||||
initialize: function initialize() {
|
||||
ghostBookshelf.Model.prototype.initialize.apply(this, arguments);
|
||||
this.on('created', function onCreated(model) {
|
||||
model.emitChange('added');
|
||||
});
|
||||
this.on('updated', function onUpdated(model) {
|
||||
model.emitChange('edited');
|
||||
});
|
||||
this.on('destroyed', function onDestroyed(model) {
|
||||
model.emitChange('deleted');
|
||||
});
|
||||
}
|
||||
}, {
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
permissible: function permissible(postModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
// CASE: external is only allowed to add and edit subscribers
|
||||
if (context.external) {
|
||||
if (['add', 'edit'].indexOf(action) !== -1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUserPermission && hasAppPermission) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.subscriber.notEnoughPermission')));
|
||||
},
|
||||
|
||||
// TODO: This is a copy paste of models/user.js!
|
||||
getByEmail: function getByEmail(email, options) {
|
||||
options = options || {};
|
||||
options.require = true;
|
||||
|
||||
return Subscribers.forge(options).fetch(options).then(function then(subscribers) {
|
||||
var subscriberWithEmail = subscribers.find(function findSubscriber(subscriber) {
|
||||
return subscriber.get('email').toLowerCase() === email.toLowerCase();
|
||||
});
|
||||
|
||||
if (subscriberWithEmail) {
|
||||
return subscriberWithEmail;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Subscribers = ghostBookshelf.Collection.extend({
|
||||
model: Subscriber
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Subscriber: ghostBookshelf.model('Subscriber', Subscriber),
|
||||
Subscribers: ghostBookshelf.collection('Subscriber', Subscribers)
|
||||
};
|
@ -25,11 +25,17 @@ function hasActionsMap() {
|
||||
function parseContext(context) {
|
||||
// Parse what's passed to canThis.beginCheck for standard user and app scopes
|
||||
var parsed = {
|
||||
internal: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: true
|
||||
};
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: true
|
||||
};
|
||||
|
||||
if (context && (context === 'external' || context.external)) {
|
||||
parsed.external = true;
|
||||
parsed.public = false;
|
||||
}
|
||||
|
||||
if (context && (context === 'internal' || context.internal)) {
|
||||
parsed.internal = true;
|
||||
@ -117,8 +123,10 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
|
||||
role: Models.Role,
|
||||
user: Models.User,
|
||||
permission: Models.Permission,
|
||||
setting: Models.Settings
|
||||
setting: Models.Settings,
|
||||
subscriber: Models.Subscriber
|
||||
};
|
||||
|
||||
// Iterate through the object types, i.e. ['post', 'tag', 'user']
|
||||
return _.reduce(objTypes, function (objTypeHandlers, objType) {
|
||||
// Grab the TargetModel through the objectTypeModelMap
|
||||
|
@ -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));
|
||||
|
||||
|
@ -197,6 +197,9 @@
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"subscriber": {
|
||||
"notEnoughPermission": "You do not have permission to perform this action"
|
||||
},
|
||||
"post": {
|
||||
"untitled": "(Untitled)",
|
||||
"valueCannotBeBlank": "Value in {key} cannot be blank.",
|
||||
@ -333,6 +336,10 @@
|
||||
"tags": {
|
||||
"tagNotFound": "Tag not found."
|
||||
},
|
||||
"subscribers": {
|
||||
"subscriberNotFound": "Subscriber not found.",
|
||||
"subscriberAlreadyExist": "Email already exist."
|
||||
},
|
||||
"themes": {
|
||||
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
|
||||
"noPermissionToEditThemes": "You do not have permission to edit themes.",
|
||||
@ -447,6 +454,9 @@
|
||||
"unableToSendEmail": "Ghost is currently unable to send email."
|
||||
},
|
||||
"helpers": {
|
||||
"helperNotAvailable": "The \\{\\{{helperName}\\}\\} helper is not available.",
|
||||
"apiMustBeEnabled": "The {flagName} labs flag must be enabled if you wish to use the \\{\\{{helperName}\\}\\} helper.",
|
||||
"seeLink": "See {url}",
|
||||
"foreach": {
|
||||
"iteratorNeeded": "Need to pass an iterator to #foreach"
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
var unidecode = require('unidecode'),
|
||||
_ = require('lodash'),
|
||||
readCSV = require('./read-csv'),
|
||||
|
||||
utils,
|
||||
getRandomInt;
|
||||
@ -99,7 +100,9 @@ utils = {
|
||||
/*jslint unparam:true*/
|
||||
res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S});
|
||||
res.redirect(301, path);
|
||||
}
|
||||
},
|
||||
|
||||
readCSV: readCSV
|
||||
};
|
||||
|
||||
module.exports = utils;
|
||||
|
66
core/server/utils/read-csv.js
Normal file
66
core/server/utils/read-csv.js
Normal file
@ -0,0 +1,66 @@
|
||||
var readline = require('readline'),
|
||||
Promise = require('bluebird'),
|
||||
lodash = require('lodash'),
|
||||
errors = require('../errors'),
|
||||
fs = require('fs');
|
||||
|
||||
function readCSV(options) {
|
||||
var path = options.path,
|
||||
columnsToExtract = options.columnsToExtract || [],
|
||||
firstLine = true,
|
||||
mapping = {},
|
||||
toReturn = [],
|
||||
rl;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
rl = readline.createInterface({
|
||||
input: fs.createReadStream(path),
|
||||
terminal: false
|
||||
});
|
||||
|
||||
rl.on('line', function (line) {
|
||||
var values = line.split(','),
|
||||
entry = {};
|
||||
|
||||
// CASE: column headers
|
||||
if (firstLine) {
|
||||
if (values.length === 1) {
|
||||
mapping[columnsToExtract[0]] = 0;
|
||||
} else {
|
||||
try {
|
||||
lodash.each(columnsToExtract, function (columnToExtract) {
|
||||
mapping[columnToExtract] = lodash.findIndex(values, function (value) {
|
||||
if (value.match(columnToExtract)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// CASE: column does not exist
|
||||
if (mapping[columnToExtract] === -1) {
|
||||
throw new errors.ValidationError(
|
||||
'Column header missing: "{{column}}".'.replace('{{column}}', columnToExtract)
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
firstLine = false;
|
||||
} else {
|
||||
lodash.each(mapping, function (index, columnName) {
|
||||
entry[columnName] = values[index];
|
||||
});
|
||||
|
||||
toReturn.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
rl.on('close', function () {
|
||||
resolve(toReturn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = readCSV;
|
303
core/test/integration/api/api_subscription_spec.js
Normal file
303
core/test/integration/api/api_subscription_spec.js
Normal file
@ -0,0 +1,303 @@
|
||||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
fs = require('fs'),
|
||||
_ = require('lodash'),
|
||||
context = testUtils.context,
|
||||
errors = require('../../../server/errors'),
|
||||
serverUtils = require('../../../server/utils'),
|
||||
apiUtils = require('../../../server/api/utils'),
|
||||
SubscribersAPI = require('../../../server/api/subscribers');
|
||||
|
||||
describe('Subscribers API', function () {
|
||||
// Keep the DB clean
|
||||
before(testUtils.teardown);
|
||||
afterEach(testUtils.teardown);
|
||||
beforeEach(testUtils.setup('users:roles', 'perms:subscriber', '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 (editor)', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.editor)
|
||||
.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 (author)', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.author)
|
||||
.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('duplicate subscriber', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
|
||||
.then(function () {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
|
||||
.then(function () {
|
||||
return done();
|
||||
})
|
||||
.catch(done);
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('CANNOT add subscriber without context', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]})
|
||||
.then(function () {
|
||||
done(new Error('Add subscriber without context should have no access.'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
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 subscriber (external)', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.external, {id: firstSubscriber}))
|
||||
.then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.length.should.be.above(0);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('CANNOT edit a subscriber (editor)', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.editor, {id: firstSubscriber}))
|
||||
.then(function () {
|
||||
done(new Error('Edit subscriber as author should have no access.'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('CANNOT edit subscriber (author)', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.author, {id: firstSubscriber}))
|
||||
.then(function () {
|
||||
done(new Error('Edit subscriber as author should have no access.'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
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 instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Destroy', function () {
|
||||
var firstSubscriber = 1;
|
||||
|
||||
it('can destroy subscriber as admin', function (done) {
|
||||
SubscribersAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstSubscriber}))
|
||||
.then(function (results) {
|
||||
should.not.exist(results);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('CANNOT destroy subscriber', function (done) {
|
||||
SubscribersAPI.destroy(_.extend({}, testUtils.context.editor, {id: firstSubscriber}))
|
||||
.then(function () {
|
||||
done(new Error('Destroy subscriber should not be possible as editor.'));
|
||||
}, function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
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);
|
||||
});
|
||||
|
||||
it('CANNOT browse subscriber (external)', function (done) {
|
||||
SubscribersAPI.browse(testUtils.context.external)
|
||||
.then(function () {
|
||||
done(new Error('Browse subscriber should be denied with external context.'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
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 instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Read CSV', function () {
|
||||
var scope = {};
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(fs, 'unlink', function (path, cb) {
|
||||
cb();
|
||||
});
|
||||
sinon.stub(apiUtils, 'checkFileExists').returns(true);
|
||||
sinon.stub(serverUtils, 'readCSV', function () {
|
||||
if (scope.csvError) {
|
||||
return Promise.reject(new Error('csv'));
|
||||
}
|
||||
|
||||
return Promise.resolve(scope.values);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fs.unlink.restore();
|
||||
apiUtils.checkFileExists.restore();
|
||||
serverUtils.readCSV.restore();
|
||||
});
|
||||
|
||||
it('check that fn works in general', function (done) {
|
||||
scope.values = [{email: 'lol@hallo.de'}, {email: 'test'}, {email:'lol@hallo.de'}];
|
||||
|
||||
SubscribersAPI.importCSV(_.merge(testUtils.context.internal, {path: '/somewhere'}))
|
||||
.then(function (result) {
|
||||
result.meta.stats.imported.should.eql(1);
|
||||
result.meta.stats.duplicates.should.eql(1);
|
||||
result.meta.stats.invalid.should.eql(1);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('check that fn works in general', function (done) {
|
||||
scope.values = [{email: 'lol@hallo.de'}, {email: '1@kate.de'}];
|
||||
|
||||
SubscribersAPI.importCSV(_.merge(testUtils.context.internal, {path: '/somewhere'}))
|
||||
.then(function (result) {
|
||||
result.meta.stats.imported.should.eql(2);
|
||||
result.meta.stats.duplicates.should.eql(0);
|
||||
result.meta.stats.invalid.should.eql(0);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('read csv throws an error', function (done) {
|
||||
scope.csvError = true;
|
||||
|
||||
SubscribersAPI.importCSV(_.merge(testUtils.context.internal, {path: '/somewhere'}))
|
||||
.then(function () {
|
||||
done(new Error('we expected an error here!'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
err.message.should.eql('csv');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -135,6 +135,20 @@ describe('Database Migration (special functions)', function () {
|
||||
permissions[33].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
|
||||
permissions[34].name.should.eql('Delete clients');
|
||||
permissions[34].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
|
||||
|
||||
console.log(permissions[38]);
|
||||
|
||||
// Subscribers
|
||||
permissions[35].name.should.eql('Browse subscribers');
|
||||
permissions[35].should.be.AssignedToRoles(['Administrator']);
|
||||
permissions[36].name.should.eql('Read subscribers');
|
||||
permissions[36].should.be.AssignedToRoles(['Administrator']);
|
||||
permissions[37].name.should.eql('Edit subscribers');
|
||||
permissions[37].should.be.AssignedToRoles(['Administrator']);
|
||||
permissions[38].name.should.eql('Add subscribers');
|
||||
permissions[38].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
|
||||
permissions[39].name.should.eql('Delete subscribers');
|
||||
permissions[39].should.be.AssignedToRoles(['Administrator']);
|
||||
});
|
||||
|
||||
describe('Populate', function () {
|
||||
@ -195,7 +209,7 @@ describe('Database Migration (special functions)', function () {
|
||||
result.roles.at(3).get('name').should.eql('Owner');
|
||||
|
||||
// Permissions
|
||||
result.permissions.length.should.eql(35);
|
||||
result.permissions.length.should.eql(40);
|
||||
result.permissions.toJSON().should.be.CompletePermissions();
|
||||
|
||||
done();
|
||||
|
@ -442,7 +442,7 @@ describe('API Utils', function () {
|
||||
describe('handlePublicPermissions', function () {
|
||||
it('should return empty options if passed empty options', function (done) {
|
||||
apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
|
||||
options.should.eql({context: {app: null, internal: false, public: true, user: null}});
|
||||
options.should.eql({context: {app: null, external: false, internal: false, public: true, user: null}});
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
@ -451,7 +451,7 @@ describe('API Utils', function () {
|
||||
var aPPStub = sandbox.stub(apiUtils, 'applyPublicPermissions').returns(Promise.resolve({}));
|
||||
apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
|
||||
aPPStub.calledOnce.should.eql(true);
|
||||
options.should.eql({context: {app: null, internal: false, public: true, user: null}});
|
||||
options.should.eql({context: {app: null, external: false, internal: false, public: true, user: null}});
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
@ -467,7 +467,7 @@ describe('API Utils', function () {
|
||||
apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function (options) {
|
||||
cTStub.calledOnce.should.eql(true);
|
||||
cTMethodStub.test.test.calledOnce.should.eql(true);
|
||||
options.should.eql({context: {app: null, internal: false, public: false, user: 1}});
|
||||
options.should.eql({context: {app: null, external: false, internal: false, public: false, user: 1}});
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
@ -132,10 +132,12 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('01-move-jquery-with-alert', function () {
|
||||
var moveJquery = fixtures004[0];
|
||||
|
||||
it('tries to move jQuery to ghost_foot', function (done) {
|
||||
getObjStub.get.returns('');
|
||||
|
||||
fixtures004[0]({}, loggerStub).then(function () {
|
||||
moveJquery({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||
settingsEditStub.calledOnce.should.be.true();
|
||||
@ -152,7 +154,7 @@ describe('Fixtures', function () {
|
||||
+ '<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.min.js"></script>\n\n'
|
||||
);
|
||||
|
||||
fixtures004[0]({}, loggerStub).then(function () {
|
||||
moveJquery({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||
settingsEditStub.calledOnce.should.be.false();
|
||||
@ -166,7 +168,7 @@ describe('Fixtures', function () {
|
||||
it('does not move jQuery to ghost_foot if the setting is missing', function (done) {
|
||||
settingsOneStub.returns(Promise.resolve());
|
||||
|
||||
fixtures004[0]({}, loggerStub).then(function () {
|
||||
moveJquery({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||
settingsEditStub.called.should.be.false();
|
||||
@ -182,7 +184,7 @@ describe('Fixtures', function () {
|
||||
configUtils.set({privacy: {useGoogleFonts: false}});
|
||||
getObjStub.get.returns('');
|
||||
|
||||
fixtures004[0]({}, loggerStub).then(function () {
|
||||
moveJquery({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||
settingsEditStub.calledOnce.should.be.true();
|
||||
@ -196,8 +198,10 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('02-update-private-setting-type', function () {
|
||||
var updateSettingType = fixtures004[1];
|
||||
|
||||
it('tries to update setting type correctly', function (done) {
|
||||
fixtures004[1]({}, loggerStub).then(function () {
|
||||
updateSettingType({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('isPrivate').should.be.true();
|
||||
getObjStub.get.calledOnce.should.be.true();
|
||||
@ -215,7 +219,7 @@ describe('Fixtures', function () {
|
||||
it('does not try to update setting type if it is already set', function (done) {
|
||||
getObjStub.get.returns('private');
|
||||
|
||||
fixtures004[1]({}, loggerStub).then(function () {
|
||||
updateSettingType({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('isPrivate').should.be.true();
|
||||
getObjStub.get.calledOnce.should.be.true();
|
||||
@ -233,8 +237,10 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('03-update-password-setting-type', function () {
|
||||
var updateSettingType = fixtures004[2];
|
||||
|
||||
it('tries to update setting type correctly', function (done) {
|
||||
fixtures004[2]({}, loggerStub).then(function () {
|
||||
updateSettingType({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('password').should.be.true();
|
||||
settingsEditStub.calledOnce.should.be.true();
|
||||
@ -250,7 +256,7 @@ describe('Fixtures', function () {
|
||||
it('does not try to update setting type if it is already set', function (done) {
|
||||
getObjStub.get.returns('private');
|
||||
|
||||
fixtures004[2]({}, loggerStub).then(function () {
|
||||
updateSettingType({}, loggerStub).then(function () {
|
||||
settingsOneStub.calledOnce.should.be.true();
|
||||
settingsOneStub.calledWith('password').should.be.true();
|
||||
getObjStub.get.calledOnce.should.be.true();
|
||||
@ -268,8 +274,10 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('04-update-ghost-admin-client', function () {
|
||||
var updateClient = fixtures004[3];
|
||||
|
||||
it('tries to update client correctly', function (done) {
|
||||
fixtures004[3]({}, loggerStub).then(function () {
|
||||
updateClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||
getObjStub.get.calledTwice.should.be.true();
|
||||
@ -290,7 +298,7 @@ describe('Fixtures', function () {
|
||||
getObjStub.get.withArgs('secret').returns('abc');
|
||||
getObjStub.get.withArgs('status').returns('enabled');
|
||||
|
||||
fixtures004[3]({}, loggerStub).then(function () {
|
||||
updateClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||
getObjStub.get.calledTwice.should.be.true();
|
||||
@ -309,7 +317,7 @@ describe('Fixtures', function () {
|
||||
getObjStub.get.withArgs('secret').returns('abc');
|
||||
getObjStub.get.withArgs('status').returns('development');
|
||||
|
||||
fixtures004[3]({}, loggerStub).then(function () {
|
||||
updateClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||
getObjStub.get.calledTwice.should.be.true();
|
||||
@ -331,7 +339,7 @@ describe('Fixtures', function () {
|
||||
getObjStub.get.withArgs('secret').returns('not_available');
|
||||
getObjStub.get.withArgs('status').returns('enabled');
|
||||
|
||||
fixtures004[3]({}, loggerStub).then(function () {
|
||||
updateClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||
getObjStub.get.calledOnce.should.be.true();
|
||||
@ -350,11 +358,13 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('05-add-ghost-frontend-client', function () {
|
||||
var addClient = fixtures004[4];
|
||||
|
||||
it('tries to add client correctly', function (done) {
|
||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
||||
clientOneStub.returns(Promise.resolve());
|
||||
|
||||
fixtures004[4]({}, loggerStub).then(function () {
|
||||
addClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true();
|
||||
clientAddStub.calledOnce.should.be.true();
|
||||
@ -369,7 +379,7 @@ describe('Fixtures', function () {
|
||||
it('does not try to add client if it already exists', function (done) {
|
||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
||||
|
||||
fixtures004[4]({}, loggerStub).then(function () {
|
||||
addClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true();
|
||||
clientAddStub.called.should.be.false();
|
||||
@ -382,7 +392,8 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('06-clean-broken-tags', function () {
|
||||
var tagObjStub, tagCollStub, tagAllStub;
|
||||
var tagObjStub, tagCollStub, tagAllStub,
|
||||
cleanBrokenTags = fixtures004[5];
|
||||
|
||||
beforeEach(function () {
|
||||
tagObjStub = {
|
||||
@ -396,7 +407,7 @@ describe('Fixtures', function () {
|
||||
it('tries to clean broken tags correctly', function (done) {
|
||||
tagObjStub.get.returns(',hello');
|
||||
|
||||
fixtures004[5]({}, loggerStub).then(function () {
|
||||
cleanBrokenTags({}, loggerStub).then(function () {
|
||||
tagAllStub.calledOnce.should.be.true();
|
||||
tagCollStub.each.calledOnce.should.be.true();
|
||||
tagObjStub.get.calledOnce.should.be.true();
|
||||
@ -414,7 +425,7 @@ describe('Fixtures', function () {
|
||||
it('tries can handle tags which end up empty', function (done) {
|
||||
tagObjStub.get.returns(',');
|
||||
|
||||
fixtures004[5]({}, loggerStub).then(function () {
|
||||
cleanBrokenTags({}, loggerStub).then(function () {
|
||||
tagAllStub.calledOnce.should.be.true();
|
||||
tagCollStub.each.calledOnce.should.be.true();
|
||||
tagObjStub.get.calledOnce.should.be.true();
|
||||
@ -432,7 +443,7 @@ describe('Fixtures', function () {
|
||||
it('does not change tags if not necessary', function (done) {
|
||||
tagObjStub.get.returns('hello');
|
||||
|
||||
fixtures004[5]({}, loggerStub).then(function () {
|
||||
cleanBrokenTags({}, loggerStub).then(function () {
|
||||
tagAllStub.calledOnce.should.be.true();
|
||||
tagCollStub.each.calledOnce.should.be.true();
|
||||
tagObjStub.get.calledOnce.should.be.true();
|
||||
@ -449,7 +460,7 @@ describe('Fixtures', function () {
|
||||
it('does nothing if there are no tags', function (done) {
|
||||
tagAllStub.returns(Promise.resolve());
|
||||
|
||||
fixtures004[5]({}, loggerStub).then(function () {
|
||||
cleanBrokenTags({}, loggerStub).then(function () {
|
||||
tagAllStub.calledOnce.should.be.true();
|
||||
tagCollStub.each.called.should.be.false();
|
||||
tagObjStub.get.called.should.be.false();
|
||||
@ -464,7 +475,8 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('07-add-post-tag-order', function () {
|
||||
var tagOp1Stub, tagOp2Stub, tagObjStub, postObjStub, postCollStub, postAllStub;
|
||||
var tagOp1Stub, tagOp2Stub, tagObjStub, postObjStub, postCollStub, postAllStub,
|
||||
addPostTagOrder = fixtures004[6];
|
||||
|
||||
beforeEach(function () {
|
||||
tagOp1Stub = sandbox.stub().returns(Promise.resolve());
|
||||
@ -489,7 +501,7 @@ describe('Fixtures', function () {
|
||||
it('calls load on each post', function (done) {
|
||||
// Fake mapThen behaviour
|
||||
postCollStub.mapThen.callsArgWith(0, postObjStub).returns([]);
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
postCollStub.mapThen.calledOnce.should.be.true();
|
||||
postObjStub.load.calledOnce.should.be.true();
|
||||
@ -508,7 +520,7 @@ describe('Fixtures', function () {
|
||||
postCollStub.mapThen.returns([]);
|
||||
postAllStub.returns(Promise.resolve());
|
||||
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
loggerStub.info.calledOnce.should.be.true();
|
||||
loggerStub.warn.calledOnce.should.be.true();
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
@ -527,7 +539,7 @@ describe('Fixtures', function () {
|
||||
// By returning from mapThen, we can skip doing tag.load in this test
|
||||
postCollStub.mapThen.returns(postObjStub);
|
||||
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
loggerStub.info.calledThrice.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
@ -551,7 +563,7 @@ describe('Fixtures', function () {
|
||||
// By returning from mapThen, we can skip doing tag.load in this test
|
||||
postCollStub.mapThen.returns(postObjStub);
|
||||
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
loggerStub.info.calledThrice.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
@ -574,7 +586,7 @@ describe('Fixtures', function () {
|
||||
// By returning from mapThen, we can skip doing tag.load in this test
|
||||
postCollStub.mapThen.returns([postObjStub]);
|
||||
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
loggerStub.info.calledOnce.should.be.true();
|
||||
loggerStub.warn.calledOnce.should.be.true();
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
@ -597,7 +609,7 @@ describe('Fixtures', function () {
|
||||
// By returning from mapThen, we can skip doing tag.load in this test
|
||||
postCollStub.mapThen.returns([postObjStub]);
|
||||
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
loggerStub.info.calledThrice.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
@ -625,7 +637,7 @@ describe('Fixtures', function () {
|
||||
// By returning from mapThen, we can skip doing tag.load in this test
|
||||
postCollStub.mapThen.returns([postObjStub]);
|
||||
|
||||
fixtures004[6]({}, loggerStub).then(function () {
|
||||
addPostTagOrder({}, loggerStub).then(function () {
|
||||
loggerStub.info.calledThrice.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
postAllStub.calledOnce.should.be.true();
|
||||
@ -656,7 +668,8 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('08-add-post-fixture', function () {
|
||||
var postOneStub, postAddStub;
|
||||
var postOneStub, postAddStub,
|
||||
addPostFixture = fixtures004[7];
|
||||
|
||||
beforeEach(function () {
|
||||
postOneStub = sandbox.stub(models.Post, 'findOne').returns(Promise.resolve());
|
||||
@ -664,7 +677,7 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
it('tries to add a new post fixture correctly', function (done) {
|
||||
fixtures004[7]({}, loggerStub).then(function () {
|
||||
addPostFixture({}, loggerStub).then(function () {
|
||||
postOneStub.calledOnce.should.be.true();
|
||||
loggerStub.info.calledOnce.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
@ -678,7 +691,7 @@ describe('Fixtures', function () {
|
||||
it('does not try to add new post fixture if it already exists', function (done) {
|
||||
postOneStub.returns(Promise.resolve({}));
|
||||
|
||||
fixtures004[7]({}, loggerStub).then(function () {
|
||||
addPostFixture({}, loggerStub).then(function () {
|
||||
postOneStub.calledOnce.should.be.true();
|
||||
loggerStub.info.called.should.be.false();
|
||||
loggerStub.warn.calledOnce.should.be.true();
|
||||
@ -719,10 +732,11 @@ describe('Fixtures', function () {
|
||||
sequenceStub.firstCall.args[0][0].should.be.a.Function().with.property('name', 'runVersionTasks');
|
||||
|
||||
sequenceStub.secondCall.calledWith(sinon.match.array, sinon.match.object, loggerStub).should.be.true();
|
||||
sequenceStub.secondCall.args[0].should.be.an.Array().with.lengthOf(3);
|
||||
sequenceStub.secondCall.args[0].should.be.an.Array().with.lengthOf(4);
|
||||
sequenceStub.secondCall.args[0][0].should.be.a.Function().with.property('name', 'updateGhostClientsSecrets');
|
||||
sequenceStub.secondCall.args[0][1].should.be.a.Function().with.property('name', 'addGhostFrontendClient');
|
||||
sequenceStub.secondCall.args[0][2].should.be.a.Function().with.property('name', 'addClientPermissions');
|
||||
sequenceStub.secondCall.args[0][3].should.be.a.Function().with.property('name', 'addSubscriberPermissions');
|
||||
|
||||
// Reset
|
||||
sequenceReset();
|
||||
@ -733,11 +747,12 @@ describe('Fixtures', function () {
|
||||
describe('Tasks:', function () {
|
||||
it('should have tasks for 005', function () {
|
||||
should.exist(fixtures005);
|
||||
fixtures005.should.be.an.Array().with.lengthOf(3);
|
||||
fixtures005.should.be.an.Array().with.lengthOf(4);
|
||||
});
|
||||
|
||||
describe('01-update-ghost-client-secrets', function () {
|
||||
var queryStub, clientForgeStub, clientEditStub;
|
||||
var queryStub, clientForgeStub, clientEditStub,
|
||||
updateClient = fixtures005[0];
|
||||
|
||||
beforeEach(function () {
|
||||
queryStub = {
|
||||
@ -754,7 +769,7 @@ describe('Fixtures', function () {
|
||||
queryStub.fetch.returns(new Promise.resolve({models: []}));
|
||||
|
||||
// Execute
|
||||
fixtures005[0]({}, loggerStub).then(function () {
|
||||
updateClient({}, loggerStub).then(function () {
|
||||
clientForgeStub.calledOnce.should.be.true();
|
||||
clientEditStub.called.should.be.false();
|
||||
loggerStub.info.called.should.be.false();
|
||||
@ -768,7 +783,7 @@ describe('Fixtures', function () {
|
||||
queryStub.fetch.returns(new Promise.resolve({models: [{id: 1}]}));
|
||||
|
||||
// Execute
|
||||
fixtures005[0]({}, loggerStub).then(function () {
|
||||
updateClient({}, loggerStub).then(function () {
|
||||
clientForgeStub.calledOnce.should.be.true();
|
||||
clientEditStub.called.should.be.true();
|
||||
loggerStub.info.calledOnce.should.be.true();
|
||||
@ -779,7 +794,8 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('02-add-ghost-scheduler-client', function () {
|
||||
var clientOneStub;
|
||||
var clientOneStub,
|
||||
addClient = fixtures005[1];
|
||||
|
||||
beforeEach(function () {
|
||||
clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve({}));
|
||||
@ -789,7 +805,7 @@ describe('Fixtures', function () {
|
||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
||||
clientOneStub.returns(Promise.resolve());
|
||||
|
||||
fixtures005[1]({}, loggerStub).then(function () {
|
||||
addClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-scheduler'}).should.be.true();
|
||||
clientAddStub.calledOnce.should.be.true();
|
||||
@ -804,7 +820,7 @@ describe('Fixtures', function () {
|
||||
it('does not try to add client if it already exists', function (done) {
|
||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
||||
|
||||
fixtures005[1]({}, loggerStub).then(function () {
|
||||
addClient({}, loggerStub).then(function () {
|
||||
clientOneStub.calledOnce.should.be.true();
|
||||
clientOneStub.calledWith({slug: 'ghost-scheduler'}).should.be.true();
|
||||
clientAddStub.called.should.be.false();
|
||||
@ -817,7 +833,8 @@ describe('Fixtures', function () {
|
||||
});
|
||||
|
||||
describe('03-add-client-permissions', function () {
|
||||
var modelResult, addModelStub, relationResult, addRelationStub;
|
||||
var modelResult, addModelStub, relationResult, addRelationStub,
|
||||
addClientPermissions = fixtures005[2];
|
||||
|
||||
beforeEach(function () {
|
||||
modelResult = {expected: 1, done: 1};
|
||||
@ -831,7 +848,7 @@ describe('Fixtures', function () {
|
||||
|
||||
it('should find the correct model & relation to add', function (done) {
|
||||
// Execute
|
||||
fixtures005[2]({}, loggerStub).then(function () {
|
||||
addClientPermissions({}, loggerStub).then(function () {
|
||||
addModelStub.calledOnce.should.be.true();
|
||||
addModelStub.calledWith(
|
||||
fixtureUtils.findModelFixtures('Permission', {object_type: 'client'})
|
||||
@ -853,7 +870,7 @@ describe('Fixtures', function () {
|
||||
// Setup
|
||||
modelResult.expected = 3;
|
||||
// Execute
|
||||
fixtures005[2]({}, loggerStub).then(function () {
|
||||
addClientPermissions({}, loggerStub).then(function () {
|
||||
addModelStub.calledOnce.should.be.true();
|
||||
addModelStub.calledWith(
|
||||
fixtureUtils.findModelFixtures('Permission', {object_type: 'client'})
|
||||
@ -871,6 +888,63 @@ describe('Fixtures', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('04-add-subscriber-permissions', function () {
|
||||
var modelResult, addModelStub, relationResult, addRelationStub,
|
||||
addSubscriberPermissions = fixtures005[3];
|
||||
|
||||
beforeEach(function () {
|
||||
modelResult = {expected: 1, done: 1};
|
||||
addModelStub = sandbox.stub(fixtureUtils, 'addFixturesForModel')
|
||||
.returns(Promise.resolve(modelResult));
|
||||
|
||||
relationResult = {expected: 1, done: 1};
|
||||
addRelationStub = sandbox.stub(fixtureUtils, 'addFixturesForRelation')
|
||||
.returns(Promise.resolve(relationResult));
|
||||
});
|
||||
|
||||
it('should find the correct model & relation to add', function (done) {
|
||||
// Execute
|
||||
addSubscriberPermissions({}, loggerStub).then(function () {
|
||||
addModelStub.calledOnce.should.be.true();
|
||||
addModelStub.calledWith(
|
||||
fixtureUtils.findModelFixtures('Permission', {object_type: 'subscriber'})
|
||||
).should.be.true();
|
||||
|
||||
addRelationStub.calledOnce.should.be.true();
|
||||
addRelationStub.calledWith(
|
||||
fixtureUtils.findPermissionRelationsForObject('subscriber')
|
||||
).should.be.true();
|
||||
|
||||
loggerStub.info.calledTwice.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn the result shows less work was done than expected', function (done) {
|
||||
// Setup
|
||||
modelResult.expected = 3;
|
||||
// Execute
|
||||
addSubscriberPermissions({}, loggerStub).then(function () {
|
||||
addModelStub.calledOnce.should.be.true();
|
||||
addModelStub.calledWith(
|
||||
fixtureUtils.findModelFixtures('Permission', {object_type: 'subscriber'})
|
||||
).should.be.true();
|
||||
|
||||
addRelationStub.calledOnce.should.be.true();
|
||||
addRelationStub.calledWith(
|
||||
fixtureUtils.findPermissionRelationsForObject('subscriber')
|
||||
).should.be.true();
|
||||
|
||||
loggerStub.info.calledOnce.should.be.true();
|
||||
loggerStub.warn.calledOnce.should.be.true();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -921,9 +995,9 @@ describe('Fixtures', function () {
|
||||
clientOneStub.calledThrice.should.be.true();
|
||||
clientAddStub.calledThrice.should.be.true();
|
||||
|
||||
permOneStub.callCount.should.eql(35);
|
||||
permOneStub.callCount.should.eql(40);
|
||||
permsAddStub.called.should.be.true();
|
||||
permsAddStub.callCount.should.eql(35);
|
||||
permsAddStub.callCount.should.eql(40);
|
||||
|
||||
permsAllStub.calledOnce.should.be.true();
|
||||
rolesAllStub.calledOnce.should.be.true();
|
||||
@ -932,8 +1006,8 @@ describe('Fixtures', function () {
|
||||
|
||||
// Relations
|
||||
modelMethodStub.filter.called.should.be.true();
|
||||
// 25 permissions, 1 tag
|
||||
modelMethodStub.filter.callCount.should.eql(25 + 1);
|
||||
// 26 permissions, 1 tag
|
||||
modelMethodStub.filter.callCount.should.eql(28 + 1);
|
||||
modelMethodStub.find.called.should.be.true();
|
||||
// 3 roles, 1 post
|
||||
modelMethodStub.find.callCount.should.eql(3 + 1);
|
||||
|
@ -152,21 +152,21 @@ describe('Utils', function () {
|
||||
fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) {
|
||||
should.exist(result);
|
||||
result.should.be.an.Object();
|
||||
result.should.have.property('expected', 25);
|
||||
result.should.have.property('done', 25);
|
||||
result.should.have.property('expected', 28);
|
||||
result.should.have.property('done', 28);
|
||||
|
||||
// Permissions & Roles
|
||||
permsAllStub.calledOnce.should.be.true();
|
||||
rolesAllStub.calledOnce.should.be.true();
|
||||
dataMethodStub.filter.callCount.should.eql(25);
|
||||
dataMethodStub.filter.callCount.should.eql(28);
|
||||
dataMethodStub.find.callCount.should.eql(3);
|
||||
|
||||
fromItem.related.callCount.should.eql(25);
|
||||
fromItem.findWhere.callCount.should.eql(25);
|
||||
toItem[0].get.callCount.should.eql(50);
|
||||
fromItem.related.callCount.should.eql(28);
|
||||
fromItem.findWhere.callCount.should.eql(28);
|
||||
toItem[0].get.callCount.should.eql(56);
|
||||
|
||||
fromItem.permissions.callCount.should.eql(25);
|
||||
fromItem.attach.callCount.should.eql(25);
|
||||
fromItem.permissions.callCount.should.eql(28);
|
||||
fromItem.attach.callCount.should.eql(28);
|
||||
fromItem.attach.calledWith(toItem).should.be.true();
|
||||
|
||||
done();
|
||||
|
@ -32,8 +32,8 @@ var should = require('should'),
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
var currentDbVersion = '005',
|
||||
currentSchemaHash = 'be706cdbeb06103d90703ee733efc556',
|
||||
currentFixturesHash = 'ba195b645386b019a69c4b79e6854138';
|
||||
currentSchemaHash = 'f63f41ac97b5665a30c899409bbf9a83',
|
||||
currentFixturesHash = '56f781fa3bba0fdbf98da5f232ec9b11';
|
||||
|
||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||
// and the values above will need updating as confirmation
|
||||
@ -405,12 +405,14 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('01-add-tour-column-to-users', function () {
|
||||
var addTourColumn = updates004[0];
|
||||
|
||||
it('does not try to add a new column if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('users').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[0](loggerStub).then(function () {
|
||||
addTourColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -431,7 +433,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'tour').returns(new Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates004[0](loggerStub).then(function () {
|
||||
addTourColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -453,7 +455,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'tour').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[0](loggerStub).then(function () {
|
||||
addTourColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -472,12 +474,14 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('02-add-sortorder-column-to-poststags', function () {
|
||||
var addSortOrderColumn = updates004[1];
|
||||
|
||||
it('does not try to add a new column if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('posts_tags').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[1](loggerStub).then(function () {
|
||||
addSortOrderColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts_tags').should.be.true();
|
||||
|
||||
@ -498,7 +502,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('posts_tags', 'sort_order').returns(new Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates004[1](loggerStub).then(function () {
|
||||
addSortOrderColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts_tags').should.be.true();
|
||||
|
||||
@ -520,7 +524,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('posts_tags', 'sort_order').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[1](loggerStub).then(function () {
|
||||
addSortOrderColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts_tags').should.be.true();
|
||||
|
||||
@ -539,12 +543,14 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('03-add-many-columns-to-clients', function () {
|
||||
var addClientColumns = updates004[2];
|
||||
|
||||
it('does not try to add new columns if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[2](loggerStub).then(function () {
|
||||
addClientColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -569,7 +575,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates004[2](loggerStub).then(function () {
|
||||
addClientColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -599,7 +605,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[2](loggerStub).then(function () {
|
||||
addClientColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -634,7 +640,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates004[2](loggerStub).then(function () {
|
||||
addClientColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -658,12 +664,14 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('04-add-clienttrusteddomains-table', function () {
|
||||
var addTrustedDomains = updates004[3];
|
||||
|
||||
it('does not try to add a new table if the table already exists', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates004[3](loggerStub).then(function () {
|
||||
addTrustedDomains(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('client_trusted_domains').should.be.true();
|
||||
|
||||
@ -681,7 +689,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[3](loggerStub).then(function () {
|
||||
addTrustedDomains(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('client_trusted_domains').should.be.true();
|
||||
|
||||
@ -697,6 +705,8 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('05-drop-unique-on-clients-secret', function () {
|
||||
var dropUnique = updates004[4];
|
||||
|
||||
it('does not try to drop unique if the table does not exist', function (done) {
|
||||
// Setup
|
||||
getIndexesStub.withArgs('clients').returns(new Promise.resolve(
|
||||
@ -705,7 +715,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates004[4](loggerStub).then(function () {
|
||||
dropUnique(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -729,7 +739,7 @@ describe('Migrations', function () {
|
||||
);
|
||||
|
||||
// Execute
|
||||
updates004[4](loggerStub).then(function () {
|
||||
dropUnique(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -754,7 +764,7 @@ describe('Migrations', function () {
|
||||
);
|
||||
|
||||
// Execute
|
||||
updates004[4](loggerStub).then(function () {
|
||||
dropUnique(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||
|
||||
@ -798,7 +808,7 @@ describe('Migrations', function () {
|
||||
|
||||
tasksSpy.calledOnce.should.be.true();
|
||||
tasksSpy.calledWith('005', loggerStub).should.be.true();
|
||||
tasksSpy.firstCall.returnValue.should.be.an.Array().with.lengthOf(4);
|
||||
tasksSpy.firstCall.returnValue.should.be.an.Array().with.lengthOf(5);
|
||||
|
||||
sequenceStub.calledTwice.should.be.true();
|
||||
|
||||
@ -807,11 +817,12 @@ describe('Migrations', function () {
|
||||
sequenceStub.firstCall.args[0][0].should.be.a.Function().with.property('name', 'runVersionTasks');
|
||||
|
||||
sequenceStub.secondCall.calledWith(sinon.match.array, loggerStub).should.be.true();
|
||||
sequenceStub.secondCall.args[0].should.be.an.Array().with.lengthOf(4);
|
||||
sequenceStub.secondCall.args[0].should.be.an.Array().with.lengthOf(5);
|
||||
sequenceStub.secondCall.args[0][0].should.be.a.Function().with.property('name', 'dropHiddenColumnFromTags');
|
||||
sequenceStub.secondCall.args[0][1].should.be.a.Function().with.property('name', 'addVisibilityColumnToKeyTables');
|
||||
sequenceStub.secondCall.args[0][2].should.be.a.Function().with.property('name', 'addMobiledocColumnToPosts');
|
||||
sequenceStub.secondCall.args[0][3].should.be.a.Function().with.property('name', 'addSocialMediaColumnsToUsers');
|
||||
sequenceStub.secondCall.args[0][4].should.be.a.Function().with.property('name', 'addSubscribersTable');
|
||||
|
||||
// Reset sequence
|
||||
sequenceReset();
|
||||
@ -820,7 +831,7 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('Tasks:', function () {
|
||||
var dropColumnStub, addColumnStub,
|
||||
var dropColumnStub, addColumnStub, createTableStub,
|
||||
knexStub, knexMock;
|
||||
|
||||
beforeEach(function () {
|
||||
@ -834,6 +845,7 @@ describe('Migrations', function () {
|
||||
|
||||
dropColumnStub = sandbox.stub(schema.commands, 'dropColumn');
|
||||
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
|
||||
createTableStub = sandbox.stub(schema.commands, 'createTable');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -842,16 +854,18 @@ describe('Migrations', function () {
|
||||
|
||||
it('should have tasks for 005', function () {
|
||||
should.exist(updates005);
|
||||
updates005.should.be.an.Array().with.lengthOf(4);
|
||||
updates005.should.be.an.Array().with.lengthOf(5);
|
||||
});
|
||||
|
||||
describe('01-drop-hidden-column-from-tags', function () {
|
||||
var dropHiddenColumn = updates005[0];
|
||||
|
||||
it('does not try to drop column if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('tags').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[0](loggerStub).then(function () {
|
||||
dropHiddenColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
|
||||
@ -872,7 +886,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('tags', 'hidden').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[0](loggerStub).then(function () {
|
||||
dropHiddenColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
|
||||
@ -894,7 +908,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('tags', 'hidden').returns(Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates005[0](loggerStub).then(function () {
|
||||
dropHiddenColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
|
||||
@ -913,6 +927,8 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('02-add-visibility-column-to-key-tables', function () {
|
||||
var addVisibilityColumn = updates005[1];
|
||||
|
||||
it('does not try to add new column if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('posts').returns(Promise.resolve(false));
|
||||
@ -920,7 +936,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasTable.withArgs('users').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[1](loggerStub).then(function () {
|
||||
addVisibilityColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
@ -948,7 +964,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates005[1](loggerStub).then(function () {
|
||||
addVisibilityColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
@ -979,7 +995,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[1](loggerStub).then(function () {
|
||||
addVisibilityColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
@ -1012,7 +1028,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('tags', 'visibility').returns(Promise.resolve(true));
|
||||
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(false));
|
||||
// Execute
|
||||
updates005[1](loggerStub).then(function () {
|
||||
addVisibilityColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('tags').should.be.true();
|
||||
@ -1037,12 +1053,14 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('03-add-mobiledoc-column-to-posts', function () {
|
||||
var addMobiledocColumn = updates005[2];
|
||||
|
||||
it('does not try to add a new column if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('posts').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[2](loggerStub).then(function () {
|
||||
addMobiledocColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
|
||||
@ -1063,7 +1081,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('posts', 'mobiledoc').returns(Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates005[2](loggerStub).then(function () {
|
||||
addMobiledocColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
|
||||
@ -1085,7 +1103,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('posts', 'mobiledoc').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[2](loggerStub).then(function () {
|
||||
addMobiledocColumn(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||
|
||||
@ -1104,12 +1122,14 @@ describe('Migrations', function () {
|
||||
});
|
||||
|
||||
describe('04-add-social-media-columns-to-users', function () {
|
||||
var addSocialMediaColumns = updates005[3];
|
||||
|
||||
it('does not try to add new columns if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('users').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[3](loggerStub).then(function () {
|
||||
addSocialMediaColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -1131,7 +1151,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'twitter').returns(Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates005[3](loggerStub).then(function () {
|
||||
addSocialMediaColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -1155,7 +1175,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'twitter').returns(Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
updates005[3](loggerStub).then(function () {
|
||||
addSocialMediaColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -1181,7 +1201,7 @@ describe('Migrations', function () {
|
||||
knexMock.schema.hasColumn.withArgs('users', 'twitter').returns(Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
updates005[3](loggerStub).then(function () {
|
||||
addSocialMediaColumns(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||
|
||||
@ -1200,6 +1220,47 @@ describe('Migrations', function () {
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('05-add-subscribers-table', function () {
|
||||
var addSubscribers = updates005[4];
|
||||
|
||||
it('does not try to add a new table if the table already exists', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('subscribers').returns(new Promise.resolve(true));
|
||||
|
||||
// Execute
|
||||
addSubscribers(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('subscribers').should.be.true();
|
||||
|
||||
createTableStub.called.should.be.false();
|
||||
|
||||
loggerStub.info.called.should.be.false();
|
||||
loggerStub.warn.calledOnce.should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('tries to add a new table if the table does not exist', function (done) {
|
||||
// Setup
|
||||
knexMock.schema.hasTable.withArgs('subscribers').returns(new Promise.resolve(false));
|
||||
|
||||
// Execute
|
||||
addSubscribers(loggerStub).then(function () {
|
||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||
knexMock.schema.hasTable.calledWith('subscribers').should.be.true();
|
||||
|
||||
createTableStub.calledOnce.should.be.true();
|
||||
createTableStub.calledWith('subscribers').should.be.true();
|
||||
|
||||
loggerStub.info.calledOnce.should.be.true();
|
||||
loggerStub.warn.called.should.be.false();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -50,12 +50,14 @@ describe('Permissions', function () {
|
||||
it('should return public for no context', function () {
|
||||
permissions.parseContext().should.eql({
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: true
|
||||
});
|
||||
permissions.parseContext({}).should.eql({
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: true
|
||||
@ -65,12 +67,14 @@ describe('Permissions', function () {
|
||||
it('should return public for random context', function () {
|
||||
permissions.parseContext('public').should.eql({
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: true
|
||||
});
|
||||
permissions.parseContext({client: 'thing'}).should.eql({
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: true
|
||||
@ -80,6 +84,7 @@ describe('Permissions', function () {
|
||||
it('should return user if user populated', function () {
|
||||
permissions.parseContext({user: 1}).should.eql({
|
||||
internal: false,
|
||||
external: false,
|
||||
user: 1,
|
||||
app: null,
|
||||
public: false
|
||||
@ -89,6 +94,7 @@ describe('Permissions', function () {
|
||||
it('should return app if app populated', function () {
|
||||
permissions.parseContext({app: 5}).should.eql({
|
||||
internal: false,
|
||||
external: false,
|
||||
user: null,
|
||||
app: 5,
|
||||
public: false
|
||||
@ -98,6 +104,7 @@ describe('Permissions', function () {
|
||||
it('should return internal if internal provided', function () {
|
||||
permissions.parseContext({internal: true}).should.eql({
|
||||
internal: true,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: false
|
||||
@ -105,6 +112,7 @@ describe('Permissions', function () {
|
||||
|
||||
permissions.parseContext('internal').should.eql({
|
||||
internal: true,
|
||||
external: false,
|
||||
user: null,
|
||||
app: null,
|
||||
public: false
|
||||
|
173
core/test/unit/utils/read-csv_spec.js
Normal file
173
core/test/unit/utils/read-csv_spec.js
Normal file
@ -0,0 +1,173 @@
|
||||
/*globals describe, beforeEach, afterEach, it*/
|
||||
|
||||
var utils = require('../../../server/utils'),
|
||||
errors = require('../../../server/errors'),
|
||||
sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
fs = require('fs'),
|
||||
lodash = require('lodash'),
|
||||
readline = require('readline');
|
||||
|
||||
describe('read csv', function () {
|
||||
var scope = {};
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(fs, 'createReadStream');
|
||||
|
||||
sinon.stub(readline, 'createInterface', function () {
|
||||
return {
|
||||
on: function (eventName, cb) {
|
||||
switch (eventName) {
|
||||
case 'line':
|
||||
lodash.each(scope.csv, function (line) {
|
||||
cb(line);
|
||||
});
|
||||
break;
|
||||
case 'close':
|
||||
cb();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fs.createReadStream.restore();
|
||||
readline.createInterface.restore();
|
||||
});
|
||||
|
||||
it('read csv: one column', function (done) {
|
||||
scope.csv = [
|
||||
'email',
|
||||
'hannah@ghost.org',
|
||||
'kate@ghost.org'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email']
|
||||
}).then(function (result) {
|
||||
should.exist(result);
|
||||
result.length.should.eql(2);
|
||||
result[0].email.should.eql('hannah@ghost.org');
|
||||
result[1].email.should.eql('kate@ghost.org');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('read csv: two columns', function (done) {
|
||||
scope.csv = [
|
||||
'id,email',
|
||||
'1,hannah@ghost.org',
|
||||
'1,kate@ghost.org'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email']
|
||||
}).then(function (result) {
|
||||
should.exist(result);
|
||||
result.length.should.eql(2);
|
||||
result[0].email.should.eql('hannah@ghost.org');
|
||||
result[1].email.should.eql('kate@ghost.org');
|
||||
should.not.exist(result[0].id);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('read csv: two columns', function (done) {
|
||||
scope.csv = [
|
||||
'id,email',
|
||||
'1,hannah@ghost.org',
|
||||
'2,kate@ghost.org'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email', 'id']
|
||||
}).then(function (result) {
|
||||
should.exist(result);
|
||||
result.length.should.eql(2);
|
||||
result[0].email.should.eql('hannah@ghost.org');
|
||||
result[0].id.should.eql('1');
|
||||
result[1].email.should.eql('kate@ghost.org');
|
||||
result[1].id.should.eql('2');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('read csv: test email regex', function (done) {
|
||||
scope.csv = [
|
||||
'email_address',
|
||||
'hannah@ghost.org',
|
||||
'kate@ghost.org'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email']
|
||||
}).then(function (result) {
|
||||
should.exist(result);
|
||||
result.length.should.eql(2);
|
||||
result[0].email.should.eql('hannah@ghost.org');
|
||||
result[1].email.should.eql('kate@ghost.org');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('read csv: support single column use case', function (done) {
|
||||
scope.csv = [
|
||||
'a_column',
|
||||
'hannah@ghost.org',
|
||||
'kate@ghost.org'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email']
|
||||
}).then(function (result) {
|
||||
should.exist(result);
|
||||
result.length.should.eql(2);
|
||||
result[0].email.should.eql('hannah@ghost.org');
|
||||
result[1].email.should.eql('kate@ghost.org');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('read csv: support single column use case (we would loose the first entry)', function (done) {
|
||||
scope.csv = [
|
||||
'hannah@ghost.org',
|
||||
'kate@ghost.org'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email']
|
||||
}).then(function (result) {
|
||||
should.exist(result);
|
||||
result.length.should.eql(1);
|
||||
result[0].email.should.eql('kate@ghost.org');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('read csv: broken', function (done) {
|
||||
scope.csv = [
|
||||
'id,test',
|
||||
'1,2',
|
||||
'1,2'
|
||||
];
|
||||
|
||||
utils.readCSV({
|
||||
path: 'read-file-is-mocked',
|
||||
columnsToExtract: ['email', 'id']
|
||||
}).then(function () {
|
||||
return done(new Error('we expected an error from read csv!'));
|
||||
}).catch(function (err) {
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
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…
Reference in New Issue
Block a user