New report router and user reporting refactoring (#25713)

* Added materialized view migration

* Renamed mat view

* Added channel membership mat view and indexes

* Added channel membership mat view and indexes

* Added new index

* WIP

* Simplifying user reporting code

* Created app and API layer for cahnnel reporting, reporting refactoring in general

* New router

* Remobved channel reporting meanwhile

* Upodated autogenerated stuff

* Lint fix

* Fixed typo

* api vet

* i18n fix

* Fixed API vetting and removed channel reporting constants

* yaml

* removed app pagination tests
Этот коммит содержится в:
Harshil Sharma
2023-12-14 21:19:19 +05:30
коммит произвёл GitHub
родитель 32880efa25
Коммит 97a23d791e
16 изменённых файлов: 380 добавлений и 1339 удалений

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

@@ -139,6 +139,8 @@ type Routes struct {
Drafts *mux.Router // 'api/v4/drafts'
IPFiltering *mux.Router // 'api/v4/ip_filtering'
Reports *mux.Router // 'api/v4/reports'
}
type API struct {
@@ -265,6 +267,8 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.IPFiltering = api.BaseRoutes.APIRoot.PathPrefix("/ip_filtering").Subrouter()
api.BaseRoutes.Reports = api.BaseRoutes.APIRoot.PathPrefix("/reports").Subrouter()
api.InitUser()
api.InitBot()
api.InitTeam()
@@ -309,6 +313,7 @@ func Init(srv *app.Server) (*API, error) {
api.InitHostedCustomer()
api.InitDrafts()
api.InitIPFiltering()
api.InitReports()
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))

81
server/channels/api4/report.go Обычный файл
Просмотреть файл

@@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
func (api *API) InitReports() {
api.BaseRoutes.Reports.Handle("/users", api.APISessionRequired(getUsersForReporting)).Methods("GET")
}
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.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: sortColumn,
SortDesc: r.URL.Query().Get("sort_direction") == "desc",
PageSize: pageSize,
LastSortColumnValue: r.URL.Query().Get("last_column_value"),
DateRange: r.URL.Query().Get("date_range"),
},
Team: teamFilter,
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,
}
options.PopulateDateRange(time.Now())
// Don't allow fetching more than 100 users at a time from the normal query endpoint
if options.PageSize <= 0 || options.PageSize > model.ReportingMaxPageSize {
c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_page_size", nil, "", http.StatusBadRequest)
return
}
userReports, err := c.App.GetUsersForReporting(options)
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))
}
}

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

@@ -108,8 +108,6 @@ 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) {
@@ -3445,59 +3443,3 @@ 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))
}
}

32
server/channels/app/report.go Обычный файл
Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost/server/public/model"
)
func (a *App) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
if appErr := filter.IsValid(); appErr != nil {
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)
if err != nil {
return nil, model.NewAppError("GetUsersForReporting", "app.report.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
}

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

@@ -24,7 +24,6 @@ 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"
@@ -2818,36 +2817,3 @@ 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
}

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

@@ -1934,31 +1934,17 @@ func TestSendSubscriptionHistoryEvent(t *testing.T) {
}
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{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
StartAt: 1000,
EndAt: 500,
},
StartAt: 1000,
EndAt: 500,
})
require.Error(t, err)
require.Nil(t, userReports)
@@ -1969,7 +1955,7 @@ func TestGetUsersForReporting(t *testing.T) {
defer th.TearDown()
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "FakeColumn",
PageSize: 50,
},
@@ -2015,7 +2001,7 @@ func TestGetUsersForReporting(t *testing.T) {
mockStore.On("User").Return(&mockUserStore)
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},

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

@@ -6247,7 +6247,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should return info for all the users", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},
@@ -6268,7 +6268,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should return in the correct order", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
SortDesc: true,
PageSize: 50,
@@ -6290,7 +6290,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should fail on invalid sort column", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "FakeColumn",
SortDesc: true,
PageSize: 50,
@@ -6302,7 +6302,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should only return amount of users in page", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 2,
},
@@ -6320,12 +6320,12 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should return correct paging", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
LastSortColumnValue: u2.Username,
LastUserId: u2.Id,
},
LastUserId: u2.Id,
})
require.NoError(t, err)
require.NotNil(t, userReport)
@@ -6337,7 +6337,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
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{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},
@@ -6356,11 +6356,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate)
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
StartAt: now.AddDate(0, 0, -2).UnixMilli(),
},
StartAt: now.AddDate(0, 0, -2).UnixMilli(),
})
require.NoError(t, err)
require.Len(t, userReport, 3)
@@ -6376,11 +6376,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate)
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
},
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
})
require.NoError(t, err)
require.Len(t, userReport, 3)
@@ -6396,12 +6396,12 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[2].LastPostDate)
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
StartAt: now.AddDate(0, 0, -3).UnixMilli(),
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
},
StartAt: now.AddDate(0, 0, -3).UnixMilli(),
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
})
require.NoError(t, err)
require.Len(t, userReport, 3)
@@ -6419,11 +6419,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should filter on roles", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
Role: "system",
},
Role: "system",
})
require.NoError(t, err)
require.Len(t, userReport, 1)
@@ -6433,11 +6433,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should filter on teams", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
HasNoTeam: true,
},
HasNoTeam: true,
})
require.NoError(t, err)
require.Len(t, userReport, 2)
@@ -6445,11 +6445,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, u2.Id, userReport[1].Id)
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
Team: team.Id,
},
Team: team.Id,
})
require.NoError(t, err)
require.Len(t, userReport, 1)
@@ -6458,11 +6458,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
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,
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},
HideInactive: true,
})
require.NoError(t, err)
require.Len(t, userReport, 2)
@@ -6470,11 +6470,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, u2.Id, userReport[1].Id)
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
HideActive: true,
},
HideActive: true,
})
require.NoError(t, err)
require.Len(t, userReport, 1)