This is an automated email from the ASF dual-hosted git repository. klesh pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push: new 84efcdcb2 feat(customize): add CSV import functionality for sprints and issue c… (#8456) 84efcdcb2 is described below commit 84efcdcb2d2dc74d7f7b121e7696e9a3543b10d2 Author: NaRro <cong.w...@merico.dev> AuthorDate: Fri Jul 18 05:35:12 2025 +0000 feat(customize): add CSV import functionality for sprints and issue c… (#8456) * feat(customize): add CSV import functionality for sprints and issue changelogs/worklogs - Add new API endpoints for importing sprints, issue changelogs, and issue worklogs from CSV files - Implement corresponding handler functions to process the uploaded CSV files - Add e2e tests to verify the import functionality for sprints, issue changelogs, and issue worklogs - Update the plugin's ApiResources map to include the new endpoints #8446 * fix(customize): delete board_sprints before import sprints --- backend/plugins/customize/api/csv_issue.go | 93 ++++++++++++ .../customize/e2e/import_issue_changelogs_test.go | 101 +++++++++++++ .../customize/e2e/import_issue_worklogs_test.go | 101 +++++++++++++ .../plugins/customize/e2e/import_issues_test.go | 10 ++ .../plugins/customize/e2e/import_sprint_test.go | 85 +++++++++++ .../customize/e2e/raw_tables/issue_changelogs.csv | 7 + .../raw_tables/issue_changelogs_incremental.csv | 5 + .../customize/e2e/raw_tables/issue_worklogs.csv | 4 + .../e2e/raw_tables/issue_worklogs_incremental.csv | 3 + .../customize/e2e/raw_tables/issues_input.csv | 8 +- .../e2e/raw_tables/issues_input_incremental.csv | 8 +- .../plugins/customize/e2e/raw_tables/sprints.csv | 3 + .../e2e/raw_tables/sprints_incremental.csv | 2 + .../accounts_from_issue_changelogs.csv | 6 + .../accounts_from_issue_worklogs.csv | 4 + .../e2e/snapshot_tables/board_sprints.csv | 4 + .../e2e/snapshot_tables/issue_changelogs.csv | 7 + .../issue_changelogs_incremental.csv | 11 ++ .../e2e/snapshot_tables/issue_worklogs.csv | 4 + .../snapshot_tables/issue_worklogs_incremental.csv | 5 + .../e2e/snapshot_tables/sprint_issues.csv | 8 + .../customize/e2e/snapshot_tables/sprints.csv | 4 + backend/plugins/customize/impl/impl.go | 9 ++ backend/plugins/customize/service/service.go | 162 ++++++++++++++++++++- 24 files changed, 645 insertions(+), 9 deletions(-) diff --git a/backend/plugins/customize/api/csv_issue.go b/backend/plugins/customize/api/csv_issue.go index 51ea626c8..2e9aaddd9 100644 --- a/backend/plugins/customize/api/csv_issue.go +++ b/backend/plugins/customize/api/csv_issue.go @@ -124,6 +124,99 @@ func (h *Handlers) ImportIssueRepoCommit(input *plugin.ApiResourceInput) (*plugi return nil, h.svc.ImportIssueRepoCommit(boardId, file, incremental) } +// ImportSprint accepts a CSV file, parses and saves it to the database +// @Summary Upload sprints.csv file +// @Description Upload sprints.csv file +// @Tags plugins/customize +// @Accept multipart/form-data +// @Param boardId formData string true "the ID of the board" +// @Param file formData file true "select file to upload" +// @Param incremental formData string true "whether to save only new data" +// @Produce json +// @Success 200 +// @Failure 400 {object} shared.ApiBody "Bad Request" +// @Failure 500 {object} shared.ApiBody "Internal Error" +// @Router /plugins/customize/csvfiles/sprints.csv [post] +func (h *Handlers) ImportSprint(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + file, err := h.extractFile(input) + if err != nil { + return nil, err + } + // nolint + defer file.Close() + boardId := strings.TrimSpace(input.Request.FormValue("boardId")) + if boardId == "" { + return nil, errors.Default.New("empty boardId") + } + incremental := false + if input.Request.FormValue("incremental") == "true" { + incremental = true + } + return nil, h.svc.ImportSprint(boardId, file, incremental) +} + +// ImportIssueChangelog accepts a CSV file, parses and saves it to the database +// @Summary Upload issue_changelogs.csv file +// @Description Upload issue_changelogs.csv file +// @Tags plugins/customize +// @Accept multipart/form-data +// @Param boardId formData string true "the ID of the board" +// @Param file formData file true "select file to upload" +// @Param incremental formData boolean false "Whether to incrementally update changelogs" default(false) +// @Produce json +// @Success 200 +// @Failure 400 {object} shared.ApiBody "Bad Request" +// @Failure 500 {object} shared.ApiBody "Internal Error" +// @Router /plugins/customize/csvfiles/issue_changelogs.csv [post] +func (h *Handlers) ImportIssueChangelog(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + file, err := h.extractFile(input) + if err != nil { + return nil, err + } + // nolint + defer file.Close() + boardId := strings.TrimSpace(input.Request.FormValue("boardId")) + if boardId == "" { + return nil, errors.Default.New("empty boardId") + } + incremental := false + if input.Request.FormValue("incremental") == "true" { + incremental = true + } + return nil, h.svc.ImportIssueChangelog(boardId, file, incremental) +} + +// ImportIssueWorklog accepts a CSV file, parses and saves it to the database +// @Summary Upload issue_worklogs.csv file +// @Description Upload issue_worklogs.csv file +// @Tags plugins/customize +// @Accept multipart/form-data +// @Param boardId formData string true "the ID of the board" +// @Param file formData file true "select file to upload" +// @Param incremental formData boolean false "Whether to do incremental sync (default false +// @Produce json +// @Success 200 +// @Failure 400 {object} shared.ApiBody "Bad Request" +// @Failure 500 {object} shared.ApiBody "Internal Error" +// @Router /plugins/customize/csvfiles/issue_worklogs.csv [post] +func (h *Handlers) ImportIssueWorklog(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + file, err := h.extractFile(input) + if err != nil { + return nil, err + } + // nolint + defer file.Close() + boardId := strings.TrimSpace(input.Request.FormValue("boardId")) + if boardId == "" { + return nil, errors.Default.New("empty boardId") + } + incremental := false + if input.Request.FormValue("incremental") == "true" { + incremental = true + } + return nil, h.svc.ImportIssueWorklog(boardId, file, incremental) +} + func (h *Handlers) extractFile(input *plugin.ApiResourceInput) (io.ReadCloser, errors.Error) { if input.Request == nil { return nil, errors.Default.New("request is nil") diff --git a/backend/plugins/customize/e2e/import_issue_changelogs_test.go b/backend/plugins/customize/e2e/import_issue_changelogs_test.go new file mode 100644 index 000000000..c62bad04e --- /dev/null +++ b/backend/plugins/customize/e2e/import_issue_changelogs_test.go @@ -0,0 +1,101 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 e2e + +import ( + "os" + "testing" + + "github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain" + "github.com/apache/incubator-devlake/core/models/domainlayer/ticket" + "github.com/apache/incubator-devlake/helpers/e2ehelper" + "github.com/apache/incubator-devlake/plugins/customize/impl" + "github.com/apache/incubator-devlake/plugins/customize/service" +) + +func TestImportIssueChangelogDataFlow(t *testing.T) { + var plugin impl.Customize + dataflowTester := e2ehelper.NewDataFlowTester(t, "customize", plugin) + + // 清空表 + dataflowTester.FlushTabler(&ticket.IssueChangelogs{}) + dataflowTester.FlushTabler(&crossdomain.Account{}) + + // 初始化服务 + svc := service.NewService(dataflowTester.Dal) + + // 导入全量数据 + changelogFile, err := os.Open("raw_tables/issue_changelogs.csv") + if err != nil { + t.Fatal(err) + } + defer changelogFile.Close() + err = svc.ImportIssueChangelog("TEST_BOARD", changelogFile, false) + if err != nil { + t.Fatal(err) + } + + // 验证全量导入结果 + dataflowTester.VerifyTableWithRawData( + ticket.IssueChangelogs{}, + "snapshot_tables/issue_changelogs.csv", + []string{ + "id", + "issue_id", + "author_id", + "field_name", + "original_from_value", + "original_to_value", + "created_date", + }) + + // 导入增量数据 + incrementalFile, err := os.Open("raw_tables/issue_changelogs_incremental.csv") + if err != nil { + t.Fatal(err) + } + defer incrementalFile.Close() + err = svc.ImportIssueChangelog("TEST_BOARD", incrementalFile, true) + if err != nil { + t.Fatal(err) + } + + // 验证增量导入结果 + dataflowTester.VerifyTableWithRawData( + ticket.IssueChangelogs{}, + "snapshot_tables/issue_changelogs_incremental.csv", + []string{ + "id", + "issue_id", + "author_id", + "field_name", + "original_from_value", + "original_to_value", + "created_date", + }) + + dataflowTester.VerifyTable( + crossdomain.Account{}, + "snapshot_tables/accounts_from_issue_changelogs.csv", + []string{ + "id", + "full_name", + "user_name", + }, + ) +} diff --git a/backend/plugins/customize/e2e/import_issue_worklogs_test.go b/backend/plugins/customize/e2e/import_issue_worklogs_test.go new file mode 100644 index 000000000..71ed67e6b --- /dev/null +++ b/backend/plugins/customize/e2e/import_issue_worklogs_test.go @@ -0,0 +1,101 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 e2e + +import ( + "os" + "testing" + + "github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain" + "github.com/apache/incubator-devlake/core/models/domainlayer/ticket" + "github.com/apache/incubator-devlake/helpers/e2ehelper" + "github.com/apache/incubator-devlake/plugins/customize/impl" + "github.com/apache/incubator-devlake/plugins/customize/service" +) + +func TestImportIssueWorklogDataFlow(t *testing.T) { + var plugin impl.Customize + dataflowTester := e2ehelper.NewDataFlowTester(t, "customize", plugin) + + // 清空表 + dataflowTester.FlushTabler(&ticket.IssueWorklog{}) + dataflowTester.FlushTabler(&crossdomain.Account{}) + + // 初始化服务 + svc := service.NewService(dataflowTester.Dal) + + // 导入全量数据 + worklogFile, err := os.Open("raw_tables/issue_worklogs.csv") + if err != nil { + t.Fatal(err) + } + defer worklogFile.Close() + err = svc.ImportIssueWorklog("TEST_BOARD", worklogFile, false) + if err != nil { + t.Fatal(err) + } + + // 验证全量导入结果 + dataflowTester.VerifyTableWithRawData( + ticket.IssueWorklog{}, + "snapshot_tables/issue_worklogs.csv", + []string{ + "id", + "issue_id", + "author_id", + "time_spent_minutes", + "started_date", + "logged_date", + "comment", + }) + + // 导入增量数据 + incrementalFile, err := os.Open("raw_tables/issue_worklogs_incremental.csv") + if err != nil { + t.Fatal(err) + } + defer incrementalFile.Close() + err = svc.ImportIssueWorklog("TEST_BOARD", incrementalFile, true) + if err != nil { + t.Fatal(err) + } + + // 验证增量导入结果 + dataflowTester.VerifyTableWithRawData( + ticket.IssueWorklog{}, + "snapshot_tables/issue_worklogs_incremental.csv", + []string{ + "id", + "issue_id", + "author_id", + "time_spent_minutes", + "started_date", + "logged_date", + "comment", + }) + + dataflowTester.VerifyTable( + crossdomain.Account{}, + "snapshot_tables/accounts_from_issue_worklogs.csv", + []string{ + "id", + "full_name", + "user_name", + }, + ) +} diff --git a/backend/plugins/customize/e2e/import_issues_test.go b/backend/plugins/customize/e2e/import_issues_test.go index 82de28d09..576fdcddb 100644 --- a/backend/plugins/customize/e2e/import_issues_test.go +++ b/backend/plugins/customize/e2e/import_issues_test.go @@ -39,6 +39,7 @@ func TestImportIssueDataFlow(t *testing.T) { dataflowTester.FlushTabler(&ticket.IssueLabel{}) dataflowTester.FlushTabler(&ticket.BoardIssue{}) dataflowTester.FlushTabler(&crossdomain.Account{}) + dataflowTester.FlushTabler(&ticket.SprintIssue{}) svc := service.NewService(dataflowTester.Dal) err := svc.CreateField(&models.CustomizedField{ TbName: "issues", @@ -183,4 +184,13 @@ func TestImportIssueDataFlow(t *testing.T) { "user_name", }, ) + + dataflowTester.VerifyTableWithRawData( + &ticket.SprintIssue{}, + "snapshot_tables/sprint_issues.csv", + []string{ + "sprint_id", + "issue_id", + }, + ) } diff --git a/backend/plugins/customize/e2e/import_sprint_test.go b/backend/plugins/customize/e2e/import_sprint_test.go new file mode 100644 index 000000000..33a2a30f8 --- /dev/null +++ b/backend/plugins/customize/e2e/import_sprint_test.go @@ -0,0 +1,85 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 e2e + +import ( + "os" + "testing" + + "github.com/apache/incubator-devlake/core/models/domainlayer/ticket" + "github.com/apache/incubator-devlake/helpers/e2ehelper" + "github.com/apache/incubator-devlake/plugins/customize/impl" + "github.com/apache/incubator-devlake/plugins/customize/service" +) + +func TestImportSprintDataFlow(t *testing.T) { + var plugin impl.Customize + dataflowTester := e2ehelper.NewDataFlowTester(t, "customize", plugin) + + // 创建表 + dataflowTester.FlushTabler(&ticket.Sprint{}) + dataflowTester.FlushTabler(&ticket.BoardSprint{}) + + // 导入必要数据 + svc := service.NewService(dataflowTester.Dal) + + // 导入全量数据 + sprintFile, err := os.Open("raw_tables/sprints.csv") + if err != nil { + t.Fatal(err) + } + defer sprintFile.Close() + err = svc.ImportSprint("csv-board", sprintFile, false) + if err != nil { + t.Fatal(err) + } + + // 导入增量数据 + sprintIncrementalFile, err := os.Open("raw_tables/sprints_incremental.csv") + if err != nil { + t.Fatal(err) + } + defer sprintIncrementalFile.Close() + err = svc.ImportSprint("csv-board", sprintIncrementalFile, true) + if err != nil { + t.Fatal(err) + } + + // 验证结果 + dataflowTester.VerifyTableWithRawData( + ticket.Sprint{}, + "snapshot_tables/sprints.csv", + []string{ + "id", + "url", + "status", + "name", + "started_date", + "ended_date", + "completed_date", + "original_board_id", + }) + + dataflowTester.VerifyTableWithRawData( + ticket.BoardSprint{}, + "snapshot_tables/board_sprints.csv", + []string{ + "board_id", + "sprint_id", + }) +} diff --git a/backend/plugins/customize/e2e/raw_tables/issue_changelogs.csv b/backend/plugins/customize/e2e/raw_tables/issue_changelogs.csv new file mode 100644 index 000000000..7342bbdca --- /dev/null +++ b/backend/plugins/customize/e2e/raw_tables/issue_changelogs.csv @@ -0,0 +1,7 @@ +id,issue_id,author_id,field_name,original_from_value,original_to_value,created_date +changelog1,issue1,user1,status,Open,In Progress,2023-01-01 10:00:00+00:00 +changelog2,issue1,user2,assignee,user1,user2,2023-01-02 10:00:00+00:00 +changelog3,issue1,user3,Sprint,Sprint1,"Sprint2,Sprint3",2023-01-03 10:00:00+00:00 +changelog4,issue2,user1,status,In Progress,Done,2023-01-04 10:00:00+00:00 +changelog5,issue2,user4,assignee,user3,user4,2023-01-05 10:00:00+00:00 +changelog6,issue2,user2,Sprint,"Sprint2,Sprint3",Sprint4,2023-01-06 10:00:00+00:00 \ No newline at end of file diff --git a/backend/plugins/customize/e2e/raw_tables/issue_changelogs_incremental.csv b/backend/plugins/customize/e2e/raw_tables/issue_changelogs_incremental.csv new file mode 100644 index 000000000..95391c5b0 --- /dev/null +++ b/backend/plugins/customize/e2e/raw_tables/issue_changelogs_incremental.csv @@ -0,0 +1,5 @@ +id,issue_id,author_id,field_name,original_from_value,original_to_value,created_date +changelog7,issue1,user3,status,Done,Reopened,2023-01-07 10:00:00+00:00 +changelog8,issue1,user5,assignee,user2,user5,2023-01-08 10:00:00+00:00 +changelog9,issue1,user4,Sprint,Sprint3,"Sprint4,Sprint5",2023-01-09 10:00:00+00:00 +changelog10,issue3,user1,status,Open,In Progress,2023-01-10 10:00:00+00:00 \ No newline at end of file diff --git a/backend/plugins/customize/e2e/raw_tables/issue_worklogs.csv b/backend/plugins/customize/e2e/raw_tables/issue_worklogs.csv new file mode 100644 index 000000000..cdc5f3daa --- /dev/null +++ b/backend/plugins/customize/e2e/raw_tables/issue_worklogs.csv @@ -0,0 +1,4 @@ +id,issue_id,author_name,time_spent_minutes,started_date,logged_date,comment +worklog1,ISSUE-1,Alice,30,2023-01-01 10:00:00+00:00,2022-07-17 07:15:55.959+00:00,"Initial work" +worklog2,ISSUE-1,Bob,45,2023-01-02 11:00:00+00:00,2022-07-18 08:20:30.123+00:00,"Follow up" +worklog3,ISSUE-2,Alice,60,2023-01-03 09:00:00+00:00,2022-07-19 09:45:15.456+00:00,"Task completion" \ No newline at end of file diff --git a/backend/plugins/customize/e2e/raw_tables/issue_worklogs_incremental.csv b/backend/plugins/customize/e2e/raw_tables/issue_worklogs_incremental.csv new file mode 100644 index 000000000..1f1c56478 --- /dev/null +++ b/backend/plugins/customize/e2e/raw_tables/issue_worklogs_incremental.csv @@ -0,0 +1,3 @@ +id,issue_id,author_name,time_spent_minutes,started_date,logged_date,comment +worklog4,ISSUE-2,Charlie,15,2023-01-04 14:00:00+00:00,2022-07-20 10:30:45.789+00:00,"Quick fix" +worklog2,ISSUE-1,Bob,20,2023-01-02 11:00:00+00:00,2022-07-21 11:15:30.000+00:00,"Updated time" \ No newline at end of file diff --git a/backend/plugins/customize/e2e/raw_tables/issues_input.csv b/backend/plugins/customize/e2e/raw_tables/issues_input.csv index 1eacd5fcd..8beecf831 100644 --- a/backend/plugins/customize/e2e/raw_tables/issues_input.csv +++ b/backend/plugins/customize/e2e/raw_tables/issues_input.csv @@ -1,4 +1,4 @@ -id,url,issue_key,title,original_type,original_status,created_date,resolution_date,story_point,priority,severity,original_estimate_minutes,time_spent_minutes,component,epic_key,creator_name,assignee_name,x_int,x_time,x_varchar,x_float,labels -csv:1,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/1,1,issue test,BUG,new,2022-07-17 07:15:55.959+00:00,NULL,0,major,,0,0,,,tgp,klesh,10,2022-09-15 15:27:56,world,8,NULL -csv:10,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/10,10,issue test007,BUG,new,2022-08-12 13:43:00.783+00:00,NULL,0,trivial,,0,0,,,tgp,warren,30,2022-09-15 15:27:56,abc,24590,hello worlds -csv:11,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/11,11,issue test011,REQUIREMENT,new,2022-08-10 13:44:46.508+00:00,NULL,0,major,,0,0,,,tgp,abeizn,1,2022-09-15 15:27:56,NULL,0.00014,NULL +id,url,issue_key,title,original_type,original_status,created_date,resolution_date,story_point,priority,severity,original_estimate_minutes,time_spent_minutes,component,epic_key,creator_name,assignee_name,x_int,x_time,x_varchar,x_float,labels,sprint_ids +csv:1,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/1,1,issue test,BUG,new,2022-07-17 07:15:55.959+00:00,NULL,0,major,,0,0,,,tgp,klesh,10,2022-09-15 15:27:56,world,8,NULL,"101,102" +csv:10,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/10,10,issue test007,BUG,new,2022-08-12 13:43:00.783+00:00,NULL,0,trivial,,0,0,,,tgp,warren,30,2022-09-15 15:27:56,abc,24590,hello worlds,101 +csv:11,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/11,11,issue test011,REQUIREMENT,new,2022-08-10 13:44:46.508+00:00,NULL,0,major,,0,0,,,tgp,abeizn,1,2022-09-15 15:27:56,NULL,0.00014,NULL,102 diff --git a/backend/plugins/customize/e2e/raw_tables/issues_input_incremental.csv b/backend/plugins/customize/e2e/raw_tables/issues_input_incremental.csv index 71ca994db..90dcc4591 100644 --- a/backend/plugins/customize/e2e/raw_tables/issues_input_incremental.csv +++ b/backend/plugins/customize/e2e/raw_tables/issues_input_incremental.csv @@ -1,4 +1,4 @@ -id,url,issue_key,title,original_type,original_status,created_date,resolution_date,story_point,priority,severity,original_estimate_minutes,time_spent_minutes,component,epic_key,creator_name,assignee_name,x_int,x_time,x_varchar,x_float,labels -csv:12,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/12,12,issue test012,REQUIREMENT,new,2022-08-11 13:44:46.508+00:00,NULL,0,major,,0,0,,,tgp,,1,2022-09-15 15:27:56,NULL,0.00014,NULL -csv:13,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/13,13,issue test013,REQUIREMENT,new,2022-08-12 13:44:46.508+00:00,NULL,0,critical,,0,0,,,tgp,,1,2022-09-15 15:27:56,NULL,0.00014,NULL -csv:14,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/14,14,issue test014,INCIDENT,new,2022-08-12 13:45:12.810+00:00,NULL,0,blocker,,0,0,,,tgp,tgp,41534568464351,2022-09-15 15:27:56,NULL,NULL,"label1,label2,label3" +id,url,issue_key,title,original_type,original_status,created_date,resolution_date,story_point,priority,severity,original_estimate_minutes,time_spent_minutes,component,epic_key,creator_name,assignee_name,x_int,x_time,x_varchar,x_float,labels,sprint_ids +csv:12,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/12,12,issue test012,REQUIREMENT,new,2022-08-11 13:44:46.508+00:00,NULL,0,major,,0,0,,,tgp,,1,2022-09-15 15:27:56,NULL,0.00014,NULL,103 +csv:13,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/13,13,issue test013,REQUIREMENT,new,2022-08-12 13:44:46.508+00:00,NULL,0,critical,,0,0,,,tgp,,1,2022-09-15 15:27:56,NULL,0.00014,NULL,101 +csv:14,https://api.bitbucket.org/2.0/repositories/thenicetgp/lake/issues/14,14,issue test014,INCIDENT,new,2022-08-12 13:45:12.810+00:00,NULL,0,blocker,,0,0,,,tgp,tgp,41534568464351,2022-09-15 15:27:56,NULL,NULL,"label1,label2,label3",102 diff --git a/backend/plugins/customize/e2e/raw_tables/sprints.csv b/backend/plugins/customize/e2e/raw_tables/sprints.csv new file mode 100644 index 000000000..d9c4d98ea --- /dev/null +++ b/backend/plugins/customize/e2e/raw_tables/sprints.csv @@ -0,0 +1,3 @@ +id,url,status,name,started_date,ended_date,completed_date +SPRINT-1,http://example.com/sprint1,active,Sprint 1,2023-01-01 00:00:00+00:00,2023-01-14 00:00:00+00:00,2023-01-15 00:00:00+00:00 +SPRINT-2,http://example.com/sprint2,active,Sprint 2,2023-02-01 00:00:00+00:00,2023-02-14 00:00:00+00:00,2023-02-15 00:00:00+00:00 diff --git a/backend/plugins/customize/e2e/raw_tables/sprints_incremental.csv b/backend/plugins/customize/e2e/raw_tables/sprints_incremental.csv new file mode 100644 index 000000000..f3d956c00 --- /dev/null +++ b/backend/plugins/customize/e2e/raw_tables/sprints_incremental.csv @@ -0,0 +1,2 @@ +id,url,status,name,started_date,ended_date,completed_date +SPRINT-3,http://example.com/sprint3,active,Sprint 3,2023-03-01 00:00:00+00:00,2023-03-14 00:00:00+00:00,2023-03-15 00:00:00+00:00 diff --git a/backend/plugins/customize/e2e/snapshot_tables/accounts_from_issue_changelogs.csv b/backend/plugins/customize/e2e/snapshot_tables/accounts_from_issue_changelogs.csv new file mode 100644 index 000000000..bd9ad5701 --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/accounts_from_issue_changelogs.csv @@ -0,0 +1,6 @@ +id,full_name,user_name +csv:CsvAccount:0:user1,user1,user1 +csv:CsvAccount:0:user2,user2,user2 +csv:CsvAccount:0:user3,user3,user3 +csv:CsvAccount:0:user4,user4,user4 +csv:CsvAccount:0:user5,user5,user5 diff --git a/backend/plugins/customize/e2e/snapshot_tables/accounts_from_issue_worklogs.csv b/backend/plugins/customize/e2e/snapshot_tables/accounts_from_issue_worklogs.csv new file mode 100644 index 000000000..4e5080935 --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/accounts_from_issue_worklogs.csv @@ -0,0 +1,4 @@ +id,full_name,user_name +csv:CsvAccount:0:Alice,Alice,Alice +csv:CsvAccount:0:Bob,Bob,Bob +csv:CsvAccount:0:Charlie,Charlie,Charlie diff --git a/backend/plugins/customize/e2e/snapshot_tables/board_sprints.csv b/backend/plugins/customize/e2e/snapshot_tables/board_sprints.csv new file mode 100644 index 000000000..fd7887245 --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/board_sprints.csv @@ -0,0 +1,4 @@ +board_id,sprint_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +csv-board,SPRINT-1,,,0, +csv-board,SPRINT-2,,,0, +csv-board,SPRINT-3,,,0, diff --git a/backend/plugins/customize/e2e/snapshot_tables/issue_changelogs.csv b/backend/plugins/customize/e2e/snapshot_tables/issue_changelogs.csv new file mode 100644 index 000000000..38216871c --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/issue_changelogs.csv @@ -0,0 +1,7 @@ +id,issue_id,author_id,field_name,original_from_value,original_to_value,created_date,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +changelog1,issue1,user1,status,Open,In Progress,2023-01-01T10:00:00.000+00:00,TEST_BOARD,,, +changelog2,issue1,user2,assignee,csv:CsvAccount:0:user1,csv:CsvAccount:0:user2,2023-01-02T10:00:00.000+00:00,TEST_BOARD,,, +changelog3,issue1,user3,Sprint,Sprint1,"Sprint2,Sprint3",2023-01-03T10:00:00.000+00:00,TEST_BOARD,,, +changelog4,issue2,user1,status,In Progress,Done,2023-01-04T10:00:00.000+00:00,TEST_BOARD,,, +changelog5,issue2,user4,assignee,csv:CsvAccount:0:user3,csv:CsvAccount:0:user4,2023-01-05T10:00:00.000+00:00,TEST_BOARD,,, +changelog6,issue2,user2,Sprint,"Sprint2,Sprint3",Sprint4,2023-01-06T10:00:00.000+00:00,TEST_BOARD,,, diff --git a/backend/plugins/customize/e2e/snapshot_tables/issue_changelogs_incremental.csv b/backend/plugins/customize/e2e/snapshot_tables/issue_changelogs_incremental.csv new file mode 100644 index 000000000..e13481a0b --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/issue_changelogs_incremental.csv @@ -0,0 +1,11 @@ +id,issue_id,author_id,field_name,original_from_value,original_to_value,created_date,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +changelog1,issue1,user1,status,Open,In Progress,2023-01-01T10:00:00.000+00:00,TEST_BOARD,,, +changelog10,issue3,user1,status,Open,In Progress,2023-01-10T10:00:00.000+00:00,TEST_BOARD,,, +changelog2,issue1,user2,assignee,csv:CsvAccount:0:user1,csv:CsvAccount:0:user2,2023-01-02T10:00:00.000+00:00,TEST_BOARD,,, +changelog3,issue1,user3,Sprint,Sprint1,"Sprint2,Sprint3",2023-01-03T10:00:00.000+00:00,TEST_BOARD,,, +changelog4,issue2,user1,status,In Progress,Done,2023-01-04T10:00:00.000+00:00,TEST_BOARD,,, +changelog5,issue2,user4,assignee,csv:CsvAccount:0:user3,csv:CsvAccount:0:user4,2023-01-05T10:00:00.000+00:00,TEST_BOARD,,, +changelog6,issue2,user2,Sprint,"Sprint2,Sprint3",Sprint4,2023-01-06T10:00:00.000+00:00,TEST_BOARD,,, +changelog7,issue1,user3,status,Done,Reopened,2023-01-07T10:00:00.000+00:00,TEST_BOARD,,, +changelog8,issue1,user5,assignee,csv:CsvAccount:0:user2,csv:CsvAccount:0:user5,2023-01-08T10:00:00.000+00:00,TEST_BOARD,,, +changelog9,issue1,user4,Sprint,Sprint3,"Sprint4,Sprint5",2023-01-09T10:00:00.000+00:00,TEST_BOARD,,, diff --git a/backend/plugins/customize/e2e/snapshot_tables/issue_worklogs.csv b/backend/plugins/customize/e2e/snapshot_tables/issue_worklogs.csv new file mode 100644 index 000000000..3bacb2d21 --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/issue_worklogs.csv @@ -0,0 +1,4 @@ +id,issue_id,author_id,time_spent_minutes,started_date,logged_date,comment,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +worklog1,ISSUE-1,csv:CsvAccount:0:Alice,30,2023-01-01T10:00:00.000+00:00,2022-07-17T07:15:55.959+00:00,Initial work,TEST_BOARD,,, +worklog2,ISSUE-1,csv:CsvAccount:0:Bob,45,2023-01-02T11:00:00.000+00:00,2022-07-18T08:20:30.123+00:00,Follow up,TEST_BOARD,,, +worklog3,ISSUE-2,csv:CsvAccount:0:Alice,60,2023-01-03T09:00:00.000+00:00,2022-07-19T09:45:15.456+00:00,Task completion,TEST_BOARD,,, diff --git a/backend/plugins/customize/e2e/snapshot_tables/issue_worklogs_incremental.csv b/backend/plugins/customize/e2e/snapshot_tables/issue_worklogs_incremental.csv new file mode 100644 index 000000000..15b98726b --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/issue_worklogs_incremental.csv @@ -0,0 +1,5 @@ +id,issue_id,author_id,time_spent_minutes,started_date,logged_date,comment,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +worklog1,ISSUE-1,csv:CsvAccount:0:Alice,30,2023-01-01T10:00:00.000+00:00,2022-07-17T07:15:55.959+00:00,Initial work,TEST_BOARD,,, +worklog2,ISSUE-1,csv:CsvAccount:0:Bob,20,2023-01-02T11:00:00.000+00:00,2022-07-21T11:15:30.000+00:00,Updated time,TEST_BOARD,,, +worklog3,ISSUE-2,csv:CsvAccount:0:Alice,60,2023-01-03T09:00:00.000+00:00,2022-07-19T09:45:15.456+00:00,Task completion,TEST_BOARD,,, +worklog4,ISSUE-2,csv:CsvAccount:0:Charlie,15,2023-01-04T14:00:00.000+00:00,2022-07-20T10:30:45.789+00:00,Quick fix,TEST_BOARD,,, diff --git a/backend/plugins/customize/e2e/snapshot_tables/sprint_issues.csv b/backend/plugins/customize/e2e/snapshot_tables/sprint_issues.csv new file mode 100644 index 000000000..6d87a3881 --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/sprint_issues.csv @@ -0,0 +1,8 @@ +sprint_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +101,csv:1,,,0, +101,csv:10,,,0, +101,csv:13,,,0, +102,csv:1,,,0, +102,csv:11,,,0, +102,csv:14,,,0, +103,csv:12,,,0, diff --git a/backend/plugins/customize/e2e/snapshot_tables/sprints.csv b/backend/plugins/customize/e2e/snapshot_tables/sprints.csv new file mode 100644 index 000000000..3019c260e --- /dev/null +++ b/backend/plugins/customize/e2e/snapshot_tables/sprints.csv @@ -0,0 +1,4 @@ +id,url,status,name,started_date,ended_date,completed_date,original_board_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark +SPRINT-1,http://example.com/sprint1,active,Sprint 1,2023-01-01T00:00:00.000+00:00,2023-01-14T00:00:00.000+00:00,2023-01-15T00:00:00.000+00:00,csv-board,csv-board,,, +SPRINT-2,http://example.com/sprint2,active,Sprint 2,2023-02-01T00:00:00.000+00:00,2023-02-14T00:00:00.000+00:00,2023-02-15T00:00:00.000+00:00,csv-board,csv-board,,, +SPRINT-3,http://example.com/sprint3,active,Sprint 3,2023-03-01T00:00:00.000+00:00,2023-03-14T00:00:00.000+00:00,2023-03-15T00:00:00.000+00:00,csv-board,csv-board,,, diff --git a/backend/plugins/customize/impl/impl.go b/backend/plugins/customize/impl/impl.go index 599b02c60..dd689b794 100644 --- a/backend/plugins/customize/impl/impl.go +++ b/backend/plugins/customize/impl/impl.go @@ -108,6 +108,15 @@ func (p Customize) ApiResources() map[string]map[string]plugin.ApiResourceHandle "csvfiles/issue_repo_commits.csv": { "POST": handlers.ImportIssueRepoCommit, }, + "csvfiles/issue_changelogs.csv": { + "POST": handlers.ImportIssueChangelog, + }, + "csvfiles/issue_worklogs.csv": { + "POST": handlers.ImportIssueWorklog, + }, + "csvfiles/sprints.csv": { + "POST": handlers.ImportSprint, + }, "csvfiles/qa_apis.csv": { "POST": handlers.ImportQaApis, }, diff --git a/backend/plugins/customize/service/service.go b/backend/plugins/customize/service/service.go index 22267453f..89e8833cf 100644 --- a/backend/plugins/customize/service/service.go +++ b/backend/plugins/customize/service/service.go @@ -283,7 +283,7 @@ func (s *Service) createOrUpdateAccount(accountName string, rawDataParams string }, FullName: accountName, UserName: accountName, - CreatedDate: &now, + CreatedDate: &now, // FIXME: will update created_date if already exists. to debug, using created_at instead } err := s.dal.CreateOrUpdate(account) if err != nil { @@ -395,6 +395,26 @@ func (s *Service) issueHandlerFactory(boardId string, incremental bool) func(rec record["assignee_id"] = assigneeId } + // Handle sprint_ids + sprintIds, err := getStringField(record, "sprint_ids", false) + if err != nil { + return err + } + sprints := strings.Split(strings.TrimSpace(sprintIds), ",") + for _, sprintId := range sprints { + sprintId = strings.TrimSpace(sprintId) + if sprintId != "" { + err = s.dal.CreateOrUpdate(&ticket.SprintIssue{ + SprintId: sprintId, + IssueId: id, + }) + if err != nil { + return err + } + } + } + delete(record, "sprint_ids") + // Handle issues err = s.dal.CreateWithMap(&ticket.Issue{}, record) if err != nil { @@ -539,3 +559,143 @@ func (s *Service) issueRepoCommitHandler(record map[string]interface{}) errors.E delete(record, "repo_url") return s.dal.CreateWithMap(&crossdomain.IssueCommit{}, record) } + +// ImportSprint imports csv file into the table `sprints` +func (s *Service) ImportSprint(boardId string, file io.ReadCloser, incremental bool) errors.Error { + if !incremental { + err := s.dal.Delete( + &ticket.Sprint{}, + dal.Where("id IN (SELECT sprint_id FROM board_sprints WHERE board_id=? AND sprint_id NOT IN (SELECT sprint_id FROM board_sprints WHERE board_id!=?))", boardId, boardId), + ) + if err != nil { + return err + } + err = s.dal.Delete( + &ticket.BoardSprint{}, + dal.Where("board_id = ?", boardId), + ) + if err != nil { + return err + } + } + return s.importCSV(file, boardId, s.sprintHandler(boardId)) +} + +// sprintHandler saves a record into the `sprints` table +func (s *Service) sprintHandler(boardId string) func(record map[string]interface{}) errors.Error { + return func(record map[string]interface{}) errors.Error { + id, err := getStringField(record, "id", true) + if err != nil { + return err + } + record["original_board_id"] = boardId + err = s.dal.CreateWithMap(&ticket.Sprint{}, record) + if err != nil { + return err + } + + // Create board_sprint relation + return s.dal.CreateOrUpdate(&ticket.BoardSprint{ + BoardId: boardId, + SprintId: id, + }) + } +} + +// ImportIssueChangelog imports csv file into the table `issue_changelogs` +func (s *Service) ImportIssueChangelog(boardId string, file io.ReadCloser, incremental bool) errors.Error { + if !incremental { + err := s.dal.Delete( + &ticket.IssueChangelogs{}, + dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId), + ) + if err != nil { + return err + } + } + return s.importCSV(file, boardId, s.issueChangelogHandler) +} + +// issueChangelogHandler saves a record into the `issue_changelogs` table +func (s *Service) issueChangelogHandler(record map[string]interface{}) errors.Error { + // create account + authorName, err := getStringField(record, "author_name", false) + if err != nil { + return err + } + rawDataParams, err := getStringField(record, "_raw_data_params", true) + if err != nil { + return err + } + if authorName != "" { + authorId, err := s.createOrUpdateAccount(authorName, rawDataParams) + if err != nil { + return err + } + record["author_id"] = authorId + } + // set field_id = field_name + fieldName, err := getStringField(record, "field_name", true) + if err != nil { + return err + } + record["field_id"] = fieldName + // handle assignee + if fieldName == "assignee" { + originalFromValue, err := getStringField(record, "original_from_value", false) + if err != nil { + return err + } + originalToValue, err := getStringField(record, "original_to_value", false) + if err != nil { + return err + } + fromId, err := s.createOrUpdateAccount(originalFromValue, rawDataParams) + if err != nil { + return err + } + record["original_from_value"] = fromId + toId, err := s.createOrUpdateAccount(originalToValue, rawDataParams) + if err != nil { + return err + } + record["original_to_value"] = toId + } + return s.dal.CreateWithMap(&ticket.IssueChangelogs{}, record) +} + +// ImportIssueWorklog imports csv file into the table `issue_worklogs` +func (s *Service) ImportIssueWorklog(boardId string, file io.ReadCloser, incremental bool) errors.Error { + if !incremental { + err := s.dal.Delete( + &ticket.IssueWorklog{}, + dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId), + ) + if err != nil { + return err + } + } + return s.importCSV(file, boardId, s.issueWorklogHandler) +} + +// issueWorklogHandler saves a record into the `issue_worklogs` table +func (s *Service) issueWorklogHandler(record map[string]interface{}) errors.Error { + // create account + authorName, err := getStringField(record, "author_name", false) + if err != nil { + return err + } + if authorName != "" { + rawDataParams, err := getStringField(record, "_raw_data_params", true) + if err != nil { + return err + } + authorId, err := s.createOrUpdateAccount(authorName, rawDataParams) + if err != nil { + return err + } + record["author_id"] = authorId + } + delete(record, "author_name") + return s.dal.CreateWithMap(&ticket.IssueWorklog{}, record) +}