[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>
Этот коммит содержится в:
Shivashis Padhi
2022-06-20 19:57:17 +05:30
коммит произвёл GitHub
родитель de50943d61
Коммит 2cd83d2f8d
17 изменённых файлов: 1460 добавлений и 2 удалений

Просмотреть файл

@@ -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)
})
}