Skip to content

Commit

Permalink
Added support for filtering email events by post_id
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonBackx committed Oct 20, 2022
1 parent 03e0962 commit 0e1fde8
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 26 deletions.
4 changes: 2 additions & 2 deletions ghost/admin/app/components/gh-member-details-activity.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
<div>
<h3>
{{or @member.name @member.email}}
{{#unless (or @member.name @member.email)}}
{{#if (not (or @member.name @member.email))}}
{{#if @member.isNew}}
<span class="midgrey">New member</span>
{{/if}}
{{/unless}}
{{/if}}
</h3>
<p>
{{#if (and @member.name @member.email)}}
Expand Down
4 changes: 2 additions & 2 deletions ghost/admin/app/components/gh-member-details.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
<div>
<h3>
{{or @member.name @member.email}}
{{#unless (or @member.name @member.email)}}
{{#if (not (or @member.name @member.email))}}
{{#if @member.isNew}}
<span class="midgrey">New member</span>
{{/if}}
{{/unless}}
{{/if}}
</h3>
<p>
{{#if (and @member.name @member.email)}}
Expand Down
6 changes: 3 additions & 3 deletions ghost/admin/app/components/gh-post-settings-menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
@disabled={{this.post.isScheduled}}
@isActive={{not this.isViewingSubview}}
/>
{{#unless (or this.post.isDraft this.post.isPublished this.post.pastScheduledTime this.post.isSent)}}
{{#if (not (or this.post.isDraft this.post.isPublished this.post.pastScheduledTime this.post.isSent))}}
<p>Use the publish menu to re-schedule</p>
{{/unless}}
{{/if}}
</div>

{{#unless this.session.user.isContributor}}
Expand Down Expand Up @@ -187,7 +187,7 @@
</div>{{! .post-settings-menu }}

{{#if this.isViewingSubview}}
<div class="settings-menu settings-menu-pane {{unless (eq this.subview "email-settings") "settings-menu-pane-wide"}}">
<div class="settings-menu settings-menu-pane {{if (not (eq this.subview "email-settings")) "settings-menu-pane-wide"}}">
<div class="active">
{{#if (eq this.subview "meta-data")}}
<div class="settings-menu-header subview">
Expand Down
4 changes: 2 additions & 2 deletions ghost/admin/app/components/modal-import-members.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@
</div>
{{/if}}
{{#if this.importResponse.errorCount}}
{{#unless (eq this.importResponse.importedCount 0)}}
{{#if (not (eq this.importResponse.importedCount 0))}}
<hr>
<div class="gh-member-import-result-summary">
<p><strong>{{format-number this.importResponse.errorCount}}</strong> {{gh-pluralize this.importResponse.errorCount "member" without-count=true}} {{if (eq this.importResponse.errorCount 1) "was" "were"}} skipped due to the following errors:</p>
</div>
{{/unless}}
{{/if}}
<div class="gh-member-import-errorlist">
<ul>
{{#each this.importResponse.errorList as |error|}}
Expand Down
14 changes: 14 additions & 0 deletions ghost/core/core/server/models/email-recipient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ const ghostBookshelf = require('./base');
const EmailRecipient = ghostBookshelf.Model.extend({
tableName: 'email_recipients',
hasTimestamps: false,

filterRelations: function filterRelations() {
return {
email: {
// Mongo-knex doesn't support belongsTo relations
tableName: 'emails',
tableNameAs: 'email',
type: 'manyToMany',
joinTable: 'email_recipients',
joinFrom: 'id',
joinTo: 'email_id'
}
};
},

email() {
return this.belongsTo('Email', 'email_id');
Expand Down
6 changes: 1 addition & 5 deletions ghost/core/core/server/models/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ const Email = ghostBookshelf.Model.extend({

model.emitChange('deleted', options);
}
}, {
post() {
return this.belongsTo('Post');
}
});
}, {});

const Emails = ghostBookshelf.Collection.extend({
model: Email
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
Expand All @@ -43,7 +51,7 @@ Object {
"page": null,
"pages": 1,
"prev": null,
"total": 8,
"total": 10,
},
},
}
Expand All @@ -53,7 +61,7 @@ exports[`Activity Feed API Can filter events by post id 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "15074",
"content-length": "17358",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
Expand All @@ -78,9 +86,9 @@ Object {
"limit": "2",
"next": null,
"page": null,
"pages": 4,
"pages": 5,
"prev": null,
"total": 8,
"total": 10,
},
},
}
Expand All @@ -90,7 +98,7 @@ exports[`Activity Feed API Can limit events 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1239",
"content-length": "1240",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
Expand Down Expand Up @@ -348,6 +356,72 @@ Object {
}
`;
exports[`Activity Feed API Returns email delivered events in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 1,
},
},
}
`;
exports[`Activity Feed API Returns email delivered events in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1246",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Returns email opened events in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 1,
},
},
}
`;
exports[`Activity Feed API Returns email opened events in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1243",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Returns feedback events in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Expand Down
56 changes: 50 additions & 6 deletions ghost/core/test/e2e-api/admin/activity-feed.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyObjectId, anyUuid, anyISODate, anyString, anyObject, anyNumber} = matchers;
const models = require('../../../core/server/models');

const assert = require('assert');

let agent;
describe('Activity Feed API', function () {
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments', 'redirects', 'clicks', 'feedback');
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments', 'redirects', 'clicks', 'feedback', 'members:emails');
await agent.loginAsOwner();
});

Expand Down Expand Up @@ -125,32 +126,75 @@ describe('Activity Feed API', function () {
});
});

it('Returns email delivered events in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:email_delivered_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(1).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(body.events.find(e => e.type === 'email_delivered_event'), 'Expected an email delivered event');
assert(!body.events.find(e => e.type !== 'email_delivered_event'), 'Expected only email delivered events');
});
});

it('Returns email opened events in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:email_opened_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(1).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(body.events.find(e => e.type === 'email_opened_event'), 'Expected an email opened event');
assert(!body.events.find(e => e.type !== 'email_opened_event'), 'Expected only email opened events');
});
});

it('Can filter events by post id', async function () {
const postId = fixtureManager.get('posts', 0).id;

await agent
.get(`/members/events?filter=data.post_id:${postId}`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(8).fill({
events: new Array(10).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id) !== postId), 'Should only return events for the post');
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId), 'Should only return events for the post');

// Check all post_id event types are covered by this test
assert(body.events.find(e => e.type === 'click_event'), 'Expected a click event');
assert(body.events.find(e => e.type === 'comment_event'), 'Expected a comment event');
assert(body.events.find(e => e.type === 'feedback_event'), 'Expected a feedback event');
assert(body.events.find(e => e.type === 'signup_event'), 'Expected a signup event');
assert(body.events.find(e => e.type === 'subscription_event'), 'Expected a subscription event');
assert(body.events.find(e => e.type === 'email_delivered_event'), 'Expected an email delivered event');
assert(body.events.find(e => e.type === 'email_opened_event'), 'Expected an email opened event');

// Assert total is correct
assert.equal(body.meta.pagination.total, 8);
assert.equal(body.meta.pagination.total, 10);
});
});

Expand All @@ -169,10 +213,10 @@ describe('Activity Feed API', function () {
})
})
.expect(({body}) => {
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id) !== postId), 'Should only return events for the post');
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId), 'Should only return events for the post');

// Assert total is correct
assert.equal(body.meta.pagination.total, 8);
assert.equal(body.meta.pagination.total, 10);
});
});
});
19 changes: 18 additions & 1 deletion ghost/core/test/utils/fixtures/data-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,19 @@ DataGenerator.Content = {
member_uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b343',
member_email: 'member1@test.com',
member_name: 'Mr Egg'
},
{
id: ObjectId().toHexString(),
email_id: null, // emails[0] relation added later
member_id: null, // members[4] relation added later
batch_id: null, // email_batches[0] relation added later
processed_at: moment().toDate(),
delivered_at: moment().toDate(),
opened_at: moment().toDate(),
failed_at: null,
member_uuid: 'f6f91461-d7d8-4a3f-aa5d-8e582c40b344',
member_email: 'member4@test.com',
member_name: 'Mr Egg'
}
],

Expand Down Expand Up @@ -850,6 +863,9 @@ DataGenerator.Content.email_recipients[2].member_id = DataGenerator.Content.memb
DataGenerator.Content.email_recipients[3].batch_id = DataGenerator.Content.email_batches[0].id;
DataGenerator.Content.email_recipients[3].email_id = DataGenerator.Content.email_batches[0].email_id;
DataGenerator.Content.email_recipients[3].member_id = DataGenerator.Content.members[3].id;
DataGenerator.Content.email_recipients[4].batch_id = DataGenerator.Content.email_batches[0].id;
DataGenerator.Content.email_recipients[4].email_id = DataGenerator.Content.email_batches[0].email_id;
DataGenerator.Content.email_recipients[4].member_id = DataGenerator.Content.members[4].id;
DataGenerator.Content.members_stripe_customers[0].member_id = DataGenerator.Content.members[2].id;
DataGenerator.Content.members_stripe_customers[1].member_id = DataGenerator.Content.members[3].id;
DataGenerator.Content.members_stripe_customers[2].member_id = DataGenerator.Content.members[4].id;
Expand Down Expand Up @@ -1533,7 +1549,8 @@ DataGenerator.forKnex = (function () {
createEmailRecipient(DataGenerator.Content.email_recipients[0]),
createEmailRecipient(DataGenerator.Content.email_recipients[1]),
createEmailRecipient(DataGenerator.Content.email_recipients[2]),
createEmailRecipient(DataGenerator.Content.email_recipients[3])
createEmailRecipient(DataGenerator.Content.email_recipients[3]),
createEmailRecipient(DataGenerator.Content.email_recipients[4])
];

const members = [
Expand Down
Loading

0 comments on commit 0e1fde8

Please sign in to comment.