Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into activity-more-info-re…
Browse files Browse the repository at this point in the history
…direct
  • Loading branch information
molon committed Dec 23, 2024
2 parents cd6f7d6 + e0a29e6 commit 9ae947f
Show file tree
Hide file tree
Showing 65 changed files with 1,697 additions and 808 deletions.
2 changes: 1 addition & 1 deletion activity/activity_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type ActivityLog struct {
Hidden bool `gorm:"index;default:false;not null;"`
ModelName string `gorm:"index;not null;"`
ModelKeys string `gorm:"index;not null;"`
ModelLabel string `gorm:"not null;"`
ModelLabel string `gorm:"not null;"` // IMPROVE: need named resource sign
ModelLink string `gorm:"not null;"`
Detail string `gorm:"not null;"`
Scope string `gorm:"index;"`
Expand Down
40 changes: 36 additions & 4 deletions activity/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/qor5/admin/v3/presets/gorm2op"
"github.com/qor5/web/v3"
"github.com/qor5/x/v3/i18n"
"github.com/qor5/x/v3/perm"
. "github.com/qor5/x/v3/ui/vuetify"
"github.com/qor5/x/v3/ui/vuetifyx"
"github.com/samber/lo"
Expand Down Expand Up @@ -80,9 +81,9 @@ func (ab *Builder) defaultLogModelInstall(b *presets.Builder, mb *presets.ModelB

// should use own DataOperator
op := gorm2op.DataOperator(ab.db)
setupDetailing(dp, op, ab)
setupDetailing(b, dp, op, ab)
setupEditing(eb)
setupListing(lb, op, ab)
setupListing(b, lb, op, ab)

return nil
}
Expand All @@ -96,9 +97,33 @@ func setupEditing(eb *presets.EditingBuilder) {
})
}

