forked from mirrors/homebox
fix: csv-importer (#10)
* update item fields to support import_ref * add additional rows to CSV importer * add CSV import documentation * update readme * update readme * fix failed test
This commit is contained in:
parent
90813abf76
commit
ca36e3b080
21 changed files with 447 additions and 135 deletions
|
@ -84,12 +84,10 @@ func TestItemsRepository_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result.ID)
|
||||
|
||||
// Cleanup
|
||||
// Cleanup - Also deletes item
|
||||
err = tRepos.Locations.Delete(context.Background(), location.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = tRepos.Items.Delete(context.Background(), result.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestItemsRepository_Create_Location(t *testing.T) {
|
||||
|
@ -111,11 +109,9 @@ func TestItemsRepository_Create_Location(t *testing.T) {
|
|||
assert.Equal(t, result.ID, foundItem.ID)
|
||||
assert.Equal(t, location.ID, foundItem.Edges.Location.ID)
|
||||
|
||||
// Cleanup
|
||||
// Cleanup - Also deletes item
|
||||
err = tRepos.Locations.Delete(context.Background(), location.ID)
|
||||
assert.NoError(t, err)
|
||||
err = tRepos.Items.Delete(context.Background(), result.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestItemsRepository_Delete(t *testing.T) {
|
||||
|
|
|
@ -156,7 +156,8 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
if len(row) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(row) != 14 {
|
||||
|
||||
if len(row) != NumOfCols {
|
||||
return ErrInvalidCsv
|
||||
}
|
||||
|
||||
|
@ -227,15 +228,16 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
}
|
||||
|
||||
log.Info().
|
||||
Str("name", row.Name).
|
||||
Str("name", row.Item.Name).
|
||||
Str("location", row.Location).
|
||||
Strs("labels", row.getLabels()).
|
||||
Str("locationId", locationID.String()).
|
||||
Msgf("Creating Item: %s", row.Name)
|
||||
Msgf("Creating Item: %s", row.Item.Name)
|
||||
|
||||
result, err := svc.repo.Items.Create(ctx, gid, types.ItemCreate{
|
||||
Name: row.Name,
|
||||
Description: row.Description,
|
||||
ImportRef: row.Item.ImportRef,
|
||||
Name: row.Item.Name,
|
||||
Description: row.Item.Description,
|
||||
LabelIDs: labelIDs,
|
||||
LocationID: locationID,
|
||||
})
|
||||
|
@ -246,21 +248,36 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
|
||||
// Update the item with the rest of the data
|
||||
_, err = svc.repo.Items.Update(ctx, types.ItemUpdate{
|
||||
ID: result.ID,
|
||||
Name: result.Name,
|
||||
LocationID: locationID,
|
||||
LabelIDs: labelIDs,
|
||||
Description: result.Description,
|
||||
SerialNumber: row.SerialNumber,
|
||||
ModelNumber: row.ModelNumber,
|
||||
Manufacturer: row.Manufacturer,
|
||||
Notes: row.Notes,
|
||||
PurchaseFrom: row.PurchaseFrom,
|
||||
PurchasePrice: row.parsedPurchasedPrice(),
|
||||
PurchaseTime: row.parsedPurchasedAt(),
|
||||
SoldTo: row.SoldTo,
|
||||
SoldPrice: row.parsedSoldPrice(),
|
||||
SoldTime: row.parsedSoldAt(),
|
||||
// Edges
|
||||
LocationID: locationID,
|
||||
LabelIDs: labelIDs,
|
||||
|
||||
// General Fields
|
||||
ID: result.ID,
|
||||
Name: result.Name,
|
||||
Description: result.Description,
|
||||
Insured: row.Item.Insured,
|
||||
Notes: row.Item.Notes,
|
||||
|
||||
// Identifies the item as imported
|
||||
SerialNumber: row.Item.SerialNumber,
|
||||
ModelNumber: row.Item.ModelNumber,
|
||||
Manufacturer: row.Item.Manufacturer,
|
||||
|
||||
// Purchase
|
||||
PurchaseFrom: row.Item.PurchaseFrom,
|
||||
PurchasePrice: row.Item.PurchasePrice,
|
||||
PurchaseTime: row.Item.PurchaseTime,
|
||||
|
||||
// Warranty
|
||||
LifetimeWarranty: row.Item.LifetimeWarranty,
|
||||
WarrantyExpires: row.Item.WarrantyExpires,
|
||||
WarrantyDetails: row.Item.WarrantyDetails,
|
||||
|
||||
SoldTo: row.Item.SoldTo,
|
||||
SoldPrice: row.Item.SoldPrice,
|
||||
SoldTime: row.Item.SoldTime,
|
||||
SoldNotes: row.Item.SoldNotes,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -5,10 +5,14 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
)
|
||||
|
||||
var ErrInvalidCsv = errors.New("invalid csv")
|
||||
|
||||
const NumOfCols = 21
|
||||
|
||||
func parseFloat(s string) float64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
|
@ -26,60 +30,56 @@ func parseDate(s string) time.Time {
|
|||
return p
|
||||
}
|
||||
|
||||
func parseBool(s string) bool {
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "yes", "1":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseInt(s string) int {
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
||||
|
||||
type csvRow struct {
|
||||
Location string
|
||||
Labels string
|
||||
Name string
|
||||
Description string
|
||||
SerialNumber string
|
||||
ModelNumber string
|
||||
Manufacturer string
|
||||
Notes string
|
||||
PurchaseFrom string
|
||||
PurchasedPrice string
|
||||
PurchasedAt string
|
||||
SoldTo string
|
||||
SoldPrice string
|
||||
SoldAt string
|
||||
Item types.ItemSummary
|
||||
Location string
|
||||
LabelStr string
|
||||
}
|
||||
|
||||
func newCsvRow(row []string) csvRow {
|
||||
return csvRow{
|
||||
Location: row[0],
|
||||
Labels: row[1],
|
||||
Name: row[2],
|
||||
Description: row[3],
|
||||
SerialNumber: row[4],
|
||||
ModelNumber: row[5],
|
||||
Manufacturer: row[6],
|
||||
Notes: row[7],
|
||||
PurchaseFrom: row[8],
|
||||
PurchasedPrice: row[9],
|
||||
PurchasedAt: row[10],
|
||||
SoldTo: row[11],
|
||||
SoldPrice: row[12],
|
||||
SoldAt: row[13],
|
||||
Location: row[1],
|
||||
LabelStr: row[2],
|
||||
Item: types.ItemSummary{
|
||||
ImportRef: row[0],
|
||||
Quantity: parseInt(row[3]),
|
||||
Name: row[4],
|
||||
Description: row[5],
|
||||
Insured: parseBool(row[6]),
|
||||
SerialNumber: row[7],
|
||||
ModelNumber: row[8],
|
||||
Manufacturer: row[9],
|
||||
Notes: row[10],
|
||||
PurchaseFrom: row[11],
|
||||
PurchasePrice: parseFloat(row[12]),
|
||||
PurchaseTime: parseDate(row[13]),
|
||||
LifetimeWarranty: parseBool(row[14]),
|
||||
WarrantyExpires: parseDate(row[15]),
|
||||
WarrantyDetails: row[16],
|
||||
SoldTo: row[17],
|
||||
SoldPrice: parseFloat(row[18]),
|
||||
SoldTime: parseDate(row[19]),
|
||||
SoldNotes: row[20],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c csvRow) parsedSoldPrice() float64 {
|
||||
return parseFloat(c.SoldPrice)
|
||||
}
|
||||
|
||||
func (c csvRow) parsedPurchasedPrice() float64 {
|
||||
return parseFloat(c.PurchasedPrice)
|
||||
}
|
||||
|
||||
func (c csvRow) parsedPurchasedAt() time.Time {
|
||||
return parseDate(c.PurchasedAt)
|
||||
}
|
||||
|
||||
func (c csvRow) parsedSoldAt() time.Time {
|
||||
return parseDate(c.SoldAt)
|
||||
}
|
||||
|
||||
func (c csvRow) getLabels() []string {
|
||||
split := strings.Split(c.Labels, ";")
|
||||
split := strings.Split(c.LabelStr, ";")
|
||||
|
||||
// Trim each
|
||||
for i, s := range split {
|
||||
|
|
|
@ -8,14 +8,13 @@ import (
|
|||
)
|
||||
|
||||
const CSV_DATA = `
|
||||
Location,Labels,Name,Description,Serial Number,Mode Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased At,Sold To,Sold Price,Sold At
|
||||
Garage,IOT;Home Assistant; Z-Wave,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,
|
||||
Living Room,IOT;Home Assistant; Z-Wave,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,
|
||||
Office,IOT;Home Assistant; Z-Wave,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,
|
||||
Downstairs,IOT;Home Assistant; Z-Wave,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,
|
||||
Entry,IOT;Home Assistant; Z-Wave,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,
|
||||
Kitchen,IOT;Home Assistant; Z-Wave,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,39351,Honeywell,,Amazon,65.98,09/30/0202,,,
|
||||
`
|
||||
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
|
||||
,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||
,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
|
||||
,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||
,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,,
|
||||
,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,,
|
||||
,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,`
|
||||
|
||||
func loadcsv() [][]string {
|
||||
reader := csv.NewReader(bytes.NewBuffer([]byte(CSV_DATA)))
|
||||
|
@ -30,7 +29,7 @@ func loadcsv() [][]string {
|
|||
|
||||
func Test_csvRow_getLabels(t *testing.T) {
|
||||
type fields struct {
|
||||
Labels string
|
||||
LabelStr string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -40,28 +39,28 @@ func Test_csvRow_getLabels(t *testing.T) {
|
|||
{
|
||||
name: "basic test",
|
||||
fields: fields{
|
||||
Labels: "IOT;Home Assistant;Z-Wave",
|
||||
LabelStr: "IOT;Home Assistant;Z-Wave",
|
||||
},
|
||||
want: []string{"IOT", "Home Assistant", "Z-Wave"},
|
||||
},
|
||||
{
|
||||
name: "no labels",
|
||||
fields: fields{
|
||||
Labels: "",
|
||||
LabelStr: "",
|
||||
},
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "single label",
|
||||
fields: fields{
|
||||
Labels: "IOT",
|
||||
LabelStr: "IOT",
|
||||
},
|
||||
want: []string{"IOT"},
|
||||
},
|
||||
{
|
||||
name: "trailing semicolon",
|
||||
fields: fields{
|
||||
Labels: "IOT;",
|
||||
LabelStr: "IOT;",
|
||||
},
|
||||
want: []string{"IOT"},
|
||||
},
|
||||
|
@ -69,7 +68,7 @@ func Test_csvRow_getLabels(t *testing.T) {
|
|||
{
|
||||
name: "whitespace",
|
||||
fields: fields{
|
||||
Labels: " IOT; Home Assistant; Z-Wave ",
|
||||
LabelStr: " IOT; Home Assistant; Z-Wave ",
|
||||
},
|
||||
want: []string{"IOT", "Home Assistant", "Z-Wave"},
|
||||
},
|
||||
|
@ -77,7 +76,7 @@ func Test_csvRow_getLabels(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := csvRow{
|
||||
Labels: tt.fields.Labels,
|
||||
LabelStr: tt.fields.LabelStr,
|
||||
}
|
||||
if got := c.getLabels(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("csvRow.getLabels() = %v, want %v", got, tt.want)
|
||||
|
|
|
@ -73,21 +73,21 @@ func TestItemService_CsvImport(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, csvRow := range dataCsv {
|
||||
if csvRow.Name == item.Name {
|
||||
assert.Equal(t, csvRow.Description, item.Description)
|
||||
assert.Equal(t, csvRow.SerialNumber, item.SerialNumber)
|
||||
assert.Equal(t, csvRow.Manufacturer, item.Manufacturer)
|
||||
assert.Equal(t, csvRow.Notes, item.Notes)
|
||||
if csvRow.Item.Name == item.Name {
|
||||
assert.Equal(t, csvRow.Item.Description, item.Description)
|
||||
assert.Equal(t, csvRow.Item.SerialNumber, item.SerialNumber)
|
||||
assert.Equal(t, csvRow.Item.Manufacturer, item.Manufacturer)
|
||||
assert.Equal(t, csvRow.Item.Notes, item.Notes)
|
||||
|
||||
// Purchase Fields
|
||||
assert.Equal(t, csvRow.parsedPurchasedAt(), item.PurchaseTime)
|
||||
assert.Equal(t, csvRow.PurchaseFrom, item.PurchaseFrom)
|
||||
assert.Equal(t, csvRow.parsedPurchasedPrice(), item.PurchasePrice)
|
||||
assert.Equal(t, csvRow.Item.PurchaseTime, item.PurchaseTime)
|
||||
assert.Equal(t, csvRow.Item.PurchaseFrom, item.PurchaseFrom)
|
||||
assert.Equal(t, csvRow.Item.PurchasePrice, item.PurchasePrice)
|
||||
|
||||
// Sold Fields
|
||||
assert.Equal(t, csvRow.parsedSoldAt(), item.SoldTime)
|
||||
assert.Equal(t, csvRow.SoldTo, item.SoldTo)
|
||||
assert.Equal(t, csvRow.parsedSoldPrice(), item.SoldPrice)
|
||||
assert.Equal(t, csvRow.Item.SoldTime, item.SoldTime)
|
||||
assert.Equal(t, csvRow.Item.SoldTo, item.SoldTo)
|
||||
assert.Equal(t, csvRow.Item.SoldPrice, item.SoldPrice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
type ItemCreate struct {
|
||||
ImportRef string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
||||
|
@ -53,6 +54,7 @@ type ItemUpdate struct {
|
|||
}
|
||||
|
||||
type ItemSummary struct {
|
||||
ImportRef string `json:"-"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue