[MM-44084] Feature: Top threads insights (#20195)
* Add route endpoints, model, store functions, and tests for top threads
* Run make store-layers
* Make the following changes
- Fix top user threads query
- Fix passing parameters in api4/insights.go to handler in app
- Add top user threads test
* Add post-message, user_id, participants information to insights results
* model.TopThread.UserID -> model.TopThread.UserId, for compatibility with MySQL
* Rename name -> channel_name
* Add user information to response
* Link post in response, filter out deleted root posts from top threads
* Handle thread delete cases, add app tests for threads insights
* lint: fix typo
* lint: rename asserts
* lint: require.nil -> require.NoError
* Add integration tests for thread insights
* Add embeds and images to top posts
* Add license checks for top threads endpoints
* Query users in batch to populate post-creator
* Make the following changes
- Add license to test server in api4/
- Add tests for threads insights
- top team threads shouldn't include threads from other teams, DMs
- Test duration constraint
- Pagination testing for top threads in model/insights_test.go
* Add i18n-extract
* i18n fixes
* Add username, nickname to user_information
* Hide message, user_id, post_id, reply_count in depth=1 of top threads response
* Fix tests using response.reply_count to use response.post.reply_count
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
de50943d61
Коммит
2cd83d2f8d
@@ -255,6 +255,52 @@ func (_m *ThreadStore) GetThreadsForUser(userId string, teamID string, opts mode
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTopThreadsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
|
||||
func (_m *ThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
ret := _m.Called(teamID, userID, since, offset, limit)
|
||||
|
||||
var r0 *model.TopThreadList
|
||||
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
|
||||
r0 = rf(teamID, userID, since, offset, limit)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.TopThreadList)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
|
||||
r1 = rf(teamID, userID, since, offset, limit)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTopThreadsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
|
||||
func (_m *ThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
ret := _m.Called(teamID, userID, since, offset, limit)
|
||||
|
||||
var r0 *model.TopThreadList
|
||||
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
|
||||
r0 = rf(teamID, userID, since, offset, limit)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.TopThreadList)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
|
||||
r1 = rf(teamID, userID, since, offset, limit)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTotalThreads provides a mock function with given fields: userId, teamID, opts
|
||||
func (_m *ThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
|
||||
ret := _m.Called(userId, teamID, opts)
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
|
||||
t.Run("GetTeamsUnreadForUser", func(t *testing.T) { testGetTeamsUnreadForUser(t, ss) })
|
||||
t.Run("GetVarious", func(t *testing.T) { testVarious(t, ss) })
|
||||
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
|
||||
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
|
||||
}
|
||||
|
||||
func testThreadStorePopulation(t *testing.T, ss store.Store) {
|
||||
@@ -1226,3 +1227,345 @@ func testMarkAllAsReadByChannels(t *testing.T, ss store.Store) {
|
||||
assertThreadReplyCount(t, userBID, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetTopThreads(t *testing.T, ss store.Store) {
|
||||
// create two users
|
||||
u1 := model.User{
|
||||
Email: MakeEmail(),
|
||||
Username: model.NewId(),
|
||||
}
|
||||
|
||||
_, err := ss.User().Save(&u1)
|
||||
require.NoError(t, err)
|
||||
|
||||
u2 := model.User{
|
||||
Email: MakeEmail(),
|
||||
Username: model.NewId(),
|
||||
}
|
||||
|
||||
_, err = ss.User().Save(&u2)
|
||||
require.NoError(t, err)
|
||||
|
||||
u3 := model.User{
|
||||
Email: MakeEmail(),
|
||||
Username: model.NewId(),
|
||||
}
|
||||
|
||||
_, err = ss.User().Save(&u3)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("test get top team threads", func(t *testing.T) {
|
||||
const limit = 10
|
||||
team, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
channel, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
post1, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
post2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u2.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
|
||||
|
||||
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 2000)
|
||||
|
||||
// get top threads
|
||||
topThreadsInTeam, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
// require length of top threads to be 2
|
||||
require.Len(t, topThreadsInTeam.Items, 2)
|
||||
|
||||
// require first element to be post1 with 2 replyCount=2
|
||||
require.Equal(t, topThreadsInTeam.Items[0].PostId, post1.Id)
|
||||
require.Equal(t, topThreadsInTeam.Items[0].UserId, post1.UserId)
|
||||
require.Equal(t, topThreadsInTeam.Items[0].UserInformation.Id, post1.UserId)
|
||||
require.Equal(t, topThreadsInTeam.Items[0].Post.ReplyCount, int64(2))
|
||||
require.Equal(t, topThreadsInTeam.Items[0].Post.Message, post1.Message)
|
||||
// require second element to be post2 with 2 replyCount=2
|
||||
require.Equal(t, topThreadsInTeam.Items[1].PostId, post2.Id)
|
||||
require.Equal(t, topThreadsInTeam.Items[1].Post.ReplyCount, int64(1))
|
||||
require.Equal(t, topThreadsInTeam.Items[1].UserId, post2.UserId)
|
||||
require.Equal(t, topThreadsInTeam.Items[1].UserInformation.Id, post2.UserId)
|
||||
require.Equal(t, topThreadsInTeam.Items[1].Post.Message, post2.Message)
|
||||
|
||||
// require topThreads[i].Post is not null
|
||||
require.Equal(t, topThreadsInTeam.Items[0].Post.Id, post1.Id)
|
||||
require.Equal(t, topThreadsInTeam.Items[1].Post.Id, post2.Id)
|
||||
})
|
||||
t.Run("test get top user threads", func(t *testing.T) {
|
||||
const limit = 10
|
||||
team, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
channel, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
post1, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
post2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u2.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
post3, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u3.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
|
||||
|
||||
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post3.Id, post3.UserId, 2000)
|
||||
opts := store.ThreadMembershipOpts{
|
||||
Following: true,
|
||||
IncrementMentions: false,
|
||||
UpdateFollowing: true,
|
||||
UpdateViewedTimestamp: false,
|
||||
UpdateParticipants: false,
|
||||
}
|
||||
|
||||
// create threadmemberships entries.
|
||||
_, err = ss.Thread().MaintainMembership(post1.UserId, post1.Id, opts)
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Thread().MaintainMembership(post2.UserId, post2.Id, opts)
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Thread().MaintainMembership(post2.UserId, post3.Id, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get top threads by user
|
||||
topThreadsByUser1, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post1.UserId, 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
topThreadsByUser2, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post2.UserId, 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
// require length of top threads by users to be 1,2 respectively
|
||||
require.Len(t, topThreadsByUser1.Items, 1)
|
||||
require.Len(t, topThreadsByUser2.Items, 2)
|
||||
|
||||
// require first element of topThreadsByUser1 to be post1 with 2 replyCount=2
|
||||
require.Equal(t, topThreadsByUser1.Items[0].PostId, post1.Id)
|
||||
require.Equal(t, topThreadsByUser1.Items[0].Post.ReplyCount, int64(2))
|
||||
require.Equal(t, topThreadsByUser1.Items[0].Post.Message, post1.Message)
|
||||
require.Equal(t, topThreadsByUser1.Items[0].UserId, post1.UserId)
|
||||
require.Equal(t, topThreadsByUser1.Items[0].UserInformation.Id, post1.UserId)
|
||||
// require elements of topThreadsByUser2 to be post2 and post3 respectively
|
||||
require.Equal(t, topThreadsByUser2.Items[0].PostId, post2.Id)
|
||||
require.Equal(t, topThreadsByUser2.Items[0].Post.ReplyCount, int64(2))
|
||||
require.Equal(t, topThreadsByUser2.Items[0].Post.Message, post2.Message)
|
||||
require.Equal(t, topThreadsByUser2.Items[0].UserId, post2.UserId)
|
||||
require.Equal(t, topThreadsByUser2.Items[0].UserInformation.Id, post2.UserId)
|
||||
|
||||
require.Equal(t, topThreadsByUser2.Items[1].PostId, post3.Id)
|
||||
require.Equal(t, topThreadsByUser2.Items[1].Post.ReplyCount, int64(1))
|
||||
require.Equal(t, topThreadsByUser2.Items[1].Post.Message, post3.Message)
|
||||
require.Equal(t, topThreadsByUser2.Items[1].UserId, post3.UserId)
|
||||
require.Equal(t, topThreadsByUser2.Items[1].UserInformation.Id, post3.UserId)
|
||||
|
||||
// require topThreads[i].Post is not null
|
||||
require.Equal(t, topThreadsByUser1.Items[0].Post.Id, post1.Id)
|
||||
require.Equal(t, topThreadsByUser2.Items[1].Post.Id, post3.Id)
|
||||
})
|
||||
t.Run("test get top threads only from given teamid", func(t *testing.T) {
|
||||
const limit = 10
|
||||
team1, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team2, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
channel1, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team1.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel2, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team2.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
post1, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel1.Id,
|
||||
UserId: u1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
post2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel2.Id,
|
||||
UserId: u2.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
|
||||
|
||||
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, 2000)
|
||||
|
||||
// assert that getting top threads from teamid 1 doesn't have post1.Id
|
||||
|
||||
topThreadsTeam2, err := ss.Thread().GetTopThreadsForTeamSince(team2.Id, u1.Id, 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, topThreadsTeam2.Items, 1)
|
||||
require.Equal(t, topThreadsTeam2.Items[0].Post.Id, post2.Id)
|
||||
})
|
||||
t.Run("test get top threads only from non-direct channels", func(t *testing.T) {
|
||||
const limit = 10
|
||||
team1, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
channel1, err := ss.Channel().CreateDirectChannel(&u1, &u2)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel2, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team1.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
post1, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel1.Id,
|
||||
UserId: u1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
post2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel2.Id,
|
||||
UserId: u2.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
|
||||
|
||||
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, u1.Id, 2000)
|
||||
|
||||
opts := store.ThreadMembershipOpts{
|
||||
Following: true,
|
||||
IncrementMentions: false,
|
||||
UpdateFollowing: true,
|
||||
UpdateViewedTimestamp: false,
|
||||
UpdateParticipants: false,
|
||||
}
|
||||
|
||||
// create threadmemberships entries.
|
||||
_, err = ss.Thread().MaintainMembership(u1.Id, post1.Id, opts)
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Thread().MaintainMembership(u1.Id, post2.Id, opts)
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Thread().MaintainMembership(u2.Id, post1.Id, opts)
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Thread().MaintainMembership(u2.Id, post2.Id, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// assert that getting top threads from teamid 1 doesn't have DMs
|
||||
|
||||
topThreadsTeam1, err := ss.Thread().GetTopThreadsForTeamSince(team1.Id, u1.Id, 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, topThreadsTeam1.Items, 1)
|
||||
require.Equal(t, topThreadsTeam1.Items[0].Post.Id, post2.Id)
|
||||
|
||||
// assert that getting top threads from user 1 doesn't contain dm threads.
|
||||
topUserThreads, err := ss.Thread().GetTopThreadsForUserSince(team1.Id, u1.Id, 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, topUserThreads.Items, 1)
|
||||
require.Equal(t, topUserThreads.Items[0].Post.Id, post2.Id)
|
||||
})
|
||||
t.Run("test get top threads doesn't exceed duration", func(t *testing.T) {
|
||||
const limit = 10
|
||||
team, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: "team" + model.NewId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
channel, err := ss.Channel().Save(&model.Channel{
|
||||
TeamId: team.Id,
|
||||
DisplayName: "DisplayName",
|
||||
Name: "channel" + model.NewId(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
post1, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// post 2 has replies after 10 ms unix time.
|
||||
post2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
UserId: u2.Id,
|
||||
CreateAt: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
|
||||
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
|
||||
|
||||
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 10)
|
||||
|
||||
// get top threads
|
||||
topThreadsInTeamNewer, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
|
||||
require.NoError(t, err)
|
||||
// require length of top threads to be 2
|
||||
require.Len(t, topThreadsInTeamNewer.Items, 1)
|
||||
|
||||
// require first element to be post1 with 2 replyCount=2
|
||||
require.Equal(t, topThreadsInTeamNewer.Items[0].PostId, post1.Id)
|
||||
|
||||
// get top threads
|
||||
topThreadsInTeamOlder, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 9, 0, limit)
|
||||
require.NoError(t, err)
|
||||
// require length of top threads to be 2
|
||||
require.Len(t, topThreadsInTeamOlder.Items, 2)
|
||||
|
||||
// require first element to be post1 with 2 replyCount=2
|
||||
require.Equal(t, topThreadsInTeamOlder.Items[1].PostId, post2.Id)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user