You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2022/11/19 03:32:23 UTC
[incubator-devlake] branch feat-plugin-zentao updated: Feat update zentao (#3763)
This is an automated email from the ASF dual-hosted git repository.
warren pushed a commit to branch feat-plugin-zentao
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/feat-plugin-zentao by this push:
new 626bcb054 Feat update zentao (#3763)
626bcb054 is described below
commit 626bcb0547d64a04c071ccdbabfa429e9ef5b680
Author: Warren Chen <yi...@merico.dev>
AuthorDate: Sat Nov 19 11:32:20 2022 +0800
Feat update zentao (#3763)
* feat(zentao): create new plugin
Relate to #2961
* feat(zentao): create new plugin
Relate to #2961
* fix(zentao): update connection
* feat(zentao): update project collector
* feat(zentao):add zentao project extractor
* fix(zentao): fix minor issues
* feat(zentao): add execution
* feat(zentao): fix execution time
* feat(zentao): add zentao stories
* fix(zentao):apiurl&model error
* fix(zentao):restore code
* feat(zentao):add zentao bugs
* fix(zentao): rebase
* feat(zentao):add zentao task and change stories to story
* feat(zentao): finish the initial version
* fix(zentao): update lint
* feat(zentao): add config-ui support
Co-authored-by: yuqiangabab <11...@qq.com>
---
.../blueprints/ProviderTransformationSettings.jsx | 2 +
.../create-workflow/DataTransformations.jsx | 2 +
.../src/components/menus/PipelineConfigsMenu.jsx | 10 +
.../src/components/pipelines/StageTaskName.jsx | 1 +
config-ui/src/data/Providers.js | 29 ++-
config-ui/src/data/availablePlugins.js | 2 +-
config-ui/src/data/integrations.jsx | 22 ++
.../zentao.js} | 16 +-
config-ui/src/hooks/useConnectionValidation.jsx | 1 +
config-ui/src/hooks/useIntegrations.jsx | 2 +
config-ui/src/images/integrations/zentao.svg | 80 ++++++
.../src/pages/blueprints/blueprint-settings.jsx | 1 +
.../configure/connections/ConfigureConnection.jsx | 3 +-
.../pages/configure/connections/ConnectionForm.jsx | 1 +
config-ui/src/pages/configure/settings/zentao.jsx | 57 +++++
config-ui/src/registry/plugins/zentao.json | 55 +++++
plugins/helper/iso8601time.go | 31 +++
plugins/zentao/api/blueprint.go | 70 ++++++
plugins/zentao/api/connection.go | 148 +++++++++++
plugins/zentao/api/init.go | 39 +++
plugins/zentao/e2e/bug_test.go | 69 ++++++
plugins/zentao/e2e/execution_test.go | 63 +++++
plugins/zentao/e2e/product_test.go | 63 +++++
.../zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv | 5 +
.../e2e/raw_tables/_raw_zentao_api_executions.csv | 2 +
.../e2e/raw_tables/_raw_zentao_api_products.csv | 2 +
.../e2e/raw_tables/_raw_zentao_api_stories.csv | 8 +
.../e2e/raw_tables/_raw_zentao_api_tasks.csv | 4 +
.../e2e/snapshot_tables/_tool_zentao_bugs.csv | 13 +
.../snapshot_tables/_tool_zentao_executions.csv | 2 +
.../e2e/snapshot_tables/_tool_zentao_products.csv | 2 +
.../e2e/snapshot_tables/_tool_zentao_stories.csv | 8 +
.../e2e/snapshot_tables/_tool_zentao_tasks.csv | 4 +
.../e2e/snapshot_tables/board_issues_bug.csv | 5 +
.../e2e/snapshot_tables/board_issues_story.csv | 8 +
.../e2e/snapshot_tables/board_issues_task.csv | 1 +
.../e2e/snapshot_tables/boards_execution.csv | 2 +
.../zentao/e2e/snapshot_tables/boards_product.csv | 2 +
plugins/zentao/e2e/snapshot_tables/issues_bug.csv | 5 +
.../zentao/e2e/snapshot_tables/issues_story.csv | 8 +
plugins/zentao/e2e/snapshot_tables/issues_task.csv | 1 +
plugins/zentao/e2e/story_test.go | 69 ++++++
plugins/zentao/e2e/task_test.go | 68 +++++
plugins/zentao/impl/impl.go | 136 ++++++++++
plugins/zentao/models/access_token.go | 27 ++
plugins/zentao/models/archived/bug.go | 99 ++++++++
plugins/zentao/models/archived/connection.go | 70 ++++++
plugins/zentao/models/archived/execution.go | 90 +++++++
plugins/zentao/models/archived/product.go | 62 +++++
plugins/zentao/models/archived/project.go | 113 +++++++++
plugins/zentao/models/archived/story.go | 89 +++++++
plugins/zentao/models/archived/task.go | 97 ++++++++
plugins/zentao/models/bug.go | 192 +++++++++++++++
plugins/zentao/models/connection.go | 51 ++++
plugins/zentao/models/execution.go | 274 +++++++++++++++++++++
.../migrationscripts/20220906_add_init_tables.go | 48 ++++
plugins/zentao/models/migrationscripts/register.go | 29 +++
plugins/zentao/models/product.go | 126 ++++++++++
plugins/zentao/models/project.go | 113 +++++++++
plugins/zentao/models/story.go | 169 +++++++++++++
plugins/zentao/models/task.go | 201 +++++++++++++++
plugins/zentao/tasks/api_client.go | 95 +++++++
plugins/zentao/tasks/bug_collector.go | 82 ++++++
plugins/zentao/tasks/bug_convertor.go | 116 +++++++++
plugins/zentao/tasks/bug_extractor.go | 138 +++++++++++
plugins/zentao/tasks/execution_collector.go | 81 ++++++
plugins/zentao/tasks/execution_convertor.go | 93 +++++++
plugins/zentao/tasks/execution_extractor.go | 128 ++++++++++
plugins/zentao/tasks/product_collector.go | 80 ++++++
plugins/zentao/tasks/product_convertor.go | 92 +++++++
plugins/zentao/tasks/product_extractor.go | 101 ++++++++
plugins/zentao/tasks/project_collector.go | 79 ++++++
plugins/zentao/tasks/project_extractor.go | 69 ++++++
plugins/zentao/tasks/shared.go | 35 +++
plugins/zentao/tasks/story_collector.go | 83 +++++++
plugins/zentao/tasks/story_convertor.go | 115 +++++++++
plugins/zentao/tasks/story_extractor.go | 125 ++++++++++
plugins/zentao/tasks/task_collector.go | 82 ++++++
plugins/zentao/tasks/task_convertor.go | 117 +++++++++
plugins/zentao/tasks/task_data.go | 62 +++++
plugins/zentao/tasks/task_extractor.go | 139 +++++++++++
plugins/zentao/zentao.go | 47 ++++
82 files changed, 4926 insertions(+), 5 deletions(-)
diff --git a/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx b/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx
index 22237cf1e..cefd7855c 100644
--- a/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx
+++ b/config-ui/src/components/blueprints/ProviderTransformationSettings.jsx
@@ -25,6 +25,7 @@ import GithubSettings from '@/pages/configure/settings/github'
import AzureSettings from '@/pages/configure/settings/azure'
import BitbucketSettings from '@/pages/configure/settings/bitbucket'
import GiteeSettings from '@/pages/configure/settings/gitee'
+import ZentaoSettings from '@/pages/configure/settings/zentao'
// Transformation Higher-Order Component (HOC) Settings Loader
const withTransformationSettings = (
@@ -70,6 +71,7 @@ const ProviderTransformationSettings = (props) => {
[Providers.JIRA]: JiraSettings,
[Providers.JENKINS]: JenkinsSettings,
[Providers.TAPD]: TapdSettings,
+ [Providers.ZENTAO]: ZentaoSettings,
[Providers.AZURE]: AzureSettings,
[Providers.BITBUCKET]: BitbucketSettings,
[Providers.GITEE]: GiteeSettings
diff --git a/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx b/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
index be956819f..f00eae0d6 100644
--- a/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
+++ b/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
@@ -77,6 +77,7 @@ const DataTransformations = (props) => {
const noTransformationsAvailable = useMemo(
() =>
[Providers.TAPD].includes(configuredConnection?.provider) ||
+ [Providers.ZENTAO].includes(configuredConnection?.provider) ||
([Providers.GITLAB].includes(configuredConnection?.provider) &&
dataDomainsGroup[configuredConnection?.id].every(
(e) => e.value !== DataDomainTypes.DEVOPS
@@ -86,6 +87,7 @@ const DataTransformations = (props) => {
configuredConnection?.id,
dataDomainsGroup,
Providers.TAPD,
+ Providers.ZENTAO,
Providers.GITLAB
]
)
diff --git a/config-ui/src/components/menus/PipelineConfigsMenu.jsx b/config-ui/src/components/menus/PipelineConfigsMenu.jsx
index d45a4312e..930912b54 100644
--- a/config-ui/src/components/menus/PipelineConfigsMenu.jsx
+++ b/config-ui/src/components/menus/PipelineConfigsMenu.jsx
@@ -23,6 +23,7 @@ import { gitextractorConfig as sampleGitextractorPipelineConfig } from '@/data/p
import { githubConfig as sampleGithubPipelineConfig } from '@/data/pipeline-config-samples/github'
import { gitlabConfig as sampleGitlabPipelineConfig } from '@/data/pipeline-config-samples/gitlab'
import { jiraConfig as sampleJiraPipelineConfig } from '@/data/pipeline-config-samples/jira'
+import { zentaoConfig as sampleZentaoPipelineConfig } from '@/data/pipeline-config-samples/zentao'
import { jenkinsConfig as sampleJenkinsPipelineConfig } from '@/data/pipeline-config-samples/jenkins'
import { feishuConfig as sampleFeishuPipelineConfig } from '@/data/pipeline-config-samples/feishu'
import { dbtConfig as sampleDbtPipelineConfig } from '@/data/pipeline-config-samples/dbt'
@@ -137,6 +138,15 @@ const PipelineConfigsMenu = (props) => {
)
}
/>
+ <Menu.Item
+ icon='code'
+ text='Load Zentao Configuration'
+ onClick={() =>
+ setRawConfiguration(
+ JSON.stringify(sampleZentaoPipelineConfig, null, ' ')
+ )
+ }
+ />
</Menu>
)
}
diff --git a/config-ui/src/components/pipelines/StageTaskName.jsx b/config-ui/src/components/pipelines/StageTaskName.jsx
index b4b044ce2..eeb7c4993 100644
--- a/config-ui/src/components/pipelines/StageTaskName.jsx
+++ b/config-ui/src/components/pipelines/StageTaskName.jsx
@@ -120,6 +120,7 @@ const StageTaskName = (props) => {
<>{ProviderLabels.JENKINS}</>
)}
{task.plugin === Providers.TAPD && <>{ProviderLabels.TAPD}</>}
+ {task.plugin === Providers.ZENTAO && <>{ProviderLabels.ZENTAO}</>}
{task.plugin === Providers.JIRA && (
<>Board ID {task.options.boardId}</>
)}
diff --git a/config-ui/src/data/Providers.js b/config-ui/src/data/Providers.js
index 1a4d4f979..eb4c63134 100644
--- a/config-ui/src/data/Providers.js
+++ b/config-ui/src/data/Providers.js
@@ -22,6 +22,7 @@ import { ReactComponent as JenkinsProviderIcon } from '@/images/integrations/jen
import { ReactComponent as JiraProviderIcon } from '@/images/integrations/jira.svg'
import { ReactComponent as GitHubProviderIcon } from '@/images/integrations/github.svg'
import { ReactComponent as TapdProviderIcon } from '@/images/integrations/tapd.svg'
+import { ReactComponent as ZentaoProviderIcon } from '@/images/integrations/zentao.svg'
import { ReactComponent as AzureProviderIcon } from '@/images/integrations/azure.svg'
import { ReactComponent as BitbucketProviderIcon } from '@/images/integrations/bitbucket.svg'
import { ReactComponent as GiteeProviderIcon } from '@/images/integrations/gitee.svg'
@@ -50,6 +51,7 @@ const Providers = {
DBT: 'dbt',
STARROCKS: 'starrocks',
TAPD: 'tapd',
+ ZENTAO: 'zentao',
AZURE: 'azure',
BITBUCKET: 'bitbucket',
GITEE: 'gitee',
@@ -76,7 +78,8 @@ const ProviderLabels = {
AE: 'Analysis Engine (AE)',
DBT: 'Data Build Tool (DBT)',
STARROCKS: 'StarRocks',
- TAPD: 'TAPD',
+ TAPD: 'Tapd',
+ ZENTAO: 'Zentao',
AZURE: 'Azure CI',
BITBUCKET: 'BitBucket',
GITEE: 'Gitee',
@@ -146,6 +149,19 @@ const ProviderFormLabels = {
</>
)
},
+ zentao: {
+ name: 'Connection Name',
+ endpoint: 'Endpoint URL',
+ proxy: 'Proxy URL',
+ username: 'Username',
+ password: 'Password',
+ rateLimitPerHour: (
+ <>
+ Rate Limit <sup>(per hour)</sup>
+ <RateLimitTooltip />
+ </>
+ )
+ },
jira: {
name: 'Connection Name',
endpoint: 'Endpoint URL',
@@ -308,6 +324,14 @@ const ProviderFormPlaceholders = {
password: 'eg. ************',
rateLimitPerHour: '1000'
},
+ zentao: {
+ name: 'eg. Zentao',
+ endpoint: 'URL eg. http://subdomain.domain:port/api.php/v1/',
+ proxy: 'eg. http://proxy.localhost:8080',
+ username: 'eg. devlake',
+ password: 'eg. ************',
+ rateLimitPerHour: '1000'
+ },
jira: {
name: 'eg. JIRA',
endpoint: 'eg. https://your-domain.atlassian.net/rest/',
@@ -366,6 +390,9 @@ const ProviderIcons = {
[Providers.TAPD]: (w, h) => (
<TapdProviderIcon width={w || 24} height={h || 24} />
),
+ [Providers.ZENTAO]: (w, h) => (
+ <ZentaoProviderIcon width={w || 24} height={h || 24} />
+ ),
[Providers.JIRA]: (w, h) => (
<JiraProviderIcon width={w || 24} height={h || 24} />
),
diff --git a/config-ui/src/data/availablePlugins.js b/config-ui/src/data/availablePlugins.js
index 4433f79cb..dc5f79d2d 100644
--- a/config-ui/src/data/availablePlugins.js
+++ b/config-ui/src/data/availablePlugins.js
@@ -15,6 +15,6 @@
* limitations under the License.
*
*/
-const AVAILABLE_PLUGINS = ['gitlab', 'jira', 'jenkins', 'github', 'tapd']
+const AVAILABLE_PLUGINS = ['gitlab', 'jira', 'jenkins', 'github', 'tapd', 'zentao']
module.exports = AVAILABLE_PLUGINS
diff --git a/config-ui/src/data/integrations.jsx b/config-ui/src/data/integrations.jsx
index 1a10f7aa0..b5f0165c3 100644
--- a/config-ui/src/data/integrations.jsx
+++ b/config-ui/src/data/integrations.jsx
@@ -30,6 +30,7 @@ import { ReactComponent as JenkinsProvider } from '@/images/integrations/jenkins
import { ReactComponent as JiraProvider } from '@/images/integrations/jira.svg'
import { ReactComponent as GitHubProvider } from '@/images/integrations/github.svg'
import { ReactComponent as TapdProvider } from '@/images/integrations/tapd.svg'
+import { ReactComponent as ZentaoProvider } from '@/images/integrations/zentao.svg'
import { ReactComponent as AzureProvider } from '@/images/integrations/azure.svg'
import { ReactComponent as BitbucketProvider } from '@/images/integrations/bitbucket.svg'
import { ReactComponent as GiteeProvider } from '@/images/integrations/gitee.svg'
@@ -102,6 +103,27 @@ const integrationsData = [
// relocated to ProviderTransformationSettings since v0.12.0
settings: {}
},
+ {
+ id: Providers.ZENTAO,
+ type: ProviderTypes.INTEGRATION,
+ enabled: true,
+ multiConnection: true,
+ isBeta: true,
+ name: ProviderLabels.ZENTAO,
+ icon: (
+ <ZentaoProvider
+ className='providerIconSvg'
+ width='30'
+ height='30'
+ style={{ float: 'left', marginTop: '5px' }}
+ />
+ ),
+ iconDashboard: (
+ <ZentaoProvider className='providerIconSvg' width='40' height='40' />
+ ),
+ // relocated to ProviderTransformationSettings since v0.12.0
+ settings: {}
+ },
{
id: Providers.JIRA,
type: ProviderTypes.INTEGRATION,
diff --git a/config-ui/src/data/availablePlugins.js b/config-ui/src/data/pipeline-config-samples/zentao.js
similarity index 79%
copy from config-ui/src/data/availablePlugins.js
copy to config-ui/src/data/pipeline-config-samples/zentao.js
index 4433f79cb..22590ee72 100644
--- a/config-ui/src/data/availablePlugins.js
+++ b/config-ui/src/data/pipeline-config-samples/zentao.js
@@ -15,6 +15,18 @@
* limitations under the License.
*
*/
-const AVAILABLE_PLUGINS = ['gitlab', 'jira', 'jenkins', 'github', 'tapd']
+const zentaoConfig = [
+ [
+ {
+ plugin: 'zentao',
+ options: {
+ connectionId: 1,
+ productId: 1,
+ projectId: 1,
+ executionId: 1,
+ }
+ }
+ ]
+]
-module.exports = AVAILABLE_PLUGINS
+export { zentaoConfig }
diff --git a/config-ui/src/hooks/useConnectionValidation.jsx b/config-ui/src/hooks/useConnectionValidation.jsx
index fbf109a67..5596c6634 100644
--- a/config-ui/src/hooks/useConnectionValidation.jsx
+++ b/config-ui/src/hooks/useConnectionValidation.jsx
@@ -96,6 +96,7 @@ function useConnectionValidation({
}
break
case Providers.JIRA:
+ case Providers.ZENTAO:
case Providers.JENKINS:
if (!username || username.length <= 2) {
errs.push('Username is required')
diff --git a/config-ui/src/hooks/useIntegrations.jsx b/config-ui/src/hooks/useIntegrations.jsx
index a3c5f8840..19f95621a 100644
--- a/config-ui/src/hooks/useIntegrations.jsx
+++ b/config-ui/src/hooks/useIntegrations.jsx
@@ -26,6 +26,7 @@ import GitHubPlugin from '@/registry/plugins/github.json'
import GitLabPlugin from '@/registry/plugins/gitlab.json'
import JenkinsPlugin from '@/registry/plugins/jenkins.json'
import TapdPlugin from '@/registry/plugins/tapd.json'
+import ZentaoPlugin from '@/registry/plugins/zentao.json'
import AzurePlugin from '@/registry/plugins/azure.json'
import BitbucketPlugin from '@/registry/plugins/bitbucket.json'
import GiteePlugin from '@/registry/plugins/gitee.json'
@@ -49,6 +50,7 @@ function useIntegrations(
GitLabPlugin,
JenkinsPlugin,
TapdPlugin,
+ ZentaoPlugin,
AzurePlugin,
BitbucketPlugin,
GiteePlugin,
diff --git a/config-ui/src/images/integrations/zentao.svg b/config-ui/src/images/integrations/zentao.svg
new file mode 100644
index 000000000..9dab076a3
--- /dev/null
+++ b/config-ui/src/images/integrations/zentao.svg
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ version="1.1"
+ id="图层_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 189.83999 189.84"
+ xml:space="preserve"
+ sodipodi:docname="zentao.svg"
+ width="189.84"
+ height="189.84"
+ inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><defs
+ id="defs156" /><sodipodi:namedview
+ id="namedview154"
+ pagecolor="#ffffff"
+ bordercolor="#111111"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="1"
+ inkscape:deskcolor="#d1d1d1"
+ showgrid="false"
+ inkscape:zoom="3.1447385"
+ inkscape:cx="289.69023"
+ inkscape:cy="95.079449"
+ inkscape:window-width="3840"
+ inkscape:window-height="2050"
+ inkscape:window-x="-12"
+ inkscape:window-y="-12"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g151" />
+<style
+ type="text/css"
+ id="style80">
+ .st0{fill:#888988;}
+ .st1{fill:#221714;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#221714;}
+ .st3{fill:url(#SVGID_1_);}
+ .st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g
+ id="g151"
+ transform="translate(-7.92,-5.74)">
+
+ <g
+ id="g149">
+ <linearGradient
+ id="SVGID_1_"
+ gradientUnits="userSpaceOnUse"
+ x1="102.8407"
+ y1="195.5705"
+ x2="102.8407"
+ y2="5.7350001">
+ <stop
+ offset="0"
+ style="stop-color:#2975FE"
+ id="stop140" />
+ <stop
+ offset="1"
+ style="stop-color:#0CD5FF"
+ id="stop142" />
+ </linearGradient>
+ <path
+ class="st3"
+ d="m 102.84,5.74 c -52.42,0 -94.92,42.5 -94.92,94.92 0,52.42 42.5,94.92 94.92,94.92 52.43,0 94.92,-42.49 94.92,-94.92 0,-52.43 -42.49,-94.92 -94.92,-94.92 z"
+ id="path145"
+ style="fill:url(#SVGID_1_)" />
+ <path
+ class="st4"
+ d="m 174.02,47.18 c 0,0 15.56,23.18 8.66,42.87 -4.63,13.22 -21.42,17.86 -34.85,13.59 C 126.97,97 101.09,74.82 72.82,95.21 c -23.9,17.25 -13.94,47.33 3.13,57.35 12.79,7.51 30.62,9.86 41.04,0.99 8.35,-7.12 12.9,-25.67 -1.17,-32.03 -4.8,-2.17 -14.24,-2.65 -19.44,3.41 -6.79,7.92 -4.21,18.99 5.19,22.5 0,0 -16.8,-0.84 -19.03,-18.22 -3.8,-29.57 34.05,-34.45 52,-23.36 32.28,19.94 20.83,65.12 -27.19,73.35 -24.33,4.16 -64.53,-4.21 -80.46,-36.76 -5.97,-12.2 -4.34,-10.11 -4.34,-10.11 0,0 2.12,2.3 [...]
+ id="path147" />
+ </g>
+</g>
+</svg>
diff --git a/config-ui/src/pages/blueprints/blueprint-settings.jsx b/config-ui/src/pages/blueprints/blueprint-settings.jsx
index 0f9e3e728..e3e322fb6 100644
--- a/config-ui/src/pages/blueprints/blueprint-settings.jsx
+++ b/config-ui/src/pages/blueprints/blueprint-settings.jsx
@@ -515,6 +515,7 @@ const BlueprintSettings = (props) => {
dataDomainsGroup[configuredConnection?.id]?.length > 0
break
case Providers.TAPD:
+ case Providers.ZENTAO:
isValid = dataDomainsGroup[configuredConnection?.id]?.length > 0
break
default:
diff --git a/config-ui/src/pages/configure/connections/ConfigureConnection.jsx b/config-ui/src/pages/configure/connections/ConfigureConnection.jsx
index 197e04a88..73ca3ee53 100644
--- a/config-ui/src/pages/configure/connections/ConfigureConnection.jsx
+++ b/config-ui/src/pages/configure/connections/ConfigureConnection.jsx
@@ -273,7 +273,8 @@ export default function ConfigureConnection() {
{[
Providers.GITLAB,
Providers.JIRA,
- Providers.TAPD
+ Providers.TAPD,
+ Providers.ZENTAO
].includes(activeProvider?.id) && (
<h2 style={{ margin: 0 }}>
#{activeConnection?.id} {activeConnection.name}
diff --git a/config-ui/src/pages/configure/connections/ConnectionForm.jsx b/config-ui/src/pages/configure/connections/ConnectionForm.jsx
index deb7eab04..fcd2dab72 100644
--- a/config-ui/src/pages/configure/connections/ConnectionForm.jsx
+++ b/config-ui/src/pages/configure/connections/ConnectionForm.jsx
@@ -803,6 +803,7 @@ export default function ConnectionForm(props) {
Providers.JIRA,
Providers.JENKINS,
Providers.TAPD,
+ Providers.ZENTAO,
Providers.AZURE,
Providers.BITBUCKET,
Providers.GITEE
diff --git a/config-ui/src/pages/configure/settings/zentao.jsx b/config-ui/src/pages/configure/settings/zentao.jsx
new file mode 100644
index 000000000..96de85a1f
--- /dev/null
+++ b/config-ui/src/pages/configure/settings/zentao.jsx
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ *
+ */
+import React, { useEffect, useState } from 'react'
+import { useParams, useHistory } from 'react-router-dom'
+import { DataDomainTypes } from '@/data/DataDomains'
+
+import '@/styles/integration.scss'
+import '@/styles/connections.scss'
+
+export default function ZentaoSettings(props) {
+ const {
+ provider,
+ connection,
+ dataDomains = [],
+ onSettingsChange = () => {}
+ } = props
+ const history = useHistory()
+
+ const cancel = () => {
+ history.push(`/integrations/${provider.id}`)
+ }
+
+ return (
+ <>
+ <div className='headlineContainer'>
+ <h3 className='headline'>No Additional Settings</h3>
+ <p className='description'>
+ This integration doesn’t require any configuration. You can continue
+ to
+ <a href='#' style={{ textDecoration: 'underline' }} onClick={cancel}>
+ add other data connections
+ </a>
+ or trigger collection at the{' '}
+ <a href='#' style={{ textDecoration: 'underline' }} onClick={cancel}>
+ previous page
+ </a>
+ .
+ </p>
+ </div>
+ </>
+ )
+}
diff --git a/config-ui/src/registry/plugins/zentao.json b/config-ui/src/registry/plugins/zentao.json
new file mode 100644
index 000000000..b89a893fb
--- /dev/null
+++ b/config-ui/src/registry/plugins/zentao.json
@@ -0,0 +1,55 @@
+{
+ "id": "zentao",
+ "name": "ZENTAO",
+ "type": "integration",
+ "enabled": true,
+ "multiConnection": true,
+ "connectionLimit": 0,
+ "isBeta": true,
+ "isProvider": true,
+ "icon": "src/images/integrations/zentao.svg",
+ "private": false,
+ "connection": {
+ "authentication": "plain",
+ "fields": {
+ "name": { "enable": true, "required": true, "readonly": false },
+ "endpoint": { },
+ "proxy": { },
+ "username": { },
+ "password": { },
+ "rateLimitPerHour": { }
+ },
+ "labels": {
+ "name": "Connection Name",
+ "endpoint": "Endpoint URL",
+ "proxy": "Proxy URL",
+ "username": "Username",
+ "password": "Password",
+ "rateLimitPerHour": "Rate Limit (per hour)"
+ },
+ "placeholders": {
+ "name": "eg. Zentao",
+ "endpoint": "URL eg. http://subdomain.domain:port/api.php/v1/",
+ "proxy": "eg. http://proxy.localhost:8080",
+ "username": "eg. admin",
+ "password": "eg. ************",
+ "rateLimit": "1000"
+ },
+ "tooltips": {
+ }
+ },
+ "availableDataDomains": ["TICKET"],
+ "transformations": {
+ "scopes": {
+ "options": {
+ }
+ },
+ "default": {
+ "issueTypeRequirement": "",
+ "issueTypeBug": "",
+ "issueTypeIncident": "",
+ "productionPattern": "",
+ "deploymentPattern": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/helper/iso8601time.go b/plugins/helper/iso8601time.go
index 8d4092626..3029adb19 100644
--- a/plugins/helper/iso8601time.go
+++ b/plugins/helper/iso8601time.go
@@ -18,6 +18,7 @@ limitations under the License.
package helper
import (
+ "database/sql/driver"
"fmt"
"regexp"
"strings"
@@ -63,6 +64,10 @@ func init() {
Matcher: regexp.MustCompile(`[+-][\d]{2}:[\d]{2}$`),
Format: "2006-01-02T15:04:05-07:00",
},
+ {
+ Matcher: regexp.MustCompile(`[+-][\d]{2}-[\d]{2}$`),
+ Format: "2006-01-02",
+ },
}
}
@@ -131,3 +136,29 @@ func Iso8601TimeToTime(iso8601Time *Iso8601Time) *time.Time {
t := iso8601Time.ToTime()
return &t
}
+
+// Value FIXME ...
+func (jt *Iso8601Time) Value() (driver.Value, error) {
+ if jt == nil {
+ return nil, nil
+ }
+ var zeroTime time.Time
+ t := jt.time
+ if t.UnixNano() == zeroTime.UnixNano() {
+ return nil, nil
+ }
+ return t, nil
+}
+
+// Scan FIXME ...
+func (jt *Iso8601Time) Scan(v interface{}) error {
+ value, ok := v.(time.Time)
+ if ok {
+ *jt = Iso8601Time{
+ time: value,
+ format: time.RFC3339,
+ }
+ return nil
+ }
+ return fmt.Errorf("can not convert %v to timestamp", v)
+}
diff --git a/plugins/zentao/api/blueprint.go b/plugins/zentao/api/blueprint.go
new file mode 100644
index 000000000..942de5b69
--- /dev/null
+++ b/plugins/zentao/api/blueprint.go
@@ -0,0 +1,70 @@
+/*
+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 api
+
+import (
+ "encoding/json"
+ "github.com/apache/incubator-devlake/errors"
+
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/tasks"
+)
+
+func MakePipelinePlan(subtaskMetas []core.SubTaskMeta, connectionId uint64, scope []*core.BlueprintScopeV100) (core.PipelinePlan, errors.Error) {
+ var err error
+ plan := make(core.PipelinePlan, len(scope))
+ for i, scopeElem := range scope {
+ taskOptions := make(map[string]interface{})
+ err = json.Unmarshal(scopeElem.Options, &taskOptions)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ taskOptions["connectionId"] = connectionId
+
+ //TODO Add transformation rules to task options
+
+ /*
+ var transformationRules tasks.TransformationRules
+ if len(scopeElem.Transformation) > 0 {
+ err = json.Unmarshal(scopeElem.Transformation, &transformationRules)
+ if err != nil {
+ return nil, err
+ }
+ }
+ */
+ //taskOptions["transformationRules"] = transformationRules
+ _, err := tasks.DecodeAndValidateTaskOptions(taskOptions)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ // subtasks
+ subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, scopeElem.Entities)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ plan[i] = core.PipelineStage{
+ {
+ Plugin: "zentao",
+ Subtasks: subtasks,
+ Options: taskOptions,
+ },
+ }
+ }
+ return plan, nil
+}
diff --git a/plugins/zentao/api/connection.go b/plugins/zentao/api/connection.go
new file mode 100644
index 000000000..93244c5e1
--- /dev/null
+++ b/plugins/zentao/api/connection.go
@@ -0,0 +1,148 @@
+/*
+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 api
+
+import (
+ "context"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "github.com/mitchellh/mapstructure"
+ "net/http"
+)
+
+// TODO Please modify the following code to fit your needs
+func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+ // process input
+ var params models.TestConnectionRequest
+ err := mapstructure.Decode(input.Body, ¶ms)
+ if err != nil {
+ return nil, errors.BadInput.Wrap(err, "could not decode request parameters")
+ }
+ err = vld.Struct(params)
+ if err != nil {
+ return nil, errors.BadInput.Wrap(err, "could not validate request parameters")
+ }
+
+ authApiClient, err := helper.NewApiClient(context.TODO(), params.Endpoint, nil, 0, params.Proxy, basicRes)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+
+ // request for access token
+ tokenReqBody := &models.ApiAccessTokenRequest{
+ Account: params.Username,
+ Password: params.Password,
+ }
+ tokenRes, err := authApiClient.Post("/tokens", nil, tokenReqBody, nil)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ tokenResBody := &models.ApiAccessTokenResponse{}
+ err = helper.UnmarshalResponse(tokenRes, tokenResBody)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ if tokenResBody.Token == "" {
+ return nil, errors.Default.New("failed to request access token")
+ }
+
+ // output
+ return nil, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+POST /plugins/Zentao/connections
+{
+ "name": "Zentao data connection name",
+ "endpoint": "Zentao api endpoint, i.e. https://example.com",
+ "username": "username, usually should be email address",
+ "password": "Zentao api access token"
+}
+*/
+func PostConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+ // update from request and save to database
+ connection := &models.ZentaoConnection{}
+ err := connectionHelper.Create(connection, input)
+ if err != nil {
+ return nil, err
+ }
+ return &core.ApiResourceOutput{Body: connection, Status: http.StatusOK}, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+PATCH /plugins/Zentao/connections/:connectionId
+{
+ "name": "Zentao data connection name",
+ "endpoint": "Zentao api endpoint, i.e. https://example.com",
+ "username": "username, usually should be email address",
+ "password": "Zentao api access token"
+}
+*/
+func PatchConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+ connection := &models.ZentaoConnection{}
+ err := connectionHelper.Patch(connection, input)
+ if err != nil {
+ return nil, err
+ }
+ return &core.ApiResourceOutput{Body: connection}, nil
+}
+
+/*
+DELETE /plugins/Zentao/connections/:connectionId
+*/
+func DeleteConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+ connection := &models.ZentaoConnection{}
+ err := connectionHelper.First(connection, input.Params)
+ if err != nil {
+ return nil, err
+ }
+ err = connectionHelper.Delete(connection)
+ return &core.ApiResourceOutput{Body: connection}, err
+}
+
+/*
+GET /plugins/Zentao/connections
+*/
+func ListConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+ var connections []models.ZentaoConnection
+ err := connectionHelper.List(&connections)
+ if err != nil {
+ return nil, err
+ }
+ return &core.ApiResourceOutput{Body: connections, Status: http.StatusOK}, nil
+}
+
+//TODO Please modify the folowing code to adapt to your plugin
+/*
+GET /plugins/Zentao/connections/:connectionId
+{
+ "name": "Zentao data connection name",
+ "endpoint": "Zentao api endpoint, i.e. https://merico.atlassian.net/rest",
+ "username": "username, usually should be email address",
+ "password": "Zentao api access token"
+}
+*/
+func GetConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, errors.Error) {
+ connection := &models.ZentaoConnection{}
+ err := connectionHelper.First(connection, input.Params)
+ return &core.ApiResourceOutput{Body: connection}, err
+}
diff --git a/plugins/zentao/api/init.go b/plugins/zentao/api/init.go
new file mode 100644
index 000000000..6774e1482
--- /dev/null
+++ b/plugins/zentao/api/init.go
@@ -0,0 +1,39 @@
+/*
+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 api
+
+import (
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/go-playground/validator/v10"
+ "github.com/spf13/viper"
+ "gorm.io/gorm"
+)
+
+var vld *validator.Validate
+var connectionHelper *helper.ConnectionApiHelper
+var basicRes core.BasicRes
+
+func Init(config *viper.Viper, logger core.Logger, database *gorm.DB) {
+ basicRes = helper.NewDefaultBasicRes(config, logger, database)
+ vld = validator.New()
+ connectionHelper = helper.NewConnectionHelper(
+ basicRes,
+ vld,
+ )
+}
diff --git a/plugins/zentao/e2e/bug_test.go b/plugins/zentao/e2e/bug_test.go
new file mode 100644
index 000000000..c7f9baadd
--- /dev/null
+++ b/plugins/zentao/e2e/bug_test.go
@@ -0,0 +1,69 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "testing"
+
+ "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 TestZentaoBugDataFlow(t *testing.T) {
+
+ var zentao impl.Zentao
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+ taskData := &tasks.ZentaoTaskData{
+ Options: &tasks.ZentaoOptions{
+ ConnectionId: 1,
+ ProjectId: 1,
+ ProductId: 3,
+ ExecutionId: 1,
+ },
+ }
+
+ // import raw data table
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_bugs.csv",
+ "_raw_zentao_api_bugs")
+
+ // verify extraction
+ dataflowTester.FlushTabler(&models.ZentaoBug{})
+ dataflowTester.Subtask(tasks.ExtractBugMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&models.ZentaoBug{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/_tool_zentao_bugs.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+
+ // verify conversion
+ dataflowTester.FlushTabler(&ticket.Issue{})
+ dataflowTester.FlushTabler(&ticket.BoardIssue{})
+ dataflowTester.Subtask(tasks.ConvertBugMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&ticket.Issue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/issues_bug.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+ dataflowTester.VerifyTableWithOptions(&ticket.BoardIssue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/board_issues_bug.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+}
diff --git a/plugins/zentao/e2e/execution_test.go b/plugins/zentao/e2e/execution_test.go
new file mode 100644
index 000000000..3b0371be5
--- /dev/null
+++ b/plugins/zentao/e2e/execution_test.go
@@ -0,0 +1,63 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "testing"
+
+ "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 TestZentaoExecutionDataFlow(t *testing.T) {
+
+ var zentao impl.Zentao
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+ taskData := &tasks.ZentaoTaskData{
+ Options: &tasks.ZentaoOptions{
+ ConnectionId: 1,
+ ProjectId: 1,
+ ProductId: 3,
+ ExecutionId: 1,
+ },
+ }
+
+ // import raw data table
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_executions.csv",
+ "_raw_zentao_api_executions")
+
+ // verify extraction
+ dataflowTester.FlushTabler(&models.ZentaoExecution{})
+ dataflowTester.Subtask(tasks.ExtractExecutionMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&models.ZentaoExecution{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/_tool_zentao_executions.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+
+ dataflowTester.FlushTabler(&ticket.Board{})
+ dataflowTester.Subtask(tasks.ConvertExecutionMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&ticket.Board{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/boards_execution.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+}
diff --git a/plugins/zentao/e2e/product_test.go b/plugins/zentao/e2e/product_test.go
new file mode 100644
index 000000000..af7ad2887
--- /dev/null
+++ b/plugins/zentao/e2e/product_test.go
@@ -0,0 +1,63 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "testing"
+
+ "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 TestZentaoProductDataFlow(t *testing.T) {
+
+ var zentao impl.Zentao
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+ taskData := &tasks.ZentaoTaskData{
+ Options: &tasks.ZentaoOptions{
+ ConnectionId: 1,
+ ProjectId: 1,
+ ProductId: 3,
+ ExecutionId: 9,
+ },
+ }
+
+ // import raw data table
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_products.csv",
+ "_raw_zentao_api_products")
+
+ // verify extraction
+ dataflowTester.FlushTabler(&models.ZentaoProduct{})
+ dataflowTester.Subtask(tasks.ExtractProductMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&models.ZentaoProduct{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/_tool_zentao_products.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+
+ dataflowTester.FlushTabler(&ticket.Board{})
+ dataflowTester.Subtask(tasks.ConvertProductMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&ticket.Board{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/boards_product.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+}
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv
new file mode 100644
index 000000000..8ff72a6bc
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_bugs.csv
@@ -0,0 +1,5 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":4,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":11,""execution"":1,""plan"":0,""story"":4,""storyVersion"":1,""task"":9,""toTask"":0,""toStory"":0,""title"":""\u552e\u540e\u670d\u52a1\u9875\u9762\u95ee\u9898"",""keywords"":"""",""severity"":3,""pri"":1,""type"":""codeerror"",""os"":"""",""browser"":"""",""hardware"":"""",""found"":"""",""steps"":""\u003Cp\u003E[\u6b65\ [...]
+2,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":3,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":10,""execution"":1,""plan"":0,""story"":3,""storyVersion"":2,""task"":6,""toTask"":0,""toStory"":0,""title"":""\u6210\u679c\u5c55\u793a\u9875\u9762\u95ee\u9898"",""keywords"":"""",""severity"":3,""pri"":1,""type"":""codeerror"",""os"":"""",""browser"":"""",""hardware"":"""",""found"":"""",""steps"":""\u003Cp\u003E[\u6b65\ [...]
+3,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":2,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":9,""execution"":1,""plan"":1,""story"":2,""storyVersion"":1,""task"":15,""toTask"":0,""toStory"":0,""title"":""\u65b0\u95fb\u4e2d\u5fc3\u9875\u9762\u95ee\u9898"",""keywords"":""hh"",""severity"":3,""pri"":2,""type"":""codeerror"",""os"":"",windows"",""browser"":"",chrome"",""hardware"":"""",""found"":"""",""steps"":""\u00 [...]
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""project"":7,""product"":3,""injection"":0,""identify"":0,""branch"":0,""module"":8,""execution"":1,""plan"":0,""story"":1,""storyVersion"":1,""task"":1,""toTask"":0,""toStory"":0,""title"":""\u9996\u9875\u9875\u9762\u95ee\u9898"",""keywords"":"""",""severity"":3,""pri"":1,""type"":""codeerror"",""os"":"""",""browser"":"""",""hardware"":"""",""found"":"""",""steps"":""\u003Cp\u003E[\u6b65\u9aa4]\u8fdb\ [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_executions.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_executions.csv
new file mode 100644
index 000000000..019b75cdb
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_executions.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""project"":7,""model"":"""",""type"":""sprint"",""lifetime"":""sprint"",""budget"":""0"",""budgetUnit"":""CNY"",""attribute"":"""",""percent"":0,""milestone"":""0"",""output"":"""",""auth"":"""",""parent"":7,""path"":"",7,1,"",""grade"":1,""name"":""\u4f01\u4e1a\u7f51\u7ad9\u7b2c\u4e00\u671f"",""code"":""coWeb1"",""begin"":""2020-06-05"",""end"":""2021-12-04"",""realBegan"":null,""realEnd"":null,""days [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_products.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_products.csv
new file mode 100644
index 000000000..e58feada0
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_products.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":9,""ProjectId"":1}","{""id"":3,""program"":10,""name"":""\u4ea7\u54c1\u540d\u79f01"",""code"":""\u4ea7\u54c1\u4ee3\u53f72"",""bind"":""0"",""line"":31,""type"":""normal"",""status"":""normal"",""subStatus"":"""",""desc"":""\u003Cspan style=\""background-color:#FFFFFF;\""\u003E\u4ea7\u54c1\u63cf\u8ff01\u003C\/span\u003E"",""PO"":{""id"":1,""account"":""devlake"",""avatar"":"""",""realname"":""devlake""},""QD"":{""id"":1,""account"":"" [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_stories.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_stories.csv
new file mode 100644
index 000000000..dd79bc2f1
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_stories.csv
@@ -0,0 +1,8 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":7,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":7,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u5173\u4e8e\u6211\u4eec\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""reviewing"",""subStatus"":"""",""color"":"""",""stage"":""planned"",""stagedB [...]
+2,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":6,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":6,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u5408\u4f5c\u6d3d\u8c08\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""reviewing"",""subStatus"":"""",""color"":"""",""stage"":""planned"",""stagedB [...]
+3,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":5,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":5,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u8bda\u8058\u82f1\u624d\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""reviewing"",""subStatus"":"""",""color"":"""",""stage"":""planned"",""stagedB [...]
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":4,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":4,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u552e\u540e\u670d\u52a1\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""developed"",""stagedBy [...]
+5,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":3,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":3,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u6210\u679c\u5c55\u793a\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":0,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""developing"",""stagedB [...]
+6,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":2,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":2,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u65b0\u95fb\u4e2d\u5fc3\u7684\u8bbe\u8ba1\u548c\u5f00\u53d1\u3002"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""projected"",""st [...]
+7,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""vision"":""rnd"",""parent"":0,""product"":1,""branch"":0,""module"":1,""plan"":""1"",""source"":""po"",""sourceNote"":"""",""fromBug"":0,""feedback"":0,""title"":""\u9996\u9875\u8bbe\u8ba1\u548c\u5f00\u53d1"",""keywords"":"""",""type"":""story"",""category"":""feature"",""pri"":1,""estimate"":1,""status"":""active"",""subStatus"":"""",""color"":"""",""stage"":""developing"",""stagedBy"":"""",""mailto" [...]
diff --git a/plugins/zentao/e2e/raw_tables/_raw_zentao_api_tasks.csv b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_tasks.csv
new file mode 100644
index 000000000..8aa6a4489
--- /dev/null
+++ b/plugins/zentao/e2e/raw_tables/_raw_zentao_api_tasks.csv
@@ -0,0 +1,4 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":1,""project"":13,""parent"":0,""execution"":9,""module"":0,""design"":0,""story"":0,""storyVersion"":1,""designVersion"":0,""fromBug"":0,""feedback"":0,""fromIssue"":0,""name"":""\u4efb\u52a1\u540d\u79f0"",""type"":""devel"",""mode"":"""",""pri"":3,""estimate"":0,""consumed"":0,""left"":0,""deadline"":""2022-10-01"",""status"":""wait"",""subStatus"":"""",""color"":"""",""mailto"":[{""id"":2,""account"":"" [...]
+2,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":2,""project"":13,""parent"":0,""execution"":9,""module"":0,""design"":0,""story"":0,""storyVersion"":1,""designVersion"":0,""fromBug"":0,""feedback"":0,""fromIssue"":0,""name"":""\u4efb\u52a1\u540d\u79f0"",""type"":""devel"",""mode"":"""",""pri"":3,""estimate"":0,""consumed"":0,""left"":0,""deadline"":""2022-10-01"",""status"":""wait"",""subStatus"":"""",""color"":"""",""mailto"":[{""id"":2,""account"":"" [...]
+4,"{""ConnectionId"":1,""ProductId"":3,""ExecutionId"":1,""ProjectId"":1}","{""id"":3,""project"":13,""parent"":0,""execution"":9,""module"":0,""design"":0,""story"":0,""storyVersion"":1,""designVersion"":0,""fromBug"":0,""feedback"":0,""fromIssue"":0,""name"":""\u4efb\u52a1\u540d\u79f0"",""type"":""devel"",""mode"":"""",""pri"":3,""estimate"":0,""consumed"":0,""left"":0,""deadline"":""2022-10-01"",""status"":""wait"",""subStatus"":"""",""color"":"""",""mailto"":[{""id"":2,""account"":"" [...]
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_bugs.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_bugs.csv
new file mode 100644
index 000000000..da82aa928
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_bugs.csv
@@ -0,0 +1,13 @@
+connection_id,id,project,product,injection,identify,branch,module,execution,plan,story,story_version,task,to_task,to_story,title,keywords,severity,pri,type,os,browser,hardware,found,steps,status,sub_status,color,confirmed,activated_count,activated_date,feedback_by,notify_email,opened_by_id,opened_by_name,opened_date,opened_build,assigned_to_id,assigned_to_name,assigned_date,deadline,resolved_by_id,resolution,resolved_build,resolved_date,closed_by_id,closed_date,duplicate_bug,link_bug,fee [...]
+1,1,7,3,0,0,0,8,1,0,1,1,1,0,0,首页页面问题,,3,1,codeerror,,,,,"<p>[步骤]进入首页</p>
+<p>[结果]出现乱码 </p>
+<p>[期望]正常显示</p>",active,,,0,0,,,,7,测试甲,2012-06-05T02:56:11.000+00:00,主干,4,开发甲,2012-06-05T02:56:11.000+00:00,,0,,,,0,,0,,0,0,0,0,,,,,,,0,0,2021-04-28T03:09:08.000+00:00,0,1,3,0,激活,normal
+1,2,7,3,0,0,0,9,1,1,2,1,15,0,0,新闻中心页面问题,hh,3,2,codeerror,",windows",",chrome",,,"<p>[步骤]进入新闻中心</p>
+<p>[结果]页面出现乱码</p>
+<p>[期望]正常显示rew</p>",delay,,,1,1,2022-10-05T04:16:44.000+00:00,,1114255335@qq.com,7,测试甲,2012-06-05T02:57:11.000+00:00,主干,0,,2022-10-05T04:19:22.000+00:00,2022-10-06,0,,,,0,,0,,0,0,0,0,,,,,,,1,1,2022-10-05T04:19:22.000+00:00,0,2,3,0,过期Bug,normal
+1,3,7,3,0,0,0,10,1,0,3,2,6,0,0,成果展示页面问题,,3,1,codeerror,,,,,"<p>[步骤]进入成果展示 </p>
+<p>[结果]乱码</p>
+<p>[期望]正常显示</p>",active,,,0,0,,,,8,测试乙,2012-06-05T02:58:22.000+00:00,主干,4,开发甲,2012-06-05T02:58:22.000+00:00,,0,,,,0,,0,,0,0,0,0,,,,,,,0,0,2021-04-28T03:09:08.000+00:00,0,1,3,0,激活,normal
+1,4,7,3,0,0,0,11,1,0,4,1,9,0,0,售后服务页面问题,,3,1,codeerror,,,,,"<p>[步骤]进入售后服务</p>
+<p>[结果]乱码</p>
+<p>[期望]正常显示</p>",resolved,,,1,0,,,,9,测试丙,2012-06-05T03:00:19.000+00:00,主干,9,测试丙,2022-10-05T04:10:08.000+00:00,,1,fixed,主干,2022-10-05T04:09:59.000+00:00,0,,0,,0,0,0,0,,,,,,,0,1,2022-10-05T04:10:08.000+00:00,0,1,3,0,已解决,normal
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_executions.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_executions.csv
new file mode 100644
index 000000000..05762df6f
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_executions.csv
@@ -0,0 +1,2 @@
+connection_id,id,project,model,type,lifetime,budget,budget_unit,attribute,percent,milestone,output,auth,parent,path,grade,name,code,plan_begin,plan_end,real_began,real_end,days,status,sub_status,pri,description,version,parent_version,plan_duration,real_duration,opened_by_id,opened_date,opened_version,last_edited_by_id,last_edited_date,closed_by_id,closed_date,canceled_by_id,canceled_date,suspended_date,po_id,pm_id,qd_id,rd_id,team,acl,order_in,vision,display_cards,fluid_board,deleted,tot [...]
+1,1,7,,sprint,sprint,0,CNY,,0,0,,,7,",7,1,",1,企业网站第一期,coWeb1,2020-06-05,2021-12-04,,,391,doing,,1,开发企业网站的基本雏形。<br />,0,0,0,0,0,,,0,,0,,0,,0000-00-00,2,3,10,2,公司开发团队,open,5,rnd,0,0,0,11753,52,40,29,0,58,0
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_products.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_products.csv
new file mode 100644
index 000000000..b0bd40c9c
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_products.csv
@@ -0,0 +1,2 @@
+connection_id,id,program,name,code,bind,line,type,status,sub_status,description,po_id,qd_id,rd_id,acl,reviewer,created_by_id,created_date,created_version,order_in,deleted,plans,releases,builds,cases,projects,executions,bugs,docs,progress,case_review
+1,3,10,产品名称1,产品代号2,0,31,normal,normal,,"<span style=""background-color:#FFFFFF;"">产品描述1</span>",1,1,1,private,"devlake,dev1",1,2022-11-17T06:42:25.000+00:00,17.6,15,0,1,0,0,0,0,0,0,0,0,0
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_stories.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_stories.csv
new file mode 100644
index 000000000..c36663ef0
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_stories.csv
@@ -0,0 +1,8 @@
+connection_id,id,product,branch,version,order_in,vision,parent,module,plan,source,source_note,from_bug,feedback,title,keywords,type,category,pri,estimate,status,sub_status,color,stage,lib,from_story,from_version,opened_by_id,opened_by_name,opened_date,assigned_to_id,assigned_to_name,assigned_date,approved_date,last_edited_id,last_edited_date,changed_date,reviewed_by_id,reviewed_date,closed_id,closed_date,closed_reason,activated_date,to_bug,child_stories,link_stories,link_requirements,dup [...]
+1,1,1,0,1,0,rnd,0,1,1,po,,0,0,首页设计和开发,,story,feature,1,1,active,,,developing,0,0,1,2,产品经理,2012-06-05T02:09:49.000+00:00,2,产品经理,,0000-00-00,2,2012-06-05T02:25:19.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
+1,2,1,0,1,0,rnd,0,2,1,po,,0,0,新闻中心的设计和开发。,,story,feature,1,1,active,,,projected,0,0,1,2,产品经理,2012-06-05T02:16:37.000+00:00,2,产品经理,2012-06-05T02:16:37.000+00:00,0000-00-00,2,2012-06-05T02:25:33.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
+1,3,1,0,2,0,rnd,0,3,1,po,,0,0,成果展示的设计和开发,,story,feature,1,0,active,,,developing,0,0,1,2,产品经理,2012-06-05T02:18:10.000+00:00,2,产品经理,2012-06-05T02:18:10.000+00:00,0000-00-00,2,2012-06-05T02:25:38.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
+1,4,1,0,1,0,rnd,0,4,1,po,,0,0,售后服务的设计和开发,,story,feature,1,1,active,,,developed,0,0,1,2,产品经理,2012-06-05T02:20:16.000+00:00,2,产品经理,2012-06-05T02:20:16.000+00:00,0000-00-00,2,2012-06-05T02:25:42.000+00:00,0000-00-00 00:00:00,0,2012-06-04T16:00:00.000+00:00,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
+1,5,1,0,1,0,rnd,0,5,1,po,,0,0,诚聘英才的设计和开发,,story,feature,1,1,reviewing,,,planned,0,0,1,2,产品经理,2012-06-05T02:21:39.000+00:00,2,产品经理,2012-06-05T02:21:39.000+00:00,0000-00-00,0,,0000-00-00 00:00:00,0,,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
+1,6,1,0,1,0,rnd,0,6,1,po,,0,0,合作洽谈的设计和开发,,story,feature,1,1,reviewing,,,planned,0,0,1,2,产品经理,2012-06-05T02:23:11.000+00:00,2,产品经理,2012-06-05T02:23:11.000+00:00,0000-00-00,0,,0000-00-00 00:00:00,0,,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
+1,7,1,0,1,0,rnd,0,7,1,po,,0,0,关于我们的设计和开发,,story,feature,1,1,reviewing,,,planned,0,0,1,2,产品经理,2012-06-05T02:24:19.000+00:00,2,产品经理,2012-06-05T02:24:19.000+00:00,0000-00-00,0,,0000-00-00 00:00:00,0,,0,,,0000-00-00 00:00:00,0,,,,0,0,,,0,0,1,1.0版本
diff --git a/plugins/zentao/e2e/snapshot_tables/_tool_zentao_tasks.csv b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_tasks.csv
new file mode 100644
index 000000000..276d53cda
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/_tool_zentao_tasks.csv
@@ -0,0 +1,4 @@
+connection_id,id,execution_id,project,parent,execution,module,design,story,story_version,design_version,from_bug,feedback,from_issue,name,type,mode,pri,estimate,consumed,deadline,status,sub_status,color,description,version,opened_by_id,opened_by_name,opened_date,assigned_to_id,assigned_to_name,assigned_date,est_started,real_started,finished_id,finished_date,finished_list,canceled_id,canceled_date,closed_by_id,closed_date,plan_duration,real_duration,closed_reason,last_edited_id,last_edite [...]
+1,1,1,13,0,9,0,0,0,1,0,0,0,0,任务名称,devel,,3,0,0,2022-10-01,wait,,,任务描述<span> </span><br /><div><br /></div>,1,1,devlake,2022-09-19T01:50:37.000+00:00,5,开发乙,2022-09-19T01:50:37.000+00:00,2022-09-20,,0,,,0,,0,,0,0,,0,,0000-00-00 00:00:00,0,0,0,,,,,0,rnd,0,,0,0,,开发乙,3,0,0
+1,2,1,13,0,9,0,0,0,1,0,0,0,0,任务名称,devel,,3,0,0,2022-10-01,wait,,,任务描述<span> </span><br /><div><br /></div>,1,1,devlake,2022-09-19T01:50:37.000+00:00,5,开发乙,2022-09-19T01:50:37.000+00:00,2022-09-20,,0,,,0,,0,,0,0,,0,,0000-00-00 00:00:00,0,0,0,,,,,0,rnd,0,,0,0,,开发乙,3,0,0
+1,3,1,13,0,9,0,0,0,1,0,0,0,0,任务名称,devel,,3,0,0,2022-10-01,wait,,,任务描述<span> </span><br /><div><br /></div>,1,1,devlake,2022-09-19T01:50:37.000+00:00,5,开发乙,2022-09-19T01:50:37.000+00:00,2022-09-20,,0,,,0,,0,,0,0,,0,,0000-00-00 00:00:00,0,0,0,,,,,0,rnd,0,,0,0,,开发乙,3,0,0
diff --git a/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv b/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv
new file mode 100644
index 000000000..af59f0f72
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv
@@ -0,0 +1,5 @@
+board_id,issue_id
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:1
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:2
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:3
+zentao:ZentaoProduct:1:3,zentao:ZentaoBug:1:4
diff --git a/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv b/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv
new file mode 100644
index 000000000..2bfddefa4
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv
@@ -0,0 +1,8 @@
+board_id,issue_id
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:1
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:2
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:3
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:4
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:5
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:6
+zentao:ZentaoProduct:1:3,zentao:ZentaoStory:1:7
diff --git a/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv b/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv
new file mode 100644
index 000000000..5500305c8
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv
@@ -0,0 +1 @@
+board_id,issue_id
diff --git a/plugins/zentao/e2e/snapshot_tables/boards_execution.csv b/plugins/zentao/e2e/snapshot_tables/boards_execution.csv
new file mode 100644
index 000000000..e596c62f9
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/boards_execution.csv
@@ -0,0 +1,2 @@
+id,name,description,url,created_date,type
+zentao:ZentaoExecution:1:1,企业网站第一期,开发企业网站的基本雏形。<br />,",7,1,",,sprint
diff --git a/plugins/zentao/e2e/snapshot_tables/boards_product.csv b/plugins/zentao/e2e/snapshot_tables/boards_product.csv
new file mode 100644
index 000000000..1da72455a
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/boards_product.csv
@@ -0,0 +1,2 @@
+id,name,description,url,created_date,type
+zentao:ZentaoProduct:1:3,产品名称1,"<span style=""background-color:#FFFFFF;"">产品描述1</span>",,2022-11-17T06:42:25.000+00:00,normal
diff --git a/plugins/zentao/e2e/snapshot_tables/issues_bug.csv b/plugins/zentao/e2e/snapshot_tables/issues_bug.csv
new file mode 100644
index 000000000..0bb589b18
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/issues_bug.csv
@@ -0,0 +1,5 @@
+id,url,icon_url,issue_key,title,description,epic_key,type,status,original_status,story_point,resolution_date,created_date,updated_date,lead_time_minutes,parent_issue_id,priority,original_estimate_minutes,time_spent_minutes,time_remaining_minutes,creator_id,creator_name,assignee_id,assignee_name,severity,component,deployment_id
+zentao:ZentaoBug:1:1,,,1,首页页面问题,,,codeerror,IN_PROGRESS,active,0,,2012-06-05T02:56:11.000+00:00,2021-04-28T03:09:08.000+00:00,0,zentao:ZentaoStory:1:1,,0,0,0,7,测试甲,4,开发甲,,,
+zentao:ZentaoBug:1:2,,,2,新闻中心页面问题,,,codeerror,IN_PROGRESS,delay,0,,2012-06-05T02:57:11.000+00:00,2022-10-05T04:19:22.000+00:00,0,zentao:ZentaoStory:1:2,,0,0,0,7,测试甲,0,,,,
+zentao:ZentaoBug:1:3,,,3,成果展示页面问题,,,codeerror,IN_PROGRESS,active,0,,2012-06-05T02:58:22.000+00:00,2021-04-28T03:09:08.000+00:00,0,zentao:ZentaoStory:1:3,,0,0,0,8,测试乙,4,开发甲,,,
+zentao:ZentaoBug:1:4,,,4,售后服务页面问题,,,codeerror,DONE,resolved,0,,2012-06-05T03:00:19.000+00:00,2022-10-05T04:10:08.000+00:00,0,zentao:ZentaoStory:1:4,,0,0,0,9,测试丙,9,测试丙,,,
diff --git a/plugins/zentao/e2e/snapshot_tables/issues_story.csv b/plugins/zentao/e2e/snapshot_tables/issues_story.csv
new file mode 100644
index 000000000..06752d1b5
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/issues_story.csv
@@ -0,0 +1,8 @@
+id,url,icon_url,issue_key,title,description,epic_key,type,status,original_status,story_point,resolution_date,created_date,updated_date,lead_time_minutes,parent_issue_id,priority,original_estimate_minutes,time_spent_minutes,time_remaining_minutes,creator_id,creator_name,assignee_id,assignee_name,severity,component,deployment_id
+zentao:ZentaoStory:1:1,,,1,首页设计和开发,,,story,IN_PROGRESS,developing,0,,2012-06-05T02:09:49.000+00:00,2012-06-05T02:25:19.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:2,,,2,新闻中心的设计和开发。,,,story,IN_PROGRESS,projected,0,,2012-06-05T02:16:37.000+00:00,2012-06-05T02:25:33.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:3,,,3,成果展示的设计和开发,,,story,IN_PROGRESS,developing,0,,2012-06-05T02:18:10.000+00:00,2012-06-05T02:25:38.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:4,,,4,售后服务的设计和开发,,,story,IN_PROGRESS,developed,0,,2012-06-05T02:20:16.000+00:00,2012-06-05T02:25:42.000+00:00,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:5,,,5,诚聘英才的设计和开发,,,story,IN_PROGRESS,planned,0,,2012-06-05T02:21:39.000+00:00,,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:6,,,6,合作洽谈的设计和开发,,,story,IN_PROGRESS,planned,0,,2012-06-05T02:23:11.000+00:00,,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
+zentao:ZentaoStory:1:7,,,7,关于我们的设计和开发,,,story,IN_PROGRESS,planned,0,,2012-06-05T02:24:19.000+00:00,,0,zentao:ZentaoStory:1:0,,0,0,0,2,产品经理,2,产品经理,,,
diff --git a/plugins/zentao/e2e/snapshot_tables/issues_task.csv b/plugins/zentao/e2e/snapshot_tables/issues_task.csv
new file mode 100644
index 000000000..37eaa7ab1
--- /dev/null
+++ b/plugins/zentao/e2e/snapshot_tables/issues_task.csv
@@ -0,0 +1 @@
+id,url,icon_url,issue_key,title,description,epic_key,type,status,original_status,story_point,resolution_date,created_date,updated_date,lead_time_minutes,parent_issue_id,priority,original_estimate_minutes,time_spent_minutes,time_remaining_minutes,creator_id,creator_name,assignee_id,assignee_name,severity,component,deployment_id
diff --git a/plugins/zentao/e2e/story_test.go b/plugins/zentao/e2e/story_test.go
new file mode 100644
index 000000000..c4bdf01d8
--- /dev/null
+++ b/plugins/zentao/e2e/story_test.go
@@ -0,0 +1,69 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "testing"
+
+ "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 TestZentaoStoryDataFlow(t *testing.T) {
+
+ var zentao impl.Zentao
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+ taskData := &tasks.ZentaoTaskData{
+ Options: &tasks.ZentaoOptions{
+ ConnectionId: 1,
+ ProjectId: 1,
+ ProductId: 3,
+ ExecutionId: 1,
+ },
+ }
+
+ // import raw data table
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_stories.csv",
+ "_raw_zentao_api_stories")
+
+ // verify extraction
+ dataflowTester.FlushTabler(&models.ZentaoStory{})
+ dataflowTester.Subtask(tasks.ExtractStoryMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&models.ZentaoStory{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/_tool_zentao_stories.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+
+ // verify conversion
+ dataflowTester.FlushTabler(&ticket.Issue{})
+ dataflowTester.FlushTabler(&ticket.BoardIssue{})
+ dataflowTester.Subtask(tasks.ConvertStoryMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&ticket.Issue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/issues_story.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+ dataflowTester.VerifyTableWithOptions(&ticket.BoardIssue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/board_issues_story.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+}
diff --git a/plugins/zentao/e2e/task_test.go b/plugins/zentao/e2e/task_test.go
new file mode 100644
index 000000000..355635259
--- /dev/null
+++ b/plugins/zentao/e2e/task_test.go
@@ -0,0 +1,68 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "testing"
+
+ "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 TestZentaoTaskDataFlow(t *testing.T) {
+
+ var zentao impl.Zentao
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "zentao", zentao)
+
+ taskData := &tasks.ZentaoTaskData{
+ Options: &tasks.ZentaoOptions{
+ ConnectionId: 1,
+ ProjectId: 1,
+ ProductId: 3,
+ ExecutionId: 1,
+ },
+ }
+
+ // import raw data table
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_zentao_api_tasks.csv",
+ "_raw_zentao_api_tasks")
+
+ // verify extraction
+ dataflowTester.FlushTabler(&models.ZentaoTask{})
+ dataflowTester.Subtask(tasks.ExtractTaskMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&models.ZentaoTask{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/_tool_zentao_tasks.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+
+ dataflowTester.FlushTabler(&ticket.Issue{})
+ dataflowTester.FlushTabler(&ticket.BoardIssue{})
+ dataflowTester.Subtask(tasks.ConvertTaskMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&ticket.Issue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/issues_task.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+ dataflowTester.VerifyTableWithOptions(&ticket.BoardIssue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/board_issues_task.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+}
diff --git a/plugins/zentao/impl/impl.go b/plugins/zentao/impl/impl.go
new file mode 100644
index 000000000..573b8eb16
--- /dev/null
+++ b/plugins/zentao/impl/impl.go
@@ -0,0 +1,136 @@
+/*
+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 impl
+
+import (
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/api"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "github.com/apache/incubator-devlake/plugins/zentao/models/migrationscripts"
+ "github.com/apache/incubator-devlake/plugins/zentao/tasks"
+ "github.com/spf13/viper"
+ "gorm.io/gorm"
+)
+
+// make sure interface is implemented
+var _ core.PluginMeta = (*Zentao)(nil)
+var _ core.PluginInit = (*Zentao)(nil)
+var _ core.PluginTask = (*Zentao)(nil)
+var _ core.PluginApi = (*Zentao)(nil)
+var _ core.PluginBlueprintV100 = (*Zentao)(nil)
+var _ core.CloseablePluginTask = (*Zentao)(nil)
+
+type Zentao struct{}
+
+func (plugin Zentao) Description() string {
+ return "collect some Zentao data"
+}
+
+func (plugin Zentao) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) errors.Error {
+ api.Init(config, logger, db)
+ return nil
+}
+
+func (plugin Zentao) SubTaskMetas() []core.SubTaskMeta {
+ // TODO add your sub task here
+ return []core.SubTaskMeta{
+ tasks.CollectProductMeta,
+ tasks.ExtractProductMeta,
+ tasks.ConvertProductMeta,
+ tasks.CollectExecutionMeta,
+ tasks.ExtractExecutionMeta,
+ tasks.ConvertExecutionMeta,
+ tasks.CollectStoryMeta,
+ tasks.ExtractStoryMeta,
+ tasks.ConvertStoryMeta,
+ tasks.CollectBugMeta,
+ tasks.ExtractBugMeta,
+ tasks.ConvertBugMeta,
+ tasks.CollectTaskMeta,
+ tasks.ExtractTaskMeta,
+ tasks.ConvertTaskMeta,
+ }
+}
+
+func (plugin Zentao) PrepareTaskData(taskCtx core.TaskContext, options map[string]interface{}) (interface{}, errors.Error) {
+ op, err := tasks.DecodeAndValidateTaskOptions(options)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "could not decode Zentao options")
+ }
+ connectionHelper := helper.NewConnectionHelper(
+ taskCtx,
+ nil,
+ )
+ connection := &models.ZentaoConnection{}
+ err = connectionHelper.FirstById(connection, op.ConnectionId)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "unable to get Zentao connection by the given connection ID: %v")
+ }
+
+ apiClient, err := tasks.NewZentaoApiClient(taskCtx, connection)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "unable to get Zentao API client instance: %v")
+ }
+
+ return &tasks.ZentaoTaskData{
+ Options: op,
+ ApiClient: apiClient,
+ }, nil
+}
+
+// PkgPath information lost when compiled as plugin(.so)
+func (plugin Zentao) RootPkgPath() string {
+ return "github.com/apache/incubator-devlake/plugins/zentao"
+}
+
+func (plugin Zentao) MigrationScripts() []core.MigrationScript {
+ return migrationscripts.All()
+}
+
+func (plugin Zentao) ApiResources() map[string]map[string]core.ApiResourceHandler {
+ return map[string]map[string]core.ApiResourceHandler{
+ "test": {
+ "POST": api.TestConnection,
+ },
+ "connections": {
+ "POST": api.PostConnections,
+ "GET": api.ListConnections,
+ },
+ "connections/:connectionId": {
+ "GET": api.GetConnection,
+ "PATCH": api.PatchConnection,
+ "DELETE": api.DeleteConnection,
+ },
+ }
+}
+
+func (plugin Zentao) MakePipelinePlan(connectionId uint64, scope []*core.BlueprintScopeV100) (core.PipelinePlan, errors.Error) {
+ return api.MakePipelinePlan(plugin.SubTaskMetas(), connectionId, scope)
+}
+
+func (plugin Zentao) Close(taskCtx core.TaskContext) errors.Error {
+ data, ok := taskCtx.GetData().(*tasks.ZentaoTaskData)
+ if !ok {
+ return errors.Default.New(fmt.Sprintf("GetData failed when try to close %+v", taskCtx))
+ }
+ data.ApiClient.Release()
+ return nil
+}
diff --git a/plugins/zentao/models/access_token.go b/plugins/zentao/models/access_token.go
new file mode 100644
index 000000000..60bf26178
--- /dev/null
+++ b/plugins/zentao/models/access_token.go
@@ -0,0 +1,27 @@
+/*
+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
+
+type ApiAccessTokenRequest struct {
+ Account string `json:"account"`
+ Password string `json:"password"`
+}
+
+type ApiAccessTokenResponse struct {
+ Token string `json:"token"`
+}
diff --git a/plugins/zentao/models/archived/bug.go b/plugins/zentao/models/archived/bug.go
new file mode 100644
index 000000000..a316cd809
--- /dev/null
+++ b/plugins/zentao/models/archived/bug.go
@@ -0,0 +1,99 @@
+/*
+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/models/migrationscripts/archived"
+ "time"
+)
+
+type ZentaoBug struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Product uint64 `json:"product"`
+ Injection int `json:"injection"`
+ Identify int `json:"identify"`
+ Branch int `json:"branch"`
+ Module int `json:"module"`
+ Execution uint64 `json:"execution"`
+ Plan int `json:"plan"`
+ Story uint64 `json:"story"`
+ StoryVersion int `json:"storyVersion"`
+ Task int `json:"task"`
+ ToTask int `json:"toTask"`
+ ToStory uint64 `json:"toStory"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Severity int `json:"severity"`
+ Pri int `json:"pri"`
+ Type string `json:"type"`
+ Os string `json:"os"`
+ Browser string `json:"browser"`
+ Hardware string `json:"hardware"`
+ Found string `json:"found"`
+ Steps string `json:"steps"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Confirmed int `json:"confirmed"`
+ ActivatedCount int `json:"activatedCount"`
+ ActivatedDate *time.Time `json:"activatedDate"`
+ FeedbackBy string `json:"feedbackBy"`
+ NotifyEmail string `json:"notifyEmail"`
+ OpenedById uint64
+ OpenedByName string
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedBuild string `json:"openedBuild"`
+ AssignedToId uint64
+ AssignedToName string
+ AssignedDate *time.Time `json:"assignedDate"`
+ Deadline string `json:"deadline"`
+ ResolvedById uint64
+ Resolution string `json:"resolution"`
+ ResolvedBuild string `json:"resolvedBuild"`
+ ResolvedDate *time.Time `json:"resolvedDate"`
+ ClosedById uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ DuplicateBug int `json:"duplicateBug"`
+ LinkBug string `json:"linkBug"`
+ Feedback int `json:"feedback"`
+ Result int `json:"result"`
+ Repo int `json:"repo"`
+ Mr int `json:"mr"`
+ Entry string `json:"entry"`
+ Lines string `json:"lines"`
+ V1 string `json:"v1"`
+ V2 string `json:"v2"`
+ RepoType string `json:"repoType"`
+ IssueKey string `json:"issueKey"`
+ Testtask int `json:"testtask"`
+ LastEditedById uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ Deleted bool `json:"deleted"`
+ PriOrder string `json:"priOrder"`
+ SeverityOrder int `json:"severityOrder"`
+ Needconfirm bool `json:"needconfirm"`
+ StatusName string `json:"statusName"`
+ ProductStatus string `json:"productStatus"`
+}
+
+func (ZentaoBug) TableName() string {
+ return "_tool_zentao_bugs"
+}
diff --git a/plugins/zentao/models/archived/connection.go b/plugins/zentao/models/archived/connection.go
new file mode 100644
index 000000000..5a89da666
--- /dev/null
+++ b/plugins/zentao/models/archived/connection.go
@@ -0,0 +1,70 @@
+/*
+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/models/migrationscripts/archived"
+)
+
+// TODO Please modify the following code to fit your needs
+// This object conforms to what the frontend currently sends.
+type ZentaoConnection struct {
+ RestConnection `mapstructure:",squash"`
+ //TODO you may need to use helper.BasicAuth instead of helper.AccessToken
+ BasicAuth `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+ Endpoint string `json:"endpoint"`
+ Proxy string `json:"proxy"`
+ BasicAuth `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type ZentaoResponse struct {
+ Name string `json:"name"`
+ ID uint64 `json:"id"`
+ ZentaoConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+ Id uint64
+ Name string `json:"name"`
+}
+
+func (ZentaoConnection) TableName() string {
+ return "_tool_zentao_connections"
+}
+
+type BasicAuth struct {
+ Username string `mapstructure:"username" validate:"required" json:"username"`
+ Password string `mapstructure:"password" validate:"required" json:"password"`
+}
+
+type RestConnection struct {
+ BaseConnection `mapstructure:",squash"`
+ Endpoint string `mapstructure:"endpoint" validate:"required" json:"endpoint"`
+ Proxy string `mapstructure:"proxy" json:"proxy"`
+ RateLimitPerHour int `comment:"api request rate limt per hour" json:"rateLimit"`
+}
+
+type BaseConnection struct {
+ Name string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
+ archived.Model
+}
diff --git a/plugins/zentao/models/archived/execution.go b/plugins/zentao/models/archived/execution.go
new file mode 100644
index 000000000..cca92bd95
--- /dev/null
+++ b/plugins/zentao/models/archived/execution.go
@@ -0,0 +1,90 @@
+/*
+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/models/migrationscripts/archived"
+ "time"
+)
+
+type ZentaoExecution struct {
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Id uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Model string `json:"model"`
+ Type string `json:"type"`
+ Lifetime string `json:"lifetime"`
+ Budget string `json:"budget"`
+ BudgetUnit string `json:"budgetUnit"`
+ Attribute string `json:"attribute"`
+ Percent int `json:"percent"`
+ Milestone string `json:"milestone"`
+ Output string `json:"output"`
+ Auth string `json:"auth"`
+ Parent uint64 `json:"parent"`
+ Path string `json:"path"`
+ Grade int `json:"grade"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ PlanBegin string `json:"begin"`
+ PlanEnd string `json:"end"`
+ RealBegan string `json:"realBegan"`
+ RealEnd string `json:"realEnd"`
+ Days int `json:"days"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Pri string `json:"pri"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ ParentVersion int `json:"parentVersion"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ OpenedById uint64
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedVersion string `json:"openedVersion"`
+ LastEditedById uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ClosedById uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ CanceledById uint64
+ CanceledDate *time.Time `json:"canceledDate"`
+ SuspendedDate string `json:"suspendedDate"`
+ POId uint64
+ PMId uint64
+ QDId uint64
+ RDId uint64
+ Team string `json:"team"`
+ Acl string `json:"acl"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ DisplayCards int `json:"displayCards"`
+ FluidBoard string `json:"fluidBoard"`
+ Deleted bool `json:"deleted"`
+ TotalHours int `json:"totalHours"`
+ TotalEstimate int `json:"totalEstimate"`
+ TotalConsumed int `json:"totalConsumed"`
+ TotalLeft int `json:"totalLeft"`
+ ProjectId uint64
+ Progress int `json:"progress"`
+ CaseReview bool `json:"caseReview"`
+ archived.NoPKModel
+}
+
+func (ZentaoExecution) TableName() string {
+ return "_tool_zentao_executions"
+}
diff --git a/plugins/zentao/models/archived/product.go b/plugins/zentao/models/archived/product.go
new file mode 100644
index 000000000..c6977a099
--- /dev/null
+++ b/plugins/zentao/models/archived/product.go
@@ -0,0 +1,62 @@
+/*
+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/models/migrationscripts/archived"
+ "time"
+)
+
+type ZentaoProduct struct {
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Id uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Program int `json:"program"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ Bind string `json:"bind"`
+ Line int `json:"line"`
+ Type string `json:"type"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Description string `json:"desc"`
+ POId uint64
+ QDId uint64
+ RDId uint64
+ Acl string `json:"acl"`
+ Reviewer string `json:"reviewer"`
+ CreatedById uint64
+ CreatedDate *time.Time `json:"createdDate"`
+ CreatedVersion string `json:"createdVersion"`
+ OrderIn int `json:"order"`
+ Deleted string `json:"deleted"`
+ Plans int `json:"plans"`
+ Releases int `json:"releases"`
+ Builds int `json:"builds"`
+ Cases int `json:"cases"`
+ Projects int `json:"projects"`
+ Executions int `json:"executions"`
+ Bugs int `json:"bugs"`
+ Docs int `json:"docs"`
+ Progress int `json:"progress"`
+ CaseReview bool `json:"caseReview"`
+ archived.NoPKModel
+}
+
+func (ZentaoProduct) TableName() string {
+ return "_tool_zentao_products"
+}
diff --git a/plugins/zentao/models/archived/project.go b/plugins/zentao/models/archived/project.go
new file mode 100644
index 000000000..8d884420b
--- /dev/null
+++ b/plugins/zentao/models/archived/project.go
@@ -0,0 +1,113 @@
+/*
+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/models/migrationscripts/archived"
+ "time"
+)
+
+type ZentaoProject struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Model string `json:"model"`
+ Type string `json:"type"`
+ Lifetime string `json:"lifetime"`
+ Budget string `json:"budget"`
+ BudgetUnit string `json:"budgetUnit"`
+ Attribute string `json:"attribute"`
+ Percent int `json:"percent"`
+ Milestone string `json:"milestone"`
+ Output string `json:"output"`
+ Auth string `json:"auth"`
+ Parent uint64 `json:"parent"`
+ Path string `json:"path"`
+ Grade int `json:"grade"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ PlanBegin string `json:"begin"`
+ PlanEnd string `json:"end"`
+ RealBegan string `json:"realBegan"`
+ RealEnd string `json:"realEnd"`
+ Days int `json:"days"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Pri string `json:"pri"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ ParentVersion int `json:"parentVersion"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ //OpenedBy string `json:"openedBy"`
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedVersion string `json:"openedVersion"`
+ LastEditedBy string `json:"lastEditedBy"`
+ LastEditedDate **time.Time `json:"lastEditedDate"`
+ ClosedBy string `json:"closedBy"`
+ ClosedDate **time.Time `json:"closedDate"`
+ CanceledBy string `json:"canceledBy"`
+ CanceledDate **time.Time `json:"canceledDate"`
+ SuspendedDate string `json:"suspendedDate"`
+ PO string `json:"PO"`
+ PM `json:"PM"`
+ QD string `json:"QD"`
+ RD string `json:"RD"`
+ Team string `json:"team"`
+ Acl string `json:"acl"`
+ Whitelist `json:"whitelist" gorm:"-"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ DisplayCards int `json:"displayCards"`
+ FluidBoard string `json:"fluidBoard"`
+ Deleted bool `json:"deleted"`
+ Delay int `json:"delay"`
+ Hours `json:"hours"`
+ TeamCount int `json:"teamCount"`
+ LeftTasks string `json:"leftTasks"`
+ //TeamMembers []interface{} `json:"teamMembers" gorm:"-"`
+ TotalEstimate int `json:"totalEstimate"`
+ TotalConsumed int `json:"totalConsumed"`
+ TotalLeft int `json:"totalLeft"`
+ Progress int `json:"progress"`
+ TotalReal int `json:"totalReal"`
+}
+type PM struct {
+ PmId uint64 `json:"id"`
+ PmAccount string `json:"account"`
+ PmAvatar string `json:"avatar"`
+ PmRealname string `json:"realname"`
+}
+type Whitelist []struct {
+ WhitelistID uint64 `json:"id"`
+ WhitelistAccount string `json:"account"`
+ WhitelistAvatar string `json:"avatar"`
+ WhitelistRealname string `json:"realname"`
+}
+type Hours struct {
+ HoursTotalEstimate int `json:"totalEstimate"`
+ HoursTotalConsumed int `json:"totalConsumed"`
+ HoursTotalLeft int `json:"totalLeft"`
+ HoursProgress int `json:"progress"`
+ HoursTotalReal int `json:"totalReal"`
+}
+
+func (ZentaoProject) TableName() string {
+ return "_tool_zentao_projects"
+}
diff --git a/plugins/zentao/models/archived/story.go b/plugins/zentao/models/archived/story.go
new file mode 100644
index 000000000..770ecdfd8
--- /dev/null
+++ b/plugins/zentao/models/archived/story.go
@@ -0,0 +1,89 @@
+/*
+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/models/migrationscripts/archived"
+ "time"
+)
+
+type ZentaoStory struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL" `
+ Project uint64 `json:"project"`
+ Product uint64 `json:"product"`
+ Branch int `json:"branch"`
+ Version int `json:"version"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ Parent uint64 `json:"parent"`
+ Module int `json:"module"`
+ Plan string `json:"plan"`
+ Source string `json:"source"`
+ SourceNote string `json:"sourceNote"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ FeedbackBy string `json:"feedbackBy"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Type string `json:"type"`
+ Category string `json:"category"`
+ Pri int `json:"pri"`
+ Estimate int `json:"estimate"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Stage string `json:"stage"`
+ StagedById uint64 `json:"stagedBy"`
+ //Mailto []interface{} `json:"mailto"`
+ Lib int `json:"lib"`
+ FromStory uint64 `json:"fromStory"`
+ FromVersion int `json:"fromVersion"`
+ OpenedById uint64
+ OpenedByName string
+ OpenedDate *time.Time `json:"openedDate"`
+ AssignedToId uint64
+ AssignedToName string
+ AssignedDate *time.Time `json:"assignedDate"`
+ ApprovedDate string `json:"approvedDate"`
+ LastEditedId uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ChangedDate string `json:"changedDate"`
+ ReviewedById uint64 `json:"reviewedBy"`
+ ReviewedDate *time.Time `json:"reviewedDate"`
+ ClosedId uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ ClosedReason string `json:"closedReason"`
+ ActivatedDate string `json:"activatedDate"`
+ ToBug int `json:"toBug"`
+ ChildStories string `json:"childStories"`
+ LinkStories string `json:"linkStories"`
+ LinkRequirements string `json:"linkRequirements"`
+ DuplicateStory uint64 `json:"duplicateStory"`
+ StoryChanged string `json:"storyChanged"`
+ NotifyEmail string `json:"notifyEmail"`
+ URChanged string `json:"URChanged"`
+ Deleted bool `json:"deleted"`
+ PriOrder string `json:"priOrder"`
+ PlanTitle string `json:"planTitle"`
+}
+
+func (ZentaoStory) TableName() string {
+ return "_tool_zentao_stories"
+}
diff --git a/plugins/zentao/models/archived/task.go b/plugins/zentao/models/archived/task.go
new file mode 100644
index 000000000..4fd3533d3
--- /dev/null
+++ b/plugins/zentao/models/archived/task.go
@@ -0,0 +1,97 @@
+/*
+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/models/migrationscripts/archived"
+ "time"
+)
+
+type ZentaoTask struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ExecutionId uint64 `json:"execution_id"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Parent uint64 `json:"parent"`
+ Execution uint64 `json:"execution"`
+ Module int `json:"module"`
+ Design int `json:"design"`
+ Story uint64 `json:"story"`
+ StoryVersion int `json:"storyVersion"`
+ DesignVersion int `json:"designVersion"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ FromIssue int `json:"fromIssue"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Mode string `json:"mode"`
+ Pri int `json:"pri"`
+ Estimate int `json:"estimate"`
+ Consumed int `json:"consumed"`
+ Deadline string `json:"deadline"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ //Mailto interface{} `json:"mailto"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ OpenedById uint64
+ OpenedByName string
+ OpenedDate *time.Time `json:"openedDate"`
+ AssignedToId uint64
+ AssignedToName string
+ AssignedDate *time.Time `json:"assignedDate"`
+ EstStarted string `json:"estStarted"`
+ RealStarted *time.Time `json:"realStarted"`
+ FinishedId uint64
+ FinishedDate *time.Time `json:"finishedDate"`
+ FinishedList string `json:"finishedList"`
+ CanceledId uint64
+ CanceledDate *time.Time `json:"canceledDate"`
+ ClosedById uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ ClosedReason string `json:"closedReason"`
+ LastEditedId uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ActivatedDate string `json:"activatedDate"`
+ OrderIn int `json:"order"`
+ Repo int `json:"repo"`
+ Mr int `json:"mr"`
+ Entry string `json:"entry"`
+ Lines string `json:"lines"`
+ V1 string `json:"v1"`
+ V2 string `json:"v2"`
+ Deleted bool `json:"deleted"`
+ Vision string `json:"vision"`
+ StoryID uint64 `json:"storyID"`
+ StoryTitle string `json:"storyTitle"`
+ Branch int `json:"branch"`
+ LatestStoryVersion int `json:"latestStoryVersion"`
+ StoryStatus string `json:"storyStatus"`
+ AssignedToRealName string `json:"assignedToRealName"`
+ PriOrder string `json:"priOrder"`
+ NeedConfirm bool `json:"needConfirm"`
+ Progress int `json:"progress"`
+}
+
+func (ZentaoTask) TableName() string {
+ return "_tool_zentao_tasks"
+}
diff --git a/plugins/zentao/models/bug.go b/plugins/zentao/models/bug.go
new file mode 100644
index 000000000..ed0d65599
--- /dev/null
+++ b/plugins/zentao/models/bug.go
@@ -0,0 +1,192 @@
+/*
+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/models/common"
+ "time"
+)
+
+type ZentaoBugRes struct {
+ ID uint64 `json:"id"`
+ Project uint64 `json:"project"`
+ Product uint64 `json:"product"`
+ Injection int `json:"injection"`
+ Identify int `json:"identify"`
+ Branch int `json:"branch"`
+ Module int `json:"module"`
+ Execution uint64 `json:"execution"`
+ Plan int `json:"plan"`
+ Story uint64 `json:"story"`
+ StoryVersion int `json:"storyVersion"`
+ Task int `json:"task"`
+ ToTask int `json:"toTask"`
+ ToStory uint64 `json:"toStory"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Severity int `json:"severity"`
+ Pri int `json:"pri"`
+ Type string `json:"type"`
+ Os string `json:"os"`
+ Browser string `json:"browser"`
+ Hardware string `json:"hardware"`
+ Found string `json:"found"`
+ Steps string `json:"steps"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Confirmed int `json:"confirmed"`
+ ActivatedCount int `json:"activatedCount"`
+ ActivatedDate *time.Time `json:"activatedDate"`
+ FeedbackBy string `json:"feedbackBy"`
+ NotifyEmail string `json:"notifyEmail"`
+ OpenedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"openedBy"`
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedBuild string `json:"openedBuild"`
+ AssignedTo struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"assignedTo"`
+ AssignedDate *time.Time `json:"assignedDate"`
+ Deadline string `json:"deadline"`
+ ResolvedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"resolvedBy"`
+ Resolution string `json:"resolution"`
+ ResolvedBuild string `json:"resolvedBuild"`
+ ResolvedDate *time.Time `json:"resolvedDate"`
+ ClosedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"closedBy"`
+ ClosedDate *time.Time `json:"closedDate"`
+ DuplicateBug int `json:"duplicateBug"`
+ LinkBug string `json:"linkBug"`
+ Feedback int `json:"feedback"`
+ Result int `json:"result"`
+ Repo int `json:"repo"`
+ Mr int `json:"mr"`
+ Entry string `json:"entry"`
+ Lines string `json:"lines"`
+ V1 string `json:"v1"`
+ V2 string `json:"v2"`
+ RepoType string `json:"repoType"`
+ IssueKey string `json:"issueKey"`
+ Testtask int `json:"testtask"`
+ LastEditedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"lastEditedBy"`
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ Deleted bool `json:"deleted"`
+ PriOrder string `json:"priOrder"`
+ SeverityOrder int `json:"severityOrder"`
+ Needconfirm bool `json:"needconfirm"`
+ StatusName string `json:"statusName"`
+ ProductStatus string `json:"productStatus"`
+}
+
+type ZentaoBug struct {
+ common.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Product uint64 `json:"product"`
+ Injection int `json:"injection"`
+ Identify int `json:"identify"`
+ Branch int `json:"branch"`
+ Module int `json:"module"`
+ Execution uint64 `json:"execution"`
+ Plan int `json:"plan"`
+ Story uint64 `json:"story"`
+ StoryVersion int `json:"storyVersion"`
+ Task int `json:"task"`
+ ToTask int `json:"toTask"`
+ ToStory uint64 `json:"toStory"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Severity int `json:"severity"`
+ Pri int `json:"pri"`
+ Type string `json:"type"`
+ Os string `json:"os"`
+ Browser string `json:"browser"`
+ Hardware string `json:"hardware"`
+ Found string `json:"found"`
+ Steps string `json:"steps"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Confirmed int `json:"confirmed"`
+ ActivatedCount int `json:"activatedCount"`
+ ActivatedDate *time.Time `json:"activatedDate"`
+ FeedbackBy string `json:"feedbackBy"`
+ NotifyEmail string `json:"notifyEmail"`
+ OpenedById uint64
+ OpenedByName string
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedBuild string `json:"openedBuild"`
+ AssignedToId uint64
+ AssignedToName string
+ AssignedDate *time.Time `json:"assignedDate"`
+ Deadline string `json:"deadline"`
+ ResolvedById uint64
+ Resolution string `json:"resolution"`
+ ResolvedBuild string `json:"resolvedBuild"`
+ ResolvedDate *time.Time `json:"resolvedDate"`
+ ClosedById uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ DuplicateBug int `json:"duplicateBug"`
+ LinkBug string `json:"linkBug"`
+ Feedback int `json:"feedback"`
+ Result int `json:"result"`
+ Repo int `json:"repo"`
+ Mr int `json:"mr"`
+ Entry string `json:"entry"`
+ Lines string `json:"lines"`
+ V1 string `json:"v1"`
+ V2 string `json:"v2"`
+ RepoType string `json:"repoType"`
+ IssueKey string `json:"issueKey"`
+ Testtask int `json:"testtask"`
+ LastEditedById uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ Deleted bool `json:"deleted"`
+ PriOrder string `json:"priOrder"`
+ SeverityOrder int `json:"severityOrder"`
+ Needconfirm bool `json:"needconfirm"`
+ StatusName string `json:"statusName"`
+ ProductStatus string `json:"productStatus"`
+}
+
+func (ZentaoBug) TableName() string {
+ return "_tool_zentao_bugs"
+}
diff --git a/plugins/zentao/models/connection.go b/plugins/zentao/models/connection.go
new file mode 100644
index 000000000..e25f0c3d5
--- /dev/null
+++ b/plugins/zentao/models/connection.go
@@ -0,0 +1,51 @@
+/*
+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/plugins/helper"
+
+// TODO Please modify the following code to fit your needs
+// This object conforms to what the frontend currently sends.
+type ZentaoConnection struct {
+ helper.RestConnection `mapstructure:",squash"`
+ //TODO you may need to use helper.BasicAuth instead of helper.AccessToken
+ helper.BasicAuth `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+ Endpoint string `json:"endpoint"`
+ Proxy string `json:"proxy"`
+ helper.BasicAuth `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type ZentaoResponse struct {
+ Name string `json:"name"`
+ ID uint64 `json:"id"`
+ ZentaoConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+ Id uint64
+ Name string `json:"name"`
+}
+
+func (ZentaoConnection) TableName() string {
+ return "_tool_zentao_connections"
+}
diff --git a/plugins/zentao/models/execution.go b/plugins/zentao/models/execution.go
new file mode 100644
index 000000000..38768ae96
--- /dev/null
+++ b/plugins/zentao/models/execution.go
@@ -0,0 +1,274 @@
+/*
+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/models/common"
+ "time"
+)
+
+type ZentaoExecutionRes struct {
+ ID uint64 `json:"id"`
+ Project uint64 `json:"project"`
+ Model string `json:"model"`
+ Type string `json:"type"`
+ Lifetime string `json:"lifetime"`
+ Budget string `json:"budget"`
+ BudgetUnit string `json:"budgetUnit"`
+ Attribute string `json:"attribute"`
+ Percent int `json:"percent"`
+ Milestone string `json:"milestone"`
+ Output string `json:"output"`
+ Auth string `json:"auth"`
+ Parent uint64 `json:"parent"`
+ Path string `json:"path"`
+ Grade int `json:"grade"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ PlanBegin string `json:"begin"`
+ PlanEnd string `json:"end"`
+ RealBegan *time.Time `json:"realBegan"`
+ RealEnd *time.Time `json:"realEnd"`
+ Days int `json:"days"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Pri string `json:"pri"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ ParentVersion int `json:"parentVersion"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ OpenedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"openedBy"`
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedVersion string `json:"openedVersion"`
+ LastEditedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"lastEditedBy"`
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ClosedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"closedBy"`
+ ClosedDate *time.Time `json:"closedDate"`
+ CanceledBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"canceledBy"`
+ CanceledDate *time.Time `json:"canceledDate"`
+ SuspendedDate string `json:"suspendedDate"`
+ PO struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"PO"`
+ PM struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"PM"`
+ QD struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"QD"`
+ RD struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"RD"`
+ Team string `json:"team"`
+ Acl string `json:"acl"`
+ Whitelist []struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"whitelist"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ DisplayCards int `json:"displayCards"`
+ FluidBoard string `json:"fluidBoard"`
+ Deleted bool `json:"deleted"`
+ TotalHours int `json:"totalHours"`
+ TotalEstimate int `json:"totalEstimate"`
+ TotalConsumed int `json:"totalConsumed"`
+ TotalLeft int `json:"totalLeft"`
+ ProjectInfo struct {
+ ID uint64 `json:"id"`
+ Project uint64 `json:"project"`
+ Model string `json:"model"`
+ Type string `json:"type"`
+ Lifetime string `json:"lifetime"`
+ Budget string `json:"budget"`
+ BudgetUnit string `json:"budgetUnit"`
+ Attribute string `json:"attribute"`
+ Percent int `json:"percent"`
+ Milestone string `json:"milestone"`
+ Output string `json:"output"`
+ Auth string `json:"auth"`
+ Parent uint64 `json:"parent"`
+ Path string `json:"path"`
+ Grade int `json:"grade"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ PlanBegin string `json:"begin"`
+ PlanEnd string `json:"end"`
+ RealBegan string `json:"realBegan"`
+ RealEnd string `json:"realEnd"`
+ Days int `json:"days"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Pri string `json:"pri"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ ParentVersion int `json:"parentVersion"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ OpenedBy string `json:"openedBy"`
+ OpenedDate string `json:"openedDate"`
+ OpenedVersion string `json:"openedVersion"`
+ LastEditedBy string `json:"lastEditedBy"`
+ LastEditedDate string `json:"lastEditedDate"`
+ ClosedBy string `json:"closedBy"`
+ ClosedDate string `json:"closedDate"`
+ CanceledBy string `json:"canceledBy"`
+ CanceledDate string `json:"canceledDate"`
+ SuspendedDate string `json:"suspendedDate"`
+ PO string `json:"PO"`
+ PM string `json:"PM"`
+ QD string `json:"QD"`
+ RD string `json:"RD"`
+ Team string `json:"team"`
+ Acl string `json:"acl"`
+ Whitelist string `json:"whitelist"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ DisplayCards int `json:"displayCards"`
+ FluidBoard string `json:"fluidBoard"`
+ Deleted string `json:"deleted"`
+ } `json:"projectInfo"`
+ Progress int `json:"progress"`
+ TeamMembers []struct {
+ ID uint64 `json:"id"`
+ Root int `json:"root"`
+ Type string `json:"type"`
+ Account string `json:"account"`
+ Role string `json:"role"`
+ Position string `json:"position"`
+ Limited string `json:"limited"`
+ Join string `json:"join"`
+ Days int `json:"days"`
+ Hours int `json:"hours"`
+ Estimate string `json:"estimate"`
+ Consumed string `json:"consumed"`
+ Left string `json:"left"`
+ OrderIn int `json:"order"`
+ TotalHours int `json:"totalHours"`
+ UserID uint64 `json:"userID"`
+ Realname string `json:"realname"`
+ } `json:"teamMembers"`
+ Products []struct {
+ ID uint64 `json:"id"`
+ Name string `json:"name"`
+ Plans []interface{} `json:"plans"`
+ } `json:"products"`
+ CaseReview bool `json:"caseReview"`
+}
+
+type ZentaoExecution struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ Id uint64 `json:"id" gorm:"primaryKey"`
+ Project uint64 `json:"project"`
+ Model string `json:"model"`
+ Type string `json:"type"`
+ Lifetime string `json:"lifetime"`
+ Budget string `json:"budget"`
+ BudgetUnit string `json:"budgetUnit"`
+ Attribute string `json:"attribute"`
+ Percent int `json:"percent"`
+ Milestone string `json:"milestone"`
+ Output string `json:"output"`
+ Auth string `json:"auth"`
+ Parent uint64 `json:"parent"`
+ Path string `json:"path"`
+ Grade int `json:"grade"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ PlanBegin string `json:"begin"`
+ PlanEnd string `json:"end"`
+ RealBegan *time.Time `json:"realBegan"`
+ RealEnd *time.Time `json:"realEnd"`
+ Days int `json:"days"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Pri string `json:"pri"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ ParentVersion int `json:"parentVersion"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ OpenedById uint64
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedVersion string `json:"openedVersion"`
+ LastEditedById uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ClosedById uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ CanceledById uint64
+ CanceledDate *time.Time `json:"canceledDate"`
+ SuspendedDate string `json:"suspendedDate"`
+ POId uint64
+ PMId uint64
+ QDId uint64
+ RDId uint64
+ Team string `json:"team"`
+ Acl string `json:"acl"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ DisplayCards int `json:"displayCards"`
+ FluidBoard string `json:"fluidBoard"`
+ Deleted bool `json:"deleted"`
+ TotalHours int `json:"totalHours"`
+ TotalEstimate int `json:"totalEstimate"`
+ TotalConsumed int `json:"totalConsumed"`
+ TotalLeft int `json:"totalLeft"`
+ ProjectId uint64
+ Progress int `json:"progress"`
+ CaseReview bool `json:"caseReview"`
+ common.NoPKModel
+}
+
+func (ZentaoExecution) TableName() string {
+ return "_tool_zentao_executions"
+}
diff --git a/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go b/plugins/zentao/models/migrationscripts/20220906_add_init_tables.go
new file mode 100644
index 000000000..5d6e3cb23
--- /dev/null
+++ b/plugins/zentao/models/migrationscripts/20220906_add_init_tables.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 migrationscripts
+
+import (
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/zentao/models/archived"
+)
+
+type addInitTables struct{}
+
+func (*addInitTables) Up(basicRes core.BasicRes) errors.Error {
+ return migrationhelper.AutoMigrateTables(
+ basicRes,
+ &archived.ZentaoConnection{},
+ &archived.ZentaoProject{},
+ &archived.ZentaoProduct{},
+ &archived.ZentaoExecution{},
+ &archived.ZentaoStory{},
+ &archived.ZentaoBug{},
+ &archived.ZentaoTask{},
+ )
+}
+
+func (*addInitTables) Version() uint64 {
+ return 20220910000001
+}
+
+func (*addInitTables) Name() string {
+ return "zentao init schemas"
+}
diff --git a/plugins/zentao/models/migrationscripts/register.go b/plugins/zentao/models/migrationscripts/register.go
new file mode 100644
index 000000000..c0c766cd6
--- /dev/null
+++ b/plugins/zentao/models/migrationscripts/register.go
@@ -0,0 +1,29 @@
+/*
+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 migrationscripts
+
+import (
+ "github.com/apache/incubator-devlake/plugins/core"
+)
+
+// All return all the migration scripts
+func All() []core.MigrationScript {
+ return []core.MigrationScript{
+ new(addInitTables),
+ }
+}
diff --git a/plugins/zentao/models/product.go b/plugins/zentao/models/product.go
new file mode 100644
index 000000000..802e8668d
--- /dev/null
+++ b/plugins/zentao/models/product.go
@@ -0,0 +1,126 @@
+/*
+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/models/common"
+ "time"
+)
+
+type ZentaoProductRes struct {
+ ID uint64 `json:"id"`
+ Program int `json:"program"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ Bind string `json:"bind"`
+ Line int `json:"line"`
+ Type string `json:"type"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Description string `json:"desc"`
+ PO struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"PO"`
+ QD struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"QD"`
+ RD struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"RD"`
+ Feedback interface{} `json:"feedback"`
+ Acl string `json:"acl"`
+ Whitelist []interface{} `json:"whitelist"`
+ Reviewer string `json:"reviewer"`
+ CreatedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"createdBy"`
+ CreatedDate *time.Time `json:"createdDate"`
+ CreatedVersion string `json:"createdVersion"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ Deleted string `json:"deleted"`
+ Stories struct {
+ Active int `json:"active"`
+ Reviewing int `json:"reviewing"`
+ int `json:""`
+ Draft int `json:"draft"`
+ Closed int `json:"closed"`
+ Changing int `json:"changing"`
+ } `json:"stories"`
+ Plans int `json:"plans"`
+ Releases int `json:"releases"`
+ Builds int `json:"builds"`
+ Cases int `json:"cases"`
+ Projects int `json:"projects"`
+ Executions int `json:"executions"`
+ Bugs int `json:"bugs"`
+ Docs int `json:"docs"`
+ Progress int `json:"progress"`
+ CaseReview bool `json:"caseReview"`
+}
+
+type ZentaoProduct struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ Id uint64 `json:"id" gorm:"primaryKey"`
+ Program int `json:"program"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ Bind string `json:"bind"`
+ Line int `json:"line"`
+ Type string `json:"type"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Description string `json:"desc"`
+ POId uint64
+ QDId uint64
+ RDId uint64
+ Acl string `json:"acl"`
+ Reviewer string `json:"reviewer"`
+ CreatedById uint64
+ CreatedDate *time.Time `json:"createdDate"`
+ CreatedVersion string `json:"createdVersion"`
+ OrderIn int `json:"order"`
+ Deleted string `json:"deleted"`
+ Plans int `json:"plans"`
+ Releases int `json:"releases"`
+ Builds int `json:"builds"`
+ Cases int `json:"cases"`
+ Projects int `json:"projects"`
+ Executions int `json:"executions"`
+ Bugs int `json:"bugs"`
+ Docs int `json:"docs"`
+ Progress int `json:"progress"`
+ CaseReview bool `json:"caseReview"`
+ common.NoPKModel
+}
+
+func (ZentaoProduct) TableName() string {
+ return "_tool_zentao_products"
+}
diff --git a/plugins/zentao/models/project.go b/plugins/zentao/models/project.go
new file mode 100644
index 000000000..08238a8a5
--- /dev/null
+++ b/plugins/zentao/models/project.go
@@ -0,0 +1,113 @@
+/*
+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/models/common"
+ "time"
+)
+
+type ZentaoProject struct {
+ common.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Model string `json:"model"`
+ Type string `json:"type"`
+ Lifetime string `json:"lifetime"`
+ Budget string `json:"budget"`
+ BudgetUnit string `json:"budgetUnit"`
+ Attribute string `json:"attribute"`
+ Percent int `json:"percent"`
+ Milestone string `json:"milestone"`
+ Output string `json:"output"`
+ Auth string `json:"auth"`
+ Parent uint64 `json:"parent"`
+ Path string `json:"path"`
+ Grade int `json:"grade"`
+ Name string `json:"name"`
+ Code string `json:"code"`
+ PlanBegin string `json:"begin"`
+ PlanEnd string `json:"end"`
+ RealBegan string `json:"realBegan"`
+ RealEnd string `json:"realEnd"`
+ Days int `json:"days"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Pri string `json:"pri"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ ParentVersion int `json:"parentVersion"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ //OpenedBy string `json:"openedBy"`
+ OpenedDate *time.Time `json:"openedDate"`
+ OpenedVersion string `json:"openedVersion"`
+ LastEditedBy string `json:"lastEditedBy"`
+ LastEditedDate **time.Time `json:"lastEditedDate"`
+ ClosedBy string `json:"closedBy"`
+ ClosedDate **time.Time `json:"closedDate"`
+ CanceledBy string `json:"canceledBy"`
+ CanceledDate **time.Time `json:"canceledDate"`
+ SuspendedDate string `json:"suspendedDate"`
+ PO string `json:"PO"`
+ PM `json:"PM"`
+ QD string `json:"QD"`
+ RD string `json:"RD"`
+ Team string `json:"team"`
+ Acl string `json:"acl"`
+ Whitelist `json:"whitelist" gorm:"-"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ DisplayCards int `json:"displayCards"`
+ FluidBoard string `json:"fluidBoard"`
+ Deleted bool `json:"deleted"`
+ Delay int `json:"delay"`
+ Hours `json:"hours"`
+ TeamCount int `json:"teamCount"`
+ LeftTasks string `json:"leftTasks"`
+ //TeamMembers []interface{} `json:"teamMembers" gorm:"-"`
+ TotalEstimate int `json:"totalEstimate"`
+ TotalConsumed int `json:"totalConsumed"`
+ TotalLeft int `json:"totalLeft"`
+ Progress int `json:"progress"`
+ TotalReal int `json:"totalReal"`
+}
+type PM struct {
+ PmId uint64 `json:"id"`
+ PmAccount string `json:"account"`
+ PmAvatar string `json:"avatar"`
+ PmRealname string `json:"realname"`
+}
+type Whitelist []struct {
+ WhitelistID uint64 `json:"id"`
+ WhitelistAccount string `json:"account"`
+ WhitelistAvatar string `json:"avatar"`
+ WhitelistRealname string `json:"realname"`
+}
+type Hours struct {
+ HoursTotalEstimate float64 `json:"totalEstimate"`
+ HoursTotalConsumed float64 `json:"totalConsumed"`
+ HoursTotalLeft float64 `json:"totalLeft"`
+ HoursProgress float64 `json:"progress"`
+ HoursTotalReal float64 `json:"totalReal"`
+}
+
+func (ZentaoProject) TableName() string {
+ return "_tool_zentao_projects"
+}
diff --git a/plugins/zentao/models/story.go b/plugins/zentao/models/story.go
new file mode 100644
index 000000000..3b319d8b3
--- /dev/null
+++ b/plugins/zentao/models/story.go
@@ -0,0 +1,169 @@
+/*
+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/models/common"
+ "time"
+)
+
+type ZentaoStoryRes struct {
+ ID uint64 `json:"id"`
+ Vision string `json:"vision"`
+ Parent uint64 `json:"parent"`
+ Product uint64 `json:"product"`
+ Branch int `json:"branch"`
+ Module int `json:"module"`
+ Plan string `json:"plan"`
+ Source string `json:"source"`
+ SourceNote string `json:"sourceNote"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Type string `json:"type"`
+ Category string `json:"category"`
+ Pri int `json:"pri"`
+ Estimate int `json:"estimate"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Stage string `json:"stage"`
+ Mailto []interface{} `json:"mailto"`
+ Lib int `json:"lib"`
+ FromStory uint64 `json:"fromStory"`
+ FromVersion int `json:"fromVersion"`
+ OpenedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"openedBy"`
+ OpenedDate *time.Time `json:"openedDate"`
+ AssignedTo struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"assignedTo"`
+ AssignedDate *time.Time `json:"assignedDate"`
+ ApprovedDate string `json:"approvedDate"`
+ LastEditedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"lastEditedBy"`
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ChangedBy string `json:"changedBy"`
+ ChangedDate string `json:"changedDate"`
+ ReviewedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"reviewedBy"`
+ ReviewedDate *time.Time `json:"reviewedDate"`
+ ClosedBy struct {
+ ID uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"closedBy"`
+ ClosedDate *time.Time `json:"closedDate"`
+ ClosedReason string `json:"closedReason"`
+ ActivatedDate string `json:"activatedDate"`
+ ToBug int `json:"toBug"`
+ ChildStories string `json:"childStories"`
+ LinkStories string `json:"linkStories"`
+ LinkRequirements string `json:"linkRequirements"`
+ DuplicateStory uint64 `json:"duplicateStory"`
+ Version int `json:"version"`
+ StoryChanged string `json:"storyChanged"`
+ FeedbackBy string `json:"feedbackBy"`
+ NotifyEmail string `json:"notifyEmail"`
+ URChanged string `json:"URChanged"`
+ Deleted bool `json:"deleted"`
+ PriOrder string `json:"priOrder"`
+ PlanTitle string `json:"planTitle"`
+ ProductStatus string `json:"productStatus"`
+}
+
+type ZentaoStory struct {
+ common.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL" `
+ Product uint64 `json:"product"`
+ Branch int `json:"branch"`
+ Version int `json:"version"`
+ OrderIn int `json:"order"`
+ Vision string `json:"vision"`
+ Parent uint64 `json:"parent"`
+ Module int `json:"module"`
+ Plan string `json:"plan"`
+ Source string `json:"source"`
+ SourceNote string `json:"sourceNote"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ Title string `json:"title"`
+ Keywords string `json:"keywords"`
+ Type string `json:"type"`
+ Category string `json:"category"`
+ Pri int `json:"pri"`
+ Estimate int `json:"estimate"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Stage string `json:"stage"`
+ //Mailto []interface{} `json:"mailto"`
+ Lib int `json:"lib"`
+ FromStory uint64 `json:"fromStory"`
+ FromVersion int `json:"fromVersion"`
+ OpenedById uint64
+ OpenedByName string
+ OpenedDate *time.Time `json:"openedDate"`
+ AssignedToId uint64
+ AssignedToName string
+ AssignedDate *time.Time `json:"assignedDate"`
+ ApprovedDate string `json:"approvedDate"`
+ LastEditedId uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ChangedDate string `json:"changedDate"`
+ ReviewedById uint64 `json:"reviewedBy"`
+ ReviewedDate *time.Time `json:"reviewedDate"`
+ ClosedId uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ ClosedReason string `json:"closedReason"`
+ ActivatedDate string `json:"activatedDate"`
+ ToBug int `json:"toBug"`
+ ChildStories string `json:"childStories"`
+ LinkStories string `json:"linkStories"`
+ LinkRequirements string `json:"linkRequirements"`
+ DuplicateStory uint64 `json:"duplicateStory"`
+ StoryChanged string `json:"storyChanged"`
+ FeedbackBy string `json:"feedbackBy"`
+ NotifyEmail string `json:"notifyEmail"`
+ URChanged string `json:"URChanged"`
+ Deleted bool `json:"deleted"`
+ PriOrder string `json:"priOrder"`
+ PlanTitle string `json:"planTitle"`
+}
+
+func (ZentaoStory) TableName() string {
+ return "_tool_zentao_stories"
+}
diff --git a/plugins/zentao/models/task.go b/plugins/zentao/models/task.go
new file mode 100644
index 000000000..4ca8777c5
--- /dev/null
+++ b/plugins/zentao/models/task.go
@@ -0,0 +1,201 @@
+/*
+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/models/common"
+ "time"
+)
+
+type ZentaoTaskRes struct {
+ Id uint64 `json:"id"`
+ Project uint64 `json:"project"`
+ Parent uint64 `json:"parent"`
+ Execution uint64 `json:"execution"`
+ Module int `json:"module"`
+ Design int `json:"design"`
+ Story uint64 `json:"story"`
+ StoryVersion int `json:"storyVersion"`
+ DesignVersion int `json:"designVersion"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ FromIssue int `json:"fromIssue"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Mode string `json:"mode"`
+ Pri int `json:"pri"`
+ Estimate int `json:"estimate"`
+ Consumed int `json:"consumed"`
+ Deadline string `json:"deadline"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ Mailto []struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"mailto"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ OpenedBy struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"openedBy"`
+ OpenedDate *time.Time `json:"openedDate"`
+ AssignedTo struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"assignedTo"`
+ AssignedDate *time.Time `json:"assignedDate"`
+ EstStarted string `json:"estStarted"`
+ RealStarted *time.Time `json:"realStarted"`
+ FinishedBy struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"finishedBy"`
+ FinishedDate *time.Time `json:"finishedDate"`
+ FinishedList string `json:"finishedList"`
+ CanceledBy struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"canceledBy"`
+ CanceledDate *time.Time `json:"canceledDate"`
+ ClosedBy struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"closedBy"`
+ ClosedDate *time.Time `json:"closedDate"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ ClosedReason string `json:"closedReason"`
+ LastEditedBy struct {
+ Id uint64 `json:"id"`
+ Account string `json:"account"`
+ Avatar string `json:"avatar"`
+ Realname string `json:"realname"`
+ } `json:"lastEditedBy"`
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ActivatedDate string `json:"activatedDate"`
+ OrderIn int `json:"order"`
+ Repo int `json:"repo"`
+ Mr int `json:"mr"`
+ Entry string `json:"entry"`
+ Lines string `json:"lines"`
+ V1 string `json:"v1"`
+ V2 string `json:"v2"`
+ Deleted bool `json:"deleted"`
+ Vision string `json:"vision"`
+ StoryID uint64 `json:"storyID"`
+ StoryTitle string `json:"storyTitle"`
+ Branch interface {
+ } `json:"branch"`
+ LatestStoryVersion interface {
+ } `json:"latestStoryVersion"`
+ StoryStatus interface {
+ } `json:"storyStatus"`
+ AssignedToRealName string `json:"assignedToRealName"`
+ PriOrder string `json:"priOrder"`
+ Delay int `json:"delay"`
+ NeedConfirm bool `json:"needConfirm"`
+ Progress int `json:"progress"`
+}
+
+type ZentaoTask struct {
+ common.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ ExecutionId uint64 `json:"execution_id"`
+ ID uint64 `json:"id" gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Project uint64 `json:"project"`
+ Parent uint64 `json:"parent"`
+ Execution uint64 `json:"execution"`
+ Module int `json:"module"`
+ Design int `json:"design"`
+ Story uint64 `json:"story"`
+ StoryVersion int `json:"storyVersion"`
+ DesignVersion int `json:"designVersion"`
+ FromBug int `json:"fromBug"`
+ Feedback int `json:"feedback"`
+ FromIssue int `json:"fromIssue"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Mode string `json:"mode"`
+ Pri int `json:"pri"`
+ Estimate int `json:"estimate"`
+ Consumed int `json:"consumed"`
+ Deadline string `json:"deadline"`
+ Status string `json:"status"`
+ SubStatus string `json:"subStatus"`
+ Color string `json:"color"`
+ //Mailto interface{} `json:"mailto"`
+ Description string `json:"desc"`
+ Version int `json:"version"`
+ OpenedById uint64
+ OpenedByName string
+ OpenedDate *time.Time `json:"openedDate"`
+ AssignedToId uint64
+ AssignedToName string
+ AssignedDate *time.Time `json:"assignedDate"`
+ EstStarted string `json:"estStarted"`
+ RealStarted *time.Time `json:"realStarted"`
+ FinishedId uint64
+ FinishedDate *time.Time `json:"finishedDate"`
+ FinishedList string `json:"finishedList"`
+ CanceledId uint64
+ CanceledDate *time.Time `json:"canceledDate"`
+ ClosedById uint64
+ ClosedDate *time.Time `json:"closedDate"`
+ PlanDuration int `json:"planDuration"`
+ RealDuration int `json:"realDuration"`
+ ClosedReason string `json:"closedReason"`
+ LastEditedId uint64
+ LastEditedDate *time.Time `json:"lastEditedDate"`
+ ActivatedDate string `json:"activatedDate"`
+ OrderIn int `json:"order"`
+ Repo int `json:"repo"`
+ Mr int `json:"mr"`
+ Entry string `json:"entry"`
+ Lines string `json:"lines"`
+ V1 string `json:"v1"`
+ V2 string `json:"v2"`
+ Deleted bool `json:"deleted"`
+ Vision string `json:"vision"`
+ StoryID uint64 `json:"storyID"`
+ StoryTitle string `json:"storyTitle"`
+ Branch int `json:"branch"`
+ LatestStoryVersion int `json:"latestStoryVersion"`
+ StoryStatus string `json:"storyStatus"`
+ AssignedToRealName string `json:"assignedToRealName"`
+ PriOrder string `json:"priOrder"`
+ NeedConfirm bool `json:"needConfirm"`
+ Progress int `json:"progress"`
+}
+
+func (ZentaoTask) TableName() string {
+ return "_tool_zentao_tasks"
+}
diff --git a/plugins/zentao/tasks/api_client.go b/plugins/zentao/tasks/api_client.go
new file mode 100644
index 000000000..aff7b4d69
--- /dev/null
+++ b/plugins/zentao/tasks/api_client.go
@@ -0,0 +1,95 @@
+/*
+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 (
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+func NewZentaoApiClient(taskCtx core.TaskContext, connection *models.ZentaoConnection) (*helper.ApiAsyncClient, error) {
+ authApiClient, err := helper.NewApiClient(taskCtx.GetContext(), connection.Endpoint, nil, 0, connection.Proxy, taskCtx)
+ if err != nil {
+ return nil, err
+ }
+
+ // request for access token
+ tokenReqBody := &models.ApiAccessTokenRequest{
+ Account: connection.Username,
+ Password: connection.Password,
+ }
+ tokenRes, err := authApiClient.Post("/tokens", nil, tokenReqBody, nil)
+ if err != nil {
+ return nil, err
+ }
+ tokenResBody := &models.ApiAccessTokenResponse{}
+ err = helper.UnmarshalResponse(tokenRes, tokenResBody)
+ if err != nil {
+ return nil, err
+ }
+ if tokenResBody.Token == "" {
+ return nil, errors.Default.New("failed to request access token")
+ }
+ // real request apiClient
+ apiClient, err := helper.NewApiClient(taskCtx.GetContext(), connection.Endpoint, nil, 0, connection.Proxy, taskCtx)
+ if err != nil {
+ return nil, err
+ }
+ // set token
+ apiClient.SetHeaders(map[string]string{
+ "Token": fmt.Sprintf("%v", tokenResBody.Token),
+ })
+
+ // create rate limit calculator
+ rateLimiter := &helper.ApiRateLimitCalculator{
+ UserRateLimitPerHour: connection.RateLimitPerHour,
+ DynamicRateLimit: func(res *http.Response) (int, time.Duration, errors.Error) {
+ rateLimitHeader := res.Header.Get("RateLimit-Limit")
+ if rateLimitHeader == "" {
+ // use default
+ return 0, 0, nil
+ }
+ rateLimit, err := strconv.Atoi(rateLimitHeader)
+ if err != nil {
+ return 0, 0, errors.Default.Wrap(err, "failed to parse RateLimit-Limit header: %w")
+ }
+ // seems like {{ .plugin-ame }} rate limit is on minute basis
+ return rateLimit, 1 * time.Minute, nil
+ },
+ }
+ asyncApiClient, err := helper.CreateAsyncApiClient(
+ taskCtx,
+ apiClient,
+ rateLimiter,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return asyncApiClient, nil
+}
+
+type ZentaoPagination struct {
+ Page int `json:"page"`
+}
diff --git a/plugins/zentao/tasks/bug_collector.go b/plugins/zentao/tasks/bug_collector.go
new file mode 100644
index 000000000..843438583
--- /dev/null
+++ b/plugins/zentao/tasks/bug_collector.go
@@ -0,0 +1,82 @@
+/*
+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"
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "net/http"
+ "net/url"
+)
+
+const RAW_BUG_TABLE = "zentao_api_bugs"
+
+var _ core.SubTaskEntryPoint = CollectBug
+
+func CollectBug(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_BUG_TABLE,
+ },
+ ApiClient: data.ApiClient,
+
+ PageSize: 100,
+ // TODO write which api would you want request
+ UrlTemplate: "/products/{{ .Params.ProductId }}/bugs",
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ var data struct {
+ Bugs []json.RawMessage `json:"bugs"`
+ }
+ err := helper.UnmarshalResponse(res, &data)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao bug collector")
+ }
+ return data.Bugs, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
+
+var CollectBugMeta = core.SubTaskMeta{
+ Name: "CollectBug",
+ EntryPoint: CollectBug,
+ EnabledByDefault: true,
+ Description: "Collect Bug data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/bug_convertor.go b/plugins/zentao/tasks/bug_convertor.go
new file mode 100644
index 000000000..c58c332f1
--- /dev/null
+++ b/plugins/zentao/tasks/bug_convertor.go
@@ -0,0 +1,116 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "reflect"
+ "strconv"
+)
+
+var _ core.SubTaskEntryPoint = ConvertBug
+
+var ConvertBugMeta = core.SubTaskMeta{
+ Name: "convertBug",
+ EntryPoint: ConvertBug,
+ EnabledByDefault: true,
+ Description: "convert Zentao bug",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertBug(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ db := taskCtx.GetDal()
+ bugIdGen := didgen.NewDomainIdGenerator(&models.ZentaoBug{})
+ boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+ storyIdGen := didgen.NewDomainIdGenerator(&models.ZentaoStory{})
+ cursor, err := db.Cursor(
+ dal.From(&models.ZentaoBug{}),
+ dal.Where(`_tool_zentao_bugs.product = ? and
+ _tool_zentao_bugs.connection_id = ?`, data.Options.ProductId, data.Options.ConnectionId),
+ )
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+ convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+ InputRowType: reflect.TypeOf(models.ZentaoBug{}),
+ Input: cursor,
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_BUG_TABLE,
+ },
+ Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+ toolEntity := inputRow.(*models.ZentaoBug)
+ domainEntity := &ticket.Issue{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: bugIdGen.Generate(toolEntity.ConnectionId, toolEntity.ID),
+ },
+ IssueKey: strconv.FormatUint(toolEntity.ID, 10),
+ Title: toolEntity.Title,
+ Type: toolEntity.Type,
+ OriginalStatus: toolEntity.Status,
+ ResolutionDate: toolEntity.ClosedDate,
+ CreatedDate: toolEntity.OpenedDate,
+ UpdatedDate: toolEntity.LastEditedDate,
+ ParentIssueId: storyIdGen.Generate(data.Options.ConnectionId, toolEntity.Story),
+ Priority: string(rune(toolEntity.Pri)),
+ CreatorId: strconv.FormatUint(toolEntity.OpenedById, 10),
+ CreatorName: toolEntity.OpenedByName,
+ AssigneeId: strconv.FormatUint(toolEntity.AssignedToId, 10),
+ AssigneeName: toolEntity.AssignedToName,
+ Severity: string(rune(toolEntity.Severity)),
+ }
+ switch toolEntity.Status {
+ case "resolved":
+ domainEntity.Status = "DONE"
+ default:
+ domainEntity.Status = "IN_PROGRESS"
+ }
+ if toolEntity.ClosedDate != nil {
+ domainEntity.LeadTimeMinutes = int64(toolEntity.ClosedDate.Sub(*toolEntity.OpenedDate).Minutes())
+ }
+ domainBoardIssue := &ticket.BoardIssue{
+ BoardId: boardIdGen.Generate(data.Options.ConnectionId, data.Options.ProductId),
+ IssueId: domainEntity.Id,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, domainEntity, domainBoardIssue)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/bug_extractor.go b/plugins/zentao/tasks/bug_extractor.go
new file mode 100644
index 000000000..088c75602
--- /dev/null
+++ b/plugins/zentao/tasks/bug_extractor.go
@@ -0,0 +1,138 @@
+/*
+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/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractBug
+
+var ExtractBugMeta = core.SubTaskMeta{
+ Name: "extractBug",
+ EntryPoint: ExtractBug,
+ EnabledByDefault: true,
+ Description: "extract Zentao bug",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractBug(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_BUG_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ res := &models.ZentaoBugRes{}
+ err := json.Unmarshal(row.Data, res)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ bug := &models.ZentaoBug{
+ ConnectionId: data.Options.ConnectionId,
+ ID: res.ID,
+ Project: res.Project,
+ Product: res.Product,
+ Injection: res.Injection,
+ Identify: res.Identify,
+ Branch: res.Branch,
+ Module: res.Module,
+ Execution: res.Execution,
+ Plan: res.Plan,
+ Story: res.Story,
+ StoryVersion: res.StoryVersion,
+ Task: res.Task,
+ ToTask: res.ToTask,
+ ToStory: res.ToStory,
+ Title: res.Title,
+ Keywords: res.Keywords,
+ Severity: res.Severity,
+ Pri: res.Pri,
+ Type: res.Type,
+ Os: res.Os,
+ Browser: res.Browser,
+ Hardware: res.Hardware,
+ Found: res.Found,
+ Steps: res.Steps,
+ Status: res.Status,
+ SubStatus: res.SubStatus,
+ Color: res.Color,
+ Confirmed: res.Confirmed,
+ ActivatedCount: res.ActivatedCount,
+ ActivatedDate: res.ActivatedDate,
+ FeedbackBy: res.FeedbackBy,
+ NotifyEmail: res.NotifyEmail,
+ OpenedById: res.OpenedBy.ID,
+ OpenedByName: res.OpenedBy.Realname,
+ OpenedDate: res.OpenedDate,
+ OpenedBuild: res.OpenedBuild,
+ AssignedToId: res.AssignedTo.ID,
+ AssignedToName: res.AssignedTo.Realname,
+ AssignedDate: res.AssignedDate,
+ Deadline: res.Deadline,
+ ResolvedById: res.ResolvedBy.ID,
+ Resolution: res.Resolution,
+ ResolvedBuild: res.ResolvedBuild,
+ ResolvedDate: res.ResolvedDate,
+ ClosedById: res.ClosedBy.ID,
+ ClosedDate: res.ClosedDate,
+ DuplicateBug: res.DuplicateBug,
+ LinkBug: res.LinkBug,
+ Feedback: res.Feedback,
+ Result: res.Result,
+ Repo: res.Repo,
+ Mr: res.Mr,
+ Entry: res.Entry,
+ Lines: res.Lines,
+ V1: res.V1,
+ V2: res.V2,
+ RepoType: res.RepoType,
+ IssueKey: res.IssueKey,
+ Testtask: res.Testtask,
+ LastEditedById: res.LastEditedBy.ID,
+ LastEditedDate: res.LastEditedDate,
+ Deleted: res.Deleted,
+ PriOrder: res.PriOrder,
+ SeverityOrder: res.SeverityOrder,
+ Needconfirm: res.Needconfirm,
+ StatusName: res.StatusName,
+ ProductStatus: res.ProductStatus,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, bug)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/execution_collector.go b/plugins/zentao/tasks/execution_collector.go
new file mode 100644
index 000000000..8f6d2c6ae
--- /dev/null
+++ b/plugins/zentao/tasks/execution_collector.go
@@ -0,0 +1,81 @@
+/*
+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"
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "io"
+ "net/http"
+ "net/url"
+)
+
+const RAW_EXECUTION_TABLE = "zentao_api_executions"
+
+var _ core.SubTaskEntryPoint = CollectExecution
+
+func CollectExecution(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_EXECUTION_TABLE,
+ },
+ ApiClient: data.ApiClient,
+
+ PageSize: 100,
+ // TODO write which api would you want request
+ UrlTemplate: "executions/{{ .Params.ExecutionId }}",
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao execution collector")
+ }
+ res.Body.Close()
+ return []json.RawMessage{body}, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
+
+var CollectExecutionMeta = core.SubTaskMeta{
+ Name: "CollectExecution",
+ EntryPoint: CollectExecution,
+ EnabledByDefault: true,
+ Description: "Collect Execution data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/execution_convertor.go b/plugins/zentao/tasks/execution_convertor.go
new file mode 100644
index 000000000..a53ebb802
--- /dev/null
+++ b/plugins/zentao/tasks/execution_convertor.go
@@ -0,0 +1,93 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "reflect"
+)
+
+var _ core.SubTaskEntryPoint = ConvertExecutions
+
+var ConvertExecutionMeta = core.SubTaskMeta{
+ Name: "convertExecutions",
+ EntryPoint: ConvertExecutions,
+ EnabledByDefault: true,
+ Description: "convert Zentao executions",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertExecutions(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ db := taskCtx.GetDal()
+ boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoExecution{})
+ cursor, err := db.Cursor(
+ dal.From(&models.ZentaoExecution{}),
+ dal.Where(`_tool_zentao_executions.id = ? and
+ _tool_zentao_executions.connection_id = ?`, data.Options.ExecutionId, data.Options.ConnectionId),
+ )
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+ convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+ InputRowType: reflect.TypeOf(models.ZentaoExecution{}),
+ Input: cursor,
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_EXECUTION_TABLE,
+ },
+ Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+ toolExecution := inputRow.(*models.ZentaoExecution)
+
+ domainBoard := &ticket.Board{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: boardIdGen.Generate(toolExecution.ConnectionId, toolExecution.Id),
+ },
+ Name: toolExecution.Name,
+ Description: toolExecution.Description,
+ Url: toolExecution.Path,
+ CreatedDate: toolExecution.OpenedDate,
+ Type: toolExecution.Type,
+ }
+
+ results := make([]interface{}, 0)
+ results = append(results, domainBoard)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/execution_extractor.go b/plugins/zentao/tasks/execution_extractor.go
new file mode 100644
index 000000000..3e1094c6c
--- /dev/null
+++ b/plugins/zentao/tasks/execution_extractor.go
@@ -0,0 +1,128 @@
+/*
+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/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractExecutions
+
+var ExtractExecutionMeta = core.SubTaskMeta{
+ Name: "extractExecutions",
+ EntryPoint: ExtractExecutions,
+ EnabledByDefault: true,
+ Description: "extract Zentao executions",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractExecutions(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_EXECUTION_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ res := &models.ZentaoExecutionRes{}
+ err := json.Unmarshal(row.Data, res)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ execution := &models.ZentaoExecution{
+ ConnectionId: data.Options.ConnectionId,
+ Id: res.ID,
+ Project: res.Project,
+ Model: res.Model,
+ Type: res.Type,
+ Lifetime: res.Lifetime,
+ Budget: res.Budget,
+ BudgetUnit: res.BudgetUnit,
+ Attribute: res.Attribute,
+ Percent: res.Percent,
+ Milestone: res.Milestone,
+ Output: res.Output,
+ Auth: res.Auth,
+ Parent: res.Parent,
+ Path: res.Path,
+ Grade: res.Grade,
+ Name: res.Name,
+ Code: res.Code,
+ PlanBegin: res.PlanBegin,
+ PlanEnd: res.PlanEnd,
+ RealBegan: res.RealBegan,
+ RealEnd: res.RealEnd,
+ Days: res.Days,
+ Status: res.Status,
+ SubStatus: res.SubStatus,
+ Pri: res.Pri,
+ Description: res.Description,
+ Version: res.Version,
+ ParentVersion: res.ParentVersion,
+ PlanDuration: res.PlanDuration,
+ RealDuration: res.RealDuration,
+ OpenedById: res.OpenedBy.ID,
+ OpenedDate: res.OpenedDate,
+ OpenedVersion: res.OpenedVersion,
+ LastEditedById: res.LastEditedBy.ID,
+ LastEditedDate: res.LastEditedDate,
+ ClosedById: res.ClosedBy.ID,
+ ClosedDate: res.ClosedDate,
+ CanceledById: res.CanceledBy.ID,
+ CanceledDate: res.CanceledDate,
+ SuspendedDate: res.SuspendedDate,
+ POId: res.PO.ID,
+ PMId: res.PM.ID,
+ QDId: res.QD.ID,
+ RDId: res.RD.ID,
+ Team: res.Team,
+ Acl: res.Acl,
+ OrderIn: res.OrderIn,
+ Vision: res.Vision,
+ DisplayCards: res.DisplayCards,
+ FluidBoard: res.FluidBoard,
+ Deleted: res.Deleted,
+ TotalHours: res.TotalHours,
+ TotalEstimate: res.TotalEstimate,
+ TotalConsumed: res.TotalConsumed,
+ TotalLeft: res.TotalLeft,
+ Progress: res.Progress,
+ CaseReview: res.CaseReview,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, execution)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/product_collector.go b/plugins/zentao/tasks/product_collector.go
new file mode 100644
index 000000000..cc1e9d8cb
--- /dev/null
+++ b/plugins/zentao/tasks/product_collector.go
@@ -0,0 +1,80 @@
+/*
+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"
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "io"
+ "net/http"
+ "net/url"
+)
+
+const RAW_PRODUCT_TABLE = "zentao_api_products"
+
+var _ core.SubTaskEntryPoint = CollectProduct
+
+func CollectProduct(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ExecutionId: data.Options.ExecutionId,
+ ProductId: data.Options.ProductId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_PRODUCT_TABLE,
+ },
+ ApiClient: data.ApiClient,
+ PageSize: 100,
+ // TODO write which api would you want request
+ UrlTemplate: "products/{{ .Params.ProductId }}",
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao product collector")
+ }
+ res.Body.Close()
+ return []json.RawMessage{body}, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
+
+var CollectProductMeta = core.SubTaskMeta{
+ Name: "CollectProduct",
+ EntryPoint: CollectProduct,
+ EnabledByDefault: true,
+ Description: "Collect Product data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/product_convertor.go b/plugins/zentao/tasks/product_convertor.go
new file mode 100644
index 000000000..83fa41154
--- /dev/null
+++ b/plugins/zentao/tasks/product_convertor.go
@@ -0,0 +1,92 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "reflect"
+)
+
+var _ core.SubTaskEntryPoint = ConvertProducts
+
+var ConvertProductMeta = core.SubTaskMeta{
+ Name: "convertProducts",
+ EntryPoint: ConvertProducts,
+ EnabledByDefault: true,
+ Description: "convert Zentao products",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertProducts(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ db := taskCtx.GetDal()
+ boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+ cursor, err := db.Cursor(
+ dal.From(&models.ZentaoProduct{}),
+ dal.Where(`_tool_zentao_products.id = ? and
+ _tool_zentao_products.connection_id = ?`, data.Options.ProductId, data.Options.ConnectionId),
+ )
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+ convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+ InputRowType: reflect.TypeOf(models.ZentaoProduct{}),
+ Input: cursor,
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ExecutionId: data.Options.ExecutionId,
+ ProductId: data.Options.ProductId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_PRODUCT_TABLE,
+ },
+ Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+ toolProduct := inputRow.(*models.ZentaoProduct)
+
+ domainBoard := &ticket.Board{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: boardIdGen.Generate(toolProduct.ConnectionId, toolProduct.Id),
+ },
+ Name: toolProduct.Name,
+ Description: toolProduct.Description,
+ CreatedDate: toolProduct.CreatedDate,
+ Type: toolProduct.Type,
+ }
+
+ results := make([]interface{}, 0)
+ results = append(results, domainBoard)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/product_extractor.go b/plugins/zentao/tasks/product_extractor.go
new file mode 100644
index 000000000..21a624846
--- /dev/null
+++ b/plugins/zentao/tasks/product_extractor.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 tasks
+
+import (
+ "encoding/json"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractProducts
+
+var ExtractProductMeta = core.SubTaskMeta{
+ Name: "extractProducts",
+ EntryPoint: ExtractProducts,
+ EnabledByDefault: true,
+ Description: "extract Zentao products",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractProducts(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ExecutionId: data.Options.ExecutionId,
+ ProductId: data.Options.ProductId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_PRODUCT_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ res := &models.ZentaoProductRes{}
+ err := json.Unmarshal(row.Data, res)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao product extractor")
+ }
+ product := &models.ZentaoProduct{
+ ConnectionId: data.Options.ConnectionId,
+ Id: uint64(res.ID),
+ Program: res.Program,
+ Name: res.Name,
+ Code: res.Code,
+ Bind: res.Bind,
+ Line: res.Line,
+ Type: res.Type,
+ Status: res.Status,
+ SubStatus: res.SubStatus,
+ Description: res.Description,
+ POId: res.PO.ID,
+ QDId: res.QD.ID,
+ RDId: res.RD.ID,
+ Acl: res.Acl,
+ Reviewer: res.Reviewer,
+ CreatedById: res.CreatedBy.ID,
+ CreatedDate: res.CreatedDate,
+ CreatedVersion: res.CreatedVersion,
+ OrderIn: res.OrderIn,
+ Deleted: res.Deleted,
+ Plans: res.Plans,
+ Releases: res.Releases,
+ Builds: res.Builds,
+ Cases: res.Cases,
+ Projects: res.Projects,
+ Executions: res.Executions,
+ Bugs: res.Bugs,
+ Docs: res.Docs,
+ Progress: res.Progress,
+ CaseReview: res.CaseReview,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, product)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/project_collector.go b/plugins/zentao/tasks/project_collector.go
new file mode 100644
index 000000000..6d737a0b5
--- /dev/null
+++ b/plugins/zentao/tasks/project_collector.go
@@ -0,0 +1,79 @@
+/*
+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"
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "net/http"
+ "net/url"
+)
+
+const RAW_PROJECT_TABLE = "zentao_api_projects"
+
+var _ core.SubTaskEntryPoint = CollectProject
+
+func CollectProject(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ExecutionId: data.Options.ExecutionId,
+ ProductId: data.Options.ProductId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_PROJECT_TABLE,
+ },
+ ApiClient: data.ApiClient,
+
+ PageSize: 100,
+ // TODO write which api would you want request
+ UrlTemplate: "projects",
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ var data struct {
+ Projects []json.RawMessage `json:"projects"`
+ }
+ err := helper.UnmarshalResponse(res, &data)
+ return data.Projects, err
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
+
+var CollectProjectMeta = core.SubTaskMeta{
+ Name: "CollectProject",
+ EntryPoint: CollectProject,
+ EnabledByDefault: true,
+ Description: "Collect Project data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/project_extractor.go b/plugins/zentao/tasks/project_extractor.go
new file mode 100644
index 000000000..ce1463471
--- /dev/null
+++ b/plugins/zentao/tasks/project_extractor.go
@@ -0,0 +1,69 @@
+/*
+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/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractProjects
+
+var ExtractProjectMeta = core.SubTaskMeta{
+ Name: "extractProjects",
+ EntryPoint: ExtractProjects,
+ EnabledByDefault: true,
+ Description: "extract Zentao projects",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractProjects(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ExecutionId: data.Options.ExecutionId,
+ ProductId: data.Options.ProductId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_PROJECT_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ project := &models.ZentaoProject{}
+ err := json.Unmarshal(row.Data, project)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao project executor")
+ }
+ project.ConnectionId = data.Options.ConnectionId
+ results := make([]interface{}, 0)
+ results = append(results, project)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/shared.go b/plugins/zentao/tasks/shared.go
new file mode 100644
index 000000000..2dc63d789
--- /dev/null
+++ b/plugins/zentao/tasks/shared.go
@@ -0,0 +1,35 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/errors"
+ "net/http"
+
+ "github.com/apache/incubator-devlake/plugins/helper"
+)
+
+func GetTotalPagesFromResponse(res *http.Response, args *helper.ApiCollectorArgs) (int, errors.Error) {
+ body := &ZentaoPagination{}
+ err := helper.UnmarshalResponse(res, body)
+ if err != nil {
+ return 0, err
+ }
+ return body.Page, nil
+
+}
diff --git a/plugins/zentao/tasks/story_collector.go b/plugins/zentao/tasks/story_collector.go
new file mode 100644
index 000000000..d7db9c4f7
--- /dev/null
+++ b/plugins/zentao/tasks/story_collector.go
@@ -0,0 +1,83 @@
+/*
+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"
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "net/http"
+ "net/url"
+)
+
+const RAW_STORY_TABLE = "zentao_api_stories"
+
+var _ core.SubTaskEntryPoint = CollectStory
+
+func CollectStory(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_STORY_TABLE,
+ },
+ ApiClient: data.ApiClient,
+
+ PageSize: 100,
+ // TODO write which api would you want request
+ UrlTemplate: "/products/{{ .Params.ProjectId }}/stories",
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+ query.Set("status", "allstory")
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ var data struct {
+ Story []json.RawMessage `json:"stories"`
+ }
+ err := helper.UnmarshalResponse(res, &data)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao bug collector")
+ }
+ return data.Story, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
+
+var CollectStoryMeta = core.SubTaskMeta{
+ Name: "CollectStory",
+ EntryPoint: CollectStory,
+ EnabledByDefault: true,
+ Description: "Collect Story data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/story_convertor.go b/plugins/zentao/tasks/story_convertor.go
new file mode 100644
index 000000000..95b713535
--- /dev/null
+++ b/plugins/zentao/tasks/story_convertor.go
@@ -0,0 +1,115 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "reflect"
+ "strconv"
+)
+
+var _ core.SubTaskEntryPoint = ConvertStory
+
+var ConvertStoryMeta = core.SubTaskMeta{
+ Name: "convertStory",
+ EntryPoint: ConvertStory,
+ EnabledByDefault: true,
+ Description: "convert Zentao story",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertStory(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ db := taskCtx.GetDal()
+ storyIdGen := didgen.NewDomainIdGenerator(&models.ZentaoStory{})
+ boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+ cursor, err := db.Cursor(
+ dal.From(&models.ZentaoStory{}),
+ dal.Where(`_tool_zentao_stories.product = ? and
+ _tool_zentao_stories.connection_id = ?`, data.Options.ExecutionId, data.Options.ConnectionId),
+ )
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+ convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+ InputRowType: reflect.TypeOf(models.ZentaoStory{}),
+ Input: cursor,
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_STORY_TABLE,
+ },
+ Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+ toolEntity := inputRow.(*models.ZentaoStory)
+
+ domainEntity := &ticket.Issue{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: storyIdGen.Generate(toolEntity.ConnectionId, toolEntity.ID),
+ },
+ IssueKey: strconv.FormatUint(toolEntity.ID, 10),
+ Title: toolEntity.Title,
+ Type: toolEntity.Type,
+ OriginalStatus: toolEntity.Stage,
+ ResolutionDate: toolEntity.ClosedDate,
+ CreatedDate: toolEntity.OpenedDate,
+ UpdatedDate: toolEntity.LastEditedDate,
+ ParentIssueId: storyIdGen.Generate(data.Options.ConnectionId, toolEntity.Parent),
+ Priority: string(rune(toolEntity.Pri)),
+ CreatorId: strconv.FormatUint(toolEntity.OpenedById, 10),
+ CreatorName: toolEntity.OpenedByName,
+ AssigneeId: strconv.FormatUint(toolEntity.AssignedToId, 10),
+ AssigneeName: toolEntity.AssignedToName,
+ }
+ switch toolEntity.Stage {
+ case "closed":
+ domainEntity.Status = "DONE"
+ default:
+ domainEntity.Status = "IN_PROGRESS"
+ }
+ if toolEntity.ClosedDate != nil {
+ domainEntity.LeadTimeMinutes = int64(toolEntity.ClosedDate.Sub(*toolEntity.OpenedDate).Minutes())
+ }
+ domainBoardIssue := &ticket.BoardIssue{
+ BoardId: boardIdGen.Generate(data.Options.ConnectionId, data.Options.ProductId),
+ IssueId: domainEntity.Id,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, domainEntity, domainBoardIssue)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/story_extractor.go b/plugins/zentao/tasks/story_extractor.go
new file mode 100644
index 000000000..b144d738a
--- /dev/null
+++ b/plugins/zentao/tasks/story_extractor.go
@@ -0,0 +1,125 @@
+/*
+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/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractStory
+
+var ExtractStoryMeta = core.SubTaskMeta{
+ Name: "extractStory",
+ EntryPoint: ExtractStory,
+ EnabledByDefault: true,
+ Description: "extract Zentao story",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractStory(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_STORY_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ res := &models.ZentaoStoryRes{}
+ err := json.Unmarshal(row.Data, res)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ story := &models.ZentaoStory{
+ ConnectionId: data.Options.ConnectionId,
+ ID: res.ID,
+ Product: res.Product,
+ Branch: res.Branch,
+ Version: res.Version,
+ OrderIn: 0,
+ Vision: res.Vision,
+ Parent: res.Parent,
+ Module: res.Module,
+ Plan: res.Plan,
+ Source: res.Source,
+ SourceNote: res.SourceNote,
+ FromBug: res.FromBug,
+ Feedback: res.Feedback,
+ Title: res.Title,
+ Keywords: res.Keywords,
+ Type: res.Type,
+ Category: res.Category,
+ Pri: res.Pri,
+ Estimate: res.Estimate,
+ Status: res.Status,
+ SubStatus: res.SubStatus,
+ Color: res.Color,
+ Stage: res.Stage,
+ Lib: res.Lib,
+ FromStory: res.FromStory,
+ FromVersion: res.FromVersion,
+ OpenedById: res.OpenedBy.ID,
+ OpenedByName: res.OpenedBy.Realname,
+ OpenedDate: res.OpenedDate,
+ AssignedToId: res.AssignedTo.ID,
+ AssignedToName: res.AssignedTo.Realname,
+ AssignedDate: res.AssignedDate,
+ ApprovedDate: res.ApprovedDate,
+ LastEditedId: res.LastEditedBy.ID,
+ LastEditedDate: res.LastEditedDate,
+ ChangedDate: res.ChangedDate,
+ ReviewedById: res.ReviewedBy.ID,
+ ReviewedDate: res.ReviewedDate,
+ ClosedId: res.ClosedBy.ID,
+ ClosedDate: res.ClosedDate,
+ ClosedReason: res.ClosedReason,
+ ActivatedDate: res.ActivatedDate,
+ ToBug: res.ToBug,
+ ChildStories: res.ChildStories,
+ LinkStories: res.LinkStories,
+ LinkRequirements: res.LinkRequirements,
+ DuplicateStory: res.DuplicateStory,
+ StoryChanged: res.StoryChanged,
+ FeedbackBy: res.FeedbackBy,
+ NotifyEmail: res.NotifyEmail,
+ URChanged: res.URChanged,
+ Deleted: res.Deleted,
+ PriOrder: res.PriOrder,
+ PlanTitle: res.PlanTitle,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, story)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/plugins/zentao/tasks/task_collector.go b/plugins/zentao/tasks/task_collector.go
new file mode 100644
index 000000000..8094870cf
--- /dev/null
+++ b/plugins/zentao/tasks/task_collector.go
@@ -0,0 +1,82 @@
+/*
+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"
+ "fmt"
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "net/http"
+ "net/url"
+)
+
+const RAW_TASK_TABLE = "zentao_api_tasks"
+
+var _ core.SubTaskEntryPoint = CollectTask
+
+func CollectTask(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_TASK_TABLE,
+ },
+ ApiClient: data.ApiClient,
+
+ PageSize: 100,
+ // TODO write which api would you want request
+ UrlTemplate: "/executions/{{ .Params.ExecutionId }}/tasks",
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ var data struct {
+ Task []json.RawMessage `json:"tasks"`
+ }
+ err := helper.UnmarshalResponse(res, &data)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error reading endpoint response by Zentao bug collector")
+ }
+ return data.Task, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
+
+var CollectTaskMeta = core.SubTaskMeta{
+ Name: "CollectTask",
+ EntryPoint: CollectTask,
+ EnabledByDefault: true,
+ Description: "Collect Task data from Zentao api",
+}
diff --git a/plugins/zentao/tasks/task_convertor.go b/plugins/zentao/tasks/task_convertor.go
new file mode 100644
index 000000000..9063289fe
--- /dev/null
+++ b/plugins/zentao/tasks/task_convertor.go
@@ -0,0 +1,117 @@
+/*
+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 (
+ "github.com/apache/incubator-devlake/errors"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+ "reflect"
+ "strconv"
+)
+
+var _ core.SubTaskEntryPoint = ConvertTask
+
+var ConvertTaskMeta = core.SubTaskMeta{
+ Name: "convertTask",
+ EntryPoint: ConvertTask,
+ EnabledByDefault: true,
+ Description: "convert Zentao task",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertTask(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ db := taskCtx.GetDal()
+ storyIdGen := didgen.NewDomainIdGenerator(&models.ZentaoStory{})
+ boardIdGen := didgen.NewDomainIdGenerator(&models.ZentaoProduct{})
+ taskIdGen := didgen.NewDomainIdGenerator(&models.ZentaoTask{})
+ cursor, err := db.Cursor(
+ dal.From(&models.ZentaoTask{}),
+ dal.Where(`_tool_zentao_tasks.execution = ? and
+ _tool_zentao_tasks.connection_id = ?`, data.Options.ExecutionId, data.Options.ConnectionId),
+ )
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+ convertor, err := helper.NewDataConverter(helper.DataConverterArgs{
+ InputRowType: reflect.TypeOf(models.ZentaoTask{}),
+ Input: cursor,
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_TASK_TABLE,
+ },
+ Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+ toolEntity := inputRow.(*models.ZentaoTask)
+
+ domainEntity := &ticket.Issue{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: taskIdGen.Generate(toolEntity.ConnectionId, toolEntity.ID),
+ },
+ IssueKey: strconv.FormatUint(toolEntity.ID, 10),
+ Title: toolEntity.Name,
+ Description: toolEntity.Description,
+ Type: toolEntity.Type,
+ OriginalStatus: toolEntity.Status,
+ ResolutionDate: toolEntity.ClosedDate,
+ CreatedDate: toolEntity.OpenedDate,
+ UpdatedDate: toolEntity.LastEditedDate,
+ ParentIssueId: storyIdGen.Generate(data.Options.ConnectionId, toolEntity.Parent),
+ Priority: string(rune(toolEntity.Pri)),
+ CreatorId: strconv.FormatUint(toolEntity.OpenedById, 10),
+ CreatorName: toolEntity.OpenedByName,
+ AssigneeId: strconv.FormatUint(toolEntity.AssignedToId, 10),
+ AssigneeName: toolEntity.AssignedToName,
+ }
+ switch toolEntity.Status {
+ case "done", "closed":
+ domainEntity.Status = "DONE"
+ default:
+ domainEntity.Status = "IN_PROGRESS"
+ }
+ if toolEntity.ClosedDate != nil {
+ domainEntity.LeadTimeMinutes = int64(toolEntity.ClosedDate.Sub(*toolEntity.OpenedDate).Minutes())
+ }
+ domainBoardIssue := &ticket.BoardIssue{
+ BoardId: boardIdGen.Generate(data.Options.ConnectionId, data.Options.ProductId),
+ IssueId: domainEntity.Id,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, domainEntity, domainBoardIssue)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return convertor.Execute()
+}
diff --git a/plugins/zentao/tasks/task_data.go b/plugins/zentao/tasks/task_data.go
new file mode 100644
index 000000000..3a3d8ac74
--- /dev/null
+++ b/plugins/zentao/tasks/task_data.go
@@ -0,0 +1,62 @@
+/*
+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 (
+ "fmt"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/mitchellh/mapstructure"
+)
+
+type ZentaoApiParams struct {
+ ConnectionId uint64
+ ProductId uint64
+ ExecutionId uint64
+ ProjectId uint64
+}
+
+type ZentaoOptions struct {
+ // TODO add some custom options here if necessary
+ // options means some custom params required by plugin running.
+ // Such As How many rows do your want
+ // You can use it in sub tasks and you need pass it in main.go and pipelines.
+ ConnectionId uint64 `json:"connectionId"`
+ ProductId uint64
+ ExecutionId uint64
+ ProjectId uint64
+ Tasks []string `json:"tasks,omitempty"`
+ Since string
+}
+
+type ZentaoTaskData struct {
+ Options *ZentaoOptions
+ ApiClient *helper.ApiAsyncClient
+}
+
+func DecodeAndValidateTaskOptions(options map[string]interface{}) (*ZentaoOptions, error) {
+ var op ZentaoOptions
+ err := mapstructure.Decode(options, &op)
+ if err != nil {
+ return nil, err
+ }
+
+ if op.ConnectionId == 0 {
+ return nil, fmt.Errorf("connectionId is invalid")
+ }
+ return &op, nil
+}
diff --git a/plugins/zentao/tasks/task_extractor.go b/plugins/zentao/tasks/task_extractor.go
new file mode 100644
index 000000000..d736bc008
--- /dev/null
+++ b/plugins/zentao/tasks/task_extractor.go
@@ -0,0 +1,139 @@
+/*
+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/errors"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "github.com/apache/incubator-devlake/plugins/zentao/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractTask
+
+var ExtractTaskMeta = core.SubTaskMeta{
+ Name: "extractTask",
+ EntryPoint: ExtractTask,
+ EnabledByDefault: true,
+ Description: "extract Zentao task",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET},
+}
+
+func ExtractTask(taskCtx core.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*ZentaoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: ZentaoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProductId: data.Options.ProductId,
+ ExecutionId: data.Options.ExecutionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_TASK_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ res := &models.ZentaoTaskRes{}
+ err := json.Unmarshal(row.Data, res)
+ if err != nil {
+ return nil, errors.Default.WrapRaw(err)
+ }
+ task := &models.ZentaoTask{
+ ConnectionId: data.Options.ConnectionId,
+ ExecutionId: data.Options.ExecutionId,
+ ID: res.Id,
+ Project: res.Project,
+ Parent: res.Parent,
+ Execution: res.Execution,
+ Module: res.Module,
+ Design: res.Design,
+ Story: res.Story,
+ StoryVersion: res.StoryVersion,
+ DesignVersion: res.DesignVersion,
+ FromBug: res.FromBug,
+ Feedback: res.Feedback,
+ FromIssue: res.FromIssue,
+ Name: res.Name,
+ Type: res.Type,
+ Mode: res.Mode,
+ Pri: res.Pri,
+ Estimate: res.Estimate,
+ Consumed: res.Consumed,
+ Deadline: res.Deadline,
+ Status: res.Status,
+ SubStatus: res.SubStatus,
+ Color: res.Color,
+ Description: res.Description,
+ Version: res.Version,
+ OpenedById: res.OpenedBy.Id,
+ OpenedByName: res.OpenedBy.Realname,
+ OpenedDate: res.OpenedDate,
+ AssignedToId: res.AssignedTo.Id,
+ AssignedToName: res.AssignedTo.Realname,
+ AssignedDate: res.AssignedDate,
+ EstStarted: res.EstStarted,
+ RealStarted: res.RealStarted,
+ FinishedId: res.FinishedBy.Id,
+ FinishedDate: res.FinishedDate,
+ FinishedList: res.FinishedList,
+ CanceledId: res.CanceledBy.Id,
+ CanceledDate: res.CanceledDate,
+ ClosedById: res.ClosedBy.Id,
+ ClosedDate: res.ClosedDate,
+ PlanDuration: res.PlanDuration,
+ RealDuration: res.RealDuration,
+ ClosedReason: res.ClosedReason,
+ LastEditedId: res.LastEditedBy.Id,
+ LastEditedDate: res.LastEditedDate,
+ ActivatedDate: res.ActivatedDate,
+ OrderIn: res.OrderIn,
+ Repo: res.Repo,
+ Mr: res.Mr,
+ Entry: res.Entry,
+ Lines: res.Lines,
+ V1: res.V1,
+ V2: res.V2,
+ Deleted: res.Deleted,
+ Vision: res.Vision,
+ StoryID: res.Story,
+ StoryTitle: res.StoryTitle,
+ Branch: 0,
+ LatestStoryVersion: 0,
+ //Product: res.Product.Id,
+ //Branch: res.Branch,
+ //LatestStoryVersion: res.LatestStoryVersion,
+ //StoryStatus: res.StoryStatus,
+ AssignedToRealName: res.AssignedToRealName,
+ PriOrder: res.PriOrder,
+ NeedConfirm: res.NeedConfirm,
+ //ProductType: res.ProductType,
+ Progress: res.Progress,
+ }
+ results := make([]interface{}, 0)
+ results = append(results, task)
+ return results, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
diff --git a/plugins/zentao/zentao.go b/plugins/zentao/zentao.go
new file mode 100644
index 000000000..11d73050a
--- /dev/null
+++ b/plugins/zentao/zentao.go
@@ -0,0 +1,47 @@
+/*
+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 main
+
+import (
+ "github.com/apache/incubator-devlake/plugins/zentao/impl"
+ "github.com/apache/incubator-devlake/runner"
+ "github.com/spf13/cobra"
+)
+
+// PluginEntry Export a variable named PluginEntry for Framework to search and load
+var PluginEntry impl.Zentao //nolint
+
+// standalone mode for debugging
+func main() {
+ cmd := &cobra.Command{Use: "zentao"}
+
+ connectionId := cmd.Flags().Uint64P("connectionId", "c", 0, "zentao connection id")
+ executionId := cmd.Flags().IntP("executionId", "e", 8, "execution id")
+ productId := cmd.Flags().IntP("productId", "o", 8, "product id")
+ projectId := cmd.Flags().IntP("projectId", "p", 8, "project id")
+
+ cmd.Run = func(cmd *cobra.Command, args []string) {
+ runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
+ "connectionId": *connectionId,
+ "executionId": *executionId,
+ "productId": *productId,
+ "projectId": *projectId,
+ })
+ }
+ runner.RunCmd(cmd)
+}