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:
Hayden 2022-09-12 20:54:30 -08:00 committed by GitHub
parent 90813abf76
commit ca36e3b080
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 447 additions and 135 deletions

View file

@ -30,21 +30,22 @@
- [x] Create
- [x] Update
- [x] Delete
- [ ] Items CRUD
- [x] Items CRUD
- [x] Create
- [ ] Update
- [x] Update
- [x] Delete
- [ ] Asset Attachments for Items
- [x] Fields To Add
- [x] Quantity
- [x] Insured (bool)
- [ ] Bulk Import via CSV
- [x] Bulk Import via CSV
- [x] Initial
- [ ] Add Warranty Columns
- [x] Add Warranty Columns
- [x] All Fields
- [x] Documentations
- [ ] Documentation
- [ ] Docker Compose
- [ ] Config File
- [ ] Import CSV Format
- [ ] TLDR; Getting Started
- [x] Release Flow
- [x] CI/CD Docker Builds w/ Multi-arch

View file

@ -27,6 +27,8 @@ type Item struct {
Name string `json:"name,omitempty"`
// Description holds the value of the "description" field.
Description string `json:"description,omitempty"`
// ImportRef holds the value of the "import_ref" field.
ImportRef string `json:"import_ref,omitempty"`
// Notes holds the value of the "notes" field.
Notes string `json:"notes,omitempty"`
// Quantity holds the value of the "quantity" field.
@ -147,7 +149,7 @@ func (*Item) scanValues(columns []string) ([]interface{}, error) {
values[i] = new(sql.NullFloat64)
case item.FieldQuantity:
values[i] = new(sql.NullInt64)
case item.FieldName, item.FieldDescription, item.FieldNotes, item.FieldSerialNumber, item.FieldModelNumber, item.FieldManufacturer, item.FieldWarrantyDetails, item.FieldPurchaseFrom, item.FieldSoldTo, item.FieldSoldNotes:
case item.FieldName, item.FieldDescription, item.FieldImportRef, item.FieldNotes, item.FieldSerialNumber, item.FieldModelNumber, item.FieldManufacturer, item.FieldWarrantyDetails, item.FieldPurchaseFrom, item.FieldSoldTo, item.FieldSoldNotes:
values[i] = new(sql.NullString)
case item.FieldCreatedAt, item.FieldUpdatedAt, item.FieldWarrantyExpires, item.FieldPurchaseTime, item.FieldSoldTime:
values[i] = new(sql.NullTime)
@ -202,6 +204,12 @@ func (i *Item) assignValues(columns []string, values []interface{}) error {
} else if value.Valid {
i.Description = value.String
}
case item.FieldImportRef:
if value, ok := values[j].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field import_ref", values[j])
} else if value.Valid {
i.ImportRef = value.String
}
case item.FieldNotes:
if value, ok := values[j].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field notes", values[j])
@ -377,6 +385,9 @@ func (i *Item) String() string {
builder.WriteString("description=")
builder.WriteString(i.Description)
builder.WriteString(", ")
builder.WriteString("import_ref=")
builder.WriteString(i.ImportRef)
builder.WriteString(", ")
builder.WriteString("notes=")
builder.WriteString(i.Notes)
builder.WriteString(", ")

View file

@ -21,6 +21,8 @@ const (
FieldName = "name"
// FieldDescription holds the string denoting the description field in the database.
FieldDescription = "description"
// FieldImportRef holds the string denoting the import_ref field in the database.
FieldImportRef = "import_ref"
// FieldNotes holds the string denoting the notes field in the database.
FieldNotes = "notes"
// FieldQuantity holds the string denoting the quantity field in the database.
@ -107,6 +109,7 @@ var Columns = []string{
FieldUpdatedAt,
FieldName,
FieldDescription,
FieldImportRef,
FieldNotes,
FieldQuantity,
FieldInsured,
@ -164,6 +167,8 @@ var (
NameValidator func(string) error
// DescriptionValidator is a validator for the "description" field. It is called by the builders before save.
DescriptionValidator func(string) error
// ImportRefValidator is a validator for the "import_ref" field. It is called by the builders before save.
ImportRefValidator func(string) error
// NotesValidator is a validator for the "notes" field. It is called by the builders before save.
NotesValidator func(string) error
// DefaultQuantity holds the default value on creation for the "quantity" field.

View file

@ -110,6 +110,13 @@ func Description(v string) predicate.Item {
})
}
// ImportRef applies equality check predicate on the "import_ref" field. It's identical to ImportRefEQ.
func ImportRef(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldImportRef), v))
})
}
// Notes applies equality check predicate on the "notes" field. It's identical to NotesEQ.
func Notes(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
@ -562,6 +569,119 @@ func DescriptionContainsFold(v string) predicate.Item {
})
}
// ImportRefEQ applies the EQ predicate on the "import_ref" field.
func ImportRefEQ(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldImportRef), v))
})
}
// ImportRefNEQ applies the NEQ predicate on the "import_ref" field.
func ImportRefNEQ(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.NEQ(s.C(FieldImportRef), v))
})
}
// ImportRefIn applies the In predicate on the "import_ref" field.
func ImportRefIn(vs ...string) predicate.Item {
v := make([]interface{}, len(vs))
for i := range v {
v[i] = vs[i]
}
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.In(s.C(FieldImportRef), v...))
})
}
// ImportRefNotIn applies the NotIn predicate on the "import_ref" field.
func ImportRefNotIn(vs ...string) predicate.Item {
v := make([]interface{}, len(vs))
for i := range v {
v[i] = vs[i]
}
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.NotIn(s.C(FieldImportRef), v...))
})
}
// ImportRefGT applies the GT predicate on the "import_ref" field.
func ImportRefGT(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.GT(s.C(FieldImportRef), v))
})
}
// ImportRefGTE applies the GTE predicate on the "import_ref" field.
func ImportRefGTE(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.GTE(s.C(FieldImportRef), v))
})
}
// ImportRefLT applies the LT predicate on the "import_ref" field.
func ImportRefLT(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.LT(s.C(FieldImportRef), v))
})
}
// ImportRefLTE applies the LTE predicate on the "import_ref" field.
func ImportRefLTE(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.LTE(s.C(FieldImportRef), v))
})
}
// ImportRefContains applies the Contains predicate on the "import_ref" field.
func ImportRefContains(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.Contains(s.C(FieldImportRef), v))
})
}
// ImportRefHasPrefix applies the HasPrefix predicate on the "import_ref" field.
func ImportRefHasPrefix(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.HasPrefix(s.C(FieldImportRef), v))
})
}
// ImportRefHasSuffix applies the HasSuffix predicate on the "import_ref" field.
func ImportRefHasSuffix(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.HasSuffix(s.C(FieldImportRef), v))
})
}
// ImportRefIsNil applies the IsNil predicate on the "import_ref" field.
func ImportRefIsNil() predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.IsNull(s.C(FieldImportRef)))
})
}
// ImportRefNotNil applies the NotNil predicate on the "import_ref" field.
func ImportRefNotNil() predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.NotNull(s.C(FieldImportRef)))
})
}
// ImportRefEqualFold applies the EqualFold predicate on the "import_ref" field.
func ImportRefEqualFold(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.EqualFold(s.C(FieldImportRef), v))
})
}
// ImportRefContainsFold applies the ContainsFold predicate on the "import_ref" field.
func ImportRefContainsFold(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.ContainsFold(s.C(FieldImportRef), v))
})
}
// NotesEQ applies the EQ predicate on the "notes" field.
func NotesEQ(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) {

View file

@ -74,6 +74,20 @@ func (ic *ItemCreate) SetNillableDescription(s *string) *ItemCreate {
return ic
}
// SetImportRef sets the "import_ref" field.
func (ic *ItemCreate) SetImportRef(s string) *ItemCreate {
ic.mutation.SetImportRef(s)
return ic
}
// SetNillableImportRef sets the "import_ref" field if the given value is not nil.
func (ic *ItemCreate) SetNillableImportRef(s *string) *ItemCreate {
if s != nil {
ic.SetImportRef(*s)
}
return ic
}
// SetNotes sets the "notes" field.
func (ic *ItemCreate) SetNotes(s string) *ItemCreate {
ic.mutation.SetNotes(s)
@ -519,6 +533,11 @@ func (ic *ItemCreate) check() error {
return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "Item.description": %w`, err)}
}
}
if v, ok := ic.mutation.ImportRef(); ok {
if err := item.ImportRefValidator(v); err != nil {
return &ValidationError{Name: "import_ref", err: fmt.Errorf(`ent: validator failed for field "Item.import_ref": %w`, err)}
}
}
if v, ok := ic.mutation.Notes(); ok {
if err := item.NotesValidator(v); err != nil {
return &ValidationError{Name: "notes", err: fmt.Errorf(`ent: validator failed for field "Item.notes": %w`, err)}
@ -635,6 +654,14 @@ func (ic *ItemCreate) createSpec() (*Item, *sqlgraph.CreateSpec) {
})
_node.Description = value
}
if value, ok := ic.mutation.ImportRef(); ok {
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
Type: field.TypeString,
Value: value,
Column: item.FieldImportRef,
})
_node.ImportRef = value
}
if value, ok := ic.mutation.Notes(); ok {
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
Type: field.TypeString,

View file

@ -694,6 +694,12 @@ func (iu *ItemUpdate) sqlSave(ctx context.Context) (n int, err error) {
Column: item.FieldDescription,
})
}
if iu.mutation.ImportRefCleared() {
_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
Type: field.TypeString,
Column: item.FieldImportRef,
})
}
if value, ok := iu.mutation.Notes(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeString,
@ -1834,6 +1840,12 @@ func (iuo *ItemUpdateOne) sqlSave(ctx context.Context) (_node *Item, err error)
Column: item.FieldDescription,
})
}
if iuo.mutation.ImportRefCleared() {
_spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
Type: field.TypeString,
Column: item.FieldImportRef,
})
}
if value, ok := iuo.mutation.Notes(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeString,

View file

@ -142,6 +142,7 @@ var (
{Name: "updated_at", Type: field.TypeTime},
{Name: "name", Type: field.TypeString, Size: 255},
{Name: "description", Type: field.TypeString, Nullable: true, Size: 1000},
{Name: "import_ref", Type: field.TypeString, Nullable: true, Size: 100},
{Name: "notes", Type: field.TypeString, Nullable: true, Size: 1000},
{Name: "quantity", Type: field.TypeInt, Default: 1},
{Name: "insured", Type: field.TypeBool, Default: false},
@ -169,15 +170,15 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "items_groups_items",
Columns: []*schema.Column{ItemsColumns[21]},
Columns: []*schema.Column{ItemsColumns[22]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.Cascade,
},
{
Symbol: "items_locations_items",
Columns: []*schema.Column{ItemsColumns[22]},
Columns: []*schema.Column{ItemsColumns[23]},
RefColumns: []*schema.Column{LocationsColumns[0]},
OnDelete: schema.SetNull,
OnDelete: schema.Cascade,
},
},
Indexes: []*schema.Index{
@ -189,17 +190,17 @@ var (
{
Name: "item_manufacturer",
Unique: false,
Columns: []*schema.Column{ItemsColumns[10]},
Columns: []*schema.Column{ItemsColumns[11]},
},
{
Name: "item_model_number",
Unique: false,
Columns: []*schema.Column{ItemsColumns[9]},
Columns: []*schema.Column{ItemsColumns[10]},
},
{
Name: "item_serial_number",
Unique: false,
Columns: []*schema.Column{ItemsColumns[8]},
Columns: []*schema.Column{ItemsColumns[9]},
},
},
}

View file

@ -3413,6 +3413,7 @@ type ItemMutation struct {
updated_at *time.Time
name *string
description *string
import_ref *string
notes *string
quantity *int
addquantity *int
@ -3712,6 +3713,55 @@ func (m *ItemMutation) ResetDescription() {
delete(m.clearedFields, item.FieldDescription)
}
// SetImportRef sets the "import_ref" field.
func (m *ItemMutation) SetImportRef(s string) {
m.import_ref = &s
}
// ImportRef returns the value of the "import_ref" field in the mutation.
func (m *ItemMutation) ImportRef() (r string, exists bool) {
v := m.import_ref
if v == nil {
return
}
return *v, true
}
// OldImportRef returns the old "import_ref" field's value of the Item entity.
// If the Item object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ItemMutation) OldImportRef(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldImportRef is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldImportRef requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldImportRef: %w", err)
}
return oldValue.ImportRef, nil
}
// ClearImportRef clears the value of the "import_ref" field.
func (m *ItemMutation) ClearImportRef() {
m.import_ref = nil
m.clearedFields[item.FieldImportRef] = struct{}{}
}
// ImportRefCleared returns if the "import_ref" field was cleared in this mutation.
func (m *ItemMutation) ImportRefCleared() bool {
_, ok := m.clearedFields[item.FieldImportRef]
return ok
}
// ResetImportRef resets all changes to the "import_ref" field.
func (m *ItemMutation) ResetImportRef() {
m.import_ref = nil
delete(m.clearedFields, item.FieldImportRef)
}
// SetNotes sets the "notes" field.
func (m *ItemMutation) SetNotes(s string) {
m.notes = &s
@ -4750,7 +4800,7 @@ func (m *ItemMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ItemMutation) Fields() []string {
fields := make([]string, 0, 20)
fields := make([]string, 0, 21)
if m.created_at != nil {
fields = append(fields, item.FieldCreatedAt)
}
@ -4763,6 +4813,9 @@ func (m *ItemMutation) Fields() []string {
if m.description != nil {
fields = append(fields, item.FieldDescription)
}
if m.import_ref != nil {
fields = append(fields, item.FieldImportRef)
}
if m.notes != nil {
fields = append(fields, item.FieldNotes)
}
@ -4827,6 +4880,8 @@ func (m *ItemMutation) Field(name string) (ent.Value, bool) {
return m.Name()
case item.FieldDescription:
return m.Description()
case item.FieldImportRef:
return m.ImportRef()
case item.FieldNotes:
return m.Notes()
case item.FieldQuantity:
@ -4876,6 +4931,8 @@ func (m *ItemMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldName(ctx)
case item.FieldDescription:
return m.OldDescription(ctx)
case item.FieldImportRef:
return m.OldImportRef(ctx)
case item.FieldNotes:
return m.OldNotes(ctx)
case item.FieldQuantity:
@ -4945,6 +5002,13 @@ func (m *ItemMutation) SetField(name string, value ent.Value) error {
}
m.SetDescription(v)
return nil
case item.FieldImportRef:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetImportRef(v)
return nil
case item.FieldNotes:
v, ok := value.(string)
if !ok {
@ -5129,6 +5193,9 @@ func (m *ItemMutation) ClearedFields() []string {
if m.FieldCleared(item.FieldDescription) {
fields = append(fields, item.FieldDescription)
}
if m.FieldCleared(item.FieldImportRef) {
fields = append(fields, item.FieldImportRef)
}
if m.FieldCleared(item.FieldNotes) {
fields = append(fields, item.FieldNotes)
}
@ -5179,6 +5246,9 @@ func (m *ItemMutation) ClearField(name string) error {
case item.FieldDescription:
m.ClearDescription()
return nil
case item.FieldImportRef:
m.ClearImportRef()
return nil
case item.FieldNotes:
m.ClearNotes()
return nil
@ -5232,6 +5302,9 @@ func (m *ItemMutation) ResetField(name string) error {
case item.FieldDescription:
m.ResetDescription()
return nil
case item.FieldImportRef:
m.ResetImportRef()
return nil
case item.FieldNotes:
m.ResetNotes()
return nil

View file

@ -227,48 +227,52 @@ func init() {
itemDescDescription := itemMixinFields1[1].Descriptor()
// item.DescriptionValidator is a validator for the "description" field. It is called by the builders before save.
item.DescriptionValidator = itemDescDescription.Validators[0].(func(string) error)
// itemDescImportRef is the schema descriptor for import_ref field.
itemDescImportRef := itemFields[0].Descriptor()
// item.ImportRefValidator is a validator for the "import_ref" field. It is called by the builders before save.
item.ImportRefValidator = itemDescImportRef.Validators[0].(func(string) error)
// itemDescNotes is the schema descriptor for notes field.
itemDescNotes := itemFields[0].Descriptor()
itemDescNotes := itemFields[1].Descriptor()
// item.NotesValidator is a validator for the "notes" field. It is called by the builders before save.
item.NotesValidator = itemDescNotes.Validators[0].(func(string) error)
// itemDescQuantity is the schema descriptor for quantity field.
itemDescQuantity := itemFields[1].Descriptor()
itemDescQuantity := itemFields[2].Descriptor()
// item.DefaultQuantity holds the default value on creation for the quantity field.
item.DefaultQuantity = itemDescQuantity.Default.(int)
// itemDescInsured is the schema descriptor for insured field.
itemDescInsured := itemFields[2].Descriptor()
itemDescInsured := itemFields[3].Descriptor()
// item.DefaultInsured holds the default value on creation for the insured field.
item.DefaultInsured = itemDescInsured.Default.(bool)
// itemDescSerialNumber is the schema descriptor for serial_number field.
itemDescSerialNumber := itemFields[3].Descriptor()
itemDescSerialNumber := itemFields[4].Descriptor()
// item.SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save.
item.SerialNumberValidator = itemDescSerialNumber.Validators[0].(func(string) error)
// itemDescModelNumber is the schema descriptor for model_number field.
itemDescModelNumber := itemFields[4].Descriptor()
itemDescModelNumber := itemFields[5].Descriptor()
// item.ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save.
item.ModelNumberValidator = itemDescModelNumber.Validators[0].(func(string) error)
// itemDescManufacturer is the schema descriptor for manufacturer field.
itemDescManufacturer := itemFields[5].Descriptor()
itemDescManufacturer := itemFields[6].Descriptor()
// item.ManufacturerValidator is a validator for the "manufacturer" field. It is called by the builders before save.
item.ManufacturerValidator = itemDescManufacturer.Validators[0].(func(string) error)
// itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field.
itemDescLifetimeWarranty := itemFields[6].Descriptor()
itemDescLifetimeWarranty := itemFields[7].Descriptor()
// item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field.
item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool)
// itemDescWarrantyDetails is the schema descriptor for warranty_details field.
itemDescWarrantyDetails := itemFields[8].Descriptor()
itemDescWarrantyDetails := itemFields[9].Descriptor()
// item.WarrantyDetailsValidator is a validator for the "warranty_details" field. It is called by the builders before save.
item.WarrantyDetailsValidator = itemDescWarrantyDetails.Validators[0].(func(string) error)
// itemDescPurchasePrice is the schema descriptor for purchase_price field.
itemDescPurchasePrice := itemFields[11].Descriptor()
itemDescPurchasePrice := itemFields[12].Descriptor()
// item.DefaultPurchasePrice holds the default value on creation for the purchase_price field.
item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64)
// itemDescSoldPrice is the schema descriptor for sold_price field.
itemDescSoldPrice := itemFields[14].Descriptor()
itemDescSoldPrice := itemFields[15].Descriptor()
// item.DefaultSoldPrice holds the default value on creation for the sold_price field.
item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64)
// itemDescSoldNotes is the schema descriptor for sold_notes field.
itemDescSoldNotes := itemFields[15].Descriptor()
itemDescSoldNotes := itemFields[16].Descriptor()
// item.SoldNotesValidator is a validator for the "sold_notes" field. It is called by the builders before save.
item.SoldNotesValidator = itemDescSoldNotes.Validators[0].(func(string) error)
// itemDescID is the schema descriptor for id field.

View file

@ -34,6 +34,10 @@ func (Item) Indexes() []ent.Index {
// Fields of the Item.
func (Item) Fields() []ent.Field {
return []ent.Field{
field.String("import_ref").
Optional().
MaxLen(100).
Immutable(),
field.String("notes").
MaxLen(1000).
Optional(),
@ -102,7 +106,10 @@ func (Item) Edges() []ent.Edge {
OnDelete: entsql.Cascade,
}),
edge.From("label", Label.Type).
Ref("items"),
Ref("items").
Annotations(entsql.Annotation{
OnDelete: entsql.Cascade,
}),
edge.To("attachments", Attachment.Type).
Annotations(entsql.Annotation{
OnDelete: entsql.Cascade,

View file

@ -2,6 +2,7 @@ package schema
import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/hay-kot/content/backend/ent/schema/mixins"
@ -35,6 +36,9 @@ func (Label) Edges() []ent.Edge {
Ref("labels").
Required().
Unique(),
edge.To("items", Item.Type),
edge.To("items", Item.Type).
Annotations(entsql.Annotation{
OnDelete: entsql.Cascade,
}),
}
}

View file

@ -2,6 +2,7 @@ package schema
import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/edge"
"github.com/hay-kot/content/backend/ent/schema/mixins"
)
@ -30,6 +31,9 @@ func (Location) Edges() []ent.Edge {
Ref("locations").
Unique().
Required(),
edge.To("items", Item.Type),
edge.To("items", Item.Type).
Annotations(entsql.Annotation{
OnDelete: entsql.Cascade,
}),
}
}

View file

@ -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) {

View file

@ -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,
// Edges
LocationID: locationID,
LabelIDs: labelIDs,
// General Fields
ID: result.ID,
Name: result.Name,
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(),
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 {

View file

@ -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 {
Item types.ItemSummary
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
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 {

View file

@ -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)

View file

@ -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)
}
}
}

View file

@ -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"`

View file

@ -1,5 +1,5 @@
[data-md-color-scheme="homebox"] {
--md-primary-fg-color: #5d5656;
--md-primary-fg-color: #5b7f67;
--md-primary-fg-color--light: #5b7f67;
--md-primary-fg-color--dark: #90030c;
}

View file

@ -1,28 +1,41 @@
# CSV Imports
This document outlines the CSV import feature of Homebox and how to use it.
## Quick Start
Using the CSV import is the recommended way for adding items to the database. It is always going to be the fastest way to import any large amount of items and provides the most flexibility when it comes to adding items.
**Limitations**
- Currently only supports importing items
- Currently only supports importing items, locations, and labels
- Does not support attachments. Attachments must be uploaded after import
**Template**
You can use this snippet as the headers for your CSV. Copy and paste it into your spreadsheet editor of choice and fill in the value.
```csv
Import RefLocation Labels Quantity Name Description Insured Serial Number Model Number Manufacturer Notes Purchase From Purchased Price Purchased Time Lifetime Warranty Warranty Expires Warranty Details Sold To Sold Price Sold Time Sold Notes
```
!!! tip "Column Order"
Column headers are just there for reference, the important thing is that the order is correct. You can change the headers to anything you like.
## CSV Reference
| Column Heading | Type | Description |
| Column | Type | Description |
| ----------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| ImportRef | String (100) | Future |
| Location | String | This is the location of the item that will be created. These are de-duplicated and won't create another instance when reused. |
| Labels | `;` Separated String | List of labels to apply to the item seperated by a `;`, can be existing or new |
| Labels | `;` Separated String | List of labels to apply to the item separated by a `;`, can be existing or new |
| Quantity | Integer | The quantity of items to create |
| Name | String | Name of the item |
| Description | String | Description of the item |
| Insured | Boolean | Whether or not the item is insured |
| Serial Number | String | Serial number of the item |
| Model Number | String | Model of the item |
| Manufacturer | String | Manufacturer of the item |
| Notes | String | General notes about the product |
| Notes | String (1000) | General notes about the product |
| Purchase From | String | Name of the place the item was purchased from |
| Purchase Price | Float64 | |
| Purchase At | Date | Date the item was purchased |
@ -32,10 +45,12 @@ Using the CSV import is the recommended way for adding items to the database. It
| Sold To | String | Name of the person the item was sold to |
| Sold At | Date | Date the item was sold |
| Sold Price | Float64 | |
| Sold Notes | String (1000) | |
**Type Key**
| Type | Format |
| ------ | ------------------ |
| String | Max 255 Characters |
| ------- | --------------------------------------------------- |
| String | Max 255 Characters unless otherwise specified |
| Date | YYYY-MM-DD |
| Boolean | true or false, yes or no, 1 or 0 - case insensitive |

View file

@ -27,3 +27,17 @@ theme:
extra_css:
- assets/stylesheets/extras.css
markdown_extensions:
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- def_list
- pymdownx.highlight
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- admonition
- attr_list
- pymdownx.tabbed
- pymdownx.superfences