Merge branch release/21.7.8 of zentao/zentaopms (#13248)
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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
@@ -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)开发(以下简称我)。我依法拥有该软件的所有版权。
|
||||
本着共享开放的角度,我以开放源代码的形式发布该软件。您可以在遵守该协议的前提下使用该软件。
|
||||
自您安装该软件开始,您和我之间的合同关系自动成立。除非您停止使用该软件或与我有签署额外合同,
|
||||
您须认真遵循该授权协议约定的每一条款。
|
||||
|
||||
@@ -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())));
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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¶m=: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
@@ -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
@@ -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
@@ -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
@@ -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', '', '', '', '');
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+12035
-11969
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 . '看板所包含的任务', '团队成员安排');
|
||||
|
||||
@@ -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),
|
||||
|
||||
+1029
-1008
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
{
|
||||
|
||||
@@ -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
@@ -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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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();}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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')) . ',' : '';
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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获取动态
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'。
|
||||
|
||||
@@ -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 的动态信息
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'><del>旧描述</del><ins>新描述</ins></blockquote>"
|
||||
- 执行actionTest模块的printChangesTest方法,参数是'task', 1, $histories3, false @修改了 <strong><i>任务描述</i></strong>,区别为:' . "\n" . "<blockquote class='textdiff'></blockquote>" . "\n" . "<blockquote class='original'><del>旧描述</del><ins>新描述</ins></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'><del>旧描述</del><ins>新描述</ins></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'><del>旧描述</del><ins>新描述</ins></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之间的操作记录,返回的数据是否正确
|
||||
|
||||
@@ -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,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
Reference in New Issue
Block a user