diff --git a/backend/internal/core/services/reporting/.testdata/import.csv b/backend/internal/core/services/reporting/.testdata/import.csv deleted file mode 100644 index 08bd9c8..0000000 --- a/backend/internal/core/services/reporting/.testdata/import.csv +++ /dev/null @@ -1,7 +0,0 @@ -Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Mode Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,Sold Notes -A,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,Description 1,TRUE,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,10/13/2021,,,,10/13/2021, -B,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,Description 2,FALSE,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,10/15/2021,,,,10/15/2021, -C,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,Description 3,TRUE,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,10/13/2021,,,,10/13/2021, -D,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,Description 4,FALSE,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,10/21/2020,,,,10/21/2020, -E,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,Description 5,TRUE,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,10/14/2020,,,,10/14/2020, -F,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,Description 6,FALSE,,39351,Honeywell,,Amazon,65.98,09/30/2020,,09/30/2020,,,,09/30/2020, \ No newline at end of file diff --git a/backend/internal/core/services/reporting/.testdata/import.tsv b/backend/internal/core/services/reporting/.testdata/import.tsv deleted file mode 100644 index 503c777..0000000 --- a/backend/internal/core/services/reporting/.testdata/import.tsv +++ /dev/null @@ -1,7 +0,0 @@ -Import Ref Location Labels Quantity Name Description Insured Serial Number Mode Number Manufacturer Notes Purchase From Purchased Price Purchased Time Lifetime Warranty Warranty Expires Warranty Details Sold To Sold Price Sold Time Sold Notes -A Garage IOT;Home Assistant; Z-Wave 1 Zooz Universal Relay ZEN17 Description 1 TRUE ZEN17 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021 -B Living Room IOT;Home Assistant; Z-Wave 1 Zooz Motion Sensor Description 2 FALSE ZSE18 Zooz Amazon 29.95 10/15/2021 10/15/2021 10/15/2021 -C Office IOT;Home Assistant; Z-Wave 1 Zooz 110v Power Switch Description 3 TRUE ZEN15 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021 -D Downstairs IOT;Home Assistant; Z-Wave 1 Ecolink Z-Wave PIR Motion Sensor Description 4 FALSE PIRZWAVE2.5-ECO Ecolink Amazon 35.58 10/21/2020 10/21/2020 10/21/2020 -E Entry IOT;Home Assistant; Z-Wave 1 Yale Security Touchscreen Deadbolt Description 5 TRUE YRD226ZW2619 Yale Amazon 120.39 10/14/2020 10/14/2020 10/14/2020 -F Kitchen IOT;Home Assistant; Z-Wave 1 Smart Rocker Light Dimmer Description 6 FALSE 39351 Honeywell Amazon 65.98 09/30/2020 09/30/2020 09/30/2020 \ No newline at end of file diff --git a/backend/internal/core/services/reporting/io_row.go b/backend/internal/core/services/reporting/io_row.go index faa5d25..f097c83 100644 --- a/backend/internal/core/services/reporting/io_row.go +++ b/backend/internal/core/services/reporting/io_row.go @@ -83,3 +83,13 @@ func parseLocationString(s string) LocationString { func (csf LocationString) String() string { return strings.Join(csf, " / ") } + +func fromPathSlice(s []repo.LocationPath) LocationString { + v := make(LocationString, len(s)) + + for i := range s { + v[i] = s[i].Name + } + + return v +} diff --git a/backend/internal/core/services/reporting/io_sheet.go b/backend/internal/core/services/reporting/io_sheet.go index 88d1d36..6fb0c9d 100644 --- a/backend/internal/core/services/reporting/io_sheet.go +++ b/backend/internal/core/services/reporting/io_sheet.go @@ -1,6 +1,7 @@ package reporting import ( + "context" "fmt" "io" "reflect" @@ -8,6 +9,7 @@ import ( "strconv" "strings" + "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/types" "github.com/rs/zerolog/log" @@ -151,7 +153,7 @@ func (s *IOSheet) Read(data io.Reader) error { } // Write writes the sheet to a writer. -func (s *IOSheet) ReadItems(items []repo.ItemOut) { +func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.UUID, repos *repo.AllRepos) error { s.Rows = make([]ExportTSVRow, len(items)) extraHeaders := map[string]struct{}{} @@ -160,7 +162,15 @@ func (s *IOSheet) ReadItems(items []repo.ItemOut) { item := items[i] // TODO: Support fetching nested locations - locString := LocationString{item.Location.Name} + locId := item.Location.ID + + locPaths, err := repos.Locations.PathForLoc(context.Background(), GID, locId) + if err != nil { + log.Error().Err(err).Msg("could not get location path") + return err + } + + locString := fromPathSlice(locPaths) labelString := make([]string, len(item.Labels)) @@ -238,6 +248,8 @@ func (s *IOSheet) ReadItems(items []repo.ItemOut) { for _, h := range customHeaders { s.headers = append(s.headers, "HB.field."+h) } + + return nil } // Writes the current sheet to a writer in TSV format. diff --git a/backend/internal/core/services/reporting/io_sheet_test.go b/backend/internal/core/services/reporting/io_sheet_test.go index 9d7f9a0..845a791 100644 --- a/backend/internal/core/services/reporting/io_sheet_test.go +++ b/backend/internal/core/services/reporting/io_sheet_test.go @@ -20,12 +20,6 @@ var ( //go:embed .testdata/import/types.csv customTypesImportCSV []byte - - //go:embed .testdata/import.csv - CSVData_Comma []byte - - //go:embed .testdata/import.tsv - CSVData_Tab []byte ) func TestSheet_Read(t *testing.T) { @@ -189,7 +183,7 @@ func Test_determineSeparator(t *testing.T) { { name: "comma", args: args{ - data: CSVData_Comma, + data: []byte("a,b,c"), }, want: ',', wantErr: false, @@ -197,7 +191,7 @@ func Test_determineSeparator(t *testing.T) { { name: "tab", args: args{ - data: CSVData_Tab, + data: []byte("a\tb\tc"), }, want: '\t', wantErr: false, diff --git a/backend/internal/core/services/service_items.go b/backend/internal/core/services/service_items.go index c1a37d5..3ea79e2 100644 --- a/backend/internal/core/services/service_items.go +++ b/backend/internal/core/services/service_items.go @@ -337,7 +337,10 @@ func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]strin sheet := reporting.IOSheet{} - sheet.ReadItems(items) + err = sheet.ReadItems(ctx, items, GID, svc.repo) + if err != nil { + return nil, err + } return sheet.TSV() } diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index e65a983..28e3968 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -249,6 +249,56 @@ type TreeQuery struct { WithItems bool `json:"withItems" schema:"withItems"` } +type LocationPath struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` +} + +func (lr *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUID) ([]LocationPath, error) { + query := `WITH RECURSIVE location_path AS ( + SELECT id, name, location_children + FROM locations + WHERE id = ? -- Replace ? with the ID of the item's location + AND group_locations = ? -- Replace ? with the ID of the group + + UNION ALL + + SELECT loc.id, loc.name, loc.location_children + FROM locations loc + JOIN location_path lp ON loc.id = lp.location_children + ) + + SELECT id, name + FROM location_path` + + rows, err := lr.db.Sql().QueryContext(ctx, query, locID, GID) + if err != nil { + return nil, err + } + + var locations []LocationPath + + for rows.Next() { + var location LocationPath + if err := rows.Scan(&location.ID, &location.Name); err != nil { + return nil, err + } + locations = append(locations, location) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + // Reverse the order of the locations so that the root is last + for i := len(locations)/2 - 1; i >= 0; i-- { + opp := len(locations) - 1 - i + locations[i], locations[opp] = locations[opp], locations[i] + } + + return locations, nil +} + func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) { query := ` WITH recursive location_tree(id, NAME, parent_id, level, node_type) AS diff --git a/backend/internal/data/repo/repo_locations_test.go b/backend/internal/data/repo/repo_locations_test.go index 644828d..78c9f77 100644 --- a/backend/internal/data/repo/repo_locations_test.go +++ b/backend/internal/data/repo/repo_locations_test.go @@ -146,6 +146,34 @@ func TestItemRepository_TreeQuery(t *testing.T) { } } +func TestLocationRepository_PathForLoc(t *testing.T) { + locs := useLocations(t, 3) + + // Set relations 3 -> 2 -> 1 + for i := 0; i < 2; i++ { + _, err := tRepos.Locations.UpdateByGroup(context.Background(), tGroup.ID, locs[i].ID, LocationUpdate{ + ID: locs[i].ID, + ParentID: locs[i+1].ID, + Name: locs[i].Name, + Description: locs[i].Description, + }) + assert.NoError(t, err) + } + + last := locs[0] + + path, err := tRepos.Locations.PathForLoc(context.Background(), tGroup.ID, last.ID) + + assert.NoError(t, err) + assert.Equal(t, 3, len(path)) + + // Check path and order + for i, loc := range path { + assert.Equal(t, locs[i].ID, loc.ID) + assert.Equal(t, locs[i].Name, loc.Name) + } +} + func TestConvertLocationsToTree(t *testing.T) { uuid1, uuid2, uuid3, uuid4 := uuid.New(), uuid.New(), uuid.New(), uuid.New()