mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 19:02:29 +03:00
Added comment events to activity feed (#15064)
refs https://github.com/TryGhost/Team/issues/1709 - New event type `comment_event` (comments and replies of a member in the activity feed) - Includes member, post and parent relation by default - Added new output mapper for ActivityFeed events **Changes to `Comment` model:** * **Only limit comment fetched to root comments when not authenticated as a user:** `enforcedFilters` is applied to all queries, which is a problem because for the activity feed we also need to fetch comments which have a parent_id that is not null (`Member x replied to a comment`). The current filter in the model is specifically for the members API, not the admin API (so checking the user should fix that, not sure if that is a good pattern but couldn’t find a better alternative). * **Only set default relations for comments when withRelated is empty or not set:** `defaultRelations`: Right now, for every fetch it would force all these relations. But we don’t need all those relations for the activity feed; So I updated the pattern to only set the default relations when it is empty (which we also do on a couple of other places and seems like a good pattern). I also updated the comments-ui frontend to not send ?include
This commit is contained in:
parent
31a4135fec
commit
5235d67fed
@ -0,0 +1,17 @@
|
||||
const mapComment = require('./comments');
|
||||
|
||||
const commentEventMapper = (json, frame) => {
|
||||
return {
|
||||
...json,
|
||||
data: mapComment(json.data, frame)
|
||||
};
|
||||
};
|
||||
|
||||
const activityFeedMapper = (event, frame) => {
|
||||
if (event.type === 'comment_event') {
|
||||
return commentEventMapper(event, frame);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
|
||||
module.exports = activityFeedMapper;
|
@ -1,4 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const url = require('../utils/url');
|
||||
|
||||
const commentFields = [
|
||||
'id',
|
||||
@ -16,6 +17,13 @@ const memberFields = [
|
||||
'avatar_image'
|
||||
];
|
||||
|
||||
const postFields = [
|
||||
'id',
|
||||
'uuid',
|
||||
'title',
|
||||
'url'
|
||||
];
|
||||
|
||||
const commentMapper = (model, frame) => {
|
||||
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
|
||||
|
||||
@ -37,6 +45,16 @@ const commentMapper = (model, frame) => {
|
||||
response.replies = jsonModel.replies.map(reply => commentMapper(reply, frame));
|
||||
}
|
||||
|
||||
if (jsonModel.parent) {
|
||||
response.parent = commentMapper(jsonModel.parent, frame);
|
||||
}
|
||||
|
||||
if (jsonModel.post) {
|
||||
// We could use the post mapper here, but we need less field + don't need al the async behaviour support
|
||||
url.forPost(jsonModel.post.id, jsonModel.post, frame);
|
||||
response.post = _.pick(jsonModel.post, postFields);
|
||||
}
|
||||
|
||||
// todo
|
||||
response.liked = false;
|
||||
if (jsonModel.likes && frame.original.context.member && frame.original.context.member.id) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
actions: require('./actions'),
|
||||
activityFeedEvents: require('./activity-feed-events'),
|
||||
authors: require('./authors'),
|
||||
comments: require('./comments'),
|
||||
emails: require('./emails'),
|
||||
|
@ -1,6 +1,7 @@
|
||||
//@ts-check
|
||||
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:members');
|
||||
const {unparse} = require('@tryghost/members-csv');
|
||||
const mappers = require('./mappers');
|
||||
|
||||
module.exports = {
|
||||
browse: createSerializer('browse', paginatedMembers),
|
||||
@ -18,7 +19,7 @@ module.exports = {
|
||||
importCSV: createSerializer('importCSV', passthrough),
|
||||
memberStats: createSerializer('memberStats', passthrough),
|
||||
mrrStats: createSerializer('mrrStats', passthrough),
|
||||
activityFeed: createSerializer('activityFeed', passthrough)
|
||||
activityFeed: createSerializer('activityFeed', activityFeed)
|
||||
};
|
||||
|
||||
/**
|
||||
@ -73,6 +74,16 @@ function bulkAction(bulkActionResult, _apiConfig, frame) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {{events: any[]}}
|
||||
*/
|
||||
function activityFeed(data, _apiConfig, frame) {
|
||||
return {
|
||||
events: data.events.map(e => mappers.activityFeedEvents(e, frame))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @template PageMeta
|
||||
*
|
||||
|
@ -72,7 +72,11 @@ const Comment = ghostBookshelf.Model.extend({
|
||||
model.emitChange('added', options);
|
||||
},
|
||||
|
||||
enforcedFilters: function enforcedFilters() {
|
||||
enforcedFilters: function enforcedFilters(options) {
|
||||
if (options.context && options.context.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'parent_id:null';
|
||||
}
|
||||
|
||||
@ -146,7 +150,9 @@ const Comment = ghostBookshelf.Model.extend({
|
||||
defaultRelations: function defaultRelations(methodName, options) {
|
||||
// @todo: the default relations are not working for 'add' when we add it below
|
||||
if (['findAll', 'findPage', 'edit', 'findOne'].indexOf(methodName) !== -1) {
|
||||
options.withRelated = _.union(['member', 'likes', 'replies', 'replies.member', 'replies.likes'], options.withRelated || []);
|
||||
if (!options.withRelated || options.withRelated.length === 0) {
|
||||
options.withRelated = ['member', 'likes', 'replies', 'replies.member', 'replies.likes'];
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
|
@ -189,7 +189,8 @@ function createApiInstance(config) {
|
||||
StripeProduct: models.StripeProduct,
|
||||
StripePrice: models.StripePrice,
|
||||
Product: models.Product,
|
||||
Settings: models.Settings
|
||||
Settings: models.Settings,
|
||||
Comment: models.Comment
|
||||
},
|
||||
stripeAPIService: stripeService.api,
|
||||
offersAPI: offersService.api,
|
||||
|
@ -3278,6 +3278,45 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Returns comments in activity feed 1: [body] 1`] = `
|
||||
Object {
|
||||
"events": Array [
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Returns comments 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": "3484",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Search by case-insensitive email MEMBER2 receives member with email member2@test.com 1: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
|
@ -1,5 +1,5 @@
|
||||
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {anyEtag, anyObjectId, anyUuid, anyISODateTime, anyISODate, anyString, anyArray, anyLocationFor, anyErrorId} = matchers;
|
||||
const {anyEtag, anyObjectId, anyUuid, anyISODateTime, anyISODate, anyString, anyArray, anyLocationFor, anyErrorId, anyObject} = matchers;
|
||||
const ObjectId = require('bson-objectid');
|
||||
|
||||
const assert = require('assert');
|
||||
@ -155,7 +155,7 @@ describe('Members API', function () {
|
||||
|
||||
before(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
await fixtureManager.init('newsletters', 'members:newsletters');
|
||||
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments');
|
||||
await agent.loginAsOwner();
|
||||
|
||||
newsletters = await getNewsletters();
|
||||
@ -170,6 +170,26 @@ describe('Members API', function () {
|
||||
mockManager.restore();
|
||||
});
|
||||
|
||||
// Activity feed
|
||||
it('Returns comments in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(5).fill({
|
||||
type: anyString,
|
||||
data: anyObject
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
should(body.events.find(e => e.type === 'comment_event')).not.be.undefined();
|
||||
});
|
||||
});
|
||||
|
||||
// List Members
|
||||
|
||||
it('Can browse', async function () {
|
||||
|
@ -316,6 +316,7 @@ module.exports = {
|
||||
anyBoolean: any(Boolean),
|
||||
anyString: any(String),
|
||||
anyArray: any(Array),
|
||||
anyObject: any(Object),
|
||||
anyNumber: any(Number),
|
||||
anyStringNumber: stringMatching(/\d+/),
|
||||
anyISODateTime: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/),
|
||||
|
@ -53,7 +53,8 @@ module.exports = function MembersAPI({
|
||||
StripeProduct,
|
||||
StripePrice,
|
||||
Product,
|
||||
Settings
|
||||
Settings,
|
||||
Comment
|
||||
},
|
||||
stripeAPIService,
|
||||
offersAPI,
|
||||
@ -103,6 +104,7 @@ module.exports = function MembersAPI({
|
||||
MemberPaymentEvent,
|
||||
MemberStatusEvent,
|
||||
MemberLoginEvent,
|
||||
Comment,
|
||||
labsService
|
||||
});
|
||||
|
||||
|
@ -9,6 +9,7 @@ module.exports = class EventRepository {
|
||||
MemberStatusEvent,
|
||||
MemberLoginEvent,
|
||||
MemberPaidSubscriptionEvent,
|
||||
Comment,
|
||||
labsService
|
||||
}) {
|
||||
this._MemberSubscribeEvent = MemberSubscribeEvent;
|
||||
@ -17,6 +18,7 @@ module.exports = class EventRepository {
|
||||
this._MemberStatusEvent = MemberStatusEvent;
|
||||
this._MemberLoginEvent = MemberLoginEvent;
|
||||
this._EmailRecipient = EmailRecipient;
|
||||
this._Comment = Comment;
|
||||
this._labsService = labsService;
|
||||
}
|
||||
|
||||
@ -175,6 +177,35 @@ module.exports = class EventRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async getCommentEvents(options = {}, filters = {}) {
|
||||
options = {
|
||||
...options,
|
||||
withRelated: ['member', 'post', 'parent'],
|
||||
filter: ['member_id:-null']
|
||||
};
|
||||
if (filters['data.created_at']) {
|
||||
options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:'));
|
||||
}
|
||||
if (filters['data.member_id']) {
|
||||
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||
}
|
||||
options.filter = options.filter.join('+');
|
||||
|
||||
const {data: models, meta} = await this._Comment.findPage(options);
|
||||
|
||||
const data = models.map((model) => {
|
||||
return {
|
||||
type: 'comment_event',
|
||||
data: model.toJSON(options)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
meta
|
||||
};
|
||||
}
|
||||
|
||||
async getEmailDeliveredEvents(options = {}, filters = {}) {
|
||||
options = {
|
||||
...options,
|
||||
@ -358,7 +389,8 @@ module.exports = class EventRepository {
|
||||
{type: 'subscription_event', action: 'getSubscriptionEvents'},
|
||||
{type: 'login_event', action: 'getLoginEvents'},
|
||||
{type: 'payment_event', action: 'getPaymentEvents'},
|
||||
{type: 'signup_event', action: 'getSignupEvents'}
|
||||
{type: 'signup_event', action: 'getSignupEvents'},
|
||||
{type: 'comment_event', action: 'getCommentEvents'}
|
||||
];
|
||||
if (this._EmailRecipient) {
|
||||
pageActions.push({type: 'email_delivered_event', action: 'getEmailDeliveredEvents'});
|
||||
|
Loading…
Reference in New Issue
Block a user