From edb550052c5fdbbfab3e6e135d5b409bc1d57b22 Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Fri, 24 Jan 2025 11:41:29 +0000 Subject: [PATCH 1/9] Admin Requested Office Users Filtering --- pkg/handlers/adminapi/api.go | 10 +- .../adminapi/requested_office_users.go | 100 ++++++++++++++ .../adminapi/requested_office_users_test.go | 130 ++++++++++++++++++ pkg/handlers/ghcapi/tranportation_offices.go | 4 +- .../internalapi/transportation_offices.go | 2 +- pkg/models/roles/roles.go | 22 +++ pkg/models/roles/roles_test.go | 36 +++++ .../mocks/TransportationOfficesFetcher.go | 18 +-- pkg/services/transportation_office.go | 2 +- .../transportation_office_fetcher.go | 18 ++- .../transportation_office_fetcher_test.go | 6 +- .../RequestedOfficeUserList.jsx | 24 +++- .../RequestedOfficeUserShow.module.scss | 4 +- 13 files changed, 346 insertions(+), 30 deletions(-) diff --git a/pkg/handlers/adminapi/api.go b/pkg/handlers/adminapi/api.go index 34d6729a0f2..0af87177ea9 100644 --- a/pkg/handlers/adminapi/api.go +++ b/pkg/handlers/adminapi/api.go @@ -53,16 +53,19 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { adminAPI.ServeError = handlers.ServeCustomError + transportationOfficeFetcher := transportationoffice.NewTransportationOfficesFetcher() + userRolesCreator := usersroles.NewUsersRolesCreator() + newRolesFetcher := roles.NewRolesFetcher() + adminAPI.RequestedOfficeUsersIndexRequestedOfficeUsersHandler = IndexRequestedOfficeUsersHandler{ handlerConfig, requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), query.NewQueryFilter, pagination.NewPagination, + transportationOfficeFetcher, + newRolesFetcher, } - userRolesCreator := usersroles.NewUsersRolesCreator() - newRolesFetcher := roles.NewRolesFetcher() - adminAPI.RequestedOfficeUsersGetRequestedOfficeUserHandler = GetRequestedOfficeUserHandler{ handlerConfig, requestedofficeusers.NewRequestedOfficeUserFetcher(queryBuilder), @@ -119,7 +122,6 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { pagination.NewPagination, } - transportationOfficeFetcher := transportationoffice.NewTransportationOfficesFetcher() adminAPI.TransportationOfficesGetOfficeByIDHandler = GetOfficeByIdHandler{ handlerConfig, transportationOfficeFetcher, diff --git a/pkg/handlers/adminapi/requested_office_users.go b/pkg/handlers/adminapi/requested_office_users.go index ece06237578..e0ab3dc7175 100644 --- a/pkg/handlers/adminapi/requested_office_users.go +++ b/pkg/handlers/adminapi/requested_office_users.go @@ -21,6 +21,7 @@ import ( "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/handlers/authentication/okta" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/notifications" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/query" @@ -148,12 +149,62 @@ func CreateOfficeOktaAccount(appCtx appcontext.AppContext, params requested_offi return res, nil } +// Function that filters Requested Office Users based on filtered Transportation Offices +func filterByTransportationOffice(officeUsers models.OfficeUsers, filteredTransportationOffices models.TransportationOffices) models.OfficeUsers { + var filteredOfficeUsers models.OfficeUsers + var currentOfficeUser models.OfficeUser + var currentTransportationOffice models.TransportationOffice + + for i := range officeUsers { + currentOfficeUser = officeUsers[i] + for j := range filteredTransportationOffices { + currentTransportationOffice = filteredTransportationOffices[j] + + if currentOfficeUser.TransportationOfficeID == currentTransportationOffice.ID { + filteredOfficeUsers = append(filteredOfficeUsers, currentOfficeUser) + } + } + } + + return filteredOfficeUsers +} + +// Function that filters Requested Office Users based on filtered Roles +func filterByRoles(officeUsers models.OfficeUsers, roles roles.Roles) models.OfficeUsers { + var filteredOfficeUsers models.OfficeUsers + filteredRoles := roles + + for i := range officeUsers { + currentOfficeUser := officeUsers[i] + userRoles := currentOfficeUser.User.Roles + hasFilteredRole := false + + for j := range userRoles { + compUserRole := userRoles[j] + for k := range filteredRoles { + compFilteredRole := filteredRoles[k] + if compUserRole.ID == compFilteredRole.ID { + hasFilteredRole = true + } + } + } + + if hasFilteredRole { + filteredOfficeUsers = append(filteredOfficeUsers, currentOfficeUser) + } + } + + return filteredOfficeUsers +} + // IndexRequestedOfficeUsersHandler returns a list of requested office users via GET /requested_office_users type IndexRequestedOfficeUsersHandler struct { handlers.HandlerConfig services.RequestedOfficeUserListFetcher services.NewQueryFilter services.NewPagination + services.TransportationOfficesFetcher + services.RoleAssociater } var requestedOfficeUserFilterConverters = map[string]func(string) []services.QueryFilter{ @@ -167,6 +218,9 @@ var requestedOfficeUserFilterConverters = map[string]func(string) []services.Que }, } +var TransportationOfficeSearch = "transportationOfficeSearch" +var RoleSearch = "rolesSearch" + // Handle retrieves a list of requested office users func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.IndexRequestedOfficeUsersParams) middleware.Responder { return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, @@ -192,6 +246,52 @@ func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.I return handlers.ResponseForError(appCtx.Logger(), err), err } + // Requested office user filters that is being used + requestedOfficeUserFilters := map[string]string{} + + if params.Filter != nil { + if err := json.Unmarshal([]byte(*params.Filter), &requestedOfficeUserFilters); err != nil { + return handlers.ResponseForError(appCtx.Logger(), err), err + } + } + + var filteredTransportationOffices models.TransportationOffices + // If there was a Transportation Office filter applied then get the filtered Transportation Offices + if requestedOfficeUserFilters[TransportationOfficeSearch] != "" { + var tErr error + searchString := requestedOfficeUserFilters[TransportationOfficeSearch] + transportationOfficesFilterResults, tErr := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, searchString, false, true) + if tErr != nil { + appCtx.Logger().Error("Error searching for Transportation Offices using filter: ", zap.Error(err)) + return handlers.ResponseForError(appCtx.Logger(), err), err + } + + filteredTransportationOffices = *transportationOfficesFilterResults + } + + // If there was a Roles filter applied then get the filtered Roles + var filteredRoles roles.Roles + if requestedOfficeUserFilters[RoleSearch] != "" { + var rErr error + rolesFilterResult, err := roles.FindRoles(appCtx.DB(), requestedOfficeUserFilters[RoleSearch]) + if rErr != nil { + appCtx.Logger().Error("Error searching for Roles using filter: ", zap.Error(err)) + return handlers.ResponseForError(appCtx.Logger(), err), err + } + + filteredRoles = rolesFilterResult + } + + // Filter users by filteredTransportationOffices if the filter is used + if len(filteredTransportationOffices) > 0 && len(officeUsers) > 0 { + officeUsers = filterByTransportationOffice(officeUsers, filteredTransportationOffices) + } + + // Filter users by roles if the filter is used + if len(filteredRoles) > 0 && len(officeUsers) > 0 { + officeUsers = filterByRoles(officeUsers, filteredRoles) + } + totalOfficeUsersCount, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersCount(appCtx, queryFilters) if err != nil { return handlers.ResponseForError(appCtx.Logger(), err), err diff --git a/pkg/handlers/adminapi/requested_office_users_test.go b/pkg/handlers/adminapi/requested_office_users_test.go index d84c87ad69c..117741cf35d 100644 --- a/pkg/handlers/adminapi/requested_office_users_test.go +++ b/pkg/handlers/adminapi/requested_office_users_test.go @@ -1,6 +1,7 @@ package adminapi import ( + "encoding/json" "fmt" "net/http" "time" @@ -22,6 +23,7 @@ import ( "github.com/transcom/mymove/pkg/services/pagination" "github.com/transcom/mymove/pkg/services/query" requestedofficeusers "github.com/transcom/mymove/pkg/services/requested_office_users" + transportationofficeservice "github.com/transcom/mymove/pkg/services/transportation_office" ) func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { @@ -486,6 +488,134 @@ func (suite *HandlerSuite) TestUpdateRequestedOfficeUserHandlerWithOktaAccountCr }) } +func (suite *HandlerSuite) TestFilterByTransportationOffice() { + + transportationOffice1 := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + Name: "PPPO Camp Houston", + ProvidesCloseout: false, + }, + }, + }, nil) + transportationOffice2 := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + Name: "PPPO Camp David", + ProvidesCloseout: false, + }, + }, + }, nil) + transportationOffice3 := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + Name: "Fort Bliss", + ProvidesCloseout: false, + }, + }, + }, nil) + + mockRoleAssociator := &mocks.RoleAssociater{} + tioRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypeTIO) + tooRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypeTOO) + scRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypeServicesCounselor) + primeRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypePrimeSimulator) + mockRoles := roles.Roles{tioRole, tooRole, scRole, primeRole} + mockRoleAssociator.On( + "FetchRolesForUser", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + ).Return(mockRoles, nil) + + requestedStatus := models.OfficeUserStatusREQUESTED + + requestedOfficeUsers := models.OfficeUsers{ + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice1, + LinkOnly: true, + }, + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}), + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice2, + LinkOnly: true, + }, + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}), + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice2, + LinkOnly: true, + }, + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}), + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice3, + LinkOnly: true, + }, + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}), + } + + type paramFilter struct { + TransportationOfficeSearch string `json:"transportationOfficeSearch"` + RolesSearch string `json:"rolesSearch"` + } + + var testParamFilter paramFilter + + testParamFilter.RolesSearch = "Task" + testParamFilter.TransportationOfficeSearch = "PPPO" + + testParamFilterJsonStr, err := json.Marshal(testParamFilter) + suite.NoError(err) + + rolesSearchFilterString := string(testParamFilterJsonStr) + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &rolesSearchFilterString, + } + + queryBuilder := query.NewQueryBuilder() + transportationOfficeFetcher := transportationofficeservice.NewTransportationOfficesFetcher() + + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewQueryFilter: query.NewQueryFilter, + NewPagination: pagination.NewPagination, + TransportationOfficesFetcher: transportationOfficeFetcher, + RoleAssociater: mockRoleAssociator, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 2) + suite.Equal(requestedOfficeUsers[0].ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(requestedOfficeUsers[1].ID.String(), okResponse.Payload[1].ID.String()) +} + // Generate and activate Okta endpoints that will be using during the handler func mockAndActivateOktaEndpoints(provider *okta.Provider, responseCode int) { activate := "true" diff --git a/pkg/handlers/ghcapi/tranportation_offices.go b/pkg/handlers/ghcapi/tranportation_offices.go index 405580923bb..0f2e28cff30 100644 --- a/pkg/handlers/ghcapi/tranportation_offices.go +++ b/pkg/handlers/ghcapi/tranportation_offices.go @@ -22,7 +22,7 @@ func (h GetTransportationOfficesHandler) Handle(params transportationofficeop.Ge // B-21022: forPpm param is set true. This is used by PPM closeout widget. Need to ensure certain offices are included/excluded // if location has ppm closedout enabled. - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true) + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true, false) if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) @@ -43,7 +43,7 @@ func (h GetTransportationOfficesOpenHandler) Handle(params transportationofficeo return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, false) + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, false, false) if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) return transportationofficeop.NewGetTransportationOfficesOpenInternalServerError(), err diff --git a/pkg/handlers/internalapi/transportation_offices.go b/pkg/handlers/internalapi/transportation_offices.go index 6b535fd9ba9..4694eba8fed 100644 --- a/pkg/handlers/internalapi/transportation_offices.go +++ b/pkg/handlers/internalapi/transportation_offices.go @@ -51,7 +51,7 @@ func (h GetTransportationOfficesHandler) Handle(params transportationofficeop.Ge return transportationofficeop.NewGetTransportationOfficesForbidden(), noServiceMemberIDErr } - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true) + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true, false) if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) return transportationofficeop.NewGetTransportationOfficesInternalServerError(), err diff --git a/pkg/models/roles/roles.go b/pkg/models/roles/roles.go index 10fedc99b6e..dd326a3f895 100644 --- a/pkg/models/roles/roles.go +++ b/pkg/models/roles/roles.go @@ -94,3 +94,25 @@ func FetchRolesForUser(db *pop.Connection, userID uuid.UUID) (Roles, error) { All(&roles) return roles, err } + +// Fetch like roles based on the search parameter +func FindRoles(db *pop.Connection, search string) (Roles, error) { + var rolesList Roles + + // The % operator filters out strings that are below this similarity threshold + err := db.Q().RawQuery("SET pg_trgm.similarity_threshold = 0.03").Exec() + if err != nil { + return rolesList, err + } + + sqlQuery := `select * from roles where role_name % $1` + + query := db.Q().RawQuery(sqlQuery, search) + if err := query.All(&rolesList); err != nil { + if err != nil { + return rolesList, err + } + } + + return rolesList, nil +} diff --git a/pkg/models/roles/roles_test.go b/pkg/models/roles/roles_test.go index 9ec5a0e7d86..8df5ab6a6be 100644 --- a/pkg/models/roles/roles_test.go +++ b/pkg/models/roles/roles_test.go @@ -3,10 +3,12 @@ package roles_test import ( "testing" + "github.com/gofrs/uuid" "github.com/stretchr/testify/suite" "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" m "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/testingsuite" ) @@ -66,3 +68,37 @@ func (suite *RolesSuite) TestFetchRolesForUser() { suite.NoError(err) suite.Equal(1, len(userRoles), userRoles) } + +func (suite *RolesSuite) TestFindRoles() { + id1, _ := uuid.NewV4() + role1 := roles.Role{ + ID: id1, + RoleName: "Task Invoicing Officer", + RoleType: "role1", + } + + id2, _ := uuid.NewV4() + role2 := roles.Role{ + ID: id2, + RoleName: "Task Ordering Officer", + RoleType: "role2", + } + + id3, _ := uuid.NewV4() + role3 := roles.Role{ + ID: id3, + RoleName: "Contracting Officer", + RoleType: "role3", + } + + // Create roles + rs := roles.Roles{role1, role2, role3} + err := suite.DB().Create(rs) + suite.NoError(err) + + userRoles, err := m.FindRoles(suite.DB(), "Ta") + + suite.NoError(err) + suite.Equal(2, len(userRoles), userRoles) + +} diff --git a/pkg/services/mocks/TransportationOfficesFetcher.go b/pkg/services/mocks/TransportationOfficesFetcher.go index eed32753594..e455e6e082e 100644 --- a/pkg/services/mocks/TransportationOfficesFetcher.go +++ b/pkg/services/mocks/TransportationOfficesFetcher.go @@ -106,9 +106,9 @@ func (_m *TransportationOfficesFetcher) GetTransportationOffice(appCtx appcontex return r0, r1 } -// GetTransportationOffices provides a mock function with given fields: appCtx, search, forPpm -func (_m *TransportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool) (*models.TransportationOffices, error) { - ret := _m.Called(appCtx, search, forPpm) +// GetTransportationOffices provides a mock function with given fields: appCtx, search, forPpm, forAdminOfficeUserReqFilter +func (_m *TransportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (*models.TransportationOffices, error) { + ret := _m.Called(appCtx, search, forPpm, forAdminOfficeUserReqFilter) if len(ret) == 0 { panic("no return value specified for GetTransportationOffices") @@ -116,19 +116,19 @@ func (_m *TransportationOfficesFetcher) GetTransportationOffices(appCtx appconte var r0 *models.TransportationOffices var r1 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool) (*models.TransportationOffices, error)); ok { - return rf(appCtx, search, forPpm) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool, bool) (*models.TransportationOffices, error)); ok { + return rf(appCtx, search, forPpm, forAdminOfficeUserReqFilter) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool) *models.TransportationOffices); ok { - r0 = rf(appCtx, search, forPpm) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool, bool) *models.TransportationOffices); ok { + r0 = rf(appCtx, search, forPpm, forAdminOfficeUserReqFilter) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.TransportationOffices) } } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, bool) error); ok { - r1 = rf(appCtx, search, forPpm) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, bool, bool) error); ok { + r1 = rf(appCtx, search, forPpm, forAdminOfficeUserReqFilter) } else { r1 = ret.Error(1) } diff --git a/pkg/services/transportation_office.go b/pkg/services/transportation_office.go index 307f0d0fef5..f596fbbceae 100644 --- a/pkg/services/transportation_office.go +++ b/pkg/services/transportation_office.go @@ -9,7 +9,7 @@ import ( //go:generate mockery --name TransportationOfficesFetcher type TransportationOfficesFetcher interface { - GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool) (*models.TransportationOffices, error) + GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (*models.TransportationOffices, error) GetTransportationOffice(appCtx appcontext.AppContext, transportationOfficeID uuid.UUID, includeOnlyPPMCloseoutOffices bool) (*models.TransportationOffice, error) GetAllGBLOCs(appCtx appcontext.AppContext) (*models.GBLOCs, error) GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID) (*models.TransportationOffices, error) diff --git a/pkg/services/transportation_office/transportation_office_fetcher.go b/pkg/services/transportation_office/transportation_office_fetcher.go index be942890c75..c67b7234cef 100644 --- a/pkg/services/transportation_office/transportation_office_fetcher.go +++ b/pkg/services/transportation_office/transportation_office_fetcher.go @@ -46,8 +46,8 @@ func (o transportationOfficesFetcher) GetTransportationOffice(appCtx appcontext. return &transportationOffice, nil } -func (o transportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool) (*models.TransportationOffices, error) { - officeList, err := FindTransportationOffice(appCtx, search, forPpm) +func (o transportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (*models.TransportationOffices, error) { + officeList, err := FindTransportationOffice(appCtx, search, forPpm, forAdminOfficeUserReqFilter) if err != nil { switch err { @@ -61,9 +61,15 @@ func (o transportationOfficesFetcher) GetTransportationOffices(appCtx appcontext return &officeList, nil } -func FindTransportationOffice(appCtx appcontext.AppContext, search string, forPpm bool) (models.TransportationOffices, error) { +func FindTransportationOffice(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (models.TransportationOffices, error) { var officeList []models.TransportationOffice + // Changing return limit for Admin Requested Office Users Transportation Office Filter implementation + var limit = 5 + if forAdminOfficeUserReqFilter { + limit = 50 + } + // The % operator filters out strings that are below this similarity threshold err := appCtx.DB().Q().RawQuery("SET pg_trgm.similarity_threshold = 0.03").Exec() if err != nil { @@ -80,13 +86,13 @@ func FindTransportationOffice(appCtx appcontext.AppContext, search string, forPp } sqlQuery += ` order by sim desc - limit 5) + limit $2) select office.* from names n inner join transportation_offices office on n.transportation_office_id = office.id group by office.id order by max(n.sim) desc, office.name - limit 5` - query := appCtx.DB().Q().RawQuery(sqlQuery, search) + limit $2` + query := appCtx.DB().Q().RawQuery(sqlQuery, search, limit) if err := query.All(&officeList); err != nil { if errors.Cause(err).Error() != models.RecordNotFoundErrorString { return officeList, err diff --git a/pkg/services/transportation_office/transportation_office_fetcher_test.go b/pkg/services/transportation_office/transportation_office_fetcher_test.go index 9ce5bab765c..408010619c1 100644 --- a/pkg/services/transportation_office/transportation_office_fetcher_test.go +++ b/pkg/services/transportation_office/transportation_office_fetcher_test.go @@ -43,7 +43,7 @@ func (suite *TransportationOfficeServiceSuite) Test_SearchTransportationOffice() }, }, }, nil) - office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true) + office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true, false) suite.NoError(err) suite.Equal(transportationOffice.Name, office[0].Name) @@ -54,7 +54,7 @@ func (suite *TransportationOfficeServiceSuite) Test_SearchTransportationOffice() func (suite *TransportationOfficeServiceSuite) Test_SearchWithNoTransportationOffices() { - office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true) + office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true, false) suite.NoError(err) suite.Len(office, 0) } @@ -88,7 +88,7 @@ func (suite *TransportationOfficeServiceSuite) Test_SortedTransportationOffices( }, }, nil) - office, err := FindTransportationOffice(suite.AppContextForTest(), "JPPSO", true) + office, err := FindTransportationOffice(suite.AppContextForTest(), "JPPSO", true, false) suite.NoError(err) suite.Equal(transportationOffice1.Name, office[0].Name) diff --git a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx index 9aeb51ea0f9..87f901d0d07 100644 --- a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx +++ b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx @@ -1,5 +1,16 @@ -import React from 'react'; -import { Datagrid, DateField, Filter, List, ReferenceField, TextField, TextInput, TopToolbar } from 'react-admin'; +import { React } from 'react'; +import { + Datagrid, + DateField, + Filter, + List, + ReferenceField, + TextField, + TopToolbar, + ArrayField, + SingleFieldList, + SearchInput, +} from 'react-admin'; import AdminPagination from 'scenes/SystemAdmin/shared/AdminPagination'; @@ -10,7 +21,9 @@ const ListActions = () => { const RequestedOfficeUserListFilter = () => ( - + + + ); @@ -34,6 +47,11 @@ const RequestedOfficeUserList = () => ( + + + + + ); diff --git a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss index b37e5801e46..ba0802b28c7 100644 --- a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss +++ b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss @@ -50,4 +50,6 @@ margin-left: 15px; margin-right: 15px; margin-bottom: 10px; -} \ No newline at end of file +} + +ul { list-style-type: none; } \ No newline at end of file From ef194e4fdfa2bbf2de6003f5ca0ed5d153db9c98 Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Mon, 27 Jan 2025 16:25:40 +0000 Subject: [PATCH 2/9] Adjusting Role display impl --- .../RequestedOfficeUserList.jsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx index 87f901d0d07..6ab0ff379e9 100644 --- a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx +++ b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx @@ -8,8 +8,8 @@ import { TextField, TopToolbar, ArrayField, - SingleFieldList, SearchInput, + useRecordContext, } from 'react-admin'; import AdminPagination from 'scenes/SystemAdmin/shared/AdminPagination'; @@ -29,6 +29,26 @@ const RequestedOfficeUserListFilter = () => ( const defaultSort = { field: 'createdAt', order: 'DESC' }; +const RolesTextField = (user) => { + const { roles } = user; + + let roleStr = ''; + for (let i = 0; i < roles.length; i += 1) { + roleStr += roles[i].roleName; + + if (i < roles.length - 1) { + roleStr += ', '; + } + } + + return roleStr; +}; + +const RolesField = () => { + const record = useRecordContext(); + return
{RolesTextField(record)}
; +}; + const RequestedOfficeUserList = () => ( } @@ -48,9 +68,7 @@ const RequestedOfficeUserList = () => ( - - - + From 3bf5db9b3fc6a26d3cecb08c1e7509afe9da6d7b Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Mon, 27 Jan 2025 18:19:38 +0000 Subject: [PATCH 3/9] Error handling adjustments --- pkg/handlers/adminapi/requested_office_users.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/handlers/adminapi/requested_office_users.go b/pkg/handlers/adminapi/requested_office_users.go index e0ab3dc7175..e81b438b420 100644 --- a/pkg/handlers/adminapi/requested_office_users.go +++ b/pkg/handlers/adminapi/requested_office_users.go @@ -258,10 +258,9 @@ func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.I var filteredTransportationOffices models.TransportationOffices // If there was a Transportation Office filter applied then get the filtered Transportation Offices if requestedOfficeUserFilters[TransportationOfficeSearch] != "" { - var tErr error searchString := requestedOfficeUserFilters[TransportationOfficeSearch] - transportationOfficesFilterResults, tErr := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, searchString, false, true) - if tErr != nil { + transportationOfficesFilterResults, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, searchString, false, true) + if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices using filter: ", zap.Error(err)) return handlers.ResponseForError(appCtx.Logger(), err), err } @@ -272,9 +271,8 @@ func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.I // If there was a Roles filter applied then get the filtered Roles var filteredRoles roles.Roles if requestedOfficeUserFilters[RoleSearch] != "" { - var rErr error rolesFilterResult, err := roles.FindRoles(appCtx.DB(), requestedOfficeUserFilters[RoleSearch]) - if rErr != nil { + if err != nil { appCtx.Logger().Error("Error searching for Roles using filter: ", zap.Error(err)) return handlers.ResponseForError(appCtx.Logger(), err), err } From f148ebe275c0223dd0064e538416b52c5e712ac4 Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Tue, 28 Jan 2025 19:03:18 +0000 Subject: [PATCH 4/9] Adjust role filtering logic --- .../adminapi/requested_office_users.go | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/pkg/handlers/adminapi/requested_office_users.go b/pkg/handlers/adminapi/requested_office_users.go index e81b438b420..647ecc0ad1b 100644 --- a/pkg/handlers/adminapi/requested_office_users.go +++ b/pkg/handlers/adminapi/requested_office_users.go @@ -172,25 +172,18 @@ func filterByTransportationOffice(officeUsers models.OfficeUsers, filteredTransp // Function that filters Requested Office Users based on filtered Roles func filterByRoles(officeUsers models.OfficeUsers, roles roles.Roles) models.OfficeUsers { var filteredOfficeUsers models.OfficeUsers - filteredRoles := roles - for i := range officeUsers { - currentOfficeUser := officeUsers[i] - userRoles := currentOfficeUser.User.Roles - hasFilteredRole := false - - for j := range userRoles { - compUserRole := userRoles[j] - for k := range filteredRoles { - compFilteredRole := filteredRoles[k] - if compUserRole.ID == compFilteredRole.ID { - hasFilteredRole = true - } - } - } + roleIDSet := make(map[uuid.UUID]struct{}) + for _, role := range roles { + roleIDSet[role.ID] = struct{}{} + } - if hasFilteredRole { - filteredOfficeUsers = append(filteredOfficeUsers, currentOfficeUser) + for _, officeUser := range officeUsers { + for _, userRole := range officeUser.User.Roles { + if _, exists := roleIDSet[userRole.ID]; exists { + filteredOfficeUsers = append(filteredOfficeUsers, officeUser) + break + } } } From 7dafc7594d7d437ea762d7e5ced2bb5fe2c9ab18 Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Thu, 30 Jan 2025 15:25:13 +0000 Subject: [PATCH 5/9] test adjustment --- pkg/models/roles/roles_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/models/roles/roles_test.go b/pkg/models/roles/roles_test.go index 8df5ab6a6be..6bcf0c07704 100644 --- a/pkg/models/roles/roles_test.go +++ b/pkg/models/roles/roles_test.go @@ -99,6 +99,5 @@ func (suite *RolesSuite) TestFindRoles() { userRoles, err := m.FindRoles(suite.DB(), "Ta") suite.NoError(err) - suite.Equal(2, len(userRoles), userRoles) - + suite.GreaterOrEqual(len(userRoles), 2) } From b03381c77567fc456c3c3339929f1f341c3e111f Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Wed, 5 Feb 2025 23:01:44 +0000 Subject: [PATCH 6/9] Sorting and filtering fix --- .../adminapi/requested_office_users.go | 143 ++------ .../adminapi/requested_office_users_test.go | 306 ++++++++++-------- .../mocks/RequestedOfficeUserListFetcher.go | 33 +- pkg/services/requested_office_users.go | 3 +- ...requested_office_user_list_fetcher_test.go | 57 ++-- .../requested_office_users_list_fetcher.go | 59 +++- .../RequestedOfficeUserList.jsx | 17 +- 7 files changed, 308 insertions(+), 310 deletions(-) diff --git a/pkg/handlers/adminapi/requested_office_users.go b/pkg/handlers/adminapi/requested_office_users.go index ceee52b22c3..2d3d968cb8c 100644 --- a/pkg/handlers/adminapi/requested_office_users.go +++ b/pkg/handlers/adminapi/requested_office_users.go @@ -3,12 +3,14 @@ package adminapi import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" "strings" "github.com/go-openapi/runtime/middleware" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/spf13/viper" "go.uber.org/zap" @@ -21,7 +23,6 @@ import ( "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/handlers/authentication/okta" "github.com/transcom/mymove/pkg/models" - "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/notifications" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/query" @@ -149,47 +150,6 @@ func CreateOfficeOktaAccount(appCtx appcontext.AppContext, params requested_offi return res, nil } -// Function that filters Requested Office Users based on filtered Transportation Offices -func filterByTransportationOffice(officeUsers models.OfficeUsers, filteredTransportationOffices models.TransportationOffices) models.OfficeUsers { - var filteredOfficeUsers models.OfficeUsers - var currentOfficeUser models.OfficeUser - var currentTransportationOffice models.TransportationOffice - - for i := range officeUsers { - currentOfficeUser = officeUsers[i] - for j := range filteredTransportationOffices { - currentTransportationOffice = filteredTransportationOffices[j] - - if currentOfficeUser.TransportationOfficeID == currentTransportationOffice.ID { - filteredOfficeUsers = append(filteredOfficeUsers, currentOfficeUser) - } - } - } - - return filteredOfficeUsers -} - -// Function that filters Requested Office Users based on filtered Roles -func filterByRoles(officeUsers models.OfficeUsers, roles roles.Roles) models.OfficeUsers { - var filteredOfficeUsers models.OfficeUsers - - roleIDSet := make(map[uuid.UUID]struct{}) - for _, role := range roles { - roleIDSet[role.ID] = struct{}{} - } - - for _, officeUser := range officeUsers { - for _, userRole := range officeUser.User.Roles { - if _, exists := roleIDSet[userRole.ID]; exists { - filteredOfficeUsers = append(filteredOfficeUsers, officeUser) - break - } - } - } - - return filteredOfficeUsers -} - // IndexRequestedOfficeUsersHandler returns a list of requested office users via GET /requested_office_users type IndexRequestedOfficeUsersHandler struct { handlers.HandlerConfig @@ -200,90 +160,53 @@ type IndexRequestedOfficeUsersHandler struct { services.RoleAssociater } -var requestedOfficeUserFilterConverters = map[string]func(string) []services.QueryFilter{ - "search": func(content string) []services.QueryFilter { - nameSearch := fmt.Sprintf("%s%%", content) - return []services.QueryFilter{ - query.NewQueryFilter("email", "ILIKE", fmt.Sprintf("%%%s%%", content)), - query.NewQueryFilter("first_name", "ILIKE", nameSearch), - query.NewQueryFilter("last_name", "ILIKE", nameSearch), +var requestedOfficeUserFilterConverters = map[string]func(string) func(*pop.Query){ + "search": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + nameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.email ILIKE ? OR office_users.first_name ILIKE ? OR office_users.last_name ILIKE ?", nameSearch, nameSearch, nameSearch) } }, -} -var TransportationOfficeSearch = "transportationOfficeSearch" -var RoleSearch = "rolesSearch" + "offices": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + nameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("transportation_offices.name ILIKE ?", nameSearch) + } + }, + + "rolesSearch": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + nameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("roles.role_name ILIKE ?", nameSearch) + } + }, +} // Handle retrieves a list of requested office users func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.IndexRequestedOfficeUsersParams) middleware.Responder { return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - // adding in filters for when a search or filtering is done - queryFilters := generateQueryFilters(appCtx.Logger(), params.Filter, requestedOfficeUserFilterConverters) - - // We only want users that are in a REQUESTED status - queryFilters = append(queryFilters, query.NewQueryFilter("status", "=", "REQUESTED")) - - // adding in pagination for the UI - pagination := h.NewPagination(params.Page, params.PerPage) - ordering := query.NewQueryOrder(params.Sort, params.Order) - - // need to also get the user's roles - queryAssociations := query.NewQueryAssociationsPreload([]services.QueryAssociation{ - query.NewQueryAssociation("User.Roles"), - }) - - officeUsers, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersList(appCtx, queryFilters, queryAssociations, pagination, ordering) - if err != nil { - return handlers.ResponseForError(appCtx.Logger(), err), err - } - - // Requested office user filters that is being used - requestedOfficeUserFilters := map[string]string{} - - if params.Filter != nil { - if err := json.Unmarshal([]byte(*params.Filter), &requestedOfficeUserFilters); err != nil { - return handlers.ResponseForError(appCtx.Logger(), err), err - } - } - - var filteredTransportationOffices models.TransportationOffices - // If there was a Transportation Office filter applied then get the filtered Transportation Offices - if requestedOfficeUserFilters[TransportationOfficeSearch] != "" { - searchString := requestedOfficeUserFilters[TransportationOfficeSearch] - transportationOfficesFilterResults, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, searchString, false, true) + var filtersMap map[string]string + if params.Filter != nil && *params.Filter != "" { + err := json.Unmarshal([]byte(*params.Filter), &filtersMap) if err != nil { - appCtx.Logger().Error("Error searching for Transportation Offices using filter: ", zap.Error(err)) - return handlers.ResponseForError(appCtx.Logger(), err), err + return handlers.ResponseForError(appCtx.Logger(), errors.New("invalid filter format")), err } - - filteredTransportationOffices = *transportationOfficesFilterResults } - // If there was a Roles filter applied then get the filtered Roles - var filteredRoles roles.Roles - if requestedOfficeUserFilters[RoleSearch] != "" { - rolesFilterResult, err := roles.FindRoles(appCtx.DB(), requestedOfficeUserFilters[RoleSearch]) - if err != nil { - appCtx.Logger().Error("Error searching for Roles using filter: ", zap.Error(err)) - return handlers.ResponseForError(appCtx.Logger(), err), err + var filterFuncs []func(*pop.Query) + for key, filterFunc := range requestedOfficeUserFilterConverters { + if filterValue, exists := filtersMap[key]; exists { + filterFuncs = append(filterFuncs, filterFunc(filterValue)) } - - filteredRoles = rolesFilterResult } - // Filter users by filteredTransportationOffices if the filter is used - if len(filteredTransportationOffices) > 0 && len(officeUsers) > 0 { - officeUsers = filterByTransportationOffice(officeUsers, filteredTransportationOffices) - } - - // Filter users by roles if the filter is used - if len(filteredRoles) > 0 && len(officeUsers) > 0 { - officeUsers = filterByRoles(officeUsers, filteredRoles) - } + pagination := h.NewPagination(params.Page, params.PerPage) + ordering := query.NewQueryOrder(params.Sort, params.Order) - totalOfficeUsersCount, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersCount(appCtx, queryFilters) + officeUsers, count, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersList(appCtx, filterFuncs, pagination, ordering) if err != nil { return handlers.ResponseForError(appCtx.Logger(), err), err } @@ -296,7 +219,7 @@ func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.I payload[i] = payloadForRequestedOfficeUserModel(s) } - return requested_office_users.NewIndexRequestedOfficeUsersOK().WithContentRange(fmt.Sprintf("requested office users %d-%d/%d", pagination.Offset(), pagination.Offset()+queriedOfficeUsersCount, totalOfficeUsersCount)).WithPayload(payload), nil + return requested_office_users.NewIndexRequestedOfficeUsersOK().WithContentRange(fmt.Sprintf("requested office users %d-%d/%d", pagination.Offset(), pagination.Offset()+queriedOfficeUsersCount, count)).WithPayload(payload), nil }) } diff --git a/pkg/handlers/adminapi/requested_office_users_test.go b/pkg/handlers/adminapi/requested_office_users_test.go index 117741cf35d..88341d0d13c 100644 --- a/pkg/handlers/adminapi/requested_office_users_test.go +++ b/pkg/handlers/adminapi/requested_office_users_test.go @@ -1,7 +1,6 @@ package adminapi import ( - "encoding/json" "fmt" "net/http" "time" @@ -23,13 +22,10 @@ import ( "github.com/transcom/mymove/pkg/services/pagination" "github.com/transcom/mymove/pkg/services/query" requestedofficeusers "github.com/transcom/mymove/pkg/services/requested_office_users" - transportationofficeservice "github.com/transcom/mymove/pkg/services/transportation_office" ) func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { - // test that everything is wired up suite.Run("requested users result in ok response", func() { - // building two office user with requested status requestedOfficeUsers := models.OfficeUsers{ factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeQae}), factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeQae})} @@ -47,16 +43,184 @@ func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { response := handler.Handle(params) - // should get an ok response suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) suite.Len(okResponse.Payload, 2) - suite.Equal(requestedOfficeUsers[0].ID.String(), okResponse.Payload[0].ID.String()) + requestedOfficeUser1Id := requestedOfficeUsers[0].ID.String() + requestedOfficeUser2Id := requestedOfficeUsers[1].ID.String() + payloadRequestedUser1Id := okResponse.Payload[0].ID.String() + payloadRequestedUser2Id := okResponse.Payload[1].ID.String() + + // requested office users should exist in response no matter the ordering that has been applied + user1ExistsInResponse := requestedOfficeUser1Id == payloadRequestedUser1Id || requestedOfficeUser1Id == payloadRequestedUser2Id + user2ExistsInResponse := requestedOfficeUser2Id == payloadRequestedUser1Id || requestedOfficeUser2Id == payloadRequestedUser2Id + suite.True(user1ExistsInResponse) + suite.True(user2ExistsInResponse) + }) + + suite.Run("able to search by name & email", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // partial first name search + filterJSON := "{\"search\":\"Angel\"}" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + + // search by first name + filterJSON = "{\"search\":\"Bill\"}" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[0].ID.String()) + + // email search + filterJSON = "{\"search\":\"conAir\"}" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[0].ID.String()) + }) + + suite.Run("able to search by transportation office", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + Name: "Tinker", + }, + }, + }, nil) + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + TransportationOfficeID: transportationOffice.ID, + Status: &requestedStatus, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeTOO}) + factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeTIO}) + + filterJSON := "{\"offices\":\"Tinker\"}" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + }) + + suite.Run("able to search by role", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + filterJSON := "{\"rolesSearch\":\"services\"}" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) }) } func (suite *HandlerSuite) TestGetRequestedOfficeUserHandler() { - // test that everything is wired up suite.Run("integration test ok response", func() { requestedOfficeUser := factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeQae}) params := requestedofficeuserop.GetRequestedOfficeUserParams{ @@ -488,134 +652,6 @@ func (suite *HandlerSuite) TestUpdateRequestedOfficeUserHandlerWithOktaAccountCr }) } -func (suite *HandlerSuite) TestFilterByTransportationOffice() { - - transportationOffice1 := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - Name: "PPPO Camp Houston", - ProvidesCloseout: false, - }, - }, - }, nil) - transportationOffice2 := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - Name: "PPPO Camp David", - ProvidesCloseout: false, - }, - }, - }, nil) - transportationOffice3 := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - Name: "Fort Bliss", - ProvidesCloseout: false, - }, - }, - }, nil) - - mockRoleAssociator := &mocks.RoleAssociater{} - tioRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypeTIO) - tooRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypeTOO) - scRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypeServicesCounselor) - primeRole := factory.FetchOrBuildRoleByRoleType(suite.DB(), roles.RoleTypePrimeSimulator) - mockRoles := roles.Roles{tioRole, tooRole, scRole, primeRole} - mockRoleAssociator.On( - "FetchRolesForUser", - mock.AnythingOfType("*appcontext.appContext"), - mock.Anything, - ).Return(mockRoles, nil) - - requestedStatus := models.OfficeUserStatusREQUESTED - - requestedOfficeUsers := models.OfficeUsers{ - factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - { - Model: transportationOffice1, - LinkOnly: true, - }, - { - Model: models.OfficeUser{ - Status: &requestedStatus, - }, - }, - }, []roles.RoleType{roles.RoleTypeTOO}), - factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - { - Model: transportationOffice2, - LinkOnly: true, - }, - { - Model: models.OfficeUser{ - Status: &requestedStatus, - }, - }, - }, []roles.RoleType{roles.RoleTypeTIO}), - factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - { - Model: transportationOffice2, - LinkOnly: true, - }, - { - Model: models.OfficeUser{ - Status: &requestedStatus, - }, - }, - }, []roles.RoleType{roles.RoleTypeServicesCounselor}), - factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - { - Model: transportationOffice3, - LinkOnly: true, - }, - { - Model: models.OfficeUser{ - Status: &requestedStatus, - }, - }, - }, []roles.RoleType{roles.RoleTypeServicesCounselor}), - } - - type paramFilter struct { - TransportationOfficeSearch string `json:"transportationOfficeSearch"` - RolesSearch string `json:"rolesSearch"` - } - - var testParamFilter paramFilter - - testParamFilter.RolesSearch = "Task" - testParamFilter.TransportationOfficeSearch = "PPPO" - - testParamFilterJsonStr, err := json.Marshal(testParamFilter) - suite.NoError(err) - - rolesSearchFilterString := string(testParamFilterJsonStr) - params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ - HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), - Filter: &rolesSearchFilterString, - } - - queryBuilder := query.NewQueryBuilder() - transportationOfficeFetcher := transportationofficeservice.NewTransportationOfficesFetcher() - - handler := IndexRequestedOfficeUsersHandler{ - HandlerConfig: suite.HandlerConfig(), - RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), - NewQueryFilter: query.NewQueryFilter, - NewPagination: pagination.NewPagination, - TransportationOfficesFetcher: transportationOfficeFetcher, - RoleAssociater: mockRoleAssociator, - } - - response := handler.Handle(params) - - suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) - okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) - suite.Len(okResponse.Payload, 2) - suite.Equal(requestedOfficeUsers[0].ID.String(), okResponse.Payload[0].ID.String()) - suite.Equal(requestedOfficeUsers[1].ID.String(), okResponse.Payload[1].ID.String()) -} - // Generate and activate Okta endpoints that will be using during the handler func mockAndActivateOktaEndpoints(provider *okta.Provider, responseCode int) { activate := "true" diff --git a/pkg/services/mocks/RequestedOfficeUserListFetcher.go b/pkg/services/mocks/RequestedOfficeUserListFetcher.go index 98a226808eb..98211d4b57b 100644 --- a/pkg/services/mocks/RequestedOfficeUserListFetcher.go +++ b/pkg/services/mocks/RequestedOfficeUserListFetcher.go @@ -8,6 +8,8 @@ import ( models "github.com/transcom/mymove/pkg/models" + pop "github.com/gobuffalo/pop/v6" + services "github.com/transcom/mymove/pkg/services" ) @@ -44,34 +46,41 @@ func (_m *RequestedOfficeUserListFetcher) FetchRequestedOfficeUsersCount(appCtx return r0, r1 } -// FetchRequestedOfficeUsersList provides a mock function with given fields: appCtx, filters, associations, pagination, ordering -func (_m *RequestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filters []services.QueryFilter, associations services.QueryAssociations, pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, error) { - ret := _m.Called(appCtx, filters, associations, pagination, ordering) +// FetchRequestedOfficeUsersList provides a mock function with given fields: appCtx, filterFuncs, pagination, ordering +func (_m *RequestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, int, error) { + ret := _m.Called(appCtx, filterFuncs, pagination, ordering) if len(ret) == 0 { panic("no return value specified for FetchRequestedOfficeUsersList") } var r0 models.OfficeUsers - var r1 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, []services.QueryFilter, services.QueryAssociations, services.Pagination, services.QueryOrder) (models.OfficeUsers, error)); ok { - return rf(appCtx, filters, associations, pagination, ordering) + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) (models.OfficeUsers, int, error)); ok { + return rf(appCtx, filterFuncs, pagination, ordering) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, []services.QueryFilter, services.QueryAssociations, services.Pagination, services.QueryOrder) models.OfficeUsers); ok { - r0 = rf(appCtx, filters, associations, pagination, ordering) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) models.OfficeUsers); ok { + r0 = rf(appCtx, filterFuncs, pagination, ordering) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(models.OfficeUsers) } } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, []services.QueryFilter, services.QueryAssociations, services.Pagination, services.QueryOrder) error); ok { - r1 = rf(appCtx, filters, associations, pagination, ordering) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) int); ok { + r1 = rf(appCtx, filterFuncs, pagination, ordering) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(int) } - return r0, r1 + if rf, ok := ret.Get(2).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) error); ok { + r2 = rf(appCtx, filterFuncs, pagination, ordering) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // NewRequestedOfficeUserListFetcher creates a new instance of RequestedOfficeUserListFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. diff --git a/pkg/services/requested_office_users.go b/pkg/services/requested_office_users.go index 574d5915c83..ad917128256 100644 --- a/pkg/services/requested_office_users.go +++ b/pkg/services/requested_office_users.go @@ -1,6 +1,7 @@ package services import ( + "github.com/gobuffalo/pop/v6" "github.com/gobuffalo/validate/v3" "github.com/gofrs/uuid" @@ -13,7 +14,7 @@ import ( // //go:generate mockery --name RequestedOfficeUserListFetcher type RequestedOfficeUserListFetcher interface { - FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filters []QueryFilter, associations QueryAssociations, pagination Pagination, ordering QueryOrder) (models.OfficeUsers, error) + FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination Pagination, ordering QueryOrder) (models.OfficeUsers, int, error) FetchRequestedOfficeUsersCount(appCtx appcontext.AppContext, filters []QueryFilter) (int, error) } diff --git a/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go b/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go index b3271f90a9c..7816b440ba7 100644 --- a/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go +++ b/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go @@ -1,26 +1,17 @@ package adminuser import ( - "errors" - "reflect" - - "github.com/gofrs/uuid" - "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/pagination" "github.com/transcom/mymove/pkg/services/query" ) type testRequestedOfficeUsersListQueryBuilder struct { - fakeFetchMany func(appCtx appcontext.AppContext, model interface{}) error - fakeCount func(appCtx appcontext.AppContext, model interface{}) (int, error) -} - -func (t *testRequestedOfficeUsersListQueryBuilder) FetchMany(appCtx appcontext.AppContext, model interface{}, _ []services.QueryFilter, _ services.QueryAssociations, _ services.Pagination, _ services.QueryOrder) error { - m := t.fakeFetchMany(appCtx, model) - return m + fakeCount func(appCtx appcontext.AppContext, model interface{}) (int, error) } func (t *testRequestedOfficeUsersListQueryBuilder) Count(appCtx appcontext.AppContext, model interface{}, _ []services.QueryFilter) (int, error) { @@ -33,50 +24,38 @@ func defaultPagination() services.Pagination { return pagination.NewPagination(&page, &perPage) } -func defaultAssociations() services.QueryAssociations { - return query.NewQueryAssociations([]services.QueryAssociation{}) -} - func defaultOrdering() services.QueryOrder { return query.NewQueryOrder(nil, nil) } func (suite *RequestedOfficeUsersServiceSuite) TestFetchRequestedOfficeUserList() { suite.Run("if the users are successfully fetched, they should be returned", func() { - id, err := uuid.NewV4() - suite.NoError(err) - fakeFetchMany := func(_ appcontext.AppContext, model interface{}) error { - value := reflect.ValueOf(model).Elem() - requestedStatus := models.OfficeUserStatusREQUESTED - value.Set(reflect.Append(value, reflect.ValueOf(models.OfficeUser{ID: id, Status: &requestedStatus}))) - return nil - } - builder := &testRequestedOfficeUsersListQueryBuilder{ - fakeFetchMany: fakeFetchMany, - } + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + builder := &testRequestedOfficeUsersListQueryBuilder{} fetcher := NewRequestedOfficeUsersListFetcher(builder) - requestedOfficeUsers, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultAssociations(), defaultPagination(), defaultOrdering()) + requestedOfficeUsers, _, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), defaultOrdering()) suite.NoError(err) - suite.Equal(id, requestedOfficeUsers[0].ID) + suite.Equal(officeUser1.ID, requestedOfficeUsers[0].ID) }) - suite.Run("if there is an error, we get it with no requested office users", func() { - fakeFetchMany := func(_ appcontext.AppContext, _ interface{}) error { - return errors.New("Fetch error") - } - builder := &testRequestedOfficeUsersListQueryBuilder{ - fakeFetchMany: fakeFetchMany, - } + suite.Run("if there are no requested office users, we don't receive any requested office users", func() { + builder := &testRequestedOfficeUsersListQueryBuilder{} fetcher := NewRequestedOfficeUsersListFetcher(builder) - requestedOfficeUsers, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), []services.QueryFilter{}, defaultAssociations(), defaultPagination(), defaultOrdering()) + requestedOfficeUsers, _, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), defaultOrdering()) - suite.Error(err) - suite.Equal(err.Error(), "Fetch error") + suite.NoError(err) suite.Equal(models.OfficeUsers(nil), requestedOfficeUsers) }) } diff --git a/pkg/services/requested_office_users/requested_office_users_list_fetcher.go b/pkg/services/requested_office_users/requested_office_users_list_fetcher.go index 29004b8485e..3be7f079f79 100644 --- a/pkg/services/requested_office_users/requested_office_users_list_fetcher.go +++ b/pkg/services/requested_office_users/requested_office_users_list_fetcher.go @@ -1,13 +1,17 @@ package adminuser import ( + "fmt" + "sort" + + "github.com/gobuffalo/pop/v6" + "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" ) type requestedOfficeUsersListQueryBuilder interface { - FetchMany(appCtx appcontext.AppContext, model interface{}, filters []services.QueryFilter, associations services.QueryAssociations, pagination services.Pagination, ordering services.QueryOrder) error Count(appCtx appcontext.AppContext, model interface{}, filters []services.QueryFilter) (int, error) } @@ -16,10 +20,57 @@ type requestedOfficeUserListFetcher struct { } // FetchAdminUserList uses the passed query builder to fetch a list of office users -func (o *requestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filters []services.QueryFilter, associations services.QueryAssociations, pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, error) { +func (o *requestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, int, error) { + var query *pop.Query var requestedUsers models.OfficeUsers - err := o.builder.FetchMany(appCtx, &requestedUsers, filters, associations, pagination, ordering) - return requestedUsers, err + + query = appCtx.DB().Q().EagerPreload( + "User.Roles", + "TransportationOffice"). + Join("users", "users.id = office_users.user_id"). + Join("users_roles", "users.id = users_roles.user_id"). + Join("roles", "users_roles.role_id = roles.id"). + Join("transportation_offices", "office_users.transportation_office_id = transportation_offices.id") + + for _, filterFunc := range filterFuncs { + filterFunc(query) + } + + query = query.Where("status = ?", models.OfficeUserStatusREQUESTED) + query.GroupBy("office_users.id") + + var order = "desc" + if ordering.SortOrder() != nil && ordering.SortOrder() == models.BoolPointer(true) { + order = "asc" + } + + var orderTerm = "id" + if ordering.Column() != nil { + orderTerm = *ordering.Column() + } + + query.Order(fmt.Sprintf("%s %s", orderTerm, order)) + query.Select("office_users.*") + + err := query.Paginate(pagination.Page(), pagination.PerPage()).All(&requestedUsers) + if err != nil { + return nil, 0, err + } + + if orderTerm == "transportation_office_id" { + if order == "desc" { + sort.Slice(requestedUsers, func(i, j int) bool { + return requestedUsers[i].TransportationOffice.Name > requestedUsers[j].TransportationOffice.Name + }) + } else { + sort.Slice(requestedUsers, func(i, j int) bool { + return requestedUsers[i].TransportationOffice.Name < requestedUsers[j].TransportationOffice.Name + }) + } + } + + count := query.Paginator.TotalEntriesSize + return requestedUsers, count, nil } // FetchAdminUserList uses the passed query builder to fetch a list of office users diff --git a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx index 6ab0ff379e9..b1f1dc5bec1 100644 --- a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx +++ b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx @@ -1,14 +1,14 @@ -import { React } from 'react'; +import React from 'react'; import { + ArrayField, Datagrid, DateField, Filter, List, ReferenceField, TextField, + TextInput, TopToolbar, - ArrayField, - SearchInput, useRecordContext, } from 'react-admin'; @@ -18,12 +18,11 @@ import AdminPagination from 'scenes/SystemAdmin/shared/AdminPagination'; const ListActions = () => { return ; }; - -const RequestedOfficeUserListFilter = () => ( - - - - +const RequestedOfficeUserListFilter = (props) => ( + + + + ); From 1da01b9ab0793bf516ab7069d1ff5710685e8bd3 Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Thu, 6 Feb 2025 17:00:57 +0000 Subject: [PATCH 7/9] Adding additional test coverage --- .../adminapi/requested_office_users_test.go | 193 ++++++++++++++++++ pkg/models/errors.go | 3 + .../requested_office_users_list_fetcher.go | 2 +- 3 files changed, 197 insertions(+), 1 deletion(-) diff --git a/pkg/handlers/adminapi/requested_office_users_test.go b/pkg/handlers/adminapi/requested_office_users_test.go index 88341d0d13c..38f1c9c8afd 100644 --- a/pkg/handlers/adminapi/requested_office_users_test.go +++ b/pkg/handlers/adminapi/requested_office_users_test.go @@ -140,6 +140,129 @@ func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { suite.Equal(officeUser3.ID.String(), okResponse.Payload[0].ID.String()) }) + suite.Run("test the return of sorted requested office users in asc order", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Kirtland AFB - USAF", + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Fort Knox - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Detroit Arsenal - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + sortColumn := "transportation_office_id" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(true), + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 3) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[1].ID.String()) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[2].ID.String()) + + // sort by transportation office name in desc order + sortColumn = "transportation_office_id" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(false), + } + + queryBuilder = query.NewQueryBuilder() + handler = IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 3) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[1].ID.String()) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[2].ID.String()) + + // sort by first name in asc order + sortColumn = "first_name" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(true), + } + + queryBuilder = query.NewQueryBuilder() + handler = IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 3) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[1].ID.String()) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[2].ID.String()) + }) + suite.Run("able to search by transportation office", func() { transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ { @@ -218,6 +341,76 @@ func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { suite.Len(okResponse.Payload, 1) suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) }) + + suite.Run("return error when querying for unhandled data", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + sortColumn := "unknown_column" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(true), + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&handlers.ErrResponse{}, response) + errResponse := response.(*handlers.ErrResponse) + suite.Equal(http.StatusInternalServerError, errResponse.Code) + errMsg := errResponse.Err.Error() + suite.Equal(errMsg, "Unhandled data error encountered") + }) + + suite.Run("should error when a param filter format is incorrect", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // Invalid format for filter params + filterJSON := "test{\"unknown\":\"value\"}test" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + expectedError := models.ErrInvalidFilterFormat + expectedResponse := &handlers.ErrResponse{ + Code: http.StatusInternalServerError, + Err: expectedError, + } + + suite.Equal(expectedResponse, response) + }) } func (suite *HandlerSuite) TestGetRequestedOfficeUserHandler() { diff --git a/pkg/models/errors.go b/pkg/models/errors.go index 2a1cdeeff0e..78011442f48 100644 --- a/pkg/models/errors.go +++ b/pkg/models/errors.go @@ -63,3 +63,6 @@ var ErrMissingDestinationAddress = errors.New("DESTINATION_ADDRESS_MISSING") // ErrUnsupportedShipmentType is used if the shipment type is not supported by a method var ErrUnsupportedShipmentType = errors.New("UNSUPPORTED_SHIPMENT_TYPE") + +// ErrInvalidFilterFormat is used if the param filter is not in the expected format +var ErrInvalidFilterFormat = errors.New("invalid filter format") diff --git a/pkg/services/requested_office_users/requested_office_users_list_fetcher.go b/pkg/services/requested_office_users/requested_office_users_list_fetcher.go index 3be7f079f79..3c883225b3b 100644 --- a/pkg/services/requested_office_users/requested_office_users_list_fetcher.go +++ b/pkg/services/requested_office_users/requested_office_users_list_fetcher.go @@ -40,7 +40,7 @@ func (o *requestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx ap query.GroupBy("office_users.id") var order = "desc" - if ordering.SortOrder() != nil && ordering.SortOrder() == models.BoolPointer(true) { + if ordering.SortOrder() != nil && *ordering.SortOrder() { order = "asc" } From d53fe96770bde111e8062a31739bec27daf5ef01 Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Thu, 6 Feb 2025 18:23:17 +0000 Subject: [PATCH 8/9] Query adjustment --- pkg/handlers/adminapi/requested_office_users.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/handlers/adminapi/requested_office_users.go b/pkg/handlers/adminapi/requested_office_users.go index 2d3d968cb8c..1571e8dbbc0 100644 --- a/pkg/handlers/adminapi/requested_office_users.go +++ b/pkg/handlers/adminapi/requested_office_users.go @@ -164,21 +164,21 @@ var requestedOfficeUserFilterConverters = map[string]func(string) func(*pop.Quer "search": func(content string) func(*pop.Query) { return func(query *pop.Query) { nameSearch := fmt.Sprintf("%%%s%%", content) - query.Where("office_users.email ILIKE ? OR office_users.first_name ILIKE ? OR office_users.last_name ILIKE ?", nameSearch, nameSearch, nameSearch) + query.Where("office_users.email ILIKE ? AND office_users.status = 'REQUESTED' OR office_users.first_name ILIKE ? AND office_users.status = 'REQUESTED' OR office_users.last_name ILIKE ? AND office_users.status = 'REQUESTED'", nameSearch, nameSearch, nameSearch) } }, "offices": func(content string) func(*pop.Query) { return func(query *pop.Query) { nameSearch := fmt.Sprintf("%%%s%%", content) - query.Where("transportation_offices.name ILIKE ?", nameSearch) + query.Where("transportation_offices.name ILIKE ? AND office_users.status = 'REQUESTED'", nameSearch) } }, "rolesSearch": func(content string) func(*pop.Query) { return func(query *pop.Query) { nameSearch := fmt.Sprintf("%%%s%%", content) - query.Where("roles.role_name ILIKE ?", nameSearch) + query.Where("roles.role_name ILIKE ? AND office_users.status = 'REQUESTED'", nameSearch) } }, } From 9f914506dd379e3a965ecfb416c9efeccfae238d Mon Sep 17 00:00:00 2001 From: Tevin Adams Date: Fri, 7 Feb 2025 15:40:29 +0000 Subject: [PATCH 9/9] Adding tests --- ...requested_office_user_list_fetcher_test.go | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go b/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go index 7816b440ba7..9192bf73d6e 100644 --- a/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go +++ b/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go @@ -58,4 +58,85 @@ func (suite *RequestedOfficeUsersServiceSuite) TestFetchRequestedOfficeUserList( suite.NoError(err) suite.Equal(models.OfficeUsers(nil), requestedOfficeUsers) }) + + suite.Run("should sort and order requested office users", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Kirtland AFB - USAF", + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Fort Knox - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Detroit Arsenal - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + builder := &testRequestedOfficeUsersListQueryBuilder{} + + fetcher := NewRequestedOfficeUsersListFetcher(builder) + + column := "transportation_office_id" + ordering := query.NewQueryOrder(&column, models.BoolPointer(true)) + + requestedOfficeUsers, _, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.NoError(err) + suite.Len(requestedOfficeUsers, 3) + suite.Equal(officeUser3.ID.String(), requestedOfficeUsers[0].ID.String()) + suite.Equal(officeUser2.ID.String(), requestedOfficeUsers[1].ID.String()) + suite.Equal(officeUser1.ID.String(), requestedOfficeUsers[2].ID.String()) + + ordering = query.NewQueryOrder(&column, models.BoolPointer(false)) + + requestedOfficeUsers, _, err = fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.NoError(err) + suite.Len(requestedOfficeUsers, 3) + suite.Equal(officeUser1.ID.String(), requestedOfficeUsers[0].ID.String()) + suite.Equal(officeUser2.ID.String(), requestedOfficeUsers[1].ID.String()) + suite.Equal(officeUser3.ID.String(), requestedOfficeUsers[2].ID.String()) + + column = "unknown_column" + + requestedOfficeUsers, _, err = fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.Error(err) + suite.Len(requestedOfficeUsers, 0) + }) }