diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000..319fd640ef
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,13 @@
+sonarqube:
+ variables:
+ SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
+ GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
+ cache:
+ key: "${CI_JOB_NAME}"
+ paths:
+ - .sonar/cache
+ script:
+ - sonar-scanner -Dsonar.inclusions=$(git diff --name-only HEAD~1|tr '\n' ',')
+ allow_failure: true
+ only:
+ - master # or the name of your main branch
diff --git a/db/update16.5.sql b/db/update16.5.sql
index bb437efddd..37e492cc7d 100644
--- a/db/update16.5.sql
+++ b/db/update16.5.sql
@@ -89,3 +89,326 @@ REPLACE INTO `zt_zoutput` (`id`, `activity`, `name`, `content`, `optional`, `sta
DELETE FROM `zt_config` WHERE `section` = 'customMenu';
UPDATE `zt_story` SET `plan` = '' WHERE `plan` = 0;
+
+UPDATE `zt_workflowfield` set `control` = 'datetime' WHERE `module` = 'testcase' and `field` IN ('openedDate','reviewedDate','lastEditedDate','lastRunDate');
+
+UPDATE `zt_workflowfield` set `control` = 'date' WHERE `module` = 'testtask' and `field` IN ('begin','end');
+UPDATE `zt_workflowfield` set `control` = 'richtext' WHERE `module` = 'testtask' and `field` = 'desc';
+
+UPDATE `zt_workflowfield` set `control` = 'datetime' WHERE `module` IN ('testsuite','caselib') and `field` IN ('addedDate','lastEditedDate');
+UPDATE `zt_workflowfield` set `control` = 'richtext' WHERE `module` = 'testsuite' and `field` = 'desc';
+
+UPDATE `zt_workflowfield` set `control` = 'datetime' WHERE `module` = 'feedback' and `field` IN ('openedDate','reviewedDate','processedDate','closedDate','editedDate','assignedDate');
+
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'product' and `field` = 'createdDate';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'productplan' and `field` = 'begin';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'productplan' and `field` = 'end';
+UPDATE `zt_workflowfield` SET `control` = 'radio' WHERE `module` = 'productplan' and `field` = 'deleted';
+UPDATE `zt_workflowfield` SET `control` = 'select', `options` = 12 WHERE `module` = 'productplan' and `field` = 'parent';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'release' and `field` = 'date';
+UPDATE `zt_workflowfield` SET `control` = 'richtext' WHERE `module` = 'release' and `field` = 'desc';
+UPDATE `zt_workflowfield` SET `control` = 'radio' WHERE `module` = 'release' and `field` = 'deleted';
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'story' and `field` = 'assignedDate';
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'story' and `field` = 'lastEditedDate';
+UPDATE `zt_workflowfield` SET `control` = 'date' WHERE `module` = 'story' and `field` = 'reviewedDate';
+UPDATE `zt_workflowfield` SET `control` = 'datetime' WHERE `module` = 'story' and `field` = 'closedDate';
+UPDATE `zt_workflowfield` SET `control` = 'radio' WHERE `module` = 'story' and `field` = 'deleted';
+
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'product' and `action` = 'all' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch', `position` = 'browse' WHERE `module` = 'product' and `action` = 'batchedit' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'product' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `show` = 'direct' WHERE `module` = 'product' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch', `position` = 'browse' WHERE `module` = 'productplan' and `action` = 'batchedit' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'productplan' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `show` = 'direct' WHERE `module` = 'productplan' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'release' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `show` = 'direct' WHERE `module` = 'release' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch' WHERE `module` = 'story' and `action` = 'batchcreate' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `type` = 'batch' WHERE `module` = 'story' and `action` = 'batchedit' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'story' and `action` = 'browse' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'story' and `action` = 'exporttemplate' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'browse' WHERE `module` = 'story' and `action` = 'import' and `vision` = 'rnd';
+UPDATE `zt_workflowaction` SET `position` = 'view' WHERE `module` = 'story' and `action` = 'view' and `vision` = 'rnd';
+DELETE FROM `zt_workflowfield` WHERE `module`='execution' AND `field`='stage';
+DELETE FROM `zt_workflowfield` WHERE `module`='program' AND `field`='stage';
+DELETE FROM `zt_workflowfield` WHERE `module`='project' AND `field`='stage';
+DELETE FROM `zt_workflowfield` WHERE `module`='bug' AND `field`='feedback';
+DELETE FROM `zt_workflowfield` WHERE `module`='task' AND `field`='feedback';
+
+ALTER TABLE `zt_workflowdatasource` ADD UNIQUE `code` (`code`);
+
+REPLACE INTO `zt_workflowdatasource` (`type`, `name`, `code`, `buildin`, `datasource`, `view`, `keyField`, `valueField`) VALUES
+('lang', '项目模型', 'projectModel', '1', 'projectModel', '', '', ''),
+('lang', '反馈类型', 'feedbackType', '1', 'feedbackType', '', '', ''),
+('lang', '反馈处理方案', 'feedbackSolution', '1', 'feedbackSolution', '', '', '');
+
+REPLACE INTO `zt_workflowfield` (`module`, `field`, `type`, `length`, `name`, `control`, `expression`, `options`, `default`, `rules`, `placeholder`, `order`, `searchOrder`, `exportOrder`, `canExport`, `canSearch`, `isValue`, `readonly`, `buildin`, `desc`, `createdBy`, `createdDate`, `editedBy`, `editedDate`) VALUES
+('execution', 'type', 'char', '30', '迭代类型', 'select', '', '16', 'sprint', '', '', 3, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'begin', 'date', '', '计划开始', 'date', '', '', '', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'end', 'date', '', '计划完成', 'date', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'days', 'smallint', '5', '可用工作日', 'integer', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'pri', 'enum', '', '优先级', 'select', '', '[1,2,3,4]', '1', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'desc', 'text', '', '迭代描述', 'richtext', '', '', '', '', '', 21, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'canceledDate', 'datetime', '', '取消日期', 'datetime', '', '', '', '', '', 34, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'acl', 'char', '30', '访问控制', 'select', '', '18', 'open', '', '', 41, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'whitelist', 'text', '', '白名单', 'multi-select', '', 'user', '', '', '', 42, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'type', 'char', '30', '类型', 'select', '', '16', 'sprint', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'begin', 'date', '', '计划开始', 'date', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'end', 'date', '', '计划完成', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'days', 'smallint', '5', '可用工作日', 'integer', '', '', '', '', '', 19, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'pri', 'enum', '', '优先级', 'select', '', '[1,2,3,4]', '1', '', '', 21, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'desc', 'text', '', '项目集描述', 'richtext', '', '', '', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 33, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'canceledDate', 'datetime', '', '取消日期', 'datetime', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'whitelist', 'text', '', '白名单', 'multi-select', '', 'user', '', '', '', 43, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'type', 'char', '30', '项目类型', 'select', '', '16', 'sprint', '', '', 3, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'model', 'char', '30', '项目管理方式', 'input', '', '45', '', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'name', 'varchar', '90', '项目名称', 'input', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'begin', 'date', '', '计划开始', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'end', 'date', '', '计划完成', 'date', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'days', 'smallint', '5', '可用工作日', 'integer', '', '', '', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'pri', 'enum', '', '优先级', 'select', '', '[1,2,3,4]', '1', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'desc', 'text', '', '项目描述', 'richtext', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 34, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'canceledDate', 'datetime', '', '取消日期', 'datetime', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'acl', 'char', '30', '访问控制', 'select', '', '18', 'open', '', '', 43, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'whitelist', 'text', '', '项目白名单', 'multi-select', '', 'user', '', '', '', 44, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 49, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'project', 'mediumint', '8', '所属项目', 'select', '', '2', '0', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'date', 'date', '', '打包日期', 'date', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'desc', 'text', '', '描述', 'richtext', '', '', '', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('build', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'project', 'mediumint', '8', '所属项目', 'select', '', '2', '0', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'storyVersion', 'smallint', '6', '研发需求版本', 'integer', '', '', '1', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'toTask', 'mediumint', '8', '转任务', 'select', '', '5', '0', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'toStory', 'mediumint', '8', '转研发需求', 'select', '', '4', '0', '', '', 12, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'steps', 'mediumtext', '', '重现步骤', 'richtext', '', '', '', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'activatedDate', 'datetime', '', '激活日期', 'datetime', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'openedBuild', 'varchar', '255', '影响版本', 'multi-select', '', '10', '', '', '', 33, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'assignedDate', 'datetime', '', '指派日期', 'datetime', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'deadline', 'date', '', '截止日期', 'date', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'resolvedDate', 'datetime', '30', '解决日期', 'datetime', '', '', '', '', '', 40, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'closedDate', 'datetime', '', '关闭日期', 'datetime', '', '', '', '', '', 42, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'case', 'mediumint', '8', '相关用例', 'select', '', '41', '', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'caseVersion', 'smallint', '6', '用例版本', 'integer', '', '', '', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'lastEditedDate', 'datetime', '30', '修改日期', 'datetime', '', '', '', '', '', 58, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'deleted', 'enum', '', '已删除', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 59, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'project', 'mediumint', '8', '所属项目', 'select', '', '2', '0', '', '', 2, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'storyVersion', 'smallint', '6', '研发需求版本', 'integer', '', '', '1', '', '', 7, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'estimate', 'float', '', '最初预计', 'decimal', '', '', '', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'consumed', 'float', '', '总计消耗', 'decimal', '', '', '', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'left', 'float', '', '预计剩余', 'decimal', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'deadline', 'date', '', '截止日期', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'desc', 'text', '', '任务描述', 'richtext', '', '', '', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'openedDate', 'datetime', '', '创建日期', 'datetime', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'assignedDate', 'datetime', '30', '指派日期', 'datetime', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'estStarted', 'date', '', '预计开始', 'date', '', '', '', '', '', 26, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'realStarted', 'date', '', '实际开始', 'date', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'finishedDate', 'datetime', '', '实际完成', 'datetime', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'canceledDate', 'datetime', '', '取消时间', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'closedDate', 'datetime', '', '关闭时间', 'datetime', '', '', '', '', '', 34, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'lastEditedDate', 'datetime', '', '最后修改日期', 'datetime', '', '', '', '', '', 39, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'deleted', 'enum', '', '已删除', 'select', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'lifetime', 'char', '30', 'lifetime', 'input', '', '', '', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'lifetime', 'char', '30', 'lifetime', 'input', '', '', '', '', '', 3, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'designVersion', 'smallint', '6', '设计版本', 'integer', '', '', '', '', '', 8, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'attribute', 'varchar', '30', 'attribute', 'input', '', '', '0', '', '', 5, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'percent', 'float', '', 'percent', 'input', '', '', '0', '', '', 6, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'milestone', 'enum', '', 'milestone', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 7, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'output', 'text', '', 'output', 'textarea', '', '', '', '', '', 8, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'auth', 'char', '30', 'auth', 'input', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'path', 'varchar', '255', 'path', 'input', '', '', '0', '', '', 10, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'grade', 'tinyint', '3', 'grade', 'integer', '', '', '0', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'realBegan', 'date', '', '实际开始日期', 'date', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'realEnd', 'date', '', '实际完成日期', 'date', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'version', 'smallint', '6', 'version', 'integer', '', '', '', '', '', 22, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'parentVersion', 'smallint', '6', 'parentVersion', 'integer', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'planDuration', 'int', '11', 'planDuration', 'integer', '', '', '', '', '', 24, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'realDuration', 'int', '11', 'realDuration', 'integer', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'openedVersion', 'varchar', '20', 'openedVersion', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'lastEditedBy', 'varchar', '30', 'lastEditedBy', 'select', '', 'user', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'lastEditedDate', 'datetime', '', 'lastEditedDate', 'datetime', '', '', '', '', '', 30, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'suspendedDate', 'date', '', 'suspendedDate', 'date', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'vision', 'varchar', '10', 'vision', 'input', '', '', 'rnd', '', '', 44, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'displayCards', 'enum', '', 'displayCards', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('execution', 'fluidBoard', 'enum', '', 'fluidBoard', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'output', 'text', '', 'output', 'textarea', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'auth', 'char', '30', 'auth', 'input', '', '', '', '', '', 10, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'path', 'varchar', '255', 'path', 'input', '', '', '0', '', '', 12, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'grade', 'tinyint', '3', 'grade', 'integer', '', '', '0', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'realBegan', 'date', '', 'realBegan', 'date', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'realEnd', 'date', '', 'realEnd', 'date', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'version', 'smallint', '6', 'version', 'integer', '', '', '', '', '', 23, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'parentVersion', 'smallint', '6', 'parentVersion', 'integer', '', '', '', '', '', 24, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'planDuration', 'int', '11', 'planDuration', 'integer', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'realDuration', 'int', '11', 'realDuration', 'integer', '', '', '', '', '', 26, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'openedVersion', 'varchar', '20', 'openedVersion', 'input', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'lastEditedBy', 'varchar', '30', 'lastEditedBy', 'select', '', 'user', '', '', '', 30, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'lastEditedDate', 'datetime', '', 'lastEditedDate', 'datetime', '', '', '', '', '', 31, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'suspendedDate', 'date', '', 'suspendedDate', 'date', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'vision', 'varchar', '10', 'vision', 'input', '', '', 'rnd', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'displayCards', 'enum', '', 'displayCards', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('program', 'fluidBoard', 'enum', '', 'fluidBoard', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'lifetime', 'char', '30', '项目周期', 'input', '', '', '', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'attribute', 'varchar', '30', '阶段类型', 'input', '', '', '0', '', '', 7, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'percent', 'float', '', '工作量占比', 'input', '', '', '0', '', '', 8, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'milestone', 'enum', '', '里程碑', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'output', 'text', '', '输出', 'textarea', '', '', '', '', '', 10, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'auth', 'char', '30', '权限控制', 'input', '', '', '', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'path', 'varchar', '255', '路径', 'input', '', '', '0', '', '', 13, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'grade', 'tinyint', '3', '层级', 'integer', '', '', '0', '', '', 14, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'realBegan', 'date', '', '实际开始日期', 'date', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'realEnd', 'date', '', '实际完成日期', 'date', '', '', '', '', '', 19, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'version', 'smallint', '6', '版本', 'integer', '', '', '', '', '', 24, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'parentVersion', 'smallint', '6', '父版本', 'integer', '', '', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'planDuration', 'int', '11', '计划周期天数', 'integer', '', '', '', '', '', 26, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'realDuration', 'int', '11', '实际周期天数', 'integer', '', '', '', '', '', 27, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'openedVersion', 'varchar', '20', '创建版本', 'input', '', '', '', '', '', 30, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'lastEditedBy', 'varchar', '30', '最后编辑人', 'select', '', 'user', '', '', '', 31, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'lastEditedDate', 'datetime', '', '最后编辑日期', 'datetime', '', '', '', '', '', 32, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'suspendedDate', 'date', '', 'suspendedDate', 'date', '', '', '', '', '', 37, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'vision', 'varchar', '10', 'vision', 'input', '', '', 'rnd', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'displayCards', 'enum', '', 'displayCards', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('project', 'fluidBoard', 'enum', '', 'fluidBoard', 'radio', '', '[\"\\u672a\\u5220\\u9664\",\"\\u5df2\\u5220\\u9664\"]', '0', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'hardware', 'varchar', '30', '硬件', 'input', '', '', '', '', '', 20, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'feedbackBy', 'varchar', '100', '反馈者', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'notifyEmail', 'varchar', '100', '通知邮箱', 'input', '', '', '', '', '', 29, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'result', 'mediumint', '8', '结果', 'input', '', '', '', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'repo', 'mediumint', '8', '所属版本库', 'input', '', '', '', '', '', 48, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'mr', 'mediumint', '8', 'mr', 'input', '', '', '', '', '', 49, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'entry', 'text', '', 'entry', 'input', '', '', '', '', '', 50, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'lines', 'varchar', '10', '代码行', 'input', '', '', '', '', '', 51, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'v1', 'varchar', '40', '版本1', 'input', '', '', '', '', '', 52, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'v2', 'varchar', '40', '版本2', 'input', '', '', '', '', '', 53, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'repoType', 'varchar', '30', '版本库类型', 'input', '', '', '', '', '', 54, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('bug', 'issueKey', 'varchar', '50', 'issueKey', 'input', '', '', '', '', '', 55, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'version', 'smallint', '6', '版本', 'integer', '', '', '', '', '', 21, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'planDuration', 'int', '11', '计划持续天数', 'integer', '', '', '', '', '', 35, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'realDuration', 'int', '11', '实际持续天数', 'integer', '', '', '', '', '', 36, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'activatedDate', 'datetime', '', '激活日期', 'datetime', '', '', '', '', '', 40, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'repo', 'mediumint', '8', 'repo', 'input', '', '', '', '', '', 41, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'mr', 'mediumint', '8', 'mr', 'input', '', '', '', '', '', 42, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'entry', 'text', '', 'entry', 'input', '', '', '', '', '', 43, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'lines', 'varchar', '10', 'lines', 'input', '', '', '', '', '', 44, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'v1', 'varchar', '40', 'v1', 'input', '', '', '', '', '', 45, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'v2', 'varchar', '40', 'v2', 'input', '', '', '', '', '', 46, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('task', 'vision', 'varchar', '10', 'vision', 'input', '', '', '', '', '', 47, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'type', 'char', '30', '反馈类型', 'select', '', '', '', '', '', 4, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'notifyEmail', 'varchar', '100', '通知邮箱', 'input', '', '', '', '', '', 9, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'faq', 'mediumint', '8', 'FAQ', 'input', '', '', '', '', '', 11, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'feedbackBy', 'varchar', '100', '反馈者', 'select', '', 'user', '', '', '', 25, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('feedback', 'solution', 'char', '30', '反馈处理方案', 'select', '', '', '', '', '', 5, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'auto', 'varchar', '10', '自动化测试用例', 'input', '', '', '', '', '', 15, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'frame', 'varchar', '10', '自动化测试框架', 'input', '', '', '', '', '', 16, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'howRun', 'char', '30', '测试方式', 'input', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptedBy', 'char', '30', '脚本由谁创建', 'select', '', 'user', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptedDate', 'char', '30', '脚本创建日期', 'date', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptStatus', 'char', '30', '脚本状态', 'input', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'scriptLocation', 'char', '30', '脚本地址', 'input', '', '', '', '', '', 18, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'frequency', 'char', '30', '使用频率', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'fromCaseID', 'char', '30', '用例来源ID', 'input', '', '', '', '', '', 28, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'fromCaseVersion', 'char', '30', '用例来源版本', 'input', '', '', '', '', '', 5, 0, 0, '0', '0', '0', '1', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00'),
+('testcase', 'subStatus', 'varchar', '30', '子状态', 'select', '', '', '', '', '', 17, 0, 0, '0', '0', '0', '0', 1, '', '', '0000-00-00 00:00:00', '', '0000-00-00 00:00:00');
+UPDATE `zt_workflowfield` SET `options`=(SELECT id FROM `zt_workflowdatasource` WHERE `code`='projectModel' LIMIT 1) WHERE `module`='project' AND `field`='model';
+UPDATE `zt_workflowfield` SET `options`=(SELECT id FROM `zt_workflowdatasource` WHERE `code`='feedbackType' LIMIT 1) WHERE `module`='feedback' AND `field`='type';
+UPDATE `zt_workflowfield` SET `options`=(SELECT id FROM `zt_workflowdatasource` WHERE `code`='feedbackSolution' LIMIT 1) WHERE `module`='feedback' AND `field`='solution';
+
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='project' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='none', `layout`='normal' WHERE `module`='project' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='suspend';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='task';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='execution' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='putoff';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='execution' AND `action`='suspend';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='execution' AND `action`='all';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchcreate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='exporttemplate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='import';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='showimport';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='assignTo';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='confirmStoryChange';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='finish';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='pause';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='restart';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='cancel';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='build' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='build' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='batchcreate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='bug' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='exporttemplate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='import';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='showimport';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='bug' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='bug' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='bug' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='assignTo';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='confirmBug';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='bug' AND `action`='resolve';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='bug' AND `action`='batchconfirm';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='bug' AND `action`='batchresolve';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='none', `layout`='normal' WHERE `module`='project' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='project' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='dropdownlist', `open`='modal', `layout`='normal' WHERE `module`='project' AND `action`='suspend';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='project' AND `action`='view';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='activate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='assignTo';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchcreate';
+UPDATE `zt_workflowaction` SET `type`='batch', `position`='browse', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='batchedit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='browse';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='cancel';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='close';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='confirmStoryChange';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='normal' WHERE `module`='task' AND `action`='create';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='none', `layout`='normal' WHERE `module`='task' AND `action`='delete';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='edit';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='exporttemplate';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='finish';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='import';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='view', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='pause';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='restart';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='showimport';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browseandview', `show`='direct', `open`='modal', `layout`='normal' WHERE `module`='task' AND `action`='start';
+UPDATE `zt_workflowaction` SET `type`='single', `position`='browse', `show`='direct', `open`='normal', `layout`='side' WHERE `module`='task' AND `action`='view';
diff --git a/framework/base/control.class.php b/framework/base/control.class.php
index c4d051fadb..1576843b29 100644
--- a/framework/base/control.class.php
+++ b/framework/base/control.class.php
@@ -416,7 +416,7 @@ class baseControl
$moduleName = basename(dirname(dirname(realpath($viewFile))));
$extPath = $this->app->getModuleExtPath('', $moduleName, 'view');
- $checkedOrder = array('site', 'custom', 'vision', 'xuan', 'common');
+ $checkedOrder = array('site', 'saas', 'custom', 'vision', 'xuan', 'common');
$fileName = basename($viewFile);
foreach($checkedOrder as $checkedType)
{
@@ -805,6 +805,12 @@ class baseControl
$commonActionExtFile = $actionExtPath['custom'] . strtolower($methodName) . '.php';
if(file_exists($commonActionExtFile)) $file2Included = $commonActionExtFile;
+ if(!empty($actionExtPath['saas']))
+ {
+ $commonActionExtFile = $actionExtPath['saas'] . strtolower($methodName) . '.php';
+ if(file_exists($commonActionExtFile)) $file2Included = $commonActionExtFile;
+ }
+
if(!empty($actionExtPath['site']))
{
/**
diff --git a/framework/base/model.class.php b/framework/base/model.class.php
index 0dbb3372be..519def4741 100644
--- a/framework/base/model.class.php
+++ b/framework/base/model.class.php
@@ -260,6 +260,7 @@ class baseModel
$moduleExtPath = $this->app->getModuleExtPath($this->appName, $moduleName, 'model');
if(!empty($moduleExtPath['site'])) $extensionFile = $moduleExtPath['site'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['custom'] . 'class/' . $extensionName . '.class.php';
+ if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['saas'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['vision'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['xuan'] . 'class/' . $extensionName . '.class.php';
if(!isset($extensionFile) or !file_exists($extensionFile)) $extensionFile = $moduleExtPath['common'] . 'class/' . $extensionName . '.class.php';
diff --git a/framework/base/router.class.php b/framework/base/router.class.php
index 1216e2f4a6..1a44b0585d 100644
--- a/framework/base/router.class.php
+++ b/framework/base/router.class.php
@@ -1390,6 +1390,9 @@ class baseRouter
if($this->checkModuleName($moduleName))
{
+ $modulePath = $this->getExtensionRoot() . 'saas' . DS . $moduleName . DS;
+ if(is_dir($modulePath) and (file_exists($modulePath . 'control.php') or file_exists($modulePath . 'model.php'))) return $modulePath;
+
/* 1. 最后尝试在定制开发中寻找。 Finally, try to find the module in the custom dir. */
$modulePath = $this->getExtensionRoot() . 'custom' . DS . $moduleName . DS;
if(is_dir($modulePath) and (file_exists($modulePath . 'control.php') or file_exists($modulePath . 'model.php'))) return $modulePath;
@@ -1432,8 +1435,10 @@ class baseRouter
*/
public function getModuleExtPath($appName, $moduleName, $ext)
{
+ $saasExtPath = $this->getExtensionRoot() . 'saas' . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
+
/* 检查失败或者extensionLevel为0,直接返回空。If check failed or extensionLevel == 0, return empty array. */
- if(!$this->checkModuleName($moduleName) or $this->config->framework->extensionLevel == 0) return array();
+ if(!$this->checkModuleName($moduleName) or $this->config->framework->extensionLevel == 0) return array('saas' => $saasExtPath);
$paths = array();
@@ -1442,10 +1447,16 @@ class baseRouter
$paths['xuan'] = $this->getExtensionRoot() . 'xuan' . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
$paths['vision'] = $this->config->vision == 'rnd' ? '' : $this->basePath . 'extension' . DS . $this->config->vision . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
$paths['custom'] = $this->getExtensionRoot() . 'custom' . DS . $moduleName . DS . 'ext' . DS . $ext . DS;
- if($this->config->framework->extensionLevel == 1) return $paths;
+ if($this->config->framework->extensionLevel == 1)
+ {
+ $paths['saas'] = $saasExtPath;
+ return $paths;
+ }
/* When extensionLevel == 2. */
$paths['site'] = empty($this->siteCode) ? '' : $this->getExtensionRoot() . $this->config->edition . DS . $moduleName . DS . 'ext' . DS . '_' . $this->siteCode . DS . $ext . DS;
+ $paths['saas'] = $saasExtPath;
+
return $paths;
}
@@ -1494,6 +1505,12 @@ class baseRouter
/* 如果扩展目录为空,不包含任何扩展文件。If there's no ext paths return false.*/
if(empty($moduleExtPaths)) return false;
+ if(!empty( $moduleExtPaths['saas']))
+ {
+ $this->extActionFile = $moduleExtPaths['saas'] . $this->methodName . '.php';
+ if(file_exists($this->extActionFile)) return true;
+ }
+
/* 1. 如果extensionLevel == 2,且扩展文件存在,返回该站点扩展文件。 If extensionLevel == 2 and site extensionFile exists, return it. */
if($this->config->framework->extensionLevel == 2 and !empty( $moduleExtPaths['site']))
{
@@ -2322,6 +2339,7 @@ class baseRouter
if(!empty($extConfigPath['common'])) $commonExtConfigFiles = helper::ls($extConfigPath['common'], '.php');
if(!empty($extConfigPath['xuan'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['xuan'], '.php'));
if(!empty($extConfigPath['vision'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['vision'], '.php'));
+ if(!empty($extConfigPath['saas'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['saas'], '.php'));
if(!empty($extConfigPath['custom'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['custom'], '.php'));
}
if($config->framework->extensionLevel == 2 and !empty($extConfigPath['site'])) $siteExtConfigFiles = helper::ls($extConfigPath['site'], '.php');
@@ -2427,6 +2445,9 @@ class baseRouter
{
$path = $moduleName . DS . 'lang' . DS . $this->clientLang . '.php';
+ $modulePath = $this->getExtensionRoot() . 'saas' . DS;
+ if(file_exists($modulePath . $path)) return $modulePath . $path;
+
/* 1. 如果通用版本里有此模块,优先使用。 If module is in the open edition, use it. */
$modulePath = $this->getModuleRoot($appName);
if(file_exists($modulePath . $path)) return $modulePath . $path;
@@ -2489,6 +2510,7 @@ class baseRouter
if(!empty($extLangPath['xuan'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['xuan'] . $this->clientLang, '.php'));
if(!empty($extLangPath['vision'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['vision'] . $this->clientLang, '.php'));
if(!empty($extLangPath['custom'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['custom'] . $this->clientLang, '.php'));
+ if(!empty($extLangPath['saas'])) $commonExtLangFiles = array_merge($commonExtLangFiles, helper::ls($extLangPath['saas'] . $this->clientLang, '.php'));
}
if($this->config->framework->extensionLevel == 2 and !empty($extLangPath['site'])) $siteExtLangFiles = helper::ls($extLangPath['site'] . $this->clientLang, '.php');
$extLangFiles = array_merge($commonExtLangFiles, $siteExtLangFiles);
diff --git a/framework/control.class.php b/framework/control.class.php
index c7666ca0db..326c9f9c20 100644
--- a/framework/control.class.php
+++ b/framework/control.class.php
@@ -86,7 +86,7 @@ class control extends baseControl
/**
* Det default priv by workflow.
- *
+ *
* @access public
* @return bool
*/
@@ -197,6 +197,7 @@ class control extends baseControl
$commonExtViewFile = $viewExtPath['common'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$xuanExtViewFile = $viewExtPath['xuan'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$visionExtViewFile = $viewExtPath['vision'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
+ $saasExtViewFile = $viewExtPath['saas'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$customExtViewFile = $viewExtPath['custom'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
$siteExtViewFile = empty($viewExtPath['site']) ? '' : $viewExtPath['site'] . $this->devicePrefix . $methodName . ".{$viewType}.php";
@@ -217,6 +218,10 @@ class control extends baseControl
{
$viewFile = $xuanExtViewFile;
}
+ else if(file_exists($saasExtViewFile))
+ {
+ $viewFile = $saasExtViewFile;
+ }
else if(file_exists($commonExtViewFile))
{
$viewFile = $commonExtViewFile;
@@ -235,10 +240,11 @@ class control extends baseControl
$commonExtHookFiles = glob($viewExtPath['vision'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
}
$xuanExtHookFiles = glob($viewExtPath['xuan'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
+ $saasExtHookFiles = glob($viewExtPath['saas'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
$customExtHookFiles = glob($viewExtPath['custom'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
$siteExtHookFiles = empty($viewExtPath['site']) ? '' : glob($viewExtPath['site'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php");
- $extHookFiles = array_merge((array)$commonExtHookFiles, (array)$xuanExtHookFiles, (array)$customExtHookFiles, (array)$siteExtHookFiles);
+ $extHookFiles = array_merge((array)$commonExtHookFiles, (array)$xuanExtHookFiles, (array)$saasExtHookFiles, (array)$customExtHookFiles, (array)$siteExtHookFiles);
}
if(!empty($extHookFiles)) return array('viewFile' => $viewFile, 'hookFiles' => $extHookFiles);
@@ -320,7 +326,7 @@ class control extends baseControl
if(!isset($this->config->bizVersion)) return false;
$moduleName = $this->moduleName;
- if($moduleName == 'bug' || $moduleName == 'feedback') return $this->$moduleName->buildOperateMenu($object, $type);
+ if(strpos(',bug,feedback,caselib,testsuite,testtask,testcase,product,productplan,', ",{$moduleName},") !== false) return $this->$moduleName->buildOperateMenu($object, $type);
$flow = $this->loadModel('workflow')->getByModule($moduleName);
return $this->loadModel('flow')->buildOperateMenu($flow, $object, $type);
@@ -342,9 +348,9 @@ class control extends baseControl
}
/**
- * Set workflow export fields
- *
- * @param array $fields
+ * Set workflow export fields
+ *
+ * @param array $fields
* @access public
* @return array
*/
diff --git a/framework/model.class.php b/framework/model.class.php
index 267fbc1edf..be9480d01f 100644
--- a/framework/model.class.php
+++ b/framework/model.class.php
@@ -83,7 +83,15 @@ class model extends baseModel
if(isset($this->config->bizVersion))
{
static $actions;
- if(empty($actions)) $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)->where('module')->eq($moduleName)->andWhere('buildin')->eq('1')->andWhere('status')->eq('enable')->fetchAll('action');
+ if(empty($actions))
+ {
+ $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)
+ ->where('module')->eq($moduleName)
+ ->andWhere('buildin')->eq('1')
+ ->andWhere('status')->eq('enable')
+ ->beginIF(!empty($this->config->vision))->andWhere('vision')->eq($this->config->vision)->fi()
+ ->fetchAll('action');
+ }
}
$enabled = true;
@@ -133,7 +141,16 @@ class model extends baseModel
if(strpos($module, '.') !== false) list($appName, $moduleName) = explode('.', $module);
static $actions;
- if(empty($actions)) $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)->where('module')->eq($moduleName)->andWhere('buildin')->eq('0')->andWhere('status')->eq('enable')->orderBy('order_asc')->fetchAll();
+ if(empty($actions))
+ {
+ $actions = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)
+ ->where('module')->eq($moduleName)
+ ->andWhere('buildin')->eq('0')
+ ->andWhere('status')->eq('enable')
+ ->beginIF(!empty($this->config->vision))->andWhere('vision')->eq($this->config->vision)->fi()
+ ->orderBy('order_asc')
+ ->fetchAll();
+ }
$menu = '';
if($show)
diff --git a/framework/router.class.php b/framework/router.class.php
index 500761517b..3fb5136cfa 100755
--- a/framework/router.class.php
+++ b/framework/router.class.php
@@ -327,6 +327,7 @@ class router extends baseRouter
if(!empty($extConfigPath['common'])) $commonExtConfigFiles = helper::ls($extConfigPath['common'], '.php');
if(!empty($extConfigPath['xuan'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['xuan'], '.php'));
if(!empty($extConfigPath['vision'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['vision'], '.php'));
+ if(!empty($extConfigPath['saas'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['saas'], '.php'));
if(!empty($extConfigPath['custom'])) $commonExtConfigFiles = array_merge($commonExtConfigFiles, helper::ls($extConfigPath['custom'], '.php'));
}
if($config->framework->extensionLevel == 2 and !empty($extConfigPath['site'])) $siteExtConfigFiles = helper::ls($extConfigPath['site'], '.php');
diff --git a/module/action/config.php b/module/action/config.php
index caa2de9145..b5c5f0c8c0 100755
--- a/module/action/config.php
+++ b/module/action/config.php
@@ -60,3 +60,9 @@ $config->action->majorList['execution'] = array('opened', 'edited');
$config->action->needGetProjectType = 'build,task,bug,case,testcase,caselib,testtask,testsuite,testreport,doc,issue,release,risk,design,opportunity,trainplan,gapanalysis,researchplan,researchreport,';
$config->action->needGetRelateField = ',story,productplan,release,task,build,bug,testcase,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,';
$config->action->noLinkModules = ',doclib,module,webhook,gitlab,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,';
+
+$config->action->preferredTypeNum = 10;
+
+$config->action->preferredType = new stdclass();
+$config->action->preferredType->new = array('user', 'story', 'task', 'bug', 'case', 'doc', 'program', 'product', 'project', 'execution');
+$config->action->preferredType->classic = array('user', 'story', 'task', 'bug', 'case', 'doc', 'product', 'execution', 'productplan', 'build');
diff --git a/module/action/control.php b/module/action/control.php
index 7a143d26ba..1057f4fc8a 100755
--- a/module/action/control.php
+++ b/module/action/control.php
@@ -14,6 +14,7 @@ class action extends control
/**
* Trash.
*
+ * @param string $browseType
* @param string $type all|hidden
* @param string $orderBy
* @param int $recTotal
@@ -22,7 +23,7 @@ class action extends control
* @access public
* @return void
*/
- public function trash($type = 'all', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
+ public function trash($browseType = 'all', $type = 'all', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
{
$this->loadModel('backup');
@@ -60,18 +61,37 @@ class action extends control
$pager = pager::init($recTotal, $recPerPage, $pageID);
/* Append id for secend sort. */
- $sort = common::appendOrder($orderBy);
- $trashes = $this->action->getTrashes($type, $sort, $pager);
+ $sort = common::appendOrder($orderBy);
+ $trashes = $this->action->getTrashes($browseType, $type, $sort, $pager);
+ $objectTypeList = $this->action->getTrashObjectTypes($type);
+ $objectTypeList = array_keys($objectTypeList);
+
+ $preferredType = array();
+ $moreType = array();
+ $preferredTypeConfig = $this->config->systemMode == 'new' ? $this->config->action->preferredType->new : $this->config->action->preferredType->classic;
+ foreach($objectTypeList as $objectType)
+ {
+ in_array($objectType, $preferredTypeConfig) ? $preferredType[$objectType] = $objectType : $moreType[$objectType] = $objectType;
+ }
+ if(count($preferredType) < $this->config->action->preferredTypeNum)
+ {
+ $toPreferredType = array_splice($moreType, 0, $this->config->action->preferredTypeNum - count($preferredType));
+ $preferredType = $preferredType + $toPreferredType;
+ }
/* Title and position. */
$this->view->title = $this->lang->action->trash;
$this->view->position[] = $this->lang->action->trash;
- $this->view->trashes = $trashes;
- $this->view->type = $type;
- $this->view->orderBy = $orderBy;
- $this->view->pager = $pager;
- $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->trashes = $trashes;
+ $this->view->type = $type;
+ $this->view->currentObjectType = $browseType;
+ $this->view->orderBy = $orderBy;
+ $this->view->pager = $pager;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->preferredType = $preferredType;
+ $this->view->moreType = $moreType;
+ $this->view->preferredTypeConfig = $preferredTypeConfig;
$this->display();
}
diff --git a/module/action/css/trash.css b/module/action/css/trash.css
index afbfc580cc..2f9190111c 100755
--- a/module/action/css/trash.css
+++ b/module/action/css/trash.css
@@ -1,3 +1,7 @@
.table-footer .table-actions {width: auto; visibility: visible; opacity: 1;}
.table-footer .table-actions .text {line-height: 28px;}
table tbody tr td {overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
+
+#mainMenu .btn-toolbar .btn-group .dropdown-menu .btn-active-text:hover .text {color: #fff}
+
+.dropdown-menu .btn-active-text:hover .text:after {border-bottom: 0px;}
diff --git a/module/action/model.php b/module/action/model.php
index bbea80d598..2791f3ee01 100755
--- a/module/action/model.php
+++ b/module/action/model.php
@@ -610,17 +610,19 @@ class actionModel extends model
/**
* Get deleted objects.
*
+ * @param string $objectType
* @param string $type all|hidden
* @param string $orderBy
* @param object $pager
* @access public
* @return array
*/
- public function getTrashes($type, $orderBy, $pager)
+ public function getTrashes($objectType, $type, $orderBy, $pager)
{
- $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
+ $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
$trashes = $this->dao->select('*')->from(TABLE_ACTION)
->where('action')->eq('deleted')
+ ->beginIF($objectType != 'all')->andWhere('objectType')->eq($objectType)->fi()
->andWhere('extra')->eq($extra)
->andWhere('vision')->eq($this->config->vision)
->orderBy($orderBy)->page($pager)->fetchAll();
@@ -665,9 +667,23 @@ class actionModel extends model
$trash->objectName = isset($objectNames[$objectType][$trash->objectID]) ? $objectNames[$objectType][$trash->objectID] : '';
}
+
return $trashes;
}
+ /**
+ * Get object type list of trashes.
+ *
+ * @param string $type
+ * @access public
+ * @return array
+ */
+ public function getTrashObjectTypes($type)
+ {
+ $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
+ return $this->dao->select('objectType')->from(TABLE_ACTION)->where('action')->eq('deleted')->andWhere('extra')->eq($extra)->andWhere('vision')->eq($this->config->vision)->fetchAll('objectType');
+ }
+
/**
* Get histories of an action.
*
diff --git a/module/action/view/trash.html.php b/module/action/view/trash.html.php
index 2651f5754a..fa646c1fc9 100755
--- a/module/action/view/trash.html.php
+++ b/module/action/view/trash.html.php
@@ -12,6 +12,50 @@
?>
+
goback, '', "class='btn'");?>
" . $lang->action->dynamic->hidden, '', "class='btn btn-danger'");?>
diff --git a/module/api/model.php b/module/api/model.php
index 4296aa73d6..0cfde0ab69 100644
--- a/module/api/model.php
+++ b/module/api/model.php
@@ -764,6 +764,7 @@ class apiModel extends model
public function createDemoData($name, $baseUrl, $version = '16.0')
{
/* Replace the doc lib name to api lib name. */
+ $this->app->loadLang('doc');
$this->lang->doclib->name = $this->lang->doclib->apiLibName;
$firstAccount = $this->dao->select('account')->from(TABLE_USER)->orderBy('id_asc')->limit(1)->fetch('account');
diff --git a/module/bug/config.php b/module/bug/config.php
index 23ccd42efb..a2a00b49c6 100644
--- a/module/bug/config.php
+++ b/module/bug/config.php
@@ -71,6 +71,7 @@ $config->bug->search['fields']['resolvedBy'] = $lang->bug->resolvedBy;
$config->bug->search['fields']['status'] = $lang->bug->status;
$config->bug->search['fields']['confirmed'] = $lang->bug->confirmed;
+$config->bug->search['fields']['story'] = $lang->bug->story;
if($config->systemMode == 'new') $config->bug->search['fields']['project'] = $lang->bug->project;
$config->bug->search['fields']['product'] = $lang->bug->product;
@@ -116,6 +117,7 @@ $config->bug->search['params']['resolvedBy'] = array('operator' => '=',
$config->bug->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $lang->bug->statusList);
$config->bug->search['params']['confirmed'] = array('operator' => '=', 'control' => 'select', 'values' => $lang->bug->confirmedList);
+$config->bug->search['params']['story'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
if($config->systemMode == 'new') $config->bug->search['params']['project'] = array('operator' => '=', 'control' => 'select', 'values' => '');
$config->bug->search['params']['product'] = array('operator' => '=', 'control' => 'select', 'values' => '');
diff --git a/module/bug/model.php b/module/bug/model.php
index a534aded89..2f445064f5 100644
--- a/module/bug/model.php
+++ b/module/bug/model.php
@@ -2730,6 +2730,26 @@ class bugModel extends model
/* Fix bug #2878. */
if(strpos($bugQuery, ' `resolvedDate` ') !== false) $bugQuery = str_replace(' `resolvedDate` ', " `resolvedDate` != '0000-00-00 00:00:00' AND `resolvedDate` ", $bugQuery);
if(strpos($bugQuery, ' `closedDate` ') !== false) $bugQuery = str_replace(' `closedDate` ', " `closedDate` != '0000-00-00 00:00:00' AND `closedDate` ", $bugQuery);
+ if(strpos($bugQuery, ' `story` ') !== false)
+ {
+ preg_match_all("/`story`[ ]+(NOT *)?LIKE[ ]+'%([^%]*)%'/Ui", $bugQuery, $out);
+ if(!empty($out[2]))
+ {
+ foreach($out[2] as $searchValue)
+ {
+ $story = $this->dao->select('id')->from(TABLE_STORY)->alias('t1')
+ ->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story')
+ ->where('t1.title')->like("%$searchValue%")
+ ->orWhere('t1.keywords')->like("%$searchValue%")
+ ->orWhere('t2.spec')->like("%$searchValue%")
+ ->orWhere('t2.verify')->like("%$searchValue%")
+ ->fetchPairs('id');
+
+ $bugQuery = preg_replace("/`story`[ ]+(NOT[ ]*)?LIKE[ ]+'%$searchValue%'/Ui", '`story` $1 IN (' . implode($story, ',') .')', $bugQuery);
+ }
+ }
+ $bugQuery .= ' AND `story` != 0';
+ }
$bugs = $this->dao->select('*')->from(TABLE_BUG)->where($bugQuery)
->beginIF(!$this->app->user->admin)->andWhere('execution')->in('0,' . $this->app->user->view->sprints)->fi()
@@ -3234,9 +3254,9 @@ class bugModel extends model
/**
* Build bug menu.
- *
- * @param object $bug
- * @param string $type
+ *
+ * @param object $bug
+ * @param string $type
* @access public
* @return string
*/
diff --git a/module/bug/view/view.html.php b/module/bug/view/view.html.php
index f80245adc1..b7f0629533 100644
--- a/module/bug/view/view.html.php
+++ b/module/bug/view/view.html.php
@@ -96,7 +96,7 @@
deleted):?>
- buildOperateMenu($bug, 'view');?>
+ bug->buildOperateMenu($bug, 'view');?>
diff --git a/module/caselib/lang/zh-cn.php b/module/caselib/lang/zh-cn.php
index 90cc8b069e..114092a784 100644
--- a/module/caselib/lang/zh-cn.php
+++ b/module/caselib/lang/zh-cn.php
@@ -25,7 +25,8 @@ $lang->caselib->exportTemplet = '导出模板';
$lang->caselib->batchCreateCase = '批量创建用例';
$lang->caselib->import = '导入';
$lang->caselib->showImport = '显示导入数据';
-
+$lang->caselib->type = '类型';
+$lang->caselib->product = '所属' . $lang->productCommon;
$lang->caselib->browseAction = '用例库列表';
$lang->caselib->deleteAction = "删除用例库";
$lang->caselib->importAction = '导入用例';
diff --git a/module/caselib/model.php b/module/caselib/model.php
index 12470e3b52..bbdd5e2ee2 100644
--- a/module/caselib/model.php
+++ b/module/caselib/model.php
@@ -595,4 +595,65 @@ class caselibModel extends model
}
}
}
+
+ /**
+ * Build case lib menu.
+ *
+ * @param object $object
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($object, $type = 'view')
+ {
+ $function = 'buildOperate' . ucfirst($type) . 'Menu';
+ return $this->$function($object);
+ }
+
+ /**
+ * Build case lib view menu.
+ *
+ * @param object $lib
+ * @access public
+ * @return string
+ */
+ public function buildOperateViewMenu($lib)
+ {
+ if($lib->deleted) return '';
+
+ $menu = '';
+ $params = "libID=$lib->id";
+ $menu .= $this->buildFlowMenu('caselib', $lib, 'view', 'direct');
+ $menu .= "";
+ $menu .= $this->buildMenu('caselib', 'edit', $params, $lib, 'view');
+ $menu .= $this->buildMenu('caselib', 'delete', $params, $lib, 'view', 'trash', 'hiddenwin');
+
+ return $menu;
+ }
+
+ /**
+ * Build case lib browse menu.
+ *
+ * @param object $case
+ * @access public
+ * @return string
+ */
+ public function buildOperateBrowseMenu($case)
+ {
+ $menu = '';
+ $params = "caseID=$case->id";
+
+ if($this->config->testcase->needReview || !empty($this->config->testcase->forceReview))
+ {
+ $menu .= $this->buildMenu('testcase', 'review', $params, $case, 'browse', 'glasses', '', 'iframe');
+ }
+ $menu .= $this->buildMenu('testcase', 'edit', $params, $case, 'browse');
+ if(common::hasPriv('testcase', 'delete'))
+ {
+ $deleteURL = helper::createLink('testcase', 'delete', "$params&confirm=yes");
+ $menu .= html::a("javascript:ajaxDelete(\"$deleteURL\", \"caseList\", confirmDelete)", '', '', "title='{$this->lang->testcase->delete}' class='btn'");
+ }
+
+ return $menu;
+ }
}
diff --git a/module/caselib/view/browse.html.php b/module/caselib/view/browse.html.php
index 5fc8f8174d..35101bfe14 100644
--- a/module/caselib/view/browse.html.php
+++ b/module/caselib/view/browse.html.php
@@ -157,15 +157,7 @@ js::set('flow', $config->global->flow);
processStatus('testcase', $case);?> |
" . $this->loadModel('flow')->getFieldValue($extendField, $case) . "";?>
- testcase->needReview or !empty($config->testcase->forceReview)) common::printIcon('testcase', 'review', "caseID=$case->id", $case, 'list', 'glasses', '', 'iframe');
- common::printIcon('testcase', 'edit', "caseID=$case->id", $case, 'list');
- if(common::hasPriv('testcase', 'delete'))
- {
- $deleteURL = $this->createLink('testcase', 'delete', "caseID=$case->id&confirm=yes");
- echo html::a("javascript:ajaxDelete(\"$deleteURL\", \"caseList\", confirmDelete)", '', '', "title='{$lang->testcase->delete}' class='btn'");
- }
- ?>
+ buildOperateMenu($case, 'browse');?>
|
diff --git a/module/caselib/view/view.html.php b/module/caselib/view/view.html.php
index 1d3b1dc160..868c67dccc 100644
--- a/module/caselib/view/view.html.php
+++ b/module/caselib/view/view.html.php
@@ -37,15 +37,9 @@
";?>
+ buildOperateMenu($lib, 'view');?>
diff --git a/module/custom/config.php b/module/custom/config.php
index ff9462b546..9b42284642 100644
--- a/module/custom/config.php
+++ b/module/custom/config.php
@@ -48,8 +48,8 @@ $config->custom->fieldList['release']['create'] = 'desc';
$config->custom->fieldList['release']['edit'] = 'desc';
$config->custom->fieldList['execution']['create'] = 'days,desc,PO,PM,QD,RD';
$config->custom->fieldList['execution']['edit'] = 'days,desc,PO,PM,QD,RD';
-$config->custom->fieldList['task']['create'] = 'story,pri,estimate,desc,estStarted,deadline';
-$config->custom->fieldList['task']['edit'] = 'pri,estimate,estStarted,deadline';
+$config->custom->fieldList['task']['create'] = 'module,story,pri,estimate,desc,estStarted,deadline';
+$config->custom->fieldList['task']['edit'] = 'module,pri,estimate,estStarted,deadline';
$config->custom->fieldList['task']['finish'] = 'comment';
$config->custom->fieldList['task']['activate'] = 'assignedTo,comment';
$config->custom->fieldList['build'] = 'scmPath,filePath,desc';
diff --git a/module/custom/model.php b/module/custom/model.php
index aa725021d4..a8ffc88a32 100644
--- a/module/custom/model.php
+++ b/module/custom/model.php
@@ -251,7 +251,9 @@ class customModel extends model
foreach($item['subMenu'] as $subMenu)
{
if(!isset($subMenu['link']) or strpos($subMenu['link'], '|') === false) continue;
- list($subLabel, $module, $method, $vars) = explode('|', $subMenu['link']);
+ list($subLabel, $module, $method) = explode('|', $subMenu['link']);
+ if(count(explode('|', $subMenu['link'])) > 3) list($subLabel, $module, $method, $vars) = explode('|', $subMenu['link']);
+
$hasPriv = commonModel::hasPriv($module, $method);
if($hasPriv) break;
}
diff --git a/module/execution/control.php b/module/execution/control.php
index 25f8e63cb8..be01e4169e 100644
--- a/module/execution/control.php
+++ b/module/execution/control.php
@@ -3148,7 +3148,16 @@ class execution extends control
}
$projectExecutions = array();
- foreach($orderedExecutions as $execution) $projectExecutions[$execution->project][] = $execution;
+ $parentIdList = array();
+ foreach($orderedExecutions as $execution)
+ {
+ $projectExecutions[$execution->project][] = $execution;
+ if($execution->type != 'stage') continue;
+ if($execution->grade == 2 and $execution->project != $execution->parent) $parentIdList[$execution->parent] = $execution->parent;
+ }
+
+ $parents = array();
+ if($parentIdList) $parents = $this->execution->getByIdList($parentIdList);
$this->view->link = $this->execution->getLink($module, $method, $extra);
$this->view->module = $module;
@@ -3157,6 +3166,7 @@ class execution extends control
$this->view->extra = $extra;
$this->view->projects = $projectPairs;
$this->view->executions = $projectExecutions;
+ $this->view->parents = $parents;
$this->display();
}
diff --git a/module/execution/js/kanban.js b/module/execution/js/kanban.js
index ff3a64f56c..b125445210 100644
--- a/module/execution/js/kanban.js
+++ b/module/execution/js/kanban.js
@@ -328,6 +328,10 @@ if(!window.kanbanDropRules)
{
backlog: ['ready', 'backlog'],
ready: ['backlog', 'ready'],
+ tested: ['verified'],
+ verified: ['tested', 'released'],
+ released: ['verified', 'closed'],
+ closed: ['released'],
},
bug:
{
@@ -957,7 +961,12 @@ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, car
/* Story lane. */
if(cardType == 'story')
{
- if(toColType == 'ready' || toColType == 'backlog')
+ if(toColType == 'closed' && priv.canCloseStory)
+ {
+ var link = createLink('story', 'close', 'storyID=' + objectID, '', true);
+ showIframe = true;
+ }
+ else
{
if(toColType == 'ready' && typeof(reviewStoryParis[objectID]) != 'undefined')
{
diff --git a/module/execution/js/taskkanban.js b/module/execution/js/taskkanban.js
index 8df8beb996..56a6cf5af4 100644
--- a/module/execution/js/taskkanban.js
+++ b/module/execution/js/taskkanban.js
@@ -480,6 +480,10 @@ if(!window.kanbanDropRules)
{
backlog: ['ready'],
ready: ['backlog'],
+ tested: ['verified'],
+ verified: ['tested', 'released'],
+ released: ['verified', 'closed'],
+ closed: ['released'],
},
bug:
{
@@ -495,7 +499,7 @@ if(!window.kanbanDropRules)
{
'wait': ['developing', 'developed', 'canceled', 'closed'],
'developing': ['developed', 'pause', 'canceled'],
- 'developed': ['closed'],
+ 'developed': ['developing', 'closed'],
'pause': ['developing'],
'canceled': ['developing'],
'closed': ['developing'],
@@ -683,7 +687,12 @@ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, car
/* Story lane. */
if(cardType == 'story')
{
- if(toColType == 'ready' || toColType == 'backlog')
+ if(toColType == 'closed' && priv.canCloseStory)
+ {
+ var link = createLink('story', 'close', 'storyID=' + objectID, '', true);
+ showIframe = true;
+ }
+ else
{
if(toColType == 'ready' && typeof(reviewStoryParis[objectID]) != 'undefined')
{
@@ -706,7 +715,7 @@ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, car
updateKanban(browseType, kanbanGroup[groupBy]);
}
}
- })
+ });
}
}
diff --git a/module/execution/model.php b/module/execution/model.php
index 4d2e28feba..b3180f1394 100644
--- a/module/execution/model.php
+++ b/module/execution/model.php
@@ -2791,15 +2791,70 @@ class executionModel extends model
public function computeBurn()
{
$today = helper::today();
- $burns = array();
-
$executions = $this->dao->select('id, code')->from(TABLE_EXECUTION)
->where('end')->ge($today)
- ->andWhere('type')->ne('ops')
+ ->andWhere('type')->in('sprint,stage')
+ ->andWhere('lifetime')->ne('ops')
->andWhere('status')->notin('done,closed,suspended')
->fetchPairs();
- if(!$executions) return $burns;
+ if(!$executions) return array();
+ /* Update historical data of burn. */
+ $table = $this->config->edition == 'open' ? TABLE_TASKESTIMATE : TABLE_EFFORT;
+ $field = $this->config->edition == 'open' ? 'task' : 'objectID';
+ $totalConsumed = $this->dao->select("t2.execution as execution, t1.$field as task, t1.date as date, sum(t1.consumed) as totalConsumed")->from($table)->alias('t1')
+ ->leftJoin(TABLE_TASK)->alias('t2')->on("t1.$field=t2.id")
+ ->where('t2.execution')->in(array_keys($executions))
+ ->beginIF($this->config->edition != 'open')->andWhere('t1.objectType')->eq('task')->fi()
+ ->andWhere('t2.deleted')->eq('0')
+ ->andWhere('t2.parent')->ge('0')
+ ->andWhere('t2.status')->ne('cancel')
+ ->andWhere('t1.date')->ne($today)
+ ->groupBy("t1.$field, t1.date")
+ ->orderBy('t1.id_desc')
+ ->fetchGroup('task', 'date');
+
+ $latestIdList = $this->dao->select('max(id) as id')->from($table)
+ ->where($field)->in(array_keys($totalConsumed))
+ ->beginIF($this->config->edition != 'open')->andWhere('objectType')->eq('task')->fi()
+ ->groupBy("$field,date")->fetchAll('id');
+ $latestLefts = $this->dao->select("$field, date, `left`")->from($table)
+ ->where('id')->in(array_keys($latestIdList))
+ ->fetchGroup($field, 'date');
+
+ $burnList = array();
+ foreach($totalConsumed as $taskID => $consumedList)
+ {
+ foreach($consumedList as $date => $consumed)
+ {
+ $executionID = $consumed->execution;
+ if(!isset($burnList[$executionID])) $burnList[$executionID] = array();
+
+ if(!isset($burnList[$executionID][$date]))
+ {
+ $burnList[$executionID][$date] = new stdclass();
+ $burnList[$executionID][$date]->consumed = 0;
+ $burnList[$executionID][$date]->left = 0;
+ }
+
+ $burnList[$executionID][$date]->consumed += $consumed->totalConsumed;
+ $burnList[$executionID][$date]->left += $latestLefts[$taskID][$date]->left;
+ }
+ }
+
+ foreach($burnList as $executionID => $burns)
+ {
+ foreach($burns as $date => $burn)
+ {
+ $burn->execution = $executionID;
+ $burn->date = $date;
+
+ $this->dao->replace(TABLE_BURN)->data($burn)->exec();
+ $burn->executionName = $executions[$burn->execution];
+ }
+ }
+
+ /* Update today's data of burn. */
$burns = $this->dao->select("execution, '$today' AS date, sum(estimate) AS `estimate`, sum(`left`) AS `left`, SUM(consumed) AS `consumed`")
->from(TABLE_TASK)
->where('execution')->in(array_keys($executions))
@@ -2819,8 +2874,9 @@ class executionModel extends model
->where('execution')->in(array_keys($executions))
->andWhere('deleted')->eq('0')
->andWhere('parent')->ge('0')
- ->andWhere('status')->eq('done')
+ ->andWhere('status', true)->eq('done')
->orWhere('status')->eq('closed')
+ ->markRight(1)
->groupBy('execution')
->fetchAll('execution');
$storyPoints = $this->dao->select('t1.project, sum(t2.estimate) AS `storyPoint`')->from(TABLE_PROJECTSTORY)->alias('t1')
@@ -2851,8 +2907,10 @@ class executionModel extends model
$this->dao->replace(TABLE_BURN)->data($burn)->exec();
$burn->executionName = $executions[$burn->execution];
+ $burnList[$executionID][$today] = $burn;
}
- return $burns;
+
+ return $burnList;
}
/**
diff --git a/module/execution/view/ajaxgetdropmenu.html.php b/module/execution/view/ajaxgetdropmenu.html.php
index 5d014e6ce1..f5c2d77aa6 100644
--- a/module/execution/view/ajaxgetdropmenu.html.php
+++ b/module/execution/view/ajaxgetdropmenu.html.php
@@ -46,7 +46,12 @@ foreach($executions as $projectID => $projectExecutions)
if($execution->status != 'done' and $execution->status != 'closed' and ($execution->PM == $this->app->user->account or isset($execution->teams[$this->app->user->account]))) $executionCounts[$projectID]['myExecution'] ++;
if($execution->status != 'done' and $execution->status != 'closed' and $execution->PM != $this->app->user->account and !isset($execution->teams[$this->app->user->account])) $executionCounts[$projectID]['others'] ++;
if($execution->status == 'done' or $execution->status == 'closed') $executionCounts[$projectID]['closed'] ++;
- $executionNames[] = $execution->name;
+ $onlyChildStage = $execution->grade == 2 and $execution->project != $execution->parent;
+ $executionNames[$execution->id] = $execution->name;
+ if($onlyChildStage and isset($parents[$execution->parent]))
+ {
+ $executionNames[$execution->id] = $parents[$execution->parent]->name . '/' . $execution->name;
+ }
}
}
$executionsPinYin = common::convert2Pinyin($executionNames);
@@ -77,7 +82,7 @@ foreach($executions as $projectID => $projectExecutions)
$selected = $execution->id == $executionID ? 'selected' : '';
if($execution->status != 'done' and $execution->status != 'closed' and ($execution->PM == $this->app->user->account or isset($execution->teams[$this->app->user->account])))
{
- $myExecutionsHtml .= '' . html::a(sprintf($link, $execution->id), $execution->name, '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
+ $myExecutionsHtml .= '' . html::a(sprintf($link, $execution->id), $executionNames[$execution->id], '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
if($selected == 'selected') $tabActive = 'myExecution';
@@ -85,7 +90,7 @@ foreach($executions as $projectID => $projectExecutions)
}
else if($execution->status != 'done' and $execution->status != 'closed' and $execution->PM != $this->app->user->account and !isset($execution->teams[$this->app->user->account]))
{
- $normalExecutionsHtml .= '' . html::a(sprintf($link, $execution->id), $execution->name, '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
+ $normalExecutionsHtml .= '' . html::a(sprintf($link, $execution->id), $executionNames[$execution->id], '', "class='$selected clickable' title='{$execution->name}' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
if($selected == 'selected') $tabActive = 'other';
@@ -93,7 +98,7 @@ foreach($executions as $projectID => $projectExecutions)
}
else if($execution->status == 'done' or $execution->status == 'closed')
{
- $closedExecutionsHtml .= '' . html::a(sprintf($link, $execution->id), $execution->name, '', "class='$selected clickable' title='$execution->name' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
+ $closedExecutionsHtml .= '' . html::a(sprintf($link, $execution->id), $executionNames[$execution->id], '', "class='$selected clickable' title='$execution->name' data-key='" . zget($executionsPinYin, $execution->name, '') . "' data-app='{$this->app->tab}'") . '';
if($selected == 'selected') $tabActive = 'closed';
}
@@ -171,7 +176,7 @@ $(function()
$('li.has-list div.hide-in-search').removeClass('hidden');
$('.nav-tabs li.active').find('span').show();
}
-
+
if($('.form-control.search-input').val().length > 0)
{
$('#closed').attr("hidden", false);
@@ -191,7 +196,7 @@ $(function()
$('#closed').attr("hidden", true);
$('#gray-line').attr("hidden", true);
}
-
+
var listItem = $(this).find('.has-list');
listItem.each(function ()
{
diff --git a/module/execution/view/burn.html.php b/module/execution/view/burn.html.php
index 71da19f850..2d0bd26ae7 100644
--- a/module/execution/view/burn.html.php
+++ b/module/execution/view/burn.html.php
@@ -22,7 +22,7 @@
' . $lang->execution->computeBurn, 'hiddenwin', "title='{$lang->execution->computeBurn}{$lang->execution->burn}' class='btn btn-primary' id='computeBurn'");
+ common::printLink('execution', 'computeBurn', 'reload=yes', '
' . $lang->execution->computeBurn, 'hiddenwin', "title='{$lang->execution->computeBurn}' class='btn btn-primary' id='computeBurn'");
echo '
';
echo html::a($this->createLink('execution', 'burn', "executionID=$executionID&type=$weekend&interval=$interval"), $lang->execution->$weekend, '', "class='btn btn-link'");
if(common::canModify('execution', $execution)) common::printLink('execution', 'fixFirst', "execution=$execution->id", $lang->execution->fixFirst, '', "class='btn btn-link iframe' data-width='700'");
diff --git a/module/execution/view/computeburn.html.php b/module/execution/view/computeburn.html.php
index ea9ea7d339..8cbbbff589 100644
--- a/module/execution/view/computeburn.html.php
+++ b/module/execution/view/computeburn.html.php
@@ -11,8 +11,11 @@
*/
?>
$burnList)
{
- echo $burn->execution . "\t" . $burn->executionName . "\t" . $burn->date . "\t" . $burn->left . "\n";
+ foreach($burnList as $date => $burn)
+ {
+ echo $burn->execution . "\t" . $burn->executionName . "\t" . $burn->date . "\t" . $burn->left . "\n";
+ }
}
?>
diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php
index cfddc793bd..ec45f38420 100644
--- a/module/execution/view/kanban.html.php
+++ b/module/execution/view/kanban.html.php
@@ -105,6 +105,7 @@ js::set('priv',
'canEditStory' => common::hasPriv('story', 'edit'),
'canDeleteStory' => common::hasPriv('story', 'delete'),
'canChangeStory' => common::hasPriv('story', 'change'),
+ 'canCloseStory' => common::hasPriv('story', 'close'),
'canUnlinkStory' => common::hasPriv('execution', 'unlinkStory'),
)
);
diff --git a/module/execution/view/taskkanban.html.php b/module/execution/view/taskkanban.html.php
index 4d86ea58cd..7ec69b0041 100644
--- a/module/execution/view/taskkanban.html.php
+++ b/module/execution/view/taskkanban.html.php
@@ -193,7 +193,8 @@ js::set('priv',
'canStartTask' => common::hasPriv('task', 'start'),
'canAssignBug' => common::hasPriv('bug', 'assignto'),
'canConfirmBug' => common::hasPriv('bug', 'confirmBug'),
- 'canActivateBug' => common::hasPriv('bug', 'activate')
+ 'canActivateBug' => common::hasPriv('bug', 'activate'),
+ 'canCloseStory' => common::hasPriv('story', 'close')
)
);
?>
diff --git a/module/kanban/control.php b/module/kanban/control.php
index eeb922a87d..72fc0b3a02 100644
--- a/module/kanban/control.php
+++ b/module/kanban/control.php
@@ -1320,8 +1320,14 @@ class kanban extends control
{
$region = $this->kanban->getRegionByID($regionID);
+ $cards = $this->kanban->getCardsByObject('region', $regionID, 1);
+ foreach($this->config->kanban->fromType as $fromType)
+ {
+ $cards = $this->kanban->getImportedCards($region->kanban, $cards, $fromType, 1);
+ }
+
$this->view->kanban = $this->kanban->getByID($region->kanban);
- $this->view->cards = $this->kanban->getCardsByObject('region', $regionID, 1);
+ $this->view->cards = $cards;
$this->view->users = $this->loadModel('user')->getPairs('noletter|nodeleted');
$this->view->userIdPairs = $this->user->getPairs('noletter|nodeleted|showid');
$this->view->usersAvatar = $this->user->getAvatarPairs();
@@ -1374,13 +1380,20 @@ class kanban extends control
*/
public function deleteCard($cardID, $confirm = 'no')
{
+ $card = $this->kanban->getCardByID($cardID);
if($confirm == 'no')
{
- return print(js::confirm($this->lang->kanbancard->confirmDelete, $this->createLink('kanban', 'deleteCard', "cardID=$cardID&confirm=yes")));
+ $confirmTip = $card->fromType == '' ? $this->lang->kanbancard->confirmDelete : $this->lang->kanbancard->confirmRemove;
+ return print(js::confirm($confirmTip, $this->createLink('kanban', 'deleteCard', "cardID=$cardID&confirm=yes")));
}
else
{
- $this->kanban->delete(TABLE_KANBANCARD, $cardID);
+ if($card->fromType == '') $this->kanban->delete(TABLE_KANBANCARD, $cardID);
+ if($card->fromType != '')
+ {
+ $this->dao->delete()->from(TABLE_KANBANCARD)->where('id')->eq($cardID)->exec();
+ $this->loadModel('action')->create('kanbancard', $cardID, 'Deleted', '', $cardID);
+ }
if(isonlybody()) return print(js::reload('parent.parent'));
return print(js::reload('parent'));
@@ -1663,6 +1676,20 @@ class kanban extends control
->andWhere('`column`')->eq($toColID)
->exec();
+ $toColumn = $this->kanban->getColumnByID($toColID);
+ if($toColumn->laneType == 'story' and in_array($toColumn->type, array('tested', 'verified', 'released', 'closed')))
+ {
+ $data = new stdclass();
+ $data->stage = $toColumn->type;
+ if($toColumn->type == 'released')
+ {
+ $fromColumn = $this->kanban->getColumnByID($fromColID);
+ if($fromColumn->type == 'closed') $data->status = 'active';
+ }
+ $this->dao->update(TABLE_STORY)->data($data)->where('id')->eq($cardID)->exec();
+ $this->dao->update(TABLE_STORYSTAGE)->set('stage')->eq($toColumn->type)->where('story')->eq($cardID)->exec();
+ }
+
$kanbanGroup = $regionID == 0 ? $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy) : $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $regionID, $groupBy);
echo json_encode($kanbanGroup);
}
diff --git a/module/kanban/css/view.css b/module/kanban/css/view.css
index 52ff8d0e5b..313bd267e2 100644
--- a/module/kanban/css/view.css
+++ b/module/kanban/css/view.css
@@ -121,13 +121,13 @@
.label-closed, .label-terminate {background: #e5e5e5 !important; color: #b8b8b8;}
.label-deleted {background: #ff5d5d !important; color: #fff;}
.label-normal {background: #e1e8d9 !important; color: #9abe76;}
-.kanban-item > .kanban-card > .execInfo > .user, .kanban-item > .kanban-card > .releaseInfo > .user, .kanban-item > .kanban-card > .productplanInfo > .user {position: absolute; right: 13px; bottom: 1px}
+.kanban-item > .kanban-card > .execInfo > .user, .kanban-item > .kanban-card > .releaseInfo > .user, .kanban-item > .kanban-card > .productplanInfo > .user {float:right}
.kanban-item > .kanban-card > .execInfo > .user > .avatar, .kanban-item > .kanban-card > .releaseInfo > .user > .avatar, .kanban-item > .kanban-card > .productplanInfo > .user > .avatar {display: inline-block; width: 24px; height: 24px; line-height: 24px; margin-right: 0px; margin-left: 2px}
.kanban-card .releaseTitle .icon, .kanban-card .title .icon, .kanban-card .productplanTitle .icon, .kanban-card .title .icon, .kanban-card .buildTitle .icon{padding-right: 5px;}
.kanban-card .label-finish {margin-right: 7px; margin-top: -1px; padding: 3px 5px; float: left; background-color:#2a5f29;}
.kanban-card .releaseTitle, .kanban-card .productplanTitle {width: 100%; float: left;}
-.progress-box {display: flex; flex-direction: row; margin-top: 10px;}
+.progress-box {width:100%; display: flex; flex-direction: row; margin-top: 10px;}
.progress {flex: auto; margin: 5px auto 5px;}
.progress-number {padding-left: 5px;}
diff --git a/module/kanban/js/view.js b/module/kanban/js/view.js
index 0d0b723b39..1ba790454f 100644
--- a/module/kanban/js/view.js
+++ b/module/kanban/js/view.js
@@ -278,6 +278,22 @@ function renderKanbanItem(item, $item)
var whiteStyle = null;
if(item.color == '#2a5f29' || item.color == '#b10b0b' || item.color == '#cfa227') whiteStyle = 'style="color:#FFFFFF"';
+ var privs = item.actions;
+ var printMoreBtn = (privs.includes('editCard') || privs.includes('archiveCard') || privs.includes('copyCard') || privs.includes('deleteCard') || privs.includes('moveCard') || privs.includes('setCardColor'));
+ var $actions = $item.children('.actions');
+ var $title = $item.children('.title');
+ if(printMoreBtn && !$actions.length)
+ {
+ $(
+ [
+ '
'
+ ].join('')).appendTo($item);
+ }
+
if(item.fromType == 'execution')
{
renderExecutionItem(item, $item);
@@ -296,53 +312,13 @@ function renderKanbanItem(item, $item)
}
else
{
- var $title = $item.children('.title');
- var privs = item.actions;
- var printMoreBtn = (privs.includes('editCard') || privs.includes('archiveCard') || privs.includes('copyCard') || privs.includes('deleteCard') || privs.includes('moveCard') || privs.includes('setCardColor'));
-
- if(privs.includes('sortCard')) $item.parent().addClass('sort');
if(!$title.length)
{
- $('
' + kanbanLang.finished + '
').appendTo($item);
if(privs.includes('viewCard')) $title = $('
').appendTo($item).attr('href', createLink('kanban', 'viewCard', 'cardID=' + item.id, '', true));
if(!privs.includes('viewCard')) $title = $('
').appendTo($item);
}
$title.text(item.name).attr('title', item.name);
- if(kanban.performable == 1 && item.status == 'done' )
- {
- var finishColor = '#2a5f29';
- if(item.color == '#2a5f29') finishColor = '#FFFFFF';
- $title.text(item.name).css('color', finishColor);
- $item.children('.label-finish').show();
- if(item.color == '#2a5f29')
- {
- $item.children('.label-finish').css({'background-color':'#FFFFFF','color':'#2a5f29'});
- }
- else
- {
- $item.children('.label-finish').css({'background-color':'','color':'#FFFFFF'});
- }
- }
- else
- {
- $title.text(item.name).css('color', '');
- $item.children('.label-finish').hide();
- }
-
- var $actions = $item.children('.actions');
- if(printMoreBtn && !$actions.length)
- {
- $(
- [
- '
'
- ].join('')).appendTo($item);
- }
-
var $info = $item.children('.info');
if(!$info.length) $info = $(
[
@@ -353,11 +329,6 @@ function renderKanbanItem(item, $item)
'
',
'
'
].join('')).appendTo($item);
- if(kanban.performable == 1)
- {
- var $progress = $item.children('.progress-box');
- if(!$progress.length) $progress = $('').appendTo($item);
- }
$item.data('card', item);
@@ -367,15 +338,6 @@ function renderKanbanItem(item, $item)
$info.children('.pri')
.attr('class', 'pri label-pri label-pri-' + item.pri)
.text(item.pri);
- if(kanban.performable == 1)
- {
- $progress.find('.progress-bar').css('width', item.progress + '%');
- $progress.find('.progress-number').html(item.progress + '%');
- }
-
- $item.css('background-color', item.color);
- $item.toggleClass('has-color', item.color != '#fff' && item.color != '');
- $item.find('.info > .label-light').css('background-color', item.color);
var $time = $info.children('.time');
if(item.end == '0000-00-00' && item.begin == '0000-00-00')
@@ -413,6 +375,41 @@ function renderKanbanItem(item, $item)
for(i = 0; i < assignedTo.length; i++) title.push(users[assignedTo[i]]);
$user.html(renderUsersAvatar(assignedTo, item.id)).attr('title', title);
}
+
+ if(kanban.performable == 1)
+ {
+ var $progress = $item.children('.progress-box');
+ if(!$progress.length) $progress = $('').appendTo($item);
+ $progress.find('.progress-bar').css('width', item.progress + '%');
+ $progress.find('.progress-number').html(item.progress + '%');
+ }
+ $item.css('background-color', item.color);
+ $item.toggleClass('has-color', item.color != '#fff' && item.color != '');
+ $item.find('.info > .label-light').css('background-color', item.color);
+
+ $title = item.fromType == 'execution' ? $item.children('.header').children('.executionName').children('.title') : $item.children('.title');
+ if(!$title.children('.label-finish').length) $title.prepend('' + kanbanLang.finished + '
');
+ var name = item.title ? item.title : item.name;
+ if(kanban.performable == 1 && item.status == 'done' )
+ {
+ var finishColor = '#2a5f29';
+ if(item.color == '#2a5f29') finishColor = '#FFFFFF';
+ $title.css('color', finishColor);
+ $title.children('.label-finish').show();
+ if(item.color == '#2a5f29')
+ {
+ $item.children('.label-finish').css({'background-color':'#FFFFFF','color':'#2a5f29'});
+ }
+ else
+ {
+ $item.children('.label-finish').css({'background-color':'','color':'#FFFFFF'});
+ }
+ }
+ else
+ {
+ $title.css('color', '');
+ $title.children('.label-finish').hide();
+ }
}
/**
@@ -445,14 +442,19 @@ function renderExecutionItem(item, $item)
/* Print execution name. */
var $title = $titleBox.children('.title');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
var icon = mode == 'new' ? 'run' : 'project';
var viewMethod = item.execType == 'kanban' ? 'kanban' : 'view';
- if(privs.includes('viewExecution') && item.deleted == '0') $title = $('' + item.name + '').appendTo($titleBox).attr('href', createLink('execution', viewMethod, 'executionID=' + item.fromID));
- if(!privs.includes('viewExecution') || item.deleted == '1') $title = $('' + item.name + '
').appendTo($titleBox);
+ if(privs.includes('viewExecution') && item.deleted == '0') $title = $('' + name + '').appendTo($titleBox).attr('href', createLink('execution', viewMethod, 'executionID=' + item.fromID));
+ if(!privs.includes('viewExecution') || item.deleted == '1') $title = $('' + name + '
').appendTo($titleBox);
}
- $title.attr('title', item.name);
+ if(!$title.children('i').length)
+ {
+ $title.append('' + item.title);
+ }
+ $title.attr('title', name);
if(item.delay)
{
@@ -477,7 +479,7 @@ function renderExecutionItem(item, $item)
{
if(item.deleted == '0')
{
- $statusBox = $('' + executionLang.statusList[item.status] + '').appendTo($info);
+ $statusBox = $('' + executionLang.statusList[item.objectStatus] + '').appendTo($info);
}
else
{
@@ -519,12 +521,17 @@ function renderReleaseItem(item, $item)
/* Print name. */
var $title = $item.children('.releaseTitle');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
- if(privs.includes('viewRelease') && item.deleted == '0') $title = $('' + item.name + '').appendTo($item).attr('href', createLink('release', 'view', 'releaseID=' + item.fromID));
- if(!privs.includes('viewRelease') || item.deleted == '1') $title = $('' + item.name + '
').appendTo($item);
+ if(privs.includes('viewRelease') && item.deleted == '0') $title = $('' + name + '').appendTo($item).attr('href', createLink('release', 'view', 'releaseID=' + item.fromID));
+ if(!privs.includes('viewRelease') || item.deleted == '1') $title = $('' + name + '
').appendTo($item);
}
- $title.attr('title', item.name);
+ if(!$title.children('i').length)
+ {
+ $title.prepend('');
+ }
+ $title.attr('title', name);
var $info = $item.children('.releaseInfo');
if(!$info.length) $info = $(
@@ -538,7 +545,7 @@ function renderReleaseItem(item, $item)
{
if(item.deleted == '0')
{
- $statusBox = $('' + releaseLang.statusList[item.status] + '').appendTo($info);
+ $statusBox = $('' + releaseLang.statusList[item.objectStatus] + '').appendTo($info);
}
else
{
@@ -578,10 +585,15 @@ function renderProductplanItem(item, $item)
/* Print name. */
var $title = $item.children('.productplanTitle');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
- if(privs.includes('viewPlan') && item.deleted == '0') $title = $('' + item.title + '').appendTo($item).attr('href', createLink('productplan', 'view', 'productplanID=' + item.fromID));
- if(!privs.includes('viewPlan') || item.deleted == '1') $title = $('' + item.title + '
').appendTo($item);
+ if(privs.includes('viewPlan') && item.deleted == '0') $title = $('' + name + '').appendTo($item).attr('href', createLink('productplan', 'view', 'productplanID=' + item.fromID));
+ if(!privs.includes('viewPlan') || item.deleted == '1') $title = $('' + name + '
').appendTo($item);
+ }
+ if(!$title.children('i').length)
+ {
+ $title.append('' + name);
}
$title.attr('title', item.title);
@@ -597,7 +609,7 @@ function renderProductplanItem(item, $item)
{
if(item.deleted == '0')
{
- $statusBox = $('' + productplanLang.statusList[item.status] + '').appendTo($info);
+ $statusBox = $('' + productplanLang.statusList[item.objectStatus] + '').appendTo($info);
}
else
{
@@ -646,12 +658,17 @@ function renderBuildItem(item, $item)
/* Print name. */
var $title = $item.children('.buildTitle');
+ var name = item.title ? item.title : item.name;
if(!$title.length)
{
- if(privs.includes('viewBuild') && item.deleted == '0') $title = $('' + item.name + '').appendTo($item).attr('href', createLink('build', 'view', 'buildID=' + item.fromID));
- if(!privs.includes('viewBuild') || item.deleted == '1') $title = $('' + item.name + '
').appendTo($item);
+ if(privs.includes('viewBuild') && item.deleted == '0') $title = $('' + name + '').appendTo($item).attr('href', createLink('build', 'view', 'buildID=' + item.fromID));
+ if(!privs.includes('viewBuild') || item.deleted == '1') $title = $('' + name + '
').appendTo($item);
}
- $title.attr('title', item.name);
+ if(!$title.children('i').length)
+ {
+ $title.append('' + item.title);
+ }
+ $title.attr('title', name);
var $info = $item.children('.info');
if(!$info.length) $info = $(
@@ -1069,7 +1086,7 @@ function createCardMenu(options)
if(!privs.length) return [];
var items = [];
- if(privs.includes('editCard')) items.push({label: kanbanLang.editCard, icon: 'edit', url: createLink('kanban', 'editCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}});
+ if(privs.includes('editCard') && card.fromType == '') items.push({label: kanbanLang.editCard, icon: 'edit', url: createLink('kanban', 'editCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}});
if(privs.includes('performable') && kanban.performable == 1)
{
if(card.status == 'done')
@@ -1081,9 +1098,6 @@ function createCardMenu(options)
items.push({label: kanbanLang.finishCard, icon: 'checked', onClick: function(){finishCard(card.id, card.kanban, card.region);}});
}
}
- if(privs.includes('archiveCard') && kanban.archived == '1') items.push({label: kanbanLang.archiveCard, icon: 'card-archive', url: createLink('kanban', 'archiveCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
- if(privs.includes('copyCard')) items.push({label: kanbanLang.copyCard, icon: 'copy', url: createLink('kanban', 'copyCard', 'cardID=' + card.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal'}});
- if(privs.includes('deleteCard')) items.push({label: kanbanLang.deleteCard, icon: 'trash', url: createLink('kanban', 'deleteCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
if(privs.includes('moveCard'))
{
var moveCardItems = [];
@@ -1107,10 +1121,11 @@ function createCardMenu(options)
moveCardItems = moveCardItems.reverse();
items.push({label: kanbanLang.moveCard, icon: 'move', items: moveCardItems});
}
+ if(privs.includes('archiveCard') && kanban.archived == '1') items.push({label: kanbanLang.archiveCard, icon: 'card-archive', url: createLink('kanban', 'archiveCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
if(privs.includes('setCardColor'))
{
var cardColoritems = [];
- if(!card.color) color = "#fff";
+ if(!card.color) card.color = "#fff";
for(let i = 0 ; i < colorList.length ; i ++ )
{
var attr = card.color == colorList[i] ? '' : '';
@@ -1120,6 +1135,7 @@ function createCardMenu(options)
};
items.push({label: kanbanLang.cardColor, icon: 'color', items: cardColoritems});
}
+ if(privs.includes('deleteCard')) items.push({label: card.fromType == '' ? kanbanLang.deleteCard : kanbanLang.removeCard, icon: card.fromType == '' ? 'trash' : 'unlink', url: createLink('kanban', 'deleteCard', 'cardID=' + card.id), attrs: {'target': 'hiddenwin'}});
var bounds = options.$trigger[0].getBoundingClientRect();
items.$options = {x: bounds.right, y: bounds.top};
diff --git a/module/kanban/lang/en.php b/module/kanban/lang/en.php
index b6d8efb4a2..cce018447c 100644
--- a/module/kanban/lang/en.php
+++ b/module/kanban/lang/en.php
@@ -40,6 +40,7 @@ $lang->kanban->moveCard = 'Move Card';
$lang->kanban->cardColor = 'Card Color';
$lang->kanban->setCardColor = 'Set Card Color';
$lang->kanban->deleteCard = 'Delete Card';
+$lang->kanban->removeCard = 'Remove Card';
$lang->kanban->assigntoCard = 'Assign';
$lang->kanban->setting = 'Setting';
$lang->kanban->enableArchived = 'Enable Archived';
@@ -365,6 +366,7 @@ $lang->kanbancard->confirmArchive = 'Are you sure to archive this card? After
$lang->kanbancard->confirmDelete = 'Are you sure to delete this card? After deleting the card, it will be deleted from the Kanban. You can only view it in the system recycle bin.';
$lang->kanbancard->confirmRestore = "Are you sure you want to restore this card? After the card is restored, the card will be restored to the '%s' Kanban column.";
$lang->kanbancard->confirmRestoreTip = "The card's column has been archived or deleted, please restore '%s' column first.";
+$lang->kanbancard->confirmRemove = 'Are you sure to remove this card? After removing the card, the original data would not be affected.The card would be removed from the Kanban and counld not be restored.';
$lang->kanbancard->priList[1] = 1;
$lang->kanbancard->priList[2] = 2;
diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php
index 3b30ccdcdf..00a8305975 100644
--- a/module/kanban/lang/zh-cn.php
+++ b/module/kanban/lang/zh-cn.php
@@ -40,6 +40,7 @@ $lang->kanban->moveCard = '移动卡片';
$lang->kanban->cardColor = '卡片颜色';
$lang->kanban->setCardColor = '设置卡片颜色';
$lang->kanban->deleteCard = '删除卡片';
+$lang->kanban->removeCard = '移除卡片';
$lang->kanban->assigntoCard = '指派';
$lang->kanban->setting = '设置';
$lang->kanban->enableArchived = '设置归档功能';
@@ -365,6 +366,7 @@ $lang->kanbancard->confirmArchive = '您确认归档该卡片吗?归档卡
$lang->kanbancard->confirmDelete = '您确认删除该卡片吗?删除卡片后,该卡片将从看板中删除,您只能通过系统回收站查看。';
$lang->kanbancard->confirmRestore = '您确定要还原该卡片吗?还原卡片后,该卡片将还原到“%s”看板列中。';
$lang->kanbancard->confirmRestoreTip = '该卡片所属的看板列已被归档或删除,请先还原“%s”看板列。';
+$lang->kanbancard->confirmRemove = '您确认移除该卡片吗?移除卡片后,源数据不会受到影响,卡片将从看板中移除且无法恢复。';
$lang->kanbancard->priList[1] = 1;
$lang->kanbancard->priList[2] = 2;
diff --git a/module/kanban/model.php b/module/kanban/model.php
index 78ad667080..b3f439b886 100644
--- a/module/kanban/model.php
+++ b/module/kanban/model.php
@@ -1113,16 +1113,17 @@ class kanbanModel extends model
* @param int $kanbanID
* @param object $cards
* @param string $fromType
+ * @param int $archived
* @access public
* @return array
*/
- public function getImportedCards($kanbanID, $cards, $fromType)
+ public function getImportedCards($kanbanID, $cards, $fromType, $archived = 0)
{
/* Get imported cards based on imported object type. */
$objectCards = $this->dao->select('*')->from(TABLE_KANBANCARD)
->where('deleted')->eq(0)
->andWhere('kanban')->eq($kanbanID)
- ->andWhere('archived')->eq(0)
+ ->andWhere('archived')->eq($archived)
->andWhere('fromType')->eq($fromType)
->fetchGroup('fromID', 'id');
@@ -1168,6 +1169,8 @@ class kanbanModel extends model
$objectCard->execType = $object->type;
}
+ $objectCard->objectStatus = $objectCard->status;
+ $objectCard->status = $objectCard->progress == 100 ? 'done' : 'doing';
$cards[$cardID] = $objectCard;
}
}
diff --git a/module/kanban/view/viewarchivedcard.html.php b/module/kanban/view/viewarchivedcard.html.php
index 551ca80854..09da6b4a0f 100644
--- a/module/kanban/view/viewarchivedcard.html.php
+++ b/module/kanban/view/viewarchivedcard.html.php
@@ -31,6 +31,7 @@
#archivedCards .info > .users > span:after {right: -4px; content: ''; display: block; position: absolute; width: 2px; height: 2px; background-color: #8990a2; top: 0px; border-radius: 50%;}
#archivedCards .info > .users .avatar {display: inline-block; position: relative; border-radius: 50%; top: -5px; margin: 5px; right: -7px; margin-left: -4px;}
#archivedCards .cardName {word-wrap: break-word;}
+#archivedCards .card-item .icon {margin-right:2px;}
#archivedCards .card-item .red {background-color: #b10b0b;}
#archivedCards .card-item .yellow {background-color: #cfa227;}
#archivedCards .card-item .green {background-color: #2a5f29;}
@@ -44,6 +45,12 @@
#archivedCards .progress-box {width: 97%;}
#performable {padding: 25px 10px !important;}
+loadLang('execution');
+$app->loadLang('release');
+$app->loadLang('build');
+$app->loadLang('productplan');
+?>
kanban->archivedCard;?>
@@ -76,10 +83,21 @@
status == 'done'):?>
kanban->finished;?>
+ fromType)):?>
createLink('kanban', 'viewCard', "cardID=$card->id", '', true), $card->name, '', "class='cardName iframe' data-toggle='modal' data-width='80%' title='$card->name'");?>
pri;?>
estimate and $card->estimate != 0) echo "
{$card->estimate}h";?>
+
+ title) ? $card->title : $card->name;
+ if(common::hasPriv($card->fromType, 'view')) echo html::a($this->createLink('productplan', 'view', "id=$card->fromID"), $name, '', "class='cardName' title='$name'");
+ if(!common::hasPriv($card->fromType, 'view')) echo "
$name
";
+ echo "
";
+ if($card->fromType != 'release' and isset($lang->{$card->fromType}->statusList[$card->objectStatus])) echo "" . $lang->{$card->fromType}->statusList[$card->objectStatus] . '';
+ if(isset($card->date) and !helper::isZeroDate($card->date)) echo "" . date("m/d", strtotime($card->date)) . ""
+ ?>
+
end) and !helper::isZeroDate($card->begin)):?>
begin)) . $lang->kanbancard->beginAB;?>
@@ -137,7 +155,7 @@
if($canRestore) echo html::a(inlink('restoreCard', "cardID={$card->id}"), $lang->kanban->restore, '', "class='btn btn-xs btn-primary' target='hiddenwin'");
- if($canDelete) echo html::a($this->createLink('kanban', 'deleteCard', "cardID=$card->id"), $lang->delete, '', "class='btn btn-xs delete-card' target='hiddenwin'");
+ if($canDelete) echo html::a($this->createLink('kanban', 'deleteCard', "cardID=$card->id"), $card->fromType == '' ? $lang->delete : $lang->unlink, '', "class='btn btn-xs delete-card' target='hiddenwin'");
?>
@@ -152,5 +170,17 @@ $(function()
{
$('#archivedCards').animate({right: -400}, 500);
});
+ $('.card-item').each(function()
+ {
+ var $item = $(this).children('.col-xs-10').children('.kanban-item');
+ var icon = '';
+
+ if($item.children('.build').length) icon = '
';
+ if($item.children('.execution').length) icon = '';
+ if($item.children('.productplan').length) icon = '';
+ if($item.children('.release').length) icon = '';
+
+ $item.children('.cardName').prepend(icon);
+ });
})
diff --git a/module/product/config.php b/module/product/config.php
index 15b950ff39..ba557eda1f 100644
--- a/module/product/config.php
+++ b/module/product/config.php
@@ -87,6 +87,36 @@ $config->product->search['params']['closedDate'] = array('operator' => '=',
$config->product->search['params']['lastEditedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
$config->product->search['params']['activatedDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->product->all = new stdclass();
+$config->product->all->search['module'] = 'product';
+$config->product->all->search['fields']['name'] = $lang->product->name;
+$config->product->all->search['fields']['code'] = $lang->product->code;
+$config->product->all->search['fields']['id'] = $lang->product->id;
+if($config->systemMode == 'new') $config->product->all->search['fields']['program'] = $lang->product->program;
+$config->product->all->search['fields']['line'] = $lang->product->line;
+$config->product->all->search['fields']['desc'] = $lang->product->desc;
+$config->product->all->search['fields']['PO'] = $lang->product->PO;
+$config->product->all->search['fields']['QD'] = $lang->product->QD;
+$config->product->all->search['fields']['RD'] = $lang->product->RD;
+$config->product->all->search['fields']['reviewer'] = $lang->product->reviewer;
+$config->product->all->search['fields']['type'] = $lang->product->type;
+$config->product->all->search['fields']['createdDate'] = $lang->product->createdDate;
+$config->product->all->search['fields']['createdBy'] = $lang->product->createdBy;
+
+$config->product->all->search['params']['name'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->product->all->search['params']['code'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->product->all->search['params']['id'] = array('operator' => '=', 'control' => 'input', 'values' => '');
+if($config->systemMode == 'new') $config->product->all->search['params']['program'] = array('operator' => '=', 'control' => 'select', 'values' => '');
+$config->product->all->search['params']['line'] = array('operator' => '=', 'control' => 'select', 'values' => '');
+$config->product->all->search['params']['desc'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
+$config->product->all->search['params']['PO'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['QD'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['RD'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['reviewer'] = array('operator' => 'include', 'control' => 'select', 'values' => 'users');
+$config->product->all->search['params']['type'] = array('operator' => '=', 'control' => 'select', 'values' => $lang->product->typeList);
+$config->product->all->search['params']['createdDate'] = array('operator' => '=', 'control' => 'input', 'values' => '', 'class' => 'date');
+$config->product->all->search['params']['createdBy'] = array('operator' => '=', 'control' => 'select', 'values' => 'users');
+
$config->product->create = new stdclass();
$config->product->edit = new stdclass();
$config->product->create->requiredFields = 'name,code';
diff --git a/module/product/control.php b/module/product/control.php
index 17e5520626..e6d17166e3 100644
--- a/module/product/control.php
+++ b/module/product/control.php
@@ -1197,12 +1197,14 @@ class product extends control
* @access public
* @return void
*/
- public function all($browseType = 'noclosed', $orderBy = 'order_asc')
+ public function all($browseType = 'noclosed', $orderBy = 'order_asc', $param = 0)
{
/* Load module and set session. */
$this->loadModel('program');
$this->session->set('productList', $this->app->getURI(true), 'product');
+ $queryID = ($browseType == 'bySearch') ? (int)$param : 0;
+
if($this->app->viewType == 'mhtml')
{
$productID = $this->product->saveState(0, $this->products);
@@ -1210,9 +1212,12 @@ class product extends control
}
/* Process product structure. */
- $productStats = $this->product->getStats($orderBy, '', $browseType, '', 'story');
+ $productStats = $this->product->getStats($orderBy, '', $browseType, '', 'story', '', $queryID);
$productStructure = $this->product->statisticProgram($productStats);
+ $actionURL = $this->createLink('product', 'all', "browseType=bySearch&orderBy=order_asc&queryID=myQueryID");
+ $this->product->buildProductSearchForm($param, $actionURL);
+
$this->view->title = $this->lang->product->common;
$this->view->position[] = $this->lang->product->common;
diff --git a/module/product/model.php b/module/product/model.php
index 2f1239d04d..79493dc5df 100644
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -275,6 +275,48 @@ class productModel extends model
->fetchAll('id');
}
+ /**
+ * Get list by search.
+ *
+ * @param int $queryID
+ * @access public
+ * @return array
+ */
+ public function getListBySearch($queryID = 0)
+ {
+ if($queryID)
+ {
+ $query = $this->loadModel('search')->getQuery($queryID);
+ if($query)
+ {
+ $this->session->set('productQuery', $query->sql);
+ $this->session->set('productForm', $query->form);
+ }
+ else
+ {
+ $this->session->set('productQuery', ' 1 = 1');
+ }
+ }
+ else
+ {
+ if($this->session->productQuery == false) $this->session->set('productQuery', ' 1 = 1');
+ }
+
+ $productQuery = $this->session->productQuery;
+ $productQuery = preg_replace('/`(\w+)`/', 't1.`$1`', $productQuery);
+
+ $products = $this->dao->select('t1.id as id,t1.*')->from(TABLE_PRODUCT)->alias('t1')
+ ->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id')
+ ->where($productQuery)
+ ->andWhere('t1.deleted')->eq(0)
+ ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi()
+ ->andWhere('t1.vision')->eq($this->config->vision)->fi()
+ ->orderBy('t1.order_asc')
+ ->fetchAll('id');
+
+ return $products;
+ }
+
/**
* Get product pairs.
*
@@ -1047,6 +1089,31 @@ class productModel extends model
$this->loadModel('search')->setSearchParams($this->config->product->search);
}
+ /**
+ * Build search form for all method of product module.
+ *
+ * @param int $queryID
+ * @param string $actionURL
+ * @access public
+ * @return void
+ */
+ public function buildProductSearchForm($queryID, $actionURL)
+ {
+ $this->config->product->all->search['queryID'] = $queryID;
+ $this->config->product->all->search['actionURL'] = $actionURL;
+
+ $linePairs = $this->getLinePairs();
+ $this->config->product->all->search['params']['line']['values'] = array('' => '') + $linePairs;
+
+ if($this->config->systemMode == 'new')
+ {
+ $programPairs = $this->loadModel('program')->getTopPairs('', 'noclosed');
+ $this->config->product->all->search['params']['program']['values'] = array('' => '') + $programPairs;
+ }
+
+ $this->loadModel('search')->setSearchParams($this->config->product->all->search);
+ }
+
/**
* Get project pairs by product.
*
@@ -1567,13 +1634,13 @@ class productModel extends model
* @access public
* @return array
*/
- public function getStats($orderBy = 'order_asc', $pager = null, $status = 'noclosed', $line = 0, $storyType = 'story', $programID = 0)
+ public function getStats($orderBy = 'order_asc', $pager = null, $status = 'noclosed', $line = 0, $storyType = 'story', $programID = 0, $param = 0)
{
$this->loadModel('report');
$this->loadModel('story');
$this->loadModel('bug');
- $products = $this->getList($programID, $status, $limit = 0, $line);
+ $products = $status == 'bySearch' ? $this->getListBySearch($param) : $this->getList($programID, $status, $limit = 0, $line);
if(empty($products)) return array();
$productKeys = array_keys($products);
@@ -2022,6 +2089,37 @@ class productModel extends model
return true;
}
+ /**
+ * Build operate menu.
+ *
+ * @param object $product
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($product, $type = 'view')
+ {
+ $menu = '';
+ $params = "product=$product->id";
+
+ if($type == 'view')
+ {
+ $menu .= "";
+ $menu .= $this->buildFlowMenu('product', $product, $type, 'direct');
+ $menu .= "";
+
+ $menu .= $this->buildMenu('product', 'close', $params, $product, $type, '', '', '', '', "data-app='product'");
+ $menu .= "";
+ }
+
+ $menu .= $this->buildMenu('product', 'edit', $params, $product, $type);
+
+ if($type == 'view') $menu .= $this->buildMenu('product', 'delete', $params, $product, $type, 'trash', 'hiddenwin');
+ if($type == 'browse' && common::hasPriv('product', 'updateOrder')) $menu .= "";
+
+ return $menu;
+ }
+
/**
* Create the link from module,method,extra,branch.
*
diff --git a/module/product/view/all.html.php b/module/product/view/all.html.php
index 1365dd5449..83b96d0c5c 100644
--- a/module/product/view/all.html.php
+++ b/module/product/view/all.html.php
@@ -17,6 +17,7 @@
{$recTotal}" : '';?>
{$label}" . $recTotalLabel, '', "class='btn btn-link' id='{$key}Tab'");?>
+ product->searchStory;?>
" . $lang->export, '', "class='btn btn-link export'", true, true)?>
@@ -31,11 +32,9 @@
diff --git a/module/release/view/create.html.php b/module/release/view/create.html.php
index 8767c42c7a..70466f980c 100644
--- a/module/release/view/create.html.php
+++ b/module/release/view/create.html.php
@@ -75,10 +75,4 @@
-
diff --git a/module/story/control.php b/module/story/control.php
index 64527571e3..70dcd47022 100644
--- a/module/story/control.php
+++ b/module/story/control.php
@@ -1351,7 +1351,7 @@ class story extends control
if(isonlybody())
{
$execution = $this->execution->getByID($this->session->execution);
- if($this->app->tab == 'execution' and $execution->type == 'kanban' and $this->app->tab == 'execution')
+ if($this->app->tab == 'execution' and $execution->type == 'kanban')
{
$this->loadModel('kanban')->updateLane($this->session->execution, 'story', $storyID);
$kanbanData = $this->kanban->getRDKanban($this->session->execution, $this->session->execLaneType ? $this->session->execLaneType : 'all');
diff --git a/module/story/model.php b/module/story/model.php
index eb728e444e..bf5d1bc64f 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -1987,6 +1987,7 @@ class storyModel extends model
->add('lastEditedBy', $this->app->user->account)
->add('lastEditedDate', $now)
->add('assignedDate', $now)
+ ->remove('comment')
->get();
$this->dao->update(TABLE_STORY)->data($story)->autoCheck()->checkFlow()->where('id')->eq((int)$storyID)->exec();
@@ -3038,6 +3039,8 @@ class storyModel extends model
public function closeParentRequirement($storyID)
{
$parentID = $this->dao->select('BID')->from(TABLE_RELATION)->where('AID')->eq($storyID)->fetch();
+ if(empty($parentID)) return;
+
$stories = $this->dao->select('t2.id, t2.status')->from(TABLE_RELATION)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t2.id=t1.AID')
->where('t1.BType')->eq('requirement')
diff --git a/module/testcase/model.php b/module/testcase/model.php
index b8bc0a7575..c77e6dd6ca 100644
--- a/module/testcase/model.php
+++ b/module/testcase/model.php
@@ -1713,22 +1713,8 @@ class testcaseModel extends model
echo $case->stepNumber;
break;
case 'actions':
- if($canBeChanged)
- {
- if($case->needconfirm or $browseType == 'needconfirm')
- {
- common::printIcon('testcase', 'confirmstorychange', "caseID=$case->id", $case, 'list', 'confirm', 'hiddenwin', '', '', '', $this->lang->confirm);
- break;
- }
-
- common::printIcon('testtask', 'results', "runID=0&caseID=$case->id", $case, 'list', '', '', 'iframe', true, "data-width='95%'");
- common::printIcon('testtask', 'runCase', "runID=0&caseID=$case->id&version=$case->version", $case, 'list', 'play', '', 'runCase iframe', false, "data-width='95%'");
- common::printIcon('testcase', 'edit', "caseID=$case->id", $case, 'list');
- if($this->config->testcase->needReview or !empty($this->config->testcase->forceReview)) common::printIcon('testcase', 'review', "caseID=$case->id", $case, 'list', 'glasses', '', 'iframe');
- common::printIcon('testcase', 'createBug', "product=$case->product&branch=$case->branch&extra=caseID=$case->id,version=$case->version,runID=", $case, 'list', 'bug', '', 'iframe', '', "data-width='90%'");
- common::printIcon('testcase', 'create', "productID=$case->product&branch=$case->branch&moduleID=$case->module&from=testcase¶m=$case->id", $case, 'list', 'copy');
- }
-
+ $case->browseType = $browseType;
+ echo $this->buildOperateMenu($case, 'browse');
break;
}
echo '';
@@ -2014,4 +2000,116 @@ class testcaseModel extends model
return '';
}
+
+ /**
+ * Build test case menu.
+ *
+ * @param object $case
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($case, $type = 'view')
+ {
+ $function = 'buildOperate' . ucfirst($type) . 'Menu';
+ return $this->$function($case);
+ }
+
+ /**
+ * Build test case view menu.
+ *
+ * @param object $case
+ * @access public
+ * @return string
+ */
+ public function buildOperateViewMenu($case)
+ {
+ if($case->deleted) return '';
+
+ $menu = '';
+ $params = "caseID=$case->id";
+ $extraParams = "runID=$case->runID&$params";
+ if(!$case->needconfirm)
+ {
+ if(!$case->isLibCase)
+ {
+ if($this->app->getViewType() == 'xhtml')
+ {
+ $menu .= $this->buildMenu('testtask', 'runCase', "$extraParams&version=$case->currentVersion", $case, 'view', 'play', '', 'showinonlybody', false, "data-width='95%'");
+ $menu .= $this->buildMenu('testtask', 'results', "$extraParams&version=$case->version", $case, 'view', '', '', 'showinonlybody', false, "data-width='95%'");
+ }
+ else
+ {
+ $menu .= $this->buildMenu('testtask', 'runCase', "$extraParams&version=$case->currentVersion", $case, 'view', 'play', '', 'showinonlybody iframe', false, "data-width='95%'");
+ $menu .= $this->buildMenu('testtask', 'results', "$extraParams&version=$case->version", $case, 'view', '', '', 'showinonlybody iframe', false, "data-width='95%'");
+ }
+ if($case->caseFails > 0)
+ {
+ $menu .= $this->buildMenu('testcase', 'createBug', "product=$case->product&branch=$case->branch&extra=$params,version=$case->version,runID=$case->runID", $case, 'view', 'bug', '', 'iframe', '', "data-width='90%'");
+ }
+ }
+ if($this->config->testcase->needReview || !empty($this->config->testcase->forceReview))
+ {
+ $menu .= $this->buildMenu('testcase', 'review', $params, $case, 'view', '', '', 'iframe', '', '', $this->lang->testcase->reviewAB);
+ }
+ }
+ else
+ {
+ $menu .= $this->buildMenu('testcase', 'confirmstorychange', $params, $case, 'view', 'confirm', 'hiddenwin', '', '', '', $this->lang->confirm);
+ }
+
+ $menu .= "";
+ $menu .= $this->buildFlowMenu('testcase', $case, 'view', 'direct');
+ $menu .= "";
+
+ if(!$case->needconfirm)
+ {
+ if(!isonlybody()) $menu .= $this->buildMenu('testcase', 'edit', $params, $case, 'view', '', '', 'showinonlybody');
+ if(!$case->isLibCase && $case->auto != 'unit')
+ {
+ $menu .= $this->buildMenu('testcase', 'create', "productID=$case->product&branch=$case->branch&moduleID=$case->module&from=testcase¶m=$case->id", $case, 'view', 'copy');
+ }
+ if($case->isLibCase && common::hasPriv('caselib', 'createCase'))
+ {
+ echo html::a(helper::createLink('caselib', 'createCase', "libID=$case->lib&moduleID=$case->module¶m=$case->id"), "", '', "class='btn' title='{$this->lang->testcase->copy}'");
+ }
+
+ $menu .= $this->buildMenu('testcase', 'delete', $params, $case, 'view', 'trash', 'hiddenwin', '');
+ }
+
+ return $menu;
+ }
+
+ /**
+ * Build test case browse menu.
+ *
+ * @param object $case
+ * @access public
+ * @return string
+ */
+ public function buildOperateBrowseMenu($case)
+ {
+ $canBeChanged = common::canBeChanged('case', $case);
+ if(!$canBeChanged) return '';
+
+ $menu = '';
+ $params = "caseID=$case->id";
+
+ if($case->needconfirm || $case->browseType == 'needconfirm')
+ {
+ return $this->buildMenu('testcase', 'confirmstorychange', $params, $case, 'browse', 'confirm', 'hiddenwin', '', '', '', $this->lang->confirm);
+ }
+
+ $menu .= $this->buildMenu('testtask', 'results', "runID=0&$params", $case, 'browse', '', '', 'iframe', true, "data-width='95%'");
+ $menu .= $this->buildMenu('testtask', 'runCase', "runID=0&$params&version=$case->version", $case, 'browse', 'play', '', 'runCase iframe', false, "data-width='95%'");
+ $menu .= $this->buildMenu('testcase', 'edit', "caseID=$case->id", $case, 'browse');
+ if($this->config->testcase->needReview || !empty($this->config->testcase->forceReview))
+ {
+ common::printIcon('testcase', 'review', $params, $case, 'browse', 'glasses', '', 'iframe');
+ }
+ $menu .= $this->buildMenu('testcase', 'createBug', "product=$case->product&branch=$case->branch&extra=caseID=$case->id,version=$case->version,runID=", $case, 'browse', 'bug', '', 'iframe', '', "data-width='90%'");
+ $menu .= $this->buildMenu('testcase', 'create', "productID=$case->product&branch=$case->branch&moduleID=$case->module&from=testcase¶m=$case->id", $case, 'browse', 'copy');
+
+ return $menu;
+ }
}
diff --git a/module/testcase/view/view.html.php b/module/testcase/view/view.html.php
index 37a3ad7e13..28a613dd52 100644
--- a/module/testcase/view/view.html.php
+++ b/module/testcase/view/view.html.php
@@ -112,41 +112,11 @@
- deleted):?>
";?>
- needconfirm):?>
-
";
- if($this->app->getViewType() == 'xhtml')
- {
- common::printIcon('testtask', 'runCase', "runID=$runID&caseID=$case->id&version=$case->currentVersion", $case, 'button', 'play', '', 'showinonlybody', false, "data-width='95%'");
- common::printIcon('testtask', 'results', "runID=$runID&caseID=$case->id&version=$case->version", $case, 'button', '', '', 'showinonlybody', false, "data-width='95%'");
- }
- else
- {
- common::printIcon('testtask', 'runCase', "runID=$runID&caseID=$case->id&version=$case->currentVersion", $case, 'button', 'play', '', 'showinonlybody iframe', false, "data-width='95%'");
- common::printIcon('testtask', 'results', "runID=$runID&caseID=$case->id&version=$case->version", $case, 'button', '', '', 'showinonlybody iframe', false, "data-width='95%'");
- }
-
- if($caseFails > 0) common::printIcon('testcase', 'createBug', "product=$case->product&branch=$case->branch&extra=caseID=$case->id,version=$case->version,runID=$runID", $case, 'button', 'bug', '', 'iframe', '', "data-width='90%'");
- }
- if($config->testcase->needReview or !empty($config->testcase->forceReview)) common::printIcon('testcase', 'review', "caseID=$case->id", $case, 'button', '', '', 'iframe', '', '', $lang->testcase->reviewAB);
- ?>
-
+ isLibCase = $isLibCase;?>
+ caseFails = $caseFails;?>
+ runID = $runID;?>
buildOperateMenu($case, 'view');?>
-
- id", $case, 'button', '', '', 'showinonlybody');
- if(!$isLibCase and $case->auto != 'unit') common::printIcon('testcase', 'create', "productID=$case->product&branch=$case->branch&moduleID=$case->module&from=testcase¶m=$case->id", $case, 'button', 'copy');
- if($isLibCase and common::hasPriv('caselib', 'createCase')) echo html::a($this->createLink('caselib', 'createCase', "libID=$case->lib&moduleID=$case->module¶m=$case->id"), "", '', "class='btn' title='{$lang->testcase->copy}'");
- common::printIcon('testcase', 'delete', "caseID=$case->id", $case, 'button', 'trash', 'hiddenwin', '');
- ?>
- needconfirm):?>
- id", $case, 'list', 'confirm', 'hiddenwin', '', '', '', $this->lang->confirm);?>
-
-
diff --git a/module/testsuite/model.php b/module/testsuite/model.php
index b0a9708dca..d3d60f92aa 100644
--- a/module/testsuite/model.php
+++ b/module/testsuite/model.php
@@ -327,4 +327,27 @@ class testsuiteModel extends model
->page($pager)
->fetchAll('id');
}
+
+
+ /**
+ * Build testsuite menu.
+ *
+ * @param object $suite
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($suite, $type = 'view')
+ {
+ $menu = '';
+ $params = "suiteID=$suite->id";
+
+ if($type == 'view') $menu .= $this->buildFlowMenu('testsuite', $suite, $type, 'direct');
+
+ $menu .= $this->buildMenu('testsuite', 'linkCase', $params, $suite, $type, 'link', '', '', '', '', $this->lang->testsuite->linkCase);
+ $menu .= $this->buildMenu('testsuite', 'edit', $params, $suite, $type);
+ $menu .= $this->buildMenu('testsuite', 'delete', $params, $suite, $type, 'trash', 'hiddenwin');
+
+ return $menu;
+ }
}
diff --git a/module/testsuite/view/browse.html.php b/module/testsuite/view/browse.html.php
index a4d24ca90e..70fd6b4070 100644
--- a/module/testsuite/view/browse.html.php
+++ b/module/testsuite/view/browse.html.php
@@ -70,11 +70,7 @@
addedDate;?> |
" . $this->loadModel('flow')->getFieldValue($extendField, $suite) . "";?>
- id", $suite, 'list', 'link');
- common::printIcon('testsuite', 'edit', "suiteID=$suite->id", $suite, 'list');
- common::printIcon('testsuite', 'delete', "suiteID=$suite->id", $suite, 'list', 'trash', 'hiddenwin');
- ?>
+ buildOperateMenu($suite, 'browse');?>
|
diff --git a/module/testsuite/view/view.html.php b/module/testsuite/view/view.html.php
index b3e3faf113..6a44d631bc 100644
--- a/module/testsuite/view/view.html.php
+++ b/module/testsuite/view/view.html.php
@@ -32,10 +32,6 @@
if(!$suite->deleted and $canBeChanged)
{
echo $this->buildOperateMenu($suite, 'view');
-
- common::printLink('testsuite', 'linkCase', "suiteID=$suite->id", " " . '', '', "class='btn btn-link' title='{$lang->testsuite->linkCase}'");
- common::printIcon('testsuite', 'edit', "suiteID=$suite->id");
- common::printIcon('testsuite', 'delete', "suiteID=$suite->id", '', 'button', 'trash', 'hiddenwin');
}
?>
diff --git a/module/testtask/model.php b/module/testtask/model.php
index b573fc2f85..a216209699 100644
--- a/module/testtask/model.php
+++ b/module/testtask/model.php
@@ -2166,4 +2166,76 @@ class testtaskModel extends model
return array('suites' => $suites, 'cases' => $cases, 'results' => $results, 'suiteNames' => $suiteNames, 'caseTitles' => $caseTitles);
}
+
+ /**
+ * Build test task menu.
+ *
+ * @param object $task
+ * @param string $type
+ * @access public
+ * @return string
+ */
+ public function buildOperateMenu($task, $type = 'view')
+ {
+ $function = 'buildOperate' . ucfirst($type) . 'Menu';
+ return $this->$function($task);
+ }
+
+ /**
+ * Build test task view menu.
+ *
+ * @param object $task
+ * @access public
+ * @return string
+ */
+ public function buildOperateViewMenu($task)
+ {
+ if($task->deleted) return '';
+
+ $menu = '';
+ $params = "taskID=$task->id";
+
+ $menu .= $this->buildMenu('testtask', 'start', $params, $task, 'view', '', '', 'iframe showinonlybody', true);
+ $menu .= $this->buildMenu('testtask', 'close', $params, $task, 'view', '', '', 'iframe showinonlybody', true);
+ $menu .= $this->buildMenu('testtask', 'block', $params, $task, 'view', 'pause', '', 'iframe showinonlybody', true);
+ $menu .= $this->buildMenu('testtask', 'activate', $params, $task, 'view', 'magic', '', 'iframe showinonlybody', true);
+ $menu .= $this->buildMenu('testtask', 'cases', $params, $task, 'view', 'sitemap');
+ $menu .= $this->buildMenu('testtask', 'linkCase', $params, $task, 'view', 'link');
+
+ $menu .= "";
+ $menu .= $this->buildFlowMenu('testtask', $task, 'view', 'direct');
+ $menu .= "";
+
+ $menu .= $this->buildMenu('testtask', 'edit', $params, $task, 'view');
+ $menu .= $this->buildMenu('testtask', 'delete', $params, $task, 'view', 'trash', 'hiddenwin');
+
+ return $menu;
+ }
+
+ /**
+ * Build test task browse menu.
+ *
+ * @param object $task
+ * @access public
+ * @return string
+ */
+ public function buildOperateBrowseMenu($task)
+ {
+ $menu = '';
+ $params = "taskID=$task->id";
+
+ $menu .= '';
+ $menu .= $this->buildMenu('testtask', 'cases', $params, $task, 'browse', 'sitemap');
+ $menu .= $this->buildMenu('testtask', 'linkCase', "$params&type=all¶m=myQueryID", $task, 'browse', 'link');
+ $menu .= $this->buildMenu('testreport', 'browse', "objectID=$task->product&objectType=product&extra=$task->id", $task, 'browse', 'flag');
+ $menu .= '
';
+ $menu .= $this->buildMenu('testtask', 'view', $params, $task, 'browse', 'list-alt', '', 'iframe', true, "data-width='90%'");
+ $menu .= $this->buildMenu('testtask', 'edit', $params, $task, 'browse');
+ if(common::hasPriv('testtask', 'delete', $task))
+ {
+ $deleteURL = helper::createLink('testtask', 'delete', "taskID=$task->id&confirm=yes");
+ $menu .= html::a("javascript:ajaxDelete(\"$deleteURL\",\"taskList\",confirmDelete)", '', '', "title='{$this->lang->testtask->delete}' class='btn'");
+ }
+ return $menu;
+ }
}
diff --git a/module/testtask/view/browse.html.php b/module/testtask/view/browse.html.php
index 74b54c4461..e417cb11f6 100644
--- a/module/testtask/view/browse.html.php
+++ b/module/testtask/view/browse.html.php
@@ -102,20 +102,7 @@ $status = $this->session->testTaskVersionStatus;
" . $this->loadModel('flow')->getFieldValue($extendField, $task) . "";?>
- ';
- common::printIcon('testtask', 'cases', "taskID=$task->id", $task, 'list', 'sitemap');
- common::printIcon('testtask', 'linkCase', "taskID=$task->id&type=all¶m=myQueryID", $task, 'list', 'link');
- common::printIcon('testreport', 'browse', "objectID=$task->product&objectType=product&extra=$task->id", $task, 'list', 'flag');
- echo '';
- common::printIcon('testtask', 'view', "taskID=$task->id", '', 'list', 'list-alt', '', 'iframe', true, "data-width='90%'");
- common::printIcon('testtask', 'edit', "taskID=$task->id", $task, 'list');
- if(common::hasPriv('testtask', 'delete', $task))
- {
- $deleteURL = $this->createLink('testtask', 'delete', "taskID=$task->id&confirm=yes");
- echo html::a("javascript:ajaxDelete(\"$deleteURL\",\"taskList\",confirmDelete)", '', '', "title='{$lang->testtask->delete}' class='btn'");
- }
- ?>
+ buildOperateMenu($task, 'browse');?>
|
diff --git a/module/testtask/view/view.html.php b/module/testtask/view/view.html.php
index 14fa49a23d..7324b9d56e 100644
--- a/module/testtask/view/view.html.php
+++ b/module/testtask/view/view.html.php
@@ -50,25 +50,8 @@
' . $lang->goback, '', 'btn btn-secondary');?>
- deleted):?>
- id", $task, 'button', '', '', 'iframe showinonlybody', true);
- common::printIcon('testtask', 'close', "taskID=$task->id", $task, 'button', '', '', 'iframe showinonlybody', true);
- common::printIcon('testtask', 'block', "taskID=$task->id", $task, 'button', 'pause', '', 'iframe showinonlybody', true);
- common::printIcon('testtask', 'activate', "taskID=$task->id", $task, 'button', 'magic', '', 'iframe showinonlybody', true);
- common::printIcon('testtask', 'cases', "taskID=$task->id", $task, 'button', 'sitemap');
- common::printIcon('testtask', 'linkCase', "taskID=$task->id", $task, 'button', 'link');
- ?>
-
buildOperateMenu($task, 'view');?>
-
-
- id", $task);
- common::printIcon('testtask', 'delete', "taskID=$task->id", $task, 'button', 'trash', 'hiddenwin');
- ?>
-
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000000..53903424ea
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,3 @@
+sonar.projectKey=zentaopms
+sonar.qualitygate.wait=true
+sonar.coverage.exclusions=**/*.*
diff --git a/test/class/mail.class.php b/test/class/mail.class.php
new file mode 100644
index 0000000000..8b31b2cd3b
--- /dev/null
+++ b/test/class/mail.class.php
@@ -0,0 +1,299 @@
+dao = $dao;
+ $this->tester = $tester;
+ $this->objectModel = $tester->loadModel('mail');
+ }
+
+ /**
+ * AutoDetect.
+ *
+ * @param int $email
+ * @access public
+ * @return object
+ */
+ public function autoDetectTest($email)
+ {
+ $objects = $this->objectModel->autoDetect($email);
+
+ if(dao::isError()) return dao::getError();
+ if(!$objects->host) return '没有检测到相关信息';
+
+ return $objects;
+ }
+
+ /**
+ * Get config from provider.
+ *
+ * @param int $domain
+ * @param int $username
+ * @access public
+ * @return object
+ */
+ public function getConfigFromProviderTest($domain, $username)
+ {
+ $object = $this->objectModel->getConfigFromProvider($domain, $username);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Get config by MXRR.
+ *
+ * @param int $domain
+ * @param int $username
+ * @access public
+ * @return object
+ */
+ public function getConfigByMXRRTest($domain, $username)
+ {
+ $object = $this->objectModel->getConfigByMXRR($domain, $username);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Get config by detecting SMTP.
+ *
+ * @param int $domain
+ * @param int $username
+ * @param int $port
+ * @access public
+ * @return object
+ */
+ public function getConfigByDetectingSMTPTest($domain, $username, $port)
+ {
+ $object = $this->objectModel->getConfigByDetectingSMTP($domain, $username, $port);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Set MTA.
+ *
+ * @access public
+ * @return object
+ */
+ public function setMTATest()
+ {
+ $object = $this->objectModel->setMTA();
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Set sendmail.
+ *
+ * @access public
+ * @return object
+ */
+ public function setSendMailTest()
+ {
+ $object = $this->objectModel->setSendMail();
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+
+ /**
+ * Send.
+ *
+ * @param int $toList
+ * @param int $subject
+ * @param string $body
+ * @param string $ccList
+ * @param int $includeMe
+ * @param array $emails
+ * @access public
+ * @return object
+ */
+ public function sendTest($toList, $subject, $body = '', $ccList = '', $includeMe = false, $emails = array())
+ {
+ $object = $this->objectModel->send($toList, $subject, $body, $ccList, $includeMe, $emails);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Set CC.
+ *
+ * @param int $ccList
+ * @param int $emails
+ * @access public
+ * @return object
+ */
+ public function setCCTest($ccList, $emails)
+ {
+ $this->objectModel->setCC($ccList, $emails);
+
+ return $this->setMTATest();
+ }
+
+ /**
+ * Set subject.
+ *
+ * @param int $subject
+ * @access public
+ * @return object
+ */
+ public function setSubjectTest($subject)
+ {
+ $this->objectModel->setSubject($subject);
+
+ return $this->setMTATest();
+ }
+
+ /**
+ * Set body.
+ *
+ * @param int $body
+ * @access public
+ * @return object
+ */
+ public function setBodyTest($body)
+ {
+ $this->objectModel->setBody($body);
+
+ return $this->setMTATest();
+ }
+
+ /**
+ * Mail exist.
+ *
+ * @access public
+ * @return object
+ */
+ public function mailExistTest()
+ {
+ $object = $this->objectModel->mailExist();
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Add queue.
+ *
+ * @param int $toList
+ * @param int $subject
+ * @param string $body
+ * @param string $ccList
+ * @param int $includeMe
+ * @access public
+ * @return object
+ */
+ public function addQueueTest($toList, $subject, $body = '', $ccList = '', $includeMe = false)
+ {
+ $this->objectModel->addQueue($toList, $subject, $body, $ccList, $includeMe);
+
+ if($toList and $subject)
+ {
+ $id = $this->dao->lastInsertID();
+ $object = $this->dao->select('*')->from(TABLE_NOTIFY)->where('id')->eq($id)->fetch();
+ }
+
+ if(!$object) return '没有数据提交';
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Get queue.
+ *
+ * @param string $status
+ * @param string $orderBy
+ * @param int $pager
+ * @access public
+ * @return object
+ */
+ public function getQueueTest($status = '', $orderBy = 'id_desc', $pager = null)
+ {
+ $objects = $this->objectModel->getQueue($status = '', $orderBy = 'id_desc', $pager = null);
+
+ if(dao::isError()) return dao::getError();
+
+ return $objects;
+ }
+
+ /**
+ * Get queue by id.
+ *
+ * @param int $queueID
+ * @access public
+ * @return object
+ */
+ public function getQueueByIdTest($queueID)
+ {
+ $object = $this->objectModel->getQueueById($queueID);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Merge mails.
+ *
+ * @param int $user
+ * @access public
+ * @return object
+ */
+ public function mergeMailsTest($user)
+ {
+ $mails = $this->dao->select('*')->from(TABLE_NOTIFY)
+ ->where('objectType')->eq('mail')
+ ->andWhere('toList')->eq($user)
+ ->fetchAll();
+
+ $object = $this->objectModel->mergeMails($mails);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+
+ /**
+ * Get subject.
+ *
+ * @param int $objectType
+ * @param int $object
+ * @param int $title
+ * @param int $actionType
+ * @access public
+ * @return object
+ */
+ public function getSubjectTest($objectType, $object, $title, $actionType)
+ {
+ $object = $this->tester->loadModel($objectType)->getByID($object);
+ $object = $this->objectModel->getSubject($objectType, $object, $title, $actionType);
+
+ if(dao::isError()) return dao::getError();
+
+ return $object;
+ }
+}
diff --git a/test/data/user.yaml b/test/data/user.yaml
index 16e537f0bd..f1f96e460b 100644
--- a/test/data/user.yaml
+++ b/test/data/user.yaml
@@ -80,9 +80,9 @@ fields:
note: "邮箱"
fields:
- field: email1
- range: 100-999:R
+ range: 1000-9999:2
- field: email2
- range: 100-999:R
+ range: 1000-9999:2
- field: email3
range: [@qq.com,@163.com,@gmail.com]
- field: skype
diff --git a/test/model/mail/addqueue.php b/test/model/mail/addqueue.php
new file mode 100644
index 0000000000..f374cf31cd
--- /dev/null
+++ b/test/model/mail/addqueue.php
@@ -0,0 +1,25 @@
+#!/usr/bin/env php
+addQueue();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$result1 = $mail->addQueueTest('','');
+$result2 = $mail->addQueueTest('user3', '测试提交队列', '测试发信内容');
+$result3 = $mail->addQueueTest('user3,admin', '测试提交队列', '测试发信内容');
+$result4 = $mail->addQueueTest('user3,admin', '测试提交队列', '测试发信内容', '', true);
+
+r($result1) && p() && e('没有数据提交'); //提交数据为空时
+r($result2) && p('subject') && e('测试提交队列'); //获取添加的邮件主题
+r($result3) && p('toList') && e('user3'); //获取收件人列表不包含自己
+r($result4) && p('toList') && e('user3,admin'); //获取收件人列表包含自己
diff --git a/test/model/mail/autodetect.php b/test/model/mail/autodetect.php
new file mode 100644
index 0000000000..df019977f5
--- /dev/null
+++ b/test/model/mail/autodetect.php
@@ -0,0 +1,23 @@
+#!/usr/bin/env php
+autoDetect();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$data = array('test@qq.com', 'test@163.com', 'test@sohu.com', 'test@vip.sohu.com', 'testm');
+
+r($mail->autoDetectTest($data[0])) && p('host') && e('smtp.qq.com'); //检测qq邮箱信息
+r($mail->autoDetectTest($data[1])) && p('host') && e('smtp.163.com'); //检测163邮箱信息
+r($mail->autoDetectTest($data[2])) && p('host') && e('smtp.sohu.com'); //检测搜狐邮箱信息
+r($mail->autoDetectTest($data[3])) && p('host') && e('smtp.vip.sohu.com'); //检测搜狐vip邮箱信息
+r($mail->autoDetectTest($data[4])) && p() && e('没有检测到相关信息'); //检测不存在的邮箱服务商信息
diff --git a/test/model/mail/getconfigbydetectingsmtp.php b/test/model/mail/getconfigbydetectingsmtp.php
new file mode 100644
index 0000000000..9240d80afa
--- /dev/null
+++ b/test/model/mail/getconfigbydetectingsmtp.php
@@ -0,0 +1,25 @@
+#!/usr/bin/env php
+getConfigByDetectingSMTP();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$result1 = $mail->getConfigByDetectingSMTPTest('qq.com','122@qq.com',465);
+$result2 = $mail->getConfigByDetectingSMTPTest('qq.com','122@qq.com',465);
+$result3 = $mail->getConfigByDetectingSMTPTest('qq.com','122@qq.com',22);
+$result4 = $mail->getConfigByDetectingSMTPTest('testqq.com','122@qq.com',22);
+
+r($result1) && p('host') && e('smtp.qq.com'); //根据域名获取smtp
+r($result2) && p('secure') && e('ssl'); //根据域名获取secure
+r($result3) && p('secure') && e('0'); //当端口号不为465时获取secure
+r($result4) && p() && e('0'); //当域名不存在时
diff --git a/test/model/mail/getconfigbymxrr.php b/test/model/mail/getconfigbymxrr.php
new file mode 100644
index 0000000000..4954701b39
--- /dev/null
+++ b/test/model/mail/getconfigbymxrr.php
@@ -0,0 +1,21 @@
+#!/usr/bin/env php
+getConfigByMXRR();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$result1 = $mail->getConfigByMXRRTest('qq.com','test');
+$result2 = $mail->getConfigByMXRRTest('sohu.com','test');
+
+r($result1) && p('host') && e('smtp.qq.com'); //获取qq相关信息
+r($result2) && p('host') && e('smtp.sohu.com'); //获取sohu相关信息
diff --git a/test/model/mail/getconfigfromprovider.php b/test/model/mail/getconfigfromprovider.php
new file mode 100644
index 0000000000..bf51755547
--- /dev/null
+++ b/test/model/mail/getconfigfromprovider.php
@@ -0,0 +1,21 @@
+#!/usr/bin/env php
+getConfigFromProvider();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$result1 = $mail->getConfigFromProviderTest('qq.com','123');
+$result2 = $mail->getConfigFromProviderTest('sohu.com','123');
+
+r($result1) && p('host') && e('smtp.qq.com'); //获取qq相关信息
+r($result2) && p('host') && e('smtp.sohu.com'); //获取sohu相关信息
diff --git a/test/model/mail/getqueue.php b/test/model/mail/getqueue.php
new file mode 100644
index 0000000000..e8dde37f52
--- /dev/null
+++ b/test/model/mail/getqueue.php
@@ -0,0 +1,25 @@
+#!/usr/bin/env php
+getQueue();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$result1 = count($mail->getQueueTest());
+$result2 = count($mail->getQueueTest('fail'));
+$result3 = count($mail->getQueueTest('wait'));
+$result4 = $mail->getQueueTest('wait');
+
+r($result1) && p() && e('7'); //按照收件人分组获取邮件列表个数
+r($result2) && p() && e('7'); //按照收件人分组获取发送失败的邮件数量
+r($result3) && p() && e('7'); //按照收件人分组获取待发送的邮件数量
+r($result4) && p('0:data') && e('测试发信内容'); //按照收件人分组获取第一条待发送的邮件内容
diff --git a/test/model/mail/getqueuebyid.php b/test/model/mail/getqueuebyid.php
new file mode 100644
index 0000000000..cf6ef9ac90
--- /dev/null
+++ b/test/model/mail/getqueuebyid.php
@@ -0,0 +1,21 @@
+#!/usr/bin/env php
+getQueueById();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$queueID = array(1,10,0);
+
+r($mail->getQueueByIdTest($queueID[0])) && p('subject') && e('主题1'); //获取id为1的邮件主题
+r($mail->getQueueByIdTest($queueID[1])) && p('toList') && e('top10'); //获取id为10的收件人
+r($mail->getQueueByIdTest($queueID[2])) && p() && e('0'); //获取id不存在的邮件信息
diff --git a/test/model/mail/getsubject.php b/test/model/mail/getsubject.php
new file mode 100644
index 0000000000..b6279cdba7
--- /dev/null
+++ b/test/model/mail/getsubject.php
@@ -0,0 +1,27 @@
+#!/usr/bin/env php
+getSubject();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+$result1 = $mail->getSubjectTest('testtask', 1, '123', 'closed');
+$result2 = $mail->getSubjectTest('doc', 1, 'test', 'created');
+$result3 = $mail->getSubjectTest('story', 1, 'test', 'created');
+$result4 = $mail->getSubjectTest('task', 1, 'test', 'created');
+$result5 = $mail->getSubjectTest('bug', 1, 'test', 'created');
+
+r($result1) && p() && e('admin关闭了测试单 #1:测试单1'); //获取关闭测试单时邮件主题
+r($result2) && p() && e('admin创建了文档 #1:文档标题901'); //获取创建文档时的邮件主题
+r($result3) && p() && e('STORY #1 test - 正常产品1'); //获取操作需求时的邮件主题
+r($result4) && p() && e('TASK #1 test - 迭代1'); //获取操作任务时的邮件主题
+r($result5) && p() && e('BUG #1 test - 正常产品1'); //获取操作BUG时的邮件主题
diff --git a/test/model/mail/mailexist.php b/test/model/mail/mailexist.php
new file mode 100644
index 0000000000..58b0075cd8
--- /dev/null
+++ b/test/model/mail/mailexist.php
@@ -0,0 +1,17 @@
+#!/usr/bin/env php
+mailExist();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+r($mail->mailExistTest()) && p('email') && e('10001000@qq.com'); //检查系统是否至少存在邮件
diff --git a/test/model/mail/mergemails.php b/test/model/mail/mergemails.php
new file mode 100644
index 0000000000..d48ae69688
--- /dev/null
+++ b/test/model/mail/mergemails.php
@@ -0,0 +1,18 @@
+#!/usr/bin/env php
+mergeMails();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+r($mail->mergeMailsTest('admin')) && p('id') && e('6,1'); //获取邮件合并后的id
+r($mail->mergeMailsTest('admin')) && p('subject') && e('主题6|主题1'); //获取邮件合并后的主题
diff --git a/test/model/mail/send.php b/test/model/mail/send.php
new file mode 100644
index 0000000000..679584cc87
--- /dev/null
+++ b/test/model/mail/send.php
@@ -0,0 +1,41 @@
+#!/usr/bin/env php
+send();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+/* If you don't set SMTP Please input you SMTP setting. */
+$mailConfig = new stdclass();
+$mailConfig->smtp = new stdclass();
+
+$mailConfig->turnon = 1;
+$mailConfig->mta = 'smtp';
+$mailConfig->async = 0;
+$mailConfig->fromAddress = '';
+$mailConfig->fromName = '';
+$mailConfig->domain = '';
+$mailConfig->smtp->host = '';
+$mailConfig->smtp->port = '';
+$mailConfig->smtp->auth = '1';
+$mailConfig->smtp->username = '';
+$mailConfig->smtp->password = '';
+$mailConfig->smtp->secure = '';
+$mailConfig->smtp->debug = 1;
+$mailConfig->smtp->charset = 'utf-8';
+
+global $tester;
+if(!isset($tester->config->mail->turnon)) $tester->loadModel('setting')->setItems('system.mail', $mailConfig);
+
+$result1 = $mail->sendTest('admin','test','test','',true);
+
+r($result1) && p() && e('0');
diff --git a/test/model/mail/sendmail.php b/test/model/mail/sendmail.php
new file mode 100644
index 0000000000..fcc4fca854
--- /dev/null
+++ b/test/model/mail/sendmail.php
@@ -0,0 +1,17 @@
+#!/usr/bin/env php
+sendmail();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+r($mail->sendmailTest(48,48)) && p() && e('0'); //测试为dev4用户发送邮件通知
diff --git a/test/model/mail/setbody.php b/test/model/mail/setbody.php
new file mode 100644
index 0000000000..6a7e73e688
--- /dev/null
+++ b/test/model/mail/setbody.php
@@ -0,0 +1,17 @@
+#!/usr/bin/env php
+setCC();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+r($mail->setBodyTest('test body')) && p('Body') && e('test body'); //测试设置内容
diff --git a/test/model/mail/setmta.php b/test/model/mail/setmta.php
new file mode 100644
index 0000000000..3fe819cb15
--- /dev/null
+++ b/test/model/mail/setmta.php
@@ -0,0 +1,18 @@
+#!/usr/bin/env php
+setCC();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+r($mail->setMTATest()) && p('Host') && e('localhost'); //获取MTA主机
+r($mail->setMTATest()) && p('SMTPDebug') && e('0'); //获取MTAdebug设置
diff --git a/test/model/mail/setsubject.php b/test/model/mail/setsubject.php
new file mode 100644
index 0000000000..41be42134c
--- /dev/null
+++ b/test/model/mail/setsubject.php
@@ -0,0 +1,17 @@
+#!/usr/bin/env php
+setCC();
+cid=1
+pid=1
+
+*/
+
+$mail = new mailTest();
+
+r($mail->setSubjectTest('test subject')) && p('Subject') && e('test subject'); //测试设置主题
diff --git a/test/ztest b/test/ztest
index 7eb3023bb7..2770206cc8 100755
--- a/test/ztest
+++ b/test/ztest
@@ -153,6 +153,9 @@ switch($argv[1])
case 'tutorial':
ztfRun('model/tutorial');
break;
+ case 'mail':
+ ztfRun('model/mail');
+ break;
case 'tree':
ztfRun('model/tree');
break;