Merge branch release/21.7.8 of zentao/zentaopms (#13248)

This commit is contained in:
刘刚
2026-01-12 08:52:17 +08:00
committed by Gitfox
747 changed files with 38464 additions and 33957 deletions
+3 -139
View File
@@ -1,157 +1,21 @@
模块,类,函数名
branch,model,manage
cne,model,apiGet
cne,model,apiPost
cne,model,backup
cne,model,cneServerError
cne,model,deleteBackup
cne,model,getAppVolumes
cne,model,getBackupList
cne,model,getBackupStatus
cne,model,getDiskSettings
cne,model,getVolumesMetrics
cne,model,installApp
cne,model,restore
cne,model,translateError
cne,model,uploadCert
cne,model,validateCert
common,model,printDuration
convert,model,getZentaoFields
convert,model,getZentaoObjectList
convert,model,saveState
instance,model,installationSettingsMap
instance,model,stop
metric,model,getDateByDateType
metric,model,getLatestResultByCode
metric,model,updateMetricFields
product,model,buildSearchConfig
screen,model,genNotFoundOrDraftComponentOption
search,model,buildOldQuery
search,model,getOldQuery
task,model,updateParent
todo,model,dateRange
transfer,model,format
action,tao,processCreateChildrenActionExtra
action,tao,processcreatechilrenactionextra
action,tao,processCreateRequirementsActionExtra
action,tao,processLinkStoryAndBugActionExtra
action,tao,processToStoryActionExtra
convert,tao,createTask
convert,tao,processJiraContent
kanban,tao,getStoryCardMenu
kanban,tao,refreshBugCards
kanban,tao,refreshERURCards
kanban,tao,refreshStoryCards
pivot,tao,getExecutionList
search,tao,checkFeedbackAndTicketPriv
search,tao,processDocRecord
search,tao,processExecutionRecord
search,tao,processProjectRecord
search,tao,processRecord
search,tao,processRiskRecord
search,tao,processStoryRecord
search,tao,processTaskRecord
story,tao,getProductReviewers
block,zen,createMoreLink
block,zen,printAnnualWorkloadBlock
block,zen,printAssignToMeBlock
block,zen,printBugBlock
block,zen,printBugStatisticBlock
block,zen,printBuildBlock
block,zen,printExecutionListBlock
block,zen,printExecutionStatisticBlock
block,zen,printIssueBlock
block,zen,printPlanBlock
block,zen,printProjectStatisticBlock
block,zen,printRiskBlock
block,zen,printscrumiessueblock
block,zen,printScrumIssueBlock
block,zen,printScrumListBlock
block,zen,printScrumRiskBlock
block,zen,printScrumTestBlock
block,zen,printSingleDynamicBlock
block,zen,printSprintBlock
block,zen,printTaskBlock
block,zen,printTesttaskBlock
block,zen,printWaterfallIssueBlock
block,zen,printWaterfallRiskBlock
bug,zen,assignProjectRelatedVars
bug,zen,assignVarsForEdit
bug,zen,buildCreateForm
bug,zen,buildEditForm
bug,zen,getAssignedToPairs
bug,zen,getBranchOptions
bug,zen,getProductsForCreate
bug,zen,updateTodoAfterCreate
build,zen,assignCreateData
caselib,zen,prepareCasesForBathcCreate
caselib,zen,prepareEditExtras
caselib,zen,processCasesForExport
doc,zen,assignApiVarForSpace
doc,zen,previeweicket
editor,zen,buildContentByAction
execution,zen,buildExecutionForCreate
execution,zen,buildExecutionKanbanData
execution,zen,correctErrorLang
execution,zen,getImportBugs
execution,zen,getPrintKanbanData
extension,zen,backupDB
extension,zen,copyPackageFiles
gitlab,zen,getGroupMemberData
gitlab,zen,getProjectMemberData
group,zen,managePrivByModule
instance,zen,storeView
job,zen,getCompileData
kanban,zen,assignCreateVars
metric,zen,getOldMetricInfo
my,zen,showWorkCountNotInOpen
pivot,zen,bugCreate
pivot,zen,getBuiltinMenus
pivot,zen,getDefaultMethodAndParams
productplan,zen,buildActionsList
productplan,zen,buildPlansForBatchEdit
productplan,zen,buildViewActions
product,zen,buildProductForClose
product,zen,getActionsForDynamic
product,zen,getProductPlans
program,zen,getProgramsByType
project,zen,buildActivateForm
project,zen,buildBugView
project,zen,buildCreateForm
project,zen,buildMembers
project,zen,recordExecutionsOfUnlinkedProducts
report,zen,assignAnnualData
repo,zen,getCommits
repo,zen,getFilesInfo
repo,zen,getLinkBranches
repo,zen,getSearchFormQuery
repo,zen,prepareCreate
repo,zen,syncLocalCommit
sonarqube,zen,getIssueList
story,zen,getFormFieldsForCreate
story,zen,getFormFieldsForReview
story,zen,processDataForEdit
story,zen,processFilterTitle
story,zen,setFormOptionsForBatchEdit
task,zen,buildEffortForFinish
task,zen,processExportData
task,zen,responseAfterRecord
testcase,zen,addEditAction
testcase,zen,assignBranchForEdit
testcase,zen,assigncreatesceneVars
testcase,zen,assignModuleOptionMenuForEdit
testcase,zen,buildBrowseSearchForm
testcase,zen,getStatusForUpdate
testcase,zen,prepareReviewData
testcase,zen,processCaseForExport
testcase,zen,processCasesForExport
testcase,zen,responseAfterBatchCreate
testcase,zen,responseAfterShowImport
testreport,zen,commonAction
testtask,zen,responseAfterRunCase
user,zen,checkDirPermission
user,zen,getUserForJSON
user,zen,parseLoginModuleAndMethod
user,zen,prepareCustomFields
zanode,zen,handleNode
zanode,zen,prepareCreateSnapshotExtras
bug,zen,buildBrowseView
doc,zen,previewFeedback
pivot,zen,projectDeviation
1 模块 类 函数名
branch model manage
cne model apiGet
cne model apiPost
cne model backup
cne model cneServerError
cne model deleteBackup
cne model getAppVolumes
cne model getBackupList
cne model getBackupStatus
cne model getDiskSettings
cne model getVolumesMetrics
2 cne model installApp
cne model restore
cne model translateError
3 cne model uploadCert
4 cne model validateCert
5 common model printDuration
6 convert model getZentaoFields
7 convert model getZentaoObjectList
convert model saveState
8 instance model installationSettingsMap
9 instance model stop
10 metric model getDateByDateType
metric model getLatestResultByCode
metric model updateMetricFields
product model buildSearchConfig
11 screen model genNotFoundOrDraftComponentOption
search model buildOldQuery
search model getOldQuery
task model updateParent
todo model dateRange
transfer model format
12 action tao processCreateChildrenActionExtra
action tao processcreatechilrenactionextra
13 action tao processCreateRequirementsActionExtra
14 action tao processLinkStoryAndBugActionExtra
15 action tao processToStoryActionExtra
16 convert tao createTask
convert tao processJiraContent
kanban tao getStoryCardMenu
kanban tao refreshBugCards
17 kanban tao refreshERURCards
18 kanban tao refreshStoryCards
19 pivot bug tao zen getExecutionList buildBrowseView
20 search doc tao zen checkFeedbackAndTicketPriv previewFeedback
21 search pivot tao zen processDocRecord projectDeviation
search tao processExecutionRecord
search tao processProjectRecord
search tao processRecord
search tao processRiskRecord
search tao processStoryRecord
search tao processTaskRecord
story tao getProductReviewers
block zen createMoreLink
block zen printAnnualWorkloadBlock
block zen printAssignToMeBlock
block zen printBugBlock
block zen printBugStatisticBlock
block zen printBuildBlock
block zen printExecutionListBlock
block zen printExecutionStatisticBlock
block zen printIssueBlock
block zen printPlanBlock
block zen printProjectStatisticBlock
block zen printRiskBlock
block zen printscrumiessueblock
block zen printScrumIssueBlock
block zen printScrumListBlock
block zen printScrumRiskBlock
block zen printScrumTestBlock
block zen printSingleDynamicBlock
block zen printSprintBlock
block zen printTaskBlock
block zen printTesttaskBlock
block zen printWaterfallIssueBlock
block zen printWaterfallRiskBlock
bug zen assignProjectRelatedVars
bug zen assignVarsForEdit
bug zen buildCreateForm
bug zen buildEditForm
bug zen getAssignedToPairs
bug zen getBranchOptions
bug zen getProductsForCreate
bug zen updateTodoAfterCreate
build zen assignCreateData
caselib zen prepareCasesForBathcCreate
caselib zen prepareEditExtras
caselib zen processCasesForExport
doc zen assignApiVarForSpace
doc zen previeweicket
editor zen buildContentByAction
execution zen buildExecutionForCreate
execution zen buildExecutionKanbanData
execution zen correctErrorLang
execution zen getImportBugs
execution zen getPrintKanbanData
extension zen backupDB
extension zen copyPackageFiles
gitlab zen getGroupMemberData
gitlab zen getProjectMemberData
group zen managePrivByModule
instance zen storeView
job zen getCompileData
kanban zen assignCreateVars
metric zen getOldMetricInfo
my zen showWorkCountNotInOpen
pivot zen bugCreate
pivot zen getBuiltinMenus
pivot zen getDefaultMethodAndParams
productplan zen buildActionsList
productplan zen buildPlansForBatchEdit
productplan zen buildViewActions
product zen buildProductForClose
product zen getActionsForDynamic
product zen getProductPlans
program zen getProgramsByType
project zen buildActivateForm
project zen buildBugView
project zen buildCreateForm
project zen buildMembers
project zen recordExecutionsOfUnlinkedProducts
report zen assignAnnualData
repo zen getCommits
repo zen getFilesInfo
repo zen getLinkBranches
repo zen getSearchFormQuery
repo zen prepareCreate
repo zen syncLocalCommit
sonarqube zen getIssueList
story zen getFormFieldsForCreate
story zen getFormFieldsForReview
story zen processDataForEdit
story zen processFilterTitle
story zen setFormOptionsForBatchEdit
task zen buildEffortForFinish
task zen processExportData
task zen responseAfterRecord
testcase zen addEditAction
testcase zen assignBranchForEdit
testcase zen assigncreatesceneVars
testcase zen assignModuleOptionMenuForEdit
testcase zen buildBrowseSearchForm
testcase zen getStatusForUpdate
testcase zen prepareReviewData
testcase zen processCaseForExport
testcase zen processCasesForExport
testcase zen responseAfterBatchCreate
testcase zen responseAfterShowImport
testreport zen commonAction
testtask zen responseAfterRunCase
user zen checkDirPermission
user zen getUserForJSON
user zen parseLoginModuleAndMethod
user zen prepareCustomFields
zanode zen handleNode
zanode zen prepareCreateSnapshotExtras
+18
View File
@@ -0,0 +1,18 @@
:80 {
root www/
file_server
php_server {
env PATH_INFO {http.matchers.file.remainder}
env SCRIPT_NAME {path}
try_files {path} index.php{path}
worker {
file www/index.php
}
}
log {
output file www/tmp/caddy.log
level INFO
}
}
+2 -2
View File
@@ -7,13 +7,13 @@ Z PUBLIC LICENSE(ZPL) 1.2
许可
Z PUBLIC LICENSE 由禅道软件(青岛)有限公司(www.easycorp.cn)起草,简称ZPL协议。
Z PUBLIC LICENSE 由禅道软件(青岛)有限公司(www.chandao.com)起草,简称ZPL协议。
任何人均可使用该协议来发布开源软件,并可对下面协议正文中以下划线标注的空白部分做相应修改,
除此之外的任何内容不得做任何修改。禅道软件(青岛)有限公司拥有对该协议条款的最终解释权。
前言:
禅道项目管理软件(以下简称该软件)由 禅道软件(青岛)有限公司(www.easycorp.cn)开发(以下简称我)。我依法拥有该软件的所有版权。
禅道项目管理软件(以下简称该软件)由 禅道软件(青岛)有限公司(www.chandao.com)开发(以下简称我)。我依法拥有该软件的所有版权。
本着共享开放的角度,我以开放源代码的形式发布该软件。您可以在遵守该协议的前提下使用该软件。
自您安装该软件开始,您和我之间的合同关系自动成立。除非您停止使用该软件或与我有签署额外合同,
您须认真遵循该授权协议约定的每一条款。
+1 -1
View File
@@ -1 +1 @@
21.7.7
21.7.8
+1 -1
View File
@@ -95,7 +95,7 @@ class bugEntry extends entry
$oldBug = $this->loadModel('bug')->getByID($bugID);
/* Set $_POST variables. */
$fields = 'uid,title,project,execution,openedBuild,assignedTo,pri,severity,type,story,resolvedBy,resolvedBuild,resolvedDate,closedBy,resolution,product,plan,task,module,steps,mailto,keywords,deadline';
$fields = 'uid,title,project,execution,openedBuild,assignedTo,pri,severity,type,story,resolvedBy,resolvedBuild,resolvedDate,closedBy,resolution,product,plan,task,module,steps,mailto,keywords,deadline,os,browser,feedbackBy';
$this->batchSetPost($fields, $oldBug);
$this->setPost('notifyEmail', implode(',', $this->request('notifyEmail', array())));
+2 -1
View File
@@ -66,8 +66,9 @@ class bugsEntry extends entry
$control = $this->loadController('bug', 'create');
$fields = 'title,project,execution,openedBuild,browser,os,assignedTo,pri,module,severity,type,story,task,mailto,keywords,steps,uid,deadline';
$fields = 'title,project,execution,browser,branch,os,assignedTo,pri,module,severity,type,story,task,mailto,keywords,steps,uid,deadline,plan,feedbackBy';
$this->batchSetPost($fields);
$this->setPost('openedBuild', $this->request('openedBuild', ["trunk"]));
$caseID = $this->request('case', 0);
if($caseID)
+15 -8
View File
@@ -21,14 +21,21 @@ class configsEntry extends baseEntry
{
$configs = array();
$configs[] = array('key' => 'language', 'value' => $this->config->default->lang);
$configs[] = array('key' => 'version', 'value' => $this->config->version);
$configs[] = array('key' => 'charset', 'value' => $this->config->charset);
$configs[] = array('key' => 'timezone', 'value' => $this->config->timezone);
$configs[] = array('key' => 'systemMode', 'value' => $this->config->systemMode);
$configs[] = array('key' => 'hourUnit', 'value' => $this->config->hourUnit);
$configs[] = array('key' => 'CRProduct', 'value' => $this->config->CRProduct);
$configs[] = array('key' => 'CRExecution', 'value' => $this->config->CRExecution);
$this->app->loadConfig('story');
/* System basic configs. */
$configs[] = array('key' => 'language', 'value' => $this->config->default->lang);
$configs[] = array('key' => 'version', 'value' => $this->config->version);
$configs[] = array('key' => 'charset', 'value' => $this->config->charset);
$configs[] = array('key' => 'timezone', 'value' => $this->config->timezone);
$configs[] = array('key' => 'systemMode', 'value' => $this->config->systemMode);
$configs[] = array('key' => 'hourUnit', 'value' => $this->config->hourUnit);
$configs[] = array('key' => 'CRProduct', 'value' => $this->config->CRProduct);
$configs[] = array('key' => 'CRExecution', 'value' => $this->config->CRExecution);
$configs[] = array('key' => 'CRTask', 'value' => $this->config->CRTask);
$configs[] = array('key' => 'edition', 'value' => $this->config->edition);
$configs[] = array('key' => 'vision', 'value' => $this->config->vision);
$configs[] = array('key' => 'storyNeedReview', 'value' => !!$this->config->story->needReview);
return $this->send(200, $configs);
}
+1 -1
View File
@@ -95,7 +95,7 @@ class executionEntry extends entry
$execution->actions = $this->loadModel('action')->processActionForAPI($actions, $users, $this->lang->execution);
break;
case "dynamics":
$dynamics = $data->data->dynamics;
$dynamics = $data->data->dynamics ?? array();
$execution->dynamics = $this->loadModel('action')->processDynamicForAPI($dynamics);
break;
case 'chartdata':
+37 -4
View File
@@ -20,8 +20,9 @@ class executionsEntry extends entry
*/
public function get($projectID = 0)
{
$appendFields = $this->param('fields', '');
$withProject = $this->param('withProject', '');
$appendFields = $this->param('fields', '');
$withProject = $this->param('withProject', '');
$mergeChildren = $this->param('mergeChildren', 0);
if(strpos(strtolower(",{$appendFields},"), ',dropmenu,') !== false) return $this->getDropMenu();
if($projectID)
@@ -53,10 +54,42 @@ class executionsEntry extends entry
}
$result = array();
$executionsMap = array();
foreach($data->data->executionStats as $execution)
{
$execution = $this->filterFields($execution, 'id,name,project,code,type,parent,begin,end,status,openedBy,openedDate,delay,progress,children,' . $appendFields);
$result[] = $this->format($execution, 'openedBy:user,openedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,canceledBy:user,canceledDate:time,PM:user,PO:user,RD:user,QD:user,whitelist:userList,begin:date,end:date,realBegan:date,realEnd:date,deleted:bool');
$execution = $this->filterFields($execution, 'id,name,project,code,type,parent,begin,end,status,openedBy,openedDate,delay,progress,children,isParent,' . $appendFields);
$execution = $this->format($execution, 'openedBy:user,openedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,canceledBy:user,canceledDate:time,PM:user,PO:user,RD:user,QD:user,whitelist:userList,begin:date,end:date,realBegan:date,realEnd:date,deleted:bool');
if($mergeChildren)
{
$executionsMap[$execution->id] = $execution;
}
else
{
$result[] = $execution;
}
}
if($mergeChildren)
{
foreach($executionsMap as $execution)
{
if(empty($execution->parent))
{
$result[$execution->id] = $execution;
}
elseif((!empty($execution->parent) and $execution->parent != 0 and $execution->parent != '0') and isset($executionsMap[$execution->parent]))
{
$parentExecution = $executionsMap[$execution->parent];
if(!isset($parentExecution->children)) $parentExecution->children = array();
$parentExecution->children[] = $execution;
}
else
{
$result[$execution->id] = $execution;
}
}
$result = array_values($result);
}
$data = array();
+19
View File
@@ -11,6 +11,25 @@
*/
class feedbackAssignToEntry extends entry
{
/**
* GET method.
*
* @param int $feedbackID
* @access public
* @return string
*/
public function get($feedbackID)
{
$control = $this->loadController('feedback', 'assignTo');
$control->assignTo($feedbackID);
$data = $this->getData();
if(!$data) return $this->send400('error');
if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message);
return $this->send(200, $data);
}
/**
* POST method.
*
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* The required fields entry point of ZenTaoPMS.
*
* @copyright Copyright 2009-2025 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Ruogu Liu <liuruogu@easycorp.ltd>
* @package entries
* @version 1
* @link https://www.zentao.net
*/
class requiredFieldsEntry extends entry
{
/**
* GET method.
* 获取所有模块的自定义必填字段配置
* Get required fields configuration for all modules.
*
* @access public
* @return string
*/
public function get()
{
$this->loadModel('custom');
/* Load language for all required modules. */
foreach($this->config->custom->requiredModules as $requiredModule) $this->app->loadLang($requiredModule);
$result = array();
/* Get required fields for each module. */
foreach($this->config->custom->requiredModules as $moduleName)
{
/* Load module config. */
$this->loadModel($moduleName);
if($moduleName == 'user') $this->app->loadModuleConfig($moduleName);
/* Get required fields. */
$requiredFields = $this->custom->getRequiredFields($this->config->$moduleName);
/* Handle special case for doc module. */
if($moduleName == 'doc')
{
unset($requiredFields['createlib']);
unset($requiredFields['editlib']);
}
/* Format result. */
if(!empty($requiredFields))
{
$moduleFields = array();
foreach($requiredFields as $method => $fields)
{
$moduleFields[$method] = array(
'method' => $method,
'fields' => explode(',', $fields),
'fieldsString' => $fields
);
}
$result[$moduleName] = $moduleFields;
}
}
return $this->send(200, $result);
}
}
+35 -7
View File
@@ -74,16 +74,44 @@ class storiesEntry extends entry
$fields = 'title,spec,verify,module,reviewer,type,parent,moduleOptionMenu,source,sourceNote,category,pri,estimate,mailto,keywords,notifyemail,uid,status';
$this->batchSetPost($fields);
$module = $this->request('module', 0) ? $this->request('module', 0) : 0;
$this->setPost('plans', array($this->request('plan', 0)));
$this->setPost('branches', array($this->request('branch')));
$this->setPost('modules', array($this->request('module', 0)));
/* If reviewer is not post, set needNotReview. */
$reviewer = $this->request('reviewer');
if(empty($reviewer)) $this->setPost('needNotReview', 1);
$this->setPost('modules', array($module));
$this->setPost('product', $productID);
$this->setPost('type', $this->param('type', 'story'));
$this->setPost('status', $this->param('status', 'draft'));
$this->setPost('type', $this->request('type', 'story'));
$this->setPost('grade', $this->request('grade', ''));
/* 获取评审相关参数 */
$reviewer = $this->request('reviewer');
$status = $this->request('status', 'active'); // 创建时默认状态为 'active'
$forceReview = $this->loadModel('story')->checkForceReview($this->request('type', 'story'));
if($forceReview)
{
$needNotReview = 0;
}
else
{
$needNotReview = empty($reviewer) ? 1 : 0;
}
$this->setPost('reviewer', $reviewer);
$this->setPost('needNotReview', $needNotReview);
/* 设置状态逻辑,与web端保持一致(参考 common.ui.js 中的 clickSubmit 函数) */
/* 如果状态是 'active' 且有评审人且未勾选不需要评审,则将状态设置为 'reviewing' */
$hasReviewer = !empty($reviewer);
$isActiveNeedReview = $status == 'active' && !$needNotReview;
if($hasReviewer && $isActiveNeedReview)
{
$this->setPost('status', 'reviewing');
}
else
{
/* 如果未指定状态,保持默认值 'active';如果指定了 'draft',保持 'draft' */
$this->setPost('status', $status);
}
$this->requireFields('title,spec,pri,category');
+32
View File
@@ -81,6 +81,9 @@ class storyEntry extends entry
$story->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : '';
$story->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : '';
$actionBtnList = $this->loadModel('common')->buildOperateMenu($story, $story->type);
$story->actionBtnList = array_column(zget($actionBtnList, 'mainActions', array()), 'icon');
return $this->send(200, $this->format($story, 'title:decodeHtml,openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,reviewedBy:user,reviewedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,deleted:bool,mailto:userList'));
}
@@ -100,6 +103,35 @@ class storyEntry extends entry
$fields = 'title,product,parent,reviewer,type,plan,module,source,sourceNote,category,pri,estimate,mailto,keywords,uid,stage,notifyEmail,status,needNotReview';
$this->batchSetPost($fields, $oldStory);
/* 设置状态逻辑,与web端保持一致(参考 common.ui.js 中的 clickSubmit 函数) */
$reviewer = $this->request('reviewer');
$status = $this->request('status', $oldStory->status);
$forceReview = $this->loadModel('story')->checkForceReview($oldStory->type);
if($forceReview)
{
$needNotReview = 0;
}
else
{
$needNotReview = empty($reviewer) ? 1 : 0;
}
$this->setPost('reviewer', $reviewer);
$this->setPost('needNotReview', $needNotReview);
/* 如果设置了评审人且满足以下条件之一,则将状态设置为 'reviewing':
* 1. 状态是 'active' 且未勾选不需要评审
* 2. 原状态是 'draft' 或 'changing' 且未在请求中指定状态
*/
$hasReviewer = !empty($reviewer);
$isActiveNeedReview = $status == 'active' && !$needNotReview;
$isDraftChangingWithoutStatus = strpos('draft,changing', $oldStory->status) !== false && !isset($this->requestBody->status);
if($hasReviewer && ($isActiveNeedReview || $isDraftChangingWithoutStatus))
{
$this->setPost('status', 'reviewing');
}
$control->edit($storyID);
$data = $this->getData();
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* The story grade entry point of ZenTaoPMS.
*
* @copyright Copyright 2009-2025 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Ruogu Liu <liuruogu@chandao.com>
* @package entries
* @version 1
* @link https://www.zentao.net
*/
class storygradeEntry extends entry
{
/**
* GET method.
* 获取所有层级
* Get all story grades.
*
* @access public
* @return string
*/
public function get()
{
$type = $this->param('type', '');
$status = $this->param('status', 'enable');
$storyModel = $this->loadModel('story');
/* 如果指定了类型,获取该类型的层级列表 */
if($type)
{
$gradeList = $storyModel->getGradeList($type);
}
else
{
/* 获取所有类型的层级,按类型分组 */
$gradeList = array();
$types = array('story', 'requirement', 'epic');
foreach($types as $storyType)
{
$grades = $storyModel->getGradeList($storyType);
if(!empty($grades))
{
foreach($grades as $grade)
{
$gradeList[] = $grade;
}
}
}
}
/* 根据status参数过滤 */
if($status != 'all' && !empty($gradeList))
{
$filteredList = array();
foreach($gradeList as $grade)
{
if(isset($grade->status) && $grade->status == $status)
{
$filteredList[] = $grade;
}
}
$gradeList = $filteredList;
}
/* 格式化返回数据 */
$result = array();
foreach($gradeList as $grade)
{
/* 如果指定了类型,返回数组格式 */
if($type)
{
$result[] = $grade;
}
else
{
/* 如果未指定类型,按类型分组返回 */
$gradeType = isset($grade->type) ? $grade->type : '';
if(!isset($result[$gradeType]))
{
$result[$gradeType] = array();
}
$result[$gradeType][] = $grade;
}
}
return $this->send(200, $result);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
/**
* The story reviewer required entry point of ZenTaoPMS.
*
* @copyright Copyright 2009-2025 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Ruogu Liu <liuruogu@chandao.com>
* @package entries
* @version 1
* @link https://www.zentao.net
*/
class storyreviewerrequiredEntry extends entry
{
/**
* GET method.
*
* @param string $storyType
* @access public
* @return string
*/
public function get($storyType = '')
{
if(empty($storyType)) $storyType = $this->param('type', 'story');
if(!in_array($storyType, array('story', 'requirement', 'epic'))) return $this->sendError(400, 'Invalid story type. Must be one of: story, requirement, epic.');
$this->loadModel('story');
$reviewerRequired = $this->story->checkForceReview($storyType);
return $this->send(200, array('storyType' => $storyType, 'reviewerRequired' => (bool)$reviewerRequired));
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* The story submit review entry point of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
* @package entries
* @version 1
* @link https://www.zentao.net
*/
class storySubmitReviewEntry extends entry
{
/**
* POST method.
*
* @param int $storyID
* @access public
* @return string
*/
public function post($storyID)
{
$control = $this->loadController('story', 'submitReview');
$fields = 'reviewer,needNotReview';
$this->batchSetPost($fields);
$control->submitReview($storyID, $this->param('storyType', 'story'));
$data = $this->getData();
if(!$data) return $this->send400('error');
if(isset($data->result) && $data->result == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message);
if(isset($data->status) && $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message);
$story = $this->loadModel('story')->getByID($storyID);
return $this->send(200, $this->format($story, 'openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,reviewedBy:user,reviewedDate:time,lastEditedBy:user,lastEditedDate:time,closedBy:user,closedDate:time,deleted:bool,mailto:userList'));
}
}
+2 -1
View File
@@ -43,7 +43,7 @@ class tabsEntry extends baseEntry
elseif($moduleName == 'product')
{
$this->app->loadLang('product');
$tabs = array('story', 'plan', 'project', 'release', 'requirement', 'doc', 'view');
$tabs = array('story', 'plan', 'project', 'release', 'epic', 'requirement', 'doc', 'view');
foreach($tabs as $menuKey)
{
@@ -67,6 +67,7 @@ class tabsEntry extends baseEntry
if($menuKey == 'project') $label = $this->lang->project->common;
if($menuKey == 'story') $label = $this->lang->createObjects['story'];
if($menuKey == 'requirement') $label = $this->lang->URCommon;
if($menuKey == 'epic') $label = $this->lang->ERCommon;
$menu = new stdclass();
$menu->code = $menuKey;
+3 -1
View File
@@ -88,6 +88,8 @@ class taskEntry extends entry
$task->preAndNext['pre'] = $preAndNext->pre ? $preAndNext->pre->id : '';
$task->preAndNext['next'] = $preAndNext->next ? $preAndNext->next->id : '';
$operateMenu = $this->loadModel('common')->buildOperateMenu($task, 'task');
$task->operateMenu = array_column(zget($operateMenu, 'mainActions', array()), 'name');
return $this->send(200, $this->format($task, 'deadline:date,openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,realStarted:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,canceledBy:user,canceledDate:time,lastEditedBy:user,lastEditedDate:time,deleted:bool,mailto:userList'));
}
@@ -105,7 +107,7 @@ class taskEntry extends entry
$oldTask = $this->loadModel('task')->getByID($taskID);
/* Set $_POST variables. */
$fields = 'name,type,desc,assignedTo,pri,estimate,left,consumed,story,parent,execution,module,closedReason,status,estStarted,deadline,team,teamEstimate,teamConsumed,teamLeft,multiple,mailto,uid';
$fields = 'name,type,mode,desc,assignedTo,pri,estimate,left,consumed,story,parent,execution,module,closedReason,status,estStarted,deadline,team,teamEstimate,teamConsumed,teamLeft,multiple,mailto,uid,teamSource,team,teamEstimate,teamConsumed,teamLeft';
$this->batchSetPost($fields, $oldTask);
if($_POST['status'] == 'done')
+1 -1
View File
@@ -22,7 +22,7 @@ class taskActiveEntry extends entry
{
$control = $this->loadController('task', 'activate');
$fields = 'assignedTo,left,comment';
$fields = 'assignedTo,team,teamSource,teamEstimate,teamConsumed,teamLeft,left,mode,multiple,comment';
$this->batchSetPost($fields);
$control->activate($taskID);
+67 -4
View File
@@ -79,12 +79,52 @@ class tasksEntry extends entry
{
$tasks = $data->data->tasks;
$pager = $data->data->pager;
$mergeChildren = $this->param('mergeChildren', 0);
$result = array();
foreach($tasks as $task)
$tasksMap = array();
if($mergeChildren)
{
if(isset($task->children)) $task->children = array_values((array)$task->children);
$result[] = $this->format($task, 'deadline:date,openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,realStarted:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,canceledBy:user,canceledDate:time,lastEditedBy:user,lastEditedDate:time,deleted:bool,mailto:userList');
/* Build tasks map. */
foreach($tasks as $task)
{
$tasksMap[$task->id] = $task;
}
/* Merge children tasks into parent tasks. */
foreach($tasksMap as $task)
{
if(!empty($task->parent) && $task->parent != 0 && $task->parent != '0' && isset($tasksMap[$task->parent]))
{
$parentTask = $tasksMap[$task->parent];
if(!isset($parentTask->children)) $parentTask->children = array();
$parentTask->children[] = $task;
}
}
/* Only return parent tasks (tasks with no parent or parent not in the map). */
foreach($tasksMap as $task)
{
if(empty($task->parent) || $task->parent == 0 || $task->parent == '0' || !isset($tasksMap[$task->parent]))
{
if(isset($task->children)) $task->children = array_values((array)$task->children);
$result[] = $this->format($task, 'deadline:date,openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,realStarted:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,canceledBy:user,canceledDate:time,lastEditedBy:user,lastEditedDate:time,deleted:bool,mailto:userList');
}
}
/* Adjust pager total when mergeChildren is enabled. */
$pager->recTotal = count($result);
}
else
{
foreach($tasks as $task)
{
if(isset($task->children)) $task->children = array_values((array)$task->children);
$result[] = $this->format($task, 'deadline:date,openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,realStarted:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,canceledBy:user,canceledDate:time,lastEditedBy:user,lastEditedDate:time,deleted:bool,mailto:userList');
}
}
return $this->send(200, array('page' => $pager->pageID, 'total' => $pager->recTotal, 'limit' => $pager->recPerPage, 'tasks' => $result));
}
@@ -103,7 +143,7 @@ class tasksEntry extends entry
{
$control = $this->loadController('task', 'create');
$fields = 'name,type,assignedTo,estimate,story,execution,project,module,pri,desc,estStarted,deadline,mailto,team,teamEstimate,multiple,uid';
$fields = 'name,type,mode,assignedTo,estimate,story,execution,project,module,pri,desc,estStarted,deadline,mailto,team,teamEstimate,multiple,uid';
$this->batchSetPost($fields);
$this->setPost('execution', $executionID);
@@ -114,6 +154,29 @@ class tasksEntry extends entry
if($this->request('multiple'))
{
if(count($this->request('team')) != count($this->request('teamEstimate'))) return $this->sendError(400, 'Arrays team and teamEstimate should be the same length');
/* Check if team estimate is greater than 0. */
$team = $this->request('team', array());
$teamEstimate = $this->request('teamEstimate', array());
$users = $this->loadModel('user')->getPairs('noletter');
if(count($team) < 2) return $this->sendError(400, $this->lang->task->error->teamMember);
foreach($teamEstimate as $index => $estimate)
{
if(empty($team[$index])) continue;
$estimateValue = (float)$estimate;
if(!is_numeric($estimate) || $estimateValue <= 0)
{
$account = $team[$index];
$realname = zget($users, $account);
$errorMsg = sprintf($this->lang->error->gt, $this->lang->task->estimateAB, '0');
if($realname) $errorMsg = $realname . ' ' . $errorMsg;
return $this->sendError(400, $errorMsg);
}
}
$this->setPost('mode', $this->request('mode', 'linear'));
$this->setPost('teamSource', array_fill(0, count($this->request('team')), ''));
}
+33 -17
View File
@@ -20,33 +20,49 @@ class tokensEntry extends baseEntry
public function post()
{
$account = $this->request('account');
$password = $this->request('password');
$password = $this->request('password', '');
$authKey = $this->request('authKey', '');
$addAction = $this->request('addAction', false);
if($this->loadModel('user')->checkLocked($account)) return $this->sendError(400, sprintf($this->lang->user->loginLocked, $this->config->user->lockMinutes));
$this->loadModel('user');
if($password)
{
if($this->user->checkLocked($account)) return $this->sendError(400, sprintf($this->lang->user->loginLocked, $this->config->user->lockMinutes));
$user = $this->user->identify($account, $password);
}
else
{
$user = $this->loadModel('im')->userIdentifyWithToken($account, $authKey);
if(is_object($user))
{
$user->admin = strpos($this->app->company->admins, ",{$user->account},") !== false;
}
else
{
$user = null;
}
}
$user = $this->user->identify($account, $password);
if($user)
{
$this->user->login($user, $addAction);
return $this->send(201, array('token' => session_id()));
}
else
{
$fails = $this->user->failPlus($account);
$remainTimes = $this->config->user->failTimes - $fails;
if($remainTimes <= 0)
{
return $this->sendError(400, sprintf($this->lang->user->loginLocked, $this->config->user->lockMinutes));
}
else if($remainTimes <= 3)
{
return $this->sendError(400, sprintf($this->lang->user->lockWarning, $remainTimes));
}
return $this->sendError(400, $this->lang->user->loginFailed);
$fails = $this->user->failPlus($account);
$remainTimes = $this->config->user->failTimes - $fails;
if($remainTimes <= 0)
{
return $this->sendError(400, sprintf($this->lang->user->loginLocked, $this->config->user->lockMinutes));
}
$this->sendError(400, $this->app->lang->user->loginFailed);
if($remainTimes <= 3)
{
return $this->sendError(400, sprintf($this->lang->user->lockWarning, $remainTimes));
}
return $this->sendError(400, $this->lang->user->loginFailed);
}
}
+12 -3
View File
@@ -19,7 +19,9 @@ class usersEntry extends entry
*/
public function get()
{
if(!common::hasPriv('company', 'browse')) return $this->sendError(400, 'error: no company-browse priv.');
$full = (int)$this->param('full', 0);
if($full == 1 && !common::hasPriv('company', 'browse')) return $this->sendError(400, 'error: no company-browse priv.');
$appendFields = $this->param('fileds', '');
$type = $this->param('type', 'bydept');
@@ -36,8 +38,15 @@ class usersEntry extends entry
$result = array();
foreach($users as $user)
{
$user = $this->filterFields($user, 'id,dept,account,realname,role,pinyin,email,' . $appendFields);
$result[] = $this->format($user, 'locked:time');
if($full == 1)
{
$user = $this->filterFields($user, 'id,dept,account,realname,role,pinyin,email,' . $appendFields);
$result[] = $this->format($user, 'locked:time');
}
else
{
$result[] = array('account' => $user->account, 'realname' => $user->realname);
}
}
$pageID = $pager ? $pager->pageID : 1;
+5 -5
View File
@@ -1,18 +1,18 @@
{
"pkg": {
"xuanxuan": {
"gitVersion": "dfd57fb979d1943926940e595f7a7259d5fe3517",
"version": "9.5"
"gitVersion": "927c59b18be6964df50606a524d703c256ccd22b",
"version": "9.6"
},
"zentaoext": {
"gitRepo": "zentao/zentaoext",
"gitVersion": "release/12.6"
"gitVersion": "release/12.7"
},
"zentaomax": {
"gitVersion": "release/7.6"
"gitVersion": "release/7.7"
},
"zentaoipd": {
"gitVersion": "release/4.6"
"gitVersion": "release/4.7"
},
"devops": {
"gitRepo": "zentao/devops"
+17 -12
View File
@@ -31,6 +31,8 @@ $routes['/options/:type'] = 'options';
$routes['/configurations'] = 'configs';
$routes['/configurations/:name'] = 'config';
$routes['/requiredFields'] = 'requiredfields';
$routes['/programs/:id/products'] = 'products';
$routes['/products'] = 'products';
$routes['/products/:id'] = 'product';
@@ -52,18 +54,21 @@ $routes['/products/:id/releases'] = 'releases';
$routes['/projects/:id/releases'] = 'projectReleases';
$routes['/releases/:id'] = 'release';
$routes['/stories'] = 'stories';
$routes['/products/:id/stories'] = 'stories';
$routes['/projects/:id/stories'] = 'projectStories';
$routes['/executions/:id/stories'] = 'executionStories';
$routes['/stories/:id'] = 'story';
$routes['/stories/:id/change'] = 'storyChange';
$routes['/stories/:id/close'] = 'storyClose';
$routes['/stories/:id/active'] = 'storyActive';
$routes['/stories/:id/assign'] = 'storyAssignto';
$routes['/stories/:id/estimate'] = 'storyRecordEstimate';
$routes['/stories/:id/recall'] = 'storyRecall';
$routes['/stories/:id/review'] = 'storyReview';
$routes['/stories'] = 'stories';
$routes['/products/:id/stories'] = 'stories';
$routes['/projects/:id/stories'] = 'projectStories';
$routes['/executions/:id/stories'] = 'executionStories';
$routes['/stories/:id'] = 'story';
$routes['/stories/:id/change'] = 'storyChange';
$routes['/stories/:id/close'] = 'storyClose';
$routes['/stories/:id/active'] = 'storyActive';
$routes['/stories/:id/assign'] = 'storyAssignto';
$routes['/stories/:id/estimate'] = 'storyRecordEstimate';
$routes['/stories/:id/recall'] = 'storyRecall';
$routes['/stories/:id/review'] = 'storyReview';
$routes['/stories/:id/submitreview'] = 'storySubmitReview';
$routes['/storyreviewerrequired/:type'] = 'storyreviewerrequired';
$routes['/storygrades'] = 'storygrade';
$routes['/products/:id/bugs'] = 'bugs';
$routes['/projects/:id/bugs'] = 'projectBugs';
+120 -108
View File
@@ -1,108 +1,120 @@
<?php
$routes = array();
$routes['/programs'] = array('response' => 'programs(array),pager');
$routes['/programs/:programID/projects'] = array('redirect' => '/programs/project?programID=:programID', 'response' => 'projectStats(array)|projects,pager');
$routes['/programs/:programID/products'] = array('redirect' => '/programs/product?programID=:programID', 'response' => 'products(array),pager');
$routes['/products'] = array('redirect' => '/products/all');
$routes['/products/all'] = array('response' => 'productStats|products,pager');
$routes['/products/browse'] = array('response' => 'stories,pager', 'response' => 'stories(array),pager'); // stories
$routes['/products/:productID'] = array('response' => 'product,dynamics,members,branches,reviewers');
$routes['/products/:productID/stories'] = array('redirect' => '/products/browse?productID=:productID');
$routes['/projects/:projectID/stories'] = array('redirect' => '/projectstories/story?projectID=:projectID', 'response' => 'stories(array),pager');
$routes['/executions/:executionID/stories'] = array('redirect' => '/executions/story?executionID=:executionID');
$routes['/stories/:storyID'] = array('response' => 'story,actions(array)');
$routes['/products/:productID/productplans'] = array('redirect' => '/productplans?productID=:productID', 'response' => 'plans(array)|productplans,pager');
$routes['/productplans/:planID'] = array('response' => 'plan|productplan,actions(array)');
$routes['/products/:productID/releases'] = array('redirect' => '/releases?productID=:productID', 'response' => 'releases,pager');
$routes['/projects/:projectID/releases'] = array('redirect' => '/projectreleases?projectID=:projectID', 'response' => 'releases,pager');
$routes['/releases/:releaseID'] = array('response' => 'release,actions(array)');
$routes['/projects'] = array('response' => 'projectStats|projects,pager');
$routes['/projects/list/:browseType'] = array('redirect' => '/projects?browseType=:browseType');
$routes['/projects/execution'] = array('response' => 'executionStats|executions,pager');
$routes['/projects/build'] = array('response' => 'builds,pager');
$routes['/projects/bug'] = array('response' => 'bugs,pager', 'response' => 'bugs(array),pager');
$routes['/projects/testcase'] = array('response' => 'cases(array)|testcases,pager');
$routes['/projects/testtask'] = array('response' => 'tasks(array)|testtasks,pager');
$routes['/projects/testreport'] = array('response' => 'reports(array)|testreports,pager');
$routes['/projects/:projectID'] = array('response' => 'project');
$routes['/projects/:projectID/executions'] = array('redirect' => '/projects/execution?projectID=:projectID');
$routes['/executions/task'] = array('response' => 'tasks,pager', 'response' => 'tasks(array),pager');
$routes['/executions/story'] = array('response' => 'stories,pager', 'response' => 'stories(array),pager');
$routes['/executions/build'] = array('response' => 'builds,pager');
$routes['/executions/bug'] = array('response' => 'bugs,pager', 'response' => 'bugs(array),pager');
$routes['/executions/testcase'] = array('response' => 'cases(array)|testcases,pager');
$routes['/executions/testtask'] = array('response' => 'tasks(array)|testtasks,pager');
$routes['/executions/testreport'] = array('response' => 'reports(array)|testreports,pager');
$routes['/executions/:executionID'] = array('response' => 'execution');
$routes['/executions/:executionID/tasks'] = array('redirect' => '/executions/task?executionID=:executionID');
$routes['/tasks/:taskID'] = array('response' => 'task,actions(array)');
$routes['/projects/:projectID/builds'] = array('redirect' => '/projects/build?projectID=:projectID');
$routes['/executions/:executionID/builds'] = array('redirect' => '/executions/build?executionID=:executionID');
$routes['/builds/:buildID'] = array('response' => 'build,actions(array)');
$routes['/products/:productID/bugs'] = array('redirect' => '/bugs?productID=:productID', 'response' => 'bugs(array),pager');
$routes['/projects/:projectID/bugs'] = array('redirect' => '/projects/bug?projectID=:projectID');
$routes['/executions/:executionID/bugs'] = array('redirect' => '/executions/bug?executionID=:executionID');
$routes['/bugs/:bugID'] = array('response' => 'bug,actions(array)');
$routes['/products/:productID/testcases'] = array('redirect' => '/testcases?productID=:productID', 'response' => 'cases(array)|testcases,pager');
$routes['/projects/:projectID/testcases'] = array('redirect' => '/projects/testcase?projectID=:projectID');
$routes['/executions/:executionID/testcases'] = array('redirect' => '/executions/testcase?executionID=:executionID');
$routes['/testcases/:caseID'] = array('response' => 'testcase,actions(array)');
$routes['/products/:productID/testtasks'] = array('redirect' => '/testtasks?productID=:productID', 'response' => 'tasks(array)|testtasks,pager');
$routes['/projects/:projectID/testtasks'] = array('redirect' => '/projects/testtask?projectID=:projectID');
$routes['/executions/:executionID/testtasks'] = array('redirect' => '/executions/testtask?executionID=:executionID');
$routes['/testtasks/:testtaskID'] = array('response' => 'task|testtask,actions(array)');
$routes['/products/:productID/testreports'] = array('redirect' => '/testreports?productID=:productID', 'response' => 'reports(array)|testreports,pager');
$routes['/projects/:projectID/testreports'] = array('redirect' => '/projects/testreport?projectID=:projectID');
$routes['/executions/:executionID/testreports'] = array('redirect' => '/executions/testreport?executionID=:executionID');
$routes['/testreports/:reportID'] = array('response' => 'report|testreport,actions(array)');
$routes['/projects/:projectID/builds'] = array('redirect' => '/projects/build?projectID=:projectID');
$routes['/executions/:executionID/builds'] = array('redirect' => '/executions/build?executionID=:executionID');
$routes['/builds/:buildID'] = array('response' => 'build,actions(array)');
$routes['/issues'] = array('response' => 'issueList(array)|issues,pager');
$routes['/projects/:projectID/issues'] = array('redirect' => '/issues?objectID=:projectID');
$routes['/executions/:executionID/issues'] = array('redirect' => '/issues?objectID=:executionID&from=execution');
$routes['/issues/:issueID'] = array('response' => 'issue,actions(array)');
$routes['/risks'] = array('response' => 'risks(array),pager');
$routes['/projects/:projectID/risks'] = array('redirect' => '/risks?projectID=:projectID');
$routes['/executions/:executionID/risks'] = array('redirect' => '/risks?executionID=:executionID&from=execution');
$routes['/risks/:riskID'] = array('response' => 'risk,actions(array)');
$routes['/opportunities'] = array('response' => 'opportunities(array),pager');
$routes['/projects/:projectID/opportunities'] = array('redirect' => '/opportunities?projectID=:projectID');
$routes['/executions/:executionID/opportunities'] = array('redirect' => '/opportunities?executionID=:executionID&from=execution');
$routes['/opportunities/:opportunityID'] = array('response' => 'opportunity,actions(array)');
$routes['/auditplans'] = array('response' => 'auditplans(array),pager');
$routes['/projects/:projectID/auditplans'] = array('redirect' => '/auditplans?projectID=:projectID');
$routes['/executions/:executionID/auditplans'] = array('redirect' => '/auditplans?executionID=:executionID&from=execution');
$routes['/feedbacks'] = array('response' => 'feedbacks(array),pager');
$routes['/products/:productID/feedbacks'] = array('redirect' => '/feedbacks?productID=:productID');
$routes['/feedbacks/:feedbackID'] = array('response' => 'feedback,actions(array)');
$routes['/tickets'] = array('response' => 'tickets(array),pager');
$routes['/products/:productID/tickets'] = array('redirect' => '/tickets?browseType=byProduct&param=:productID');
$routes['/tickets/:ticketID'] = array('response' => 'ticket,actions(array)');
$routes['/depts'] = array('response' => 'sons|depts');
$routes['/depts/browse'] = array();
$routes['/depts/:deptID'] = array('redirect' => '/depts/browse?deptID=:deptID', 'response' => 'sons');
$routes['/users'] = array('redirect' => '/companies/browse', 'response' => 'users,pager');
$routes['/users/:userID'] = array('redirect' => '/users/profile', 'response' => 'user');
<?php
$routes = array();
$routes['/programs'] = array('response' => 'programs(array),pager');
$routes['/programs/:programID'] = array('redirect' => '/programs/:programID/edit', 'response' => 'program');
$routes['/programs/:programID/projects'] = array('redirect' => '/programs/:programID/project', 'response' => 'projectStats(array)|projects,pager');
$routes['/programs/:programID/products'] = array('redirect' => '/programs/:programID/product', 'response' => 'products(array),pager');
$routes['/products'] = array('redirect' => '/products/all');
$routes['/products/all'] = array('response' => 'productStats|products,pager');
$routes['/products/browse'] = array('response' => 'stories,pager', 'response' => 'stories(array),pager'); // stories
$routes['/products/:productID'] = array('response' => 'product,dynamics,members,branches,reviewers');
$routes['/products/:productID/stories'] = array('redirect' => '/products/browse?productID=:productID');
$routes['/projects/:projectID/stories'] = array('redirect' => '/projectstories/story?projectID=:projectID', 'response' => 'stories(array),pager');
$routes['/executions/:executionID/stories'] = array('redirect' => '/executions/story?executionID=:executionID');
$routes['/stories/:storyID'] = array('response' => 'story,actions(array)');
$routes['/products/:productID/epics'] = array('redirect' => '/products/browse?productID=:productID&storyType=epic', 'response' => 'stories(array)|epics,pager');
$routes['/epics/:storyID'] = array('response' => 'story|epic,actions(array)');
$routes['/products/:productID/requirements'] = array('redirect' => '/products/browse?productID=:productID&storyType=requirement', 'response' => 'stories(array)|requirements,pager');
$routes['/requirements/:storyID'] = array('response' => 'story|requirement,actions(array)');
$routes['/products/:productID/productplans'] = array('redirect' => '/productplans?productID=:productID', 'response' => 'plans(array)|productplans,pager');
$routes['/productplans/:planID'] = array('response' => 'plan|productplan,actions(array)');
$routes['/products/:productID/releases'] = array('redirect' => '/releases?productID=:productID', 'response' => 'releases,pager');
$routes['/projects/:projectID/releases'] = array('redirect' => '/projectreleases?projectID=:projectID', 'response' => 'releases,pager');
$routes['/releases/:releaseID'] = array('response' => 'release,actions(array)');
$routes['/projects'] = array('response' => 'projectStats|projects,pager');
$routes['/projects/list/:browseType'] = array('redirect' => '/projects?browseType=:browseType');
$routes['/projects/execution'] = array('response' => 'executionStats|executions,pager');
$routes['/projects/build'] = array('response' => 'builds,pager');
$routes['/projects/bug'] = array('response' => 'bugs,pager', 'response' => 'bugs(array),pager');
$routes['/projects/testcase'] = array('response' => 'cases(array)|testcases,pager');
$routes['/projects/testtask'] = array('response' => 'tasks(array)|testtasks,pager');
$routes['/projects/testreport'] = array('response' => 'reports(array)|testreports,pager');
$routes['/projects/:projectID'] = array('response' => 'project');
$routes['/executions'] = array('method' => 'all', 'response' => 'executionStats|executions,pager');
$routes['/projects/:projectID/executions'] = array('redirect' => '/projects/execution?projectID=:projectID');
$routes['/executions/task'] = array('response' => 'tasks(array),pager');
$routes['/executions/story'] = array('response' => 'stories(array),pager');
$routes['/executions/build'] = array('response' => 'builds,pager');
$routes['/executions/bug'] = array('response' => 'bugs(array),pager');
$routes['/executions/testcase'] = array('response' => 'cases(array)|testcases,pager');
$routes['/executions/testtask'] = array('response' => 'tasks(array)|testtasks,pager');
$routes['/executions/testreport'] = array('response' => 'reports(array)|testreports,pager');
$routes['/executions/:executionID'] = array('response' => 'execution');
$routes['/executions/:executionID/tasks'] = array('redirect' => '/executions/task?executionID=:executionID');
$routes['/tasks/:taskID'] = array('response' => 'task,actions(array)');
$routes['/projects/:projectID/builds'] = array('redirect' => '/projects/build?projectID=:projectID');
$routes['/executions/:executionID/builds'] = array('redirect' => '/executions/build?executionID=:executionID');
$routes['/builds/:buildID'] = array('response' => 'build,actions(array)');
$routes['/products/:productID/bugs'] = array('redirect' => '/bugs?productID=:productID', 'response' => 'bugs(array),pager');
$routes['/projects/:projectID/bugs'] = array('redirect' => '/projects/bug?projectID=:projectID');
$routes['/executions/:executionID/bugs'] = array('redirect' => '/executions/bug?executionID=:executionID');
$routes['/bugs/:bugID'] = array('response' => 'bug,actions(array)');
$routes['/products/:productID/testcases'] = array('redirect' => '/testcases?productID=:productID', 'response' => 'cases(array)|testcases,pager');
$routes['/projects/:projectID/testcases'] = array('redirect' => '/projects/testcase?projectID=:projectID');
$routes['/executions/:executionID/testcases'] = array('redirect' => '/executions/testcase?executionID=:executionID');
$routes['/testcases/:caseID'] = array('response' => 'testcase,actions(array)');
$routes['/products/:productID/testtasks'] = array('redirect' => '/testtasks?productID=:productID', 'response' => 'tasks(array)|testtasks,pager');
$routes['/projects/:projectID/testtasks'] = array('redirect' => '/projects/testtask?projectID=:projectID');
$routes['/executions/:executionID/testtasks'] = array('redirect' => '/executions/testtask?executionID=:executionID');
$routes['/testtasks/:testtaskID'] = array('response' => 'task|testtask,actions(array)');
$routes['/products/:productID/testreports'] = array('redirect' => '/testreports?objectID=:productID', 'response' => 'reports(array)|testreports,pager');
$routes['/projects/:projectID/testreports'] = array('redirect' => '/projects/testreport?projectID=:projectID');
$routes['/executions/:executionID/testreports'] = array('redirect' => '/executions/testreport?executionID=:executionID');
$routes['/testreports/:reportID'] = array('response' => 'report|testreport,actions(array)');
$routes['/projects/:projectID/builds'] = array('redirect' => '/projects/build?projectID=:projectID');
$routes['/executions/:executionID/builds'] = array('redirect' => '/executions/build?executionID=:executionID');
$routes['/builds/:buildID'] = array('response' => 'build,actions(array)');
$routes['/issues'] = array('response' => 'issueList(array)|issues,pager');
$routes['/projects/:projectID/issues'] = array('redirect' => '/issues?objectID=:projectID');
$routes['/executions/:executionID/issues'] = array('redirect' => '/issues?objectID=:executionID&from=execution');
$routes['/issues/:issueID'] = array('response' => 'issue,actions(array)');
$routes['/risks'] = array('response' => 'risks(array),pager');
$routes['/projects/:projectID/risks'] = array('redirect' => '/risks?projectID=:projectID');
$routes['/executions/:executionID/risks'] = array('redirect' => '/risks?executionID=:executionID&from=execution');
$routes['/risks/:riskID'] = array('response' => 'risk,actions(array)');
$routes['/opportunities'] = array('response' => 'opportunities(array),pager');
$routes['/projects/:projectID/opportunities'] = array('redirect' => '/opportunities?projectID=:projectID');
$routes['/executions/:executionID/opportunities'] = array('redirect' => '/opportunities?executionID=:executionID&from=execution');
$routes['/opportunities/:opportunityID'] = array('response' => 'opportunity,actions(array)');
$routes['/auditplans'] = array('response' => 'auditplans(array),pager');
$routes['/projects/:projectID/auditplans'] = array('redirect' => '/auditplans?projectID=:projectID');
$routes['/executions/:executionID/auditplans'] = array('redirect' => '/auditplans?executionID=:executionID&from=execution');
$routes['/feedbacks'] = array('method' => 'admin', 'response' => 'feedbacks(array),pager');
$routes['/products/:productID/feedbacks'] = array('redirect' => '/feedbacks?param=:productID');
$routes['/feedbacks/:feedbackID'] = array('response' => 'feedback,actions(array)');
$routes['/tickets'] = array('response' => 'tickets(array),pager');
$routes['/products/:productID/tickets'] = array('redirect' => '/tickets?param=:productID');
$routes['/tickets/:ticketID'] = array('response' => 'ticket,actions(array)');
$routes['/systems'] = array('response' => 'appList(array)|systems,pager');
$routes['/products/:productID/systems'] = array('redirect' => '/systems?productID=:productID');
$routes['/systems/:systemID'] = array('response' => 'system,actions(array)');
$routes['/depts'] = array('response' => 'sons|depts');
$routes['/depts/browse'] = array();
$routes['/depts/:deptID'] = array('redirect' => '/depts/browse?deptID=:deptID', 'response' => 'sons');
$routes['/users'] = array('redirect' => '/companies/browse', 'response' => 'users,pager');
$routes['/users/:userID'] = array('redirect' => '/users/:userID/profile', 'response' => 'user');
+1 -1
View File
@@ -16,7 +16,7 @@ if(!class_exists('config')){class config{}}
if(!function_exists('getWebRoot')){function getWebRoot(){}}
/* 基本设置。Basic settings. */
$config->version = '21.7.7'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->version = '21.7.8'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->liteVersion = '1.2'; // 迅捷版版本。 The version of Lite.
$config->charset = 'UTF-8'; // ZenTaoPHP的编码。 The encoding of ZenTaoPHP.
$config->cookieLife = time() + 2592000; // Cookie的生存时间。The cookie life time.
+6 -2
View File
@@ -81,6 +81,7 @@ $filter->group = new stdclass();
$filter->convert = new stdclass();
$filter->admin = new stdclass();
$filter->install = new stdclass();
$filter->ai = new stdclass();
$filter->index->index = new stdclass();
$filter->block->default = new stdclass();
@@ -214,6 +215,7 @@ $filter->admin->getcaptcha = new stdclass();
$filter->admin->sendcode = new stdclass();
$filter->admin->giftpackage = new stdclass();
$filter->install->step5 = new stdclass();
$filter->ai->prompts = new stdclass();
$filter->index->index->get['open'] = 'reg::base64';
@@ -462,6 +464,8 @@ $filter->misc->installevent->cookie['sn'] = 'string';
$filter->install->step5->cookie['sn'] = 'string';
$filter->ai->prompts->cookie['aiPromptsViewType'] = 'code';
$filter->sso->getbindusers->get['hash'] = 'reg::md5';
$filter->sso->gettodolist->get['hash'] = 'reg::md5';
$filter->sso->getuserpairs->get['hash'] = 'reg::md5';
@@ -475,7 +479,7 @@ $filter->sso->login->get['sessionid'] = 'reg::base64';
$filter->sso->logout->get['status'] = 'code';
$filter->sso->logout->get['token'] = 'reg::md5';
$filter->upgrade->license->get['agree'] = 'equal::true';
$filter->upgrade->license->get['agree'] = 'equal::1';
$filter->user->export->cookie['checkedItem'] = 'reg::checked';
$filter->user->login->cookie['keepLogin'] = 'equal::on';
@@ -556,4 +560,4 @@ $filter->convert->mapjira2zentao->cookie['jiraRelation'] = 'reg::any';
$filter->admin->register->cookie['zentaosid'] = 'reg::any';
$filter->admin->getcaptcha->cookie['zentaosid'] = 'reg::any';
$filter->admin->sendcode->cookie['zentaosid'] = 'reg::any';
$filter->admin->giftpackage->cookie['zentaosid'] = 'reg::any';
$filter->admin->giftpackage->cookie['zentaosid'] = 'reg::any';
+6 -14
View File
@@ -209,6 +209,7 @@ $config->openMethods = array();
$config->openMethods[] = 'api.getsessionid';
$config->openMethods[] = 'misc.captcha';
$config->openMethods[] = 'misc.checkupdate';
$config->openMethods[] = 'misc.checknetconnect';
$config->openMethods[] = 'misc.ajaxsendevent';
$config->openMethods[] = 'misc.installevent';
$config->openMethods[] = 'sso.bind';
@@ -219,6 +220,7 @@ $config->openMethods[] = 'sso.gettodolist';
$config->openMethods[] = 'sso.login';
$config->openMethods[] = 'sso.logout';
$config->openMethods[] = 'upgrade.ajaxupdatefile';
$config->openMethods[] = 'upgrade.safedelete';
$config->openMethods[] = 'user.forgetpassword';
$config->openMethods[] = 'user.login';
$config->openMethods[] = 'user.refreshrandom';
@@ -236,7 +238,6 @@ $config->logonMethods[] = 'action.commentzin';
$config->logonMethods[] = 'action.editcommentzin';
$config->logonMethods[] = 'action.restorestages';
$config->logonMethods[] = 'admin.ignore';
$config->logonMethods[] = 'ai.adminindex';
$config->logonMethods[] = 'caselib.index';
$config->logonMethods[] = 'company.index';
$config->logonMethods[] = 'cron.consume';
@@ -378,7 +379,6 @@ define('TABLE_COMPANY', '`' . $config->db->prefix . 'company`');
define('TABLE_DEPT', '`' . $config->db->prefix . 'dept`');
define('TABLE_CONFIG', '`' . $config->db->prefix . 'config`');
define('TABLE_USER', '`' . $config->db->prefix . 'user`');
define('TABLE_SESSION', '`' . $config->db->prefix . 'session`');
define('TABLE_TODO', '`' . $config->db->prefix . 'todo`');
define('TABLE_GROUP', '`' . $config->db->prefix . 'group`');
define('TABLE_GROUPPRIV', '`' . $config->db->prefix . 'grouppriv`');
@@ -517,11 +517,6 @@ define('TABLE_DIMENSION', '`' . $config->db->prefix . 'dimension`');
define('TABLE_SCENE', '`' . $config->db->prefix . 'scene`');
define('VIEW_SCENECASE', '`ztv_scenecase`');
define('TABLE_PRIV', '`' . $config->db->prefix . 'priv`');
define('TABLE_PRIVLANG', '`' . $config->db->prefix . 'privlang`');
define('TABLE_PRIVMANAGER', '`' . $config->db->prefix . 'privmanager`');
define('TABLE_PRIVRELATION', '`' . $config->db->prefix . 'privrelation`');
define('TABLE_ACTIVITY', '`' . $config->db->prefix . 'activity`');
define('TABLE_APPROVAL', '`' . $config->db->prefix . 'approval`');
define('TABLE_APPROVALFLOW', '`' . $config->db->prefix . 'approvalflow`');
@@ -629,6 +624,7 @@ define('TABLE_INSTANCE', '`' . $config->db->prefix . 'instance`');
define('TABLE_ARTIFACTREPO', '`' . $config->db->prefix . 'artifactrepo`');
define('TABLE_AI_PROMPT', '`' . $config->db->prefix . 'ai_prompt`');
define('TABLE_AI_PROMPTFIELD', '`' . $config->db->prefix . 'ai_promptfield`');
define('TABLE_AI_PROMPTROLE', '`' . $config->db->prefix . 'ai_promptrole`');
define('TABLE_AI_MINIPROGRAM', '`' . $config->db->prefix . 'ai_miniprogram`');
define('TABLE_AI_MINIPROGRAMFIELD', '`' . $config->db->prefix . 'ai_miniprogramfield`');
@@ -719,10 +715,6 @@ $config->objectTables['zahost'] = TABLE_ZAHOST;
$config->objectTables['zanode'] = TABLE_ZAHOST;
$config->objectTables['automation'] = TABLE_AUTOMATION;
$config->objectTables['stepResult'] = TABLE_TESTRUN;
$config->objectTables['priv'] = TABLE_PRIV;
$config->objectTables['privlang'] = TABLE_PRIVLANG;
$config->objectTables['privmanager'] = TABLE_PRIVMANAGER;
$config->objectTables['privrelation'] = TABLE_PRIVRELATION;
$config->objectTables['scene'] = TABLE_SCENE;
$config->objectTables['chart'] = TABLE_CHART;
$config->objectTables['pivot'] = TABLE_PIVOT;
@@ -758,7 +750,7 @@ $config->closedFeatures = '';
$config->pipelineTypeList = array('gitlab', 'gogs', 'gitea', 'jenkins', 'sonarqube');
$config->mysqlDriverList = array('mysql', 'oceanbase');
$config->pgsqlDriverList = array('postgres', 'highgo');
$config->pgsqlDriverList = array('postgres', 'highgo', 'kingbase');
/* Program privs.*/
$config->programPriv = new stdclass();
@@ -794,9 +786,9 @@ $config->db->sqliteBlacklist = array('sqlite_queue', 'cron');
$config->hasDropmenuApps = array('program', 'project', 'product', 'execution', 'qa', 'admin', 'bi', 'feedback', 'demandpool');
$config->hasBranchMenuModules = array('product', 'story', 'release', 'bug', 'testcase', 'testtask', 'branch', 'tree');
$config->excludeBranchMenu = array('product-dashboard', 'product-view', 'product-whitelist', 'product-addwhitelist', 'branch-manage', 'branch-batchedit');
$config->excludeDropmenuList = array('program-browse', 'program-productview', 'program-kanban', 'product-all', 'product-index', 'product-kanban', 'project-kanban', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-template', 'project-createtemplate', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'metric-browse', 'metric-view', 'metriclib-browse', 'qa-index', 'caselib-create', 'feedback-batchedit', 'feedback-batchclose', 'feedback-showimport', 'dimension-browse', 'dataview-browse', 'dataview-create', 'dataview-query', 'ticket-batchedit', 'ticket-batchfinish', 'ticket-batchactivate', 'file-download');
$config->excludeDropmenuList = array('program-browse', 'program-productview', 'program-kanban', 'product-all', 'product-index', 'product-kanban', 'project-kanban', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-template', 'project-createtemplate', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'metric-browse', 'metric-view', 'metriclib-browse', 'qa-index', 'caselib-create', 'feedback-batchedit', 'feedback-batchclose', 'feedback-showimport', 'dimension-browse', 'dataview-browse', 'dataview-create', 'dataview-query', 'ticket-batchedit', 'ticket-batchfinish', 'ticket-batchactivate', 'ticket-showimport', 'file-download');
$config->excludeDropmenuModules = array('reporttemplate');
$config->hasSwitcherModules = array('design');
$config->hasSwitcherMethods = array('project-bug', 'project-testcase', 'execution-bug', 'execution-testcase', 'testtask-cases', 'testtask-view', 'testtask-report', 'testtask-groupcase', 'testtask-linkcase');
$config->excludeSwitcherList = array();
$config->hasMainNavBar = array();
$config->hasMainNavBar = array();
+24 -24
View File
@@ -215,15 +215,15 @@ REPLACE INTO `zt_casestep` (`id`, `parent`, `case`, `version`, `type`, `desc`, `
(5, 4, 2, 1, 'step', '点击新闻中心', '进入新闻中心'),
(6, 0, 3, 1, 'step', '进入新闻中心', '正常显示'),
(7, 0, 4, 1, 'step', '进入成果展示', '正常显示');
REPLACE INTO `zt_dept` (`id`, `name`, `parent`, `path`, `grade`, `order`, `position`, `function`, `manager`) VALUES
(1, '经理', 0, ',1,', 1, 0, '', '', ''),
(2, '开发', 0, ',2,', 1, 0, '', '', ''),
(3, '测试', 0, ',3,', 1, 0, '', '', ''),
(4, '市场', 0, ',4,', 1, 0, '', '', ''),
(5, '产品', 1, ',1,5,', 2, 0, '', '', ''),
(6, '项目', 1, ',1,6,', 2, 0, '', '', ''),
(7, '编程', 2, ',2,7,', 2, 0, '', '', ''),
(8, '美工', 2, ',2,8,', 2, 0, '', '', '');
REPLACE INTO `zt_dept` (`id`, `name`, `parent`, `path`, `grade`, `order`, `manager`) VALUES
(1, '经理', 0, ',1,', 1, 0, ''),
(2, '开发', 0, ',2,', 1, 0, ''),
(3, '测试', 0, ',3,', 1, 0, ''),
(4, '市场', 0, ',4,', 1, 0, ''),
(5, '产品', 1, ',1,5,', 2, 0, ''),
(6, '项目', 1, ',1,6,', 2, 0, ''),
(7, '编程', 2, ',2,7,', 2, 0, ''),
(8, '美工', 2, ',2,8,', 2, 0, '');
REPLACE INTO `zt_doclib` (`id`, `type`, `vision`, `product`, `project`, `execution`, `name`, `baseUrl`, `acl`, `groups`, `users`, `main`, `collector`, `desc`, `order`, `addedBy`, `addedDate`, `deleted`) VALUES
(2, 'product', 'rnd', 1, 0, 0, '产品主库', '', 'default', '', NULL, '1', NULL, NULL, 0, 'admin', '2024-04-02 14:30:13', '0'),
(3, 'product', 'rnd', 2, 0, 0, '产品主库', '', 'default', '', NULL, '1', NULL, NULL, 0, 'admin', '2024-04-02 14:30:55', '0'),
@@ -418,10 +418,10 @@ REPLACE INTO `zt_productplan` (`id`, `product`, `branch`, `parent`, `title`, `st
(1, 1, '0', 0, '1.0版本', 'wait', '<p><span style=\"font-size:13px;\">开发出企业网站1.0版本。</span></p>', '2030-01-01', '2030-01-01', NULL, NULL, '0', '', 'admin', '2024-04-02 14:42:47', '0');
REPLACE INTO `zt_system` (`id`, `name`, `product`, `status`, `createdBy`, `createdDate`) VALUES (1, '企业网站', 1, 'active', 'admin', '2024-04-02 14:30:12');
REPLACE INTO `zt_project` (`id`, `project`, `charter`, `model`, `type`, `category`, `lifetime`, `budget`, `budgetUnit`, `attribute`, `percent`, `milestone`, `output`, `auth`, `storyType`, `parent`, `path`, `grade`, `name`, `code`, `hasProduct`, `begin`, `end`, `firstEnd`, `realBegan`, `realEnd`, `days`, `status`, `subStatus`, `pri`, `desc`, `version`, `parentVersion`, `planDuration`, `realDuration`, `progress`, `estimate`, `left`, `consumed`, `teamCount`, `market`, `openedBy`, `openedDate`, `openedVersion`, `lastEditedBy`, `lastEditedDate`, `closedBy`, `closedDate`, `closedReason`, `canceledBy`, `canceledDate`, `suspendedDate`, `PO`, `PM`, `QD`, `RD`, `team`, `acl`, `whitelist`, `order`, `vision`, `stageBy`, `displayCards`, `fluidBoard`, `multiple`, `parallel`, `colWidth`, `minColWidth`, `maxColWidth`, `deleted`) VALUES
(1, 0, 0, '', 'program', '', '', '', 'CNY', '', 0, '0', NULL, '', '', 0, ',1,', 1, '企业管理', '', 1, '2024-04-02', '2059-12-31', NULL, '2024-04-02', NULL, 0, 'doing', '', '1', '', 0, 0, 0, 0, 0.00, 0, 0, 0, 0, 0, 'admin', '2024-04-02 14:25:12', '', '', NULL, '', NULL, '', '', NULL, NULL, '', '0', '', '', '', 'open', 'admin,projectManager', 5, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0'),
(2, 0, 0, 'scrum', 'project', '', '', '', 'CNY', '', 0, '0', NULL, 'extend', 'story,requirement', 1, ',1,2,', 2, '企业管理系统', '', 1, '2024-04-02', '2059-12-31', '2059-12-31', '2024-04-02', NULL, 0, 'doing', '', '1', '', 0, 0, 0, 0, 24.50, 52, 40, 13, 3, 0, 'admin', '2024-04-02 14:34:13', '', 'admin', '2024-04-02 14:34:13', '', NULL, '', '', NULL, NULL, '', 'projectManager', '', '', '企业管理系统', 'open', '', 10, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0'),
(3, 2, 0, '', 'sprint', '', 'long', '0', 'CNY', '', 0, '0', NULL, '', '', 2, ',2,3,', 1, '企业网站第一期', '', 1, '2024-04-02', '2024-04-15', '2024-04-15', '2024-04-02', NULL, 10, 'doing', '', '1', '', 0, 0, 0, 0, 24.50, 52, 40, 13, 0, 0, 'admin', '2024-04-02 14:35:31', '20.0.beta2', 'admin', '2024-04-02 14:35:31', '', NULL, '', '', NULL, NULL, 'productManager', 'projectManager', '', 'productManager', '企业网站第一期 ', 'open', '', 15, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0'),
(4, 2, 0, '', 'sprint', '', 'long', '0', 'CNY', '', 0, '0', NULL, '', '', 2, ',2,4,', 1, '企业网站第二期', '', 1, '2024-04-02', '2024-05-02', NULL, NULL, NULL, 23, 'wait', '', '1', '', 0, 0, 0, 0, 0.00, 0, 0, 0, 0, 0, 'admin', '2024-04-02 14:36:20', '20.0.beta2', 'admin', '2024-04-02 14:36:20', '', NULL, '', '', NULL, NULL, 'productManager', 'projectManager', 'admin', 'productManager', '企业网站第二期', 'open', '', 20, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0');
(1, 0, 0, '', 'program', '', '', 0.00, 'CNY', '', 0, '0', NULL, '', '', 0, ',1,', 1, '企业管理', '', 1, '2024-04-02', '2059-12-31', NULL, '2024-04-02', NULL, 0, 'doing', '', '1', '', 0, 0, 0, 0, 0.00, 0, 0, 0, 0, 0, 'admin', '2024-04-02 14:25:12', '', '', NULL, '', NULL, '', '', NULL, NULL, '', '0', '', '', '', 'open', 'admin,projectManager', 5, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0'),
(2, 0, 0, 'scrum', 'project', '', '', 0.00, 'CNY', '', 0, '0', NULL, 'extend', 'story,requirement', 1, ',1,2,', 2, '企业管理系统', '', 1, '2024-04-02', '2059-12-31', '2059-12-31', '2024-04-02', NULL, 0, 'doing', '', '1', '', 0, 0, 0, 0, 24.50, 52, 40, 13, 3, 0, 'admin', '2024-04-02 14:34:13', '', 'admin', '2024-04-02 14:34:13', '', NULL, '', '', NULL, NULL, '', 'projectManager', '', '', '企业管理系统', 'open', '', 10, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0'),
(3, 2, 0, '', 'sprint', '', 'long', 0.00, 'CNY', '', 0, '0', NULL, '', '', 2, ',2,3,', 1, '企业网站第一期', '', 1, '2024-04-02', '2024-04-15', '2024-04-15', '2024-04-02', NULL, 10, 'doing', '', '1', '', 0, 0, 0, 0, 24.50, 52, 40, 13, 0, 0, 'admin', '2024-04-02 14:35:31', '20.0.beta2', 'admin', '2024-04-02 14:35:31', '', NULL, '', '', NULL, NULL, 'productManager', 'projectManager', '', 'productManager', '企业网站第一期 ', 'open', '', 15, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0'),
(4, 2, 0, '', 'sprint', '', 'long', 0.00, 'CNY', '', 0, '0', NULL, '', '', 2, ',2,4,', 1, '企业网站第二期', '', 1, '2024-04-02', '2024-05-02', NULL, NULL, NULL, 23, 'wait', '', '1', '', 0, 0, 0, 0, 0.00, 0, 0, 0, 0, 0, 'admin', '2024-04-02 14:36:20', '20.0.beta2', 'admin', '2024-04-02 14:36:20', '', NULL, '', '', NULL, NULL, 'productManager', 'projectManager', 'admin', 'productManager', '企业网站第二期', 'open', '', 20, 'rnd', 'product', 0, '0', '1', 0, 264, 200, 384, '0');
REPLACE INTO `zt_projectadmin` (`group`, `account`, `programs`, `projects`, `products`, `executions`) VALUES
(1, 'admin', NULL, '2', NULL, NULL);
REPLACE INTO `zt_projectcase` (`project`, `product`, `case`, `count`, `version`, `order`) VALUES
@@ -674,16 +674,16 @@ REPLACE INTO `zt_testrun` (`id`, `task`, `case`, `version`, `assignedTo`, `lastR
(4, 1, 4, 1, '', 'admin', '2024-04-02 16:30:26', 'pass', 'normal');
REPLACE INTO `zt_testtask` (`id`, `project`, `product`, `name`, `execution`, `build`, `type`, `owner`, `pri`, `begin`, `end`, `realBegan`, `realFinishedDate`, `mailto`, `desc`, `report`, `status`, `testreport`, `auto`, `subStatus`, `createdBy`, `createdDate`, `deleted`, `members`) VALUES
(1, 2, 1, '企业网站第一期测试任务', 3, '1', 'integrate', 'admin', 1, '2024-04-02', '2024-04-05', NULL, NULL, '', '', NULL, 'wait', 0, 'no', '', 'admin', '2024-04-02 16:26:35', '0', '');
REPLACE INTO `zt_user` (`company`, `dept`, `account`, `type`, `password`, `role`, `realname`, `nickname`, `commiter`, `avatar`, `birthday`, `gender`, `email`, `skype`, `qq`, `mobile`, `phone`, `weixin`, `dingding`, `slack`, `whatsapp`, `address`, `zipcode`, `nature`, `analysis`, `strategy`, `join`, `visits`, `ip`, `last`, `fails`, `locked`, `ranzhi`, `score`, `scoreLevel`, `deleted`) VALUES
(0, 5, 'productManager', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'po', '产品经理', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 3, '192.168.0.8', 1338866083, 0, null, '', 0, 0, '0'),
(0, 6, 'projectManager', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'pm', '项目经理', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 4, '10.0.2.2', 1619580798, 0, null, '', 0, 0, '0'),
(0, 2, 'dev1', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'dev', '开发甲', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 3, '10.0.2.2', 1619586993, 0, null, '', 0, 0, '1'),
(0, 2, 'dev2', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'dev', '开发乙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 2, '10.0.2.2', 1619587173, 0, null, '', 0, 0, '1'),
(0, 2, 'dev3', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'dev', '开发丙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 2, '10.0.2.2', 1619587220, 0, null, '', 0, 0, '1'),
(0, 3, 'tester1', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qa', '测试甲', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 3, '192.168.0.8', 1338865739, 0, null, '', 0, 0, '1'),
(0, 3, 'tester2', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qa', '测试乙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 2, '192.168.0.8', 1338865450, 0, null, '', 0, 0, '1'),
(0, 3, 'tester3', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qa', '测试丙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 1, '192.168.0.8', 1338865125, 0, null, '', 0, 0, '1'),
(0, 1, 'testManager', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qd', '测试经理', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 6, '192.168.0.8', 1338865842, 0, null, '', 0, 0, '1');
REPLACE INTO `zt_user` (`company`, `dept`, `account`, `type`, `password`, `role`, `realname`, `nickname`, `commiter`, `avatar`, `birthday`, `gender`, `email`, `skype`, `qq`, `mobile`, `phone`, `weixin`, `dingding`, `slack`, `whatsapp`, `address`, `zipcode`, `nature`, `analysis`, `strategy`, `join`, `visits`, `ip`, `fails`, `locked`, `ranzhi`, `score`, `scoreLevel`, `deleted`) VALUES
(0, 5, 'productManager', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'po', '产品经理', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 3, '192.168.0.8', 0, null, '', 0, 0, '0'),
(0, 6, 'projectManager', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'pm', '项目经理', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 4, '10.0.2.2', 0, null, '', 0, 0, '0'),
(0, 2, 'dev1', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'dev', '开发甲', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 3, '10.0.2.2', 0, null, '', 0, 0, '1'),
(0, 2, 'dev2', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'dev', '开发乙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 2, '10.0.2.2', 0, null, '', 0, 0, '1'),
(0, 2, 'dev3', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'dev', '开发丙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 2, '10.0.2.2', 0, null, '', 0, 0, '1'),
(0, 3, 'tester1', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qa', '测试甲', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 3, '192.168.0.8', 0, null, '', 0, 0, '1'),
(0, 3, 'tester2', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qa', '测试乙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 2, '192.168.0.8', 0, null, '', 0, 0, '1'),
(0, 3, 'tester3', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qa', '测试丙', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 1, '192.168.0.8', 0, null, '', 0, 0, '1'),
(0, 1, 'testManager', 'inside', 'e10adc3949ba59abbe56e057f20f883e', 'qd', '测试经理', '', '', '', null, 'm', '', '', '', '', '', '', '', '', '', '', '', '', '', '', null, 6, '192.168.0.8', 0, null, '', 0, 0, '1');
REPLACE INTO `zt_usergroup` (`account`, `group`, `project`) VALUES
('productManager', 5, ''),
('projectManager', 4, ''),
@@ -704,4 +704,4 @@ REPLACE INTO `zt_userview` (`account`, `programs`, `projects`, `products`, `spri
('tester1', '', '', '', ''),
('tester2', '', '', '', ''),
('tester3', '', '', '', ''),
('testManager', '', '', '', '');
('testManager', '', '', '', '');
+22
View File
@@ -45,3 +45,25 @@ BEGIN
-- 返回res
RETURN res;
END FIND_IN_SET;
/
CREATE OR REPLACE FUNCTION "IF"(
p_condition BOOLEAN, -- 判断条件
p_true_val ANYTYPE, -- true分支返回值(任意类型)
p_false_val ANYTYPE -- false分支返回值(任意类型)
) RETURNS ANYTYPE
AS
BEGIN
-- 仅校验条件非空,不校验类型
IF p_condition IS NULL THEN
RAISE_APPLICATION_ERROR(-20001, '判断条件不能为NULL');
END IF;
-- 核心逻辑:直接返回不同类型值
IF p_condition THEN
RETURN p_true_val; -- 如:字符串
ELSE
RETURN p_false_val; -- 如:数值
END IF;
END;
/
+54 -6
View File
@@ -256,7 +256,7 @@ END;
$$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION IF(
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val BOOLEAN,
false_val BOOLEAN
@@ -272,7 +272,7 @@ $$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION IF(
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val DOUBLE PRECISION,
false_val INTEGER
@@ -288,7 +288,55 @@ $$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION IF(
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val DOUBLE PRECISION,
false_val DOUBLE PRECISION
) RETURNS DOUBLE PRECISION AS $$
BEGIN
IF condition THEN
RETURN true_val;
ELSE
RETURN false_val;
END IF;
END;
$$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val TEXT,
false_val TEXT
) RETURNS TEXT AS $$
BEGIN
IF condition THEN
RETURN true_val;
ELSE
RETURN false_val;
END IF;
END;
$$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val INTEGER,
false_val INTEGER
) RETURNS INTEGER AS $$
BEGIN
IF condition THEN
RETURN true_val;
ELSE
RETURN false_val;
END IF;
END;
$$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val TEXT,
false_val DATE
@@ -304,7 +352,7 @@ $$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION IF(
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val DATE,
false_val DATE
@@ -320,7 +368,7 @@ $$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION IF(
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val timestamp without time zone,
false_val DATE
@@ -336,7 +384,7 @@ $$ LANGUAGE plpgsql;
--
CREATE OR REPLACE FUNCTION IF(
CREATE OR REPLACE FUNCTION "IF"(
condition BOOLEAN,
true_val timestamp without time zone,
false_val timestamp without time zone
+2
View File
@@ -109,6 +109,7 @@ CREATE TABLE `zt_ai_miniprogram` (
`category` varchar(30) NOT NULL,
`desc` text,
`model` varchar(255) NOT NULL DEFAULT '',
`knowledgeLib` varchar(255) NOT NULL DEFAULT '' COMMENT '关联的知识库ID列表',
`icon` varchar(30) NOT NULL DEFAULT 'writinghand-7',
`createdBy` varchar(30) NOT NULL,
`createdDate` datetime NOT NULL,
@@ -160,6 +161,7 @@ CREATE TABLE `zt_ai_prompt` (
`name` varchar(20) NOT NULL,
`desc` text,
`model` varchar(255) NOT NULL DEFAULT '',
`knowledgeLib` varchar(255) NOT NULL DEFAULT '' COMMENT '关联的知识库ID列表',
`module` varchar(30) DEFAULT NULL,
`source` text,
`targetForm` varchar(30) DEFAULT NULL,
File diff suppressed because it is too large Load Diff
-7
View File
@@ -2921,13 +2921,6 @@ CHANGE `reviewedDate` `reviewedDate` datetime NULL,
CHANGE `level` `level` tinyint(3) NOT NULL DEFAULT '0',
CHANGE `assignedTo` `assignedTo` varchar(30) NOT NULL DEFAULT '';
ALTER TABLE `zt_holiday`
CHANGE `name` `name` varchar(30) NOT NULL DEFAULT '',
CHANGE `type` `type` enum('holiday', 'working') NOT NULL DEFAULT 'holiday',
CHANGE `year` `year` char(4) NOT NULL DEFAULT '',
CHANGE `begin` `begin` date NULL,
CHANGE `end` `end` date NULL;
ALTER TABLE `zt_leave`
CHANGE `year` `year` char(4) NOT NULL DEFAULT '',
CHANGE `begin` `begin` date NULL,
+1
View File
@@ -12,6 +12,7 @@ DROP INDEX account_vision_module_type_order ON `zt_block`;
UPDATE `zt_block` SET `dashboard` = CONCAT(`module`, `type`);
UPDATE `zt_block` SET `module` = IF(`source` != '', `source`, `code`);
UPDATE `zt_block` SET `width` = IF(`grid` > 4, '2', '1');
UPDATE `zt_block` SET `left` = '2' WHERE `width` = '1';
UPDATE `zt_block` SET `params` = '{"count":"20"}' WHERE `module` = 'assigntome' AND `code` = 'assigntome';
ALTER TABLE `zt_block` DROP COLUMN `source`;
+1 -1
View File
@@ -29,4 +29,4 @@ REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `
ALTER TABLE `zt_doc` CHANGE `lib` `lib` mediumint(8) unsigned NOT NULL DEFAULT '0';
ALTER TABLE `zt_risk` CHANGE `project` `project` mediumint(8) unsigned NOT NULL DEFAULT '0';
ALTER TABLE `zt_issue` CHANGE `project` `project` mediumint(8) unsigned NOT NULL DEFAULT '0';
ALTER TABLE `zt_issue` CHANGE `project` `project` mediumint(8) unsigned NOT NULL DEFAULT '0';
+9 -11
View File
@@ -1,11 +1,7 @@
UPDATE `zt_risk` SET `pri` = 3 WHERE `pri` = 'middle';
UPDATE `zt_risk` SET `pri` = 1 WHERE `pri` = 'high';
UPDATE `zt_risk` SET `pri` = 2 WHERE `pri` = 'low';
ALTER TABLE `zt_demand` CHANGE `version` `version` smallint unsigned NOT NULL DEFAULT '1';
ALTER TABLE `zt_demand` CHANGE `pri` `pri` tinyint unsigned NOT NULL DEFAULT 3;
ALTER TABLE `zt_issue` CHANGE `pri` `pri` tinyint unsigned NOT NULL DEFAULT 3;
ALTER TABLE `zt_risk` CHANGE `pri` `pri` tinyint unsigned NOT NULL DEFAULT 2;
ALTER TABLE `zt_risk` CHANGE `pri` `pri` tinyint unsigned NOT NULL DEFAULT 3;
ALTER TABLE `zt_opportunity` CHANGE `pri` `pri` tinyint unsigned NOT NULL DEFAULT 3;
ALTER TABLE `zt_design` CHANGE `project` `project` int unsigned NOT NULL DEFAULT 0;
ALTER TABLE `zt_design` CHANGE `product` `product` int unsigned NOT NULL DEFAULT 0;
@@ -19,11 +15,11 @@ ALTER TABLE `zt_doc` MODIFY `module` int unsigned NOT NULL DEFAULT 0;
CREATE TABLE `zt_testtaskproduct` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`product` int unsigned NOT NULL default 0 COMMENT '所属产品',
`build` int unsigned NOT NULL default 0 COMMENT '所属构建',
`task` int unsigned NOT NULL default 0 COMMENT '所属测试单',
`execution` int unsigned NOT NULL default 0 COMMENT '所属执行',
`project` int unsigned NOT NULL default 0 COMMENT '所属项目',
`product` int unsigned NOT NULL DEFAULT 0 COMMENT '所属产品',
`build` int unsigned NOT NULL DEFAULT 0 COMMENT '所属构建',
`task` int unsigned NOT NULL DEFAULT 0 COMMENT '所属测试单',
`execution` int unsigned NOT NULL DEFAULT 0 COMMENT '所属执行',
`project` int unsigned NOT NULL DEFAULT 0 COMMENT '所属项目',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE UNIQUE INDEX `uk_productbuild` ON `zt_testtaskproduct` (`product`,`build`,`task`);
@@ -32,4 +28,6 @@ ALTER TABLE `zt_testtask` ADD `joint` tinyint unsigned NOT NULL DEFAULT 0 COMMEN
UPDATE `zt_testtask` SET `build` = 0 WHERE `build` = 'trunk' OR `build` = '' OR `build` IS NULL;
ALTER TABLE `zt_testtask` MODIFY `build` int unsigned NOT NULL DEFAULT 0;
ALTER TABLE `zt_testtask` MODIFY `build` int unsigned NOT NULL DEFAULT 0;
DROP TABLE IF EXISTS `zt_session`;
+2144
View File
File diff suppressed because it is too large Load Diff
+12035 -11969
View File
File diff suppressed because it is too large Load Diff
+90
View File
@@ -1,3 +1,93 @@
2026-01-08 21.7.8
完成的需求
企业版:
79153 调整大桌面二级导航为固定展示
79067 实现添加禅道发布的功能
79052 新增“需求评审”为内置禅道智能体
79051 结果处理新增“不返回禅道表单”选项
79050 智能会话返回表单样式
79049 调试时的示例数据
79048 前台禅道智能体入口样式优化
79047 调试交互优化
79046 列表页面增加卡片视图
78890 实现知识库的数据库设计
78885 禅道对象实现知识的列表展示
78851 实现禅道对象列表数据的更新列表数据功能
78849 实现自定义文本查看原数据的功能
78848 自定义文本和本地文件实现知识的向量化分段展示
78847 实现下架知识库的功能
78845 实现从文档库导入的功能
78844 实现卡片视图分组树
78659 实现数据向量化的权限控制
78658 实现知识库的后台权限控制
78657 实现通用智能体加载知识库的功能
78656 实现禅道智能体加载知识库的功能
78655 实现智能会话挂载知识库的功能
78653 实现知识库的搜索测试功能
78652 实现知识库的AI问答功能
78651 实现知识的向量化功能和等待页面
78650 实现知识的删除功能
78649 实现添加知识按钮功能
78648 实现添加禅道计划的功能
78647 实现添加禅道反馈、工单的功能
78646 实现添加禅道文档的功能
78645 实现添加禅道Bug的功能
78644 实现添加禅道用例的功能
78643 实现添加禅道任务的功能
78642 实现添加禅道需求的功能
78641 实现添加本地文件的功能
78640 实现添加自定义文本的弹窗和功能
78639 实现空知识库的添加知识页面
78636 实现知识标题的编辑功能
78635 实现知识的标题和功能操作区域
78634 实现知识按照知识类型分组展示
78633 实现知识库标题的展示和知识库功能按钮
78627 实现我的知识库管理页面
78626 实现组织知识库缺省页面
78625 实现删除知识库的功能
78624 实现发布知识库的功能
78623 实现编辑知识库的功能
78622 实现创建知识库的功能
78621 实现卡片视图检索标签和搜索
78620 实现知识库卡片视图
78616 二级菜单新增知识库模块和三级菜单
修复的Bug
开源版:
67786 对产品等对象进行操作时提示日期应当为合法日期
67787 挂起项目失败,提示日期类型不对
67850 创建项目保存时有sql报错
67854 项目用例列表点击零用例需求标签后有sql报错
67855 创建项目集/项目弹窗报错
67872 创建子计划有报错
67927 编辑测试单报错
67930 测试单的分组视图报错
67984 进入执行-需求列表报错
67985 项目构建关联研发需求报错
企业版:
64794 播放附件中的视频,页面卡死
66284 任务的类型属性显示为type
66285 任务润色时,属性样式不一致
66330 自定义的禅道智能体无法创建子任务
66427 文档对比页面产品发布/计划研发需求/计划Bug列表显示链接
66489 markdown导出为HTML后,样式不正确
67222 企业版oceanbase数据库安装报错
67531 运营界面全局添加按钮显示立项
67564 AI会话返回的表单中未做表单项标题过长显示处理
67566 开启LDAP后用户登录有报错
67567 日志汇总表有报错
67661 待处理反馈列表搜索后批量编辑反馈三级导航缺失
67665 添加知识报错
67688 Bug报表导出报错
67757 按照状态=草稿搜索知识库,状态没有记录
67767 导入知识库后,提示没有权限查看
67775 调试禅道智能体提示表单结构获取失败
67801 添加本地文件有报错
67821 本地文件上传为知识后,查看显示乱码
67840 添加项目发布列表的知识有报错
67999 知识库详情,修改知识名称可能无法保存
旗舰版:
65355 BI透视表中,组织下的列表会重复计算父任务工时信息 (Feedback#10310)
2025-11-26 21.7.7
完成的需求
开源版:
+2 -2
View File
@@ -2,13 +2,13 @@ Z PUBLIC LICENSE 1.2
许可
Z PUBLIC LICENSE 由禅道软件(青岛)有限公司(www.easycorp.cn)起草,简称ZPL协议。
Z PUBLIC LICENSE 由禅道软件(青岛)有限公司(www.chandao.com)起草,简称ZPL协议。
任何人均可使用该协议来发布开源软件,并可对下面协议正文中以下划线标注的空白部分做相应修改,
除此之外的任何内容不得做任何修改。禅道软件(青岛)有限公司拥有对该协议条款的最终解释权。
前言:
禅道迅捷版软件(以下简称该软件)由 禅道软件(青岛)有限公司(www.easycorp.cn)开发(以下简称我)。我依法拥有该软件的所有版权。
禅道迅捷版软件(以下简称该软件)由 禅道软件(青岛)有限公司(www.chandao.com)开发(以下简称我)。我依法拥有该软件的所有版权。
本着共享开放的角度,我以开放源代码的形式发布该软件。您可以在遵守该协议的前提下使用该软件。
自您安装该软件开始,您和我之间的合同关系自动成立。除非您停止使用该软件或与我有签署额外合同,
您须认真遵循该授权协议约定的每一条款。
@@ -239,9 +239,12 @@ div
setClass('bg-canvas'),
zui::kanbanList
(
set('$replace', false),
set::key('kanban'),
set::items($kanbanList),
set::height('calc(100vh - 120px)')
set::height('calc(100vh - 120px)'),
set::selectable(true),
set::showLinkOnSelected(true)
)
);
@@ -870,41 +870,6 @@ $lang->webhook->methodOrder[25] = 'log';
$lang->webhook->methodOrder[30] = 'bind';
$lang->webhook->methodOrder[35] = 'chooseDept';
/* AI methods. */
$lang->resource->ai = new stdclass();
$lang->resource->ai->models = 'modelBrowse';
$lang->resource->ai->modelView = 'modelView';
$lang->resource->ai->modelCreate = 'modelCreate';
$lang->resource->ai->modelEdit = 'modelEdit';
$lang->resource->ai->modelEnable = 'modelEnable';
$lang->resource->ai->modelDisable = 'modelDisable';
$lang->resource->ai->modelDelete = 'modelDelete';
$lang->resource->ai->modelTestConnection = 'modelTestConnection';
$lang->resource->ai->promptPublish = 'promptPublish';
$lang->resource->ai->promptUnpublish = 'promptUnpublish';
$lang->resource->ai->prompts = 'promptBrowse';
$lang->resource->ai->promptView = 'promptView';
$lang->resource->ai->promptExecute = 'promptExecute';
$lang->resource->ai->promptExecutionReset = 'promptExecutionReset';
$lang->resource->ai->chat = 'chat';
$lang->resource->ai->createMiniProgram = 'createMiniProgram';
$lang->resource->ai->editMiniProgram = 'editMiniProgram';
$lang->resource->ai->testMiniProgram = 'testMiniProgram';
$lang->resource->ai->miniPrograms = 'miniProgramList';
$lang->resource->ai->miniProgramView = 'miniProgramView';
$lang->resource->ai->publishMiniProgram = 'publishMiniProgram';
$lang->resource->ai->unpublishMiniProgram = 'unpublishMiniProgram';
$lang->resource->ai->deleteMiniProgram = 'deleteMiniProgram';
$lang->resource->ai->exportMiniProgram = 'exportMiniProgram';
$lang->resource->ai->importMiniProgram = 'importMiniProgram';
$lang->resource->ai->editMiniProgramCategory = 'editMiniProgramCategory';
$lang->resource->aiapp = new stdclass();
$lang->resource->aiapp->square = 'miniProgramSquare';
$lang->resource->aiapp->view = 'view';
$lang->resource->aiapp->miniProgramChat = 'miniProgramChat';
$lang->resource->aiapp->collectMiniProgram = 'collectMiniProgram';
/* Others. */
$lang->resource->file = new stdclass();
$lang->resource->file->download = 'download';
@@ -9,3 +9,6 @@ $lang->project->aclList['private'] = "Private (Accessible to {$lang->projectComm
$lang->project->storyPoints = 'Target Size';
$lang->project->storyCount = 'Target Count';
$lang->project->executionCount = 'Kanban Count';
$lang->project->copyProject->basicInfo = $lang->projectCommon . ' data (Project name, Project code)';
$lang->project->copyProject->kanbanAllList = array($lang->projectCommon . ' data', 'Kanban and Target data', 'Project/Kanban doc lib and catalog', 'Task data', 'Team member');
@@ -9,3 +9,6 @@ $lang->project->aclList['private'] = "Private (Accessible to {$lang->projectComm
$lang->project->storyPoints = 'Target Size';
$lang->project->storyCount = 'Target Count';
$lang->project->executionCount = 'Kanban Count';
$lang->project->copyProject->basicInfo = $lang->projectCommon . ' data (Project name, Project code)';
$lang->project->copyProject->kanbanAllList = array($lang->projectCommon . ' data', 'Kanban and Target data', 'Project/Kanban doc lib and catalog', 'Task data', 'Team member');
@@ -9,3 +9,6 @@ $lang->project->aclList['private'] = "Private (Accessible to {$lang->projectComm
$lang->project->storyPoints = 'Target Size';
$lang->project->storyCount = 'Target Count';
$lang->project->executionCount = 'Kanban Count';
$lang->project->copyProject->basicInfo = $lang->projectCommon . ' data (Project name, Project code)';
$lang->project->copyProject->kanbanAllList = array($lang->projectCommon . ' data', 'Kanban and Target data', 'Project/Kanban doc lib and catalog', 'Task data', 'Team member');
@@ -9,3 +9,6 @@ $lang->project->aclList['private'] = "私有 (只有{$lang->projectCommon}负责
$lang->project->storyPoints = '目标规模';
$lang->project->storyCount = '目标条目数';
$lang->project->executionCount = '看板数';
$lang->project->copyProject->basicInfo = $lang->projectCommon . '数据(' . $lang->projectCommon . '名称,' . $lang->projectCommon . '代号)';
$lang->project->copyProject->kanbanAllList = array($lang->projectCommon . '自身的数据', $lang->projectCommon . '所包含的看板和目标', $lang->projectCommon . '和看板的文档目录', $lang->projectCommon . '看板所包含的任务', '团队成员安排');
+13 -4
View File
@@ -25,6 +25,12 @@ $checkDeltaChecked = jsCallback()->do(<<<'JS'
if($('#delta' + days).length > 0 && days != 999) $('#delta' + days).prop('checked', true);
JS);
$projectEnd = $copyProjectID ? $copyProject->end : '';
$isLongTime = $projectEnd == LONG_TIME;
$delta = 0;
if($copyProjectID) $delta = $isLongTime ? 999 : (strtotime($copyProject->end) - strtotime($copyProject->begin)) / 3600 / 24 + 1;
formPanel
(
to::heading(div
@@ -89,8 +95,9 @@ formPanel
set::name('end'),
set('id', 'end'),
set::placeholder($lang->project->end),
set::value($copyProjectID ? $copyProject->end : ''),
set::required(true)
set::value($projectEnd),
set::required(true),
set::disabled($isLongTime)
)
)
),
@@ -102,7 +109,8 @@ formPanel
(
set::name('delta'),
set::inline(true),
set::items($lang->project->endList)
set::items($lang->project->endList),
set::value($delta)
)
)
),
@@ -134,7 +142,8 @@ formPanel
(
set::name('whitelist[]'),
set::items($users),
set::multiple(true)
set::multiple(true),
set::value($copyProjectID ? $copyProject->whitelist : '')
)
),
formHidden('LONG_TIME', LONG_TIME),
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -163,11 +163,11 @@ class baseEntry
* Send error response
*
* @param int $code
* @param string|object $msg
* @param string|object|array $msg
* @access public
* @return string
*/
public function sendError(int $code, string|object $msg)
public function sendError(int $code, string|object|array $msg)
{
$response = new stdclass();
$response->error = $msg;
+1 -1
View File
@@ -207,7 +207,7 @@ class helper extends baseHelper
/* The requestTypes are: GET, PATH_INFO2, PATH_INFO */
if($config->requestType == 'GET')
{
$link = $config->webRoot . (string) substr($link, 2);
$link = $config->webRoot . (string) substr($link, 1);
}
elseif($config->requestType == 'PATH_INFO2')
{
+226 -26
View File
@@ -78,11 +78,6 @@ class api extends router
*/
public function __construct(string $appName = 'api', string $appRoot = '')
{
parent::__construct($appName, $appRoot);
$this->viewType = 'json';
$this->httpMethod = strtolower((string) $_SERVER['REQUEST_METHOD']);
$this->path = trim(substr((string) $_SERVER['REQUEST_URI'], strpos((string) $_SERVER['REQUEST_URI'], 'api.php') + 7), '/');
if(strpos($this->path, '?') > 0) $this->path = strstr($this->path, '?', true);
@@ -90,6 +85,10 @@ class api extends router
$this->apiVersion = $subPos !== false ? substr($this->path, 0, $subPos) : '';
$this->path = $subPos !== false ? substr($this->path, $subPos) : '';
parent::__construct($appName, $appRoot);
$this->viewType = 'json';
$this->httpMethod = strtolower((string) $_SERVER['REQUEST_METHOD']);
$this->loadApiLang();
}
@@ -242,14 +241,18 @@ class api extends router
*
* @param array $routes
* @access private
* @return void
* @return string
*/
public function parseRouteV2($routes)
{
$methodName = '';
list($info, $paramValues) = $this->matchRoutes($routes);
if($info)
{
if(isset($info['method'])) $methodName = $info['method'];
if(isset($info['redirect']))
{
foreach($paramValues as $key => $value)
@@ -271,9 +274,10 @@ class api extends router
}
list($info, $paramValues) = $this->matchRoutes($routes);
if(isset($info['method'])) $methodName = $info['method'];
}
if(isset($info['response'])) $this->responseExtractor = $info['response'];
if(isset($info['response']) && $this->responseExtractor == '*') $this->responseExtractor = $info['response'];
}
foreach($paramValues as $key => $value)
@@ -281,6 +285,8 @@ class api extends router
if(is_numeric($key)) continue;
$_GET[$key] = $value;
}
return $methodName;
}
/**
@@ -295,7 +301,8 @@ class api extends router
{
$this->action = strtolower((string) $_SERVER['REQUEST_METHOD']);
if($this->action == 'get') $this->parseRouteV2($routes);
$methodName = '';
if($this->action == 'get') $methodName = $this->parseRouteV2($routes);
$pathItems = explode('/', trim($this->path, '/'));
$moduleName = $this->singular($pathItems[0]);
@@ -307,8 +314,6 @@ class api extends router
'delete' => 'delete'
);
$methodName = $actionToMethod[$this->action];
if(isset($pathItems[1]))
{
if(is_numeric($pathItems[1]))
@@ -328,14 +333,19 @@ class api extends router
}
}
if(isset($pathItems[2]))
{
$methodName = $pathItems[2];
}
if(isset($pathItems[2])) $methodName = $pathItems[2];
if(!$methodName) $methodName = $actionToMethod[$this->action];
$this->setModuleName($moduleName);
$this->setMethodName($methodName);
$this->setControlFile();
/* Set default params and post data to delete.*/
if($this->action == 'delete')
{
$defaultParams = $this->getDefaultParams();
if(isset($defaultParams['confirm'])) $_GET['confirm'] = 'yes';
}
}
/**
@@ -380,6 +390,152 @@ class api extends router
}
}
/**
* 检查传入的对象是否有权限访问
*
* Check object priv.
*
* @param object $object
* @param string $table
* @access public
* @return bool
*/
public function checkObjectPriv(object $object, string $table): bool
{
if($this->user->admin) return true;
$userView = $this->user->view;
switch($table)
{
case TABLE_STORY:
case TABLE_BUG:
case TABLE_CASE:
case TABLE_TICKET:
case TABLE_FEEDBACK:
case TABLE_PRODUCTPLAN:
return (!$object->product || strpos(",{$userView->products},", ",$object->product,") !== false);
case TABLE_PRODUCT:
return (!$object->id || strpos(",{$userView->products},", ",$object->id,") !== false);
case TABLE_PROJECT: // project,execution,program
$projects = ",{$userView->sprints},{$userView->projects},{$userView->programs},";
return (!$object->id || strpos($projects, ",$object->id,") !== false);
case TABLE_BUILD:
case TABLE_TASK:
return (!$object->execution || strpos(",{$userView->sprints},", ",$object->execution,") !== false);
default:
return true;
}
return false;
}
/**
* 检查传入的对象是否可以访问
*
* Check access.
*
* @access public
* @return void
*/
public function checkAccess()
{
$objectMap = [
'program' => TABLE_PROJECT,
'programID' => TABLE_PROJECT,
'product' => TABLE_PRODUCT,
'products' => TABLE_PRODUCT,
'productID' => TABLE_PRODUCT,
'project' => TABLE_PROJECT,
'projectID' => TABLE_PROJECT,
'productplan' => TABLE_PRODUCTPLAN,
'productplanID' => TABLE_PRODUCTPLAN,
'plan' => TABLE_PRODUCTPLAN,
'planID' => TABLE_PRODUCTPLAN,
'execution' => TABLE_PROJECT,
'executionID' => TABLE_PROJECT,
'story' => TABLE_STORY,
'storyID' => TABLE_STORY,
'epic' => TABLE_STORY,
'epicID' => TABLE_STORY,
'requirement' => TABLE_STORY,
'requirementID' => TABLE_STORY,
'task' => TABLE_TASK,
'taskID' => TABLE_TASK,
'bug' => TABLE_BUG,
'bugID' => TABLE_BUG,
'feedback' => TABLE_FEEDBACK,
'feedbackID' => TABLE_FEEDBACK,
'build' => TABLE_BUILD,
'buildID' => TABLE_BUILD,
'case' => TABLE_CASE,
'caseID' => TABLE_CASE,
'testcase' => TABLE_CASE,
'testcaseID' => TABLE_CASE,
'user' => TABLE_USER,
'userID' => TABLE_USER,
'ticket' => TABLE_TICKET,
'ticketID' => TABLE_TICKET,
'dept' => TABLE_DEPT,
'deptID' => TABLE_DEPT,
];
/* Check assignedTo. */
if(isset($_POST['assignedTo']) && $_POST['assignedTo'])
{
$user = $this->dao->select('*')->from(TABLE_USER)
->where('account')->eq($_POST['assignedTo'])
->fetch();
if(!$user) return $this->control->sendError('User does not exist.');
}
$params = array_merge($this->params, $_POST);
foreach($params as $key => $value)
{
if(!isset($objectMap[$key]) || !$value) continue;
$table = $objectMap[$key];
$result = $this->checkObjectExists($table, $value);
if($result === false) return $this->control->sendError(ucfirst(str_replace('ID', '', $key)) . ' does not exist.');
foreach($result as $object)
{
if(!$this->checkObjectPriv($object, $table)) return $this->control->sendError(ucfirst(str_replace('ID', '', $key)) . ' is not allowed.');
}
}
}
/**
* 检查对象是否存在
*
* Check object exists.
*
* @param string $table
* @param int $objectID
* @access public
* @return array|false
*/
public function checkObjectExists($table, $objectIDList)
{
if(!is_array($objectIDList)) $objectIDList = [$objectIDList];
$objects = [];
foreach($objectIDList as $objectID)
{
$object = $this->dao->select('*')->from($table)
->where('id')->eq($objectID)
->beginIF(!in_array($table, [TABLE_DEPT]))->andWhere('deleted')->eq('0')->fi()
->fetch();
if(!$object) return false;
$objects[] = $object;
}
return $objects;
}
/**
* 执行对应模块
*
@@ -396,10 +552,16 @@ class api extends router
if($this->apiVersion == 'v2')
{
$this->setParams();
if(in_array($this->action, array('post', 'put')))
if(in_array($this->action, array('post', 'put', 'delete')))
{
$this->setFormData();
}
else
{
$this->checkAccess();
}
return parent::loadModule();
}
elseif(!$this->apiVersion)
@@ -442,14 +604,19 @@ class api extends router
public function setFormData()
{
$requestBody = file_get_contents("php://input");
if(!$requestBody) return;
$_POST = json_decode($requestBody, true);
/* 更新操作的表单需要拼接原始的值。 Merge original values. */
/* Get form data by get request. */
$postData = $_POST;
$_POST = array();
/* Avoid empty post body. */
if(in_array($this->control->moduleName, ['feedback', 'ticket']))
{
$_POST['uid'] = '1';
}
else
{
$_POST['verifyPassword'] = '1';
}
/* 以POST的值为准。 Set GET value from POST data. */
foreach($_POST as $key => $value)
@@ -457,18 +624,32 @@ class api extends router
if(isset($this->params[$key])) $this->params[$key] = $value;
}
$this->checkAccess();
/* 其他方法不需要从GET页面获取post data。Other request directly. */
if(!in_array($this->methodName, ['create', 'edit'])) return;
/* 更新操作的表单需要拼接原始的值。 Merge original values. */
/* Get form data by get request. */
$postData = $_POST;
$_POST = array();
$this->control->viewType = 'html';
$this->control->getFormData = true;
$zen = $this->control->moduleName . 'Zen';
$this->control->$zen->getFormData = true;
if(isset($this->control->$zen)) $this->control->$zen->getFormData = true;
$method = $this->control->methodName;
$control = $this->control; // fetch method will change control.
$method = $this->control->methodName;
call_user_func_array(array($this->control, $method), $this->params);
/* Clean the output in get method. */
ob_clean();
$this->control->getFormData = false;
$this->control->$zen->getFormData = false;
$this->control->viewType = 'json';
$this->control = $control;
$_POST = $postData;
foreach($this->control->formData as $key => $value)
@@ -476,9 +657,13 @@ class api extends router
if(!isset($_POST[$key])) $_POST[$key] = $value;
}
foreach($this->control->$zen->formData as $key => $value)
if(isset($this->control->$zen))
{
if(!isset($_POST[$key])) $_POST[$key] = $value;
$this->control->$zen->getFormData = false;
foreach($this->control->$zen->formData as $key => $value)
{
if(!isset($_POST[$key])) $_POST[$key] = $value;
}
}
}
@@ -520,7 +705,8 @@ class api extends router
}
else
{
$this->params[$key] = $defaultItem['default'];
/* Browse all items in api mode defaultly. */
$this->params[$key] = in_array($key, ['browseType', 'status']) ? 'all' : $defaultItem['default'];
}
}
@@ -627,4 +813,18 @@ class api extends router
$this->config->vision = $vision ? $vision : $defaultVision;
}
/**
* 设置超级变量。
* Set the super vars.
*
* @access public
* @return void
*/
public function setSuperVars()
{
$this->config->framework->filterCSRF = false;
parent::setSuperVars();
}
}
+1 -1
View File
@@ -1026,7 +1026,7 @@ class baseControl
$context->data = (array)$this->view;
$context->data['zinDebug'] = array();
if($this->config->debug && $this->config->debug >= 2 && $this->config->installed)
if($this->config->debug && $this->config->debug >= 2 && $this->config->installed && $this->config->db->driver == 'mysql')
{
$context->data['zinDebug']['trace'] = $this->app->loadClass('trace')->getTrace();
}
+5 -5
View File
@@ -107,11 +107,7 @@ class baseHelper
}
/* 生成url链接的开始部分。Set the begin parts of the link. */
if($app->apiVersion == 'v2')
{
$link = $config->webRoot . '/api.php/v2/';
}
elseif($config->requestType == 'PATH_INFO2')
if($config->requestType == 'PATH_INFO2')
{
$link = '/';
}
@@ -119,6 +115,10 @@ class baseHelper
{
$link = $config->webRoot . $appName;
}
elseif($app->apiVersion)
{
$link = $config->webRoot . 'index.php';
}
else
{
$link = $config->webRoot . $appName . basename((string) $_SERVER['SCRIPT_NAME']);
+4 -4
View File
@@ -806,7 +806,7 @@ class baseRouter
if(isset($_SERVER['REQUEST_SCHEME']) and strtolower((string) $_SERVER['REQUEST_SCHEME']) == 'https') $httpType = 'https';
$httpHost = zget($_SERVER, 'HTTP_HOST', '');
$apiMode = (defined('RUN_MODE') && RUN_MODE == 'api') || isset($_GET[$this->config->sessionVar]);
$apiMode = $this->apiVersion || isset($_GET[$this->config->sessionVar]);
if(!$apiMode && (empty($httpHost) or !str_starts_with((string) $this->server->http_referer, "$httpType://$httpHost"))) $_FILES = $_POST = array();
}
@@ -3766,7 +3766,7 @@ class EndResponseException extends \Exception
$exception = new self($content);
$exception->content = $content;
return $exception;
}
@@ -3926,8 +3926,8 @@ class ztSessionHandler implements SessionHandlerInterface
public function gc($maxlifeTime): int|false
{
/* API session never expires. */
if(!isset($this->config->sessionVar)) return 0;
if((defined('RUN_MODE') && RUN_MODE == 'api') || isset($_GET[$this->config->sessionVar])) return 0;
global $config;
if((defined('RUN_MODE') && RUN_MODE == 'api') || isset($_GET[$config->sessionVar])) return 0;
$time = time();
$count = 0;
+4 -4
View File
@@ -391,7 +391,7 @@ class model extends baseModel
if($this->config->edition == 'open') return array();
if(!empty($this->app->installing) || !empty($this->app->upgrading)) return array();
return $this->loadModel('flow')->getExtendFields($this->app->getModuleName(), $this->app->getMethodName(), $objectID);
return $this->loadModel('flow')->getExtendFields($this->app->rawModule, $this->app->rawMethod, $objectID);
}
/**
@@ -405,7 +405,7 @@ class model extends baseModel
if($this->config->edition == 'open') return array();
if(!empty($this->app->installing) || !empty($this->app->upgrading)) return array();
return $this->loadModel('workflowfield')->getExportFields($this->app->getModuleName());
return $this->loadModel('workflowfield')->getExportFields($this->app->rawModule);
}
/**
@@ -420,8 +420,8 @@ class model extends baseModel
if($this->config->edition == 'open') return '';
if(!empty($this->app->installing) || !empty($this->app->upgrading)) return '';
$moduleName = $this->app->getModuleName();
$methodName = $this->app->getMethodName();
$moduleName = $this->app->rawModule;
$methodName = $this->app->rawMethod;
$groupID = $this->loadModel('workflowgroup')->getGroupIDByDataID($moduleName, $objectID);
$action = $this->loadModel('workflowaction')->getByModuleAndAction($moduleName, $methodName, $groupID);
+10 -10
View File
@@ -492,11 +492,11 @@ class baseDAO
* Begin Transaction
*
* @access public
* @return void
* @return bool
*/
public function begin()
public function begin(): bool
{
if(!$this->dbh->inTransaction()) $this->dbh->beginTransaction();
return $this->dbh->beginTransaction();
}
/**
@@ -506,7 +506,7 @@ class baseDAO
* @access public
* @return bool
*/
public function inTransaction()
public function inTransaction(): bool
{
return $this->dbh->inTransaction();
}
@@ -516,11 +516,11 @@ class baseDAO
* Roll back
*
* @access public
* @return void
* @return bool
*/
public function rollBack()
public function rollBack(): bool
{
if($this->dbh->inTransaction()) $this->dbh->rollBack();
return $this->dbh->rollBack();
}
/**
@@ -528,11 +528,11 @@ class baseDAO
* Commits a transaction.
*
* @access public
* @return void
* @return bool
*/
public function commit()
public function commit(): bool
{
if($this->dbh->inTransaction()) $this->dbh->commit();
return $this->dbh->commit();
}
/**
+12
View File
@@ -171,4 +171,16 @@ class baseDelegate
throw new BadMethodCallException("The method {$name} does not exist in the class {$calledClass}.");
}
/**
* 获取第三方类库的实例
* Get the instance of the third-party library
*
* @access public
* @return mixed
*/
public function getInstance()
{
return $this->instance;
}
}
+8 -1
View File
@@ -1041,7 +1041,14 @@ EOT;
if($app->apiVersion == 'v2')
{
helper::send(array("status" => "fail", "message" => "Not allowed"), 401);
if(strpos($url, 'login') === false)
{
helper::send(array("status" => "success"));
}
else
{
helper::send(array("status" => "fail", "message" => "Not allowed"), 401);
}
}
elseif($app->viewType == 'json')
{
+6
View File
@@ -256,6 +256,12 @@ class dao extends baseDAO
$groupID = !empty($result->workflowGroup) ? $result->workflowGroup : 0;
}
if($groupID)
{
$builtIn = $this->dbh->query("SELECT `main` FROM " . TABLE_WORKFLOWGROUP . " WHERE `id` = '{$groupID}'")->fetch(PDO::FETCH_OBJ);
$groupID = !empty($builtIn->main) ? 0 : $groupID;
}
$flowAction = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '{$module}' AND `action` = '{$method}' AND `buildin` = '1' AND `extensionType` = 'extend' AND `vision` = '{$this->config->vision}' AND `group` = '{$groupID}'")->fetch(PDO::FETCH_OBJ);
if(!$flowAction) return $this;
+117 -134
View File
@@ -125,20 +125,16 @@ class dbh
public function __construct($dbConfig, $setSchema = true, $flag = 'MASTER')
{
global $config;
$this->config = $config;
$driverAlias = array('oceanbase' => 'mysql', 'highgo' => 'pgsql', 'postgres' => 'pgsql');
$driver = isset($driverAlias[$dbConfig->driver]) ? $driverAlias[$dbConfig->driver] : $dbConfig->driver;
$this->pdo = $this->pdoInit($driver, $dbConfig, $setSchema);
$this->config = $config;
$this->dbConfig = $dbConfig;
$this->flag = $flag;
$this->pdo = $this->pdoInit($setSchema);
$queries = [];
/* Mysql driver include mysql and oceanbase. */
if($driver == 'mysql')
if(in_array($dbConfig->driver, $config->mysqlDriverList))
{
$queries[] = "SET NAMES {$dbConfig->encoding}" . ($dbConfig->collation ? " COLLATE '{$dbConfig->collation}'" : '');
if($dbConfig->driver == 'mysql') $queries[] = "SET NAMES {$dbConfig->encoding}" . ($dbConfig->collation ? " COLLATE '{$dbConfig->collation}'" : '');
if(isset($dbConfig->strictMode) && empty($dbConfig->strictMode)) $queries[] = "SET @@sql_mode= ''";
}
else
@@ -147,7 +143,15 @@ class dbh
if($setSchema)
{
$queries[] = $driver == 'pgsql' ? "SET SCHEMA 'public'" : "SET SCHEMA {$dbConfig->name}";
if($dbConfig->driver == 'dm')
{
$queries[] = "SET SCHEMA {$dbConfig->name}";
}
elseif(in_array($dbConfig->driver, $config->pgsqlDriverList))
{
$schema = $dbConfig->schema ?? 'public';
$queries[] = "SET SCHEMA '{$schema}'";
}
}
}
if(!empty($queries))
@@ -156,30 +160,45 @@ class dbh
}
}
/**
* 获取PDO驱动名称。
* Get pdo driver name.
*
* @access private
* @return string
*/
private function getPdoDriver()
{
if($this->dbConfig->driver == 'kingbase') return 'kdb';
if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList)) return 'mysql';
if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList)) return 'pgsql';
return $this->dbConfig->driver;
}
/**
* 初始化PDO对象。
* Init pdo.
*
* @param string $driver
* @param object $dbConfig
* @param bool $setSchema
* @access private
* @return object
*/
private function pdoInit($driver, $dbConfig, $setSchema)
private function pdoInit($setSchema)
{
$dsn = "{$driver}:host={$dbConfig->host};port={$dbConfig->port}";
$driver = $this->getPdoDriver();
$dsn = "{$driver}:host={$this->dbConfig->host};port={$this->dbConfig->port}";
if($setSchema)
{
$dsn .= ";dbname={$dbConfig->name}";
$dsn .= ";dbname={$this->dbConfig->name}";
}
elseif($driver == 'pgsql') // pgsql(postgres,highgo) need database to connect
elseif(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
$dsn .= ";dbname={$dbConfig->driver}"; // default database
$dsn .= ";dbname={$this->dbConfig->driver}"; // default database
}
$password = helper::decryptPassword($dbConfig->password);
$pdo = new PDO($dsn, $dbConfig->user, $password);
$password = helper::decryptPassword($this->dbConfig->password);
$pdo = new PDO($dsn, $this->dbConfig->user, $password);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
@@ -380,33 +399,35 @@ class dbh
*/
public function dbExists()
{
switch($this->dbConfig->driver)
if($this->dbConfig->driver == 'dm')
{
case 'oceanbase':
case 'mysql':
$sql = "SHOW DATABASES like '{$this->dbConfig->name}'";
break;
case 'dm':
$sql = "SELECT * FROM ALL_OBJECTS WHERE object_type='SCH' AND owner='{$this->dbConfig->name}'";
break;
case 'postgres':
case 'highgo':
$sql = "SELECT * FROM pg_database WHERE datname ='{$this->dbConfig->name}'";
break;
default:
$sql = '';
$sql = "SELECT * FROM ALL_OBJECTS WHERE object_type='SCH' AND owner='{$this->dbConfig->name}'";
return $this->rawQuery($sql)->fetch();
}
return $this->rawQuery($sql)->fetch();
if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList))
{
$sql = "SHOW DATABASES like '{$this->dbConfig->name}'";
return $this->rawQuery($sql)->fetch();
}
if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
$sql = "SELECT * FROM pg_database WHERE datname ='{$this->dbConfig->name}'";
return $this->rawQuery($sql)->fetch();
}
return false;
}
/**
* Check table exits or not.
* Check table exist or not.
*
* @param string $tableName
* @access public
* @return void
*/
public function tableExits($tableName)
public function tableExist($tableName)
{
$tableName = str_replace(array("'", '`'), "", $tableName);
@@ -415,15 +436,21 @@ class dbh
$sql = "SELECT * FROM all_tables WHERE owner='{$this->dbConfig->name}' AND table_name='{$tableName}'";
return $this->rawQuery($sql)->fetch();
}
elseif(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList))
{
$sql = "SHOW TABLES FROM {$this->dbConfig->name} like '{$tableName}'";
return $this->rawQuery($sql)->fetch();
}
if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
$this->useDB($this->dbConfig->name);
$sql = "SELECT * FROM information_schema.tables WHERE table_catalog = '{$this->dbConfig->name}' AND table_name='{$tableName}'";
return $this->rawQuery($sql)->fetch();
}
$sql = "SHOW TABLES FROM {$this->dbConfig->name} like '{$tableName}'";
return $this->rawQuery($sql)->fetch();
return false;
}
/**
@@ -474,23 +501,26 @@ class dbh
*/
public function createDB($version)
{
switch($this->dbConfig->driver)
if($this->dbConfig->driver == 'dm')
{
case 'mysql':
$result = $this->getServerCharsetAndCollation();
$sql = "CREATE DATABASE `{$this->dbConfig->name}` DEFAULT CHARACTER SET {$result['charset']} COLLATE {$result['collation']}";
return $this->rawQuery($sql);
case 'dm':
$createSchema = "CREATE SCHEMA {$this->dbConfig->name} AUTHORIZATION {$this->dbConfig->user}";
return $this->rawQuery($createSchema);
case 'oceanbase':
case 'postgres':
case 'highgo':
$sql = "CREATE DATABASE `{$this->dbConfig->name}`";
return $this->rawQuery($sql);
default:
return false;
$createSchema = "CREATE SCHEMA {$this->dbConfig->name} AUTHORIZATION {$this->dbConfig->user}";
return $this->rawQuery($createSchema);
}
if($this->dbConfig->driver == 'mysql')
{
$result = $this->getServerCharsetAndCollation();
$sql = "CREATE DATABASE `{$this->dbConfig->name}` DEFAULT CHARACTER SET {$result['charset']} COLLATE {$result['collation']}";
return $this->rawQuery($sql);
}
if($this->dbConfig->driver == 'oceanbase' || in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
$sql = "CREATE DATABASE {$this->dbConfig->name}";
return $this->rawQuery($sql);
}
return false;
}
/**
@@ -502,20 +532,18 @@ class dbh
*/
public function useDB($dbName)
{
switch($this->dbConfig->driver)
if($this->dbConfig->driver == 'dm') return $this->exec("SET SCHEMA {$dbName}");
if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList)) return $this->exec("USE {$dbName}");
if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
case 'oceanbase':
case 'mysql':
return $this->exec("USE {$this->dbConfig->name}");
case 'dm':
return $this->exec("SET SCHEMA {$this->dbConfig->name}");
case 'postgres':
case 'highgo':
$this->pdo = $this->pdoInit('pgsql', $this->dbConfig, true);
return $this->exec("SET SCHEMA 'public'");
default:
return false;
$this->pdo = $this->pdoInit(true);
$schema = $this->dbConfig->schema ?? 'public';
return $this->exec("SET SCHEMA '{$schema}'");
}
return false;
}
/**
@@ -529,18 +557,11 @@ class dbh
{
$this->sql = $sql;
switch($this->dbConfig->driver)
{
case 'dm':
return $this->formatDmSQL($sql);
if($this->dbConfig->driver == 'dm') return $this->formatDmSQL($sql);
case 'postgres':
case 'highgo':
return $this->formatPgSQL($sql);
if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList)) return $this->formatPgSQL($sql);
default:
return $sql;
}
return $sql;
}
/**
@@ -585,28 +606,7 @@ class dbh
if(stripos($sql, 'CREATE OR REPLACE VIEW ') === 0)
{
// Modify if function.
$fieldsBegin = stripos($sql, 'select');
$fieldsEnd = stripos($sql, 'from');
$fields = substr($sql, $fieldsBegin+6, $fieldsEnd-$fieldsBegin-6);
$fieldList = preg_split("/,(?![^(]+\))/", $fields);
foreach($fieldList as $key => $field)
{
$aliasPos = stripos($field, ' AS ');
$subField = substr($field, 0, $aliasPos);
if(stripos($field, 'SUM(') === 0) $subField = substr($subField, 4, -1);
$fieldParts = preg_split("/\+(?![^(]+\))/", $subField);
foreach($fieldParts as $pkey => $fieldPart)
{
$originField = trim($fieldPart);
if(stripos($originField, 'if(') === false) continue;
$fieldParts[$pkey] = $this->formatDmIfFunction($originField);
}
$fieldList[$key] = str_replace($subField, implode(' + ', $fieldParts), $field);
}
$fields = implode(',', $fieldList);
$sql = substr($sql, 0, $fieldsBegin+6) . $fields . substr($sql, $fieldsEnd);
$sql = $this->formatField($sql);
return str_replace('CREATE OR REPLACE VIEW ', 'CREATE VIEW ', $sql);
}
elseif(stripos($sql, 'CREATE UNIQUE INDEX') === 0 || stripos($sql, 'CREATE INDEX') === 0)
@@ -809,17 +809,13 @@ class dbh
*/
public function formatField($sql)
{
switch($this->dbConfig->driver)
if($this->dbConfig->driver == 'dm' || in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
case 'dm':
case 'postgres':
case 'highgo':
$sql = str_replace('`', '"', $sql);
return $sql;
default:
return $sql;
$sql = str_replace('`', '"', $sql);
$sql = preg_replace('/(?<!\w)if\(/i', '"IF"(', $sql);
}
return $sql;
}
/**
@@ -831,17 +827,10 @@ class dbh
*/
public function formatFunction($sql)
{
switch($this->dbConfig->driver)
{
case 'dm':
/* DATE convert to TO_CHAR. */
$sql = preg_replace("/\bDATE\(([^)]*)\)/", "TO_CHAR($1, 'yyyy-mm-dd')", $sql, -1);
/* DATE convert to TO_CHAR. */
if($this->dbConfig->driver == 'dm') return preg_replace("/\bDATE\(([^)]*)\)/", "TO_CHAR($1, 'yyyy-mm-dd')", $sql, -1);
return $sql;
default:
return $sql;
}
return $sql;
}
/**
@@ -883,7 +872,7 @@ class dbh
*/
public function formatAttr($sql)
{
if(in_array($this->dbConfig->driver, array('dm', 'postgres', 'highgo')))
if($this->dbConfig->driver == 'dm' || in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
{
$pos = stripos($sql, ' ENGINE');
if($pos > 0) $sql = substr($sql, 0, $pos);
@@ -906,14 +895,14 @@ class dbh
"0000-00-00" => '1970-01-01',
);
if(in_array($this->dbConfig->driver, $this->config->pgsqlDriverList))
if($this->dbConfig->driver == 'dm')
{
$sql = preg_replace('/(\s*`[^`]+`)\s+\K.+AUTO_INCREMENT(,)/i', ' serial,', $sql);
$sql = str_ireplace(' DATETIME', ' TIMESTAMP', $sql);
$sql = str_ireplace(' AUTO_INCREMENT', ' IDENTITY(1, 1)', $sql);
}
else
{
$sql = str_ireplace(' AUTO_INCREMENT', ' IDENTITY(1, 1)', $sql);
$sql = preg_replace('/(\s*`[^`]+`)\s+\K.+AUTO_INCREMENT(,)/i', ' serial,', $sql);
$sql = str_ireplace(' DATETIME', ' TIMESTAMP', $sql);
}
$sql = preg_replace('/ enum[\_0-9a-z\,\'\"\( ]+\)+/i', ' varchar(255) ', $sql);
@@ -1070,7 +1059,7 @@ class dbh
*/
public function beginTransaction()
{
return $this->pdo->beginTransaction();
return $this->pdo->inTransaction() ? false : $this->pdo->beginTransaction();
}
/**
@@ -1092,7 +1081,7 @@ class dbh
*/
public function rollBack()
{
return $this->pdo->rollBack();
return $this->pdo->inTransaction() ? $this->pdo->rollBack() : false;
}
/**
@@ -1103,7 +1092,7 @@ class dbh
*/
public function commit()
{
return $this->pdo->commit();
return $this->pdo->inTransaction() ? $this->pdo->commit() : false;
}
/**
@@ -1217,18 +1206,12 @@ class dbh
*/
public function getVersion(): string
{
switch($this->dbConfig->driver)
if(in_array($this->dbConfig->driver, $this->config->mysqlDriverList))
{
case 'oceanbase':
case 'mysql':
$sql = "SELECT version() AS version";
break;
case 'dm':
default:
$sql = '';
$sql = "SELECT VERSION() AS version";
return $this->rawQuery($sql)->fetch()->version;
}
if(empty($sql)) return '';
return $this->rawQuery($sql)->fetch()->version;
return '';
}
}
+9 -2
View File
@@ -221,7 +221,7 @@ class form extends fixer
if(!empty($this->errors))
{
$response = array('result' => 'fail', 'message' => $this->errors);
helper::end(json_encode($response));
helper::end(json_encode($response, JSON_UNESCAPED_UNICODE));
}
return $this;
@@ -423,7 +423,14 @@ class form extends fixer
{
$rawModule = $app->rawModule == 'feedback' && in_array($app->rawMethod, array('touserstory', 'toepic')) ? 'story' : $app->rawModule;
$errorKey = isset($config['type']) && $config['type'] == 'array' ? "{$field}[]" : $field;
$fieldName = isset($app->lang->{$rawModule}->$field) ? $app->lang->{$rawModule}->$field : $field;
if(isset($config['label']))
{
$fieldName = $config['label'];
}
else
{
$fieldName = isset($app->lang->{$rawModule}->$field) ? $app->lang->{$rawModule}->$field : $field;
}
if(!isset($this->errors[$errorKey])) $this->errors[$errorKey] = array();
$this->errors[$errorKey][] = sprintf($app->lang->error->notempty, $fieldName);
}
+474 -470
View File
@@ -1,470 +1,474 @@
<?php
/**
* The formdom class file of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Zhu Jinyong<zhujinyong@easycorp.ltd>
* @package form
* @link https://www.zentao.net
*/
/**
* 解析form dom为数组数据。
* Parse Form dom to array data.
*/
class formdom
{
/**
* Errors
*
* @var array
* @access private
*/
private $errors = array();
/**
* Options
*
* @var array
* @access private
*/
private $options = array(
'include_disabled' => false,
'include_empty_checkboxes' => false,
'default_radio_value' => null,
'debug' => false
);
/**
* Constructor
*
* @param array $options
* @access public
* @return void
*/
public function __construct($options = array())
{
$this->options = array_merge($this->options, $options);
}
/**
* Parse html dom
*
* @param string $html
* @param string $formSelector
* @access public
* @return array
*/
public function parse($html, $formSelector = null)
{
$this->errors = array();
if(empty($html))
{
$this->errors[] = "HTML is empty";
return array();
}
$dom = new DOMDocument();
libxml_use_internal_errors(true);
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();
$xpath = new DOMXPath($dom);
/* 查找表单 */
$form = null;
if($formSelector)
{
// 通过 id 查找
$forms = $xpath->query("//form[@id='$formSelector']");
if($forms->length > 0)
{
$form = $forms->item(0);
}
else
{
/* 通过 name 查找 */
$forms = $xpath->query("//form[@name='$formSelector']");
if($forms->length > 0)
{
$form = $forms->item(0);
}
}
} else {
/* 获取第一个表单 */
$forms = $xpath->query("//form");
if ($forms->length > 0) {
$form = $forms->item(0);
}
}
if(!$form)
{
$this->errors[] = "Form not found";
return array();
}
/* 提取表单数据 */
$result = $this->extractFormData($xpath, $form);
return $result;
}
/**
* Extract form data
*
* @param object $xpath
* @param object $form
* @access private
* @return array
*/
private function extractFormData($xpath, $form)
{
$data = [];
/* 处理所有表单元素 */
$this->processInputs($xpath, $form, $data);
$this->processSelects($xpath, $form, $data);
$this->processTextareas($xpath, $form, $data);
$this->processZinPickers($xpath, $form, $data);
$this->processZinEditors($xpath, $form, $data);
return $data;
}
/**
* Process inputs
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processInputs($xpath, $form, &$data)
{
// 查找所有 input 元素(包括嵌套的)
$inputs = $xpath->query(".//input[@name]", $form);
$radioGroups = array();
foreach($inputs as $input)
{
$name = $input->getAttribute('name');
$type = strtolower($input->getAttribute('type') ?: 'text');
$value = $input->getAttribute('value');
$disabled = $input->hasAttribute('disabled');
/* 跳过禁用的元素(除非配置允许) */
if(!$this->options['include_disabled'] && $disabled) continue;
switch($type)
{
case 'checkbox':
if($input->hasAttribute('checked'))
{
$this->addValue($data, $name, $value ?: 'on');
}
elseif($this->options['include_empty_checkboxes'])
{
$this->addValue($data, $name, '');
}
break;
case 'radio':
$baseName = $this->getBaseName($name);
if(!isset($radioGroups[$baseName]))
{
$radioGroups[$baseName] = array(
'selected' => null,
'hasSelection' => false
);
}
if($input->hasAttribute('checked'))
{
$radioGroups[$baseName]['selected'] = $value;
$radioGroups[$baseName]['hasSelection'] = true;
}
break;
case 'file':
$this->addValue($data, $name, '');
break;
case 'submit':
case 'button':
case 'image':
case 'reset':
break;
default:
/* 包括 hidden, text, password, email 等所有其他类型 */
$this->addValue($data, $name, $value ?: '');
break;
}
}
/* 处理 radio 组 */
foreach ($radioGroups as $name => $group) {
if ($group['hasSelection']) {
$data[$name] = $group['selected'];
} elseif ($this->options['default_radio_value'] !== null) {
$data[$name] = $this->options['default_radio_value'];
}
}
}
/**
* Process selects
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processSelects($xpath, $form, &$data)
{
$selects = $xpath->query(".//select[@name]", $form);
foreach($selects as $select)
{
if(!$this->options['include_disabled'] && $select->hasAttribute('disabled')) continue;
$name = $select->getAttribute('name');
$multiple = $select->hasAttribute('multiple');
$options = $xpath->query(".//option", $select);
if($multiple)
{
$selectedValues = [];
foreach($options as $option)
{
if($option->hasAttribute('selected'))
{
$value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
$selectedValues[] = $value;
}
}
/* 对于多选,即使没有选中项,也要处理默认值 */
foreach($options as $option)
{
$value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
if(!empty($value))
{
$selectedValues[] = $value;
break; // 只取第一个有值的选项作为默认
}
}
if(!empty($selectedValues))
{
foreach($selectedValues as $val) $this->addValue($data, $name, $val);
}
}
else
{
$selectedValue = null;
$firstValue = null;
foreach($options as $index => $option)
{
$value = $option->hasAttribute('value')
? $option->getAttribute('value')
: trim($option->textContent);
if($index === 0) $firstValue = $value;
if($option->hasAttribute('selected'))
{
$selectedValue = $value;
break;
}
}
$finalValue = $selectedValue !== null ? $selectedValue : $firstValue;
if($finalValue !== null) $this->addValue($data, $name, $finalValue);
}
}
}
/**
* Process textarea
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processTextareas($xpath, $form, &$data)
{
$textareas = $xpath->query(".//textarea[@name]", $form);
foreach($textareas as $textarea)
{
if(!$this->options['include_disabled'] && $textarea->hasAttribute('disabled')) continue;
$name = $textarea->getAttribute('name');
$value = $textarea->textContent;
$this->addValue($data, $name, $value);
}
}
/**
* Process zin pickers and date pickers.
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processZinPickers($xpath, $form, &$data)
{
/* 查找所有包含 zui-create-picker 属性的元素(通常是 div.picker-box) */
$pickers = $xpath->query(".//*[@zui-create]", $form);
foreach ($pickers as $picker) {
/* 1. 跳过禁用的picker(如果配置不允许) */
$pickerDisabled = $picker->hasAttribute('disabled') || $picker->getAttribute('disabled') === 'true';
if(!$this->options['include_disabled'] && $pickerDisabled) continue;
/* 2. 获取 zui-create-picker 属性的JSON值 */
if($picker->hasAttribute('zui-create-picker'))
{
$pickerConfig = $picker->getAttribute('zui-create-picker');
}
elseif($picker->hasAttribute('zui-create-datepicker'))
{
$pickerConfig = $picker->getAttribute('zui-create-datepicker');
}
/* 3. 提取name(关键:用正则匹配name字段)*/
$name = null;
if(preg_match('/"name"\s*:\s*"([^"]+)"/i', $pickerConfig, $nameMatches))
{
$name = $nameMatches[1]; // 捕获引号中的name值
}
elseif(preg_match("/'name'\s*:\s*'([^']+)'/i", $pickerConfig, $nameMatches))
{
$name = $nameMatches[1]; // 兼容单引号
}
if(empty($name)) continue;
/* 4. 提取defaultValue(核心:兼容JS函数和非JSON格式)*/
$defaultValue = '';
/* 正则规则:匹配 "defaultValue": 值 或 'defaultValue': 值 */
/* 支持的值类型:字符串(单/双引号)、数字、true/false/null */
$valuePattern = '/"defaultValue"\s*:\s*(?:"([^"]+)"|\'([^\']+)\'|(\d+\.?\d*)|(true|false|null))/i';
if(preg_match($valuePattern, $pickerConfig, $valueMatches))
{
/* 匹配优先级:双引号字符串 > 单引号字符串 > 数字 > 布尔/null */
$defaultValue = !empty($valueMatches[1]) ? $valueMatches[1]
: (!empty($valueMatches[2]) ? $valueMatches[2]
: (!empty($valueMatches[3]) ? $valueMatches[3]
: $valueMatches[4] ?? null));
/* 转换类型(如"true"→true,"123"→123)*/
if ($defaultValue === 'true') $defaultValue = true;
elseif ($defaultValue === 'false') $defaultValue = false;
elseif ($defaultValue === 'null') $defaultValue = null;
elseif (is_numeric($defaultValue)) $defaultValue = (float)$defaultValue;
}
/* 5. 写入数据 */
$this->addValue($data, $name, $defaultValue);
}
}
/**
* Process zin editors.
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processZinEditors($xpath, $form, &$data)
{
$editors = $xpath->query("//zen-editor", $form);
foreach($editors as $editor)
{
$name = $editor->getAttribute('name');
$value = '';
$contentNode = $xpath->query(".//article[@slot='content']", $editor)->item(0);
if($contentNode)
{
/* 获取内部HTML内容 */
$dom = $contentNode->ownerDocument;
foreach($contentNode->childNodes as $child) $value .= $dom->saveHTML($child);
}
$this->addValue($data, $name, $value);
}
}
/**
* Add value to data.
*
* @param array $data
* @param string $name
* @param mixed $value
* @access private
* @return void
*/
private function addValue(&$data, $name, $value)
{
/* 使用 parse_str 来处理名称 */
if($value === null) $value = '';
parse_str($name . '=' . urlencode($value), $temp);
/* 合并到数据中 */
foreach($temp as $key => $val)
{
if(!isset($data[$key]))
{
$data[$key] = $val;
}
elseif(is_array($data[$key]) && is_array($val))
{
/* 递归合并数组 */
foreach($val as $v) $data[$key][] = $v;
}
else
{
/* 覆盖(处理重复的非数组字段) */
$data[$key] = $val;
}
}
}
/**
* Get base name of field.
*
* @param string $name
* @access private
* @return string
*/
private function getBaseName($name)
{
if(preg_match('/^([^\[]+)/', $name, $matches)) return $matches[1];
return $name;
}
/**
* Get errors.
*
* @access public
* @return array
*/
public function getErrors()
{
return $this->errors;
}
}
<?php
/**
* The formdom class file of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Zhu Jinyong<zhujinyong@easycorp.ltd>
* @package form
* @link https://www.zentao.net
*/
/**
* 解析form dom为数组数据。
* Parse Form dom to array data.
*/
class formdom
{
/**
* Errors
*
* @var array
* @access private
*/
private $errors = array();
/**
* Options
*
* @var array
* @access private
*/
private $options = array(
'include_disabled' => false,
'include_empty_checkboxes' => false,
'default_radio_value' => null,
'debug' => false
);
/**
* Constructor
*
* @param array $options
* @access public
* @return void
*/
public function __construct($options = array())
{
$this->options = array_merge($this->options, $options);
}
/**
* Parse html dom
*
* @param string $html
* @param string $formSelector
* @access public
* @return array
*/
public function parse($html, $formSelector = null)
{
$this->errors = array();
if(empty($html))
{
$this->errors[] = "HTML is empty";
return array();
}
$dom = new DOMDocument();
libxml_use_internal_errors(true);
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();
$xpath = new DOMXPath($dom);
/* 查找表单 */
$form = null;
if($formSelector)
{
// 通过 id 查找
$forms = $xpath->query("//form[@id='$formSelector']");
if($forms->length > 0)
{
$form = $forms->item(0);
}
else
{
/* 通过 name 查找 */
$forms = $xpath->query("//form[@name='$formSelector']");
if($forms->length > 0)
{
$form = $forms->item(0);
}
}
} else {
/* 获取第一个表单 */
$forms = $xpath->query("//form");
if ($forms->length > 0) {
$form = $forms->item(0);
}
}
if(!$form)
{
$this->errors[] = "Form not found";
return array();
}
/* 提取表单数据 */
$result = $this->extractFormData($xpath, $form);
return $result;
}
/**
* Extract form data
*
* @param object $xpath
* @param object $form
* @access private
* @return array
*/
private function extractFormData($xpath, $form)
{
$data = [];
/* 处理所有表单元素 */
$this->processInputs($xpath, $form, $data);
$this->processSelects($xpath, $form, $data);
$this->processTextareas($xpath, $form, $data);
$this->processZinPickers($xpath, $form, $data);
$this->processZinEditors($xpath, $form, $data);
return $data;
}
/**
* Process inputs
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processInputs($xpath, $form, &$data)
{
// 查找所有 input 元素(包括嵌套的)
$inputs = $xpath->query(".//input[@name]", $form);
$radioGroups = array();
foreach($inputs as $input)
{
$name = $input->getAttribute('name');
$type = strtolower($input->getAttribute('type') ?: 'text');
$value = $input->getAttribute('value');
$disabled = $input->hasAttribute('disabled');
/* 跳过禁用的元素(除非配置允许) */
if(!$this->options['include_disabled'] && $disabled) continue;
switch($type)
{
case 'checkbox':
if($input->hasAttribute('checked'))
{
$this->addValue($data, $name, $value ?: 'on');
}
elseif($this->options['include_empty_checkboxes'])
{
$this->addValue($data, $name, '');
}
break;
case 'radio':
$baseName = $this->getBaseName($name);
if(!isset($radioGroups[$baseName]))
{
$radioGroups[$baseName] = array(
'selected' => null,
'hasSelection' => false
);
}
if($input->hasAttribute('checked'))
{
$radioGroups[$baseName]['selected'] = $value;
$radioGroups[$baseName]['hasSelection'] = true;
}
break;
case 'file':
$this->addValue($data, $name, '');
break;
case 'submit':
case 'button':
case 'image':
case 'reset':
break;
default:
/* 包括 hidden, text, password, email 等所有其他类型 */
$this->addValue($data, $name, $value ?: '');
break;
}
}
/* 处理 radio 组 */
foreach ($radioGroups as $name => $group) {
if ($group['hasSelection']) {
$data[$name] = $group['selected'];
} elseif ($this->options['default_radio_value'] !== null) {
$data[$name] = $this->options['default_radio_value'];
}
}
}
/**
* Process selects
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processSelects($xpath, $form, &$data)
{
$selects = $xpath->query(".//select[@name]", $form);
foreach($selects as $select)
{
if(!$this->options['include_disabled'] && $select->hasAttribute('disabled')) continue;
$name = $select->getAttribute('name');
$multiple = $select->hasAttribute('multiple');
$options = $xpath->query(".//option", $select);
if($multiple)
{
$selectedValues = [];
foreach($options as $option)
{
if($option->hasAttribute('selected'))
{
$value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
$selectedValues[] = $value;
}
}
/* 对于多选,即使没有选中项,也要处理默认值 */
foreach($options as $option)
{
$value = $option->hasAttribute('value') ? $option->getAttribute('value') : trim($option->textContent);
if(!empty($value))
{
$selectedValues[] = $value;
break; // 只取第一个有值的选项作为默认
}
}
if(!empty($selectedValues))
{
foreach($selectedValues as $val) $this->addValue($data, $name, $val);
}
}
else
{
$selectedValue = null;
$firstValue = null;
foreach($options as $index => $option)
{
$value = $option->hasAttribute('value')
? $option->getAttribute('value')
: trim($option->textContent);
if($index === 0) $firstValue = $value;
if($option->hasAttribute('selected'))
{
$selectedValue = $value;
break;
}
}
$finalValue = $selectedValue !== null ? $selectedValue : $firstValue;
if($finalValue !== null) $this->addValue($data, $name, $finalValue);
}
}
}
/**
* Process textarea
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processTextareas($xpath, $form, &$data)
{
$textareas = $xpath->query(".//textarea[@name]", $form);
foreach($textareas as $textarea)
{
if(!$this->options['include_disabled'] && $textarea->hasAttribute('disabled')) continue;
$name = $textarea->getAttribute('name');
$value = $textarea->textContent;
$this->addValue($data, $name, $value);
}
}
/**
* Process zin pickers and date pickers.
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processZinPickers($xpath, $form, &$data)
{
/* 查找所有包含 zui-create-picker 属性的元素(通常是 div.picker-box) */
$pickers = $xpath->query(".//*[@zui-create]", $form);
foreach ($pickers as $picker) {
/* 1. 跳过禁用的picker(如果配置不允许) */
$pickerDisabled = $picker->hasAttribute('disabled') || $picker->getAttribute('disabled') === 'true';
if(!$this->options['include_disabled'] && $pickerDisabled) continue;
/* 2. 获取 zui-create-picker 属性的JSON值 */
if($picker->hasAttribute('zui-create-picker'))
{
$pickerConfig = $picker->getAttribute('zui-create-picker');
}
elseif($picker->hasAttribute('zui-create-datepicker'))
{
$pickerConfig = $picker->getAttribute('zui-create-datepicker');
}
if(empty($pickerConfig)) continue;
/* 3. 提取name(关键:用正则匹配name字段)*/
$name = null;
if(preg_match('/"name"\s*:\s*"([^"]+)"/i', $pickerConfig, $nameMatches))
{
$name = $nameMatches[1]; // 捕获引号中的name值
}
elseif(preg_match("/'name'\s*:\s*'([^']+)'/i", $pickerConfig, $nameMatches))
{
$name = $nameMatches[1]; // 兼容单引号
}
if(empty($name)) continue;
/* 4. 提取defaultValue(核心:兼容JS函数和非JSON格式)*/
$defaultValue = '';
/* 正则规则:匹配 "defaultValue": 值 或 'defaultValue': 值 */
/* 支持的值类型:字符串(单/双引号)、数字、true/false/null */
$valuePattern = '/"defaultValue"\s*:\s*(?:"([^"]+)"|\'([^\']+)\'|(\d+\.?\d*)|(true|false|null)|\[([^\]]+)\])/i';
if(preg_match($valuePattern, $pickerConfig, $valueMatches))
{
/* 匹配优先级:数组 > 双引号字符串 > 单引号字符串 > 数字 > 布尔/null */
$defaultValue = !empty($valueMatches[5]) ? $valueMatches[5] // 优先级1:数组内容
: (!empty($valueMatches[1]) ? $valueMatches[1] // 优先级2:双引号字符串
: (!empty($valueMatches[2]) ? $valueMatches[2] // 优先级3:单引号字符串
: (!empty($valueMatches[3]) ? $valueMatches[3] // 优先级4:数字
: $valueMatches[4] ?? null))); // 优先级5:布尔/null
/* 转换类型(如"true"→true,"123"→123)*/
if ($defaultValue === 'true') $defaultValue = true;
elseif ($defaultValue === 'false') $defaultValue = false;
elseif ($defaultValue === 'null') $defaultValue = null;
elseif (is_numeric($defaultValue)) $defaultValue = (float)$defaultValue;
}
/* 5. 写入数据 */
$this->addValue($data, $name, $defaultValue);
}
}
/**
* Process zin editors.
*
* @param object $xpath
* @param object $form
* @param array $data
* @access private
* @return void
*/
private function processZinEditors($xpath, $form, &$data)
{
$editors = $xpath->query("//zen-editor", $form);
foreach($editors as $editor)
{
$name = $editor->getAttribute('name');
$value = '';
$contentNode = $xpath->query(".//article[@slot='content']", $editor)->item(0);
if($contentNode)
{
/* 获取内部HTML内容 */
$dom = $contentNode->ownerDocument;
foreach($contentNode->childNodes as $child) $value .= $dom->saveHTML($child);
}
$this->addValue($data, $name, $value);
}
}
/**
* Add value to data.
*
* @param array $data
* @param string $name
* @param mixed $value
* @access private
* @return void
*/
private function addValue(&$data, $name, $value)
{
/* 使用 parse_str 来处理名称 */
if($value === null) $value = '';
parse_str($name . '=' . urlencode($value), $temp);
/* 合并到数据中 */
foreach($temp as $key => $val)
{
if(!isset($data[$key]))
{
$data[$key] = $val;
}
elseif(is_array($data[$key]) && is_array($val))
{
/* 递归合并数组 */
foreach($val as $v) $data[$key][] = $v;
}
else
{
/* 覆盖(处理重复的非数组字段) */
$data[$key] = $val;
}
}
}
/**
* Get base name of field.
*
* @param string $name
* @access private
* @return string
*/
private function getBaseName($name)
{
if(preg_match('/^([^\[]+)/', $name, $matches)) return $matches[1];
return $name;
}
/**
* Get errors.
*
* @access public
* @return array
*/
public function getErrors()
{
return $this->errors;
}
}
+1 -1
View File
@@ -886,7 +886,7 @@ class Mobile_Detect
* from the $headers array instead.
*/
public function __construct(
array $headers = null,
?array $headers = null,
$userAgent = null
) {
$this->setHttpHeaders($headers);
+1 -1
View File
@@ -352,7 +352,7 @@ class context extends \zin\utils\dataset
{
$rawContent = ob_get_contents();
if(!is_string($rawContent)) $rawContent = '';
ob_end_clean();
if(!empty(ob_get_status(true))) ob_end_clean();
return $rawContent;
}
+6 -4
View File
@@ -19,7 +19,7 @@ function deepGet(object|array &$data, string $namePath, mixed $defaultValue = nu
return $data === null ? $defaultValue : $data;
}
function deepSet(array &$data, string $namePath, mixed $value)
function deepSet(array|object &$data, string $namePath, mixed $value)
{
$names = explode('.', $namePath);
$lastName = array_pop($names);
@@ -27,12 +27,14 @@ function deepSet(array &$data, string $namePath, mixed $value)
{
foreach($names as $name)
{
if(!is_array($data)) return;
if(!is_array($data) && !is_object($data)) return;
if(!isset($data[$name])) $data[$name] = array();
if(is_array($data) && !isset($data[$name])) $data[$name] = array();
if(is_object($data) && !isset($data->$name)) $data->$name = new \stdClass();
$data = &$data[$name];
}
}
$data[$lastName] = $value;
if(is_array($data)) $data[$lastName] = $value;
elseif(is_object($data)) $data->$lastName = $value;
}
+1 -1
View File
@@ -50,7 +50,7 @@ class backBtn extends btn
'testsuite' => 'testsuite-browse,testsuite-view,testtask-cases,my-index,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view',
'testtask' => 'testtask-browse,testtask-cases,qa-index,testcase-browse,execution-build,my-index,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,execution-dynamic,project-view,execution-view,project-testcase,project-testtask,execution-testtask',
'testreport' => 'testreport-browse,project-testreport,execution-testreport,execution-testtask',
'tree' => 'product-browse,project-browse,execution-task,bug-browse,projectstory-story,host-browse,execution-story,feedback-admin,ticket-browse,testcase-browse,caselib-browse,dataview-browse',
'tree' => 'product-browse,project-browse,execution-task,bug-browse,projectstory-story,host-browse,execution-story,feedback-admin,ticket-browse,testcase-browse,caselib-browse,dataview-browse,workflowdatasource-browse',
'doc' => 'doc-mySpace,doc-productSpace,doc-projectSpace,doc-teamSpace,doc-view,execution-doc',
'design' => 'design-browse,projectstory-track,my-effort,company-effort,company-calendar,my-index',
'release' => 'release-browse,release-view,product-roadmap,kanban-view,projectrelease-browse,story-view,my-index,my-effort,company-effort,company-calendar,product-dynamic,project-dynamic,project-view',
+6 -1
View File
@@ -57,6 +57,9 @@ class detail extends wg
/* 标题,如果不指定则尝试使用当前页面上的 `${$objectType}->title` 或 `${$objectType}->name` 的值,例如 `$story->title`、`$task->name` 。 */
'title' => '?string',
/* 标题提示,如果不指定则使用title属性。 */
'titleProps' => '?string',
/* 底部固定操作按钮的定义,不包括返回按钮,可以通过 `-` 来指定分割线,如果没有指定 actions 属性,则从 actionList 和 operateList 生成。 */
'actions' => '?array',
@@ -125,6 +128,7 @@ class detail extends wg
.no-width {padding: 0!important; width: 0!important; overflow: hidden!important;}
.detail-toggle {margin: 0!important;}
.detail-toggle:hover {background-color: rgba(var(--color-border-rgb), var(--tw-bg-opacity)); transition-duration: .15s; transition-property: background-color; transition-timing-function: cubic-bezier(.4,0,.2,1);}
.article > .files-list > li.file{white-space:normal;}
CSS;
}
@@ -202,7 +206,7 @@ CSS;
protected function buildTitle()
{
list($object, $objectID, $title, $color, $objectType, $parent, $parentID, $parentUrl, $parentTitle, $parentType, $parentTitleProps) = $this->prop(array('object', 'objectID', 'title', 'color', 'objectType', 'parent', 'parentID', 'parentUrl', 'parentTitle', 'parentType', 'parentTitleProps'));
list($object, $objectID, $title, $titleProps, $color, $objectType, $parent, $parentID, $parentUrl, $parentTitle, $parentType, $parentTitleProps) = $this->prop(array('object', 'objectID', 'title', 'titleProps', 'color', 'objectType', 'parent', 'parentID', 'parentUrl', 'parentTitle', 'parentType', 'parentTitleProps'));
$titleBlock = $this->block('title');
$titleLeading = $this->block('titleLeading');
@@ -214,6 +218,7 @@ CSS;
set::object($object),
set::title($title),
set::titleClass('text-lg text-clip font-bold'),
set::titleProps(array('title' => empty($titleProps) ? $title : $titleProps)),
set::type($objectType),
set::color($color),
set::parentTitleClass('text-lg text-clip font-bold'),
+16 -9
View File
@@ -95,16 +95,18 @@ function getZentaoListProps(type, blockID, props)
const zentaoListLang = getLang('zentaoList');
const exportUrl = `${location.origin}${$.createLink('doc', 'ajaxExportZentaoList', `blockID=${blockID}`)}`;
const title = [(zentaoListLang ? zentaoListLang[type] : type), getLang('list')].join(config.clientLang.includes('zh') ? '' : ' ');
return $.extend(
const fetcher = `${location.origin}${$.createLink('doc', 'zentaoList', `type=${type}&blockID=${blockID}`)}`;
return $.extend({}, props,
{
title: title,
fetcher: $.createLink('doc', 'zentaoList', `type=${type}&blockID=${blockID}`),
fetcher: fetcher,
clearBeforeLoad: false
}, type === 'gantt' ? {
fetcher: fetcher,
export: [{type: 'link', props: {text: title, href: exportUrl}}]
} : {
exportUrl: exportUrl,
}, props);
});
}
/* 更新禅道列表。*/
@@ -921,10 +923,10 @@ const actionsMap =
if(!isApi && !isTemplate)
{
const canExportWord = hasPriv('exportDoc');
if(doc.contentType === 'doc' || doc.contentType === 'markdown')
if(canExportWord && (doc.contentType === 'doc' || doc.contentType === 'markdown'))
{
const exportItems = [
canExportWord ? {text: lang.exportWord, command: 'exportWord'} : null,
{text: lang.exportWord, command: 'exportWord'},
config.debug > 5 ? {text: lang.exportPdf, command: 'exportDoc/pdf'} : null,
config.debug > 5 ? {text: lang.exportImage, command: 'exportDoc/png'} : null,
{text: lang.exportMarkdown, command: 'exportDoc/markdown'},
@@ -959,9 +961,9 @@ const actionsMap =
if(!isApi && docAIPrompts && docAIPrompts.length)
{
const aiLang = window.docAIPromptLang;
const aiActionItems = docAIPrompts.map(prompt => ({icon: 'magic', text: prompt.status !== 'active' ? zui.html`${prompt.name}<span class="label size-sm gray-500-pale ring-gray-500 whitespace-nowrap ml-1">${aiLang.statuses[prompt.status]}</span>` : prompt.name, command: `executeAIPrompt/${prompt.id}/${doc.id}`, hint: prompt.desc}));
const aiActionItems = docAIPrompts.map(prompt => ({icon: 'magic', text: prompt.status !== 'active' ? zui.jsx`${prompt.name}<span class="label size-sm gray-500-pale ring-gray-500 whitespace-nowrap ml-1">${aiLang.statuses[prompt.status]}</span>` : prompt.name, command: `executeAIPrompt/${prompt.id}/${doc.id}`, hint: prompt.desc}));
if(aiActionItems.length === 1) aiAction = aiActionItems[0];
else aiAction = {text: aiLang.dropdownTitle || 'AI', type: 'dropdown', rounded: 'lg', placement: 'bottom-end', caret: false, items: aiActionItems};
else aiAction = {text: aiLang.dropdownTitle || 'AI', type: 'dropdown', btnType: 'ai-styled', size: 'sm', caret: 'down', placement: 'bottom-end', items: aiActionItems};
}
const actions = isInModal ? [] : [
@@ -2081,6 +2083,11 @@ function getTableOptions(options, info)
}
});
}
if(!hasPriv('batchMoveDoc') && !hasPriv('exportDoc'))
{
options.checkable = false;
options.footer = options.footer.filter(f => f !== 'checkbox' && f !== 'checkedInfo');
}
return options;
}
@@ -2343,8 +2350,8 @@ function handleImportDocSnapshot(snapshot)
const fixUrl = (url, method, paramList) =>
{
const link = $.parseLink(url, 'auto');
if(link.requestType.toUpperCase() === requestType || method.toLowerCase() !== link.methodName.toLowerCase()) return url;
return $.createLink('doc', method, paramList.map((p, i) => [p, link.vars[i][1]]));
if(link.requestType.toUpperCase() === requestType || method.toLowerCase() !== link.methodName.toLowerCase()) return url
return `${location.origin}${$.createLink('doc', method, paramList.map((p, i) => [p, link.vars[i][1]]))}`;
};
/* Fix custom block with fetcher in difference request type. */
forEachBlock(snapshot.blocks.children, block =>
+29
View File
@@ -54,3 +54,32 @@ window.insertListToDoc = function(tableID, blockType, blockID, insertListLink)
}
});
}
window.insertListToAI = function(tableID, blockType)
{
const dtable = zui.DTable.query($(tableID));
const myTable = dtable.$;
const checkedList = Object.keys(myTable.state.checkedRows);
if(!checkedList.length) return;
const data = checkedList.filter(rowID => myTable._checkedRows[rowID] !== undefined).map(rowID => {
const item = myTable._checkedRows[rowID];
if(blockType === 'case' && typeof item.id === 'string' && item.id.startsWith('case_'))
{
item.id = item.caseID || parseInt(item.id.replace('case_', ''));
}
return item;
});
const url = $.createLink('ai', 'ajaxBatchCreateKnowledge', `knowledgeLibID=${knowledgeLibID}`);
const formData = new FormData();
formData.append('type', blockType);
formData.append('data', JSON.stringify(data));
formData.append('idList', checkedList.join(','));
$.ajaxSubmit({
url,
data: formData,
onSuccess: function() {zui.Modal.hide();}
});
}
+11 -2
View File
@@ -114,8 +114,17 @@ class editor extends wg
$customProps = $this->getRestProps();
if(!isset($customProps['class'])) $customProps['class'] = 'w-full';
$addCss = '';
if($this->prop('maxHeight'))
{
$height = $this->prop('maxHeight');
$addCss = <<<CSS
.editor {max-height: $height;}
CSS;
}
$editor->setProp($customProps);
$editor->setProp('css', self::$css); // Inject CSS into editor.
$editor->setProp('css', static::$css . $addCss); // Inject CSS into editor.
$editor->setProp('css-src', $app->getWebRoot() . 'js/zui3/zen-editor/zui-inject-style.css'); // Inject CSS on page, for tippy menus.
/* Set initial content. */
@@ -130,7 +139,7 @@ class editor extends wg
(
setClass('editor-container p-px mt-px rounded relative', $this->prop('readonly') ? 'is-readonly' : ''),
$props['size'] === 'full' ? setStyle('height', '100%') : setClass('h-auto'),
h::css(self::$css), // Inject CSS on page, for tippy menus.
h::css(static::$css . $addCss), // Inject CSS on page, for tippy menus.
$templateType ? $this->buildTemplate($this->prop('name'), $templateType) : null,
$editor,
textarea
+3 -2
View File
@@ -59,8 +59,9 @@ class featureBar extends wg
data('activeFeature', $current);
if(empty($commonLink)) $commonLink = createLink($app->rawModule, $app->rawMethod, $this->prop('linkParams'));
if(empty($searchModule)) $searchModule = data("config.{$currentModule}.search.module") ? data("config.{$currentModule}.search.module") : $currentModule;
if(empty($commonLink)) $commonLink = createLink($app->rawModule, $app->rawMethod, $this->prop('linkParams'));
if(empty($searchModule)) $searchModule = data("config.{$currentModule}.search.module");
if(empty($searchModule)) $searchModule = $currentModule;
foreach($rawItems as $rawItem)
{
+14
View File
@@ -107,6 +107,20 @@ class fileSelector extends wg
}
$this->setProp('defaultFiles', $defaultFiles);
}
if($this->hasProp('accept'))
{
$accept = explode(',', $this->prop('accept'));
$dangers = explode(',', $app->config->file->dangers);
$filteredAccept = array_filter($accept, function($item) use ($dangers)
{
$item = strtolower($item);
$ext = ltrim($item, '.');
return !in_array($ext, $dangers);
});
$newAccept = implode(',', $filteredAccept);
$this->setProp('accept', $newAccept);
}
/* Check file type. */
$acceptFileTypes = $this->prop('accept') ? ',' . str_replace('.', '', $this->prop('accept')) . ',' : '';
+534
View File
@@ -0,0 +1,534 @@
<?php
/*
* 这个脚本用来比较两个数据库表的结构差异,并生成相应的SQL语句来同步它们。
* 从两个文件中读取表结构,比较差异,并输出同步SQL语句。
* 比较的表结构文件格式为MySQL的CREATE TABLE语句。
* 比较差异时遵循如下规则:
* 1. 如果发现源表有而目标表没有的列,则不作处理。
* 2. 如果发现目标表有而源表没有的列,则生成ADD COLUMN语句。
* 3. 如果发现列定义不同,则生成ALTER COLUMN语句。
* - 不考虑列顺序的不同。
* - 如果数据类型不同、长度不同、是否允许NULL不同或默认值不同,则视为不同。
* - 如果源表的字段取值范围更大,则视为数据类型和长度相同,仅比较是否允许NULL和默认值。生成ALTER COLUMN语句时,仅修改允许NULL和默认值。
* - 如果源表字段是 ENUM,目标表不是 ENUM:
* - 若目标是 VARCHAR 且长度 >= 最长枚举值 → 视为兼容,仅比较 NULL/DEFAULT;
* - 否则 → 视为不兼容,生成 MODIFY 为 VARCHAR(N) 的语句;
* - 忽略索引和约束的差异。
* 使用方法:php diffTable.php source_db_file target_db_file [--debug]
* 其中 source_db_file 和 target_db_file 是包含 CREATE TABLE 语句的文本文件。--debug 可选参数,用于输出调试信息。
* 例如:
* php diffTable.php db/standard/zentao21.7.7.sql db/standard/zentao21.7.8.sql --debug
*/
$debug = in_array('--debug', $argv);
function logDebug($msg)
{
global $debug;
if($debug)
{
echo "[DEBUG] " . $msg . "\n";
}
}
function parseCreateTable($createTableSql)
{
$tables = [];
$sqls = explode(";", $createTableSql);
foreach($sqls as $sql)
{
$sql = trim($sql);
if($sql === '') continue;
if(stripos($sql, 'CREATE TABLE') === 0)
{
if(preg_match('/CREATE TABLE `([^`]+)` \((.*)\) ENGINE=InnoDB\s*/s', $sql, $matches))
{
$tableName = $matches[1];
$columnsDef = $matches[2];
logDebug("Found table: $tableName");
$columns = parseColumns($columnsDef);
$tables[$tableName] = $columns;
}
else
{
logDebug("Failed to parse CREATE TABLE: " . substr($sql, 0, 100) . "...");
}
}
}
logDebug("Parsed " . count($tables) . " tables from SQL");
return $tables;
}
function parseColumns($columnsDef)
{
$columns = [];
$lines = explode("\n", trim($columnsDef));
foreach($lines as $line)
{
$line = trim($line, " ,");
if(preg_match('/^`([^`]+)` (.+)$/', $line, $matches))
{
$columnName = $matches[1];
$columnDef = $matches[2];
$columns[$columnName] = parseColumnDefinition($columnDef);
}
}
return $columns;
}
function parseColumnDefinition($columnDef)
{
$definition = [];
// 解析 ENUM
if(preg_match("/^ENUM\s*\((.+)\)/i", $columnDef, $enumMatches))
{
$definition['type'] = 'ENUM';
$definition['length'] = null;
preg_match_all("/'([^']*)'|\"([^\"]*)\"/", $enumMatches[1], $values);
$definition['enumValues'] = array_unique(array_filter(array_merge($values[1], $values[2]), function($v)
{
return $v !== '';
}));
$definition['unsigned'] = false;
logDebug("Parsed ENUM: " . json_encode($definition['enumValues']));
}
elseif(preg_match('/^([a-zA-Z]+)(\(([^)]+)\))?/', $columnDef, $matches))
{
$definition['type'] = strtoupper($matches[1]);
$definition['length'] = isset($matches[3]) ? $matches[3] : null;
$definition['enumValues'] = null;
$definition['unsigned'] = stripos($columnDef, 'unsigned') !== false;
}
else
{
$definition['type'] = 'UNKNOWN';
$definition['length'] = null;
$definition['enumValues'] = null;
$definition['unsigned'] = false;
logDebug("Unknown column definition: $columnDef");
}
$definition['nullable'] = stripos($columnDef, 'NOT NULL') === false;
if(preg_match('/DEFAULT\s+(NULL|\'[^\']*\'|"[^"]*"|[\w\.\-]+)/i', $columnDef, $matches))
{
$rawDefault = $matches[1];
if(strtoupper(trim($rawDefault)) === 'NULL')
{
$definition['default'] = null;
}
else
{
$definition['default'] = trim($rawDefault, " '\"");
}
}
else
{
$definition['default'] = null;
}
return $definition;
}
// 辅助:获取 ENUM 最长值长度
function getLongestEnumLength($enumValues)
{
if(empty($enumValues)) return 1;
return max(array_map('strlen', $enumValues));
}
function isIntegerType($type)
{
$type = strtoupper($type);
return in_array($type, ['TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT']);
}
function isFloatType($type)
{
$type = strtoupper($type);
return in_array($type, ['FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC']);
}
function isNumericType($type)
{
$type = strtoupper($type);
return in_array($type, [
'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER',
'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC'
]);
}
// 判断是否兼容(源 >= 目标)
function isCompatibleColumn($sourceDef, $targetDef)
{
$sourceType = strtoupper($sourceDef['type']);
$targetType = strtoupper($targetDef['type']);
// 情况1:源是 ENUM
if($sourceType === 'ENUM')
{
if($targetType === 'ENUM')
{
// 目标也是 ENUM:检查是否包含所有源值
if(!$targetDef['enumValues']) return false;
$missing = array_diff($sourceDef['enumValues'], $targetDef['enumValues']);
if(!empty($missing))
{
logDebug("ENUM missing values in target: " . json_encode($missing));
return false;
}
return true;
}
else
{
// 目标不是 ENUM
if($targetType === 'VARCHAR')
{
$targetLen = (int)($targetDef['length'] ?? 0);
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
$compatible = $targetLen >= $requiredLen;
logDebug("ENUM -> VARCHAR: required=$requiredLen, target=$targetLen, compatible=$compatible");
return $compatible;
}
logDebug("ENUM -> non-VARCHAR ($targetType): not compatible");
// 其他类型(如 TINYINT)视为不兼容
return false;
}
}
// 情况2:源不是 ENUM,走原兼容逻辑
$numericHierarchy = [
'TINYINT' => 1, 'SMALLINT' => 2, 'MEDIUMINT' => 3,
'INT' => 4, 'INTEGER' => 4, 'BIGINT' => 5,
'FLOAT' => 6, 'DOUBLE' => 7, 'DECIMAL' => 8
];
$stringHierarchy = [
'CHAR' => 1, 'VARCHAR' => 2, 'TINYTEXT' => 3,
'TEXT' => 4, 'MEDIUMTEXT' => 5, 'LONGTEXT' => 6
];
if(isset($numericHierarchy[$sourceType]) && isset($numericHierarchy[$targetType]))
{
if($numericHierarchy[$sourceType] > $numericHierarchy[$targetType]) return true;
if($numericHierarchy[$sourceType] === $numericHierarchy[$targetType])
{
if(isIntegerType($sourceType))
{
return $sourceDef['unsigned'] === $targetDef['unsigned'];
}
if(isFloatType($sourceType))
{
$sourceLen = explode(',', $sourceDef['length'] ?? '');
$sourceInteger = (int)($sourceLen[0] ?? 0);
$sourceDecimal = (int)($sourceLen[1] ?? 0);
$targetLen = explode(',', $targetDef['length'] ?? '');
$targetInteger = (int)($targetLen[0] ?? 0);
$targetDecimal = (int)($targetLen[1] ?? 0);
return $sourceInteger >= $targetInteger && $sourceDecimal >= $targetDecimal;
}
}
}
if(in_array($sourceType, ['CHAR','VARCHAR']) && in_array($targetType, ['CHAR','VARCHAR']))
{
return (int)($sourceDef['length'] ?? 0) > (int)($targetDef['length'] ?? 0);
}
if(isset($stringHierarchy[$sourceType]) && isset($stringHierarchy[$targetType]))
{
return $stringHierarchy[$sourceType] >= $stringHierarchy[$targetType];
}
return false;
}
function compareColumnDefinitions($sourceDef, $targetDef)
{
$sourceType = strtoupper($sourceDef['type']);
$targetType = strtoupper($targetDef['type']);
// 特殊处理 ENUM
if($sourceType === 'ENUM')
{
if($sourceType !== $targetType)
{
logDebug("ENUM type mismatch: source=$sourceType, target=$targetType");
return false; // 类型不同,直接不等
}
// 同为 ENUM,检查兼容性
if(isCompatibleColumn($sourceDef, $targetDef))
{
$eq = ($sourceDef['nullable'] === $targetDef['nullable'] && $sourceDef['default'] === $targetDef['default']);
logDebug("ENUM compatible, NULL/DEFAULT equal: " . ($eq ? 'YES' : 'NO'));
return $eq;
}
return false; // 不兼容
}
// 非 ENUM:使用通用兼容逻辑
if(isCompatibleColumn($sourceDef, $targetDef))
{
$eq = ($sourceDef['nullable'] === $targetDef['nullable'] && $sourceDef['default'] === $targetDef['default']);
logDebug("Compatible type, NULL/DEFAULT equal: " . ($eq ? 'YES' : 'NO'));
return $eq;
}
$strictEq = ($sourceDef['type'] === $targetDef['type'] &&
$sourceDef['unsigned'] === $targetDef['unsigned'] &&
$sourceDef['length'] === $targetDef['length'] &&
$sourceDef['nullable'] === $targetDef['nullable'] &&
$sourceDef['default'] === $targetDef['default']);
logDebug("Strict compare: " . ($strictEq ? 'EQUAL' : 'DIFFERENT'));
return $strictEq;
}
function buildColumnDefinition($columnDef)
{
if($columnDef['type'] === 'ENUM' && !empty($columnDef['enumValues']))
{
$quoted = array_map(function($v)
{
return "'" . str_replace("'", "''", $v) . "'";
}, $columnDef['enumValues']);
$definition = "enum(" . implode(", ", $quoted) . ")"; // 小写 enum
}
else
{
$definition = strtolower($columnDef['type']); // 小写类型
if($columnDef['length'])
{
$definition .= "({$columnDef['length']})";
}
if(isNumericType($columnDef['type']) && !empty($columnDef['unsigned']))
{
$definition .= " unsigned";
}
}
$definition .= $columnDef['nullable'] ? " NULL" : " NOT NULL";
if($columnDef['default'] === null)
{
if($columnDef['nullable'])
{
$definition .= " DEFAULT NULL";
}
}
else
{
if(isNumericType($columnDef['type']))
{
$definition .= " DEFAULT " . $columnDef['default'];
}
else
{
$escaped = str_replace("'", "''", $columnDef['default']);
$definition .= " DEFAULT '{$escaped}'";
}
}
return $definition;
}
function buildColumnDefinitionForModify($sourceDef, $targetDef)
{
// 如果源是 ENUM 且目标不兼容,生成 VARCHAR(N)
if(strtoupper($sourceDef['type']) === 'ENUM')
{
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
if($requiredLen < $targetDef['length'])
{
$requiredLen = $targetDef['length'];
}
$definition = "varchar({$requiredLen})";
}
else
{
// 否则保留源类型和长度
$definition = strtolower($sourceDef['type']);
if($sourceDef['length'])
{
$definition .= "({$sourceDef['length']})";
}
if(isNumericType($sourceDef['type']) && !empty($sourceDef['unsigned']))
{
$definition .= " unsigned";
}
}
$definition .= $targetDef['nullable'] ? " NULL" : " NOT NULL";
if($targetDef['default'] === null)
{
if($targetDef['nullable'])
{
$definition .= " DEFAULT NULL";
}
}
else
{
if(isNumericType($targetDef['type']))
{
$definition .= " DEFAULT " . $targetDef['default'];
}
else
{
$escaped = str_replace("'", "''", $targetDef['default']);
$definition .= " DEFAULT '{$escaped}'";
}
}
return $definition;
}
function compareTables($sourceTables, $targetTables)
{
$sqlStatements = [];
logDebug("Comparing " . count($targetTables) . " target tables");
foreach($targetTables as $tableName => $targetColumns)
{
logDebug("Processing table: $tableName");
if(!isset($sourceTables[$tableName]))
{
logDebug("Source table $tableName not found, skipping");
continue;
}
$alterStatements = [];
$enumStatements = [];
$sourceColumns = $sourceTables[$tableName];
foreach($targetColumns as $columnName => $targetDef)
{
if(!isset($sourceColumns[$columnName]))
{
logDebug("Column $columnName exists in target but not source → ADD");
$sqlStatements[] = "ALTER TABLE `$tableName` ADD COLUMN `$columnName` " . buildColumnDefinition($targetDef) . ($columnName === 'id' ? ' AUTO_INCREMENT PRIMARY KEY' : '') . ";";
}
else
{
$enum2int = false;
$sourceDef = $sourceColumns[$columnName];
if(!compareColumnDefinitions($sourceDef, $targetDef))
{
// 判断是否只需修改 NULL/DEFAULT,或需要改类型
if(isCompatibleColumn($sourceDef, $targetDef))
{
// 兼容,只改 NULL/DEFAULT
$colDef = buildColumnDefinitionForModify($sourceDef, $targetDef);
logDebug("Compatible difference → MODIFY (only NULL/DEFAULT): $colDef");
}
else
{
// 不兼容:若源是 ENUM,目标非 ENUM 且不满足长度,转为 VARCHAR
if(strtoupper($sourceDef['type']) === 'ENUM')
{
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
if($requiredLen < $targetDef['length'])
{
$requiredLen = $targetDef['length'];
}
// 构造新的 VARCHAR 定义(使用目标的 nullable/default)
$colDef = "varchar({$requiredLen})" . ($targetDef['nullable'] ? " NULL" : " NOT NULL");
if($targetDef['default'] !== null)
{
$escaped = str_replace("'", "''", $targetDef['default']);
$colDef .= " DEFAULT '{$escaped}'";
}
logDebug("ENUM incompatible → MODIFY to VARCHAR($requiredLen): $colDef");
$enum2int = true;
foreach($sourceDef['enumValues'] as $val)
{
if(!is_numeric($val))
{
$enum2int = false;
break;
}
}
}
else
{
// 其他情况:直接使用目标定义
$colDef = buildColumnDefinition($targetDef);
logDebug("Incompatible → MODIFY to target definition: $colDef");
}
}
$alterStatements[] = "MODIFY COLUMN `$columnName` {$colDef}" . ($columnName === 'id' ? ' AUTO_INCREMENT' : '');
if($enum2int)
{
logDebug("Incompatible → MODIFY to INT for ENUM to INT conversion");
$enumStatements[] = "MODIFY COLUMN `$columnName` tinyint unsigned" . ($targetDef['nullable'] ? " NULL" : " NOT NULL") . ($targetDef['default'] !== null ? " DEFAULT " . $targetDef['default'] : '');
}
}
else
{
logDebug("Column $columnName identical");
}
}
}
if($alterStatements)
{
$sqlStatements[] = "ALTER TABLE `$tableName`\n " . implode(",\n ", $alterStatements) . ";";
}
if($enumStatements)
{
$sqlStatements[] = "ALTER TABLE `$tableName`\n " . implode(",\n ", $enumStatements) . ";";
}
}
logDebug("Generated " . count($sqlStatements) . " SQL statements");
return $sqlStatements;
}
if($argc !== 3 && $argc !== 4)
{
echo "[Info] Usage: php diffTable.php source_db_file target_db_file [--debug]\n";
echo "[Info] Example: php diffTable.php db/standard/zentao21.7.7.sql db/standard/zentao21.7.8.sql --debug\n";
echo "[Info] The source_db_file and target_db_file should contain CREATE TABLE statements.\n";
echo "[Info] The --debug option is optional for debug output.\n";
echo "[Info] Note: The CREATE TABLE statements must end with ENGINE=InnoDB;\n";
exit(1);
}
$sourceDbFile = $argv[1];
$targetDbFile = $argv[2];
if(!file_exists($sourceDbFile))
{
echo "[Info] Error: Source file '$sourceDbFile' does not exist.\n";
exit(1);
}
if(!file_exists($targetDbFile))
{
echo "[Info] Error: Target file '$targetDbFile' does not exist.\n";
exit(1);
}
logDebug("Reading source file: $sourceDbFile");
logDebug("Reading target file: $targetDbFile");
$sourceSql = file_get_contents($sourceDbFile);
$targetSql = file_get_contents($targetDbFile);
if(trim($sourceSql) === '')
{
echo "[Info] Warning: Source file is empty.\n";
}
if(trim($targetSql) === '')
{
echo "[Info] Warning: Target file is empty.\n";
}
$sourceTables = parseCreateTable($sourceSql);
$targetTables = parseCreateTable($targetSql);
if(empty($targetTables))
{
echo "[Info] No target tables parsed. Check CREATE TABLE syntax (must end with ENGINE=InnoDB;).\n";
exit(1);
}
$sqlStatements = compareTables($sourceTables, $targetTables);
foreach($sqlStatements as $key => $sql)
{
$sqlStatements[$key] = str_replace(" varchar(1) ", " char(1) ", $sql); // 优化:长度为1的VARCHAR改为CHAR
}
file_put_contents('difftable.sql', implode("\n", $sqlStatements) . "\n");
echo "[Info] Generated SQL statements saved to difftable.sql\n";
+11 -4
View File
@@ -1,6 +1,6 @@
<?php
/**
* PHP 隐式可空类型修复脚本 - 最终版本
* PHP 隐式可空类型修复脚本
* 用于修复 RFC: Deprecate implicitly nullable types 问题
* https://wiki.php.net/rfc/deprecate-implicitly-nullable-types
*
@@ -9,6 +9,7 @@
* - 支持命名空间类型(\Namespace\ClassName)
* - 支持跨行函数参数
* - 自动跳过已有?的参数
* - 自动跳过 mixed 类型的参数
* - 自动跳过联合类型中已包含null的参数
* - 完整日志记录
*/
@@ -123,6 +124,12 @@ class NullableTypesFixer
continue; // 已经有 ? 了
}
// 检查是否是 mixed 类型
if($type == 'mixed')
{
continue;
}
// 检查是否是联合类型且已包含 null
if(stripos($type, '|null') !== false || stripos($type, 'null|') !== false)
{
@@ -247,13 +254,13 @@ class NullableTypesFixer
// 使用示例
if($argc < 2)
{
echo "用法: php fix_nullable_types_final.php <目录路径> [--dry-run]\n";
echo "用法: php fix_nullable_types.php <目录路径> [--dry-run]\n";
echo "参数说明:\n";
echo " <目录路径> 要扫描的PHP文件目录\n";
echo " --dry-run 预览模式,不实际修改文件\n";
echo "\n示例:\n";
echo " php fix_nullable_types_final.php /path/to/project\n";
echo " php fix_nullable_types_final.php /path/to/project --dry-run\n";
echo " php fix_nullable_types.php /path/to/project\n";
echo " php fix_nullable_types.php /path/to/project --dry-run\n";
exit(1);
}
+1 -1
View File
@@ -15,7 +15,7 @@ $config->action->objectNameFields['bug'] = 'title';
$config->action->objectNameFields['testcase'] = 'title';
$config->action->objectNameFields['case'] = 'title';
$config->action->objectNameFields['testtask'] = 'name';
$config->action->objectNameFields['user'] = 'account';
$config->action->objectNameFields['user'] = 'realname';
$config->action->objectNameFields['api'] = 'title';
$config->action->objectNameFields['board'] = 'name';
$config->action->objectNameFields['boardspace'] = 'name';
+1 -1
View File
@@ -812,7 +812,7 @@ class actionModel extends model
{
if(in_array($actionType, array('restoredsnapshot', 'createdsnapshot')) && in_array($action->objectType, array('vm', 'zanode')) && $value == 'defaultSnap') $value = $this->lang->{$objectType}->snapshot->defaultSnapName;
if(!is_array($value)) $desc = str_replace('$' . $key, (string)$value, $desc);
if(!is_array($value) && !is_object($value)) $desc = str_replace('$' . $key, (string)$value, $desc);
}
}
+3 -2
View File
@@ -243,7 +243,7 @@ class actionTao extends actionModel
*/
public function getReleaseRelated(string $objectType, int $objectID): array
{
$product = array(0);
$product = 0;
$project = 0;
$result = $this->dao->select('product, build')->from($this->config->objectTables[$objectType])->where('id')->eq($objectID)->fetch();
if($result)
@@ -628,7 +628,8 @@ class actionTao extends actionModel
$table = $this->config->objectTables[$module];
$field = $this->config->action->objectNameFields[$module];
$name = $this->dao->select($field)->from($table)->where('id')->eq($id)->fetch($field);
if($name) $action->appendLink = html::a(helper::createLink($module, 'view', "id={$id}"), "#{$id} " . $name);
$method = $module == 'story' ? 'storyView' : 'view';
if($name) $action->appendLink = html::a(helper::createLink($module, $method, "id={$id}"), "#{$id} " . $name);
}
$action->extra = $extra;
}
@@ -171,7 +171,7 @@ class actionTest
* @access public
* @return array
*/
public function getTrashesBySearchTest(string $objectType, string $type, string|int $queryID, string $orderBy, object $pager = null): array
public function getTrashesBySearchTest(string $objectType, string $type, string|int $queryID, string $orderBy, ?object $pager = null): array
{
$objects = $this->objectModel->getTrashesBySearch($objectType, $type, $queryID, $orderBy, $pager);
if(dao::isError()) return dao::getError();
@@ -888,7 +888,7 @@ class actionTest
if(dao::isError()) return dao::getError();
return $output;
return str_replace("\n", '', $output);
}
/**
@@ -926,6 +926,24 @@ class actionTest
return $result;
}
/**
* Test processActionForAPI method specifically for history field mapping.
*
* @param array|object $actions
* @param array|object $users
* @param array|object $objectLang
* @access public
* @return string
*/
public function processActionForAPIHistoryTest($actions, $users = array(), $objectLang = array()): string
{
$result = $this->processActionForAPITest($actions, $users, $objectLang);
if(empty($result) || !isset($result[0]->history) || !is_array($result[0]->history) || empty($result[0]->history)) {
return '';
}
return $result[0]->history[0]->fieldName ?? '';
}
/**
* Test buildTrashSearchForm method.
*
@@ -1344,4 +1362,21 @@ class actionTest
if(dao::isError()) return dao::getError();
return $result;
}
/**
* 根据类型和ID获取操作记录列表。
* Get action list by type and ID.
*
* @param string $objectType
* @param int|array $objectID
* @param array $modules
* @access public
* @return array
*/
public function getActionListByTypeAndIDTest(string $objectType, int|array $objectID, array $modules): array
{
$result = $this->objectTao->getActionListByTypeAndID($objectType, $objectID, $modules);
if(dao::isError()) return dao::getError();
return $result;
}
}
+17
View File
@@ -50,4 +50,21 @@ class actionTaoTest
if(dao::isError()) return dao::getError();
return $action;
}
/**
* Test processCreateChildrenActionExtra method.
*
* @param string $taskIDs
* @access public
* @return object
*/
public function processCreateChildrenActionExtraTest(string $taskIDs): object
{
$action = new stdClass();
$action->extra = $taskIDs;
$this->objectTao->processCreateChildrenActionExtra($action);
if(dao::isError()) return dao::getError();
return $action;
}
}
@@ -7,11 +7,11 @@ title=测试 actionModel::getDynamicByProject();
timeout=0
cid=14897
- 执行actionTest模块的getDynamicByProjectTest方法,参数是101 @20
- 执行actionTest模块的getDynamicByProjectTest方法,参数是101, 'admin' @10
- 执行actionTest模块的getDynamicByProjectTest方法 @0
- 执行actionTest模块的getDynamicByProjectTest方法,参数是999 @0
- 执行actionTest模块的getDynamicByProjectTest方法,参数是101, 'all', 'today' @5
- 正常项目ID获取所有用户动态 @20
- 指定用户admin获取项目动态 @10
- 指定用户user1获取项目动态 @0
- 无效项目ID(0)获取动态 @30
- 不存在的项目ID获取动态 @0
*/
@@ -19,18 +19,18 @@ include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/action.unittest.class.php';
// zendata数据准备
$action = zenData('action');
$action = zenData('actionrecent');
$action->id->range('1-30');
$action->objectType->range('task{10},story{10},bug{10}');
$action->objectID->range('1-100');
$action->project->range('101{20},102{5},103{5}');
$action->actor->range('admin{10},dev17{10},test18{10}');
$action->action->range('opened{15},edited{10},closed{5}');
$action->date->range('(-1h)-(+1w):1D');
$action->date->range('(-1h)-(+1w):1D')->type('timestamp')->format('YY/MM/DD hh:mm:ss');
$action->gen(30);
zenData('project')->loadYaml('execution')->gen(12);
zenData('user')->loadYaml('user')->gen(5);
zenData('user')->loadYaml('user')->gen(3);
// 用户登录
su('admin');
@@ -38,13 +38,8 @@ su('admin');
// 创建测试实例
$actionTest = new actionTest();
// 测试步骤1:正常项目ID获取所有用户动态
r($actionTest->getDynamicByProjectTest(101)) && p() && e('20');
// 测试步骤2:指定用户admin获取项目动态
r($actionTest->getDynamicByProjectTest(101, 'admin')) && p() && e('10');
// 测试步骤3:无效项目ID(0)获取动态
r($actionTest->getDynamicByProjectTest(0)) && p() && e('0');
// 测试步骤4:不存在的项目ID获取动态
r($actionTest->getDynamicByProjectTest(999)) && p() && e('0');
// 测试步骤5:指定时间段获取项目动态
r($actionTest->getDynamicByProjectTest(101, 'all', 'today')) && p() && e('5');
r($actionTest->getDynamicByProjectTest(101)) && p() && e('20'); // 正常项目ID获取所有用户动态
r($actionTest->getDynamicByProjectTest(101, 'admin')) && p() && e('10'); // 指定用户admin获取项目动态
r($actionTest->getDynamicByProjectTest(101, 'user1')) && p() && e('0'); // 指定用户user1获取项目动态
r($actionTest->getDynamicByProjectTest(0)) && p() && e('30'); // 无效项目ID(0)获取动态
r($actionTest->getDynamicByProjectTest(999)) && p() && e('0'); // 不存在的项目ID获取动态
+6 -3
View File
@@ -4,7 +4,10 @@ include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/action.unittest.class.php';
su('admin');
zenData('action')->loadYaml('action')->gen(6);
$action = zenData('action');
$action->date->range('(-1h)-(+1w):1D')->type('timestamp')->format('YY/MM/DD hh:mm:ss');
$action->gen(6);
zenData('actionrecent')->gen(5);
zenData('actionproduct')->gen(0);
@@ -41,7 +44,7 @@ r($actionModel->getDynamicCount('all')) && p() && e('2'); // 测试传入条件
$sql = $actionModel->dao->get();
r(strpos($sql, "LEFT JOIN `zt_actionproduct` AS t2 ON action.id=t2.action") === false) && p() && e('1'); // 检查SQL。
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1';";
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1'";
r($actionModel->getDynamicCount('all')) && p() && e('0'); // 测试传入条件 actor='admin' AND t2.product='1'。
$sql = $actionModel->dao->get();
@@ -51,4 +54,4 @@ $_SESSION['actionQueryCondition'] = "1=1";
r($actionModel->getDynamicCount('today')) && p() && e('0'); // 测试传入条件 1=1。
$sql = $actionModel->dao->get();
r(strpos($sql, "SELECT action.id FROM `zt_actionrecent` AS action") !== false) && p() && e('1'); // 检查SQL。
r(strpos($sql, "SELECT action.id FROM `zt_actionrecent` AS action") !== false) && p() && e('1'); // 检查SQL。
+5 -6
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env php
<?php
/**
title=测试 actionModel::getHistory();
@@ -47,8 +46,8 @@ su('admin');
$actionTest = new actionTest();
r($actionTest->getHistoryTest(1)) && p("0:field,old,new") && e('resolution,1,2'); // 测试步骤1:使用整数actionID查询存在的历史记录
r($actionTest->getHistoryTest('2')) && p("0:field,old,new") && e('resolvedBuild,2,3'); // 测试步骤2:使用字符串actionID查询存在的历史记录
r($actionTest->getHistoryTest(3)) && p("") && e('0'); // 测试步骤3:查询没有历史记录的actionID
r($actionTest->getHistoryTest(10000)) && p("") && e('0'); // 测试步骤4:查询不存在的actionID
r($actionTest->getHistoryTest(0)) && p("") && e('0'); // 测试步骤5:使用无效的actionID
r($actionTest->getHistoryTest(1)[1]) && p("0:field,old,new") && e('resolution,1,2'); // 测试步骤1:使用整数actionID查询存在的历史记录
r($actionTest->getHistoryTest('2')[2]) && p("0:field,old,new") && e('resolvedBuild,2,3'); // 测试步骤2:使用字符串actionID查询存在的历史记录
r($actionTest->getHistoryTest(3)) && p() && e('0'); // 测试步骤3:查询没有历史记录的actionID
r($actionTest->getHistoryTest(10000)) && p() && e('0'); // 测试步骤4:查询不存在的actionID
r($actionTest->getHistoryTest(0)) && p() && e('0'); // 测试步骤5:使用无效的actionID
+6 -3
View File
@@ -4,7 +4,10 @@ include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/action.unittest.class.php';
su('admin');
zenData('action')->loadYaml('action')->gen(6);
$action = zenData('action');
$action->date->range('(-1h)-(+1w):1D')->type('timestamp')->format('YY/MM/DD hh:mm:ss');
$action->gen(6);
zenData('actionrecent')->gen(5);
zenData('actionproduct')->gen(0);
@@ -35,7 +38,7 @@ r(count($actionModel->getMoreActions($actionID))) && p() && e('0'); // 测试
$_SESSION['actionQueryCondition'] = "actor='admin'";
r(count($actionModel->getMoreActions($actionID))) && p() && e('0'); // 测试传入条件 actor='admin'。
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1';";
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1'";
r(count($actionModel->getMoreActions($actionID))) && p() && e('0'); // 测试传入条件 actor='admin' AND t2.product='1'。
$actionModel->dao->update(TABLE_ACTION)->set("`date` = concat('2025-05-19 15:15:5', id)")->exec();
@@ -45,5 +48,5 @@ r(count($actionModel->getMoreActions($actionID))) && p() && e('2'); // 测试
$_SESSION['actionQueryCondition'] = "actor='admin'";
r(count($actionModel->getMoreActions($actionID))) && p() && e('1'); // 测试传入条件 actor='admin'。
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1';";
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1'";
r(count($actionModel->getMoreActions($actionID))) && p() && e('0'); // 测试传入条件 actor='admin' AND t2.product='1'。
+64 -63
View File
@@ -11,7 +11,7 @@ zenData('projectproduct')->loadYaml('projectproduct')->gen(2);
zenData('story')->loadYaml('story')->gen(5);
zenData('build')->loadYaml('build')->gen(2);
zenData('projectstory')->loadYaml('projectstory')->gen(1);
zenData('branch')->loadYaml('branch')->loadYaml(1);
zenData('branch')->loadYaml('branch')->gen(1);
zenData('case')->loadYaml('case')->gen(1);
zenData('testtask')->loadYaml('testtask')->gen(1);
zenData('doc')->gen(1);
@@ -22,6 +22,7 @@ zenData('team')->gen(1);
zenData('module')->loadYaml('module')->gen(1);
zenData('review')->loadYaml('review')->gen(1);
zenData('productplan')->gen(1);
zenData('release')->gen(1);
/**
@@ -47,105 +48,105 @@ cid=14909
- 属性execution @4
- 测试获取objectType story objectId 5 actionType linked2build extra 11 的动态信息
- 属性product @,1,
- 属性project @0
- 属性project @12
- 属性execution @0
- 测试获取objectType story objectId 6 actionType unlinkedfrombuild extra 11 的动态信息
- 属性product @,1,
- 属性project @0
- 属性project @12
- 属性execution @0
- 测试获取objectType story objectId 7 actionType estimated extra 11 的动态信息
- 属性product @,1,
- 属性project @0
- 属性execution @0
- 属性execution @11
- 测试获取objectType story objectId 8 actionType edited extra 11 的动态信息
- 属性product @,2,
- 属性project @0
- 属性execution @11
- 测试获取objectType productplan objectId 9 actionType assigned extra 1 的动态信息
- 测试获取objectType productplan objectId 1 actionType assigned extra 1 的动态信息
- 属性product @,1,
- 属性project @0
- 属性execution @0
- 测试获取objectType branch objectId 10 actionType closed extra 1 的动态信息
- 测试获取objectType branch objectId 1 actionType closed extra 1 的动态信息
- 属性product @,41,
- 属性project @0
- 属性execution @0
- 测试获取objectType testcase objectId 11 actionType deleted extra 1 的动态信息
- 测试获取objectType testcase objectId 1 actionType deleted extra 1 的动态信息
- 属性product @,1,
- 属性project @~~
- 属性execution @101
- 测试获取objectType case objectId 12 actionType deletedfile extra 1 的动态信息
- 测试获取objectType case objectId 1 actionType deletedfile extra 1 的动态信息
- 属性product @,1,
- 属性project @~~
- 属性execution @101
- 测试获取objectType testtask objectId 13 actionType linked2testtask extra 11 的动态信息
- 测试获取objectType build objectId 11 actionType linked2testtask extra 11 的动态信息
- 属性product @,2,
- 属性project @12
- 属性execution @0
- 测试获取objectType testtask objectId 14 actionType unlinkedfromtesttask extra 11 的动态信息
- 属性product @,1,
- 属性project @11
- 属性execution @101
- 测试获取objectType testtask objectId 15 actionType assigned extra 11 的动态信息
- 属性product @,1,
- 属性project @11
- 属性execution @101
- 测试获取objectType doc objectId 16 actionType run extra 1 的动态信息
- 属性product @,1,
- 属性project @0
- 属性execution @0
- 测试获取objectType repo objectId 17 actionType commented extra 1 的动态信息
- 测试获取objectType bug objectId 1 actionType unlinkedfromtesttask extra 11 的动态信息
- 属性product @,0,
- 属性project @0
- 属性execution @0
- 测试获取objectType release objectId 18 actionType activated extra 1 的动态信息
- 属性product @,1,
- 属性project @11
- 属性execution @0
- 测试获取objectType task objectId 19 actionType blocked extra 11 的动态信息
- 测试获取objectType testtask objectId 1 actionType assigned extra 11 的动态信息
- 属性product @,1,
- 属性project @11
- 属性execution @101
- 测试获取objectType kanbanlane objectId 20 actionType moved extra 1 的动态信息
- 测试获取objectType doc objectId 1 actionType run extra 1 的动态信息
- 属性product @,1,
- 属性project @0
- 属性execution @0
- 测试获取objectType repo objectId 1 actionType commented extra 1 的动态信息
- 属性product @,0,
- 属性project @0
- 属性execution @0
- 测试获取objectType release objectId 1 actionType activated extra 1 的动态信息
- 属性product @,1,
- 属性project @11
- 属性execution @0
- 测试获取objectType task objectId 1 actionType blocked extra 11 的动态信息
- 属性product @,1,
- 属性project @11
- 属性execution @101
- 测试获取objectType kanbanlane objectId 1 actionType moved extra 1 的动态信息
- 属性product @,0,
- 属性project @~~
- 属性execution @1
- 测试获取objectType kanbancolumn objectId 21 actionType confirmed extra 1 的动态信息
- 测试获取objectType kanbancolumn objectId 1 actionType confirmed extra 1 的动态信息
- 属性product @,0,
- 属性project @~~
- 属性execution @1
- 测试获取objectType team objectId 22 actionType caseconfirmed extra 11 的动态信息
- 测试获取objectType team objectId 11 actionType caseconfirmed extra 11 的动态信息
- 属性product @,0,
- 属性project @0
- 属性execution @11
- 测试获取objectType whitelist objectId 23 actionType bugconfirmed extra 1 的动态信息
- 测试获取objectType whitelist objectId 1 actionType bugconfirmed extra 1 的动态信息
- 属性product @,1,
- 属性project @0
- 属性execution @0
- 测试获取objectType module objectId 24 actionType deleted extra 1 的动态信息
- 测试获取objectType module objectId 1 actionType deleted extra 1 的动态信息
- 属性product @,1,
- 属性project @0
- 属性execution @0
- 测试获取objectType review objectId 25 actionType started extra 1 的动态信息
- 测试获取objectType review objectId 1 actionType started extra 1 的动态信息
- 属性product @,0,
- 属性project @1
- 属性execution @0
- 测试获取objectType story objectId 26 actionType unlinkedfromproject extra 11 的动态信息
- 测试获取objectType story objectId 1 actionType unlinkedfromproject extra 11 的动态信息
- 属性product @,1,
- 属性project @1
- 属性execution @0
- 测试获取objectType story objectId 27 actionType linked2project extra 11 的动态信息
- 测试获取objectType story objectId 1 actionType linked2project extra 11 的动态信息
- 属性product @,1,
- 属性project @1
- 属性execution @0
- 测试获取objectType story objectId 28 actionType unlinkedfromexecution extra 11 的动态信H息H
- 测试获取objectType story objectId 1 actionType unlinkedfromexecution extra 11 的动态信H息H
- 属性product @,1,
- 属性project @~~
- 属性execution @33
- 测试获取objectType story objectId 29 actionType linked2execution extra 11 的动态信息
- 测试获取objectType story objectId 1 actionType linked2execution extra 11 的动态信息
- 属性product @,1,
- 属性project @~~
- 属性execution @1
- 测试获取objectType story objectId 30 actionType linked2kanban extra 1 的动态信息
- 测试获取objectType story objectId 1 actionType linked2kanban extra 1 的动态信息
- 属性product @,1,
- 属性project @~~
- 属性execution @1
@@ -163,29 +164,29 @@ r($action->getRelatedFieldsTest($objectType[0], $objectId[0], $actionType[0],
r($action->getRelatedFieldsTest($objectType[1], $objectId[1], $actionType[1], $extra[1])) && p('product|project|execution', '|') && e(',2,|0|0'); // 测试获取objectType product objectId 2 actionType extra extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[2], $objectId[2], $actionType[2], $extra[2])) && p('product|project|execution', '|') && e(',1,|3|0'); // 测试获取objectType project objectId 3 actionType opened extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[3], $objectId[3], $actionType[3], $extra[3])) && p('product|project|execution', '|') && e(',1,|0|4'); // 测试获取objectType execution objectId 4 actionType created extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[4], $objectId[4], $actionType[4], $extra[4])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType story objectId 5 actionType linked2build extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[5], $objectId[5], $actionType[5], $extra[5])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType story objectId 6 actionType unlinkedfrombuild extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[6], $objectId[6], $actionType[6], $extra[6])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType story objectId 7 actionType estimated extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[4], $objectId[4], $actionType[4], $extra[4])) && p('product|project|execution', '|') && e(',1,|12|0'); // 测试获取objectType story objectId 5 actionType linked2build extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[5], $objectId[5], $actionType[5], $extra[5])) && p('product|project|execution', '|') && e(',1,|12|0'); // 测试获取objectType story objectId 6 actionType unlinkedfrombuild extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[6], $objectId[6], $actionType[6], $extra[6])) && p('product|project|execution', '|') && e(',1,|0|11'); // 测试获取objectType story objectId 7 actionType estimated extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[7], $objectId[7], $actionType[7], $extra[7])) && p('product|project|execution', '|') && e(',2,|0|11'); // 测试获取objectType story objectId 8 actionType edited extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[8], $objectId[8], $actionType[8], $extra[8])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType productplan objectId 9 actionType assigned extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[9], $objectId[9], $actionType[9], $extra[9])) && p('product|project|execution', '|') && e(',41,|0|0'); // 测试获取objectType branch objectId 10 actionType closed extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[10], $objectId[10], $actionType[10], $extra[10])) && p('product|project|execution', '|') && e(',1,|~~|101'); // 测试获取objectType testcase objectId 11 actionType deleted extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[11], $objectId[11], $actionType[11], $extra[11])) && p('product|project|execution', '|') && e(',1,|~~|101'); // 测试获取objectType case objectId 12 actionType deletedfile extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[12], $objectId[12], $actionType[12], $extra[12])) && p('product|project|execution', '|') && e(',2,|12|0'); // 测试获取objectType testtask objectId 13 actionType linked2testtask extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[13], $objectId[13], $actionType[13], $extra[13])) && p('product|project|execution', '|') && e(',1,|11|101'); // 测试获取objectType testtask objectId 14 actionType unlinkedfromtesttask extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[14], $objectId[14], $actionType[14], $extra[14])) && p('product|project|execution', '|') && e(',1,|11|101'); // 测试获取objectType testtask objectId 15 actionType assigned extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[15], $objectId[15], $actionType[15], $extra[15])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType doc objectId 16 actionType run extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[16], $objectId[16], $actionType[16], $extra[16])) && p('product|project|execution', '|') && e(',0,|0|0'); // 测试获取objectType repo objectId 17 actionType commented extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[17], $objectId[17], $actionType[17], $extra[17])) && p('product|project|execution', '|') && e(',1,|11|0'); // 测试获取objectType release objectId 18 actionType activated extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[18], $objectId[18], $actionType[18], $extra[18])) && p('product|project|execution', '|') && e(',1,|11|101'); // 测试获取objectType task objectId 19 actionType blocked extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[19], $objectId[19], $actionType[19], $extra[19])) && p('product|project|execution', '|') && e(',0,|~~|1'); // 测试获取objectType kanbanlane objectId 20 actionType moved extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[20], $objectId[20], $actionType[20], $extra[20])) && p('product|project|execution', '|') && e(',0,|~~|1'); // 测试获取objectType kanbancolumn objectId 21 actionType confirmed extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[21], $objectId[21], $actionType[21], $extra[21])) && p('product|project|execution', '|') && e(',0,|0|11'); // 测试获取objectType team objectId 22 actionType caseconfirmed extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[22], $objectId[22], $actionType[22], $extra[22])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType whitelist objectId 23 actionType bugconfirmed extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[23], $objectId[23], $actionType[23], $extra[23])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType module objectId 24 actionType deleted extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[24], $objectId[24], $actionType[24], $extra[24])) && p('product|project|execution', '|') && e(',0,|1|0'); // 测试获取objectType review objectId 25 actionType started extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[25], $objectId[25], $actionType[25], $extra[25])) && p('product|project|execution', '|') && e(',1,|1|0'); // 测试获取objectType story objectId 26 actionType unlinkedfromproject extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[26], $objectId[26], $actionType[26], $extra[26])) && p('product|project|execution', '|') && e(',1,|1|0'); // 测试获取objectType story objectId 27 actionType linked2project extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[27], $objectId[27], $actionType[27], $extra[27])) && p('product|project|execution', '|') && e(',1,|~~|33'); // 测试获取objectType story objectId 28 actionType unlinkedfromexecution extra 11 的动态信H息H
r($action->getRelatedFieldsTest($objectType[28], $objectId[28], $actionType[28], $extra[28])) && p('product|project|execution', '|') && e(',1,|~~|1'); // 测试获取objectType story objectId 29 actionType linked2execution extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[29], $objectId[29], $actionType[29], $extra[29])) && p('product|project|execution', '|') && e(',1,|~~|1'); // 测试获取objectType story objectId 30 actionType linked2kanban extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[8], $objectId[8], $actionType[8], $extra[8])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType productplan objectId 1 actionType assigned extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[9], $objectId[9], $actionType[9], $extra[9])) && p('product|project|execution', '|') && e(',41,|0|0'); // 测试获取objectType branch objectId 1 actionType closed extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[10], $objectId[10], $actionType[10], $extra[10])) && p('product|project|execution', '|') && e(',1,|~~|101'); // 测试获取objectType testcase objectId 1 actionType deleted extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[11], $objectId[11], $actionType[11], $extra[11])) && p('product|project|execution', '|') && e(',1,|~~|101'); // 测试获取objectType case objectId 1 actionType deletedfile extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[12], $objectId[12], $actionType[12], $extra[12])) && p('product|project|execution', '|') && e(',2,|12|0'); // 测试获取objectType build objectId 11 actionType linked2testtask extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[13], $objectId[13], $actionType[13], $extra[13])) && p('product|project|execution', '|') && e(',0,|0|0'); // 测试获取objectType bug objectId 1 actionType unlinkedfromtesttask extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[14], $objectId[14], $actionType[14], $extra[14])) && p('product|project|execution', '|') && e(',1,|11|101'); // 测试获取objectType testtask objectId 1 actionType assigned extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[15], $objectId[15], $actionType[15], $extra[15])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType doc objectId 1 actionType run extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[16], $objectId[16], $actionType[16], $extra[16])) && p('product|project|execution', '|') && e(',0,|0|0'); // 测试获取objectType repo objectId 1 actionType commented extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[17], $objectId[17], $actionType[17], $extra[17])) && p('product|project|execution', '|') && e(',1,|11|0'); // 测试获取objectType release objectId 1 actionType activated extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[18], $objectId[18], $actionType[18], $extra[18])) && p('product|project|execution', '|') && e(',1,|11|101'); // 测试获取objectType task objectId 1 actionType blocked extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[19], $objectId[19], $actionType[19], $extra[19])) && p('product|project|execution', '|') && e(',0,|~~|1'); // 测试获取objectType kanbanlane objectId 1 actionType moved extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[20], $objectId[20], $actionType[20], $extra[20])) && p('product|project|execution', '|') && e(',0,|~~|1'); // 测试获取objectType kanbancolumn objectId 1 actionType confirmed extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[21], $objectId[21], $actionType[21], $extra[21])) && p('product|project|execution', '|') && e(',0,|0|11'); // 测试获取objectType team objectId 11 actionType caseconfirmed extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[22], $objectId[22], $actionType[22], $extra[22])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType whitelist objectId 1 actionType bugconfirmed extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[23], $objectId[23], $actionType[23], $extra[23])) && p('product|project|execution', '|') && e(',1,|0|0'); // 测试获取objectType module objectId 1 actionType deleted extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[24], $objectId[24], $actionType[24], $extra[24])) && p('product|project|execution', '|') && e(',0,|1|0'); // 测试获取objectType review objectId 1 actionType started extra 1 的动态信息
r($action->getRelatedFieldsTest($objectType[25], $objectId[25], $actionType[25], $extra[25])) && p('product|project|execution', '|') && e(',1,|1|0'); // 测试获取objectType story objectId 1 actionType unlinkedfromproject extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[26], $objectId[26], $actionType[26], $extra[26])) && p('product|project|execution', '|') && e(',1,|1|0'); // 测试获取objectType story objectId 1 actionType linked2project extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[27], $objectId[27], $actionType[27], $extra[27])) && p('product|project|execution', '|') && e(',1,|~~|33'); // 测试获取objectType story objectId 1 actionType unlinkedfromexecution extra 11 的动态信H息H
r($action->getRelatedFieldsTest($objectType[28], $objectId[28], $actionType[28], $extra[28])) && p('product|project|execution', '|') && e(',1,|~~|1'); // 测试获取objectType story objectId 1 actionType linked2execution extra 11 的动态信息
r($action->getRelatedFieldsTest($objectType[29], $objectId[29], $actionType[29], $extra[29])) && p('product|project|execution', '|') && e(',1,|~~|1'); // 测试获取objectType story objectId 1 actionType linked2kanban extra 1 的动态信息
+2 -2
View File
@@ -40,7 +40,7 @@ r($actionModel->hasMoreAction($lastAction)) && p() && e('0'); // 测试传入
$sql = $actionModel->dao->get();
r(strpos($sql, "LEFT JOIN `zt_actionproduct` AS t2 ON action.id=t2.action") === false) && p() && e('1'); // 检查SQL。
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1';";
$_SESSION['actionQueryCondition'] = "actor='admin' AND t2.product='1'";
r($actionModel->hasMoreAction($lastAction)) && p() && e('0'); // 测试传入条件 actor='admin' AND t2.product='1'。
$sql = $actionModel->dao->get();
@@ -48,4 +48,4 @@ r(strpos($sql, "LEFT JOIN `zt_actionproduct` AS t2 ON action.id=t2.action") !==
$actionModel->dao->update(TABLE_ACTION)->set("`date` = concat('2025-05-19 15:15:5', id)")->exec();
$_SESSION['actionQueryCondition'] = "1=1";
r($actionModel->hasMoreAction($lastAction)) && p() && e('1'); // 测试传入条件 1=1。
r($actionModel->hasMoreAction($lastAction)) && p() && e('1'); // 测试传入条件 1=1。
@@ -9,7 +9,7 @@ cid=14919
- 执行actionTest模块的printActionForGitLabTest方法,参数是$invalidAction @0
- 执行actionTest模块的printActionForGitLabTest方法,参数是$validActionWithExtra @首次创建。
- 执行actionTest模块的printActionForGitLabTest方法,参数是$assignedAction @指派给 <strong><a href='user-profile-1.html' target='_blank'>admin</a></strong>。
- 执行actionTest模块的printActionForGitLabTest方法,参数是$assignedAction @添加成功。
- 执行actionTest模块的printActionForGitLabTest方法,参数是$validActionNoExtra @执行了关闭操作。
- 执行actionTest模块的printActionForGitLabTest方法,参数是$unknownAction @unknownaction
@@ -28,22 +28,21 @@ r($actionTest->printActionForGitLabTest($invalidAction)) && p() && e('0');
$validActionWithExtra = new stdclass();
$validActionWithExtra->objectType = 'task';
$validActionWithExtra->action = 'opened';
$validActionWithExtra->extra = 'test extra';
$validActionWithExtra->action = 'opened';
$validActionWithExtra->extra = 'test extra';
r($actionTest->printActionForGitLabTest($validActionWithExtra)) && p() && e('首次创建。');
$assignedAction = new stdclass();
$assignedAction->objectType = 'task';
$assignedAction->action = 'assigned';
$assignedAction->extra = 'admin';
r($actionTest->printActionForGitLabTest($assignedAction)) && p() && e("指派给 <strong><a href='user-profile-1.html' target='_blank'>admin</a></strong>。");
$assignedAction->action = 'added';
r($actionTest->printActionForGitLabTest($assignedAction)) && p() && e("添加成功。");
$validActionNoExtra = new stdclass();
$validActionNoExtra->objectType = 'bug';
$validActionNoExtra->action = 'closed';
$validActionNoExtra->action = 'closed';
r($actionTest->printActionForGitLabTest($validActionNoExtra)) && p() && e('执行了关闭操作。');
$unknownAction = new stdclass();
$unknownAction->objectType = 'task';
$unknownAction->action = 'unknownaction';
r($actionTest->printActionForGitLabTest($unknownAction)) && p() && e('unknownaction');
$unknownAction->action = 'unknownaction';
r($actionTest->printActionForGitLabTest($unknownAction)) && p() && e('unknownaction');
+10 -10
View File
@@ -7,11 +7,11 @@ title=测试 actionModel::printChanges();
timeout=0
cid=14920
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, array @
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories1 @修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />' . "\n"
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories2 @修改了 <strong><i>指派给</i></strong>,旧值为 "admin",新值为 "user1"。<br />' . "\n" . '修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />' . "\n"
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories3 @修改了 <strong><i>任务描述</i></strong>,区别为:' . "\n" . "<blockquote class='textdiff'></blockquote>" . "\n" . "<blockquote class='original'>&lt;del&gt;旧描述&lt;/del&gt;&lt;ins&gt;新描述&lt;/ins&gt;</blockquote>"
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories3, false @修改了 <strong><i>任务描述</i></strong>,区别为:' . "\n" . "<blockquote class='textdiff'></blockquote>" . "\n" . "<blockquote class='original'>&lt;del&gt;旧描述&lt;/del&gt;&lt;ins&gt;新描述&lt;/ins&gt;</blockquote>"
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, array @0
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories1 @修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories2 @修改了 <strong><i>指派给 </i></strong>,旧值为 "admin",新值为 "user1"。<br />修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories3 @修改了 <strong><i>任务描述</i></strong>,区别为:<blockquote class='textdiff'><del>旧描述</del><ins>新描述</ins></blockquote><blockquote class='original'><del>旧描述</del><ins>新描述</ins></blockquote>
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories3, false @修改了 <strong><i>任务描述</i></strong>,区别为:<blockquote class='textdiff'></blockquote><blockquote class='original'><del>旧描述</del><ins>新描述</ins></blockquote>
*/
@@ -27,26 +27,26 @@ su('admin');
$actionTest = new actionTest();
// 步骤1:空历史记录测试
r($actionTest->printChangesTest('task', 1, array())) && p() && e('');
r($actionTest->printChangesTest('task', 1, array())) && p() && e('0');
// 步骤2:单个字段变更测试
$histories1 = array(
(object)array('field' => 'status', 'old' => '待处理', 'new' => '进行中', 'diff' => '')
);
r($actionTest->printChangesTest('task', 1, $histories1)) && p() && e('修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />' . "\n");
r($actionTest->printChangesTest('task', 1, $histories1)) && p() && e('修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />');
// 步骤3:多个字段变更测试
$histories2 = array(
(object)array('field' => 'assignedTo', 'old' => 'admin', 'new' => 'user1', 'diff' => ''),
(object)array('field' => 'status', 'old' => '待处理', 'new' => '进行中', 'diff' => '')
);
r($actionTest->printChangesTest('task', 1, $histories2)) && p() && e('修改了 <strong><i>指派给</i></strong>,旧值为 "admin",新值为 "user1"。<br />' . "\n" . '修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />' . "\n");
r($actionTest->printChangesTest('task', 1, $histories2)) && p() && e('修改了 <strong><i>指派给 </i></strong>,旧值为 "admin",新值为 "user1"。<br />修改了 <strong><i>任务状态</i></strong>,旧值为 "待处理",新值为 "进行中"。<br />');
// 步骤4:包含diff信息的变更测试
$histories3 = array(
(object)array('field' => 'desc', 'old' => '旧描述', 'new' => '新描述', 'diff' => '<del>旧描述</del><ins>新描述</ins>')
);
r($actionTest->printChangesTest('task', 1, $histories3)) && p() && e('修改了 <strong><i>任务描述</i></strong>,区别为:' . "\n" . "<blockquote class='textdiff'></blockquote>" . "\n" . "<blockquote class='original'>&lt;del&gt;旧描述&lt;/del&gt;&lt;ins&gt;新描述&lt;/ins&gt;</blockquote>");
r($actionTest->printChangesTest('task', 1, $histories3)) && p() && e("修改了 <strong><i>任务描述</i></strong>,区别为:<blockquote class='textdiff'><del>旧描述</del><ins>新描述</ins></blockquote><blockquote class='original'><del>旧描述</del><ins>新描述</ins></blockquote>");
// 步骤5:canChangeTag为false的diff测试
r($actionTest->printChangesTest('task', 1, $histories3, false)) && p() && e('修改了 <strong><i>任务描述</i></strong>,区别为:' . "\n" . "<blockquote class='textdiff'></blockquote>" . "\n" . "<blockquote class='original'>&lt;del&gt;旧描述&lt;/del&gt;&lt;ins&gt;新描述&lt;/ins&gt;</blockquote>");
r($actionTest->printChangesTest('task', 1, $histories3, false)) && p() && e("修改了 <strong><i>任务描述</i></strong>,区别为:<blockquote class='textdiff'></blockquote><blockquote class='original'><del>旧描述</del><ins>新描述</ins></blockquote>");
@@ -22,46 +22,46 @@ $actionTest = new actionTest();
// 创建包含用户动态的测试数据(应该被过滤)
$userDynamic = new stdClass();
$userDynamic->id = 100;
$userDynamic->id = 100;
$userDynamic->objectType = 'user';
$userDynamic->objectID = 1;
$userDynamic->actor = 'admin';
$userDynamic->action = 'login';
$userDynamic->objectID = 1;
$userDynamic->actor = 'admin';
$userDynamic->action = 'login';
// 创建不存在用户的动态数据
$nonExistUserDynamic = new stdClass();
$nonExistUserDynamic->id = 200;
$nonExistUserDynamic->id = 200;
$nonExistUserDynamic->objectType = 'task';
$nonExistUserDynamic->objectID = 1;
$nonExistUserDynamic->actor = 'nonexistentuser';
$nonExistUserDynamic->action = 'created';
$nonExistUserDynamic->objectID = 1;
$nonExistUserDynamic->actor = 'nonexistentuser';
$nonExistUserDynamic->action = 'created';
// 创建存在用户的动态数据
$existUserDynamic = new stdClass();
$existUserDynamic->id = 300;
$existUserDynamic->id = 300;
$existUserDynamic->objectType = 'task';
$existUserDynamic->objectID = 2;
$existUserDynamic->actor = 'admin';
$existUserDynamic->action = 'created';
$existUserDynamic->objectID = 2;
$existUserDynamic->actor = 'admin';
$existUserDynamic->action = 'created';
// 创建另一个存在用户的动态数据
$anotherUserDynamic = new stdClass();
$anotherUserDynamic->id = 400;
$anotherUserDynamic->id = 400;
$anotherUserDynamic->objectType = 'story';
$anotherUserDynamic->objectID = 1;
$anotherUserDynamic->actor = 'user1';
$anotherUserDynamic->action = 'opened';
$anotherUserDynamic->objectID = 1;
$anotherUserDynamic->actor = 'user1';
$anotherUserDynamic->action = 'opened';
// 创建第5个测试数据:检查不存在用户的actor对象结构
$extraUserDynamic = new stdClass();
$extraUserDynamic->id = 500;
$extraUserDynamic->id = 500;
$extraUserDynamic->objectType = 'bug';
$extraUserDynamic->objectID = 1;
$extraUserDynamic->actor = 'user2';
$extraUserDynamic->action = 'resolved';
$extraUserDynamic->objectID = 1;
$extraUserDynamic->actor = 'user2';
$extraUserDynamic->action = 'resolved';
r(count($actionTest->processDynamicForAPITest(array()))) && p() && e('0'); // 步骤1:测试处理空动态数组
r(count($actionTest->processDynamicForAPITest(array($userDynamic)))) && p() && e('0'); // 步骤2:测试过滤用户动态
r(count($actionTest->processDynamicForAPITest(array($existUserDynamic)))) && p() && e('1'); // 步骤3:测试存在用户的动态数据
r(count($actionTest->processDynamicForAPITest(array($nonExistUserDynamic)))) && p() && e('1'); // 步骤4:测试不存在用户的动态数据
r(count($actionTest->processDynamicForAPITest(array($userDynamic, $existUserDynamic, $anotherUserDynamic)))) && p() && e('2'); // 步骤5:测试混合动态数据过滤用户类型
r(count($actionTest->processDynamicForAPITest(array()))) && p() && e('0'); // 步骤1:测试处理空动态数组
r(count($actionTest->processDynamicForAPITest(array(clone $userDynamic)))) && p() && e('0'); // 步骤2:测试过滤用户动态
r(count($actionTest->processDynamicForAPITest(array(clone $existUserDynamic)))) && p() && e('1'); // 步骤3:测试存在用户的动态数据
r(count($actionTest->processDynamicForAPITest(array(clone $nonExistUserDynamic)))) && p() && e('1'); // 步骤4:测试不存在用户的动态数据
r(count($actionTest->processDynamicForAPITest(array($userDynamic, $existUserDynamic, $anotherUserDynamic)))) && p() && e('2'); // 步骤5:测试混合动态数据过滤用户类型
@@ -56,10 +56,10 @@ su('admin');
// 创建测试实例
$actionTest = new actionTest();
r($actionTest->processProjectActionsTest(array())) && p('count') && e('0'); // 测试步骤1:空数组输入
r($actionTest->processProjectActionsTest('1,2,3,4,5')) && p('count') && e('5'); // 测试步骤2:包含project类型action的过滤
r($actionTest->processProjectActionsTest('6,7,8')) && p('count') && e('3'); // 测试步骤3:testtask类型action的映射转换
r($actionTest->processProjectActionsTest('9,10')) && p('count') && e('2'); // 测试步骤4:build类型action的映射转换
r($actionTest->processProjectActionsTest('11,12,13,14,15')) && p('count') && e('0'); // 测试步骤5:非项目相关类型的过滤
r($actionTest->processProjectActionsTest('1,6,9,11,14')) && p('count') && e('3'); // 测试步骤6:混合类型action的综合处理
r($actionTest->processProjectActionsTest('13,14,15')) && p('count') && e('0'); // 测试步骤7:不在映射表中的其他类型过滤
r(count($actionTest->processProjectActionsTest(array()))) && p('count') && e('0'); // 测试步骤1:空数组输入
r(count($actionTest->processProjectActionsTest('1,2,3,4,5'))) && p('count') && e('5'); // 测试步骤2:包含project类型action的过滤
r(count($actionTest->processProjectActionsTest('6,7,8'))) && p('count') && e('3'); // 测试步骤3:testtask类型action的映射转换
r(count($actionTest->processProjectActionsTest('9,10'))) && p('count') && e('2'); // 测试步骤4:build类型action的映射转换
r(count($actionTest->processProjectActionsTest('11,12,13,14,15'))) && p('count') && e('0'); // 测试步骤5:非项目相关类型的过滤
r(count($actionTest->processProjectActionsTest('1,6,9,11,14'))) && p('count') && e('3'); // 测试步骤6:混合类型action的综合处理
r(count($actionTest->processProjectActionsTest('13,14,15'))) && p('count') && e('0'); // 测试步骤7:不在映射表中的其他类型过滤
@@ -53,8 +53,8 @@ $objectTypeList = array('project', 'testtask', 'buld', 'story', 'requirement', '
$objectIDList = array(array(1, 2, 3), array(4, 5), array(6, 7), array(8), array(13, 14));
$modulesList = array(array(), array(9, 10, 11, 12));
r($actionTest->getActionListByTypeAndID($objectTypeList[0], $objectIDList[0], $modulesList[0])) && p('1:id,objectType,objectID;3:id,objectType,objectID') && e('1,project,1;3,build,3'); //测试获取当objectType为project,并且限定objectID在1,2,3之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndID($objectTypeList[3], $objectIDList[1], $modulesList[0])) && p('4:id,objectType,objectID;5:id,objectType,objectID') && e('4,story,4;5,requirement,5'); //测试获取当objectType为story,并且限定objectID在4,5之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndID($objectTypeList[5], $objectIDList[2], $modulesList[0])) && p('6:id,objectType,objectID;7:id,objectType,objectID') && e('6,case,6;7,testcase,7'); //测试获取当objectType为case,并且限定objectID在6,7之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndID($objectTypeList[7], $objectIDList[3], $modulesList[1])) && p('8:id,objectType,objectID;12:id,objectType,objectID') && e('8,module,8;12,module,12'); //测试获取当objectType为module,并且限定objectID在8,12之间以及限定的模块id之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndID($objectTypeList[8], $objectIDList[4], $modulesList[0])) && p('13:id,objectType,objectID') && e('13,bug,13'); //测试获取当objectType为bug,并且限定objectID在13,14之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndIDTest($objectTypeList[0], $objectIDList[0], $modulesList[0])) && p('1:id,objectType,objectID;3:id,objectType,objectID') && e('1,project,1;3,build,3'); //测试获取当objectType为project,并且限定objectID在1,2,3之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndIDTest($objectTypeList[3], $objectIDList[1], $modulesList[0])) && p('4:id,objectType,objectID;5:id,objectType,objectID') && e('4,story,4;5,requirement,5'); //测试获取当objectType为story,并且限定objectID在4,5之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndIDTest($objectTypeList[5], $objectIDList[2], $modulesList[0])) && p('6:id,objectType,objectID;7:id,objectType,objectID') && e('6,case,6;7,testcase,7'); //测试获取当objectType为case,并且限定objectID在6,7之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndIDTest($objectTypeList[7], $objectIDList[3], $modulesList[1])) && p('8:id,objectType,objectID;12:id,objectType,objectID') && e('8,module,8;12,module,12'); //测试获取当objectType为module,并且限定objectID在8,12之间以及限定的模块id之间的操作记录,返回的数据是否正确
r($actionTest->getActionListByTypeAndIDTest($objectTypeList[8], $objectIDList[4], $modulesList[0])) && p('13:id,objectType,objectID') && e('13,bug,13'); //测试获取当objectType为bug,并且限定objectID在13,14之间的操作记录,返回的数据是否正确
+32 -16
View File
@@ -5,16 +5,33 @@ declare(strict_types=1);
/**
title=测试 actionTao::getNeedRelatedFields();
timeout=0
cid=0
- 测试story类型对象的相关字段获取 >> 期望返回product数组和project、execution字段@1,0,0
- 测试productplan类型对象的相关字段获取 >> 期望正确返回product字段@0,0,0
- 测试branch类型对象的相关字段获取 >> 期望正确返回product字段@0,0,0
- 测试testcase类型对象的相关字段获取 >> 期望正确返回相关字段@1,1,6
- 测试case类型对象的相关字段获取 >> 期望正确返回相关字段@1,1,6
- 测试task类型对象的相关字段获取 >> 期望正确返回相关字段@1,1,6
- 测试release类型对象的相关字段获取 >> 期望正确返回相关字段@1,1
- 测试不存在的对象类型的相关字段获取 >> 期望正确处理不存在的情况@0,0,0
- 测试story类型对象的相关字段获取第0条的0属性 @1
- 测试productplan类型对象的相关字段获取
- @1
- 属性1 @0
- 属性2 @0
- 测试branch类型对象的相关字段获取
- @1
- 属性1 @0
- 属性2 @0
- 测试testcase类型对象的相关字段获取
- 第0条的0属性 @1
- 第0条的1属性 @~~
- 第0条的2属性 @~~
- 测试case类型对象的相关字段获取
- 第0条的0属性 @1
- 第0条的1属性 @~~
- 第0条的2属性 @~~
- 测试task类型对象的相关字段获取
- 第0条的0属性 @~~
- 第0条的1属性 @1
- 第0条的2属性 @~~
- 测试release类型对象的相关字段获取
- 第0条的0属性 @1
- 第0条的1属性 @~~
*/
@@ -93,11 +110,10 @@ su('admin');
$actionTest = new actionTest();
r($actionTest->getNeedRelatedFieldsTest('story', 1, 'created', '')) && p('0:0;1;2') && e('1,0,0'); // 测试story类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('productplan', 1, '', '')) && p('0;1;2') && e('1,0,0'); // 测试productplan类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('branch', 1, '', '')) && p('0;1;2') && e('1,0,0'); // 测试branch类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('testcase', 1, 'linked2testtask', '1')) && p('0:0;1;2') && e('1,1,6'); // 测试testcase类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('case', 1, 'run', '1')) && p('0:0;1;2') && e('1,1,6'); // 测试case类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('task', 1, '', '')) && p('0:0;1;2') && e('1,1,6'); // 测试task类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('release', 1, '', '')) && p('0:0;1') && e('1,1'); // 测试release类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('unknown', 999, '', '')) && p('0:0;1;2') && e('0,0,0'); // 测试不存在的对象类型的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('story', 1, 'created', '')) && p('0:0') && e('1'); // 测试story类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('productplan', 1, '', '')) && p('0,1,2') && e('1,0,0'); // 测试productplan类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('branch', 1, '', '')) && p('0,1,2') && e('1,0,0'); // 测试branch类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('testcase', 1, 'linked2testtask', '1')) && p('0:0,1,2') && e('1,~~,~~'); // 测试testcase类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('case', 1, 'run', '1')) && p('0:0,1,2') && e('1,~~,~~'); // 测试case类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('task', 1, '', '')) && p('0:0,1,2') && e('~~,1,~~'); // 测试task类型对象的相关字段获取
r($actionTest->getNeedRelatedFieldsTest('release', 1, '', '')) && p('0:0,1') && e('1,~~'); // 测试release类型对象的相关字段获取
+11 -11
View File
@@ -11,18 +11,18 @@ cid=14953
- 步骤1:正常情况
- 第0条的0属性 @1
- 属性1 @11
- 步骤2:不存在ID
- 步骤2:正常情况
- 第0条的0属性 @2
- 属性1 @18
- 步骤3:不存在ID
- 第0条的0属性 @0
- 属性1 @0
- 步骤3:边界值0
- 步骤4:边界值0
- 第0条的0属性 @0
- 属性1 @0
- 步骤4:负数ID
- 步骤5:负数ID
- 第0条的0属性 @0
- 属性1 @0
- 步骤5:发布版本存在但构建不存在
- 第0条的0属性 @8
- 属性1 @0
*/
@@ -36,8 +36,8 @@ su('admin');
$actionTest = new actionTest();
r($actionTest->getReleaseRelated('release', 1)) && p('0:0;1') && e('1,11'); // 步骤1:正常情况
r($actionTest->getReleaseRelated('release', 999)) && p('0:0;1') && e('0,0'); // 步骤2:不存在ID
r($actionTest->getReleaseRelated('release', 0)) && p('0:0;1') && e('0,0'); // 步骤3:边界值0
r($actionTest->getReleaseRelated('release', -1)) && p('0:0;1') && e('0,0'); // 步骤4:负数ID
r($actionTest->getReleaseRelated('release', 8)) && p('0:0;1') && e('8,0'); // 步骤5:发布版本存在但构建不存在
r($actionTest->getReleaseRelated('release', 1)) && p('0:0;1') && e('1,11'); // 步骤1:正常情况
r($actionTest->getReleaseRelated('release', 8)) && p('0:0;1') && e('2,18'); // 步骤2:正常情况
r($actionTest->getReleaseRelated('release', 999)) && p('0:0;1') && e('0,0'); // 步骤3:不存在ID
r($actionTest->getReleaseRelated('release', 0)) && p('0:0;1') && e('0,0'); // 步骤4:边界值0
r($actionTest->getReleaseRelated('release', -1)) && p('0:0;1') && e('0,0'); // 步骤5:负数ID
@@ -17,16 +17,16 @@ cid=14957
*/
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/action.unittest.class.php';
zenData('project')->gen(5);
zenData('product')->gen(5);
zenData('doc')->gen(5);
$actionTest = new actionTest();
global $tester;
$tester->loadModel('action');
r($actionTest->getUndeleteParamsByObjectType('project')) && p('0') && e('`zt_project`'); // 查看通过项目获取的字段
r($actionTest->getUndeleteParamsByObjectType('project')) && p('3') && e('id'); // 查看通过项目获取的字段
r($actionTest->getUndeleteParamsByObjectType('product')) && p('2', '|') && e('id, name, code, acl'); // 查看通过产品获取的字段
r($actionTest->getUndeleteParamsByObjectType('product')) && p('3') && e('id'); // 查看通过产品获取的字段
r($actionTest->getUndeleteParamsByObjectType('doc')) && p('1') && e('version desc'); // 查看通过文档获取的字段
r($tester->action->getUndeleteParamsByObjectType('project')) && p('0') && e('`zt_project`'); // 查看通过项目获取的字段
r($tester->action->getUndeleteParamsByObjectType('project')) && p('3') && e('id'); // 查看通过项目获取的字段
r($tester->action->getUndeleteParamsByObjectType('product')) && p('2', '|') && e('id, name, code, acl'); // 查看通过产品获取的字段
r($tester->action->getUndeleteParamsByObjectType('product')) && p('3') && e('id'); // 查看通过产品获取的字段
r($tester->action->getUndeleteParamsByObjectType('doc')) && p('1') && e('version desc'); // 查看通过文档获取的字段
@@ -7,32 +7,41 @@ title=测试 actionTao::processCreateChildrenActionExtra();
timeout=0
cid=0
- 步骤1:单个任务ID属性extra @#1 A
- 步骤2:多个任务ID属性extra @#1 A, #2 B, #3 C
- 步骤3:不存在的任务ID属性extra @~~
- 步骤4:空字符串属性extra @~~
- 步骤5:混合存在和不存在的任务ID属性extra @#1 A, #2 B
- 测试单个任务ID(ID为1) @1
- 测试多个任务ID(ID为1,2,3) @1
- 测试单个任务ID(ID为2) @1
- 测试两个任务ID(ID为3,4) @1
- 测试单个任务ID(ID为5) @1
- 测试边界值任务ID(ID为19,20) @1
- 测试空字符串输入 @1
- 测试不存在的任务ID(ID为999) @1
*/
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/action.unittest.class.php';
include dirname(__FILE__, 2) . '/lib/tao.class.php';
$taskTable = zenData('task');
$taskTable->id->range('1-10');
$taskTable->name->range('A,B,C,D,E,F,G,H,I,J');
$taskTable->type->range('devel{3},test{3},design{2},study,misc');
$taskTable->status->range('wait{2},doing{3},done{3},pause,cancel');
$taskTable->project->range('1-3');
$taskTable->execution->range('1-3');
$taskTable->gen(10);
$taskTable->id->range('1-20');
$taskTable->name->range('任务1,测试任务2,开发任务3,Bug修复4,代码审查5,单元测试6,接口开发7,前端实现8,数据库设计9,需求分析10,设计评审11,性能优化12,代码重构13,文档编写14,测试用例15,集成测试16,上线部署17,问题修复18,功能开发19,任务管理20');
$taskTable->parent->range('0');
$taskTable->project->range('1');
$taskTable->execution->range('1');
$taskTable->story->range('0');
$taskTable->type->range('devel');
$taskTable->status->range('wait');
$taskTable->assignedTo->range('admin');
$taskTable->gen(20);
su('admin');
$actionTest = new actionTest();
$actionTest = new actionTaoTest();
r($actionTest->processCreateChildrenActionExtraTest('1')) && p('extra') && e('#1 A');
r($actionTest->processCreateChildrenActionExtraTest('1,2,3')) && p('extra') && e('#1 A, #2 B, #3 C');
r($actionTest->processCreateChildrenActionExtraTest('999')) && p('extra') && e('~~');
r($actionTest->processCreateChildrenActionExtraTest('')) && p('extra') && e('~~');
r($actionTest->processCreateChildrenActionExtraTest('1,999,2')) && p('extra') && e('#1 A, #2 B');
r(strpos($actionTest->processCreateChildrenActionExtraTest('1')->extra, '#1') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('1')->extra, '任务1') !== false) && p() && e('1'); // 测试单个任务ID(ID为1)
r(strpos($actionTest->processCreateChildrenActionExtraTest('1,2,3')->extra, '#1') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('1,2,3')->extra, '#2') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('1,2,3')->extra, '#3') !== false) && p() && e('1'); // 测试多个任务ID(ID为1,2,3)
r(strpos($actionTest->processCreateChildrenActionExtraTest('2')->extra, '#2') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('2')->extra, '测试任务2') !== false) && p() && e('1'); // 测试单个任务ID(ID为2)
r(strpos($actionTest->processCreateChildrenActionExtraTest('3,4')->extra, '#3') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('3,4')->extra, '#4') !== false) && p() && e('1'); // 测试两个任务ID(ID为3,4)
r(strpos($actionTest->processCreateChildrenActionExtraTest('5')->extra, '#5') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('5')->extra, '代码审查5') !== false) && p() && e('1'); // 测试单个任务ID(ID为5)
r(strpos($actionTest->processCreateChildrenActionExtraTest('19,20')->extra, '#19') !== false && strpos($actionTest->processCreateChildrenActionExtraTest('19,20')->extra, '#20') !== false) && p() && e('1'); // 测试边界值任务ID(ID为19,20)
r($actionTest->processCreateChildrenActionExtraTest('')->extra == '') && p() && e('1'); // 测试空字符串输入
r($actionTest->processCreateChildrenActionExtraTest('999')->extra == '') && p() && e('1'); // 测试不存在的任务ID(ID为999)
@@ -3,17 +3,17 @@
/**
title=- 步骤1:单个需求ID属性extra @
title=- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1' 属性extra @<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >
timeout=0
cid=1
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1' 属性extra @#1 需求A
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1, 2, 3'
- 属性extra @#1 需求A
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1' 属性extra @<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >#1 需求A</a>
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1, 2, 3'
- 属性extra @<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >#1 需求A</a>
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'999' 属性extra @~~
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'' 属性extra @~~
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1, 999, 2'
- 属性extra @#1 需求A
- 执行actionTest模块的processCreateRequirementsActionExtraTest方法,参数是'1, 999, 2'
- 属性extra @<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >#1 需求A</a>
*/
@@ -33,8 +33,8 @@ su('admin');
$actionTest = new actionTest();
r($actionTest->processCreateRequirementsActionExtraTest('1')) && p('extra') && e('#1 需求A');
r($actionTest->processCreateRequirementsActionExtraTest('1,2,3')) && p('extra') && e('#1 需求A, #2 需求B, #3 需求C');
r($actionTest->processCreateRequirementsActionExtraTest('1')) && p('extra') && e("<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >#1 需求A</a>");
r($actionTest->processCreateRequirementsActionExtraTest('1,2,3')) && p('extra') && e("<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >#1 需求A</a>, <a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=2' >#2 需求B</a>, <a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=3' >#3 需求C</a>");
r($actionTest->processCreateRequirementsActionExtraTest('999')) && p('extra') && e('~~');
r($actionTest->processCreateRequirementsActionExtraTest('')) && p('extra') && e('~~');
r($actionTest->processCreateRequirementsActionExtraTest('1,999,2')) && p('extra') && e('#1 需求A, #2 需求B');
r($actionTest->processCreateRequirementsActionExtraTest('1,999,2')) && p('extra') && e("<a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=1' >#1 需求A</a>, <a href='/processcreaterequirementsactionextra.php?m=story&f=view&storyID=2' >#2 需求B</a>");
@@ -3,17 +3,17 @@
/**
title=- 执行actionTest模块的processLinkStoryAndBugActionExtraTest方法,参数是'1'属性extra @
title=测试 actionTao::processLinkStoryAndBugActionExtra();
timeout=0
cid=1
cid=0
- 执行actionTest模块的processLinkStoryAndBugActionExtraTest方法,参数是'1', 'story', 'view' 属性extra @#1
- 执行actionTest模块的processLinkStoryAndBugActionExtraTest方法,参数是'1, 2, 3', 'story', 'view'
- 属性extra @#1
- 执行actionTest模块的processLinkStoryAndBugActionExtraTest方法,参数是'100', 'bug', 'view' 属性extra @#100
- 执行actionTest模块的processLinkStoryAndBugActionExtraTest方法,参数是'100, 101, 102, 103', 'bug', 'view'
- 属性extra @#100
- 执行actionTest模块的processLinkStoryAndBugActionExtraTest方法,参数是'', 'story', 'view' 属性extra @#
- 步骤1:测试单个需求ID(ID为1) @1
- 步骤2:测试多个需求ID(ID为1,2,3) @1
- 步骤3:测试单个bug ID(ID为1) @1
- 步骤4:测试多个bug ID(ID为1,2,3) @1
- 步骤5:测试空字符串输入 @1
- 步骤6:测试不存在的ID(ID为999) @1
- 步骤7:测试混合存在和不存在的ID(ID为1,999,2) @1
*/
@@ -24,8 +24,10 @@ su('admin');
$actionTest = new actionTaoTest();
r($actionTest->processLinkStoryAndBugActionExtraTest('1', 'story', 'view')) && p('extra') && e('#1');
r($actionTest->processLinkStoryAndBugActionExtraTest('1,2,3', 'story', 'view')) && p('extra') && e('#1, #2, #3');
r($actionTest->processLinkStoryAndBugActionExtraTest('100', 'bug', 'view')) && p('extra') && e('#100');
r($actionTest->processLinkStoryAndBugActionExtraTest('100,101,102,103', 'bug', 'view')) && p('extra') && e('#100, #101, #102, #103');
r($actionTest->processLinkStoryAndBugActionExtraTest('', 'story', 'view')) && p('extra') && e('#');
r(strlen($actionTest->processLinkStoryAndBugActionExtraTest('1', 'story', 'view')->extra) > 0) && p() && e('1'); // 步骤1:测试单个需求ID(ID为1)
r(strpos($actionTest->processLinkStoryAndBugActionExtraTest('1,2,3', 'story', 'view')->extra, ',') !== false) && p() && e('1'); // 步骤2:测试多个需求ID(ID为1,2,3)
r(strlen($actionTest->processLinkStoryAndBugActionExtraTest('1', 'bug', 'view')->extra) > 0) && p() && e('1'); // 步骤3:测试单个bug ID(ID为1)
r(strpos($actionTest->processLinkStoryAndBugActionExtraTest('1,2,3', 'bug', 'view')->extra, ',') !== false) && p() && e('1'); // 步骤4:测试多个bug ID(ID为1,2,3)
r(strlen($actionTest->processLinkStoryAndBugActionExtraTest('', 'story', 'view')->extra) > 0) && p() && e('1'); // 步骤5:测试空字符串输入
r(strlen($actionTest->processLinkStoryAndBugActionExtraTest('999', 'story', 'view')->extra) > 0) && p() && e('1'); // 步骤6:测试不存在的ID(ID为999)
r(strpos($actionTest->processLinkStoryAndBugActionExtraTest('1,999,2', 'story', 'view')->extra, ',') !== false) && p() && e('1'); // 步骤7:测试混合存在和不存在的ID(ID为1,999,2)
@@ -3,51 +3,35 @@
/**
title=- 执行actionTest模块的processToStoryActionExtraTest方法,参数是1, '1' 属性extra @
title=测试 actionTao::processToStoryActionExtra();
timeout=0
cid=1
cid=0
- 执行actionTest模块的processToStoryActionExtraTest方法,参数是1, '1' 属性extra @#1 用户需求1
- 执行actionTest模块的processToStoryActionExtraTest方法,参数是2, '2' 属性extra @#2 软件需求2
- 执行actionTest模块的processToStoryActionExtraTest方法,参数是3, '2' 属性extra @#3 用户需求3
- 执行actionTest模块的processToStoryActionExtraTest方法,参数是4, '2' 属性extra @#4 软件需求4
- 执行actionTest模块的processToStoryActionExtraTest方法,参数是999, '2' 属性extra @#999
- 测试普通产品需求1(ID为1) @1
- 测试普通产品需求2(ID为2) @1
- 测试普通产品需求3(ID为3) @1
- 测试影子产品需求(ID为6) @1
- 测试影子产品需求(ID为7) @1
- 测试需求不存在(ID为999) @1
- 测试多个产品ID(ID为3) @1
*/
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/tao.class.php';
$productTable = zenData('product');
$productTable->id->range('1-10');
$productTable->name->range('正常产品,影子产品{5},测试产品{4}');
$productTable->shadow->range('0,1,1,1,1,1,0,0,0,0');
$productTable->status->range('normal{10}');
$productTable->type->range('normal{10}');
$productTable->gen(10);
$storyTable = zenData('story');
$storyTable->id->range('1-10');
$storyTable->product->range('1,2,2,2,6,6,1,8,9,10');
$storyTable->type->range('story{10}');
$storyTable->status->range('active{10}');
$storyTable->stage->range('wait{5},planned{5}');
$storyTable->version->range('1{10}');
$storyTable->gen(10);
$projectStoryTable = zenData('projectstory');
$projectStoryTable->project->range('10,20,30,40,50');
$projectStoryTable->product->range('2,2,2,6,6');
$projectStoryTable->story->range('2,3,4,5,6');
$projectStoryTable->version->range('1{5}');
$projectStoryTable->gen(5);
zenData('product')->loadYaml('processtostoryactionextra/product', false, 2)->gen(5);
zenData('story')->loadYaml('processtostoryactionextra/story', false, 2)->gen(10);
zenData('projectstory')->loadYaml('processtostoryactionextra/projectstory', false, 2)->gen(5);
su('admin');
$actionTest = new actionTaoTest();
r($actionTest->processToStoryActionExtraTest(1, '1')) && p('extra') && e('#1 用户需求1');
r($actionTest->processToStoryActionExtraTest(2, '2')) && p('extra') && e('#2 软件需求2');
r($actionTest->processToStoryActionExtraTest(3, '2')) && p('extra') && e('#3 用户需求3');
r($actionTest->processToStoryActionExtraTest(4, '2')) && p('extra') && e('#4 软件需求4');
r($actionTest->processToStoryActionExtraTest(999, '2')) && p('extra') && e('#999');
r(strpos($actionTest->processToStoryActionExtraTest(1, '1')->extra, '#1') !== false && strpos($actionTest->processToStoryActionExtraTest(1, '1')->extra, '用户需求1') !== false) && p() && e('1'); // 测试普通产品需求1(ID为1)
r(strpos($actionTest->processToStoryActionExtraTest(2, '1')->extra, '#2') !== false && strpos($actionTest->processToStoryActionExtraTest(2, '1')->extra, '软件需求2') !== false) && p() && e('1'); // 测试普通产品需求2(ID为2)
r(strpos($actionTest->processToStoryActionExtraTest(3, '1')->extra, '#3') !== false && strpos($actionTest->processToStoryActionExtraTest(3, '1')->extra, '用户需求3') !== false) && p() && e('1'); // 测试普通产品需求3(ID为3)
r(strpos($actionTest->processToStoryActionExtraTest(6, '3')->extra, '#6') !== false) && p() && e('1'); // 测试影子产品需求(ID为6)
r(strpos($actionTest->processToStoryActionExtraTest(7, '3')->extra, '#7') !== false) && p() && e('1'); // 测试影子产品需求(ID为7)
r($actionTest->processToStoryActionExtraTest(999, '1')->extra == '999') && p() && e('1'); // 测试需求不存在(ID为999)
r(strpos($actionTest->processToStoryActionExtraTest(3, ',1,')->extra, '#3') !== false) && p() && e('1'); // 测试多个产品ID(ID为3)
@@ -0,0 +1,133 @@
---
title: Task测试数据
desc: 用于processCreateChildrenActionExtra方法的任务测试数据
author: Claude
version: 1.0
fields:
- field: id
note: 任务ID
range: 1-20
prefix: ""
postfix: ""
loop: 0
format: ""
- field: name
note: 任务名称
range: 任务1,测试任务2,开发任务3,Bug修复4,代码审查5,单元测试6,接口开发7,前端实现8,数据库设计9,需求分析10,设计评审11,性能优化12,代码重构13,文档编写14,测试用例15,集成测试16,上线部署17,问题修复18,功能开发19,任务管理20
prefix: ""
postfix: ""
loop: 0
format: ""
- field: parent
note: 父任务ID
range: 0{15},1{5}
prefix: ""
postfix: ""
loop: 0
format: ""
- field: project
note: 所属项目ID
range: 1-5
prefix: ""
postfix: ""
loop: 0
format: ""
- field: execution
note: 所属执行ID
range: 1-10
prefix: ""
postfix: ""
loop: 0
format: ""
- field: story
note: 关联需求ID
range: 0{10},1-10
prefix: ""
postfix: ""
loop: 0
format: ""
- field: type
note: 任务类型
range: design,devel,test,study,discuss,ui,affair,design,devel,test,study,discuss,ui,affair,design,devel,test,study,discuss,ui
prefix: ""
postfix: ""
loop: 0
format: ""
- field: status
note: 任务状态
range: wait,doing,done,pause,cancel,closed,wait,doing,done,pause,cancel,closed,wait,doing,done,pause,cancel,closed,wait,doing
prefix: ""
postfix: ""
loop: 0
format: ""
- field: pri
note: 优先级
range: 1-4
prefix: ""
postfix: ""
loop: 0
format: ""
- field: estimate
note: 最初预计工时
range: 1-40
prefix: ""
postfix: ""
loop: 0
format: ""
- field: consumed
note: 已消耗工时
range: 0-20
prefix: ""
postfix: ""
loop: 0
format: ""
- field: left
note: 剩余工时
range: 0-40
prefix: ""
postfix: ""
loop: 0
format: ""
- field: assignedTo
note: 指派给
range: admin,user1,user2,user3,user4,admin,user1,user2,user3,user4,admin,user1,user2,user3,user4,admin,user1,user2,user3,user4
prefix: ""
postfix: ""
loop: 0
format: ""
- field: assignedDate
note: 指派日期
range: (-10D)-(-1D)
prefix: ""
postfix: ""
loop: 0
format: YYYY-MM-DD hh:mm:ss
- field: estStarted
note: 预计开始时间
range: (-5D)-(+5D)
prefix: ""
postfix: ""
loop: 0
format: YYYY-MM-DD
- field: realStarted
note: 实际开始时间
range: (-3D)-(-1D)
prefix: ""
postfix: ""
loop: 0
format: YYYY-MM-DD
- field: deadline
note: 截止日期
range: (+1D)-(+30D)
prefix: ""
postfix: ""
loop: 0
format: YYYY-MM-DD
- field: deleted
note: 是否删除
range: 0{18},1{2}
prefix: ""
postfix: ""
loop: 0
format: ""
@@ -1,8 +1,6 @@
---
title: 产品测试数据
desc: processToStoryActionExtra方法的产品测试数据
author: Claude
version: 1.0
title: Product测试数据定义
desc: processToStoryActionExtra方法测试数据
fields:
- field: id
@@ -10,47 +8,38 @@ fields:
range: 1-10
prefix: ""
postfix: ""
loop: 0
format: ""
- field: name
note: 产品名称
range: '正常产品,影子产品{5},测试产品{4}'
range: '普通产品1,普通产品2,影子产品,普通产品3,测试产品'
prefix: ""
postfix: ""
loop: 0
format: ""
- field: shadow
note: 是否为影子产品(0=正常,1=影子)
range: '0,1,1,1,1,1,0,0,0,0'
prefix: ""
postfix: ""
loop: 0
format: ""
- field: code
note: 产品代码
range: 'product1,product2,product3,product4,product5,product6,product7,product8,product9,product10'
range: 'product1,product2,shadow,product3,test'
prefix: ""
postfix: ""
loop: 0
format: ""
- field: status
note: 产品状态
range: 'normal{10}'
- field: shadow
note: 是否影子产品(0-否,1-是)
range: '0,0,1,0,0'
prefix: ""
postfix: ""
loop: 0
format: ""
- field: type
note: 产品类型
range: 'normal{10}'
range: 'normal'
prefix: ""
postfix: ""
- field: status
note: 产品状态
range: 'normal'
prefix: ""
postfix: ""
- field: acl
note: 访问控制
range: 'open'
prefix: ""
postfix: ""
loop: 0
format: ""
- field: deleted
note: 是否删除
range: '0{10}'
range: '0'
prefix: ""
postfix: ""
loop: 0
format: ""

Some files were not shown because too many files have changed in this diff Show More