From ca224fdd4c587bf2a0dff93faaa262f7229176c5 Mon Sep 17 00:00:00 2001 From: Attack825 <68852184+Attack825@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:43:38 +0800 Subject: [PATCH] feat: add group xlsx upload button (#3885) --- controllers/group_upload.go | 56 +++++++++++++++++++++++++++++++++ object/group.go | 35 +++++++++++++++++++++ object/group_upload.go | 61 ++++++++++++++++++++++++++++++++++++ object/user_upload.go | 2 +- object/user_util.go | 29 ++++++++--------- routers/router.go | 1 + web/src/GroupListPage.js | 44 ++++++++++++++++++++++++-- xlsx/group_test.xlsx | Bin 0 -> 9631 bytes 8 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 controllers/group_upload.go create mode 100644 object/group_upload.go create mode 100644 xlsx/group_test.xlsx diff --git a/controllers/group_upload.go b/controllers/group_upload.go new file mode 100644 index 00000000..fdb883a1 --- /dev/null +++ b/controllers/group_upload.go @@ -0,0 +1,56 @@ +// Copyright 2025 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "fmt" + "os" + + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +func (c *ApiController) UploadGroups() { + userId := c.GetSessionUsername() + owner, user := util.GetOwnerAndNameFromId(userId) + + file, header, err := c.Ctx.Request.FormFile("file") + if err != nil { + c.ResponseError(err.Error()) + return + } + + fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename)) + path := util.GetUploadXlsxPath(fileId) + defer os.Remove(path) + + err = saveFile(path, &file) + if err != nil { + c.ResponseError(err.Error()) + return + } + + affected, err := object.UploadGroups(owner, path) + if err != nil { + c.ResponseError(err.Error()) + return + } + + if affected { + c.ResponseOk() + } else { + c.ResponseError(c.T("group_upload:Failed to import groups")) + } +} diff --git a/object/group.go b/object/group.go index 22992433..2a125b80 100644 --- a/object/group.go +++ b/object/group.go @@ -181,6 +181,41 @@ func AddGroups(groups []*Group) (bool, error) { return affected != 0, nil } +func AddGroupsInBatch(groups []*Group) (bool, error) { + if len(groups) == 0 { + return false, nil + } + + session := ormer.Engine.NewSession() + defer session.Close() + err := session.Begin() + if err != nil { + return false, err + } + + for _, group := range groups { + err = checkGroupName(group.Name) + if err != nil { + return false, err + } + + affected, err := session.Insert(group) + if err != nil { + return false, err + } + if affected == 0 { + return false, nil + } + } + + err = session.Commit() + if err != nil { + return false, err + } + + return true, nil +} + func deleteGroup(group *Group) (bool, error) { affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{}) if err != nil { diff --git a/object/group_upload.go b/object/group_upload.go new file mode 100644 index 00000000..55bf385d --- /dev/null +++ b/object/group_upload.go @@ -0,0 +1,61 @@ +// Copyright 2025 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "github.com/casdoor/casdoor/xlsx" +) + +func getGroupMap(owner string) (map[string]*Group, error) { + m := map[string]*Group{} + + groups, err := GetGroups(owner) + if err != nil { + return m, err + } + + for _, group := range groups { + m[group.GetId()] = group + } + + return m, nil +} + +func UploadGroups(owner string, path string) (bool, error) { + table := xlsx.ReadXlsxFile(path) + + oldGroupMap, err := getGroupMap(owner) + if err != nil { + return false, err + } + + transGroups, err := StringArrayToStruct[Group](table) + if err != nil { + return false, err + } + + newGroups := []*Group{} + for _, group := range transGroups { + if _, ok := oldGroupMap[group.GetId()]; !ok { + newGroups = append(newGroups, group) + } + } + + if len(newGroups) == 0 { + return false, nil + } + + return AddGroupsInBatch(newGroups) +} diff --git a/object/user_upload.go b/object/user_upload.go index fcbcb074..c675c34b 100644 --- a/object/user_upload.go +++ b/object/user_upload.go @@ -81,7 +81,7 @@ func UploadUsers(owner string, path string) (bool, error) { return false, err } - transUsers, err := StringArrayToUser(table) + transUsers, err := StringArrayToStruct[User](table) if err != nil { return false, err } diff --git a/object/user_util.go b/object/user_util.go index 019ba7be..1b7f3bed 100644 --- a/object/user_util.go +++ b/object/user_util.go @@ -724,14 +724,14 @@ func setReflectAttr[T any](fieldValue *reflect.Value, fieldString string) error return nil } -func StringArrayToUser(stringArray [][]string) ([]*User, error) { +func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) { fieldNames := stringArray[0] excelMap := []map[string]string{} - userFieldMap := map[string]int{} + structFieldMap := map[string]int{} - reflectedUser := reflect.TypeOf(User{}) - for i := 0; i < reflectedUser.NumField(); i++ { - userFieldMap[strings.ToLower(reflectedUser.Field(i).Name)] = i + reflectedStruct := reflect.TypeOf(*new(T)) + for i := 0; i < reflectedStruct.NumField(); i++ { + structFieldMap[strings.ToLower(reflectedStruct.Field(i).Name)] = i } for idx, field := range stringArray { @@ -746,22 +746,23 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) { excelMap = append(excelMap, tempMap) } - users := []*User{} + instances := []*T{} var err error - for _, u := range excelMap { - user := User{} - reflectedUser := reflect.ValueOf(&user).Elem() - for k, v := range u { + for _, m := range excelMap { + instance := new(T) + reflectedInstance := reflect.ValueOf(instance).Elem() + + for k, v := range m { if v == "" || v == "null" || v == "[]" || v == "{}" { continue } fName := strings.ToLower(strings.ReplaceAll(k, "_", "")) - fieldIdx, ok := userFieldMap[fName] + fieldIdx, ok := structFieldMap[fName] if !ok { continue } - fv := reflectedUser.Field(fieldIdx) + fv := reflectedInstance.Field(fieldIdx) if !fv.IsValid() { continue } @@ -806,8 +807,8 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) { return nil, err } } - users = append(users, &user) + instances = append(instances, instance) } - return users, nil + return instances, nil } diff --git a/routers/router.go b/routers/router.go index 28dc427b..1fa219e8 100644 --- a/routers/router.go +++ b/routers/router.go @@ -81,6 +81,7 @@ func initAPI() { beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup") beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup") beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup") + beego.Router("/api/upload-groups", &controllers.ApiController{}, "POST:UploadGroups") beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers") beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers") diff --git a/web/src/GroupListPage.js b/web/src/GroupListPage.js index 97c14455..02dea913 100644 --- a/web/src/GroupListPage.js +++ b/web/src/GroupListPage.js @@ -14,7 +14,8 @@ import React from "react"; import {Link} from "react-router-dom"; -import {Button, Table, Tooltip} from "antd"; +import {Button, Table, Tooltip, Upload} from "antd"; +import {UploadOutlined} from "@ant-design/icons"; import moment from "moment"; import * as Setting from "./Setting"; import * as GroupBackend from "./backend/GroupBackend"; @@ -87,6 +88,42 @@ class GroupListPage extends BaseListPage { }); } + uploadFile(info) { + const {status, response: res} = info.file; + if (status === "done") { + if (res.status === "ok") { + Setting.showMessage("success", "Groups uploaded successfully, refreshing the page"); + const {pagination} = this.state; + this.fetch({pagination}); + } else { + Setting.showMessage("error", `Groups failed to upload: ${res.msg}`); + } + } else if (status === "error") { + Setting.showMessage("error", "File failed to upload"); + } + } + + renderUpload() { + const props = { + name: "file", + accept: ".xlsx", + method: "post", + action: `${Setting.ServerUrl}/api/upload-groups`, + withCredentials: true, + onChange: (info) => { + this.uploadFile(info); + }, + }; + + return ( + + + + ); + } + renderTable(data) { const columns = [ { @@ -231,7 +268,10 @@ class GroupListPage extends BaseListPage { title={() => (
{i18next.t("general:Groups")}     - + + { + this.renderUpload() + }
)} loading={this.state.loading} diff --git a/xlsx/group_test.xlsx b/xlsx/group_test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7f4161b34808a5105ae647bca6c8a94605fdd318 GIT binary patch literal 9631 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 literal 0 HcmV?d00001