Skip to content

Commit e57faf2

Browse files
authored
Merge pull request #235 from qor/fix-many2many-selector-bug
Fix many2many relationship not work with versioning bug
2 parents 422fa7d + b031b9c commit e57faf2

File tree

6 files changed

+284
-23
lines changed

6 files changed

+284
-23
lines changed

README.md

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## QOR Admin
1+
# QOR Admin
22

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

@@ -67,6 +67,159 @@ func main() {
6767

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

70+
## How to use remoteSelector with publish2.version integrated record
71+
72+
### **For has many relationship**
73+
Suppose we have 2 models Factory and Item. Factory **has many** Items.
74+
75+
In the struct, you need add a field `resource.CompositePrimaryKeyField` to the "many" side, which is `Item` here.
76+
```go
77+
type Factory struct {
78+
gorm.Model
79+
Name string
80+
81+
publish2.Version
82+
Items []Item `gorm:"many2many:factory_items;association_autoupdate:false"`
83+
ItemsSorter sorting.SortableCollection
84+
}
85+
86+
type Item struct {
87+
gorm.Model
88+
Name string
89+
publish2.Version
90+
91+
// github.com/qor/qor/resource
92+
resource.CompositePrimaryKeyField // Required
93+
}
94+
```
95+
96+
Then define a remote resource selector. You need configure the `ID` meta like below to make it support composite primary key, this is mandatory.
97+
```go
98+
func generateRemoteItemSelector(adm *admin.Admin) (res *admin.Resource) {
99+
res = adm.AddResource(&Item{}, &admin.Config{Name: "ItemSelector"})
100+
res.IndexAttrs("ID", "Name")
101+
102+
// Required. Convert single ID into composite primary key
103+
res.Meta(&admin.Meta{
104+
Name: "ID",
105+
Valuer: func(value interface{}, ctx *qor.Context) interface{} {
106+
if r, ok := value.(*Item); ok {
107+
// github.com/qor/qor/resource
108+
return resource.GenCompositePrimaryKey(r.ID, r.GetVersionName())
109+
}
110+
return ""
111+
},
112+
})
113+
114+
return res
115+
}
116+
```
117+
118+
Last, use it in the Factory resource.
119+
```go
120+
itemSelector := generateRemoteItemSelector(adm)
121+
factoryRes.Meta(&admin.Meta{
122+
Name: "Items",
123+
Config: &admin.SelectManyConfig{
124+
RemoteDataResource: itemSelector,
125+
},
126+
})
127+
```
128+
129+
### **For has one relationship**
130+
Suppose we have 2 models. Factory and Manager. Factory **has one** Manager.
131+
132+
First, In the struct, you need add a field `resource.CompositePrimaryKeyField` to the "one" side, which is `Manager` here.
133+
```go
134+
type Factory struct {
135+
gorm.Model
136+
Name string
137+
publish2.Version
138+
139+
ManagerID uint
140+
ManagerVersionName string // Required. in "xxxVersionName" format.
141+
Manager Manager
142+
}
143+
144+
type Manager struct {
145+
gorm.Model
146+
Name string
147+
publish2.Version
148+
149+
// github.com/qor/qor/resource
150+
resource.CompositePrimaryKeyField // Required
151+
}
152+
```
153+
154+
Then define a remote resource selector. You need configure the `ID` meta like below to make it support composite primary key, this is mandatory.
155+
```go
156+
func generateRemoteManagerSelector(adm *admin.Admin) (res *admin.Resource) {
157+
res = adm.AddResource(&Manager{}, &admin.Config{Name: "ManagerSelector"})
158+
res.IndexAttrs("ID", "Name")
159+
160+
// Required. Convert single ID into composite primary key
161+
res.Meta(&admin.Meta{
162+
Name: "ID",
163+
Valuer: func(value interface{}, ctx *qor.Context) interface{} {
164+
if r, ok := value.(*Manager); ok {
165+
// github.com/qor/qor/resource
166+
return resource.GenCompositePrimaryKey(r.ID, r.GetVersionName())
167+
}
168+
return ""
169+
},
170+
})
171+
172+
return res
173+
}
174+
175+
Last, use it in the Factory resource.
176+
```go
177+
managerSelector := generateRemoteManagerSelector(adm)
178+
factoryRes.Meta(&admin.Meta{
179+
Name: "Manager",
180+
Config: &admin.SelectOneConfig{
181+
RemoteDataResource: managerSelector,
182+
},
183+
})
184+
```
185+
186+
If you need to overwrite Collection. you have to pass composite primary key as the first element of the returning array instead of ID.
187+
```go
188+
factoryRes.Meta(&admin.Meta{
189+
Name: "Items",
190+
Config: &admin.SelectManyConfig{
191+
Collection: func(value interface{}, ctx *qor.Context) (results [][]string) {
192+
if c, ok := value.(*Factory); ok {
193+
var items []Item
194+
ctx.GetDB().Model(c).Related(&items, "Items")
195+
196+
for _, p := range items {
197+
// The first element must be the composite primary key instead of ID
198+
results = append(results, []string{resource.GenCompositePrimaryKey(p.ID, p.GetVersionName()), p.Name})
199+
}
200+
}
201+
return
202+
},
203+
RemoteDataResource: itemSelector,
204+
},
205+
})
206+
```
207+
208+
## To support assign associations when creating a new version
209+
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.
210+
e.g.
211+
```go
212+
func (fac *Factory) AssignVersionName(db *gorm.DB) {
213+
var count int
214+
name := time.Now().Format("2006-01-02")
215+
if err := db.Model(&CollectionWithVersion{}).Where("id = ? AND version_name like ?", fac.ID, name+"%").Count(&count).Error; err != nil {
216+
panic(err)
217+
}
218+
fac.VersionName = fmt.Sprintf("%s-v%v", name, count+1)
219+
}
220+
```
221+
222+
70223
## Live DEMO
71224
72225
* Live Demo [http://demo.getqor.com/admin](http://demo.getqor.com/admin)

func_map.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/jinzhu/gorm"
2121
"github.com/jinzhu/inflection"
2222
"github.com/qor/qor"
23+
"github.com/qor/qor/resource"
2324
"github.com/qor/qor/utils"
2425
"github.com/qor/roles"
2526
"github.com/qor/session"
@@ -379,6 +380,15 @@ func (context *Context) Pagination() *PaginationResult {
379380

380381
func (context *Context) primaryKeyOf(value interface{}) interface{} {
381382
if reflect.Indirect(reflect.ValueOf(value)).Kind() == reflect.Struct {
383+
obj := reflect.Indirect(reflect.ValueOf(value))
384+
385+
for i := 0; i < obj.Type().NumField(); i++ {
386+
// If given struct has CompositePrimaryKey field and it is not nil. return it as the primary key.
387+
if obj.Type().Field(i).Name == resource.CompositePrimaryKeyFieldName && obj.Field(i).FieldByName("CompositePrimaryKey").String() != "" {
388+
return obj.Field(i).FieldByName("CompositePrimaryKey")
389+
}
390+
}
391+
382392
scope := &gorm.Scope{Value: value}
383393
return fmt.Sprint(scope.PrimaryKeyValue())
384394
}
@@ -647,24 +657,23 @@ func (context *Context) renderMeta(meta *Meta, value interface{}, prefix []strin
647657
}
648658
}
649659

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

653-
if (value == nil || hasValue == nil) && (value != hasValue) {
664+
if (value == nil || comparativeValue == nil) && (value != comparativeValue) {
654665
return false
655666
}
656667

657-
if reflect.Indirect(reflect.ValueOf(hasValue)).Kind() == reflect.Struct {
658-
scope := &gorm.Scope{Value: hasValue}
659-
result = fmt.Sprint(scope.PrimaryKeyValue())
668+
if reflect.Indirect(reflect.ValueOf(comparativeValue)).Kind() == reflect.Struct {
669+
result = fmt.Sprint(context.primaryKeyOf(comparativeValue))
660670
} else {
661-
result = fmt.Sprint(hasValue)
671+
result = fmt.Sprint(comparativeValue)
662672
}
663673

664674
reflectValue := reflect.Indirect(reflect.ValueOf(value))
665675
if reflectValue.Kind() == reflect.Struct {
666-
scope := &gorm.Scope{Value: value}
667-
return fmt.Sprint(scope.PrimaryKeyValue()) == result
676+
return fmt.Sprint(context.primaryKeyOf(value)) == result
668677
} else if reflectValue.Kind() == reflect.String {
669678
// type UserType string, alias type will panic if do
670679
// return reflectValue.Interface().(string) == result

func_map_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/fatih/color"
9+
"github.com/jinzhu/gorm"
910
"github.com/qor/qor"
1011
)
1112

@@ -55,3 +56,39 @@ func TestFuncMaps(t *testing.T) {
5556
}
5657
}
5758
}
59+
60+
type FakeStruct struct {
61+
gorm.Model
62+
Name string
63+
}
64+
65+
func TestIsEqual(t *testing.T) {
66+
c1 := FakeStruct{Name: "c1"}
67+
c1.ID = 1
68+
c2 := FakeStruct{Name: "c2"}
69+
c2.ID = 1
70+
71+
context := Context{
72+
Admin: New(&qor.Config{}),
73+
}
74+
if !context.isEqual(c1, c2) {
75+
t.Error("same primary key is not equal")
76+
}
77+
78+
c1.ID = 2
79+
if context.isEqual(c1, c2) {
80+
t.Error("different primary key is equal")
81+
}
82+
83+
a := "a test"
84+
b := "another one"
85+
if context.isEqual(a, b) {
86+
t.Error("different string is equal")
87+
}
88+
89+
c := 11
90+
d := 11
91+
if !context.isEqual(c, d) {
92+
t.Error("same int is not equal")
93+
}
94+
}

go.mod

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,17 @@ module github.com/qor/admin
33
go 1.13
44

55
require (
6-
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
7-
github.com/disintegration/imaging v1.6.2 // indirect
86
github.com/fatih/color v1.9.0
9-
github.com/gorilla/context v1.1.1 // indirect
10-
github.com/gorilla/sessions v1.2.0 // indirect
11-
github.com/gosimple/slug v1.9.0 // indirect
127
github.com/jinzhu/gorm v1.9.15
138
github.com/jinzhu/inflection v1.0.0
14-
github.com/jinzhu/now v1.0.1
15-
github.com/microcosm-cc/bluemonday v1.0.3 // indirect
16-
github.com/pmezard/go-difflib v1.0.0 // indirect
9+
github.com/jinzhu/now v1.1.1
1710
github.com/qor/assetfs v0.0.0-20170713023933-ff57fdc13a14
1811
github.com/qor/media v0.0.0-20200720100650-60c52edf57cb
19-
github.com/qor/middlewares v0.0.0-20170822143614-781378b69454 // indirect
20-
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 // indirect
21-
github.com/qor/qor v0.0.0-20200715033016-13227382be83
12+
github.com/qor/qor v0.0.0-20210618082622-a52aba0a0ce1
2213
github.com/qor/responder v0.0.0-20171031032654-b6def473574f
2314
github.com/qor/roles v0.0.0-20171127035124-d6375609fe3e
24-
github.com/qor/serializable_meta v0.0.0-20180510060738-5fd8542db417 // indirect
2515
github.com/qor/session v0.0.0-20170907035918-8206b0adab70
26-
github.com/qor/validations v0.0.0-20171228122639-f364bca61b46 // indirect
2716
github.com/theplant/cldr v0.0.0-20190423050709-9f76f7ce4ee8
2817
github.com/theplant/htmltestingutils v0.0.0-20190423050759-0e06de7b6967
2918
github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61
30-
github.com/yosssi/gohtml v0.0.0-20200519115854-476f5b4b8047 // indirect
3119
)

0 commit comments

Comments
 (0)