This is an automated email from the ASF dual-hosted git repository.

lynwee pushed a commit to branch release-v1.0
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/release-v1.0 by this push:
     new ae4d8b98e cherry pick 
https://github.com/apache/incubator-devlake/pull/8298/ and 
https://github.com/apache/incubator-devlake/pull/8300 to v1.0 (#8301)
ae4d8b98e is described below

commit ae4d8b98e5190d57cb8726d30df09fd8eddc4b26
Author: Lynwee <[email protected]>
AuthorDate: Sat Feb 22 00:04:25 2025 +0800

    cherry pick https://github.com/apache/incubator-devlake/pull/8298/ and 
https://github.com/apache/incubator-devlake/pull/8300 to v1.0 (#8301)
    
    * feat: support Zentao task worklogs (#8298)
    
    * feat: support Zentao task worklogs
    
    * feat: Zentao worklog tool layer migration
    
    * fix: dict as raw data instead of slice
    
    * test: e2e for zentao task worklogs
    
    * fix: compatibility when no account available
    
    ---------
    
    Co-authored-by: Lynwee <[email protected]>
    
    * fix: go lint and import errors (#8300)
    
    * fix: import errors
    
    * fix: go fmt issues
    
    * fix: missing quote
    
    * fix: skip float accuracy issues
    
    * fix(zentao): fix e2e tests
    
    ---------
    
    Co-authored-by: Chaojie Yan <[email protected]>
---
 .../raw_tables/_raw_zentao_api_task_worklogs.csv   |   3 +
 .../e2e/snapshot_tables/_tool_zentao_worklogs.csv  |   3 +
 .../zentao/e2e/snapshot_tables/issue_worklogs.csv  |   3 +
 backend/plugins/zentao/e2e/task_worklogs_test.go   |  64 +++++++++++
 backend/plugins/zentao/impl/impl.go                |   5 +
 .../{register.go => 20250219_add_worklogs.go}      |  32 +++---
 .../models/migrationscripts/archived/worklog.go    |  48 ++++++++
 .../zentao/models/migrationscripts/register.go     |   1 +
 backend/plugins/zentao/models/worklog.go           |  48 ++++++++
 .../plugins/zentao/tasks/task_worklog_collector.go | 121 ++++++++++++++++++++
 .../plugins/zentao/tasks/task_worklog_convertor.go | 124 +++++++++++++++++++++
 .../plugins/zentao/tasks/task_worklog_extractor.go | 100 +++++++++++++++++
 12 files changed, 539 insertions(+), 13 deletions(-)

diff --git 
a/backend/plugins/zentao/e2e/raw_tables/_raw_zentao_api_task_worklogs.csv 
b/backend/plugins/zentao/e2e/raw_tables/_raw_zentao_api_task_worklogs.csv
new file mode 100644
index 000000000..41131be4b
--- /dev/null
+++ b/backend/plugins/zentao/e2e/raw_tables/_raw_zentao_api_task_worklogs.csv
@@ -0,0 +1,3 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProjectId"":48}","{""id"":106,""objectType"":""task"",""objectID"":135,""product"":"""",""project"":48,""execution"":49,""account"":""devlake"",""work"":""sample
 
worklog"",""vision"":""rnd"",""date"":""2025-02-20"",""left"":5,""consumed"":11,""begin"":0,""end"":0,""extra"":null,""order"":0,""deleted"":""0""}",http://iwater.red:8000/api.php/v1/tasks/135/estimate,"{""id"":135}",2025-02-21
 06:28:36.902
+2,"{""ConnectionId"":1,""ProjectId"":48}","{""id"":107,""objectType"":""task"",""objectID"":135,""product"":"""",""project"":48,""execution"":49,""account"":""devlake"",""work"":"""",""vision"":""rnd"",""date"":""2025-02-20"",""left"":1,""consumed"":4,""begin"":0,""end"":0,""extra"":null,""order"":0,""deleted"":""0""}",http://iwater.red:8000/api.php/v1/tasks/135/estimate,"{""id"":135}",2025-02-21
 06:28:37.001
diff --git 
a/backend/plugins/zentao/e2e/snapshot_tables/_tool_zentao_worklogs.csv 
b/backend/plugins/zentao/e2e/snapshot_tables/_tool_zentao_worklogs.csv
new file mode 100644
index 000000000..4cfce3fc1
--- /dev/null
+++ b/backend/plugins/zentao/e2e/snapshot_tables/_tool_zentao_worklogs.csv
@@ -0,0 +1,3 @@
+connection_id,id,object_id,object_type,project,execution,product,account,work,vision,date,left,consumed,begin,end,extra,order,deleted
+1,106,135,task,48,49,,devlake,sample worklog,rnd,2025-02-20,5,11,0,0,,0,0
+1,107,135,task,48,49,,devlake,,rnd,2025-02-20,1,4,0,0,,0,0
diff --git a/backend/plugins/zentao/e2e/snapshot_tables/issue_worklogs.csv 
b/backend/plugins/zentao/e2e/snapshot_tables/issue_worklogs.csv
new file mode 100644
index 000000000..2afb729c0
--- /dev/null
+++ b/backend/plugins/zentao/e2e/snapshot_tables/issue_worklogs.csv
@@ -0,0 +1,3 @@
+id,author_id,comment,time_spent_minutes,logged_date,started_date,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark
+zentao:ZentaoWorklog:1:106,zentao:ZentaoAccount:1:1,sample 
worklog,660,2025-02-20T00:00:00.000+00:00,2025-02-20T00:00:00.000+00:00,zentao:ZentaoTask:1:135,"{""ConnectionId"":1,""ProjectId"":48}",_raw_zentao_api_task_worklogs,1,
+zentao:ZentaoWorklog:1:107,zentao:ZentaoAccount:1:1,,240,2025-02-20T00:00:00.000+00:00,2025-02-20T00:00:00.000+00:00,zentao:ZentaoTask:1:135,"{""ConnectionId"":1,""ProjectId"":48}",_raw_zentao_api_task_worklogs,2,
diff --git a/backend/plugins/zentao/e2e/task_worklogs_test.go 
b/backend/plugins/zentao/e2e/task_worklogs_test.go
new file mode 100644
index 000000000..444a70477
--- /dev/null
+++ b/backend/plugins/zentao/e2e/task_worklogs_test.go
@@ -0,0 +1,64 @@
+/*
+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 (
+       "testing"
+
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
+       "github.com/apache/incubator-devlake/helpers/e2ehelper"
+       "github.com/apache/incubator-devlake/plugins/zentao/impl"
+       "github.com/apache/incubator-devlake/plugins/zentao/models"
+       "github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func TestZentaoTaskWorklogDataFlow(t *testing.T) {
+
+       var zentao impl.Zentao
+       dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+       taskData := &tasks.ZentaoTaskData{
+               Options: &tasks.ZentaoOptions{
+                       ConnectionId: 1,
+                       ProjectId:    48,
+               },
+               ApiClient: getFakeAPIClient(),
+       }
+
+       // import _raw_zentao_api_task_worklogs raw data table
+       
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_task_worklogs.csv",
+               "_raw_zentao_api_task_worklogs")
+       
dataflowTester.ImportCsvIntoTabler("./snapshot_tables/_tool_zentao_accounts.csv",
 &models.ZentaoAccount{})
+
+       // verify worklogs extraction
+       dataflowTester.FlushTabler(&models.ZentaoWorklog{})
+       dataflowTester.Subtask(tasks.ExtractTaskWorklogsMeta, taskData)
+       dataflowTester.VerifyTableWithOptions(&models.ZentaoWorklog{}, 
e2ehelper.TableOptions{
+               CSVRelPath:  "./snapshot_tables/_tool_zentao_worklogs.csv",
+               IgnoreTypes: []interface{}{common.NoPKModel{}},
+       })
+
+       // verify task repo commit conversion
+       dataflowTester.FlushTabler(&ticket.IssueWorklog{})
+       dataflowTester.Subtask(tasks.ConvertTaskWorklogsMeta, taskData)
+       dataflowTester.VerifyTableWithOptions(&ticket.IssueWorklog{}, 
e2ehelper.TableOptions{
+               CSVRelPath:  "./snapshot_tables/issue_worklogs.csv",
+               IgnoreTypes: []interface{}{common.NoPKModel{}},
+       })
+}
diff --git a/backend/plugins/zentao/impl/impl.go 
b/backend/plugins/zentao/impl/impl.go
index 2c24a235d..2111426b0 100644
--- a/backend/plugins/zentao/impl/impl.go
+++ b/backend/plugins/zentao/impl/impl.go
@@ -91,6 +91,7 @@ func (p Zentao) GetTablesInfo() []dal.Tabler {
                &models.ZentaoExecutionSummary{},
                &models.ZentaoProductSummary{},
                &models.ZentaoProjectStory{},
+               &models.ZentaoWorklog{},
        }
 }
 
@@ -160,6 +161,10 @@ func (p Zentao) SubTaskMetas() []plugin.SubTaskMeta {
 
                tasks.DBGetChangelogMeta,
                tasks.ConvertChangelogMeta,
+
+               tasks.CollectTaskWorklogsMeta,
+               tasks.ExtractTaskWorklogsMeta,
+               tasks.ConvertTaskWorklogsMeta,
        }
 }
 
diff --git a/backend/plugins/zentao/models/migrationscripts/register.go 
b/backend/plugins/zentao/models/migrationscripts/20250219_add_worklogs.go
similarity index 58%
copy from backend/plugins/zentao/models/migrationscripts/register.go
copy to backend/plugins/zentao/models/migrationscripts/20250219_add_worklogs.go
index 914eb1f11..0b11778a4 100644
--- a/backend/plugins/zentao/models/migrationscripts/register.go
+++ b/backend/plugins/zentao/models/migrationscripts/20250219_add_worklogs.go
@@ -18,19 +18,25 @@ limitations under the License.
 package migrationscripts
 
 import (
-       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+       
"github.com/apache/incubator-devlake/plugins/zentao/models/migrationscripts/archived"
 )
 
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
-       return []plugin.MigrationScript{
-               new(addInitTables),
-               new(addScopeConfigTables),
-               new(addIssueRepoCommitsTables),
-               new(addInitChangelogTables),
-               new(addTaskLeft),
-               new(addExecutionStoryAndExecutionSummary),
-               new(addRawParamTableForScope),
-               new(dropTotalReal),
-       }
+type addWorklogs struct{}
+
+func (*addWorklogs) Up(basicRes context.BasicRes) errors.Error {
+       return migrationhelper.AutoMigrateTables(
+               basicRes,
+               &archived.ZentaoWorklog{},
+       )
+}
+
+func (*addWorklogs) Version() uint64 {
+       return 20250219153329
+}
+
+func (*addWorklogs) Name() string {
+       return "add table _tool_zentao_worklogs"
 }
diff --git a/backend/plugins/zentao/models/migrationscripts/archived/worklog.go 
b/backend/plugins/zentao/models/migrationscripts/archived/worklog.go
new file mode 100644
index 000000000..86fdca66f
--- /dev/null
+++ b/backend/plugins/zentao/models/migrationscripts/archived/worklog.go
@@ -0,0 +1,48 @@
+/*
+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 archived
+
+import (
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type ZentaoWorklog struct {
+       archived.NoPKModel
+       ConnectionId uint64  `gorm:"primaryKey;type:BIGINT NOT NULL"`
+       Id           uint64  `gorm:"primaryKey;type:BIGINT NOT 
NULL;autoIncrement:false" json:"id"`
+       ObjectId     uint64  `json:"objectID"`
+       ObjectType   string  `json:"objectType"`
+       Project      uint64  `json:"project"`
+       Execution    uint64  `json:"execution"`
+       Product      string  `json:"product"`
+       Account      string  `json:"account"`
+       Work         string  `json:"work"`
+       Vision       string  `json:"vision"`
+       Date         string  `json:"date"`
+       Left         float32 `json:"left"`
+       Consumed     float32 `json:"consumed"`
+       Begin        uint64  `json:"begin"`
+       End          uint64  `json:"end"`
+       Extra        *string `json:"extra"`
+       Order        uint64  `json:"order"`
+       Deleted      string  `json:"deleted"`
+}
+
+func (ZentaoWorklog) TableName() string {
+       return "_tool_zentao_worklogs"
+}
diff --git a/backend/plugins/zentao/models/migrationscripts/register.go 
b/backend/plugins/zentao/models/migrationscripts/register.go
index 914eb1f11..582d5432c 100644
--- a/backend/plugins/zentao/models/migrationscripts/register.go
+++ b/backend/plugins/zentao/models/migrationscripts/register.go
@@ -32,5 +32,6 @@ func All() []plugin.MigrationScript {
                new(addExecutionStoryAndExecutionSummary),
                new(addRawParamTableForScope),
                new(dropTotalReal),
+               new(addWorklogs),
        }
 }
diff --git a/backend/plugins/zentao/models/worklog.go 
b/backend/plugins/zentao/models/worklog.go
new file mode 100644
index 000000000..24ec38249
--- /dev/null
+++ b/backend/plugins/zentao/models/worklog.go
@@ -0,0 +1,48 @@
+/*
+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 models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type ZentaoWorklog struct {
+       ConnectionId uint64  `gorm:"primaryKey;type:BIGINT NOT NULL"`
+       Id           int64   `gorm:"primaryKey;type:BIGINT NOT 
NULL;autoIncrement:false" json:"id"`
+       ObjectId     int64   `json:"objectID"`
+       ObjectType   string  `json:"objectType"`
+       Project      int64   `json:"project"`
+       Execution    int64   `json:"execution"`
+       Product      string  `json:"product"`
+       Account      string  `json:"account"`
+       Work         string  `json:"work"`
+       Vision       string  `json:"vision"`
+       Date         string  `json:"date"`
+       Left         float32 `json:"left"`
+       Consumed     float32 `json:"consumed"`
+       Begin        int64   `json:"begin"`
+       End          int64   `json:"end"`
+       Extra        *string `json:"extra"`
+       Order        int64   `json:"order"`
+       Deleted      string  `json:"deleted"`
+       common.NoPKModel
+}
+
+func (ZentaoWorklog) TableName() string {
+       return "_tool_zentao_worklogs"
+}
diff --git a/backend/plugins/zentao/tasks/task_worklog_collector.go 
b/backend/plugins/zentao/tasks/task_worklog_collector.go
new file mode 100644
index 000000000..4bf610288
--- /dev/null
+++ b/backend/plugins/zentao/tasks/task_worklog_collector.go
@@ -0,0 +1,121 @@
+/*
+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 tasks
+
+import (
+       "encoding/json"
+       "net/http"
+       "net/url"
+       "reflect"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+const RAW_TASK_WORKLOGS_TABLE = "zentao_api_task_worklogs"
+
+var CollectTaskWorklogsMeta = plugin.SubTaskMeta{
+       Name:             "collectTaskWorklogs",
+       EntryPoint:       CollectTaskWorklogs,
+       EnabledByDefault: true,
+       Description:      "collect Zentao task work logs, supports both 
timeFilter and diffSync.",
+       DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+}
+
+type Input struct {
+       Id uint64 `json:"id"`
+}
+
+func CollectTaskWorklogs(taskCtx plugin.SubTaskContext) errors.Error {
+       db := taskCtx.GetDal()
+       data := taskCtx.GetData().(*ZentaoTaskData)
+
+       logger := taskCtx.GetLogger()
+
+       apiCollector, err := api.NewStatefulApiCollector(api.RawDataSubTaskArgs{
+               Ctx:     taskCtx,
+               Options: data.Options,
+               Table:   RAW_TASK_WORKLOGS_TABLE,
+       })
+       if err != nil {
+               return err
+       }
+
+       // load task IDs from db
+       clauses := []dal.Clause{
+               dal.Select("id"),
+               dal.From(&models.ZentaoTask{}),
+               dal.Where(
+                       "project = ? AND connection_id = ?",
+                       data.Options.ProjectId, data.Options.ConnectionId,
+               ),
+       }
+       if apiCollector.IsIncremental() && apiCollector.GetSince() != nil {
+               clauses = append(clauses, dal.Where("last_edited_date IS NOT 
NULL AND last_edited_date > ?", apiCollector.GetSince()))
+       }
+
+       // construct the input iterator
+       cursor, err := db.Cursor(clauses...)
+       if err != nil {
+               return err
+       }
+       iterator, err := api.NewDalCursorIterator(db, cursor, 
reflect.TypeOf(Input{}))
+       if err != nil {
+               return err
+       }
+
+       // collect task worklogs
+       err = apiCollector.InitCollector(api.ApiCollectorArgs{
+               Input:       iterator,
+               ApiClient:   data.ApiClient,
+               UrlTemplate: "tasks/{{ .Input.Id }}/estimate",
+               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
+                       return nil, nil
+               },
+               ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
+                       var data struct {
+                               Effort json.RawMessage `json:"effort"`
+                       }
+                       err := api.UnmarshalResponse(res, &data)
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       if string(data.Effort) == "{}" || string(data.Effort) 
== "null" {
+                               return nil, nil
+                       }
+
+                       var efforts []json.RawMessage
+                       jsonErr := json.Unmarshal(data.Effort, &efforts)
+                       if jsonErr != nil {
+                               return nil, errors.Default.Wrap(jsonErr, 
"failed to unmarshal efforts")
+                       }
+                       return efforts, nil
+               },
+               AfterResponse: ignoreHTTPStatus404,
+       })
+       if err != nil {
+               logger.Error(err, "collect Zentao task worklogs error")
+               return err
+       }
+
+       return apiCollector.Execute()
+}
diff --git a/backend/plugins/zentao/tasks/task_worklog_convertor.go 
b/backend/plugins/zentao/tasks/task_worklog_convertor.go
new file mode 100644
index 000000000..9fea703b5
--- /dev/null
+++ b/backend/plugins/zentao/tasks/task_worklog_convertor.go
@@ -0,0 +1,124 @@
+/*
+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 tasks
+
+import (
+       "reflect"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/core/models/domainlayer"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var ConvertTaskWorklogsMeta = plugin.SubTaskMeta{
+       Name:             "convertTaskWorklogs",
+       EntryPoint:       ConvertTaskWorklogs,
+       EnabledByDefault: true,
+       Description:      "convert Zentao task worklogs",
+       DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertTaskWorklogs(taskCtx plugin.SubTaskContext) errors.Error {
+       logger := taskCtx.GetLogger()
+       db := taskCtx.GetDal()
+       data := taskCtx.GetData().(*ZentaoTaskData)
+       logger.Info(
+               "convert Zentao task worklogs of %d in %d",
+               data.Options.ProjectId,
+               data.Options.ConnectionId,
+       )
+       worklogIdGen := didgen.NewDomainIdGenerator(&models.ZentaoWorklog{})
+       clauses := []dal.Clause{
+               dal.From(&models.ZentaoWorklog{}),
+               dal.Where(
+                       "connection_id = ? AND project = ? AND object_type = ?",
+                       data.Options.ConnectionId,
+                       data.Options.ProjectId,
+                       "task",
+               ),
+       }
+
+       cursor, err := db.Cursor(clauses...)
+       if err != nil {
+               return err
+       }
+       defer cursor.Close()
+
+       taskIdGen := didgen.NewDomainIdGenerator(&models.ZentaoTask{})
+
+       converter, err := helper.NewDataConverter(helper.DataConverterArgs{
+               RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+                       Ctx:     taskCtx,
+                       Options: data.Options,
+                       Table:   RAW_TASK_WORKLOGS_TABLE,
+               },
+               InputRowType: reflect.TypeOf(models.ZentaoWorklog{}),
+               Input:        cursor,
+               Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
+                       toolL := inputRow.(*models.ZentaoWorklog)
+                       domainL := &ticket.IssueWorklog{
+                               DomainEntity: domainlayer.DomainEntity{
+                                       Id: 
worklogIdGen.Generate(data.Options.ConnectionId, toolL.Id),
+                               },
+                               Comment:          toolL.Work,
+                               TimeSpentMinutes: int(toolL.Consumed * 60),
+                       }
+                       timeData, err := common.ConvertStringToTime(toolL.Date)
+                       if err != nil {
+                               return nil, errors.Default.Wrap(err, "failed to 
convert zentao task worklog date")
+                       }
+                       // zentao task only has one field as date type for 
worklog creation
+                       domainL.StartedDate = &timeData
+                       domainL.LoggedDate = &timeData
+
+                       domainL.IssueId = 
taskIdGen.Generate(data.Options.ConnectionId, toolL.ObjectId)
+
+                       // get ID of account by username
+                       var account models.ZentaoAccount
+                       err = db.First(&account, dal.Where("connection_id = ? 
AND account = ?",
+                               data.Options.ConnectionId, toolL.Account))
+                       if err != nil {
+                               // if account isn't available, giving empty 
string as ID
+                               if db.IsErrorNotFound(err) {
+                                       logger.Warn(nil, "cannot find zentao 
account by account: %s", toolL.Account)
+                                       domainL.AuthorId = ""
+                               } else {
+                                       return nil, errors.Default.Wrap(err, 
"failed to get zentao account by account")
+                               }
+                       } else {
+                               accountIdGen := 
didgen.NewDomainIdGenerator(&models.ZentaoAccount{})
+                               domainL.AuthorId = 
accountIdGen.Generate(account.ConnectionId, account.ID)
+                       }
+
+                       return []interface{}{
+                               domainL,
+                       }, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return converter.Execute()
+}
diff --git a/backend/plugins/zentao/tasks/task_worklog_extractor.go 
b/backend/plugins/zentao/tasks/task_worklog_extractor.go
new file mode 100644
index 000000000..928890675
--- /dev/null
+++ b/backend/plugins/zentao/tasks/task_worklog_extractor.go
@@ -0,0 +1,100 @@
+/*
+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 tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractTaskWorklogs
+
+var ExtractTaskWorklogsMeta = plugin.SubTaskMeta{
+       Name:             "extractTaskWorklogs",
+       EntryPoint:       ExtractTaskWorklogs,
+       EnabledByDefault: true,
+       Description:      "Extract raw zentao task worklog data into tool layer 
table _tool_zentao_worklogs",
+       DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractTaskWorklogs(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*ZentaoTaskData)
+       extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+               RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+                       Ctx:     taskCtx,
+                       Options: data.Options,
+                       Table:   RAW_TASK_WORKLOGS_TABLE,
+               },
+               Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+                       var input struct {
+                               Id         int64   `json:"id"`
+                               ObjectType string  `json:"objectType"`
+                               ObjectId   int64   `json:"objectID"`
+                               Product    string  `json:"product"`
+                               Project    int64   `json:"project"`
+                               Execution  int64   `json:"Execution"`
+                               Account    string  `json:"account"`
+                               Work       string  `json:"work"`
+                               Vision     string  `json:"vision"`
+                               Date       string  `json:"date"`
+                               Left       float32 `json:"left"`
+                               Consumed   float32 `json:"consumed"`
+                               Begin      int64   `json:"begin"`
+                               End        int64   `json:"end"`
+                               Extra      *string `json:"extra"`
+                               Order      int64   `json:"order"`
+                               Deleted    string  `json:"deleted"`
+                       }
+
+                       err := errors.Convert(json.Unmarshal(row.Data, &input))
+                       if err != nil {
+                               return nil, err
+                       }
+                       worklog := &models.ZentaoWorklog{
+                               ConnectionId: data.Options.ConnectionId,
+                               Id:           input.Id,
+                               ObjectId:     input.ObjectId,
+                               ObjectType:   input.ObjectType,
+                               Project:      input.Project,
+                               Execution:    input.Execution,
+                               Product:      input.Product,
+                               Account:      input.Account,
+                               Work:         input.Work,
+                               Vision:       input.Vision,
+                               Date:         input.Date,
+                               Left:         input.Left,
+                               Consumed:     input.Consumed,
+                               Begin:        input.Begin,
+                               End:          input.End,
+                               Extra:        input.Extra,
+                               Order:        input.Order,
+                               Deleted:      input.Deleted,
+                       }
+                       return []interface{}{worklog}, nil
+               },
+       })
+
+       if err != nil {
+               return err
+       }
+
+       return extractor.Execute()
+}

Reply via email to