[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
@@ -9646,6 +9646,42 @@ func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamID st
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForTeamSince")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForUserSince")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalThreads")
|
||||
|
||||
@@ -11024,6 +11024,48 @@ func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamID string,
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -935,3 +935,209 @@ func (s *SqlThreadStore) GetThreadUnreadReplyCount(threadMembership *model.Threa
|
||||
|
||||
return unreadReplies, nil
|
||||
}
|
||||
|
||||
// Top threads in all public channels and private channels userID is a member of. Returns a list of threads ranked by interactions.
|
||||
func (s *SqlThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
var args []interface{}
|
||||
query := `select
|
||||
threads_list.PostId,
|
||||
threads_list.ReplyCount,
|
||||
threads_list.ChannelId,
|
||||
threads_list.DisplayName,
|
||||
threads_list.Name,
|
||||
threads_list.Participants,
|
||||
p.UserId
|
||||
from((
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
|
||||
WHERE
|
||||
t.threaddeleteat IS NULL
|
||||
AND t.LastReplyAt > ?
|
||||
AND c.TeamId = ?
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)
|
||||
UNION
|
||||
ALL (
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
|
||||
LEFT JOIN Channels c ON t.ChannelId = c.Id
|
||||
WHERE
|
||||
t.threaddeleteat IS NULL
|
||||
AND cm.UserId = ?
|
||||
AND c.Type = 'P'
|
||||
AND c.TeamId = ?
|
||||
AND t.LastReplyAt > ?
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)) as threads_list
|
||||
LEFT JOIN Posts as p on p.Id = threads_list.PostId
|
||||
ORDER BY ReplyCount DESC
|
||||
limit ? offset ?`
|
||||
|
||||
args = append(args, since, teamID, userID, teamID, since, limit+1, offset)
|
||||
|
||||
topThreads := make([]*model.TopThread, 0)
|
||||
err := s.GetReplicaX().Select(&topThreads, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
|
||||
}
|
||||
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.GetTopThreadListWithPagination(topThreads, limit), nil
|
||||
}
|
||||
|
||||
func (s *SqlThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
var args []interface{}
|
||||
|
||||
// gets all threads within the team which user follows.
|
||||
query := `select
|
||||
threads_list.PostId,
|
||||
threads_list.ReplyCount,
|
||||
threads_list.ChannelId,
|
||||
threads_list.DisplayName,
|
||||
threads_list.Name,
|
||||
threads_list.Participants,
|
||||
p.UserId
|
||||
from((
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
|
||||
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
|
||||
WHERE
|
||||
t.threaddeleteat IS NULL
|
||||
AND t.LastReplyAt > ?
|
||||
AND c.TeamId = ?
|
||||
AND tm.UserId = ?
|
||||
AND tm.Following = TRUE
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)
|
||||
UNION
|
||||
ALL (
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
|
||||
LEFT JOIN Channels c ON t.ChannelId = c.Id
|
||||
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
|
||||
WHERE
|
||||
cm.UserId = ?
|
||||
AND c.Type = 'P'
|
||||
AND c.TeamId = ?
|
||||
AND t.threaddeleteat IS NULL
|
||||
AND t.LastReplyAt > ?
|
||||
AND tm.UserId = ?
|
||||
AND tm.Following = TRUE
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)) as threads_list
|
||||
LEFT JOIN Posts as p on p.Id = threads_list.PostId
|
||||
ORDER BY ReplyCount DESC
|
||||
limit ? offset ?`
|
||||
|
||||
args = append(args, since, teamID, userID, userID, teamID, since, userID, limit+1, offset)
|
||||
|
||||
topThreads := make([]*model.TopThread, 0)
|
||||
err := s.GetReplicaX().Select(&topThreads, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
|
||||
}
|
||||
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.GetTopThreadListWithPagination(topThreads, limit), nil
|
||||
}
|
||||
|
||||
func userContains(userIDs []string, searchedUserID string) bool {
|
||||
for _, userID := range userIDs {
|
||||
if userID == searchedUserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func postProcessTopThreads(topThreads []*model.TopThread, s *SqlThreadStore, teamID string) ([]*model.TopThread, error) {
|
||||
// create list of userIDs
|
||||
var userIDs []string
|
||||
for _, topThread := range topThreads {
|
||||
userID := topThread.UserId
|
||||
if !userContains(userIDs, userID) {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
|
||||
usersMap := map[string]*model.User{}
|
||||
|
||||
users, err := s.User().GetProfileByIds(context.Background(), userIDs, &store.UserGetByIdsOpts{}, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get users for top threads in team=%s", teamID)
|
||||
}
|
||||
for _, user := range users {
|
||||
usersMap[user.Id] = user
|
||||
}
|
||||
|
||||
// resolve user, root post for each top thread
|
||||
for _, topThread := range topThreads {
|
||||
postCreator := usersMap[topThread.UserId]
|
||||
topThread.UserInformation = &model.InsightUserInformation{
|
||||
Id: postCreator.Id,
|
||||
LastPictureUpdate: postCreator.LastPictureUpdate,
|
||||
FirstName: postCreator.FirstName,
|
||||
LastName: postCreator.LastName,
|
||||
Username: postCreator.Username,
|
||||
NickName: postCreator.Nickname,
|
||||
}
|
||||
post, err := s.Post().GetSingle(topThread.PostId, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get extended post for post id=%s", topThread.PostId)
|
||||
}
|
||||
topThread.Post = post
|
||||
}
|
||||
return topThreads, nil
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ type ChannelStore interface {
|
||||
// GetTeamForChannel returns the team for a given channelID.
|
||||
GetTeamForChannel(channelID string) (*model.Team, error)
|
||||
|
||||
// Insights
|
||||
// Insights - channels
|
||||
GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error)
|
||||
GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error)
|
||||
PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error)
|
||||
@@ -331,6 +331,10 @@ type ThreadStore interface {
|
||||
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
|
||||
DeleteOrphanedRows(limit int) (deleted int64, err error)
|
||||
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
|
||||
|
||||
// Insights - threads
|
||||
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
||||
GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
||||
}
|
||||
|
||||
type PostStore interface {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -8679,6 +8679,38 @@ func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamID string,
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForTeamSince", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForUserSince", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
|
||||
start := time.Now()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user