Skip to content

Commit

Permalink
Merge pull request #235 from qor/fix-many2many-selector-bug
Browse files Browse the repository at this point in the history
Fix many2many relationship not work with versioning bug
  • Loading branch information
raven-chen authored Jun 30, 2021
2 parents 422fa7d + b031b9c commit e57faf2
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 23 deletions.
155 changes: 154 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## QOR Admin
# QOR Admin

Instantly create a beautiful, cross platform, configurable Admin Interface and API for managing your data in minutes.

Expand Down Expand Up @@ -67,6 +67,159 @@ func main() {

`go run main.go` and visit `localhost:9000/admin` to see the result!

## How to use remoteSelector with publish2.version integrated record

### **For has many relationship**
Suppose we have 2 models Factory and Item. Factory **has many** Items.

In the struct, you need add a field `resource.CompositePrimaryKeyField` to the "many" side, which is `Item` here.
```go
type Factory struct {
gorm.Model
Name string

publish2.Version
Items []Item `gorm:"many2many:factory_items;association_autoupdate:false"`
ItemsSorter sorting.SortableCollection
}

type Item struct {
gorm.Model
Name string
publish2.Version

// github.com/qor/qor/resource
resource.CompositePrimaryKeyField // Required
}
```

Then define a remote resource selector. You need configure the `ID` meta like below to make it support composite primary key, this is mandatory.
```go
func generateRemoteItemSelector(adm *admin.Admin) (res *admin.Resource) {
res = adm.AddResource(&Item{}, &admin.Config{Name: "ItemSelector"})
res.IndexAttrs("ID", "Name")

// Required. Convert single ID into composite primary key
res.Meta(&admin.Meta{
Name: "ID",
Valuer: func(value interface{}, ctx *qor.Context) interface{} {
if r, ok := value.(*Item); ok {
// github.com/qor/qor/resource
return resource.GenCompositePrimaryKey(r.ID, r.GetVersionName())
}
return ""
},
})

return res
}
```

Last, use it in the Factory resource.
```go
itemSelector := generateRemoteItemSelector(adm)
factoryRes.Meta(&admin.Meta{
Name: "Items",
Config: &admin.SelectManyConfig{
RemoteDataResource: itemSelector,
},
})
```

### **For has one relationship**
Suppose we have 2 models. Factory and Manager. Factory **has one** Manager.

First, In the struct, you need add a field `resource.CompositePrimaryKeyField` to the "one" side, which is `Manager` here.
```go
type Factory struct {
gorm.Model
Name string
publish2.Version

ManagerID uint
ManagerVersionName string // Required. in "xxxVersionName" format.
Manager Manager
}

type Manager struct {
gorm.Model
Name string
publish2.Version

// github.com/qor/qor/resource
resource.CompositePrimaryKeyField // Required
}
```

Then define a remote resource selector. You need configure the `ID` meta like below to make it support composite primary key, this is mandatory.
```go
func generateRemoteManagerSelector(adm *admin.Admin) (res *admin.Resource) {
res = adm.AddResource(&Manager{}, &admin.Config{Name: "ManagerSelector"})
res.IndexAttrs("ID", "Name")

// Required. Convert single ID into composite primary key
res.Meta(&admin.Meta{
Name: "ID",
Valuer: func(value interface{}, ctx *qor.Context) interface{} {
if r, ok := value.(*Manager); ok {
// github.com/qor/qor/resource
return resource.GenCompositePrimaryKey(r.ID, r.GetVersionName())
}
return ""
},
})

return res
}

Last, use it in the Factory resource.
```go
managerSelector := generateRemoteManagerSelector(adm)
factoryRes.Meta(&admin.Meta{
Name: "Manager",
Config: &admin.SelectOneConfig{
RemoteDataResource: managerSelector,
},
})
```

If you need to overwrite Collection. you have to pass composite primary key as the first element of the returning array instead of ID.
```go
factoryRes.Meta(&admin.Meta{
Name: "Items",
Config: &admin.SelectManyConfig{
Collection: func(value interface{}, ctx *qor.Context) (results [][]string) {
if c, ok := value.(*Factory); ok {
var items []Item
ctx.GetDB().Model(c).Related(&items, "Items")
for _, p := range items {
// The first element must be the composite primary key instead of ID
results = append(results, []string{resource.GenCompositePrimaryKey(p.ID, p.GetVersionName()), p.Name})
}
}
return
},
RemoteDataResource: itemSelector,
},
})
```

## To support assign associations when creating a new version
If you want to assign associations when creating a new version of object immediately. You need to define a function called `AssignVersionName` to the versioned struct with **pointer** receiver which should contains the generating new version name's logic and assign the new version name to the object.
e.g.
```go
func (fac *Factory) AssignVersionName(db *gorm.DB) {
var count int
name := time.Now().Format("2006-01-02")
if err := db.Model(&CollectionWithVersion{}).Where("id = ? AND version_name like ?", fac.ID, name+"%").Count(&count).Error; err != nil {
panic(err)
}
fac.VersionName = fmt.Sprintf("%s-v%v", name, count+1)
}
```
## Live DEMO
* Live Demo [http://demo.getqor.com/admin](http://demo.getqor.com/admin)
Expand Down
25 changes: 17 additions & 8 deletions func_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/jinzhu/inflection"
"github.com/qor/qor"
"github.com/qor/qor/resource"
"github.com/qor/qor/utils"
"github.com/qor/roles"
"github.com/qor/session"
Expand Down Expand Up @@ -379,6 +380,15 @@ func (context *Context) Pagination() *PaginationResult {

func (context *Context) primaryKeyOf(value interface{}) interface{} {
if reflect.Indirect(reflect.ValueOf(value)).Kind() == reflect.Struct {
obj := reflect.Indirect(reflect.ValueOf(value))

for i := 0; i < obj.Type().NumField(); i++ {
// If given struct has CompositePrimaryKey field and it is not nil. return it as the primary key.
if obj.Type().Field(i).Name == resource.CompositePrimaryKeyFieldName && obj.Field(i).FieldByName("CompositePrimaryKey").String() != "" {
return obj.Field(i).FieldByName("CompositePrimaryKey")
}
}

scope := &gorm.Scope{Value: value}
return fmt.Sprint(scope.PrimaryKeyValue())
}
Expand Down Expand Up @@ -647,24 +657,23 @@ func (context *Context) renderMeta(meta *Meta, value interface{}, prefix []strin
}
}

func (context *Context) isEqual(value interface{}, hasValue interface{}) bool {
// isEqual export for test only. If values are struct, compare their primary key. otherwise treat them as string
func (context *Context) isEqual(value interface{}, comparativeValue interface{}) bool {
var result string

if (value == nil || hasValue == nil) && (value != hasValue) {
if (value == nil || comparativeValue == nil) && (value != comparativeValue) {
return false
}

if reflect.Indirect(reflect.ValueOf(hasValue)).Kind() == reflect.Struct {
scope := &gorm.Scope{Value: hasValue}
result = fmt.Sprint(scope.PrimaryKeyValue())
if reflect.Indirect(reflect.ValueOf(comparativeValue)).Kind() == reflect.Struct {
result = fmt.Sprint(context.primaryKeyOf(comparativeValue))
} else {
result = fmt.Sprint(hasValue)
result = fmt.Sprint(comparativeValue)
}

reflectValue := reflect.Indirect(reflect.ValueOf(value))
if reflectValue.Kind() == reflect.Struct {
scope := &gorm.Scope{Value: value}
return fmt.Sprint(scope.PrimaryKeyValue()) == result
return fmt.Sprint(context.primaryKeyOf(value)) == result
} else if reflectValue.Kind() == reflect.String {
// type UserType string, alias type will panic if do
// return reflectValue.Interface().(string) == result
Expand Down
37 changes: 37 additions & 0 deletions func_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/fatih/color"
"github.com/jinzhu/gorm"
"github.com/qor/qor"
)

Expand Down Expand Up @@ -55,3 +56,39 @@ func TestFuncMaps(t *testing.T) {
}
}
}

type FakeStruct struct {
gorm.Model
Name string
}

func TestIsEqual(t *testing.T) {
c1 := FakeStruct{Name: "c1"}
c1.ID = 1
c2 := FakeStruct{Name: "c2"}
c2.ID = 1

context := Context{
Admin: New(&qor.Config{}),
}
if !context.isEqual(c1, c2) {
t.Error("same primary key is not equal")
}

c1.ID = 2
if context.isEqual(c1, c2) {
t.Error("different primary key is equal")
}

a := "a test"
b := "another one"
if context.isEqual(a, b) {
t.Error("different string is equal")
}

c := 11
d := 11
if !context.isEqual(c, d) {
t.Error("same int is not equal")
}
}
16 changes: 2 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,17 @@ module github.com/qor/admin
go 1.13

require (
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/fatih/color v1.9.0
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/sessions v1.2.0 // indirect
github.com/gosimple/slug v1.9.0 // indirect
github.com/jinzhu/gorm v1.9.15
github.com/jinzhu/inflection v1.0.0
github.com/jinzhu/now v1.0.1
github.com/microcosm-cc/bluemonday v1.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/jinzhu/now v1.1.1
github.com/qor/assetfs v0.0.0-20170713023933-ff57fdc13a14
github.com/qor/media v0.0.0-20200720100650-60c52edf57cb
github.com/qor/middlewares v0.0.0-20170822143614-781378b69454 // indirect
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 // indirect
github.com/qor/qor v0.0.0-20200715033016-13227382be83
github.com/qor/qor v0.0.0-20210618082622-a52aba0a0ce1
github.com/qor/responder v0.0.0-20171031032654-b6def473574f
github.com/qor/roles v0.0.0-20171127035124-d6375609fe3e
github.com/qor/serializable_meta v0.0.0-20180510060738-5fd8542db417 // indirect
github.com/qor/session v0.0.0-20170907035918-8206b0adab70
github.com/qor/validations v0.0.0-20171228122639-f364bca61b46 // indirect
github.com/theplant/cldr v0.0.0-20190423050709-9f76f7ce4ee8
github.com/theplant/htmltestingutils v0.0.0-20190423050759-0e06de7b6967
github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61
github.com/yosssi/gohtml v0.0.0-20200519115854-476f5b4b8047 // indirect
)
Loading

0 comments on commit e57faf2

Please sign in to comment.