Skip to content

Commit

Permalink
Merge pull request #14651 from transcom/B-21961-Add-Roles-To-Requeste…
Browse files Browse the repository at this point in the history
…d-Office-Users-List-Sorting-INT

B-21961: Add  Requested Office Users List Sorting and Filtering
  • Loading branch information
TevinAdams authored Jan 30, 2025
2 parents 5367f44 + 96119b1 commit 44764c3
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 30 deletions.
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))
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))
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

0 comments on commit 44764c3

Please sign in to comment.