[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
@@ -81,6 +81,11 @@
|
|||||||
description: If true, show only users that are active. Cannot be used at the same time as "hide_active"
|
description: If true, show only users that are active. Cannot be used at the same time as "hide_active"
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- name: search_term
|
||||||
|
in: query
|
||||||
|
description: A filtering search term that allows filtering by Username, FirstName, LastName, Nickname or Email
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: User page retrieval successful
|
description: User page retrieval successful
|
||||||
@@ -98,3 +103,64 @@
|
|||||||
$ref: "#/components/responses/Forbidden"
|
$ref: "#/components/responses/Forbidden"
|
||||||
"500":
|
"500":
|
||||||
$ref: "#/components/responses/InternalServerError"
|
$ref: "#/components/responses/InternalServerError"
|
||||||
|
/api/v4/reports/users/count:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
summary: Gets the full count of users that match the filter.
|
||||||
|
description: >
|
||||||
|
Get the full count of users admin reporting purposes, based on provided parameters.
|
||||||
|
|
||||||
|
Must be a system admin to invoke this API.
|
||||||
|
|
||||||
|
##### Permissions
|
||||||
|
|
||||||
|
Requires `sysconsole_read_user_management_users`.
|
||||||
|
|
||||||
|
operationId: GetUserCountForReporting
|
||||||
|
parameters:
|
||||||
|
- name: role_filter
|
||||||
|
in: query
|
||||||
|
description: Filter users by their role.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: team_filter
|
||||||
|
in: query
|
||||||
|
description: Filter users by a specified team ID.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: has_no_team
|
||||||
|
in: query
|
||||||
|
description: If true, show only users that have no team. Will ignore provided "team_filter" if true.
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
- name: hide_active
|
||||||
|
in: query
|
||||||
|
description: If true, show only users that are inactive. Cannot be used at the same time as "hide_inactive"
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
- name: hide_inactive
|
||||||
|
in: query
|
||||||
|
description: If true, show only users that are active. Cannot be used at the same time as "hide_active"
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
- name: search_term
|
||||||
|
in: query
|
||||||
|
description: A filtering search term that allows filtering by Username, FirstName, LastName, Nickname or Email
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: User count retrieval successful
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"500":
|
||||||
|
$ref: "#/components/responses/InternalServerError"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package api4
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
|
|
||||||
func (api *API) InitReports() {
|
func (api *API) InitReports() {
|
||||||
api.BaseRoutes.Reports.Handle("/users", api.APISessionRequired(getUsersForReporting)).Methods("GET")
|
api.BaseRoutes.Reports.Handle("/users", api.APISessionRequired(getUsersForReporting)).Methods("GET")
|
||||||
|
api.BaseRoutes.Reports.Handle("/users/count", api.APISessionRequired(getUserCountForReporting)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -23,51 +25,13 @@ func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sortColumn := "Username"
|
baseOptions := fillReportingBaseOptions(r.URL.Query())
|
||||||
if r.URL.Query().Get("sort_column") != "" {
|
options, err := fillUserReportOptions(r.URL.Query())
|
||||||
sortColumn = r.URL.Query().Get("sort_column")
|
if err != nil {
|
||||||
}
|
c.Err = err
|
||||||
|
|
||||||
direction := "down"
|
|
||||||
if r.URL.Query().Get("direction") == "up" {
|
|
||||||
direction = "up"
|
|
||||||
}
|
|
||||||
|
|
||||||
pageSize := 50
|
|
||||||
if pageSizeStr, err := strconv.ParseInt(r.URL.Query().Get("page_size"), 10, 64); err == nil {
|
|
||||||
pageSize = int(pageSizeStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
teamFilter := r.URL.Query().Get("team_filter")
|
|
||||||
if !(teamFilter == "" || model.IsValidId(teamFilter)) {
|
|
||||||
c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_team_filter", nil, "", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
options.ReportingBaseOptions = baseOptions
|
||||||
hideActive := r.URL.Query().Get("hide_active") == "true"
|
|
||||||
hideInactive := r.URL.Query().Get("hide_inactive") == "true"
|
|
||||||
if hideActive && hideInactive {
|
|
||||||
c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_active_filter", nil, "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := &model.UserReportOptions{
|
|
||||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
|
||||||
Direction: direction,
|
|
||||||
SortColumn: sortColumn,
|
|
||||||
SortDesc: r.URL.Query().Get("sort_direction") == "desc",
|
|
||||||
PageSize: pageSize,
|
|
||||||
FromColumnValue: r.URL.Query().Get("from_column_value"),
|
|
||||||
FromId: r.URL.Query().Get("from_id"),
|
|
||||||
DateRange: r.URL.Query().Get("date_range"),
|
|
||||||
},
|
|
||||||
Team: teamFilter,
|
|
||||||
Role: r.URL.Query().Get("role_filter"),
|
|
||||||
HasNoTeam: r.URL.Query().Get("has_no_team") == "true",
|
|
||||||
HideActive: hideActive,
|
|
||||||
HideInactive: hideInactive,
|
|
||||||
}
|
|
||||||
options.PopulateDateRange(time.Now())
|
|
||||||
|
|
||||||
// Don't allow fetching more than 100 users at a time from the normal query endpoint
|
// Don't allow fetching more than 100 users at a time from the normal query endpoint
|
||||||
if options.PageSize <= 0 || options.PageSize > model.ReportingMaxPageSize {
|
if options.PageSize <= 0 || options.PageSize > model.ReportingMaxPageSize {
|
||||||
@@ -85,3 +49,78 @@ func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.Logger.Warn("Error writing response", mlog.Err(jsonErr))
|
c.Logger.Warn("Error writing response", mlog.Err(jsonErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUserCountForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !(c.IsSystemAdmin() && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers)) {
|
||||||
|
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := fillUserReportOptions(r.URL.Query())
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := c.App.GetUserCountForReport(options)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonErr := json.NewEncoder(w).Encode(count); jsonErr != nil {
|
||||||
|
c.Logger.Warn("Error writing response", mlog.Err(jsonErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillReportingBaseOptions(values url.Values) model.ReportingBaseOptions {
|
||||||
|
sortColumn := "Username"
|
||||||
|
if values.Get("sort_column") != "" {
|
||||||
|
sortColumn = values.Get("sort_column")
|
||||||
|
}
|
||||||
|
|
||||||
|
direction := "next"
|
||||||
|
if values.Get("direction") == "prev" {
|
||||||
|
direction = "prev"
|
||||||
|
}
|
||||||
|
|
||||||
|
pageSize := 50
|
||||||
|
if pageSizeStr, err := strconv.ParseInt(values.Get("page_size"), 10, 64); err == nil {
|
||||||
|
pageSize = int(pageSizeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.ReportingBaseOptions{
|
||||||
|
Direction: direction,
|
||||||
|
SortColumn: sortColumn,
|
||||||
|
SortDesc: values.Get("sort_direction") == "desc",
|
||||||
|
PageSize: pageSize,
|
||||||
|
FromColumnValue: values.Get("from_column_value"),
|
||||||
|
FromId: values.Get("from_id"),
|
||||||
|
DateRange: values.Get("date_range"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillUserReportOptions(values url.Values) (*model.UserReportOptions, *model.AppError) {
|
||||||
|
teamFilter := values.Get("team_filter")
|
||||||
|
if !(teamFilter == "" || model.IsValidId(teamFilter)) {
|
||||||
|
return nil, model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_team_filter", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
hideActive := values.Get("hide_active") == "true"
|
||||||
|
hideInactive := values.Get("hide_inactive") == "true"
|
||||||
|
if hideActive && hideInactive {
|
||||||
|
return nil, model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_active_filter", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &model.UserReportOptions{
|
||||||
|
|
||||||
|
Team: teamFilter,
|
||||||
|
Role: values.Get("role_filter"),
|
||||||
|
HasNoTeam: values.Get("has_no_team") == "true",
|
||||||
|
HideActive: hideActive,
|
||||||
|
HideInactive: hideInactive,
|
||||||
|
SearchTerm: values.Get("search_term"),
|
||||||
|
}
|
||||||
|
options.PopulateDateRange(time.Now())
|
||||||
|
return options, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -838,6 +838,7 @@ type AppIface interface {
|
|||||||
GetUserByEmail(email string) (*model.User, *model.AppError)
|
GetUserByEmail(email string) (*model.User, *model.AppError)
|
||||||
GetUserByRemoteID(remoteID string) (*model.User, *model.AppError)
|
GetUserByRemoteID(remoteID string) (*model.User, *model.AppError)
|
||||||
GetUserByUsername(username string) (*model.User, *model.AppError)
|
GetUserByUsername(username string) (*model.User, *model.AppError)
|
||||||
|
GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError)
|
||||||
GetUserForLogin(c request.CTX, id, loginId string) (*model.User, *model.AppError)
|
GetUserForLogin(c request.CTX, id, loginId string) (*model.User, *model.AppError)
|
||||||
GetUserLimits() (*model.UserLimits, *model.AppError)
|
GetUserLimits() (*model.UserLimits, *model.AppError)
|
||||||
GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError)
|
GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError)
|
||||||
|
|||||||
@@ -10511,6 +10511,28 @@ func (a *OpenTracingAppLayer) GetUserByUsername(username string) (*model.User, *
|
|||||||
return resultVar0, resultVar1
|
return resultVar0, resultVar1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserCountForReport")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0, resultVar1 := a.app.GetUserCountForReport(filter)
|
||||||
|
|
||||||
|
if resultVar1 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0, resultVar1
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) GetUserForLogin(c request.CTX, id string, loginId string) (*model.User, *model.AppError) {
|
func (a *OpenTracingAppLayer) GetUserForLogin(c request.CTX, id string, loginId string) (*model.User, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserForLogin")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserForLogin")
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ func (a *App) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.Us
|
|||||||
return nil, appErr
|
return nil, appErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.getUserReport(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) getUserReport(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
|
|
||||||
userReportQuery, err := a.Srv().Store().User().GetUserReport(filter)
|
userReportQuery, err := a.Srv().Store().User().GetUserReport(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, model.NewAppError("GetUsersForReporting", "app.report.get_user_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("GetUsersForReporting", "app.report.get_user_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
@@ -30,3 +26,12 @@ func (a *App) getUserReport(filter *model.UserReportOptions) ([]*model.UserRepor
|
|||||||
|
|
||||||
return userReports, nil
|
return userReports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError) {
|
||||||
|
count, err := a.Srv().Store().User().GetUserCountForReport(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("GetUserCountForReport", "app.report.get_user_count_for_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &count, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2008,6 +2008,5 @@ func TestGetUsersForReporting(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.NotNil(t, userReports)
|
require.NotNil(t, userReports)
|
||||||
require.Equal(t, "Bob Bobson", userReports[0].DisplayName)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11892,6 +11892,24 @@ func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, chan
|
|||||||
return result, err
|
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) {
|
func (s *OpenTracingLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUserReport")
|
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) {
|
func (s *RetryLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -2268,12 +2268,56 @@ func (us SqlUserStore) RefreshPostStatsForUsers() error {
|
|||||||
return nil
|
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) {
|
func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
|
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
|
||||||
selectColumns := []string{"u.Id", "u.LastLogin", "MAX(s.LastActivityAt) AS LastStatusAt"}
|
selectColumns := []string{"u.*", "MAX(s.LastActivityAt) AS LastStatusAt"}
|
||||||
for _, column := range model.UserReportSortColumns {
|
|
||||||
selectColumns = append(selectColumns, "u."+column)
|
|
||||||
}
|
|
||||||
if isPostgres {
|
if isPostgres {
|
||||||
selectColumns = append(selectColumns,
|
selectColumns = append(selectColumns,
|
||||||
"MAX(ps.LastPostDate) AS LastPostDate",
|
"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
|
// no need to apply any filtering and pagination if there are no
|
||||||
// previous element ID and value provided.
|
// previous element ID and value provided.
|
||||||
if filter.FromId != "" && filter.FromColumnValue != "" {
|
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"
|
sortDirection = "DESC"
|
||||||
|
|
||||||
query = query.Where(sq.Or{
|
query = query.Where(sq.Or{
|
||||||
@@ -2364,19 +2408,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query = applyRoleFilter(query, filter.Role, isPostgres)
|
query = applyUserReportFilter(query, filter, 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})
|
|
||||||
}
|
|
||||||
|
|
||||||
parentQuery := query
|
parentQuery := query
|
||||||
// If we're going a page back...
|
// 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
|
// The way pagination works, we get the previous page's rows
|
||||||
// in reverse order. So, we use parent query on it to
|
// in reverse order. So, we use parent query on it to
|
||||||
// reverse the order in database itself.
|
// reverse the order in database itself.
|
||||||
if filter.Direction == "up" {
|
if filter.Direction == "prev" {
|
||||||
reverseSortDirection := "ASC"
|
reverseSortDirection := "ASC"
|
||||||
if sortDirection == "ASC" {
|
if sortDirection == "ASC" {
|
||||||
reverseSortDirection = "DESC"
|
reverseSortDirection = "DESC"
|
||||||
|
|||||||
@@ -481,6 +481,7 @@ type UserStore interface {
|
|||||||
InsertUsers(users []*model.User) error
|
InsertUsers(users []*model.User) error
|
||||||
RefreshPostStatsForUsers() error
|
RefreshPostStatsForUsers() error
|
||||||
GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error)
|
GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error)
|
||||||
|
GetUserCountForReport(filter *model.UserReportOptions) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotStore interface {
|
type BotStore interface {
|
||||||
|
|||||||
@@ -1153,6 +1153,30 @@ func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (
|
|||||||
return r0, r1
|
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
|
// GetUserReport provides a mock function with given fields: filter
|
||||||
func (_m *UserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
func (_m *UserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
ret := _m.Called(filter)
|
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{
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||||
SortColumn: "Username",
|
SortColumn: "Username",
|
||||||
Direction: "down",
|
Direction: "next",
|
||||||
PageSize: 50,
|
PageSize: 50,
|
||||||
FromColumnValue: users[10].Username,
|
FromColumnValue: users[10].Username,
|
||||||
FromId: users[10].Id,
|
FromId: users[10].Id,
|
||||||
@@ -6353,7 +6353,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||||
SortColumn: "Username",
|
SortColumn: "Username",
|
||||||
SortDesc: true,
|
SortDesc: true,
|
||||||
Direction: "down",
|
Direction: "next",
|
||||||
PageSize: 50,
|
PageSize: 50,
|
||||||
FromColumnValue: users[10].Username,
|
FromColumnValue: users[10].Username,
|
||||||
FromId: users[10].Id,
|
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{
|
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||||
SortColumn: "Username",
|
SortColumn: "Username",
|
||||||
Direction: "up",
|
Direction: "prev",
|
||||||
PageSize: 50,
|
PageSize: 50,
|
||||||
FromColumnValue: users[10].Username,
|
FromColumnValue: users[10].Username,
|
||||||
FromId: users[10].Id,
|
FromId: users[10].Id,
|
||||||
@@ -6386,7 +6386,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||||
SortColumn: "Username",
|
SortColumn: "Username",
|
||||||
SortDesc: true,
|
SortDesc: true,
|
||||||
Direction: "up",
|
Direction: "prev",
|
||||||
PageSize: 50,
|
PageSize: 50,
|
||||||
FromColumnValue: users[10].Username,
|
FromColumnValue: users[10].Username,
|
||||||
FromId: users[10].Id,
|
FromId: users[10].Id,
|
||||||
@@ -6541,4 +6541,26 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, userReport, 15)
|
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
|
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) {
|
func (s *TimerLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -6622,6 +6622,10 @@
|
|||||||
"id": "app.recover.save.app_error",
|
"id": "app.recover.save.app_error",
|
||||||
"translation": "Unable to save the token."
|
"translation": "Unable to save the token."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.report.get_user_count_for_report.store_error",
|
||||||
|
"translation": "Failed to fetch user count."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.report.get_user_report.store_error",
|
"id": "app.report.get_user_report.store_error",
|
||||||
"translation": "Failed to fetch user report."
|
"translation": "Failed to fetch user report."
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ var (
|
|||||||
|
|
||||||
type ReportingBaseOptions struct {
|
type ReportingBaseOptions struct {
|
||||||
SortDesc bool
|
SortDesc bool
|
||||||
Direction string // Accepts only "up" or "down"
|
Direction string // Accepts only "prev" or "next"
|
||||||
PageSize int
|
PageSize int
|
||||||
SortColumn string
|
SortColumn string
|
||||||
FromColumnValue string
|
FromColumnValue string
|
||||||
@@ -66,12 +66,7 @@ type UserReportQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserReport struct {
|
type UserReport struct {
|
||||||
Id string `json:"id"`
|
User
|
||||||
Username string `json:"username"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
CreateAt int64 `json:"create_at,omitempty"`
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
Roles string `json:"roles"`
|
|
||||||
UserPostStats
|
UserPostStats
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +77,7 @@ type UserReportOptions struct {
|
|||||||
HasNoTeam bool
|
HasNoTeam bool
|
||||||
HideActive bool
|
HideActive bool
|
||||||
HideInactive bool
|
HideInactive bool
|
||||||
|
SearchTerm string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserReportOptions) IsValid() *AppError {
|
func (u *UserReportOptions) IsValid() *AppError {
|
||||||
@@ -99,12 +95,7 @@ func (u *UserReportOptions) IsValid() *AppError {
|
|||||||
|
|
||||||
func (u *UserReportQuery) ToReport() *UserReport {
|
func (u *UserReportQuery) ToReport() *UserReport {
|
||||||
return &UserReport{
|
return &UserReport{
|
||||||
Id: u.Id,
|
User: u.User,
|
||||||
Username: u.Username,
|
|
||||||
Email: u.Email,
|
|
||||||
CreateAt: u.CreateAt,
|
|
||||||
DisplayName: u.GetDisplayName(ShowNicknameFullName),
|
|
||||||
Roles: u.Roles,
|
|
||||||
UserPostStats: u.UserPostStats,
|
UserPostStats: u.UserPostStats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
ChannelSearchOpts,
|
ChannelSearchOpts,
|
||||||
ServerChannel,
|
ServerChannel,
|
||||||
} from '@mattermost/types/channels';
|
} from '@mattermost/types/channels';
|
||||||
import {Options, StatusOK, ClientResponse, LogLevel, FetchPaginatedThreadOptions, UserReportOptions} from '@mattermost/types/client4';
|
import {Options, StatusOK, ClientResponse, LogLevel, FetchPaginatedThreadOptions, UserReportOptions, UserReportFilter} from '@mattermost/types/client4';
|
||||||
import {Compliance} from '@mattermost/types/compliance';
|
import {Compliance} from '@mattermost/types/compliance';
|
||||||
import {
|
import {
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
@@ -999,6 +999,14 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserCountForReporting = (filter: UserReportFilter) => {
|
||||||
|
const queryString = buildQueryString(filter);
|
||||||
|
return this.doFetch<number>(
|
||||||
|
`${this.getReportsRoute()}/users/count${queryString}`,
|
||||||
|
{method: 'get'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -44,7 +44,16 @@ export enum ReportDuration {
|
|||||||
Last6Months = 'last_6_months',
|
Last6Months = 'last_6_months',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserReportOptions = {
|
export type UserReportFilter = {
|
||||||
|
role_filter?: string,
|
||||||
|
has_no_team?: boolean,
|
||||||
|
team_filter?: string,
|
||||||
|
hide_active?: boolean,
|
||||||
|
hide_inactive?: boolean,
|
||||||
|
searchTerm?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserReportOptions = UserReportFilter & {
|
||||||
sort_column: 'CreateAt' | 'Username' | 'FirstName' | 'LastName' | 'Nickname' | 'Email',
|
sort_column: 'CreateAt' | 'Username' | 'FirstName' | 'LastName' | 'Nickname' | 'Email',
|
||||||
page_size: number,
|
page_size: number,
|
||||||
sort_direction?: 'asc' | 'desc',
|
sort_direction?: 'asc' | 'desc',
|
||||||
@@ -52,9 +61,4 @@ export type UserReportOptions = {
|
|||||||
date_range?: ReportDuration,
|
date_range?: ReportDuration,
|
||||||
from_column_value?: string,
|
from_column_value?: string,
|
||||||
from_id?: string,
|
from_id?: string,
|
||||||
role_filter?: string,
|
|
||||||
has_no_team?: boolean,
|
|
||||||
team_filter?: string,
|
|
||||||
hide_active?: boolean,
|
|
||||||
hide_inactive?: boolean,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,12 +144,7 @@ export type AuthChangeResponse = {
|
|||||||
follow_link: string;
|
follow_link: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserReport = {
|
export type UserReport = UserProfile & {
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
create_at: number;
|
|
||||||
display_name: string;
|
|
||||||
last_login_at: number;
|
last_login_at: number;
|
||||||
last_status_at?: number;
|
last_status_at?: number;
|
||||||
last_post_date?: number;
|
last_post_date?: number;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user