[MM-56399][MM-56397][MM-56456][MM-56269] Various changes for user reporting for admins (#25839)
* [MM-56399] Add user count endpoint for reporting * [MM-56397] Added search term to user report filter * Missing translation * [MM-56456] Rename up/down to prev/next for reporting cursoring * [MM-56269] Add DeleteAt, MfaActive and AuthService fields to UserReport * PR feedback * Fix test --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bb88b92b4c
Коммит
0a4e9eeb92
@@ -11892,6 +11892,24 @@ func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, chan
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUserCountForReport")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.UserStore.GetUserCountForReport(filter)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUserReport")
|
||||
|
||||
@@ -13577,6 +13577,27 @@ func (s *RetryLayerUserStore) GetUnreadCountForChannel(userID string, channelID
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.UserStore.GetUserCountForReport(filter)
|
||||
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 *RetryLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -2268,12 +2268,56 @@ func (us SqlUserStore) RefreshPostStatsForUsers() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyUserReportFilter(query sq.SelectBuilder, filter *model.UserReportOptions, isPostgres bool) sq.SelectBuilder {
|
||||
query = applyRoleFilter(query, filter.Role, isPostgres)
|
||||
if filter.HasNoTeam {
|
||||
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM TeamMembers WHERE DeleteAt = 0)"))
|
||||
} else if filter.Team != "" {
|
||||
query = query.Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.DeleteAt = 0)").
|
||||
Where(sq.Eq{"tm.TeamId": filter.Team})
|
||||
}
|
||||
if filter.HideActive {
|
||||
query = query.Where(sq.Gt{"u.DeleteAt": 0})
|
||||
}
|
||||
if filter.HideInactive {
|
||||
query = query.Where(sq.Eq{"u.DeleteAt": 0})
|
||||
}
|
||||
|
||||
if strings.TrimSpace(filter.SearchTerm) != "" {
|
||||
query = generateSearchQuery(query, strings.Fields(sanitizeSearchTerm(filter.SearchTerm, "*")), UserSearchTypeAll, isPostgres)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (us SqlUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
|
||||
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
|
||||
query := us.getQueryBuilder().
|
||||
Select("COUNT(u.Id)").
|
||||
From("Users u")
|
||||
|
||||
if isPostgres {
|
||||
query = query.LeftJoin("Bots ON u.Id = Bots.UserId").Where("Bots.UserId IS NULL")
|
||||
} else {
|
||||
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM Bots)"))
|
||||
}
|
||||
|
||||
query = applyUserReportFilter(query, filter, isPostgres)
|
||||
queryStr, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "user_count_report_tosql")
|
||||
}
|
||||
var v int64
|
||||
err = us.GetReplicaX().Get(&v, queryStr, args...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to count Users for report")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
|
||||
selectColumns := []string{"u.Id", "u.LastLogin", "MAX(s.LastActivityAt) AS LastStatusAt"}
|
||||
for _, column := range model.UserReportSortColumns {
|
||||
selectColumns = append(selectColumns, "u."+column)
|
||||
}
|
||||
selectColumns := []string{"u.*", "MAX(s.LastActivityAt) AS LastStatusAt"}
|
||||
if isPostgres {
|
||||
selectColumns = append(selectColumns,
|
||||
"MAX(ps.LastPostDate) AS LastPostDate",
|
||||
@@ -2303,7 +2347,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
|
||||
// no need to apply any filtering and pagination if there are no
|
||||
// previous element ID and value provided.
|
||||
if filter.FromId != "" && filter.FromColumnValue != "" {
|
||||
if (filter.Direction == "up" && !filter.SortDesc) || (filter.Direction == "down" && filter.SortDesc) {
|
||||
if (filter.Direction == "prev" && !filter.SortDesc) || (filter.Direction == "next" && filter.SortDesc) {
|
||||
sortDirection = "DESC"
|
||||
|
||||
query = query.Where(sq.Or{
|
||||
@@ -2364,19 +2408,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
|
||||
}
|
||||
}
|
||||
|
||||
query = applyRoleFilter(query, filter.Role, isPostgres)
|
||||
if filter.HasNoTeam {
|
||||
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM TeamMembers WHERE DeleteAt = 0)"))
|
||||
} else if filter.Team != "" {
|
||||
query = query.Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.DeleteAt = 0)").
|
||||
Where(sq.Eq{"tm.TeamId": filter.Team})
|
||||
}
|
||||
if filter.HideActive {
|
||||
query = query.Where(sq.Gt{"u.DeleteAt": 0})
|
||||
}
|
||||
if filter.HideInactive {
|
||||
query = query.Where(sq.Eq{"u.DeleteAt": 0})
|
||||
}
|
||||
query = applyUserReportFilter(query, filter, isPostgres)
|
||||
|
||||
parentQuery := query
|
||||
// If we're going a page back...
|
||||
@@ -2384,7 +2416,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
|
||||
// The way pagination works, we get the previous page's rows
|
||||
// in reverse order. So, we use parent query on it to
|
||||
// reverse the order in database itself.
|
||||
if filter.Direction == "up" {
|
||||
if filter.Direction == "prev" {
|
||||
reverseSortDirection := "ASC"
|
||||
if sortDirection == "ASC" {
|
||||
reverseSortDirection = "DESC"
|
||||
|
||||
@@ -481,6 +481,7 @@ type UserStore interface {
|
||||
InsertUsers(users []*model.User) error
|
||||
RefreshPostStatsForUsers() error
|
||||
GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error)
|
||||
GetUserCountForReport(filter *model.UserReportOptions) (int64, error)
|
||||
}
|
||||
|
||||
type BotStore interface {
|
||||
|
||||
@@ -1153,6 +1153,30 @@ func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserCountForReport provides a mock function with given fields: filter
|
||||
func (_m *UserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
|
||||
ret := _m.Called(filter)
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) (int64, error)); ok {
|
||||
return rf(filter)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) int64); ok {
|
||||
r0 = rf(filter)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*model.UserReportOptions) error); ok {
|
||||
r1 = rf(filter)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserReport provides a mock function with given fields: filter
|
||||
func (_m *UserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
ret := _m.Called(filter)
|
||||
|
||||
@@ -6336,7 +6336,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
Direction: "down",
|
||||
Direction: "next",
|
||||
PageSize: 50,
|
||||
FromColumnValue: users[10].Username,
|
||||
FromId: users[10].Id,
|
||||
@@ -6353,7 +6353,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
SortDesc: true,
|
||||
Direction: "down",
|
||||
Direction: "next",
|
||||
PageSize: 50,
|
||||
FromColumnValue: users[10].Username,
|
||||
FromId: users[10].Id,
|
||||
@@ -6369,7 +6369,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
Direction: "up",
|
||||
Direction: "prev",
|
||||
PageSize: 50,
|
||||
FromColumnValue: users[10].Username,
|
||||
FromId: users[10].Id,
|
||||
@@ -6386,7 +6386,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
SortDesc: true,
|
||||
Direction: "up",
|
||||
Direction: "prev",
|
||||
PageSize: 50,
|
||||
FromColumnValue: users[10].Username,
|
||||
FromId: users[10].Id,
|
||||
@@ -6541,4 +6541,26 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 15)
|
||||
})
|
||||
|
||||
t.Run("should filter on search term", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
SearchTerm: "username_1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 26)
|
||||
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
SearchTerm: "username_2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 11)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10709,6 +10709,22 @@ func (s *TimerLayerUserStore) GetUnreadCountForChannel(userID string, channelID
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.UserStore.GetUserCountForReport(filter)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUserCountForReport", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
start := time.Now()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user