Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

B-21961: Add Requested Office Users List Sorting and Filtering #14651

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
edb5500
Admin Requested Office Users Filtering
TevinAdams Jan 24, 2025
fc8b80f
Merge branch 'B-21961-Add-Roles-To-Requested-Office-Users-List-Sortin…
TevinAdams Jan 24, 2025
ac230af
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 27, 2025
ef194e4
Adjusting Role display impl
TevinAdams Jan 27, 2025
93170d6
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 27, 2025
3bf5db9
Error handling adjustments
TevinAdams Jan 27, 2025
a2dc099
Merge branch 'B-21961-Add-Roles-To-Requested-Office-Users-List-Sortin…
TevinAdams Jan 27, 2025
45c6efd
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 28, 2025
f148ebe
Adjust role filtering logic
TevinAdams Jan 28, 2025
64c71cd
Merge branch 'B-21961-Add-Roles-To-Requested-Office-Users-List-Sortin…
TevinAdams Jan 28, 2025
da88ec4
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 28, 2025
615a0dd
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 28, 2025
7544b9c
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 29, 2025
f4fee9a
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 29, 2025
72c9765
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 29, 2025
5e52255
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 30, 2025
7dafc75
test adjustment
TevinAdams Jan 30, 2025
ff22e47
Merge branch 'B-21961-Add-Roles-To-Requested-Office-Users-List-Sortin…
TevinAdams Jan 30, 2025
9fcb0e7
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 30, 2025
2a04382
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 30, 2025
a9927e3
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 30, 2025
12c0720
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 30, 2025
96119b1
Merge branch 'integrationTesting' into B-21961-Add-Roles-To-Requested…
TevinAdams Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions pkg/handlers/adminapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -124,7 +127,6 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI {
pagination.NewPagination,
}

transportationOfficeFetcher := transportationoffice.NewTransportationOfficesFetcher()
adminAPI.TransportationOfficesGetOfficeByIDHandler = GetOfficeByIdHandler{
handlerConfig,
transportationOfficeFetcher,
Expand Down
91 changes: 91 additions & 0 deletions pkg/handlers/adminapi/requested_office_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -148,12 +149,55 @@ 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
services.RequestedOfficeUserListFetcher
services.NewQueryFilter
services.NewPagination
services.TransportationOfficesFetcher
services.RoleAssociater
}

var requestedOfficeUserFilterConverters = map[string]func(string) []services.QueryFilter{
Expand All @@ -167,6 +211,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,
Expand All @@ -192,6 +239,50 @@ 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] != "" {
searchString := requestedOfficeUserFilters[TransportationOfficeSearch]
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))
ryan-mchugh marked this conversation as resolved.
Show resolved Hide resolved
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] != "" {
rolesFilterResult, err := roles.FindRoles(appCtx.DB(), requestedOfficeUserFilters[RoleSearch])
if err != nil {
appCtx.Logger().Error("Error searching for Roles using filter: ", zap.Error(err))
ryan-mchugh marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
130 changes: 130 additions & 0 deletions pkg/handlers/adminapi/requested_office_users_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package adminapi

import (
"encoding/json"
"fmt"
"net/http"
"time"
Expand All @@ -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() {
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions pkg/handlers/ghcapi/tranportation_offices.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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))
Expand All @@ -44,7 +44,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
Expand Down
2 changes: 1 addition & 1 deletion pkg/handlers/internalapi/transportation_offices.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions pkg/models/roles/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading