[MM-55017] Add API method to get users for Admin Reporting (#25499)
* Add store method to get reporting data * Some store changes * Added app layer * Added API call, some miscellaneous fixes * Fix lint * Fix serialized check * Add API docs * Fix user store tests leaking users * Fix test * PR feedback * Add filtering for role/team/activated user, filter out bot users * Fix mock * Fix test * Oops * Switch to using struct filter * More PR feedback * Fix gen * Fix test * Fix API docs * Fix test * Fix possible SQL injection, some query optimization * Fix migrations * Oops * Add role to API * Fix check * Add Client4 API call for load testing * Fix test * Update server/channels/store/storetest/user_store.go Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> * PR feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7afc14de36
Коммит
109f4643c6
@@ -108,6 +108,8 @@ func (api *API) InitUser() {
|
||||
|
||||
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST")
|
||||
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Users.Handle("/report", api.APISessionRequired(getUsersForReporting)).Methods("GET")
|
||||
}
|
||||
|
||||
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -3443,3 +3445,59 @@ func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Reques
|
||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !(c.IsSystemAdmin() && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers)) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
|
||||
return
|
||||
}
|
||||
|
||||
sortColumn := "Username"
|
||||
if r.URL.Query().Get("sort_column") != "" {
|
||||
sortColumn = r.URL.Query().Get("sort_column")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.UserReportOptionsAPI{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: sortColumn,
|
||||
SortDesc: r.URL.Query().Get("sort_direction") == "desc",
|
||||
PageSize: pageSize,
|
||||
Team: teamFilter,
|
||||
LastSortColumnValue: r.URL.Query().Get("last_column_value"),
|
||||
LastUserId: r.URL.Query().Get("last_id"),
|
||||
Role: r.URL.Query().Get("role_filter"),
|
||||
HasNoTeam: r.URL.Query().Get("has_no_team") == "true",
|
||||
HideActive: hideActive,
|
||||
HideInactive: hideInactive,
|
||||
},
|
||||
DateRange: r.URL.Query().Get("date_range"),
|
||||
}
|
||||
|
||||
userReports, err := c.App.GetUsersForReporting(options.ToBaseOptions(time.Now()))
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if jsonErr := json.NewEncoder(w).Encode(userReports); jsonErr != nil {
|
||||
c.Logger.Warn("Error writing response", mlog.Err(jsonErr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,6 +828,7 @@ type AppIface interface {
|
||||
GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError)
|
||||
GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError)
|
||||
GetUsersEtag(restrictionsHash string) string
|
||||
GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError)
|
||||
GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
||||
GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
||||
GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
||||
|
||||
@@ -10704,6 +10704,28 @@ func (a *OpenTracingAppLayer) GetUsersEtag(restrictionsHash string) string {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersForReporting")
|
||||
|
||||
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.GetUsersForReporting(filter)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersFromProfiles")
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
pUtils "github.com/mattermost/mattermost/server/public/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/email"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/users"
|
||||
@@ -2817,3 +2818,36 @@ func (a *App) UserIsFirstAdmin(user *model.User) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
|
||||
// Don't allow fetching more than 100 users at a time from the normal query endpoint
|
||||
if filter.PageSize <= 0 || filter.PageSize > 100 {
|
||||
return nil, model.NewAppError("GetUsersForReporting", "app.user.get_users_for_reporting.invalid_page_size", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
if filter.EndAt > 0 && filter.StartAt > filter.EndAt {
|
||||
return nil, model.NewAppError("GetUsersForReporting", "app.user.get_users_for_reporting.bad_date_range", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return a.getUserReport(filter)
|
||||
}
|
||||
|
||||
func (a *App) getUserReport(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
|
||||
// Validate against the columns we allow sorting for
|
||||
if !pUtils.Contains(model.UserReportSortColumns, filter.SortColumn) {
|
||||
return nil, model.NewAppError("GetUsersForReporting", "app.user.get_user_report.invalid_sort_column", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
userReportQuery, err := a.Srv().Store().User().GetUserReport(filter)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetUsersForReporting", "app.user.get_user_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
userReports := make([]*model.UserReport, len(userReportQuery))
|
||||
for i, user := range userReportQuery {
|
||||
userReports[i] = user.ToReport()
|
||||
}
|
||||
|
||||
return userReports, nil
|
||||
}
|
||||
|
||||
@@ -1932,3 +1932,96 @@ func TestSendSubscriptionHistoryEvent(t *testing.T) {
|
||||
require.Equal(t, 10, subscriptionHistoryEvent.Seats, "Number of seats doesn't match")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUsersForReporting(t *testing.T) {
|
||||
t.Run("should throw error on invalid page size", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 999,
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, userReports)
|
||||
})
|
||||
|
||||
t.Run("should throw error on invalid date range", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
StartAt: 1000,
|
||||
EndAt: 500,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, userReports)
|
||||
})
|
||||
|
||||
t.Run("should throw error on bad sort column", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "FakeColumn",
|
||||
PageSize: 50,
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, userReports)
|
||||
})
|
||||
|
||||
t.Run("should return some formatted reporting data", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Mock to get the user count
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockUserStore := storemocks.UserStore{}
|
||||
mockUserStore.On("GetUserReport",
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return([]*model.UserReportQuery{
|
||||
{
|
||||
User: model.User{
|
||||
Id: "some-id",
|
||||
CreateAt: 1000,
|
||||
FirstName: "Bob",
|
||||
LastName: "Bobson",
|
||||
},
|
||||
UserPostStats: model.UserPostStats{
|
||||
LastLogin: 1500,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
|
||||
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, userReports)
|
||||
require.Equal(t, "Bob Bobson", userReports[0].DisplayName)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -232,6 +232,8 @@ channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.down.sql
|
||||
channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.up.sql
|
||||
channels/db/migrations/mysql/000117_msteams_shared_channels.down.sql
|
||||
channels/db/migrations/mysql/000117_msteams_shared_channels.up.sql
|
||||
channels/db/migrations/mysql/000118_create_index_poststats.down.sql
|
||||
channels/db/migrations/mysql/000118_create_index_poststats.up.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||
@@ -464,3 +466,5 @@ channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.down.sq
|
||||
channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.up.sql
|
||||
channels/db/migrations/postgres/000117_msteams_shared_channels.down.sql
|
||||
channels/db/migrations/postgres/000117_msteams_shared_channels.up.sql
|
||||
channels/db/migrations/postgres/000118_create_index_poststats.down.sql
|
||||
channels/db/migrations/postgres/000118_create_index_poststats.up.sql
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Nothing to do for MySQL
|
||||
@@ -0,0 +1 @@
|
||||
-- Nothing to do for MySQL
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS idx_poststats_userid;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- morph:nontransactional
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_poststats_userid ON poststats(userid)
|
||||
@@ -11892,6 +11892,24 @@ func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, chan
|
||||
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")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.UserStore.GetUserReport(filter)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersBatchForIndexing")
|
||||
|
||||
@@ -13577,6 +13577,27 @@ func (s *RetryLayerUserStore) GetUnreadCountForChannel(userID string, channelID
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.UserStore.GetUserReport(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) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
@@ -2266,3 +2267,102 @@ func (us SqlUserStore) RefreshPostStatsForUsers() error {
|
||||
|
||||
return 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)
|
||||
}
|
||||
if isPostgres {
|
||||
selectColumns = append(selectColumns,
|
||||
"MAX(ps.LastPostDate) AS LastPostDate",
|
||||
"COUNT(ps.Day) AS DaysActive",
|
||||
"SUM(ps.NumPosts) AS TotalPosts",
|
||||
)
|
||||
} else {
|
||||
selectColumns = append(selectColumns,
|
||||
"MAX(p.CreateAt) AS LastPostDate",
|
||||
"COUNT(DATE(FROM_UNIXTIME(p.CreateAt / 1000))) AS DaysActive",
|
||||
"COUNT(p.Id) AS TotalPosts",
|
||||
)
|
||||
}
|
||||
|
||||
sortColumnValue := filter.SortColumn
|
||||
if filter.SortDesc {
|
||||
sortColumnValue += " DESC"
|
||||
}
|
||||
|
||||
query := us.getQueryBuilder().
|
||||
Select(selectColumns...).
|
||||
From("Users u").
|
||||
LeftJoin("Status s ON s.UserId = u.Id").
|
||||
Where(sq.Or{
|
||||
sq.Gt{filter.SortColumn: filter.LastSortColumnValue},
|
||||
sq.And{
|
||||
sq.Eq{filter.SortColumn: filter.LastSortColumnValue},
|
||||
sq.Gt{"u.Id": filter.LastUserId},
|
||||
},
|
||||
}).
|
||||
Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM Bots)")).
|
||||
GroupBy("u.Id").
|
||||
OrderBy(sortColumnValue, "u.Id")
|
||||
|
||||
if filter.PageSize > 0 {
|
||||
query = query.Limit(uint64(filter.PageSize))
|
||||
}
|
||||
|
||||
if isPostgres {
|
||||
query = query.LeftJoin("PostStats ps ON ps.UserId = u.Id")
|
||||
if filter.StartAt > 0 {
|
||||
startDate := time.UnixMilli(filter.StartAt)
|
||||
query = query.Where(sq.Or{
|
||||
sq.Expr("ps.UserId IS NULL"),
|
||||
sq.GtOrEq{"ps.Day": startDate.Format("2006-01-02")},
|
||||
})
|
||||
}
|
||||
if filter.EndAt > 0 {
|
||||
endDate := time.UnixMilli(filter.EndAt)
|
||||
query = query.Where(sq.Or{
|
||||
sq.Expr("ps.UserId IS NULL"),
|
||||
sq.Lt{"ps.Day": endDate.Format("2006-01-02")},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
query = query.LeftJoin("Posts p on p.UserId = u.Id")
|
||||
if filter.StartAt > 0 {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Expr("p.UserId IS NULL"),
|
||||
sq.GtOrEq{"p.CreateAt": filter.StartAt},
|
||||
})
|
||||
}
|
||||
if filter.EndAt > 0 {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Expr("p.UserId IS NULL"),
|
||||
sq.Lt{"p.CreateAt": filter.EndAt},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
userResults := []*model.UserReportQuery{}
|
||||
err := us.GetReplicaX().SelectBuilder(&userResults, query)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get users for reporting")
|
||||
}
|
||||
|
||||
return userResults, nil
|
||||
}
|
||||
|
||||
@@ -493,6 +493,7 @@ type UserStore interface {
|
||||
GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error)
|
||||
InsertUsers(users []*model.User) error
|
||||
RefreshPostStatsForUsers() error
|
||||
GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error)
|
||||
}
|
||||
|
||||
type BotStore interface {
|
||||
|
||||
@@ -1153,6 +1153,32 @@ func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (
|
||||
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)
|
||||
|
||||
var r0 []*model.UserReportQuery
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) ([]*model.UserReportQuery, error)); ok {
|
||||
return rf(filter)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) []*model.UserReportQuery); ok {
|
||||
r0 = rf(filter)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.UserReportQuery)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*model.UserReportOptions) error); ok {
|
||||
r1 = rf(filter)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUsersBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
|
||||
func (_m *UserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||
ret := _m.Called(startTime, startFileID, limit)
|
||||
|
||||
@@ -96,6 +96,7 @@ func TestUserStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
||||
t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, rctx, ss) })
|
||||
t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, rctx, ss) })
|
||||
t.Run("UpdateLastLogin", func(t *testing.T) { testUpdateLastLogin(t, rctx, ss) })
|
||||
t.Run("GetUserReport", func(t *testing.T) { testGetUserReport(t, rctx, ss) })
|
||||
}
|
||||
|
||||
func testUserStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
@@ -4904,6 +4905,7 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, rctx request.CTX, ss st
|
||||
CreateAt: model.GetMillis(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
@@ -4913,6 +4915,7 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, rctx request.CTX, ss st
|
||||
CreateAt: model.GetMillis(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
|
||||
_, nErr = ss.Team().SaveMember(&model.TeamMember{
|
||||
UserId: u2.Id,
|
||||
TeamId: t1.Id,
|
||||
@@ -4939,6 +4942,7 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, rctx request.CTX, ss st
|
||||
CreateAt: model.GetMillis(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
|
||||
_, nErr = ss.Team().SaveMember(&model.TeamMember{
|
||||
UserId: u3.Id,
|
||||
TeamId: t1.Id,
|
||||
@@ -6182,3 +6186,297 @@ func testUpdateLastLogin(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1234567890), user.LastLogin)
|
||||
}
|
||||
|
||||
func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
now := time.Now()
|
||||
|
||||
u1 := &model.User{Username: "u1" + model.NewId(), DeleteAt: 0}
|
||||
u1.Email = MakeEmail()
|
||||
u1, err := ss.User().Save(u1)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
p := model.Post{UserId: u1.Id, ChannelId: model.NewId(), Message: NewTestId(), CreateAt: now.AddDate(0, 0, -i).UnixMilli()}
|
||||
_, err = ss.Post().Save(&p)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
u2 := &model.User{Username: "u2" + model.NewId(), Roles: "system", DeleteAt: 0}
|
||||
u2.Email = MakeEmail()
|
||||
u2, err = ss.User().Save(u2)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
p := model.Post{UserId: u2.Id, ChannelId: model.NewId(), Message: NewTestId(), CreateAt: now.AddDate(0, 0, -i).UnixMilli()}
|
||||
_, err = ss.Post().Save(&p)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
u3 := &model.User{Username: "u3" + model.NewId(), Roles: "guest", DeleteAt: now.UnixMilli()}
|
||||
u3.Email = MakeEmail()
|
||||
u3, err = ss.User().Save(u3)
|
||||
require.NoError(t, err)
|
||||
|
||||
team, err := ss.Team().Save(&model.Team{
|
||||
DisplayName: "DisplayName",
|
||||
Name: NewTestId(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Team().SaveMember(&model.TeamMember{UserId: u3.Id, TeamId: team.Id}, 1)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, ss.Team().RemoveMember(rctx, team.Id, u3.Id))
|
||||
require.NoError(t, ss.Team().PermanentDelete(team.Id))
|
||||
}()
|
||||
|
||||
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
p := model.Post{UserId: u3.Id, ChannelId: model.NewId(), Message: NewTestId(), CreateAt: now.AddDate(0, 0, -i).UnixMilli()}
|
||||
_, err = ss.Post().Save(&p)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = ss.User().RefreshPostStatsForUsers()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should return info for all the users", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userReport)
|
||||
require.Len(t, userReport, 3)
|
||||
|
||||
require.NotNil(t, userReport[0])
|
||||
require.Equal(t, u1.Username, userReport[0].Username)
|
||||
|
||||
require.NotNil(t, userReport[1])
|
||||
require.Equal(t, u2.Username, userReport[1].Username)
|
||||
|
||||
require.NotNil(t, userReport[2])
|
||||
require.Equal(t, u3.Username, userReport[2].Username)
|
||||
})
|
||||
|
||||
t.Run("should return in the correct order", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
SortDesc: true,
|
||||
PageSize: 50,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userReport)
|
||||
require.Equal(t, 3, len(userReport))
|
||||
|
||||
require.NotNil(t, userReport[0])
|
||||
require.Equal(t, u3.Username, userReport[0].Username)
|
||||
|
||||
require.NotNil(t, userReport[1])
|
||||
require.Equal(t, u2.Username, userReport[1].Username)
|
||||
|
||||
require.NotNil(t, userReport[2])
|
||||
require.Equal(t, u1.Username, userReport[2].Username)
|
||||
})
|
||||
|
||||
t.Run("should fail on invalid sort column", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "FakeColumn",
|
||||
SortDesc: true,
|
||||
PageSize: 50,
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, userReport)
|
||||
})
|
||||
|
||||
t.Run("should only return amount of users in page", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 2,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userReport)
|
||||
require.Equal(t, 2, len(userReport))
|
||||
|
||||
require.NotNil(t, userReport[0])
|
||||
require.Equal(t, u1.Username, userReport[0].Username)
|
||||
|
||||
require.NotNil(t, userReport[1])
|
||||
require.Equal(t, u2.Username, userReport[1].Username)
|
||||
})
|
||||
|
||||
t.Run("should return correct paging", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
LastSortColumnValue: u2.Username,
|
||||
LastUserId: u2.Id,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userReport)
|
||||
require.Equal(t, 1, len(userReport))
|
||||
|
||||
require.NotNil(t, userReport[0])
|
||||
require.Equal(t, u3.Username, userReport[0].Username)
|
||||
})
|
||||
|
||||
t.Run("should return accurate post stats for various date ranges", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 3)
|
||||
|
||||
require.Equal(t, 5, *userReport[0].TotalPosts)
|
||||
require.Equal(t, 5, *userReport[0].DaysActive)
|
||||
require.Equal(t, now.UnixMilli(), *userReport[0].LastPostDate)
|
||||
require.Equal(t, 5, *userReport[1].TotalPosts)
|
||||
require.Equal(t, 5, *userReport[1].DaysActive)
|
||||
require.Equal(t, now.UnixMilli(), *userReport[1].LastPostDate)
|
||||
require.Equal(t, 5, *userReport[2].TotalPosts)
|
||||
require.Equal(t, 5, *userReport[2].DaysActive)
|
||||
require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate)
|
||||
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
StartAt: now.AddDate(0, 0, -2).UnixMilli(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 3)
|
||||
|
||||
require.Equal(t, 3, *userReport[0].TotalPosts)
|
||||
require.Equal(t, 3, *userReport[0].DaysActive)
|
||||
require.Equal(t, now.UnixMilli(), *userReport[0].LastPostDate)
|
||||
require.Equal(t, 3, *userReport[1].TotalPosts)
|
||||
require.Equal(t, 3, *userReport[1].DaysActive)
|
||||
require.Equal(t, now.UnixMilli(), *userReport[1].LastPostDate)
|
||||
require.Equal(t, 3, *userReport[2].TotalPosts)
|
||||
require.Equal(t, 3, *userReport[2].DaysActive)
|
||||
require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate)
|
||||
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 3)
|
||||
|
||||
require.Equal(t, 2, *userReport[0].TotalPosts)
|
||||
require.Equal(t, 2, *userReport[0].DaysActive)
|
||||
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[0].LastPostDate)
|
||||
require.Equal(t, 2, *userReport[1].TotalPosts)
|
||||
require.Equal(t, 2, *userReport[1].DaysActive)
|
||||
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[1].LastPostDate)
|
||||
require.Equal(t, 2, *userReport[2].TotalPosts)
|
||||
require.Equal(t, 2, *userReport[2].DaysActive)
|
||||
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[2].LastPostDate)
|
||||
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
},
|
||||
StartAt: now.AddDate(0, 0, -3).UnixMilli(),
|
||||
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 3)
|
||||
|
||||
require.Equal(t, 1, *userReport[0].TotalPosts)
|
||||
require.Equal(t, 1, *userReport[0].DaysActive)
|
||||
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[0].LastPostDate)
|
||||
require.Equal(t, 1, *userReport[1].TotalPosts)
|
||||
require.Equal(t, 1, *userReport[1].DaysActive)
|
||||
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[1].LastPostDate)
|
||||
require.Equal(t, 1, *userReport[2].TotalPosts)
|
||||
require.Equal(t, 1, *userReport[2].DaysActive)
|
||||
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[2].LastPostDate)
|
||||
})
|
||||
|
||||
t.Run("should filter on roles", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
Role: "system",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 1)
|
||||
require.Equal(t, u2.Id, userReport[0].Id)
|
||||
require.Equal(t, u2.Roles, "system")
|
||||
})
|
||||
|
||||
t.Run("should filter on teams", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
HasNoTeam: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 2)
|
||||
require.Equal(t, u1.Id, userReport[0].Id)
|
||||
require.Equal(t, u2.Id, userReport[1].Id)
|
||||
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
Team: team.Id,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 1)
|
||||
require.Equal(t, u3.Id, userReport[0].Id)
|
||||
})
|
||||
|
||||
t.Run("should filter on activation", func(t *testing.T) {
|
||||
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
HideInactive: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 2)
|
||||
require.Equal(t, u1.Id, userReport[0].Id)
|
||||
require.Equal(t, u2.Id, userReport[1].Id)
|
||||
|
||||
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||
SortColumn: "Username",
|
||||
PageSize: 50,
|
||||
HideActive: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userReport, 1)
|
||||
require.Equal(t, u3.Id, userReport[0].Id)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10709,6 +10709,22 @@ func (s *TimerLayerUserStore) GetUnreadCountForChannel(userID string, channelID
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.UserStore.GetUserReport(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.GetUserReport", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||
start := time.Now()
|
||||
|
||||
|
||||
@@ -2016,6 +2016,14 @@
|
||||
"id": "api.getThreadsForUser.bad_params",
|
||||
"translation": "Before and After parameters to getThreadsForUser are mutually exclusive"
|
||||
},
|
||||
{
|
||||
"id": "api.getUsersForReporting.invalid_active_filter",
|
||||
"translation": "Cannot hide both active and inactive users."
|
||||
},
|
||||
{
|
||||
"id": "api.getUsersForReporting.invalid_team_filter",
|
||||
"translation": "Invalid team id provided."
|
||||
},
|
||||
{
|
||||
"id": "api.image.get.app_error",
|
||||
"translation": "Requested image url cannot be parsed."
|
||||
@@ -7054,10 +7062,26 @@
|
||||
"id": "app.user.get_unread_count.app_error",
|
||||
"translation": "We could not get the unread message count for the user."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_user_report.invalid_sort_column",
|
||||
"translation": "Provided sort column is not valid."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_user_report.store_error",
|
||||
"translation": "There was an error querying the user report."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_users_batch_for_indexing.get_users.app_error",
|
||||
"translation": "Unable to get the users batch for indexing."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_users_for_reporting.bad_date_range",
|
||||
"translation": "Date range provided is invalid."
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_users_for_reporting.invalid_page_size",
|
||||
"translation": "Page size is invalid or too large."
|
||||
},
|
||||
{
|
||||
"id": "app.user.missing_account.const",
|
||||
"translation": "Unable to find the user."
|
||||
|
||||
@@ -1918,6 +1918,54 @@ func (c *Client4) EnableUserAccessToken(ctx context.Context, tokenId string) (*R
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) GetUsersForReporting(ctx context.Context, options *UserReportOptionsAPI) ([]*UserReport, *Response, error) {
|
||||
values := url.Values{}
|
||||
if options.SortColumn != "" {
|
||||
values.Set("sort_column", options.SortColumn)
|
||||
}
|
||||
if options.PageSize > 0 {
|
||||
values.Set("page_size", strconv.Itoa(options.PageSize))
|
||||
}
|
||||
if options.Team != "" {
|
||||
values.Set("team_filter", options.Team)
|
||||
}
|
||||
if options.HideActive {
|
||||
values.Set("hide_active", "true")
|
||||
}
|
||||
if options.HideInactive {
|
||||
values.Set("hide_inactive", "true")
|
||||
}
|
||||
if options.SortDesc {
|
||||
values.Set("sort_direction", "desc")
|
||||
}
|
||||
if options.LastSortColumnValue != "" {
|
||||
values.Set("last_column_value", options.LastSortColumnValue)
|
||||
}
|
||||
if options.LastUserId != "" {
|
||||
values.Set("last_id", options.LastUserId)
|
||||
}
|
||||
if options.Role != "" {
|
||||
values.Set("role_filter", options.Role)
|
||||
}
|
||||
if options.HasNoTeam {
|
||||
values.Set("has_no_team", "true")
|
||||
}
|
||||
if options.DateRange != "" {
|
||||
values.Set("date_range", options.DateRange)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/report?"+values.Encode(), "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
var list []*UserReport
|
||||
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
|
||||
return nil, nil, NewAppError("GetUsersForReporting", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return list, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// Bots section
|
||||
|
||||
// CreateBot creates a bot in the system based on the provided bot struct.
|
||||
|
||||
@@ -48,6 +48,10 @@ const (
|
||||
PushThreadsNotifyProp = "push_threads"
|
||||
EmailThreadsNotifyProp = "email_threads"
|
||||
|
||||
ReportDurationLast30Days = "last_30_days"
|
||||
ReportDurationPreviousMonth = "previous_month"
|
||||
ReportDurationLast6Months = "last_6_months"
|
||||
|
||||
DefaultLocale = "en"
|
||||
UserAuthServiceEmail = "email"
|
||||
|
||||
@@ -67,6 +71,10 @@ const (
|
||||
DesktopTokenTTL = time.Minute * 3
|
||||
)
|
||||
|
||||
var (
|
||||
UserReportSortColumns = []string{"CreateAt", "Username", "FirstName", "LastName", "Nickname", "Email", "Roles"}
|
||||
)
|
||||
|
||||
//msgp:tuple User
|
||||
|
||||
// User contains the details about the user.
|
||||
@@ -1007,3 +1015,82 @@ type UsersWithGroupsAndCount struct {
|
||||
Users []*UserWithGroups `json:"users"`
|
||||
Count int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
type UserPostStats struct {
|
||||
LastLogin int64 `json:"last_login_at,omitempty"`
|
||||
LastStatusAt *int64 `json:"last_status_at,omitempty"`
|
||||
LastPostDate *int64 `json:"last_post_date,omitempty"`
|
||||
DaysActive *int `json:"days_active,omitempty"`
|
||||
TotalPosts *int `json:"total_posts,omitempty"`
|
||||
}
|
||||
|
||||
type UserReportQuery struct {
|
||||
User
|
||||
UserPostStats
|
||||
}
|
||||
|
||||
type UserReport struct {
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
CreateAt int64 `json:"create_at,omitempty"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Roles string `json:"roles"`
|
||||
UserPostStats
|
||||
}
|
||||
|
||||
type UserReportOptionsWithoutDateRange struct {
|
||||
SortColumn string
|
||||
SortDesc bool
|
||||
PageSize int
|
||||
LastSortColumnValue string
|
||||
LastUserId string
|
||||
Role string
|
||||
Team string
|
||||
HasNoTeam bool
|
||||
HideActive bool
|
||||
HideInactive bool
|
||||
}
|
||||
|
||||
type UserReportOptions struct {
|
||||
UserReportOptionsWithoutDateRange
|
||||
StartAt int64
|
||||
EndAt int64
|
||||
}
|
||||
|
||||
type UserReportOptionsAPI struct {
|
||||
UserReportOptionsWithoutDateRange
|
||||
DateRange string
|
||||
}
|
||||
|
||||
func (u *UserReportOptionsAPI) ToBaseOptions(now time.Time) *UserReportOptions {
|
||||
startAt := int64(0)
|
||||
endAt := int64(0)
|
||||
if u.DateRange == ReportDurationLast30Days {
|
||||
startAt = now.AddDate(0, 0, -30).UnixMilli()
|
||||
} else if u.DateRange == ReportDurationPreviousMonth {
|
||||
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local)
|
||||
startAt = startOfMonth.AddDate(0, -1, 0).UnixMilli()
|
||||
endAt = startOfMonth.UnixMilli()
|
||||
} else if u.DateRange == ReportDurationLast6Months {
|
||||
startAt = now.AddDate(0, -6, -0).UnixMilli()
|
||||
}
|
||||
|
||||
return &UserReportOptions{
|
||||
UserReportOptionsWithoutDateRange: u.UserReportOptionsWithoutDateRange,
|
||||
StartAt: startAt,
|
||||
EndAt: endAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UserReportQuery) ToReport() *UserReport {
|
||||
return &UserReport{
|
||||
Id: u.Id,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
CreateAt: u.CreateAt,
|
||||
DisplayName: u.GetDisplayName(ShowNicknameFullName),
|
||||
Roles: u.Roles,
|
||||
UserPostStats: u.UserPostStats,
|
||||
}
|
||||
}
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Ссылка в новой задаче
Block a user