mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-15 03:12:54 +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(),
|
ajax: service(),
|
||||||
config: service(),
|
config: service(),
|
||||||
session: service(),
|
|
||||||
|
|
||||||
// TODO: this wouldn't be necessary if the server could accept direct
|
// TODO: this wouldn't be necessary if the server could accept direct
|
||||||
// file uploads
|
// 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(),
|
config: service(),
|
||||||
session: service(),
|
session: service(),
|
||||||
ghostPaths: service(),
|
ghostPaths: service(),
|
||||||
|
feature: service(),
|
||||||
|
|
||||||
mouseEnter() {
|
mouseEnter() {
|
||||||
this.sendAction('onMouseEnter');
|
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 () {
|
export default function () {
|
||||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
// 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.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
|
// 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
|
// keep this line, it allows all other API requests to hit the real server
|
||||||
this.passthrough();
|
this.passthrough();
|
||||||
@ -251,6 +317,10 @@ export function testConfig() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Subscribers ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
mockSubscribers(this);
|
||||||
|
|
||||||
/* Tags ----------------------------------------------------------------- */
|
/* Tags ----------------------------------------------------------------- */
|
||||||
|
|
||||||
this.post('/tags/', function (db, request) {
|
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,
|
id: 12,
|
||||||
uuid: 'd806f358-7996-4c74-b153-8876959c4b70',
|
uuid: 'd806f358-7996-4c74-b153-8876959c4b70',
|
||||||
key: 'labs',
|
key: 'labs',
|
||||||
value: '{"codeInjectionUI":true}',
|
value: '{"codeInjectionUI":true,"subscribers":true}',
|
||||||
type: 'blog',
|
type: 'blog',
|
||||||
created_at: '2015-01-12T18:29:01.000Z',
|
created_at: '2015-01-12T18:29:01.000Z',
|
||||||
created_by: 1,
|
created_by: 1,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
export default function (/* server */) {
|
export default function (server) {
|
||||||
// Seed your development database using your factories. This
|
// Seed your development database using your factories. This
|
||||||
// data will not be loaded in your tests.
|
// data will not be loaded in your tests.
|
||||||
|
|
||||||
// server.createList('contact', 10);
|
// server.createList('contact', 10);
|
||||||
|
|
||||||
|
server.createList('subscriber', 125);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import getRequestErrorMessage from 'ghost/utils/ajax';
|
|||||||
const {
|
const {
|
||||||
Mixin,
|
Mixin,
|
||||||
computed,
|
computed,
|
||||||
|
RSVP,
|
||||||
inject: {service}
|
inject: {service}
|
||||||
} = Ember;
|
} = Ember;
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ export default Mixin.create({
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
let paginationSettings = this.get('paginationSettings');
|
let paginationSettings = this.get('paginationSettings');
|
||||||
let settings = Ember.$.extend({}, defaultPaginationSettings, paginationSettings);
|
let settings = Ember.assign({}, defaultPaginationSettings, paginationSettings);
|
||||||
|
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set('paginationSettings', settings);
|
this.set('paginationSettings', settings);
|
||||||
@ -63,7 +64,7 @@ export default Mixin.create({
|
|||||||
let paginationSettings = this.get('paginationSettings');
|
let paginationSettings = this.get('paginationSettings');
|
||||||
let modelName = this.get('paginationModel');
|
let modelName = this.get('paginationModel');
|
||||||
|
|
||||||
paginationSettings.page = 1;
|
this.set('paginationSettings.page', 1);
|
||||||
|
|
||||||
this.set('isLoading', true);
|
this.set('isLoading', true);
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ export default Mixin.create({
|
|||||||
let nextPage = metadata.pagination && metadata.pagination.next;
|
let nextPage = metadata.pagination && metadata.pagination.next;
|
||||||
let paginationSettings = this.get('paginationSettings');
|
let paginationSettings = this.get('paginationSettings');
|
||||||
|
|
||||||
if (nextPage) {
|
if (nextPage && !this.get('isLoading')) {
|
||||||
this.set('isLoading', true);
|
this.set('isLoading', true);
|
||||||
this.set('paginationSettings.page', nextPage);
|
this.set('paginationSettings.page', nextPage);
|
||||||
|
|
||||||
@ -105,6 +106,8 @@ export default Mixin.create({
|
|||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.set('isLoading', false);
|
this.set('isLoading', false);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return RSVP.resolve([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -3,18 +3,20 @@ import DS from 'ember-data';
|
|||||||
import Model from 'ember-data/model';
|
import Model from 'ember-data/model';
|
||||||
import getRequestErrorMessage from 'ghost/utils/ajax';
|
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 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 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 {Mixin, RSVP, isArray} = Ember;
|
||||||
const {Errors} = DS;
|
const {Errors} = DS;
|
||||||
@ -36,17 +38,18 @@ export default Mixin.create({
|
|||||||
// in that case the model will be the class that the ValidationEngine
|
// in that case the model will be the class that the ValidationEngine
|
||||||
// was mixed into, i.e. the controller or Ember Data model.
|
// was mixed into, i.e. the controller or Ember Data model.
|
||||||
validators: {
|
validators: {
|
||||||
post: PostValidator,
|
|
||||||
setup: SetupValidator,
|
|
||||||
signup: SignupValidator,
|
|
||||||
signin: SigninValidator,
|
|
||||||
setting: SettingValidator,
|
|
||||||
reset: ResetValidator,
|
|
||||||
user: UserValidator,
|
|
||||||
tag: TagSettingsValidator,
|
|
||||||
navItem: NavItemValidator,
|
|
||||||
inviteUser: InviteUserValidator,
|
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
|
// 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('slack', {path: 'slack'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route('subscribers', function() {
|
||||||
|
this.route('new');
|
||||||
|
this.route('import');
|
||||||
|
});
|
||||||
|
|
||||||
this.route('error404', {path: '/*path'});
|
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(),
|
notifications: service(),
|
||||||
|
|
||||||
publicAPI: feature('publicAPI'),
|
publicAPI: feature('publicAPI'),
|
||||||
|
subscribers: feature('subscribers'),
|
||||||
|
|
||||||
_settings: null,
|
_settings: null,
|
||||||
|
|
||||||
|
@ -45,3 +45,4 @@
|
|||||||
@import "layouts/error.css";
|
@import "layouts/error.css";
|
||||||
@import "layouts/apps.css";
|
@import "layouts/apps.css";
|
||||||
@import "layouts/packages.css";
|
@import "layouts/packages.css";
|
||||||
|
@import "layouts/subscribers.css";
|
||||||
|
@ -61,10 +61,6 @@
|
|||||||
/* The modal
|
/* The modal
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
.fullscreen-modal .gh-image-uploader {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Modal content
|
/* Modal content
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
@ -143,6 +139,10 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body .gh-image-uploader {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Content Modifiers
|
/* 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;
|
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
|
/* Blue button
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
@ -87,9 +87,15 @@
|
|||||||
.icon-idea:before {
|
.icon-idea:before {
|
||||||
content: "\e00e";
|
content: "\e00e";
|
||||||
}
|
}
|
||||||
.icon-arrow:before {
|
.icon-arrow:before,
|
||||||
|
.icon-ascending:before,
|
||||||
|
.icon-descending:before {
|
||||||
content: "\e00f";
|
content: "\e00f";
|
||||||
}
|
}
|
||||||
|
.icon-ascending:before {
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
.icon-pen:before {
|
.icon-pen:before {
|
||||||
content: "\e010";
|
content: "\e010";
|
||||||
}
|
}
|
||||||
|
@ -59,3 +59,20 @@ table td,
|
|||||||
.table.plain tbody > tr:nth-child(odd) > th {
|
.table.plain tbody > tr:nth-child(odd) > th {
|
||||||
background: transparent;
|
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><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>{{#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>}}
|
{{!<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>
|
</ul>
|
||||||
{{#if (gh-user-can-admin session.user)}}
|
{{#if (gh-user-can-admin session.user)}}
|
||||||
<ul class="gh-nav-list gh-nav-settings">
|
<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"}}
|
{{#gh-feature-flag "publicAPI"}}
|
||||||
Public API - For full instructions, read the <a href="http://support.ghost.org/public-api-beta/">developer guide</a>.
|
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}}
|
||||||
|
{{#gh-feature-flag "subscribers"}}
|
||||||
|
Subscribers - Allow visitors to subscribe to e-mail updates of your new posts
|
||||||
|
{{/gh-feature-flag}}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</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.use('tether', ['fade', {duration: 150}], ['fade', {duration: 150}]),
|
||||||
this.reverse('tether', ['fade', {duration: 80}], ['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;
|
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
|
// Used in API request fail handlers to parse a standard api error
|
||||||
// response json for the message to display
|
// response json for the message to display
|
||||||
export default function getRequestErrorMessage(request, performConcat) {
|
export default function getRequestErrorMessage(request, performConcat) {
|
||||||
@ -20,12 +23,12 @@ export default function getRequestErrorMessage(request, performConcat) {
|
|||||||
if (request.status !== 200) {
|
if (request.status !== 200) {
|
||||||
try {
|
try {
|
||||||
// Try to parse out the error, or default to 'Unknown'
|
// Try to parse out the error, or default to 'Unknown'
|
||||||
if (request.responseJSON.errors && isArray(request.responseJSON.errors)) {
|
if (request.errors && isArray(request.errors)) {
|
||||||
message = request.responseJSON.errors.map((errorItem) => {
|
message = request.errors.map((errorItem) => {
|
||||||
return errorItem.message;
|
return errorItem.message;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message = request.responseJSON.error || 'Unknown Error';
|
message = request.error || 'Unknown Error';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msgDetail = request.status ? `${request.status} - ${request.statusText}` : 'Server was not available';
|
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-data-filter": "1.13.0",
|
||||||
"ember-export-application-global": "1.0.5",
|
"ember-export-application-global": "1.0.5",
|
||||||
"ember-invoke-action": "1.3.0",
|
"ember-invoke-action": "1.3.0",
|
||||||
|
"ember-light-table": "0.1.9",
|
||||||
"ember-load-initializers": "0.5.1",
|
"ember-load-initializers": "0.5.1",
|
||||||
"ember-myth": "0.1.1",
|
"ember-myth": "0.1.1",
|
||||||
"ember-one-way-controls": "0.6.2",
|
"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 */
|
/* jshint expr:true */
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import {
|
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.
|
// from a theme, an app, or from an external app, you'll use the Ghost JSON API to do so.
|
||||||
|
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
|
Promise = require('bluebird'),
|
||||||
config = require('../config'),
|
config = require('../config'),
|
||||||
// Include Endpoints
|
// Include Endpoints
|
||||||
configuration = require('./configuration'),
|
configuration = require('./configuration'),
|
||||||
@ -19,6 +20,7 @@ var _ = require('lodash'),
|
|||||||
themes = require('./themes'),
|
themes = require('./themes'),
|
||||||
users = require('./users'),
|
users = require('./users'),
|
||||||
slugs = require('./slugs'),
|
slugs = require('./slugs'),
|
||||||
|
subscribers = require('./subscribers'),
|
||||||
authentication = require('./authentication'),
|
authentication = require('./authentication'),
|
||||||
uploads = require('./upload'),
|
uploads = require('./upload'),
|
||||||
exporter = require('../data/export'),
|
exporter = require('../data/export'),
|
||||||
@ -28,7 +30,8 @@ var _ = require('lodash'),
|
|||||||
addHeaders,
|
addHeaders,
|
||||||
cacheInvalidationHeader,
|
cacheInvalidationHeader,
|
||||||
locationHeader,
|
locationHeader,
|
||||||
contentDispositionHeader,
|
contentDispositionHeaderExport,
|
||||||
|
contentDispositionHeaderSubscribers,
|
||||||
init;
|
init;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -138,12 +141,18 @@ locationHeader = function locationHeader(req, result) {
|
|||||||
* @see http://tools.ietf.org/html/rfc598
|
* @see http://tools.ietf.org/html/rfc598
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
contentDispositionHeader = function contentDispositionHeader() {
|
|
||||||
|
contentDispositionHeaderExport = function contentDispositionHeaderExport() {
|
||||||
return exporter.fileName().then(function then(filename) {
|
return exporter.fileName().then(function then(filename) {
|
||||||
return 'Attachment; filename="' + 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) {
|
addHeaders = function addHeaders(apiMethod, req, res, result) {
|
||||||
var cacheInvalidation,
|
var cacheInvalidation,
|
||||||
location,
|
location,
|
||||||
@ -164,15 +173,24 @@ addHeaders = function addHeaders(apiMethod, req, res, result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Export Content-Disposition Header
|
||||||
if (apiMethod === db.exportContent) {
|
if (apiMethod === db.exportContent) {
|
||||||
contentDisposition = contentDispositionHeader()
|
contentDisposition = contentDispositionHeaderExport()
|
||||||
.then(function addContentDispositionHeader(header) {
|
.then(function addContentDispositionHeaderExport(header) {
|
||||||
// Add Content-Disposition Header
|
res.set({
|
||||||
if (apiMethod === db.exportContent) {
|
'Content-Disposition': 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,
|
var object = req.body,
|
||||||
options = _.extend({}, req.file, req.query, req.params, {
|
options = _.extend({}, req.file, req.query, req.params, {
|
||||||
context: {
|
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') {
|
if (req.method === 'DELETE') {
|
||||||
return res.status(204).end();
|
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
|
// Send a properly formatting HTTP response containing the data with correct headers
|
||||||
res.json(response || {});
|
res.json(response || {});
|
||||||
}).catch(function onAPIError(error) {
|
}).catch(function onAPIError(error) {
|
||||||
@ -243,6 +264,7 @@ module.exports = {
|
|||||||
themes: themes,
|
themes: themes,
|
||||||
users: users,
|
users: users,
|
||||||
slugs: slugs,
|
slugs: slugs,
|
||||||
|
subscribers: subscribers,
|
||||||
authentication: authentication,
|
authentication: authentication,
|
||||||
uploads: uploads,
|
uploads: uploads,
|
||||||
slack: slack
|
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',
|
private: 'private',
|
||||||
subscribe: 'subscribe'
|
subscribe: 'subscribe'
|
||||||
},
|
},
|
||||||
internalApps: ['private-blogging'],
|
internalApps: ['private-blogging', 'subscribers'],
|
||||||
slugs: {
|
slugs: {
|
||||||
// Used by generateSlug to generate slugs for posts, tags, users, ..
|
// Used by generateSlug to generate slugs for posts, tags, users, ..
|
||||||
// reserved slugs are reserved but can be extended/removed by apps
|
// 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'),
|
require('./02-add-visibility-column-to-key-tables'),
|
||||||
// Add mobiledoc column to posts
|
// Add mobiledoc column to posts
|
||||||
require('./03-add-mobiledoc-column-to-posts'),
|
require('./03-add-mobiledoc-column-to-posts'),
|
||||||
// Add social media columns to isers
|
// Add social media columns to users
|
||||||
require('./04-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
|
// Update the permissions & permissions_roles tables to add entries for clients
|
||||||
var utils = require('../utils');
|
var utils = require('../utils'),
|
||||||
|
resource = 'client';
|
||||||
|
|
||||||
function getClientPermissions() {
|
function getPermissions() {
|
||||||
return utils.findModelFixtures('Permission', {object_type: 'client'});
|
return utils.findModelFixtures('Permission', {object_type: resource});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientRelations() {
|
function getRelations() {
|
||||||
return utils.findPermissionRelationsForObject('client');
|
return utils.findPermissionRelationsForObject(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
function printResult(logger, result, message) {
|
function printResult(logger, result, message) {
|
||||||
@ -18,13 +19,13 @@ function printResult(logger, result, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function addClientPermissions(options, logger) {
|
module.exports = function addClientPermissions(options, logger) {
|
||||||
var modelToAdd = getClientPermissions(),
|
var modelToAdd = getPermissions(),
|
||||||
relationToAdd = getClientRelations();
|
relationToAdd = getRelations();
|
||||||
|
|
||||||
return utils.addFixturesForModel(modelToAdd).then(function (result) {
|
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);
|
return utils.addFixturesForRelation(relationToAdd);
|
||||||
}).then(function (result) {
|
}).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
|
// add ghost-scheduler client
|
||||||
require('./02-add-ghost-scheduler-client'),
|
require('./02-add-ghost-scheduler-client'),
|
||||||
// add client permissions and permission_role relations
|
// 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",
|
"name": "Delete clients",
|
||||||
"action_type": "destroy",
|
"action_type": "destroy",
|
||||||
"object_type": "client"
|
"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",
|
"theme": "all",
|
||||||
"user": "all",
|
"user": "all",
|
||||||
"role": "all",
|
"role": "all",
|
||||||
"client": "all"
|
"client": "all",
|
||||||
|
"subscriber": "all"
|
||||||
},
|
},
|
||||||
"Editor": {
|
"Editor": {
|
||||||
"post": "all",
|
"post": "all",
|
||||||
@ -286,7 +312,8 @@
|
|||||||
"tag": "all",
|
"tag": "all",
|
||||||
"user": "all",
|
"user": "all",
|
||||||
"role": "all",
|
"role": "all",
|
||||||
"client": "all"
|
"client": "all",
|
||||||
|
"subscriber": ["add"]
|
||||||
},
|
},
|
||||||
"Author": {
|
"Author": {
|
||||||
"post": ["browse", "read", "add"],
|
"post": ["browse", "read", "add"],
|
||||||
@ -295,7 +322,8 @@
|
|||||||
"tag": ["browse", "read", "add"],
|
"tag": ["browse", "read", "add"],
|
||||||
"user": ["browse", "read"],
|
"user": ["browse", "read"],
|
||||||
"role": ["browse"],
|
"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'},
|
user_id: {type: 'integer', nullable: false, unsigned: true, references: 'users.id'},
|
||||||
client_id: {type: 'integer', nullable: false, unsigned: true, references: 'clients.id'},
|
client_id: {type: 'integer', nullable: false, unsigned: true, references: 'clients.id'},
|
||||||
expires: {type: 'bigInteger', nullable: false}
|
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');
|
context = i18n.t('errors.errors.databaseIsReadOnly');
|
||||||
help = i18n.t('errors.errors.checkDatabase');
|
help = i18n.t('errors.errors.checkDatabase');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Logging framework hookup
|
// TODO: Logging framework hookup
|
||||||
// Eventually we'll have better logging which will know about envs
|
// Eventually we'll have better logging which will know about envs
|
||||||
if ((process.env.NODE_ENV === 'development' ||
|
if ((process.env.NODE_ENV === 'development' ||
|
||||||
|
@ -38,6 +38,7 @@ coreHelpers.url = require('./url');
|
|||||||
|
|
||||||
// Specialist helpers for certain templates
|
// Specialist helpers for certain templates
|
||||||
coreHelpers.input_password = require('./input_password');
|
coreHelpers.input_password = require('./input_password');
|
||||||
|
coreHelpers.input_email = require('./input_email');
|
||||||
coreHelpers.page_url = require('./page_url');
|
coreHelpers.page_url = require('./page_url');
|
||||||
coreHelpers.pageUrl = require('./page_url').deprecated;
|
coreHelpers.pageUrl = require('./page_url').deprecated;
|
||||||
|
|
||||||
@ -97,6 +98,7 @@ registerHelpers = function (adminHbs) {
|
|||||||
registerThemeHelper('has', coreHelpers.has);
|
registerThemeHelper('has', coreHelpers.has);
|
||||||
registerThemeHelper('is', coreHelpers.is);
|
registerThemeHelper('is', coreHelpers.is);
|
||||||
registerThemeHelper('image', coreHelpers.image);
|
registerThemeHelper('image', coreHelpers.image);
|
||||||
|
registerThemeHelper('input_email', coreHelpers.input_email);
|
||||||
registerThemeHelper('input_password', coreHelpers.input_password);
|
registerThemeHelper('input_password', coreHelpers.input_password);
|
||||||
registerThemeHelper('meta_description', coreHelpers.meta_description);
|
registerThemeHelper('meta_description', coreHelpers.meta_description);
|
||||||
registerThemeHelper('meta_title', coreHelpers.meta_title);
|
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)) {
|
} else if (isBearerAutorizationHeader(req)) {
|
||||||
return errors.handleAPIError(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied')), req, res, next);
|
return errors.handleAPIError(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied')), req, res, next);
|
||||||
} else if (req.client) {
|
} else if (req.client) {
|
||||||
|
req.user = {id: 0};
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ auth = {
|
|||||||
// Workaround for missing permissions
|
// Workaround for missing permissions
|
||||||
// TODO: rework when https://github.com/TryGhost/Ghost/issues/3911 is done
|
// TODO: rework when https://github.com/TryGhost/Ghost/issues/3911 is done
|
||||||
requiresAuthorizedUser: function requiresAuthorizedUser(req, res, next) {
|
requiresAuthorizedUser: function requiresAuthorizedUser(req, res, next) {
|
||||||
if (req.user) {
|
if (req.user && req.user.id) {
|
||||||
return next();
|
return next();
|
||||||
} else {
|
} else {
|
||||||
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
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) {
|
if (labs.isSet('publicAPI') === true) {
|
||||||
return next();
|
return next();
|
||||||
} else {
|
} else {
|
||||||
if (req.user) {
|
if (req.user && req.user.id) {
|
||||||
return next();
|
return next();
|
||||||
} else {
|
} else {
|
||||||
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
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'),
|
uncapitalise = require('./uncapitalise'),
|
||||||
cors = require('./cors'),
|
cors = require('./cors'),
|
||||||
netjet = require('netjet'),
|
netjet = require('netjet'),
|
||||||
|
labs = require('./labs'),
|
||||||
|
|
||||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||||
@ -44,7 +45,8 @@ middleware = {
|
|||||||
requiresAuthorizedUser: auth.requiresAuthorizedUser,
|
requiresAuthorizedUser: auth.requiresAuthorizedUser,
|
||||||
requiresAuthorizedUserPublicAPI: auth.requiresAuthorizedUserPublicAPI,
|
requiresAuthorizedUserPublicAPI: auth.requiresAuthorizedUserPublicAPI,
|
||||||
errorHandler: errors.handleAPIError,
|
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)
|
// Setup handlebars for the current context (admin or theme)
|
||||||
configHbsForContext: function configHbsForContext(req, res, next) {
|
configHbsForContext: function configHbsForContext(req, res, next) {
|
||||||
var themeData = _.cloneDeep(config.theme),
|
var themeData = _.cloneDeep(config.theme),
|
||||||
|
labsData = _.cloneDeep(config.labs),
|
||||||
blogApp = req.app;
|
blogApp = req.app;
|
||||||
|
|
||||||
if (req.secure && config.urlSSL) {
|
if (req.secure && config.urlSSL) {
|
||||||
@ -38,7 +39,7 @@ themeHandler = {
|
|||||||
themeData.posts_per_page = themeData.postsPerPage;
|
themeData.posts_per_page = themeData.postsPerPage;
|
||||||
delete themeData.postsPerPage;
|
delete themeData.postsPerPage;
|
||||||
|
|
||||||
hbs.updateTemplateOptions({data: {blog: themeData}});
|
hbs.updateTemplateOptions({data: {blog: themeData, labs: labsData}});
|
||||||
|
|
||||||
if (config.paths.themePath && blogApp.get('activeTheme')) {
|
if (config.paths.themePath && blogApp.get('activeTheme')) {
|
||||||
blogApp.set('views', path.join(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
|
// Get the user from the options object
|
||||||
contextUser: function contextUser(options) {
|
contextUser: function contextUser(options) {
|
||||||
// Default to context user
|
// 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;
|
return options.context.user;
|
||||||
// Other wise use the internal override
|
// Other wise use the internal override
|
||||||
} else if (options.context && options.context.internal) {
|
} else if (options.context && options.context.internal) {
|
||||||
return 1;
|
return 1;
|
||||||
|
} else if (options.context && options.context.external) {
|
||||||
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
errors.logAndThrowError(new Error(i18n.t('errors.models.base.index.missingContext')));
|
errors.logAndThrowError(new Error(i18n.t('errors.models.base.index.missingContext')));
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ models = [
|
|||||||
'refreshtoken',
|
'refreshtoken',
|
||||||
'role',
|
'role',
|
||||||
'settings',
|
'settings',
|
||||||
|
'subscriber',
|
||||||
'tag',
|
'tag',
|
||||||
'user'
|
'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) {
|
function parseContext(context) {
|
||||||
// Parse what's passed to canThis.beginCheck for standard user and app scopes
|
// Parse what's passed to canThis.beginCheck for standard user and app scopes
|
||||||
var parsed = {
|
var parsed = {
|
||||||
internal: false,
|
internal: false,
|
||||||
user: null,
|
external: false,
|
||||||
app: null,
|
user: null,
|
||||||
public: true
|
app: null,
|
||||||
};
|
public: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (context && (context === 'external' || context.external)) {
|
||||||
|
parsed.external = true;
|
||||||
|
parsed.public = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (context && (context === 'internal' || context.internal)) {
|
if (context && (context === 'internal' || context.internal)) {
|
||||||
parsed.internal = true;
|
parsed.internal = true;
|
||||||
@ -117,8 +123,10 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
|
|||||||
role: Models.Role,
|
role: Models.Role,
|
||||||
user: Models.User,
|
user: Models.User,
|
||||||
permission: Models.Permission,
|
permission: Models.Permission,
|
||||||
setting: Models.Settings
|
setting: Models.Settings,
|
||||||
|
subscriber: Models.Subscriber
|
||||||
};
|
};
|
||||||
|
|
||||||
// Iterate through the object types, i.e. ['post', 'tag', 'user']
|
// Iterate through the object types, i.e. ['post', 'tag', 'user']
|
||||||
return _.reduce(objTypes, function (objTypeHandlers, objType) {
|
return _.reduce(objTypes, function (objTypeHandlers, objType) {
|
||||||
// Grab the TargetModel through the objectTypeModelMap
|
// 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.put('/tags/:id', authenticatePrivate, api.http(api.tags.edit));
|
||||||
router.del('/tags/:id', authenticatePrivate, api.http(api.tags.destroy));
|
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
|
// ## Roles
|
||||||
router.get('/roles/', authenticatePrivate, api.http(api.roles.browse));
|
router.get('/roles/', authenticatePrivate, api.http(api.roles.browse));
|
||||||
|
|
||||||
|
@ -197,6 +197,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
"subscriber": {
|
||||||
|
"notEnoughPermission": "You do not have permission to perform this action"
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"untitled": "(Untitled)",
|
"untitled": "(Untitled)",
|
||||||
"valueCannotBeBlank": "Value in {key} cannot be blank.",
|
"valueCannotBeBlank": "Value in {key} cannot be blank.",
|
||||||
@ -333,6 +336,10 @@
|
|||||||
"tags": {
|
"tags": {
|
||||||
"tagNotFound": "Tag not found."
|
"tagNotFound": "Tag not found."
|
||||||
},
|
},
|
||||||
|
"subscribers": {
|
||||||
|
"subscriberNotFound": "Subscriber not found.",
|
||||||
|
"subscriberAlreadyExist": "Email already exist."
|
||||||
|
},
|
||||||
"themes": {
|
"themes": {
|
||||||
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
|
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
|
||||||
"noPermissionToEditThemes": "You do not have permission to edit themes.",
|
"noPermissionToEditThemes": "You do not have permission to edit themes.",
|
||||||
@ -447,6 +454,9 @@
|
|||||||
"unableToSendEmail": "Ghost is currently unable to send email."
|
"unableToSendEmail": "Ghost is currently unable to send email."
|
||||||
},
|
},
|
||||||
"helpers": {
|
"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": {
|
"foreach": {
|
||||||
"iteratorNeeded": "Need to pass an iterator to #foreach"
|
"iteratorNeeded": "Need to pass an iterator to #foreach"
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
var unidecode = require('unidecode'),
|
var unidecode = require('unidecode'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
|
readCSV = require('./read-csv'),
|
||||||
|
|
||||||
utils,
|
utils,
|
||||||
getRandomInt;
|
getRandomInt;
|
||||||
@ -99,7 +100,9 @@ utils = {
|
|||||||
/*jslint unparam:true*/
|
/*jslint unparam:true*/
|
||||||
res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S});
|
res.set({'Cache-Control': 'public, max-age=' + utils.ONE_YEAR_S});
|
||||||
res.redirect(301, path);
|
res.redirect(301, path);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
readCSV: readCSV
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = utils;
|
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[33].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
|
||||||
permissions[34].name.should.eql('Delete clients');
|
permissions[34].name.should.eql('Delete clients');
|
||||||
permissions[34].should.be.AssignedToRoles(['Administrator', 'Editor', 'Author']);
|
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 () {
|
describe('Populate', function () {
|
||||||
@ -195,7 +209,7 @@ describe('Database Migration (special functions)', function () {
|
|||||||
result.roles.at(3).get('name').should.eql('Owner');
|
result.roles.at(3).get('name').should.eql('Owner');
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
result.permissions.length.should.eql(35);
|
result.permissions.length.should.eql(40);
|
||||||
result.permissions.toJSON().should.be.CompletePermissions();
|
result.permissions.toJSON().should.be.CompletePermissions();
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
@ -442,7 +442,7 @@ describe('API Utils', function () {
|
|||||||
describe('handlePublicPermissions', function () {
|
describe('handlePublicPermissions', function () {
|
||||||
it('should return empty options if passed empty options', function (done) {
|
it('should return empty options if passed empty options', function (done) {
|
||||||
apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
|
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();
|
done();
|
||||||
}).catch(done);
|
}).catch(done);
|
||||||
});
|
});
|
||||||
@ -451,7 +451,7 @@ describe('API Utils', function () {
|
|||||||
var aPPStub = sandbox.stub(apiUtils, 'applyPublicPermissions').returns(Promise.resolve({}));
|
var aPPStub = sandbox.stub(apiUtils, 'applyPublicPermissions').returns(Promise.resolve({}));
|
||||||
apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
|
apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
|
||||||
aPPStub.calledOnce.should.eql(true);
|
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();
|
done();
|
||||||
}).catch(done);
|
}).catch(done);
|
||||||
});
|
});
|
||||||
@ -467,7 +467,7 @@ describe('API Utils', function () {
|
|||||||
apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function (options) {
|
apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function (options) {
|
||||||
cTStub.calledOnce.should.eql(true);
|
cTStub.calledOnce.should.eql(true);
|
||||||
cTMethodStub.test.test.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();
|
done();
|
||||||
}).catch(done);
|
}).catch(done);
|
||||||
});
|
});
|
||||||
|
@ -132,10 +132,12 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('01-move-jquery-with-alert', function () {
|
describe('01-move-jquery-with-alert', function () {
|
||||||
|
var moveJquery = fixtures004[0];
|
||||||
|
|
||||||
it('tries to move jQuery to ghost_foot', function (done) {
|
it('tries to move jQuery to ghost_foot', function (done) {
|
||||||
getObjStub.get.returns('');
|
getObjStub.get.returns('');
|
||||||
|
|
||||||
fixtures004[0]({}, loggerStub).then(function () {
|
moveJquery({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||||
settingsEditStub.calledOnce.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'
|
+ '<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.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||||
settingsEditStub.calledOnce.should.be.false();
|
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) {
|
it('does not move jQuery to ghost_foot if the setting is missing', function (done) {
|
||||||
settingsOneStub.returns(Promise.resolve());
|
settingsOneStub.returns(Promise.resolve());
|
||||||
|
|
||||||
fixtures004[0]({}, loggerStub).then(function () {
|
moveJquery({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||||
settingsEditStub.called.should.be.false();
|
settingsEditStub.called.should.be.false();
|
||||||
@ -182,7 +184,7 @@ describe('Fixtures', function () {
|
|||||||
configUtils.set({privacy: {useGoogleFonts: false}});
|
configUtils.set({privacy: {useGoogleFonts: false}});
|
||||||
getObjStub.get.returns('');
|
getObjStub.get.returns('');
|
||||||
|
|
||||||
fixtures004[0]({}, loggerStub).then(function () {
|
moveJquery({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
settingsOneStub.calledWith('ghost_foot').should.be.true();
|
||||||
settingsEditStub.calledOnce.should.be.true();
|
settingsEditStub.calledOnce.should.be.true();
|
||||||
@ -196,8 +198,10 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('02-update-private-setting-type', function () {
|
describe('02-update-private-setting-type', function () {
|
||||||
|
var updateSettingType = fixtures004[1];
|
||||||
|
|
||||||
it('tries to update setting type correctly', function (done) {
|
it('tries to update setting type correctly', function (done) {
|
||||||
fixtures004[1]({}, loggerStub).then(function () {
|
updateSettingType({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('isPrivate').should.be.true();
|
settingsOneStub.calledWith('isPrivate').should.be.true();
|
||||||
getObjStub.get.calledOnce.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) {
|
it('does not try to update setting type if it is already set', function (done) {
|
||||||
getObjStub.get.returns('private');
|
getObjStub.get.returns('private');
|
||||||
|
|
||||||
fixtures004[1]({}, loggerStub).then(function () {
|
updateSettingType({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('isPrivate').should.be.true();
|
settingsOneStub.calledWith('isPrivate').should.be.true();
|
||||||
getObjStub.get.calledOnce.should.be.true();
|
getObjStub.get.calledOnce.should.be.true();
|
||||||
@ -233,8 +237,10 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('03-update-password-setting-type', function () {
|
describe('03-update-password-setting-type', function () {
|
||||||
|
var updateSettingType = fixtures004[2];
|
||||||
|
|
||||||
it('tries to update setting type correctly', function (done) {
|
it('tries to update setting type correctly', function (done) {
|
||||||
fixtures004[2]({}, loggerStub).then(function () {
|
updateSettingType({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('password').should.be.true();
|
settingsOneStub.calledWith('password').should.be.true();
|
||||||
settingsEditStub.calledOnce.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) {
|
it('does not try to update setting type if it is already set', function (done) {
|
||||||
getObjStub.get.returns('private');
|
getObjStub.get.returns('private');
|
||||||
|
|
||||||
fixtures004[2]({}, loggerStub).then(function () {
|
updateSettingType({}, loggerStub).then(function () {
|
||||||
settingsOneStub.calledOnce.should.be.true();
|
settingsOneStub.calledOnce.should.be.true();
|
||||||
settingsOneStub.calledWith('password').should.be.true();
|
settingsOneStub.calledWith('password').should.be.true();
|
||||||
getObjStub.get.calledOnce.should.be.true();
|
getObjStub.get.calledOnce.should.be.true();
|
||||||
@ -268,8 +274,10 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('04-update-ghost-admin-client', function () {
|
describe('04-update-ghost-admin-client', function () {
|
||||||
|
var updateClient = fixtures004[3];
|
||||||
|
|
||||||
it('tries to update client correctly', function (done) {
|
it('tries to update client correctly', function (done) {
|
||||||
fixtures004[3]({}, loggerStub).then(function () {
|
updateClient({}, loggerStub).then(function () {
|
||||||
clientOneStub.calledOnce.should.be.true();
|
clientOneStub.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||||
getObjStub.get.calledTwice.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('secret').returns('abc');
|
||||||
getObjStub.get.withArgs('status').returns('enabled');
|
getObjStub.get.withArgs('status').returns('enabled');
|
||||||
|
|
||||||
fixtures004[3]({}, loggerStub).then(function () {
|
updateClient({}, loggerStub).then(function () {
|
||||||
clientOneStub.calledOnce.should.be.true();
|
clientOneStub.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||||
getObjStub.get.calledTwice.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('secret').returns('abc');
|
||||||
getObjStub.get.withArgs('status').returns('development');
|
getObjStub.get.withArgs('status').returns('development');
|
||||||
|
|
||||||
fixtures004[3]({}, loggerStub).then(function () {
|
updateClient({}, loggerStub).then(function () {
|
||||||
clientOneStub.calledOnce.should.be.true();
|
clientOneStub.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||||
getObjStub.get.calledTwice.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('secret').returns('not_available');
|
||||||
getObjStub.get.withArgs('status').returns('enabled');
|
getObjStub.get.withArgs('status').returns('enabled');
|
||||||
|
|
||||||
fixtures004[3]({}, loggerStub).then(function () {
|
updateClient({}, loggerStub).then(function () {
|
||||||
clientOneStub.calledOnce.should.be.true();
|
clientOneStub.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true();
|
||||||
getObjStub.get.calledOnce.should.be.true();
|
getObjStub.get.calledOnce.should.be.true();
|
||||||
@ -350,11 +358,13 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('05-add-ghost-frontend-client', function () {
|
describe('05-add-ghost-frontend-client', function () {
|
||||||
|
var addClient = fixtures004[4];
|
||||||
|
|
||||||
it('tries to add client correctly', function (done) {
|
it('tries to add client correctly', function (done) {
|
||||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
||||||
clientOneStub.returns(Promise.resolve());
|
clientOneStub.returns(Promise.resolve());
|
||||||
|
|
||||||
fixtures004[4]({}, loggerStub).then(function () {
|
addClient({}, loggerStub).then(function () {
|
||||||
clientOneStub.calledOnce.should.be.true();
|
clientOneStub.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true();
|
||||||
clientAddStub.calledOnce.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) {
|
it('does not try to add client if it already exists', function (done) {
|
||||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
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.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true();
|
||||||
clientAddStub.called.should.be.false();
|
clientAddStub.called.should.be.false();
|
||||||
@ -382,7 +392,8 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('06-clean-broken-tags', function () {
|
describe('06-clean-broken-tags', function () {
|
||||||
var tagObjStub, tagCollStub, tagAllStub;
|
var tagObjStub, tagCollStub, tagAllStub,
|
||||||
|
cleanBrokenTags = fixtures004[5];
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
tagObjStub = {
|
tagObjStub = {
|
||||||
@ -396,7 +407,7 @@ describe('Fixtures', function () {
|
|||||||
it('tries to clean broken tags correctly', function (done) {
|
it('tries to clean broken tags correctly', function (done) {
|
||||||
tagObjStub.get.returns(',hello');
|
tagObjStub.get.returns(',hello');
|
||||||
|
|
||||||
fixtures004[5]({}, loggerStub).then(function () {
|
cleanBrokenTags({}, loggerStub).then(function () {
|
||||||
tagAllStub.calledOnce.should.be.true();
|
tagAllStub.calledOnce.should.be.true();
|
||||||
tagCollStub.each.calledOnce.should.be.true();
|
tagCollStub.each.calledOnce.should.be.true();
|
||||||
tagObjStub.get.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) {
|
it('tries can handle tags which end up empty', function (done) {
|
||||||
tagObjStub.get.returns(',');
|
tagObjStub.get.returns(',');
|
||||||
|
|
||||||
fixtures004[5]({}, loggerStub).then(function () {
|
cleanBrokenTags({}, loggerStub).then(function () {
|
||||||
tagAllStub.calledOnce.should.be.true();
|
tagAllStub.calledOnce.should.be.true();
|
||||||
tagCollStub.each.calledOnce.should.be.true();
|
tagCollStub.each.calledOnce.should.be.true();
|
||||||
tagObjStub.get.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) {
|
it('does not change tags if not necessary', function (done) {
|
||||||
tagObjStub.get.returns('hello');
|
tagObjStub.get.returns('hello');
|
||||||
|
|
||||||
fixtures004[5]({}, loggerStub).then(function () {
|
cleanBrokenTags({}, loggerStub).then(function () {
|
||||||
tagAllStub.calledOnce.should.be.true();
|
tagAllStub.calledOnce.should.be.true();
|
||||||
tagCollStub.each.calledOnce.should.be.true();
|
tagCollStub.each.calledOnce.should.be.true();
|
||||||
tagObjStub.get.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) {
|
it('does nothing if there are no tags', function (done) {
|
||||||
tagAllStub.returns(Promise.resolve());
|
tagAllStub.returns(Promise.resolve());
|
||||||
|
|
||||||
fixtures004[5]({}, loggerStub).then(function () {
|
cleanBrokenTags({}, loggerStub).then(function () {
|
||||||
tagAllStub.calledOnce.should.be.true();
|
tagAllStub.calledOnce.should.be.true();
|
||||||
tagCollStub.each.called.should.be.false();
|
tagCollStub.each.called.should.be.false();
|
||||||
tagObjStub.get.called.should.be.false();
|
tagObjStub.get.called.should.be.false();
|
||||||
@ -464,7 +475,8 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('07-add-post-tag-order', 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 () {
|
beforeEach(function () {
|
||||||
tagOp1Stub = sandbox.stub().returns(Promise.resolve());
|
tagOp1Stub = sandbox.stub().returns(Promise.resolve());
|
||||||
@ -489,7 +501,7 @@ describe('Fixtures', function () {
|
|||||||
it('calls load on each post', function (done) {
|
it('calls load on each post', function (done) {
|
||||||
// Fake mapThen behaviour
|
// Fake mapThen behaviour
|
||||||
postCollStub.mapThen.callsArgWith(0, postObjStub).returns([]);
|
postCollStub.mapThen.callsArgWith(0, postObjStub).returns([]);
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
postAllStub.calledOnce.should.be.true();
|
postAllStub.calledOnce.should.be.true();
|
||||||
postCollStub.mapThen.calledOnce.should.be.true();
|
postCollStub.mapThen.calledOnce.should.be.true();
|
||||||
postObjStub.load.calledOnce.should.be.true();
|
postObjStub.load.calledOnce.should.be.true();
|
||||||
@ -508,7 +520,7 @@ describe('Fixtures', function () {
|
|||||||
postCollStub.mapThen.returns([]);
|
postCollStub.mapThen.returns([]);
|
||||||
postAllStub.returns(Promise.resolve());
|
postAllStub.returns(Promise.resolve());
|
||||||
|
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
loggerStub.info.calledOnce.should.be.true();
|
loggerStub.info.calledOnce.should.be.true();
|
||||||
loggerStub.warn.calledOnce.should.be.true();
|
loggerStub.warn.calledOnce.should.be.true();
|
||||||
postAllStub.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
|
// By returning from mapThen, we can skip doing tag.load in this test
|
||||||
postCollStub.mapThen.returns(postObjStub);
|
postCollStub.mapThen.returns(postObjStub);
|
||||||
|
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
loggerStub.info.calledThrice.should.be.true();
|
loggerStub.info.calledThrice.should.be.true();
|
||||||
loggerStub.warn.called.should.be.false();
|
loggerStub.warn.called.should.be.false();
|
||||||
postAllStub.calledOnce.should.be.true();
|
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
|
// By returning from mapThen, we can skip doing tag.load in this test
|
||||||
postCollStub.mapThen.returns(postObjStub);
|
postCollStub.mapThen.returns(postObjStub);
|
||||||
|
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
loggerStub.info.calledThrice.should.be.true();
|
loggerStub.info.calledThrice.should.be.true();
|
||||||
loggerStub.warn.called.should.be.false();
|
loggerStub.warn.called.should.be.false();
|
||||||
postAllStub.calledOnce.should.be.true();
|
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
|
// By returning from mapThen, we can skip doing tag.load in this test
|
||||||
postCollStub.mapThen.returns([postObjStub]);
|
postCollStub.mapThen.returns([postObjStub]);
|
||||||
|
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
loggerStub.info.calledOnce.should.be.true();
|
loggerStub.info.calledOnce.should.be.true();
|
||||||
loggerStub.warn.calledOnce.should.be.true();
|
loggerStub.warn.calledOnce.should.be.true();
|
||||||
postAllStub.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
|
// By returning from mapThen, we can skip doing tag.load in this test
|
||||||
postCollStub.mapThen.returns([postObjStub]);
|
postCollStub.mapThen.returns([postObjStub]);
|
||||||
|
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
loggerStub.info.calledThrice.should.be.true();
|
loggerStub.info.calledThrice.should.be.true();
|
||||||
loggerStub.warn.called.should.be.false();
|
loggerStub.warn.called.should.be.false();
|
||||||
postAllStub.calledOnce.should.be.true();
|
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
|
// By returning from mapThen, we can skip doing tag.load in this test
|
||||||
postCollStub.mapThen.returns([postObjStub]);
|
postCollStub.mapThen.returns([postObjStub]);
|
||||||
|
|
||||||
fixtures004[6]({}, loggerStub).then(function () {
|
addPostTagOrder({}, loggerStub).then(function () {
|
||||||
loggerStub.info.calledThrice.should.be.true();
|
loggerStub.info.calledThrice.should.be.true();
|
||||||
loggerStub.warn.called.should.be.false();
|
loggerStub.warn.called.should.be.false();
|
||||||
postAllStub.calledOnce.should.be.true();
|
postAllStub.calledOnce.should.be.true();
|
||||||
@ -656,7 +668,8 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('08-add-post-fixture', function () {
|
describe('08-add-post-fixture', function () {
|
||||||
var postOneStub, postAddStub;
|
var postOneStub, postAddStub,
|
||||||
|
addPostFixture = fixtures004[7];
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
postOneStub = sandbox.stub(models.Post, 'findOne').returns(Promise.resolve());
|
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) {
|
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();
|
postOneStub.calledOnce.should.be.true();
|
||||||
loggerStub.info.calledOnce.should.be.true();
|
loggerStub.info.calledOnce.should.be.true();
|
||||||
loggerStub.warn.called.should.be.false();
|
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) {
|
it('does not try to add new post fixture if it already exists', function (done) {
|
||||||
postOneStub.returns(Promise.resolve({}));
|
postOneStub.returns(Promise.resolve({}));
|
||||||
|
|
||||||
fixtures004[7]({}, loggerStub).then(function () {
|
addPostFixture({}, loggerStub).then(function () {
|
||||||
postOneStub.calledOnce.should.be.true();
|
postOneStub.calledOnce.should.be.true();
|
||||||
loggerStub.info.called.should.be.false();
|
loggerStub.info.called.should.be.false();
|
||||||
loggerStub.warn.calledOnce.should.be.true();
|
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.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.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][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][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][2].should.be.a.Function().with.property('name', 'addClientPermissions');
|
||||||
|
sequenceStub.secondCall.args[0][3].should.be.a.Function().with.property('name', 'addSubscriberPermissions');
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
sequenceReset();
|
sequenceReset();
|
||||||
@ -733,11 +747,12 @@ describe('Fixtures', function () {
|
|||||||
describe('Tasks:', function () {
|
describe('Tasks:', function () {
|
||||||
it('should have tasks for 005', function () {
|
it('should have tasks for 005', function () {
|
||||||
should.exist(fixtures005);
|
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 () {
|
describe('01-update-ghost-client-secrets', function () {
|
||||||
var queryStub, clientForgeStub, clientEditStub;
|
var queryStub, clientForgeStub, clientEditStub,
|
||||||
|
updateClient = fixtures005[0];
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
queryStub = {
|
queryStub = {
|
||||||
@ -754,7 +769,7 @@ describe('Fixtures', function () {
|
|||||||
queryStub.fetch.returns(new Promise.resolve({models: []}));
|
queryStub.fetch.returns(new Promise.resolve({models: []}));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
fixtures005[0]({}, loggerStub).then(function () {
|
updateClient({}, loggerStub).then(function () {
|
||||||
clientForgeStub.calledOnce.should.be.true();
|
clientForgeStub.calledOnce.should.be.true();
|
||||||
clientEditStub.called.should.be.false();
|
clientEditStub.called.should.be.false();
|
||||||
loggerStub.info.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}]}));
|
queryStub.fetch.returns(new Promise.resolve({models: [{id: 1}]}));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
fixtures005[0]({}, loggerStub).then(function () {
|
updateClient({}, loggerStub).then(function () {
|
||||||
clientForgeStub.calledOnce.should.be.true();
|
clientForgeStub.calledOnce.should.be.true();
|
||||||
clientEditStub.called.should.be.true();
|
clientEditStub.called.should.be.true();
|
||||||
loggerStub.info.calledOnce.should.be.true();
|
loggerStub.info.calledOnce.should.be.true();
|
||||||
@ -779,7 +794,8 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('02-add-ghost-scheduler-client', function () {
|
describe('02-add-ghost-scheduler-client', function () {
|
||||||
var clientOneStub;
|
var clientOneStub,
|
||||||
|
addClient = fixtures005[1];
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve({}));
|
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());
|
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
||||||
clientOneStub.returns(Promise.resolve());
|
clientOneStub.returns(Promise.resolve());
|
||||||
|
|
||||||
fixtures005[1]({}, loggerStub).then(function () {
|
addClient({}, loggerStub).then(function () {
|
||||||
clientOneStub.calledOnce.should.be.true();
|
clientOneStub.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-scheduler'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-scheduler'}).should.be.true();
|
||||||
clientAddStub.calledOnce.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) {
|
it('does not try to add client if it already exists', function (done) {
|
||||||
var clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve());
|
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.calledOnce.should.be.true();
|
||||||
clientOneStub.calledWith({slug: 'ghost-scheduler'}).should.be.true();
|
clientOneStub.calledWith({slug: 'ghost-scheduler'}).should.be.true();
|
||||||
clientAddStub.called.should.be.false();
|
clientAddStub.called.should.be.false();
|
||||||
@ -817,7 +833,8 @@ describe('Fixtures', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('03-add-client-permissions', function () {
|
describe('03-add-client-permissions', function () {
|
||||||
var modelResult, addModelStub, relationResult, addRelationStub;
|
var modelResult, addModelStub, relationResult, addRelationStub,
|
||||||
|
addClientPermissions = fixtures005[2];
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
modelResult = {expected: 1, done: 1};
|
modelResult = {expected: 1, done: 1};
|
||||||
@ -831,7 +848,7 @@ describe('Fixtures', function () {
|
|||||||
|
|
||||||
it('should find the correct model & relation to add', function (done) {
|
it('should find the correct model & relation to add', function (done) {
|
||||||
// Execute
|
// Execute
|
||||||
fixtures005[2]({}, loggerStub).then(function () {
|
addClientPermissions({}, loggerStub).then(function () {
|
||||||
addModelStub.calledOnce.should.be.true();
|
addModelStub.calledOnce.should.be.true();
|
||||||
addModelStub.calledWith(
|
addModelStub.calledWith(
|
||||||
fixtureUtils.findModelFixtures('Permission', {object_type: 'client'})
|
fixtureUtils.findModelFixtures('Permission', {object_type: 'client'})
|
||||||
@ -853,7 +870,7 @@ describe('Fixtures', function () {
|
|||||||
// Setup
|
// Setup
|
||||||
modelResult.expected = 3;
|
modelResult.expected = 3;
|
||||||
// Execute
|
// Execute
|
||||||
fixtures005[2]({}, loggerStub).then(function () {
|
addClientPermissions({}, loggerStub).then(function () {
|
||||||
addModelStub.calledOnce.should.be.true();
|
addModelStub.calledOnce.should.be.true();
|
||||||
addModelStub.calledWith(
|
addModelStub.calledWith(
|
||||||
fixtureUtils.findModelFixtures('Permission', {object_type: 'client'})
|
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();
|
clientOneStub.calledThrice.should.be.true();
|
||||||
clientAddStub.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.called.should.be.true();
|
||||||
permsAddStub.callCount.should.eql(35);
|
permsAddStub.callCount.should.eql(40);
|
||||||
|
|
||||||
permsAllStub.calledOnce.should.be.true();
|
permsAllStub.calledOnce.should.be.true();
|
||||||
rolesAllStub.calledOnce.should.be.true();
|
rolesAllStub.calledOnce.should.be.true();
|
||||||
@ -932,8 +1006,8 @@ describe('Fixtures', function () {
|
|||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
modelMethodStub.filter.called.should.be.true();
|
modelMethodStub.filter.called.should.be.true();
|
||||||
// 25 permissions, 1 tag
|
// 26 permissions, 1 tag
|
||||||
modelMethodStub.filter.callCount.should.eql(25 + 1);
|
modelMethodStub.filter.callCount.should.eql(28 + 1);
|
||||||
modelMethodStub.find.called.should.be.true();
|
modelMethodStub.find.called.should.be.true();
|
||||||
// 3 roles, 1 post
|
// 3 roles, 1 post
|
||||||
modelMethodStub.find.callCount.should.eql(3 + 1);
|
modelMethodStub.find.callCount.should.eql(3 + 1);
|
||||||
|
@ -152,21 +152,21 @@ describe('Utils', function () {
|
|||||||
fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) {
|
fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) {
|
||||||
should.exist(result);
|
should.exist(result);
|
||||||
result.should.be.an.Object();
|
result.should.be.an.Object();
|
||||||
result.should.have.property('expected', 25);
|
result.should.have.property('expected', 28);
|
||||||
result.should.have.property('done', 25);
|
result.should.have.property('done', 28);
|
||||||
|
|
||||||
// Permissions & Roles
|
// Permissions & Roles
|
||||||
permsAllStub.calledOnce.should.be.true();
|
permsAllStub.calledOnce.should.be.true();
|
||||||
rolesAllStub.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);
|
dataMethodStub.find.callCount.should.eql(3);
|
||||||
|
|
||||||
fromItem.related.callCount.should.eql(25);
|
fromItem.related.callCount.should.eql(28);
|
||||||
fromItem.findWhere.callCount.should.eql(25);
|
fromItem.findWhere.callCount.should.eql(28);
|
||||||
toItem[0].get.callCount.should.eql(50);
|
toItem[0].get.callCount.should.eql(56);
|
||||||
|
|
||||||
fromItem.permissions.callCount.should.eql(25);
|
fromItem.permissions.callCount.should.eql(28);
|
||||||
fromItem.attach.callCount.should.eql(25);
|
fromItem.attach.callCount.should.eql(28);
|
||||||
fromItem.attach.calledWith(toItem).should.be.true();
|
fromItem.attach.calledWith(toItem).should.be.true();
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
@ -32,8 +32,8 @@ var should = require('should'),
|
|||||||
describe('DB version integrity', function () {
|
describe('DB version integrity', function () {
|
||||||
// Only these variables should need updating
|
// Only these variables should need updating
|
||||||
var currentDbVersion = '005',
|
var currentDbVersion = '005',
|
||||||
currentSchemaHash = 'be706cdbeb06103d90703ee733efc556',
|
currentSchemaHash = 'f63f41ac97b5665a30c899409bbf9a83',
|
||||||
currentFixturesHash = 'ba195b645386b019a69c4b79e6854138';
|
currentFixturesHash = '56f781fa3bba0fdbf98da5f232ec9b11';
|
||||||
|
|
||||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
// 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
|
// and the values above will need updating as confirmation
|
||||||
@ -405,12 +405,14 @@ describe('Migrations', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('01-add-tour-column-to-users', 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) {
|
it('does not try to add a new column if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('users').returns(new Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('users').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[0](loggerStub).then(function () {
|
addTourColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').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));
|
knexMock.schema.hasColumn.withArgs('users', 'tour').returns(new Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[0](loggerStub).then(function () {
|
addTourColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').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));
|
knexMock.schema.hasColumn.withArgs('users', 'tour').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[0](loggerStub).then(function () {
|
addTourColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').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 () {
|
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) {
|
it('does not try to add a new column if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('posts_tags').returns(new Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('posts_tags').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[1](loggerStub).then(function () {
|
addSortOrderColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts_tags').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));
|
knexMock.schema.hasColumn.withArgs('posts_tags', 'sort_order').returns(new Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[1](loggerStub).then(function () {
|
addSortOrderColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts_tags').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));
|
knexMock.schema.hasColumn.withArgs('posts_tags', 'sort_order').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[1](loggerStub).then(function () {
|
addSortOrderColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts_tags').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 () {
|
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) {
|
it('does not try to add new columns if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[2](loggerStub).then(function () {
|
addClientColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').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));
|
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[2](loggerStub).then(function () {
|
addClientColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').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));
|
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[2](loggerStub).then(function () {
|
addClientColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').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));
|
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[2](loggerStub).then(function () {
|
addClientColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||||
|
|
||||||
@ -658,12 +664,14 @@ describe('Migrations', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('04-add-clienttrusteddomains-table', 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) {
|
it('does not try to add a new table if the table already exists', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(true));
|
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[3](loggerStub).then(function () {
|
addTrustedDomains(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('client_trusted_domains').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));
|
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[3](loggerStub).then(function () {
|
addTrustedDomains(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('client_trusted_domains').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 () {
|
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) {
|
it('does not try to drop unique if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
getIndexesStub.withArgs('clients').returns(new Promise.resolve(
|
getIndexesStub.withArgs('clients').returns(new Promise.resolve(
|
||||||
@ -705,7 +715,7 @@ describe('Migrations', function () {
|
|||||||
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[4](loggerStub).then(function () {
|
dropUnique(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||||
|
|
||||||
@ -729,7 +739,7 @@ describe('Migrations', function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[4](loggerStub).then(function () {
|
dropUnique(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||||
|
|
||||||
@ -754,7 +764,7 @@ describe('Migrations', function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates004[4](loggerStub).then(function () {
|
dropUnique(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
knexMock.schema.hasTable.calledWith('clients').should.be.true();
|
||||||
|
|
||||||
@ -798,7 +808,7 @@ describe('Migrations', function () {
|
|||||||
|
|
||||||
tasksSpy.calledOnce.should.be.true();
|
tasksSpy.calledOnce.should.be.true();
|
||||||
tasksSpy.calledWith('005', loggerStub).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();
|
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.firstCall.args[0][0].should.be.a.Function().with.property('name', 'runVersionTasks');
|
||||||
|
|
||||||
sequenceStub.secondCall.calledWith(sinon.match.array, loggerStub).should.be.true();
|
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][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][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][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][3].should.be.a.Function().with.property('name', 'addSocialMediaColumnsToUsers');
|
||||||
|
sequenceStub.secondCall.args[0][4].should.be.a.Function().with.property('name', 'addSubscribersTable');
|
||||||
|
|
||||||
// Reset sequence
|
// Reset sequence
|
||||||
sequenceReset();
|
sequenceReset();
|
||||||
@ -820,7 +831,7 @@ describe('Migrations', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Tasks:', function () {
|
describe('Tasks:', function () {
|
||||||
var dropColumnStub, addColumnStub,
|
var dropColumnStub, addColumnStub, createTableStub,
|
||||||
knexStub, knexMock;
|
knexStub, knexMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@ -834,6 +845,7 @@ describe('Migrations', function () {
|
|||||||
|
|
||||||
dropColumnStub = sandbox.stub(schema.commands, 'dropColumn');
|
dropColumnStub = sandbox.stub(schema.commands, 'dropColumn');
|
||||||
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
|
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
|
||||||
|
createTableStub = sandbox.stub(schema.commands, 'createTable');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
@ -842,16 +854,18 @@ describe('Migrations', function () {
|
|||||||
|
|
||||||
it('should have tasks for 005', function () {
|
it('should have tasks for 005', function () {
|
||||||
should.exist(updates005);
|
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 () {
|
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) {
|
it('does not try to drop column if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('tags').returns(Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('tags').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[0](loggerStub).then(function () {
|
dropHiddenColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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));
|
knexMock.schema.hasColumn.withArgs('tags', 'hidden').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[0](loggerStub).then(function () {
|
dropHiddenColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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));
|
knexMock.schema.hasColumn.withArgs('tags', 'hidden').returns(Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[0](loggerStub).then(function () {
|
dropHiddenColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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 () {
|
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) {
|
it('does not try to add new column if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('posts').returns(Promise.resolve(false));
|
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));
|
knexMock.schema.hasTable.withArgs('users').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[1](loggerStub).then(function () {
|
addVisibilityColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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));
|
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[1](loggerStub).then(function () {
|
addVisibilityColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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));
|
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[1](loggerStub).then(function () {
|
addVisibilityColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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('tags', 'visibility').returns(Promise.resolve(true));
|
||||||
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(false));
|
knexMock.schema.hasColumn.withArgs('users', 'visibility').returns(Promise.resolve(false));
|
||||||
// Execute
|
// Execute
|
||||||
updates005[1](loggerStub).then(function () {
|
addVisibilityColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledThrice.should.be.true();
|
knexMock.schema.hasTable.calledThrice.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
knexMock.schema.hasTable.calledWith('posts').should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('tags').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 () {
|
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) {
|
it('does not try to add a new column if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('posts').returns(Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('posts').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[2](loggerStub).then(function () {
|
addMobiledocColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').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));
|
knexMock.schema.hasColumn.withArgs('posts', 'mobiledoc').returns(Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[2](loggerStub).then(function () {
|
addMobiledocColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').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));
|
knexMock.schema.hasColumn.withArgs('posts', 'mobiledoc').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[2](loggerStub).then(function () {
|
addMobiledocColumn(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('posts').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 () {
|
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) {
|
it('does not try to add new columns if the table does not exist', function (done) {
|
||||||
// Setup
|
// Setup
|
||||||
knexMock.schema.hasTable.withArgs('users').returns(Promise.resolve(false));
|
knexMock.schema.hasTable.withArgs('users').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[3](loggerStub).then(function () {
|
addSocialMediaColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').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));
|
knexMock.schema.hasColumn.withArgs('users', 'twitter').returns(Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[3](loggerStub).then(function () {
|
addSocialMediaColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').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));
|
knexMock.schema.hasColumn.withArgs('users', 'twitter').returns(Promise.resolve(false));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[3](loggerStub).then(function () {
|
addSocialMediaColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').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));
|
knexMock.schema.hasColumn.withArgs('users', 'twitter').returns(Promise.resolve(true));
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
updates005[3](loggerStub).then(function () {
|
addSocialMediaColumns(loggerStub).then(function () {
|
||||||
knexMock.schema.hasTable.calledOnce.should.be.true();
|
knexMock.schema.hasTable.calledOnce.should.be.true();
|
||||||
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
knexMock.schema.hasTable.calledWith('users').should.be.true();
|
||||||
|
|
||||||
@ -1200,6 +1220,47 @@ describe('Migrations', function () {
|
|||||||
}).catch(done);
|
}).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 () {
|
it('should return public for no context', function () {
|
||||||
permissions.parseContext().should.eql({
|
permissions.parseContext().should.eql({
|
||||||
internal: false,
|
internal: false,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: null,
|
app: null,
|
||||||
public: true
|
public: true
|
||||||
});
|
});
|
||||||
permissions.parseContext({}).should.eql({
|
permissions.parseContext({}).should.eql({
|
||||||
internal: false,
|
internal: false,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: null,
|
app: null,
|
||||||
public: true
|
public: true
|
||||||
@ -65,12 +67,14 @@ describe('Permissions', function () {
|
|||||||
it('should return public for random context', function () {
|
it('should return public for random context', function () {
|
||||||
permissions.parseContext('public').should.eql({
|
permissions.parseContext('public').should.eql({
|
||||||
internal: false,
|
internal: false,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: null,
|
app: null,
|
||||||
public: true
|
public: true
|
||||||
});
|
});
|
||||||
permissions.parseContext({client: 'thing'}).should.eql({
|
permissions.parseContext({client: 'thing'}).should.eql({
|
||||||
internal: false,
|
internal: false,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: null,
|
app: null,
|
||||||
public: true
|
public: true
|
||||||
@ -80,6 +84,7 @@ describe('Permissions', function () {
|
|||||||
it('should return user if user populated', function () {
|
it('should return user if user populated', function () {
|
||||||
permissions.parseContext({user: 1}).should.eql({
|
permissions.parseContext({user: 1}).should.eql({
|
||||||
internal: false,
|
internal: false,
|
||||||
|
external: false,
|
||||||
user: 1,
|
user: 1,
|
||||||
app: null,
|
app: null,
|
||||||
public: false
|
public: false
|
||||||
@ -89,6 +94,7 @@ describe('Permissions', function () {
|
|||||||
it('should return app if app populated', function () {
|
it('should return app if app populated', function () {
|
||||||
permissions.parseContext({app: 5}).should.eql({
|
permissions.parseContext({app: 5}).should.eql({
|
||||||
internal: false,
|
internal: false,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: 5,
|
app: 5,
|
||||||
public: false
|
public: false
|
||||||
@ -98,6 +104,7 @@ describe('Permissions', function () {
|
|||||||
it('should return internal if internal provided', function () {
|
it('should return internal if internal provided', function () {
|
||||||
permissions.parseContext({internal: true}).should.eql({
|
permissions.parseContext({internal: true}).should.eql({
|
||||||
internal: true,
|
internal: true,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: null,
|
app: null,
|
||||||
public: false
|
public: false
|
||||||
@ -105,6 +112,7 @@ describe('Permissions', function () {
|
|||||||
|
|
||||||
permissions.parseContext('internal').should.eql({
|
permissions.parseContext('internal').should.eql({
|
||||||
internal: true,
|
internal: true,
|
||||||
|
external: false,
|
||||||
user: null,
|
user: null,
|
||||||
app: null,
|
app: null,
|
||||||
public: false
|
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'],
|
tags: ['tags', 'meta'],
|
||||||
users: ['users', 'meta'],
|
users: ['users', 'meta'],
|
||||||
settings: ['settings', 'meta'],
|
settings: ['settings', 'meta'],
|
||||||
|
subscribers: ['subscribers', 'meta'],
|
||||||
roles: ['roles'],
|
roles: ['roles'],
|
||||||
pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'],
|
pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'],
|
||||||
slugs: ['slugs'],
|
slugs: ['slugs'],
|
||||||
@ -25,6 +26,7 @@ var _ = require('lodash'),
|
|||||||
// Tag API swaps parent_id to parent
|
// Tag API swaps parent_id to parent
|
||||||
tag: _(schema.tags).keys().without('parent_id').concat('parent').value(),
|
tag: _(schema.tags).keys().without('parent_id').concat('parent').value(),
|
||||||
setting: _.keys(schema.settings),
|
setting: _.keys(schema.settings),
|
||||||
|
subscriber: _.keys(schema.subscribers),
|
||||||
accesstoken: _.keys(schema.accesstokens),
|
accesstoken: _.keys(schema.accesstokens),
|
||||||
role: _.keys(schema.roles),
|
role: _.keys(schema.roles),
|
||||||
permission: _.keys(schema.permissions),
|
permission: _.keys(schema.permissions),
|
||||||
|
@ -236,6 +236,15 @@ DataGenerator.Content = {
|
|||||||
key: 'setting',
|
key: 'setting',
|
||||||
value: 'value'
|
value: 'value'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
subscribers: [
|
||||||
|
{
|
||||||
|
email: 'subscriber1@test.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'subscriber2@test.com'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -440,6 +449,7 @@ DataGenerator.forKnex = (function () {
|
|||||||
createAppField: createAppField,
|
createAppField: createAppField,
|
||||||
createAppSetting: createAppSetting,
|
createAppSetting: createAppSetting,
|
||||||
createToken: createToken,
|
createToken: createToken,
|
||||||
|
createSubscriber: createBasic,
|
||||||
|
|
||||||
posts: posts,
|
posts: posts,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
@ -386,6 +386,7 @@ toDoList = {
|
|||||||
role: function insertRole() { return fixtures.insertOne('roles', 'createRole'); },
|
role: function insertRole() { return fixtures.insertOne('roles', 'createRole'); },
|
||||||
roles: function insertRoles() { return fixtures.insertRoles(); },
|
roles: function insertRoles() { return fixtures.insertRoles(); },
|
||||||
tag: function insertTag() { return fixtures.insertOne('tags', 'createTag'); },
|
tag: function insertTag() { return fixtures.insertOne('tags', 'createTag'); },
|
||||||
|
subscriber: function insertSubscriber() { return fixtures.insertOne('subscribers', 'createSubscriber'); },
|
||||||
|
|
||||||
posts: function insertPosts() { return fixtures.insertPosts(); },
|
posts: function insertPosts() { return fixtures.insertPosts(); },
|
||||||
'posts:mu': function insertMultiAuthorPosts() { return fixtures.insertMultiAuthorPosts(); },
|
'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
|
// Helpers to make it easier to write tests which are easy to read
|
||||||
context: {
|
context: {
|
||||||
internal: {context: {internal: true}},
|
internal: {context: {internal: true}},
|
||||||
|
external: {context: {external: true}},
|
||||||
owner: {context: {user: 1}},
|
owner: {context: {user: 1}},
|
||||||
admin: {context: {user: 2}},
|
admin: {context: {user: 2}},
|
||||||
editor: {context: {user: 3}},
|
editor: {context: {user: 3}},
|
||||||
|
Loading…
Reference in New Issue
Block a user