func setupListing(lb *presets.ListingBuilder, op *gorm2op.DataOperatorBuilder, ab *Builder) {
func setupListing(b *presets.Builder, lb *presets.ListingBuilder, op *gorm2op.DataOperatorBuilder, ab *Builder) {
lb.RelayPagination(gorm2op.KeysetBasedPagination(true))
lb.SearchFunc(func(ctx *web.EventContext, params *presets.SearchParams) (result *presets.SearchResult, err error) {
var modelLabels []string
// err = ab.db.Model(&ActivityLog{}).Select("DISTINCT model_label AS model_label").Pluck("model_label", &modelLabels).Error
// if err != nil {
// return nil, err
// }
for _, m := range ab.models {
if m.label != nil {
modelLabels = append(modelLabels, m.label())
}
}
modelLabels = lo.Uniq(modelLabels)
for _, resourceSign := range modelLabels {
if resourceSign == "" || resourceSign == NopModelLabel {
continue
}
if b.GetVerifier().Spawn().SnakeOn(resourceSign).Do(presets.PermList).WithReq(ctx.R).IsAllowed() == nil {
continue
}
params.SQLConditions = append(params.SQLConditions, &presets.SQLCondition{
Query: "model_label <> ?",
Args: []any{resourceSign},
})
}

params.SQLConditions = append(params.SQLConditions, &presets.SQLCondition{
Query: "hidden = ?",
Args: []any{false},
Expand Down Expand Up @@ -282,13 +307,20 @@ func setupListing(lb *presets.ListingBuilder, op *gorm2op.DataOperatorBuilder, a
})
}

func setupDetailing(dp *presets.DetailingBuilder, op *gorm2op.DataOperatorBuilder, ab *Builder) {
func setupDetailing(b *presets.Builder, dp *presets.DetailingBuilder, op *gorm2op.DataOperatorBuilder, ab *Builder) {
dp.FetchFunc(func(obj any, id string, ctx *web.EventContext) (r any, err error) {
r, err = op.Fetch(obj, id, ctx)
if err != nil {
return r, err
}
log := r.(*ActivityLog)

if log.ModelLabel != "" && log.ModelLabel != NopModelLabel {
if b.GetVerifier().Spawn().SnakeOn(log.ModelLabel).Do(presets.PermGet).WithReq(ctx.R).IsAllowed() != nil {
return nil, perm.PermissionDenied
}
}

if err := ab.supplyUsers(ctx.R.Context(), []*ActivityLog{log}); err != nil {
return nil, err
}
Expand Down
16 changes: 8 additions & 8 deletions activity/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ var Messages_en_US = &Messages{
ActionNote: "Note",

ModelUserID: "Creator ID",
ModelCreatedAt: "Date Time",
ModelCreatedAt: "Create Time",
ModelAction: "Action",
ModelUser: "Creator",
ModelKeys: "Keys",
ModelName: "Table Name",
ModelKeys: "Model Keys",
ModelName: "Model Name",
ModelLabel: "Menu Name",
ModelLink: "Link",
ModelDiffs: "Diffs",
Expand Down Expand Up @@ -182,17 +182,17 @@ var Messages_zh_CN = &Messages{
ModelCreatedAt: "日期时间",
ModelAction: "操作",
ModelUser: "操作者",
ModelKeys: "表的主键值",
ModelName: "表名",
ModelKeys: "主键",
ModelName: "对象",
ModelLabel: "菜单名",
ModelLink: "链接",
ModelDiffs: "差异",

FilterAction: "操作类型",
FilterCreatedAt: "操作时间",
FilterUser: "操作人",
FilterModel: "操作对象",
FilterModelKeys: "操作对象主键",
FilterUser: "操作者",
FilterModel: "对象",
FilterModelKeys: "主键",

DiffDetail: "详情",
DiffAdd: "新加",
Expand Down
18 changes: 13 additions & 5 deletions activity/model_builder.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package activity

import (
"cmp"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -47,6 +46,7 @@ type ModelBuilder struct {
ignoredFields []string // ignored fields
typeHandlers map[reflect.Type]TypeHandler // type handlers
link func(any) string // display the model link on the admin detail page
label func() string // display the model label on the admin detail page
beforeCreate func(ctx context.Context, log *ActivityLog) error
}

Expand Down Expand Up @@ -176,7 +176,7 @@ func (amb *ModelBuilder) WrapperSaveFunc(in presets.SaveFunc) presets.SaveFunc {

func (amb *ModelBuilder) installPresetModelBuilder(mb *presets.ModelBuilder) {
amb.presetModel = mb
amb.LinkFunc(func(a any) string {
amb.link = func(a any) string {
id := presets.ObjectID(a)
if id == "" {
return ""
Expand All @@ -185,7 +185,10 @@ func (amb *ModelBuilder) installPresetModelBuilder(mb *presets.ModelBuilder) {
return mb.Info().DetailingHref(id)
}
return ""
})
}
amb.label = func() string {
return mb.Info().URIName()
}

pb := mb.GetPresetsBuilder()
if amb.ab.GetLogModelBuilder(pb) == nil {
Expand Down Expand Up @@ -347,6 +350,11 @@ func (mb *ModelBuilder) parseColumns(keys []string) []string {
return columns
}

func (mb *ModelBuilder) LabelFunc(f func() string) *ModelBuilder {
mb.label = f
return mb
}

func (mb *ModelBuilder) LinkFunc(f func(any) string) *ModelBuilder {
mb.link = f
return mb
Expand Down Expand Up @@ -533,8 +541,8 @@ func (mb *ModelBuilder) create(
ModelLink: modelLink,
Scope: scope,
}
if mb.presetModel != nil {
log.ModelLabel = cmp.Or(mb.presetModel.Info().URIName(), log.ModelLabel)
if mb.label != nil {
log.ModelLabel = mb.label()
}
log.CreatedAt = db.NowFunc()

Expand Down
82 changes: 69 additions & 13 deletions docs/docsrc/examples/examples_admin/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/qor5/admin/v3/presets"
"github.com/qor5/web/v3/multipartestutils"
"github.com/qor5/x/v3/perm"
"github.com/stretchr/testify/require"
"github.com/theplant/gofixtures"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -559,8 +560,8 @@ func TestActivityAdmin(t *testing.T) {
ExpectPageBodyContainsInOrder: []string{
"操作日志列表",
"全部", "创建", "编辑", "删除", "备注",
"<vx-filter", "操作类型", "操作时间", "操作人", "操作对象", "</vx-filter>",
"日期时间", "操作者", "操作", "表的主键值", "菜单名", "表名",
"<vx-filter", "操作类型", "操作时间", "操作者", "对象", "</vx-filter>",
"日期时间", "操作者", "操作", "主键", "菜单名", "对象",
"<div", "<v-btn", "mdi-chevron-left", ":disabled='true'", "<v-btn", "mdi-chevron-right", ":disabled='false'", "</div>",
},
ExpectPageBodyNotContains: []string{"v-pagination"},
Expand All @@ -575,8 +576,8 @@ func TestActivityAdmin(t *testing.T) {
ExpectPageBodyContainsInOrder: []string{
"操作日志列表",
"全部", "创建", "编辑", "删除", "备注",
"<vx-filter", "操作类型", "操作时间", "操作人", "操作对象", "</vx-filter>",
"日期时间", "操作者", "操作", "表的主键值", "菜单名", "表名",
"<vx-filter", "操作类型", "操作时间", "操作者", "对象", "</vx-filter>",
"日期时间", "操作者", "操作", "主键", "菜单名", "对象",
"<div", "<v-btn", "mdi-chevron-left", ":disabled='false'", "<v-btn", "mdi-chevron-right", ":disabled='true'", "</div>",
},
ExpectPageBodyNotContains: []string{"v-pagination"},
Expand All @@ -591,8 +592,8 @@ func TestActivityAdmin(t *testing.T) {
ExpectPageBodyContainsInOrder: []string{
"操作日志列表",
"全部", "创建", "编辑", "删除", "备注",
"<vx-filter", "操作类型", "操作时间", "操作人", "操作对象", "</vx-filter>",
"日期时间", "操作者", "操作", "表的主键值", "菜单名", "表名",
"<vx-filter", "操作类型", "操作时间", "操作者", "对象", "</vx-filter>",
"日期时间", "操作者", "操作", "主键", "菜单名", "对象",
"<div", "<v-btn", "mdi-chevron-left", ":disabled='true'", "<v-btn", "mdi-chevron-right", ":disabled='false'", "</div>",
},
ExpectPageBodyNotContains: []string{"v-pagination"},
Expand Down Expand Up @@ -636,8 +637,8 @@ func TestActivityAdmin(t *testing.T) {
ExpectPageBodyContainsInOrder: []string{
"操作日志列表",
"全部", "创建", "编辑", "删除", "备注",
"<vx-filter", "操作类型", "操作时间", "操作人", "操作对象", "</vx-filter>",
"日期时间", "操作者", "操作", "表的主键值", "菜单名", "表名",
"<vx-filter", "操作类型", "操作时间", "操作者", "对象", "</vx-filter>",
"日期时间", "操作者", "操作", "主键", "菜单名", "对象",
"<div", "<v-btn", "mdi-chevron-left", ":disabled='false'", "<v-btn", "mdi-chevron-right", ":disabled='false'", "</div>",
},
ExpectPageBodyNotContains: []string{"v-pagination"},
Expand All @@ -652,12 +653,12 @@ func TestActivityAdmin(t *testing.T) {
ExpectPageBodyContainsInOrder: []string{
"操作日志列表",
"全部", "创建", "编辑", "删除", "备注",
"<vx-filter", "操作类型", "操作时间", "操作人", "操作对象", "</vx-filter>",
"日期时间", "操作者", "操作", "表的主键值", "菜单名", "表名", "</tr>",
"<vx-filter", "操作类型", "操作时间", "操作者", "对象", "</vx-filter>",
"日期时间", "操作者", "操作", "主键", "菜单名", "对象",
"</tr>", "</tr>", "</tr>", "</tr>", "</tr>", "</tr>", "</tr>", "</tr>", "</tr>", "</tr>",
"<div", "<v-btn", "mdi-chevron-left", ":disabled='true'", "<v-btn", "mdi-chevron-right", ":disabled='false'", "</div>",
},
ExpectPageBodyNotContains: []string{"v-pagination"},
ExpectPageBodyNotContains: []string{"v-pagination", "没有可显示的记录"},
},
{
Name: "Update note",
Expand Down Expand Up @@ -737,13 +738,68 @@ func TestActivityAdmin(t *testing.T) {
},
ExpectPortalUpdate0ContainsInOrder: []string{"Activity Log", "121", "<td style='white-space: nowrap;'>AppovedAt</td>", "<td v-pre>2024-11-06 00:00:00 +0800 CST</td>"},
},

{
Name: "Activity logs without PermList",
Debug: true,
HandlerMaker: func() http.Handler {
pb := presets.New()
pb.Permission(
perm.New().Policies(
perm.PolicyFor(perm.Anybody).WhoAre(perm.Allowed).ToDo(perm.Anything).On(perm.Anything),
perm.PolicyFor(perm.Anybody).WhoAre(perm.Denied).ToDo(presets.PermList).On("*:presets:with_activity_products:*"),
),
)
activityExample(pb, TestDB, func(mb *presets.ModelBuilder, ab *activity.Builder) {
pb.Use(ab)
})
return pb
},
ReqFunc: func() *http.Request {
// activityData.TruncatePut(dbr)
return httptest.NewRequest("GET", "/activity-logs?lang=zh", nil)
},
ExpectPageBodyContainsInOrder: []string{
"没有可显示的记录",
},
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
multipartestutils.RunCase(t, c, pb)
})
}

panicCase := multipartestutils.TestCase{
Name: "Activity log detail for edit action without PermGet",
Debug: true,
HandlerMaker: func() http.Handler {
pb := presets.New()
pb.Permission(
perm.New().Policies(
perm.PolicyFor(perm.Anybody).WhoAre(perm.Allowed).ToDo(perm.Anything).On(perm.Anything),
perm.PolicyFor(perm.Anybody).WhoAre(perm.Denied).ToDo(presets.PermGet).On("*:presets:with_activity_products:*"),
),
)
activityExample(pb, TestDB, func(mb *presets.ModelBuilder, ab *activity.Builder) {
pb.Use(ab)
})
return pb
},
ReqFunc: func() *http.Request {
// activityData.TruncatePut(dbr)
req := multipartestutils.NewMultipartBuilder().
PageURL("/activity-logs?__execute_event__=presets_DetailingDrawer&id=87").
BuildEventFuncRequest()
return req
},
}
t.Run(panicCase.Name, func(t *testing.T) {
require.Panics(t, func() {
multipartestutils.RunCase(t, panicCase, pb)
})
})
}

func TestActivityBeforeCreate(t *testing.T) {
Expand Down Expand Up @@ -789,8 +845,8 @@ func TestActivityBeforeCreate(t *testing.T) {
ExpectPageBodyContainsInOrder: []string{
"操作日志列表",
"全部", "创建", "编辑", "删除", "备注",
"<vx-filter", "操作类型", "操作时间", "操作人", "操作对象", "</vx-filter>",
"日期时间", "操作者", "操作", "表的主键值", "菜单名", "表名",
"<vx-filter", "操作类型", "操作时间", "操作者", "对象", "</vx-filter>",
"日期时间", "操作者", "操作", "主键", "菜单名", "对象",
"_BeforeCreate", "_BeforeCreate",
"<div", "<v-btn", "mdi-chevron-left", "<v-btn", "mdi-chevron-right", "</div>",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,43 @@ func LinkageSelectFilterItemRemoteExample(b *presets.Builder, mux examples.Muxer
remoteUrl := "/examples/api/linkage-select-server"
eb.Field("ProvinceCityDistrict").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
p := obj.(*examples_presets.Address)

if field.Errors == nil {
field.Errors = []string{}
}
return vx.VXLinkageSelectRemote().
Attr(web.VField(field.Name,
Attr(presets.VFieldError(field.Name,
[]*Item{
getItem(p.Province),
getItem(p.City),
getItem(p.District),
})...).
}, field.Errors)...).
Labels(labels...).
RemoteUrl(remoteUrl).
IsPaging(true).
LevelStart(1)
}).SetterFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
var vs []string
vErr := &web.ValidationErrors{}
hasErr := false
for i := 0; i < 3; i++ {
vs = append(vs, ctx.R.FormValue(fmt.Sprintf("ProvinceCityDistrict[%v].Name", i)))
val := ctx.Param(fmt.Sprintf("%s[%v].Name", field.FormKey, i))
vs = append(vs, val)
msg := ""
if val == "" {
hasErr = true
msg = "Not Empty"
}
vErr.FieldError(field.FormKey, msg)
}

m := obj.(*examples_presets.Address)
m.Province = vs[0]
m.City = vs[1]
m.District = vs[2]
return nil
if hasErr {
return vErr
}
return
})

lb := mb.Listing()
Expand Down
4 changes: 2 additions & 2 deletions docs/docsrc/examples/examples_admin/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestChangePassword(t *testing.T) {
BuildEventFuncRequest()
return req
},
ExpectRunScriptContainsInOrder: []string{"Old password is incorrect"},
ExpectRunScriptContainsInOrder: []string{"Unable to change password. Please check your inputs and try again."},
},
{
Name: "password not match",
Expand All @@ -67,7 +67,7 @@ func TestChangePassword(t *testing.T) {
BuildEventFuncRequest()
return req
},
ExpectRunScriptContainsInOrder: []string{"Password do not match"},
ExpectRunScriptContainsInOrder: []string{"The new passwords do not match. Please try again."},
},
{
Name: "success",
Expand Down
Loading

0 comments on commit 9ae947f

Please sign in to comment.