Merge branch 'master' of git.zcorp.cc:easycorp/zentaopms

This commit is contained in:
dingguodong
2023-05-06 14:26:08 +08:00
30 changed files with 2106 additions and 1072 deletions
+2 -2
View File
@@ -258,7 +258,7 @@ class classlist
* @access public
* @return string
*/
public function toStr()
public function toStr(): string
{
$names = array();
foreach($this->list as $name => $toggle)
@@ -268,7 +268,7 @@ class classlist
$name = trim($name);
if(!strlen($name)) continue;
$names[] = str_replace('.', '.\\', $name);
$names[] = $name;
}
return implode(' ', $names);
}
+1 -1
View File
@@ -232,7 +232,7 @@ class programplan extends control
* @access public
* @return void
*/
public function edit(string $planID = 0, string $projectID = 0)
public function edit(string $planID = '0', string $projectID = '0')
{
$planID = (int)$planID;
$projectID = (int)$projectID;
+27 -24
View File
@@ -1866,10 +1866,8 @@ class project extends control
*/
public function updateOrder()
{
$postData = form::data();
$rawdata = $postData->rawdata;
$idList = explode(',', trim($rawdata->projects, ','));
$orderBy = $rawdata->orderBy;
$idList = explode(',', trim($this->post->projects, ','));
$orderBy = $this->post->orderBy;
if(strpos($orderBy, 'order') === false) return false;
@@ -1877,57 +1875,62 @@ class project extends control
}
/**
* 获取项目白名单列表
* Get white list personnel.
*
* @param int $projectID
* @param string $module
* @param string $from project|program|programProject
* @param string $objectType
* @param string $orderby
* @param int $recTotal
* @param int $recPerPage
* @param int $pageID
* @param string $projectID
* @param string $from project|program|programProject
* @param string $recTotal
* @param string $recPerPage
* @param string $pageID
*
* @access public
* @return void
*/
public function whitelist($projectID = 0, $module = 'project', $from = 'project', $objectType = 'project', $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 20, $pageID = 1)
public function whitelist(string $projectID = '0', string $from = 'project',int $recTotal = '0', int $recPerPage = '20', int $pageID = '1')
{
$projectID = (int)$projectID;
$projectID = $this->project->setMenu($projectID);
$project = $this->project->getById($projectID);
$project = $this->project->getByID($projectID);
if(isset($project->acl) and $project->acl == 'open') $this->locate($this->createLink('project', 'index', "projectID=$projectID"));
echo $this->fetch('personnel', 'whitelist', "objectID=$projectID&module=$module&browseType=$objectType&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID&projectID=$projectID&from=$from");
echo $this->fetch('personnel', 'whitelist', "objectID=$projectID&module=project&browseType=project&orderBy=id_desc&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID&projectID=$projectID&from=$from");
}
/**
* 添加用户到项目白名单中
* Adding users to the white list.
*
* @param int $projectID
* @param int $deptID
* @param int $copyID
* @param int $programID
* @param int $from
* @param string $projectID
* @param int $deptID
* @param int $copyID
* @param int $programID
* @param string $from
*
* @access public
* @return void
*/
public function addWhitelist($projectID = 0, $deptID = 0, $copyID = 0, $programID = 0, $from = 'project')
public function addWhitelist(string $projectID = '0', int $deptID = 0, int $copyID = 0, int $programID = 0, string $from = 'project')
{
$projectID = (int)$projectID;
$projectID = $this->project->setMenu($projectID);
$project = $this->project->getById($projectID);
$project = $this->project->getByID($projectID);
if(isset($project->acl) and $project->acl == 'open') $this->locate($this->createLink('project', 'index', "projectID=$projectID"));
echo $this->fetch('personnel', 'addWhitelist', "objectID=$projectID&dept=$deptID&copyID=$copyID&objectType=project&module=project&programID=$programID&from=$from");
}
/*
* 移除项目白名单人员
* Removing users from the white list.
*
* @param int $id
* @param string $id
* @param string $confirm
*
* @access public
* @return void
*/
public function unbindWhitelist($id = 0, $confirm = 'no')
public function unbindWhitelist(string $id = '0', $confirm = 'no')
{
echo $this->fetch('personnel', 'unbindWhitelist', "id=$id&confirm=$confirm");
}
+33 -37
View File
@@ -221,11 +221,14 @@ class projectModel extends model
*/
public function getByID(int $projectID): object|false
{
/* Using demo data during tutorials. */
if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProject();
/* Get project info. */
$project = $this->projectTao->fetchProjectInfo($projectID);
if(!$project) return false;
/* Replace image url. */
$project = $this->loadModel('file')->replaceImgURL($project, 'desc');
return $project;
}
@@ -261,21 +264,23 @@ class projectModel extends model
*/
public function getList(string $status = 'undone', string $orderBy = 'order_desc', bool $involved = false, object|null $pager = null): array
{
/* Init vars. */
/* Get projec list by status. */
$projects = $this->projectTao->fetchProjectList($status, $orderBy, $involved, $pager);
if(empty($projects)) return array();
/* Get team members and estimates under the project. */
$projectIdList = array_keys($projects);
$teams = $this->projectTao->fetchMemberCountByIdList($projectIdList);
$teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList);
$estimates = $this->projectTao->fetchTaskEstimateByIdList($projectIdList);
/* Set project attribute. */
$this->app->loadClass('pager', true);
foreach($projects as $projectID => $project)
{
$orderBy = $project->model == 'waterfall' ? 'id_asc' : 'id_desc';
$pager = $project->model == 'waterfall' ? null : new pager(0, 1, 1);
$project->executions = $this->loadModel('execution')->getStatData($projectID, 'undone', 0, 0, false, '', $orderBy, $pager);
$project->teamCount = isset($teams[$projectID]) ? $teams[$projectID] : 0;
$project->teamCount = isset($teamCount[$projectID]) ? $teamCount[$projectID] : 0;
$project->estimate = isset($estimates[$projectID]) ? round($estimates[$projectID]->estimate, 2) : 0;
$project->parentName = $project->parent ? $this->projectTao->getParentProgram($project->path, $project->grade) : '';
}
@@ -293,52 +298,43 @@ class projectModel extends model
* @access public
* @return array
*/
public function getOverviewList($queryType = 'byStatus', $param = 'all', $orderBy = 'id_desc', $limit = 15, $excludedModel = '')
public function getOverviewList(string $queryType = 'byStatus', string|int $param = 'all', string $orderBy = 'id_desc', int $limit = 15, string $excludedModel = ''): array
{
/* */
$queryType = strtolower($queryType);
$projects = $this->dao->select('*')->from(TABLE_PROJECT)
->where('type')->eq('project')
->andWhere('vision')->eq($this->config->vision)
->andWhere('deleted')->eq(0)
->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi()
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
->beginIF($queryType == 'bystatus' and $param == 'undone')->andWhere('status')->notIN('done,closed')->fi()
->beginIF($queryType == 'bystatus' and $param != 'all' and $param != 'undone')->andWhere('status')->eq($param)->fi()
->beginIF($queryType == 'byid')->andWhere('id')->eq($param)->fi()
->orderBy($orderBy)
->beginIF($limit)->limit($limit)->fi()
->fetchAll('id');
/* Get project list by query. */
$projects = $this->projectTao->fetchProjectListByQuery($queryType, $param, $orderBy, $limit, $excludedModel);
if(empty($projects)) return array();
/* Get team members under the project. */
$projectIdList = array_keys($projects);
$teams = $this->projectTao->fetchMemberCountByIdList($projectIdList);
$teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList);
/* Get all consumed and all estimate under the project. */
$fields = 't2.project, ROUND(SUM(t1.consumed), 1) AS consumed, ROUND(SUM(t1.estimate), 1) AS estimate';
$hours = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, $fields);
$storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList);
$taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList);
/* Get bug, task and story summary under the project. */
$bugSummary = $this->projectTao->getTotalBugByProject($projectIdList);
$taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList);
$storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList);
/* Set project attribute. */
foreach($projects as $projectID => $project)
{
$project->consumed = isset($hours[$projectID]) ? round((float)$hours[$projectID]->consumed, 1) : 0;
$project->estimate = isset($hours[$projectID]) ? round((float)$hours[$projectID]->estimate, 1) : 0;
$project->teamCount = isset($teams[$projectID]) ? $teams[$projectID] : 0;
$project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0;
$project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0;
$project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0;
$project->allStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->allStories: 0;
$project->doneStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->doneStories: 0;
$project->leftStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->leftStories: 0;
$project->leftTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->leftTasks : 0;
$project->allTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->allTasks : 0;
$project->waitTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->waitTasks : 0;
$project->doingTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doingTasks : 0;
$project->rndDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doneTasks : 0;
$project->liteDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->litedoneTasks : 0;
$project->teamCount = zget($teamCount, $projectID, 0);
$project->consumed = isset($hours[$projectID]) ? round((float)$hours[$projectID]->consumed, 1) : 0;
$project->estimate = isset($hours[$projectID]) ? round((float)$hours[$projectID]->estimate, 1) : 0;
$project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0;
$project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0;
$project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0;
$project->allStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->allStories : 0;
$project->doneStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->doneStories : 0;
$project->leftStories = isset($storySummary[$projectID]) ? $storySummary[$projectID]->leftStories : 0;
$project->leftTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->leftTasks : 0;
$project->allTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->allTasks : 0;
$project->waitTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->waitTasks : 0;
$project->doingTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doingTasks : 0;
$project->rndDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->doneTasks : 0;
$project->liteDoneTasks = isset($taskSummary[$projectID]) ? $taskSummary[$projectID]->litedoneTasks : 0;
}
return $projects;
+30 -1
View File
@@ -187,7 +187,7 @@ class projectTao extends projectModel
}
/**
* Get project details, including all contents of the TABLE_PROJECT.
* Get project details, including all contents of the project.
* 获取项目的详情,包含project表的所有内容。
*
* @param int $projectID
@@ -555,6 +555,35 @@ class projectTao extends projectModel
->fetchAll('id');
}
/**
* 通过条件查询和数量限制查询项目列表。
* Get project list by query and with limit.
*
* @param string $queryType
* @param string|int $param
* @param string $orderBy
* @param int $limit
* @param string $excludedModel
* @access protected
* @return array
*/
protected function fetchProjectListByQuery(string $queryType, string|int $param, string $orderBy, int $limit, string $excludedModel): array
{
$queryType = strtolower($queryType);
return $this->dao->select('*')->from(TABLE_PROJECT)
->where('type')->eq('project')
->andWhere('vision')->eq($this->config->vision)
->andWhere('deleted')->eq(0)
->beginIF($excludedModel)->andWhere('model')->ne($excludedModel)->fi()
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
->beginIF($queryType == 'bystatus' and $param == 'undone')->andWhere('status')->notIN('done,closed')->fi()
->beginIF($queryType == 'bystatus' and $param != 'all' and $param != 'undone')->andWhere('status')->eq($param)->fi()
->beginIF($queryType == 'byid')->andWhere('id')->eq($param)->fi()
->orderBy($orderBy)
->beginIF($limit)->limit($limit)->fi()
->fetchAll('id');
}
/**
* 根据项目ID列表查询团队成员数量。
* Get project team member count by project id list.
+42 -14
View File
@@ -1,20 +1,48 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
function initData()
{
$project = zdTable('project');
$project->id->range('11-19');
$project->project->range('11-19');
$project->name->prefix("项目")->range('11-19');
$project->code->prefix("project")->range('11-19');
$project->model->range("scrum");
$project->auth->range("[]");
$project->path->range("[]");
$project->type->range("project");
$project->grade->range("1");
$project->days->range("1");
$project->status->range("wait,doing,suspended,closed");
$project->desc->range("[]");
$project->budget->range("100000,200000");
$project->budgetUnit->range("CNY");
$project->percent->range("0-0");
$project->openedDate->range("`2023-05-01 10:00:10`");
$project->gen(9);
zdTable('team')->gen(10);
$stakeholder = zdTable('stakeholder');
$stakeholder->id->range('1-9');
$stakeholder->objectID->range('11-19');
$stakeholder->objectType->range('program,project');
$stakeholder->user->range("admin");
$stakeholder->type->range("inside");
$stakeholder->from->range("[]");
$stakeholder->createdBy->range("admin");
$stakeholder->createdDate->range("`2023-05-01 10:00:10`");
$stakeholder->gen(9);
}
su('admin');
/**
title=测试 projectModel::getOverviewList;
cid=1
pid=1
获取未开始的项目数量 >> 15
获取状态不为done和closed的项目数量 >> 15
根据项目ID获取项目 >> 1
获取不存在的项目 >> 0
按照ID正序获取项目列表,查看排第一个的项目详情 >> 11,项目1
按照项目名称倒序获取项目列表,查看排第一个的项目详情 >> 100,项目90
*/
@@ -30,9 +58,9 @@ $byId2 = $tester->project->getOverviewList('byid', '10000');
$byOrder1 = $tester->project->getOverviewList('byStatus', 'all', 'id_asc');
$byOrder2 = $tester->project->getOverviewList('byStatus', 'all', 'name_desc');
r(count($byStatus1)) && p() && e('15'); // 获取未开始的项目数量
r(count($byStatus2)) && p() && e('15'); // 获取状态不为done和closed的项目数量
r(count($byId1)) && p() && e('1'); // 根据项目ID获取项目
r(count($byId2)) && p() && e('0'); // 获取不存在的项目
r(current($byOrder1)) && p('id,name') && e('11,项目1'); // 按照ID正序获取项目列表,查看排第一个的项目详情
r(current($byOrder2)) && p('id,name') && e('100,项目90'); // 按照项目名称倒序获取项目列表,查看排第一个的项目详情
r(count($byStatus1)) && p() && e('3'); // 获取未开始的项目数量
r(count($byStatus2)) && p() && e('7'); // 获取状态不为done和closed的项目数量
r(count($byId1)) && p() && e('1'); // 根据项目ID获取项目
r(count($byId2)) && p() && e('0'); // 获取不存在的项目
r(current($byOrder1)) && p('id,name') && e('11,项目11'); // 按照ID正序获取项目列表,查看排第一个的项目详情
r(current($byOrder2)) && p('id,name') && e('19,项目19'); // 按照项目名称倒序获取项目列表,查看排第一个的项目详情
+14
View File
@@ -238,4 +238,18 @@ class Project
$projects = $this->project->fetchProjectList($status, 'id_desc', $involved, null);
return $projects;
}
/**
* Test getList function.
*
* @param int $status
* @param bool $involved
* @access public
* @return array
*/
public function testFetchProjectListByQuery($queryType, $param, $orderBy = 'id_desc')
{
$projects = $this->project->fetchProjectListByQuery($queryType, $param, $orderBy, 15, '');
return $projects;
}
}
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
include dirname(__FILE__, 2) . "/project.class.php";
function initData()
{
$project = zdTable('project');
$project->id->range('11-19');
$project->project->range('11-19');
$project->name->prefix("项目")->range('11-19');
$project->code->prefix("project")->range('11-19');
$project->model->range("scrum");
$project->auth->range("[]");
$project->path->range("[]");
$project->type->range("project");
$project->grade->range("1");
$project->days->range("1");
$project->status->range("wait,doing,suspended,closed");
$project->desc->range("[]");
$project->budget->range("100000,200000");
$project->budgetUnit->range("CNY");
$project->percent->range("0-0");
$project->openedDate->range("`2023-05-01 10:00:10`");
$project->gen(9);
zdTable('team')->gen(10);
$stakeholder = zdTable('stakeholder');
$stakeholder->id->range('1-9');
$stakeholder->objectID->range('11-19');
$stakeholder->objectType->range('program,project');
$stakeholder->user->range("admin");
$stakeholder->type->range("inside");
$stakeholder->from->range("[]");
$stakeholder->createdBy->range("admin");
$stakeholder->createdDate->range("`2023-05-01 10:00:10`");
$stakeholder->gen(9);
}
su('admin');
/**
title=测试 projectModel::fetchProjectListByQuery();
cid=1
*/
$projectTester = new Project();
$byStatus1 = $projectTester->testFetchProjectListByQuery('byStatus', 'wait');
$byStatus2 = $projectTester->testFetchProjectListByQuery('byStatus', 'undone');
$byId1 = $projectTester->testFetchProjectListByQuery('byid', '11');
$byId2 = $projectTester->testFetchProjectListByQuery('byid', '10000');
$byOrder1 = $projectTester->testFetchProjectListByQuery('byStatus', 'all', 'id_asc');
$byOrder2 = $projectTester->testFetchProjectListByQuery('byStatus', 'all', 'name_desc');
r(count($byStatus1)) && p() && e('3'); // 获取未开始的项目数量
r(count($byStatus2)) && p() && e('7'); // 获取状态不为done和closed的项目数量
r(count($byId1)) && p() && e('1'); // 根据项目ID获取项目
r(count($byId2)) && p() && e('0'); // 获取不存在的项目
r(current($byOrder1)) && p('id,name') && e('11,项目11'); // 按照ID正序获取项目列表,查看排第一个的项目详情
r(current($byOrder2)) && p('id,name') && e('19,项目19'); // 按照项目名称倒序获取项目列表,查看排第一个的项目详情
+5 -4
View File
@@ -684,14 +684,15 @@ class projectZen extends project
* 记录多迭代及瀑布类项目移除的产品
* Record multiple and waterfall project unlinked products
*
* @param array $oldProducts
* @param array $newProductIDs
* @param array $idList
* @param object $project
* @param array $oldProducts
* @param array $newProductIDs
* @param array $idList
*
* @access protected
* @return void
*/
protected function dealMultipleProduct(array $oldProducts, array $newProductIDs, array $idList): void
protected function dealMultipleProduct(object $project, array $oldProducts, array $newProductIDs, array $idList): void
{
/* Update multiple project linked products. */
if(empty($project->multiple))
+18
View File
@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
helper::import(dirname(__FILE__) . 'config/form.php');
$config->task = new stdclass();
$config->task->batchCreate = 10;
@@ -227,3 +230,18 @@ $config->task->datatable->fieldList['actions']['title'] = 'actions';
$config->task->datatable->fieldList['actions']['fixed'] = 'right';
$config->task->datatable->fieldList['actions']['width'] = '180';
$config->task->datatable->fieldList['actions']['required'] = 'yes';
$config->task->create->template = new stdclass();
$config->task->create->template->module = 0;
$config->task->create->template->mode = '';
$config->task->create->template->assignedTo = '';
$config->task->create->template->name = '';
$config->task->create->template->story = 0;
$config->task->create->template->type = '';
$config->task->create->template->pri = 3;
$config->task->create->template->estimate = '';
$config->task->create->template->desc = '';
$config->task->create->template->estStarted = null;
$config->task->create->template->deadline = null;
$config->task->create->template->mailto = '';
$config->task->create->template->color = '';
+44
View File
@@ -2,6 +2,26 @@
$config->task->form = new stdclass();
global $app;
$config->task->form->create = array();
$config->task->form->create['execution'] = array('type' => 'int', 'required' => true);
$config->task->form->create['type'] = array('type' => 'string', 'required' => true, 'default' => '');
$config->task->form->create['module'] = array('type' => 'int', 'required' => false);
$config->task->form->create['story'] = array('type' => 'int', 'required' => false);
$config->task->form->create['mode'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->create['color'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->create['name'] = array('type' => 'string', 'required' => true, 'default' => '');
$config->task->form->create['pri'] = array('type' => 'int', 'required' => false, 'default' => 3);
$config->task->form->create['estimate'] = array('type' => 'float', 'required' => false, 'default' => 0);
$config->task->form->create['desc'] = array('type' => 'string', 'required' => false);
$config->task->form->create['estStarted'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->create['deadline'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->create['vision'] = array('type' => 'string', 'required' => false, 'default' => $config->vision);
$config->task->form->create['status'] = array('type' => 'string', 'required' => false, 'default' => 'wait');
$config->task->form->create['openedBy'] = array('type' => 'string', 'required' => false, 'default' => $app->user->account);
$config->task->form->create['openedDate'] = array('type' => 'string', 'required' => false, 'default' => helper::now());
$config->task->form->create['mailto'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->create['version'] = array('type' => 'int', 'required' => false, 'default' => 1);
$config->task->form->assign = array();
$config->task->form->assign['assignedTo'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->assign['left'] = array('type' => 'float', 'required' => true);
@@ -37,3 +57,27 @@ $config->task->form->edit['canceledDate'] = array('type' => 'string', 'required'
$config->task->form->edit['closedBy'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->edit['closedReason'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->edit['closedDate'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->task->form->edit['team'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->edit['teamSource'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->edit['teamEstimate'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->edit['teamConsumed'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->edit['teamLeft'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->edit['deleteFiles'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate = array();
$config->task->form->batchCreate['module'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['parent'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['story'] = array('type' => 'array', 'required' => true, 'default' => array());
$config->task->form->batchCreate['storyEstimate'] = array('type' => 'array', 'required' => true, 'default' => array());
$config->task->form->batchCreate['storyDesc'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['storyPri'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['name'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['color'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['type'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['assignedTo'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['estimated'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['estStarted'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['deadline'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['desc'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->task->form->batchCreate['pri'] = array('type' => 'array', 'required' => false, 'default' => array());
+97 -446
View File
@@ -27,31 +27,33 @@ class task extends control
}
/**
* 创建一个任务。
* Create a task.
*
* @param int $executionID
* @param int $storyID
* @param int $moduleID
* @param int $taskID
* @param int $todoID
* @param string $executionID
* @param string $storyID
* @param string $moduleID
* @param string $taskID
* @param string $todoID
* @param string $extra
* @param string $bugID
* @access public
* @return void
*/
public function create($executionID = 0, $storyID = 0, $moduleID = 0, $taskID = 0, $todoID = 0, $extra = '', $bugID = 0)
public function create(string $executionID = '0', string $storyID = '0', string $moduleID = '0', string $taskID = '0', string $todoID = '0', string $extra = '', string $bugID = '0')
{
if(empty($this->app->user->view->sprints) and !$executionID) $this->locate($this->createLink('execution', 'create'));
/* Analytic parameter. */
$extra = str_replace(array(',', ' '), array('&', ''), $extra);
parse_str($extra, $output);
if(!empty($executionID)) $execution = $this->execution->getById($executionID);
$executions = $this->execution->getPairs(0, 'all', isset($execution) ? (!common::canModify('execution', $execution) ? 'noclosed' : '') : 'noclosed');
$executionID = $this->execution->saveState($executionID, $executions);
/* If you do not have permission to access any execution, go to the create execution page. */
if(empty($this->app->user->view->sprints) and !$executionID) $this->locate($this->createLink('execution', 'create'));
/* Set menu and get execution information. */
$executionID = $this->taskZen->setMenu($executionID);
$execution = $this->execution->getById($executionID);
$this->execution->setMenu($executionID);
if($this->app->tab == 'project') $this->loadModel('project')->setMenu($this->session->project);
/* Check whether the execution has permission to create tasks. */
$this->execution->getLimitedExecution();
$limitedExecutions = !empty($_SESSION['limitedExecutions']) ? $_SESSION['limitedExecutions'] : '';
if(strpos(",{$limitedExecutions},", ",$executionID,") !== false)
@@ -60,318 +62,60 @@ class task extends control
return print(js::locate($this->createLink('execution', 'task', "executionID=$executionID")));
}
$task = new stdClass();
$task->module = $moduleID;
$task->mode = '';
$task->assignedTo = '';
$task->name = '';
$task->story = $storyID;
$task->type = '';
$task->pri = '3';
$task->estimate = '';
$task->desc = '';
$task->estStarted = '';
$task->deadline = '';
$task->mailto = '';
$task->color = '';
if($taskID > 0)
{
$task = $this->task->getByID($taskID);
$executionID = $task->execution;
/* Emptying consumed hours when copy task. */
if($task->mode == 'multi')
{
foreach($task->team as $teamMember) $teamMember->consumed = 0;
}
}
if($todoID > 0)
{
$todo = $this->loadModel('todo')->getById($todoID);
$task->name = $todo->name;
$task->pri = $todo->pri;
$task->desc = $todo->desc;
}
if($bugID > 0)
{
$bug = $this->loadModel('bug')->getById($bugID);
$task->name = $bug->title;
$task->pri = $bug->pri;
$task->pri = !empty($bug->pri) ? $bug->pri : '3';
$task->assignedTo = array($bug->assignedTo);
}
$taskLink = $this->createLink('execution', 'browse', "executionID=$executionID&tab=task");
$this->loadModel('kanban');
if($execution->type == 'kanban')
{
$regionPairs = $this->kanban->getRegionPairs($execution->id, 0, 'execution');
$regionID = !empty($output['regionID']) ? $output['regionID'] : key($regionPairs);
$lanePairs = $this->kanban->getLanePairsByRegion($regionID, 'task');
$laneID = isset($output['laneID']) ? $output['laneID'] : key($lanePairs);
$this->view->regionID = $regionID;
$this->view->laneID = $laneID;
$this->view->regionPairs = $regionPairs;
$this->view->lanePairs = $lanePairs;
}
/* Submit the data processing after creating the task form. */
if(!empty($_POST))
{
$response['result'] = 'success';
/* Prepare the data information before creating the task. */
$result = $this->prepareCreate($executionID, (float)$this->post->estimate, $this->post->estStarted, $this->post->deadline, (bool)$this->post->selectTestStory);
if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
setcookie('lastTaskModule', (int)$this->post->module, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false);
if($this->post->execution) $executionID = (int)$this->post->execution;
list($task, $testTasks, $existTaskID) = $result;
if($existTaskID) return $this->send(array('result' => 'success', 'message' => sprintf($this->lang->duplicate, $this->lang->task->common), 'locate' => $this->createLink('task', 'view', "taskID={$existTaskID}")));
/* Create task here. */
$tasksID = $this->task->create($executionID, $bugID);
if(dao::isError())
{
$response['result'] = 'fail';
$response['message'] = dao::getError();
return $this->send($response);
}
/* Create task. */
$taskIdList = $this->task->create($task, $this->post->assignedTo, (int)$this->post->multiple, $this->post->team, (bool)$this->post->selectTestStory);
if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
/* if the count of tasksID is 1 then check exists. */
if(count($tasksID) == 1)
{
$taskID = current($tasksID);
if($taskID['status'] == 'exists')
{
$response['locate'] = $this->createLink('task', 'view', "taskID={$taskID['id']}");
$response['message'] = sprintf($this->lang->duplicate, $this->lang->task->common);
return $this->send($response);
}
}
/* Update other data related to the task after it is created. */
$task->id = current($taskIdList);
$columnID = isset($output['columnID']) ? (int)$output['columnID'] : 0;
$this->task->afterCreate($task, $taskIdList, $bugID, $todoID, $testTasks);
$this->task->updateKanbanData($execution, $task, (int)$_POST['lane'], $columnID);
setcookie('lastTaskModule', (int)$_POST['module'], $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false);
/* Create actions. */
$this->loadModel('action');
foreach($tasksID as $taskID)
{
/* if status is exists then this task has exists not new create. */
if($taskID['status'] == 'exists') continue;
$taskID = $taskID['id'];
$this->action->create('task', $taskID, 'Opened', '');
}
/* Create task in kanban. */
$kanbanID = $execution->type == 'kanban' ? $executionID : $_POST['execution'];
$laneID = isset($output['laneID']) ? $output['laneID'] : 0;
if(!empty($_POST['lane'])) $laneID = $_POST['lane'];
$columnID = $this->kanban->getColumnIDByLaneID($laneID, 'wait');
if(empty($columnID)) $columnID = isset($output['columnID']) ? $output['columnID'] : 0;
if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($kanbanID, $laneID, $columnID, 'task', $taskID);
if(empty($laneID) or empty($columnID)) $this->kanban->updateLane($kanbanID, 'task');
/* To do status. */
if($todoID > 0)
{
$this->dao->update(TABLE_TODO)->set('status')->eq('done')->where('id')->eq($todoID)->exec();
$this->action->create('todo', $todoID, 'finished', '', "TASK:$taskID");
if(($this->config->edition == 'biz' || $this->config->edition == 'max') && $todo->type == 'feedback' && $todo->idvalue) $this->loadModel('feedback')->updateStatus('todo', $todo->idvalue, 'done');
}
$message = $this->executeHooks($taskID);
if($message) $this->lang->saveSuccess = $message;
$response['message'] = $this->lang->saveSuccess;
/* Return task id when call the API. */
if($this->viewType == 'json' or (defined('RUN_MODE') && RUN_MODE == 'api')) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $taskID));
/* If link from no head then reload. */
if(isonlybody())
{
if($this->app->tab == 'execution' or $this->config->vision == 'lite')
{
$execLaneType = $this->session->execLaneType ? $this->session->execLaneType : 'all';
$execGroupBy = $this->session->execGroupBy ? $this->session->execGroupBy : 'default';
if($execution->type == 'kanban')
{
$rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : '';
$kanbanData = $this->loadModel('kanban')->getRDKanban($executionID, $execLaneType, 'id_desc', 0, $execGroupBy, $rdSearchValue);
$kanbanData = json_encode($kanbanData);
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "parent.updateKanban($kanbanData, 0)"));
}
else
{
$taskSearchValue = $this->session->taskSearchValue ? $this->session->taskSearchValue : '';
$kanbanData = $this->kanban->getExecutionKanban($executionID, $execLaneType, $execGroupBy, $taskSearchValue);
$kanbanType = $execLaneType == 'all' ? 'task' : key($kanbanData);
$kanbanData = $kanbanData[$kanbanType];
$kanbanData = json_encode($kanbanData);
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "parent.updateKanban(\"task\", $kanbanData)"));
}
}
else
{
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
}
}
/* Locate the browser. */
if($this->app->getViewType() == 'xhtml')
{
$taskLink = $this->createLink('task', 'view', "taskID=$taskID", 'html');
$response['locate'] = $taskLink;
return $this->send($response);
}
if($this->post->after == 'continueAdding')
{
$storyParam = $this->post->story ? $this->post->story : '';
$response['message'] = $this->lang->task->successSaved . $this->lang->task->afterChoices['continueAdding'];
$response['locate'] = $this->createLink('task', 'create', "executionID=$executionID&storyID={$storyParam}&moduleID=$moduleID");
return $this->send($response);
}
elseif($this->post->after == 'toTaskList')
{
setcookie('moduleBrowseParam', 0, 0, $this->config->webRoot, '', $this->config->cookieSecure, false);
$taskLink = $this->createLink('execution', 'task', "executionID=$executionID&status=unclosed&param=0&orderBy=id_desc");
$response['locate'] = $taskLink;
return $this->send($response);
}
elseif($this->post->after == 'toStoryList')
{
$response['locate'] = $this->createLink('execution', 'story', "executionID=$executionID");
if($this->config->vision == 'lite')
{
$projectID = $this->dao->select('project')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('project');
$response['locate'] = $this->createLink('projectstory', 'story', "projectID=$projectID");
}
return $this->send($response);
}
else
{
$response['locate'] = $taskLink;
return $this->send($response);
}
/* Get the information returned after a task is created. */
$response = $this->taskZen->responseAfterCreate($task, $execution, $_POST['after']);
return $this->send($response);
}
$users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
$members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted');
$showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : '';
$moduleOptionMenu = $this->tree->getTaskOptionMenu($executionID, 0, 0, $showAllModule ? 'allModule' : '');
/* Fix bug #3381. When the story module is the root module. */
if($storyID)
{
$task->module = $this->dao->findByID($storyID)->from(TABLE_STORY)->fetch('module');
}
else
{
$task->module = $task->module ? $task->module : (int)$this->cookie->lastTaskModule;
if(!isset($moduleOptionMenu[$task->module])) $task->module = 0;
}
$stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', '', '', 'active');
/* Get block id of assinge to me. */
$blockID = 0;
if(isonlybody())
{
$blockID = $this->dao->select('id')->from(TABLE_BLOCK)
->where('block')->eq('assingtome')
->andWhere('module')->eq('my')
->andWhere('account')->eq($this->app->user->account)
->orderBy('order_desc')
->fetch('id');
}
$title = $execution->name . $this->lang->colon . $this->lang->task->create;
$position[] = html::a($taskLink, $execution->name);
$position[] = $this->lang->task->common;
$position[] = $this->lang->task->create;
$projectID = $execution ? $execution->project : 0;
/* Set Custom*/
foreach(explode(',', $this->config->task->customCreateFields) as $field) $customFields[$field] = $this->lang->task->$field;
if(!empty($projectID))
{
$executions = $this->execution->getByProject($projectID, 'all', 0, true);
$executionKey = 0;
$executionModifyList = $this->execution->getByIdList(array_keys($executions));
foreach($executionModifyList as $modifykey)
{
if(!common::canModify('execution', $modifykey)) $executionKey = $modifykey->id;
if($executionKey) unset($executions[$executionKey]);
}
}
$lifetimeList = array();
$attributeList = array();
$executionList = $this->execution->getByIdList(array_keys($executions));
foreach($executionList as $id => $object)
{
$lifetimeList[$id] = $object->lifetime;
$attributeList[$id] = $object->attribute;
}
$testStoryIdList = $this->loadModel('story')->getTestStories(array_keys($stories), $execution->id);
/* Stories that can be used to create test tasks. */
$testStories = array();
foreach($stories as $storyID => $storyTitle)
{
if(empty($storyID) or isset($testStoryIdList[$storyID])) continue;
$testStories[$storyID] = $storyTitle;
}
$this->view->customFields = $customFields;
$this->view->showFields = $this->config->task->custom->createFields;
$this->view->showAllModule = $showAllModule;
$this->view->title = $title;
$this->view->testStories = $testStories;
$this->view->position = $position;
$this->view->gobackLink = (isset($output['from']) and $output['from'] == 'global') ? $this->createLink('execution', 'task', "executionID=$executionID") : '';
$this->view->execution = $execution;
$this->view->executions = $executions;
$this->view->lifetimeList = $lifetimeList;
$this->view->attributeList = $attributeList;
$this->view->task = $task;
$this->view->users = $users;
$this->view->storyID = $storyID;
$this->view->stories = $stories;
$this->view->testStoryIdList = $testStoryIdList;
$this->view->members = $members;
$this->view->blockID = $blockID;
$this->view->moduleOptionMenu = $moduleOptionMenu;
$this->view->projectID = $projectID;
$this->view->productID = $this->loadModel('product')->getProductIDByProject($projectID);;
$this->view->features = $this->execution->getExecutionFeatures($execution);
$this->display();
/* Shows the variables needed to create the task page. */
$this->taskZen->showCreateVars($execution, $storyID, $moduleID, $taskID, $todoID, $bugID, $output);
}
/**
* Batch create task.
*
* @param int $executionID
* @param int $storyID
* @param int $iframe
* @param int $taskID
* @param string $extra
*
* @param string $executionID
* @param string $storyID
* @param string $moduleID
* @param string $taskID
* @param string $iframe
* @param string $extra
* @access public
* @return void
*/
public function batchCreate($executionID = 0, $storyID = 0, $moduleID = 0, $taskID = 0, $iframe = 0, $extra = '')
public function batchCreate(string $executionID, string $storyID = '', string $moduleID = '', string $taskID = '', string $extra = '')
{
$this->execution->getLimitedExecution();
$limitedExecutions = !empty($_SESSION['limitedExecutions']) ? $_SESSION['limitedExecutions'] : '';
if(strpos(",{$limitedExecutions},", ",$executionID,") !== false)
/* Init vars. */
$executionID = (int)$executionID;
$storyID = (int)$storyID;
$moduleID = (int)$moduleID;
$taskID = (int)$taskID;
$extra = str_replace(array(',', ' '), array('&', ''), $extra);
parse_str($extra, $output);
/* 判断不能访问的执行。 TODO: 提示语应改为执行。 */
if($this->checkLimitedExecution($executionID))
{
echo js::alert($this->lang->task->createDenied);
return print(js::locate($this->createLink('execution', 'task', "executionID=$executionID")));
@@ -379,147 +123,37 @@ class task extends control
$execution = $this->execution->getById($executionID);
if($this->app->tab == 'my')
if(!empty($_POST))
{
$taskLink = $this->createLink('my', 'work', 'mode=task');
}
elseif($this->app->tab == 'project' and $execution->multiple)
{
$taskLink = $this->createLink('project', 'execution', "browseType=all&projectID={$execution->project}");
}
else
{
$taskLink = $this->createLink('execution', 'browse', "executionID=$executionID");
/* Form data. */
$postData = form::data($this->config->task->form->batchCreate);
$mails = $this->task->batchCreate($executionID, $postData, $output);
if(dao::isError()) return print(js::error(dao::getError()));
/* Return task id list when call the API. */
$taskIDList = array();
foreach($mails as $mail) $taskIDList[] = $mail->taskID;
if($this->viewType == 'json' or (defined('RUN_MODE') and RUN_MODE == 'api')) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $taskIDList));
/* 生成链接。 TODO: 写配置项。 */
$redirectedLink = $this->taskZen->getRedirectedLink($execution);
if(!isonlybody()) return print(js::locate($redirectedLink, 'parent'));
/* If link from no head then reload. */
if($this->app->tab == 'execution' or $this->config->vision == 'lite')
{
$kanbanData = $this->taskZen->getKanbanData($execution);
if($execution->type == 'kanban') return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban($kanbanData, 0)"));
return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban(\"task\", $kanbanData)"));
}
return print(js::reload('parent.parent'));
}
/* Set menu. */
$this->execution->setMenu($execution->id);
if($this->app->tab == 'project') $this->loadModel('project')->setMenu($this->session->project);
$this->taskZen->setMenuByTab($executionID, $execution->project);
/* When common task are child tasks, query whether common task are consumed. */
$taskConsumed = 0;
if($taskID) $taskConsumed = $this->dao->select('consumed')->from(TABLE_TASK)->where('id')->eq($taskID)->andWhere('parent')->eq(0)->fetch('consumed');
/* When common task are child tasks, query whether common task are consumed. */
$taskConsumed = 0;
if($taskID) $taskConsumed = $this->dao->select('consumed')->from(TABLE_TASK)->where('id')->eq($taskID)->andWhere('parent')->eq(0)->fetch('consumed');
if(!empty($_POST))
{
$mails = $this->task->batchCreate($executionID, $extra);
if(dao::isError()) return print(js::error(dao::getError()));
$taskIDList = array();
foreach($mails as $mail) $taskIDList[] = $mail->taskID;
/* Return task id list when call the API. */
if($this->viewType == 'json' or (defined('RUN_MODE') && RUN_MODE == 'api')) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $taskIDList));
/* If link from no head then reload. */
if(isonlybody())
{
if($this->app->tab == 'execution' or $this->config->vision == 'lite')
{
$execLaneType = $this->session->execLaneType ? $this->session->execLaneType : 'all';
$execGroupBy = $this->session->execGroupBy ? $this->session->execGroupBy : 'default';
if($execution->type == 'kanban')
{
$rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : '';
$kanbanData = $this->loadModel('kanban')->getRDKanban($executionID, $execLaneType, 'id_desc', 0, $execGroupBy, $rdSearchValue);
$kanbanData = json_encode($kanbanData);
return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban($kanbanData, 0)"));
}
else
{
$taskSearchValue = $this->session->taskSearchValue ? $this->session->taskSearchValue : '';
$kanbanData = $this->loadModel('kanban')->getExecutionKanban($executionID, $execLaneType, $execGroupBy, $taskSearchValue);
$kanbanType = $execLaneType == 'all' ? 'task' : key($kanbanData);
$kanbanData = $kanbanData[$kanbanType];
$kanbanData = json_encode($kanbanData);
return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban(\"task\", $kanbanData)"));
}
}
else
{
return print(js::reload('parent.parent'));
}
}
return print(js::locate($taskLink, 'parent'));
}
$story = $this->story->getByID($storyID);
if($story)
{
$moduleID = $story->module;
$stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', $moduleID, 'short', 'active');
}
else
{
$stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', 0, 'short', 'active');
}
$members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted');
$showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : '';
$modules = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, 0, $showAllModule ? 'allModule' : '');
/* Set Custom*/
foreach(explode(',', $this->config->task->customBatchCreateFields) as $field)
{
if($execution->type == 'stage' and strpos('estStarted,deadline', $field) !== false) continue;
$customFields[$field] = $this->lang->task->$field;
}
$showFields = $this->config->task->custom->batchCreateFields;
if($execution->lifetime == 'ops' or $execution->attribute == 'request' or $execution->attribute == 'review')
{
unset($customFields['story']);
$showFields = str_replace(',story,', ',', ",$showFields,");
$showFields = trim($showFields, ',');
}
$this->view->customFields = $customFields;
$this->view->showFields = $showFields;
if($execution->type == 'kanban')
{
$extra = str_replace(array(',', ' '), array('&', ''), $extra);
parse_str($extra, $output);
$this->loadModel('kanban');
$regionPairs = $this->kanban->getRegionPairs($executionID, 0, 'execution');
$regionID = !empty($output['regionID']) ? $output['regionID'] : key($regionPairs);
$lanePairs = $this->kanban->getLanePairsByRegion($regionID, 'task');
$laneID = isset($output['laneID']) ? $output['laneID'] : key($lanePairs);
$this->view->regionID = $regionID;
$this->view->laneID = $laneID;
$this->view->regionPairs = $regionPairs;
$this->view->lanePairs = $lanePairs;
}
$title = $execution->name . $this->lang->colon . $this->lang->task->batchCreate;
$position[] = html::a($taskLink, $execution->name);
$position[] = $this->lang->task->common;
$position[] = $this->lang->task->batchCreate;
if($taskID) $this->view->parentTitle = $this->dao->select('name')->from(TABLE_TASK)->where('id')->eq($taskID)->fetch('name');
if($taskID) $this->view->parentPri = $this->dao->select('pri')->from(TABLE_TASK)->where('id')->eq($taskID)->fetch('pri');
$this->view->title = $title;
$this->view->position = $position;
$this->view->execution = $execution;
$this->view->stories = $stories;
$this->view->modules = $modules;
$this->view->parent = $taskID;
$this->view->storyID = $storyID;
$this->view->story = $story;
$this->view->storyTasks = $this->task->getStoryTaskCounts(array_keys($stories), $executionID);
$this->view->members = $members;
$this->view->moduleID = $moduleID;
$this->view->taskConsumed = $taskConsumed;
$this->display();
$this->taskZen->buildBatchCreateForm($execution, $storyID, $moduleID, $taskID, $output);
}
/**
@@ -1129,7 +763,7 @@ class task extends control
$this->view->task = $task;
$this->view->from = $from;
$this->view->orderBy = $orderBy;
$this->view->efforts = $this->task->getTaskEstimate($taskID, '', '', $orderBy);
$this->view->efforts = $this->task->getTaskEfforts($taskID, '', '', $orderBy);
$this->view->users = $this->loadModel('user')->getPairs('noclosed|noletter');
$this->view->taskEffortFold = $taskEffortFold;
@@ -2074,7 +1708,7 @@ class task extends control
/* Get related objects title or names. */
$relatedStories = $this->dao->select('id,title')->from(TABLE_STORY)->where('id')->in($relatedStoryIdList)->fetchPairs();
$relatedFiles = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('objectType')->eq('task')->andWhere('objectID')->in(@array_keys($tasks))->andWhere('extra')->ne('editor')->fetchGroup('objectID');
$relatedFiles = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('objectType')->eq('task')->andWhere('objectID')->in(array_keys($tasks))->andWhere('extra')->ne('editor')->fetchGroup('objectID');
$relatedModules = $this->loadModel('tree')->getAllModulePairs('task');
if($tasks)
@@ -2333,4 +1967,21 @@ class task extends control
$this->view->users = $this->loadModel('user')->getPairs();
$this->display('', 'editTeam');
}
/**
* 检查当前用户在该执行中是否是受限用户。
* Checks if the current user is a limited user in this execution.
*
* @param string $executionID
* @access public
* @return bool
*/
public function checkLimitedExecution(string $executionID): bool
{
$this->execution->getLimitedExecution();
$limitedExecutions = !empty($_SESSION['limitedExecutions']) ? $_SESSION['limitedExecutions'] : '';
if(strpos(",{$limitedExecutions},", ",$executionID,") !== false) return true;
return false;
}
}
+313 -483
View File
@@ -14,298 +14,142 @@
class taskModel extends model
{
/**
* 创建一个任务。
* Create a task.
*
* @param int $executionID
* @param int $bugID
* @param object $rawData
* @param array $data
* @param bool $selectTestStory
* @access public
* @return void
* @return bool|array
*/
public function create($executionID, $bugID)
public function create(object $task, array $assignedToList, int $multiple, array $team, bool $selectTestStory): bool|array
{
if((float)$this->post->estimate < 0)
$this->loadModel('action');
/* Remove required fields for creating tasks based on conditions. */
$this->taskTao->removeCreateRequiredFields($task, $selectTestStory);
/* Create task. */
$taskIdList = array();
$taskFiles = array();
foreach($assignedToList as $assignedTo)
{
dao::$errors[] = $this->lang->task->error->recordMinus;
return false;
}
/* If the type of task is affair and assignedTo is empty, skip it.*/
if($task->type == 'affair' and empty($assignedTo)) continue;
if(!empty($this->config->limitTaskDate))
{
$this->checkEstStartedAndDeadline($executionID, $this->post->estStarted, $this->post->deadline);
if(dao::isError()) return false;
}
/* Process the assigned person data for the task. */
$task->assignedTo = $multiple ? '' : $assignedTo;
if($task->assignedTo) $task->assignedDate = helper::now();
$executionID = (int)$executionID;
$estStarted = null;
$deadline = null;
$assignedTo = '';
$taskIdList = array();
$taskDatas = array();
$taskFiles = array();
$requiredFields = "," . $this->config->task->create->requiredFields . ",";
/* Create task. */
$taskID = $this->taskTao->doCreate($task);
if(!$taskID) return false;
if($this->post->selectTestStory)
{
foreach($this->post->testStory as $i => $storyID)
/* Set attachments for tasks. */
$taskFiles = $this->setTaskFiles($taskFiles, $taskID);
/* If the task is multi-task, manage the team of task and calculate the team hours. */
if($multiple and count(array_filter($team)) > 1)
{
if(empty($storyID)) continue;
$task->id = $taskID;
$teams = $this->manageTaskTeam($task->mode, $taskID, 'wait');
if($teams) $this->computeHours4Multiple($task);
unset($task->id);
}
/* Check required fields when create test task. */
foreach($this->post->testStory as $i => $storyID)
{
if(empty($storyID)) continue;
$estStarted = (!isset($this->post->testEstStarted[$i]) or (isset($this->post->estStartedDitto[$i]) and $this->post->estStartedDitto[$i] == 'on')) ? $estStarted : $this->post->testEstStarted[$i];
$deadline = (!isset($this->post->testDeadline[$i]) or (isset($this->post->deadlineDitto[$i]) and $this->post->deadlineDitto[$i] == 'on')) ? $deadline : $this->post->testDeadline[$i];
$assignedTo = (!isset($this->post->testAssignedTo[$i]) or $this->post->testAssignedTo[$i] == 'ditto') ? $assignedTo : $this->post->testAssignedTo[$i];
$taskIdList[] = $taskID;
if(!empty($this->config->limitTaskDate))
{
$this->checkEstStartedAndDeadline($executionID, $estStarted, $deadline);
if(dao::isError())
{
foreach(dao::getError() as $field => $error)
{
dao::$errors[] = $error;
return false;
}
}
}
if($estStarted > $deadline)
{
dao::$errors[] = "ID: $storyID {$this->lang->task->error->deadlineSmall}";
return false;
}
$task = new stdclass();
$task->pri = $this->post->testPri[$i];
$task->estStarted = $estStarted;
$task->deadline = $deadline;
$task->assignedTo = $assignedTo;
$task->estimate = $this->post->testEstimate[$i];
$task->left = $this->post->testEstimate[$i];
/* Check requiredFields */
$this->dao->insert(TABLE_TASK)->data($task)->batchCheck($requiredFields, 'notempty');
if(dao::isError())
{
foreach(dao::getError() as $field => $error)
{
dao::$errors[] = $error;
return false;
}
}
$taskDatas[$i] = $task;
}
$requiredFields = str_replace(",estimate,", ',', "$requiredFields");
$requiredFields = str_replace(",story,", ',', "$requiredFields");
$requiredFields = str_replace(",estStarted,", ',', "$requiredFields");
$requiredFields = str_replace(",deadline,", ',', "$requiredFields");
$requiredFields = str_replace(",module,", ',', "$requiredFields");
}
$this->loadModel('file');
$task = fixer::input('post')
->setDefault('execution', $executionID)
->setDefault('estimate,left,story', 0)
->setDefault('status', 'wait')
->setDefault('project', $this->getProjectID($executionID))
->setIF($this->post->estimate != false, 'left', $this->post->estimate)
->setIF($this->post->story != false, 'storyVersion', $this->loadModel('story')->getVersion($this->post->story))
->setIF(strpos($requiredFields, 'estStarted') !== false, 'estStarted', helper::isZeroDate($this->post->estStarted) ? '' : $this->post->estStarted)
->setIF(strpos($requiredFields, 'deadline') !== false, 'deadline', helper::isZeroDate($this->post->deadline) ? '' : $this->post->deadline)
->setIF(strpos($requiredFields, 'estimate') !== false, 'estimate', $this->post->estimate)
->setIF(strpos($requiredFields, 'left') !== false, 'left', $this->post->left)
->setIF(strpos($requiredFields, 'story') !== false, 'story', $this->post->story)
->setIF(is_numeric($this->post->estimate), 'estimate', (float)$this->post->estimate)
->setIF(is_numeric($this->post->consumed), 'consumed', (float)$this->post->consumed)
->setIF(is_numeric($this->post->left), 'left', (float)$this->post->left)
->setDefault('openedBy', $this->app->user->account)
->setDefault('openedDate', helper::now())
->setDefault('vision', $this->config->vision)
->cleanINT('execution,story,module')
->stripTags($this->config->task->editor->create['id'], $this->config->allowedTags)
->join('mailto', ',')
->remove('after,files,labels,assignedTo,uid,storyEstimate,storyDesc,storyPri,team,teamSource,teamEstimate,teamConsumed,teamLeft,teamMember,multiple,teams,contactListMenu,selectTestStory,testStory,testPri,testEstStarted,testDeadline,testAssignedTo,testEstimate,sync,otherLane,region,lane,estStartedDitto,deadlineDitto')
->removeIF(empty($this->post->estStarted), 'estStarted')
->removeIF(empty($this->post->deadline), 'deadline')
->add('version', 1)
->get();
if($task->type != 'test') $this->post->set('selectTestStory', 0);
foreach($this->post->assignedTo as $assignedTo)
{
/* When type is affair and has assigned then ignore none. */
if($task->type == 'affair' and count($this->post->assignedTo) > 1 and empty($assignedTo)) continue;
$task->assignedTo = $assignedTo;
if($assignedTo) $task->assignedDate = helper::now();
/* Check duplicate task. */
if($task->type != 'affair' and $task->name)
{
$result = $this->loadModel('common')->removeDuplicate('task', $task, "execution={$executionID} and story=" . (int)$task->story . (isset($task->feedback) ? " and feedback=" . (int)$task->feedback : ''));
if($result['stop'])
{
$taskIdList[$assignedTo] = array('status' => 'exists', 'id' => $result['duplicate']);
continue;
}
}
$task = $this->loadModel('file')->processImgURL($task, $this->config->task->editor->create['id'], $this->post->uid);
/* Fix Bug #1525 */
$execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($task->execution)->fetch();
if($execution->lifetime == 'ops' or $execution->attribute == 'request' or $execution->attribute == 'review')
{
$requiredFields = str_replace(",story,", ',', "$requiredFields");
$task->story = 0;
}
if(strpos($requiredFields, ',estimate,') !== false)
{
if(strlen(trim($task->estimate)) == 0) dao::$errors['estimate'] = sprintf($this->lang->error->notempty, $this->lang->task->estimate);
$requiredFields = str_replace(',estimate,', ',', $requiredFields);
}
$requiredFields = trim($requiredFields, ',');
/* Fix Bug #2466 */
if($this->post->multiple) $task->assignedTo = '';
if(!$this->post->multiple or count(array_filter($this->post->team)) < 1) $task->mode = '';
$this->dao->insert(TABLE_TASK)->data($task, $skip = 'gitlab,gitlabProject')
->autoCheck()
->batchCheck($requiredFields, 'notempty')
->checkIF($task->estimate != '', 'estimate', 'float')
->checkIF(!helper::isZeroDate($task->deadline), 'deadline', 'ge', $task->estStarted)
->checkFlow()
->exec();
if(dao::isError()) return false;
$taskID = $this->dao->lastInsertID();
if($bugID > 0)
{
$this->dao->update(TABLE_TASK)->set('fromBug')->eq($bugID)->where('id')->eq($taskID)->exec();
$this->dao->update(TABLE_BUG)->set('toTask')->eq($taskID)->where('id')->eq($bugID)->exec();
$this->loadModel('action')->create('bug', $bugID, 'converttotask', '', $taskID);
}
/* Mark design version.*/
if(isset($task->design) && !empty($task->design))
{
$design = $this->loadModel('design')->getByID($task->design);
$this->dao->update(TABLE_TASK)->set('designVersion')->eq($design->version)->where('id')->eq($taskID)->exec();
}
$taskSpec = new stdClass();
$taskSpec->task = $taskID;
$taskSpec->version = $task->version;
$taskSpec->name = $task->name;
if($task->estStarted) $taskSpec->estStarted = $task->estStarted;
if($task->deadline) $taskSpec->deadline = $task->deadline;
$this->dao->insert(TABLE_TASKSPEC)->data($taskSpec)->autoCheck()->exec();
if(dao::isError()) return false;
if($this->post->story) $this->loadModel('story')->setStage($this->post->story);
if($this->post->selectTestStory)
{
$testStoryIdList = array();
$this->loadModel('action');
if($this->post->testStory)
{
foreach($this->post->testStory as $storyID)
{
if($storyID) $testStoryIdList[$storyID] = $storyID;
}
$testStories = $this->dao->select('id,title,version,module')->from(TABLE_STORY)->where('id')->in($testStoryIdList)->fetchAll('id');
foreach($this->post->testStory as $i => $storyID)
{
if(!isset($testStories[$storyID])) continue;
$assignedTo = $taskDatas[$i]->assignedTo;
$testEstStarted = $taskDatas[$i]->estStarted;
$testDeadline = $taskDatas[$i]->deadline;
$task->parent = $taskID;
$task->story = $storyID;
$task->storyVersion = $testStories[$storyID]->version;
$task->name = $this->lang->task->lblTestStory . " #{$storyID} " . $testStories[$storyID]->title;
$task->pri = $this->post->testPri[$i];
$task->estStarted = $testEstStarted;
$task->deadline = $testDeadline;
$task->assignedTo = $assignedTo;
$task->estimate = $this->post->testEstimate[$i];
$task->left = $this->post->testEstimate[$i];
$task->module = $testStories[$storyID]->module;
$this->dao->insert(TABLE_TASK)->data($task)->exec();
$childTaskID = $this->dao->lastInsertID();
$this->action->create('task', $childTaskID, 'Opened');
}
$this->computeWorkingHours($taskID);
$this->computeBeginAndEnd($taskID);
$this->dao->update(TABLE_TASK)->set('parent')->eq(-1)->where('id')->eq($taskID)->exec();
}
}
$this->file->updateObjectID($this->post->uid, $taskID, 'task');
if(!empty($taskFiles))
{
foreach($taskFiles as $taskFile)
{
$taskFile->objectID = $taskID;
$this->dao->insert(TABLE_FILE)->data($taskFile)->exec();
}
}
else
{
$taskFileTitle = $this->file->saveUpload('task', $taskID);
$taskFiles = $this->dao->select('*')->from(TABLE_FILE)->where('id')->in(array_keys($taskFileTitle))->fetchAll('id');
foreach($taskFiles as $fileID => $taskFile) unset($taskFiles[$fileID]->id);
}
if($this->post->multiple and count(array_filter($this->post->team)) > 1)
{
$teams = $this->manageTaskTeam($task->mode, $taskID, 'wait');
if($teams)
{
$task->id = $taskID;
$this->computeHours4Multiple($task);
}
}
if(!dao::isError()) $this->loadModel('score')->create('task', 'create', $taskID);
$taskIdList[$assignedTo] = array('status' => 'created', 'id' => $taskID);
$this->action->create('task', $taskID, 'Opened', '');
}
return $taskIdList;
}
/**
* Create a batch task.
* 计算多人任务工时。
* Compute hours for multiple task.
*
* @param object $oldTask
* @param object $task
* @param array $team
* @param bool $autoStatus
* @access public
* @return object|bool
*/
public function computeHours4Multiple(object $oldTask, object $task = null, array $team = array(), bool $autoStatus = true): object|bool
{
if(!$oldTask) return false;
/* If the team is empty, get the team from the task team table. */
if(empty($team)) $team = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($oldTask->id)->orderBy('order')->fetchAll();
/* If the team is not empty, compute the team hours. */
if(!empty($team))
{
/* Get members, old team and current task. */
$members = array_map(function($member){return $member->account;}, $team);
$oldTeam = zget($oldTask, 'team', array());
$currentTask = !empty($task) ? $task : new stdclass();
if(!isset($currentTask->status)) $currentTask->status = $oldTask->status;
$oldTask->team = $team;
/* If the assignedTo is not empty, the current task assignedTo is assignedTo. */
if(!empty($_POST['assignedTo']) and is_string($_POST['assignedTo']))
{
$currentTask->assignedTo = $this->post->assignedTo;
}
/* If assignedTo is empty, get the assignedTo for the multiply linear task. */
else
{
$currentTask->assignedTo = $this->getAssignedTo4Multi($members, $oldTask);
if($oldTask->assignedTo != $currentTask->assignedTo) $currentTask->assignedDate = helper::now();
$oldTask->team = $oldTeam;
}
/* Compute estimate and left. */
$currentTask->estimate = 0;
$currentTask->left = 0;
foreach($team as $member)
{
$currentTask->estimate += (float)$member->estimate;
$currentTask->left += (float)$member->left;
}
/* Get task efforts, and compute consumed. */
$efforts = $this->getTaskEfforts($oldTask->id);
$currentTask->consumed = 0;
foreach($efforts as $effort) $currentTask->consumed += (float)$effort->consumed;
/* If task is not empty, the task status is computed and the task is returned. */
if(!empty($task)) return $this->taskTao->computeCurrentTaskStatus($currentTask, $oldTask, $task, $autoStatus, empty($efforts), $members);
/* If task is empty, update the current task. */
$this->dao->update(TABLE_TASK)->data($currentTask)->autoCheck()->where('id')->eq($oldTask->id)->exec();
}
return true;
}
/**
* Batch create tasks.
*
* @param int $executionID
* @param string $extra
* @param object $postData
* @param array $output
* @access public
* @return void
*/
public function batchCreate($executionID, $extra = '')
public function batchCreate(int $executionID, object $postData, array $output)
{
$tasks = $postData->rawdata;
/* Load module and init vars. */
$this->loadModel('common');
$this->loadModel('score');
$this->loadModel('action');
$this->loadModel('kanban');
$now = helper::now();
$mails = array();
$storyIDs = array();
$taskNames = array();
$preStory = 0;
$tasks = fixer::input('post')->get();
$mails = array();
/* 运营管理界面移除泳道和泳道列。 */
if($this->config->vision == 'lite')
{
$lanes = $tasks->lane;
@@ -313,43 +157,16 @@ class taskModel extends model
unset($tasks->lane);
unset($tasks->column);
}
$extra = str_replace(array(',', ' '), array('&', ''), $extra);
parse_str($extra, $output);
/* Judge whether the current task is a parent. */
$parentID = !empty($this->post->parent[1]) ? $this->post->parent[1] : 0;
$parentID = !empty($tasks->parent[1]) ? $tasks->parent[1] : 0;
$oldParentTask = $this->dao->findById((int)$parentID)->from(TABLE_TASK)->fetch();
foreach($tasks->story as $key => $storyID)
{
if(empty($tasks->name[$key])) continue;
if($tasks->type[$key] == 'affair') continue;
if($tasks->type[$key] == 'ditto' && isset($tasks->type[$key - 1]) && $tasks->type[$key - 1] == 'affair') continue;
if($storyID == 'ditto') $storyID = $preStory;
$preStory = $storyID;
if(!isset($tasks->story[$key - 1]) and $key > 1 and !empty($tasks->name[$key - 1]))
{
$storyIDs[] = 0;
$taskNames[] = $tasks->name[$key - 1];
}
$inNames = in_array($tasks->name[$key], $taskNames);
if(!$inNames || ($inNames && !in_array($storyID, $storyIDs)))
{
$storyIDs[] = $storyID;
$taskNames[] = $tasks->name[$key];
}
else
{
dao::$errors['message'][] = sprintf($this->lang->duplicate, $this->lang->task->common) . ' ' . $tasks->name[$key];
return false;
}
}
$result = $this->loadModel('common')->removeDuplicate('task', $tasks, "execution=$executionID and story " . helper::dbIN($storyIDs));
$tasks = $result['data'];
/* 去除重复数据。 */
$tasks = $this->taskTao->removeDuplicate4BatchCreate($executionID, $tasks);
if(!$tasks) return false;
/* Init vars. */
$story = 0;
$module = 0;
$type = '';
@@ -359,10 +176,11 @@ class taskModel extends model
/* Get task data. */
$extendFields = $this->getFlowExtendFields();
$projectID = $this->getProjectID($executionID);
$execution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch();
$data = array();
foreach($tasks->name as $i => $name)
{
/* 给同上的变量赋值。 */
$story = (!isset($tasks->story[$i]) or $tasks->story[$i] == 'ditto') ? $story : $tasks->story[$i];
$module = (!isset($tasks->module[$i]) or $tasks->module[$i] == 'ditto') ? $module : $tasks->module[$i];
$type = (!isset($tasks->type[$i]) or $tasks->type[$i] == 'ditto') ? $type : $tasks->type[$i];
@@ -370,6 +188,7 @@ class taskModel extends model
$estStarted = (!isset($tasks->estStarted[$i]) or isset($tasks->estStartedDitto[$i])) ? $estStarted : $tasks->estStarted[$i];
$deadline = (!isset($tasks->deadline[$i]) or isset($tasks->deadlineDitto[$i])) ? $deadline : $tasks->deadline[$i];
/* 检查任务名称为空的数据。 */
if(empty($tasks->name[$i]))
{
if($this->common->checkValidRow('task', $tasks, $i))
@@ -380,87 +199,19 @@ class taskModel extends model
continue;
}
$data[$i] = new stdclass();
$data[$i]->story = (int)$story;
$data[$i]->type = $type;
$data[$i]->module = (int)$module;
$data[$i]->assignedTo = $assignedTo;
$data[$i]->color = $tasks->color[$i];
$data[$i]->name = $tasks->name[$i];
$data[$i]->desc = nl2br($tasks->desc[$i]);
$data[$i]->pri = $tasks->pri[$i];
$data[$i]->estimate = $tasks->estimate[$i] ? $tasks->estimate[$i] : 0;
$data[$i]->left = $tasks->estimate[$i] ? $tasks->estimate[$i] : 0;
$data[$i]->project = $projectID;
$data[$i]->execution = $executionID;
$data[$i]->estStarted = $estStarted;
$data[$i]->deadline = $deadline;
$data[$i]->status = 'wait';
$data[$i]->openedBy = $this->app->user->account;
$data[$i]->openedDate = $now;
$data[$i]->parent = $tasks->parent[$i];
$data[$i]->vision = isset($tasks->vision[$i]) ? $tasks->vision[$i] : 'rnd';
if($story) $data[$i]->storyVersion = $this->loadModel('story')->getVersion($data[$i]->story);
if($assignedTo) $data[$i]->assignedDate = $now;
if(strpos($this->config->task->create->requiredFields, 'estStarted') !== false and empty($estStarted)) $data[$i]->estStarted = '';
if(strpos($this->config->task->create->requiredFields, 'deadline') !== false and empty($deadline)) $data[$i]->deadline = '';
if(isset($tasks->lanes[$i])) $data[$i]->laneID = $tasks->lanes[$i];
foreach($extendFields as $extendField)
{
$data[$i]->{$extendField->field} = $this->post->{$extendField->field}[$i];
if(is_array($data[$i]->{$extendField->field})) $data[$i]->{$extendField->field} = join(',', $data[$i]->{$extendField->field});
$data[$i]->{$extendField->field} = htmlSpecialString($data[$i]->{$extendField->field});
}
/* 构建任务数据。 */
$dittoFields = array('story' => $story, 'module' => $module, 'type' => $type, 'assignedTo' => $assignedTo, 'estStarted' => $estStarted, 'deadline' => $deadline);
$data = $this->taskTao->constructData4BatchCreate($execution, $tasks, $i, $dittoFields, $extendFields);
}
/* Fix bug #1525*/
$execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch();
$requiredFields = ',' . $this->config->task->create->requiredFields . ',';
if($execution->lifetime == 'ops' or $execution->attribute == 'request' or $execution->attribute == 'review') $requiredFields = str_replace(',story,', ',', $requiredFields);
$requiredFields = trim($requiredFields, ',');
/* check data. */
foreach($data as $i => $task)
{
if(!empty($this->config->limitTaskDate))
{
$this->checkEstStartedAndDeadline($executionID, $task->estStarted, $task->deadline);
if(dao::isError()) return false;
}
if(!helper::isZeroDate($task->deadline) and $task->deadline < $task->estStarted)
{
dao::$errors['message'][] = $this->lang->task->error->deadlineSmall;
return false;
}
if($task->estimate and !preg_match("/^[0-9]+(.[0-9]{1,3})?$/", $task->estimate))
{
dao::$errors['message'][] = $this->lang->task->error->estimateNumber;
return false;
}
foreach(explode(',', $requiredFields) as $field)
{
$field = trim($field);
if(empty($field)) continue;
if(!isset($task->$field)) continue;
if(!empty($task->$field)) continue;
if($field == 'estimate' and strlen(trim($task->estimate)) != 0) continue;
dao::$errors['message'][] = sprintf($this->lang->error->notempty, $this->lang->task->$field);
return false;
}
if($task->estimate) $task->estimate = (float)$task->estimate;
}
$childTasks = null;
/* 检查必填项。 */
$data = $this->taskTao->checkRequired4BatchCreate($execution, $data);
if(!$data) return false;
$childTasks = '';
foreach($data as $i => $task)
{
/* 获取当前任务所属泳道并移除laneID属性。 */
$laneID = isset($output['laneID']) ? $output['laneID'] : 0;
if(isset($task->laneID))
{
@@ -468,32 +219,20 @@ class taskModel extends model
unset($task->laneID);
}
/* 将数据插入到任务表中。 */
$task->version = 1;
$this->dao->insert(TABLE_TASK)->data($task)
->autoCheck()
->checkIF($task->estimate != '', 'estimate', 'float')
->checkFlow()
->exec();
if(dao::isError()) return false;
$taskID = $this->dao->lastInsertID();
$taskSpec = new stdClass();
$taskSpec->task = $taskID;
$taskSpec->version = $task->version;
$taskSpec->name = $task->name;
if($task->estStarted) $taskSpec->estStarted = $task->estStarted;
if($task->deadline) $taskSpec->deadline = $task->deadline;
$this->dao->insert(TABLE_TASKSPEC)->data($taskSpec)->autoCheck()->exec();
if(dao::isError()) return false;
$taskID = $this->taskTao->doCreate($task);
if(!$taskID) return false;
/* 获取子任务ID。*/
$childTasks .= $taskID . ',';
/* 设置相关需求阶段。 */
if($task->story) $this->story->setStage($task->story);
$this->executeHooks($taskID);
/* 更新看板数据。 */
if($this->config->vision == 'lite')
{
$this->kanban->addKanbanCell($executionID, $lanes[$i], $columns[$i], 'task', $taskID);
@@ -506,51 +245,41 @@ class taskModel extends model
if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, $laneID, $columnID, 'task', $taskID);
}
/* 记录日志并计算积分。 */
$actionID = $this->action->create('task', $taskID, 'Opened', '');
if(!dao::isError()) $this->loadModel('score')->create('task', 'create', $taskID);
if(!dao::isError()) $this->score->create('task', 'create', $taskID);
/* 将taskID和actionID存入数组中。 */
$mails[$i] = new stdclass();
$mails[$i]->taskID = $taskID;
$mails[$i]->actionID = $actionID;
}
if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchCreate');
if(!dao::isError()) $this->score->create('ajax', 'batchCreate');
if($parentID > 0 && !empty($taskID))
/* 处理任务拆分的情况。 */
if($oldParentTask and !empty($taskID))
{
$oldParentTask = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq((int)$parentID)->fetch();
/* 当一个普通任务有消耗时,拆分子任务。 */
/* When common task are child tasks and the common task has consumption, create a child task. */
if($oldParentTask->parent == 0 and $oldParentTask->consumed > 0)
{
$clonedTask = clone $oldParentTask;
$clonedTask->parent = $parentID;
unset($clonedTask->id);
$this->dao->insert(TABLE_TASK)->data($clonedTask)->autoCheck()->exec();
$clonedTaskID = $this->dao->lastInsertID();
$this->dao->update(TABLE_EFFORT)->set('objectID')->eq($clonedTaskID)
->where('objectID')->eq($oldParentTask->id)
->andWhere('objectType')->eq('task')
->exec();
$splitResult = $this->taskTao->splitConsumedTask($oldParentTask);
if(!$splitResult) return false;
}
$this->updateParentStatus($taskID);
$this->computeBeginAndEnd($parentID);
$this->taskTao->updateParentAfterSplit($parentID);
$task = new stdclass();
$task->parent = '-1';
$task->lastEditedBy = $this->app->user->account;
$task->lastEditedDate = $now;
$this->dao->update(TABLE_TASK)->data($task)->where('id')->eq($parentID)->exec();
$newParentTask = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq((int)$parentID)->fetch();
/* 记录父任务日志。 */
$newParentTask = $this->dao->findById((int)$parentID)->from(TABLE_TASK)->fetch();
$changes = common::createChanges($oldParentTask, $newParentTask);
$actionID = $this->action->create('task', $parentID, 'createChildren', '', trim($childTasks, ','));
if(!empty($changes)) $this->action->logHistory($actionID, $changes);
}
/* 更新当前执行下的任务泳道。 */
if(!isset($output['laneID']) or !isset($output['columnID']) or !isset($lanes)) $this->kanban->updateLane($executionID, 'task');
return $mails;
}
@@ -811,59 +540,6 @@ class taskModel extends model
}
}
/**
* Compute hours for multiple task.
*
* @param object $oldTask
* @param object $task
* @param array $team
* @param bool $autoStatus
* @access public
* @return object|bool
*/
public function computeHours4Multiple($oldTask, $task = null, $team = array(), $autoStatus = true)
{
if(!$oldTask) return false;
if(empty($team)) $team = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($oldTask->id)->orderBy('order')->fetchAll();
if(!empty($team))
{
$now = helper::now();
$oldTeam = zget($oldTask, 'team', array());
$members = array_map(function($member){return $member->account;}, $team);
$currentTask = !empty($task) ? $task : new stdclass();
if(!isset($currentTask->status)) $currentTask->status = $oldTask->status;
$oldTask->team = $team;
if(!empty($_POST['assignedTo']) and is_string($_POST['assignedTo']))
{
$currentTask->assignedTo = $this->post->assignedTo;
}
else
{
$currentTask->assignedTo = $this->getAssignedTo4Multi($members, $oldTask);
if($oldTask->assignedTo != $currentTask->assignedTo) $currentTask->assignedDate = $now;
$oldTask->team = $oldTeam;
}
$currentTask->estimate = 0;
$currentTask->left = 0;
foreach($team as $member)
{
$currentTask->estimate += (float)$member->estimate;
$currentTask->left += (float)$member->left;
}
$efforts = $this->getTaskEstimate($oldTask->id);
$currentTask->consumed = 0;
foreach($efforts as $effort) $currentTask->consumed += (float)$effort->consumed;
if(!empty($task)) return $this->taskTao->computeCurrentTaskStatus($currentTask, $oldTask, $task, $autoStatus, empty($efforts), $members);
$this->dao->update(TABLE_TASK)->data($currentTask)->autoCheck()->where('id')->eq($oldTask->id)->exec();
}
}
/**
* Manage multi task team members.
*
@@ -882,9 +558,9 @@ class taskModel extends model
if($taskStatus == 'doing')
{
$efforts = $this->getTaskEstimate($taskID);
$efforts = $this->getTaskEfforts($taskID);
$doingUsers = array();
foreach($efforts as $i => $effort)
foreach($efforts as $effort)
{
if($effort->left != 0) $doingUsers[$effort->account] = $effort->account;
if($effort->left == 0) unset($doingUsers[$effort->account]);
@@ -1012,7 +688,7 @@ class taskModel extends model
}
elseif($effortID)
{
$efforts = $this->getTaskEstimate($taskID, '', $effortID);
$efforts = $this->getTaskEfforts($taskID, '', $effortID);
$prevTeam = null;
$thisTeam = null;
@@ -1138,7 +814,7 @@ class taskModel extends model
if($this->post->team and count(array_filter($this->post->team)) > 1)
{
$teams = $this->manageTaskTeam($oldTask->mode, $taskID, $task->status);
if(!empty($teams)) $task = $this->computeHours4Multiple($oldTask, $task, array(), $autoStatus = false);
if(!empty($teams)) $task = $this->computeHours4Multiple($oldTask, $task, array(), false);
}
if(empty($teams)) $task->mode = '';
@@ -1381,7 +1057,7 @@ class taskModel extends model
foreach($extendFields as $extendField)
{
$task->{$extendField->field} = $this->post->{$extendField->field}[$taskID];
if(is_array($task->{$extendField->field})) $task->{$extendField->field} = join(',', $task->{$extendField->field});
if(is_array($task->{$extendField->field})) $task->{$extendField->field} = implode(',', $task->{$extendField->field});
$task->{$extendField->field} = htmlSpecialString($task->{$extendField->field});
}
@@ -1769,7 +1445,7 @@ class taskModel extends model
{
$task->status = 'done';
$task->finishedBy = $this->app->user->account;
$task->finishedDate = $task->finishedDate;
$task->finishedDate = $now;
}
}
@@ -1825,7 +1501,7 @@ class taskModel extends model
foreach($record->dates as $id => $item) if($item > $today) dao::$errors[] = 'ID #' . $id . ' ' . $this->lang->task->error->date;
if(dao::isError()) return false;
$task = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq($taskID)->fetch();;
$task = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq($taskID)->fetch();
$task->team = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->eq($taskID)->orderBy('order')->fetchAll('id');
/* Check if the current user is in the team. */
@@ -1870,8 +1546,7 @@ class taskModel extends model
$this->loadModel('action');
$allChanges = array();;
$left = $task->left;
$allChanges = array();
$now = helper::now();
$oldStatus = $task->status;
$lastDate = $this->dao->select('*')->from(TABLE_EFFORT)->where('objectID')->eq($taskID)->andWhere('objectType')->eq('task')->orderBy('date_desc,id_desc')->limit(1)->fetch('date');
@@ -1902,7 +1577,7 @@ class taskModel extends model
$currentTeam = $this->getTeamByAccount($task->team, $this->app->user->account, $extra);
}
if($newTask->left == 0 and ((empty($currentTeam) and strpos('done,cancel,closed', $task->status) === false) or (!empty($currentTeam) and $currentTeam->status != 'done')))
if(!$newTask->left and ((empty($currentTeam) and in_array($task->status, array('done', 'cancel', 'closed'))) or (!empty($currentTeam) and $currentTeam->status != 'done')))
{
$newTask->status = 'done';
$newTask->assignedTo = $task->openedBy;
@@ -2008,7 +1683,6 @@ class taskModel extends model
$oldTask = $this->getById($taskID);
$now = helper::now();
$today = helper::today();
if($extra != 'DEVOPS' and strpos($this->config->task->finish->requiredFields, 'comment') !== false and !$this->post->comment)
{
@@ -2342,7 +2016,7 @@ class taskModel extends model
* @access public
* @return object|bool
*/
public function getById($taskID, $setImgSize = false)
public function getByID($taskID, $setImgSize = false)
{
$task = $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName')
->from(TABLE_TASK)->alias('t1')
@@ -2692,7 +2366,7 @@ class taskModel extends model
}
/**
* Get task estimate.
* Get task efforts.
*
* @param int $taskID
* @param string $account
@@ -2701,7 +2375,7 @@ class taskModel extends model
* @access public
* @return array
*/
public function getTaskEstimate($taskID, $account = '', $append = '', $orderBy = 'date,id')
public function getTaskEfforts($taskID, $account = '', $append = '', $orderBy = 'date,id')
{
return $this->dao->select('*')->from(TABLE_EFFORT)->where('objectID')->eq($taskID)
->andWhere('objectType')->eq('task')
@@ -2942,7 +2616,7 @@ class taskModel extends model
$left = $currentTeam->left;
if($task->mode == 'multi')
{
$accountEstimates = $this->getTaskEstimate($currentTeam->task, $estimate->account, $estimateID);
$accountEstimates = $this->getTaskEfforts($currentTeam->task, $estimate->account, $estimateID);
$lastEstimate = array_pop($accountEstimates);
if($lastEstimate->id == $estimateID)
{
@@ -4226,4 +3900,160 @@ class taskModel extends model
if(!empty($deadline) and !helper::isZeroDate($deadline) and $deadline > $execution->end) dao::$errors['deadline'][] = $pre . sprintf($this->lang->task->error->endGtExecution, $this->lang->execution->common, $execution->end);
if(!empty($deadline) and !helper::isZeroDate($deadline) and $deadline < $execution->begin) dao::$errors['deadline'][] = $pre . sprintf($this->lang->task->error->endLtExecution, $this->lang->execution->common, $execution->begin);
}
/**
* 创建任务后的其他数据处理。
* Other data processing after task creation.
*
* @param object $task
* @param array $taskIdList
* @param int $bugID
* @param object $rawData
* @access public
* @return bool
*/
public function afterCreate(object $task, array $taskIdList, int $bugID, int $todoID, array $testTasks): bool
{
$this->loadModel('file');
$this->loadModel('score');
foreach($taskIdList as $taskID)
{
/* If the task comes from a bug, update the task and bug information. */
if($bugID > 0)
{
$this->dao->update(TABLE_TASK)->set('fromBug')->eq($bugID)->where('id')->eq($taskID)->exec();
$this->dao->update(TABLE_BUG)->set('toTask')->eq($taskID)->where('id')->eq($bugID)->exec();
$this->loadModel('action')->create('bug', $bugID, 'converttotask', '', $taskID);
}
/* If the task comes from a todo, update the todo information. */
if($todoID > 0)
{
$this->dao->update(TABLE_TODO)->set('status')->eq('done')->where('id')->eq($todoID)->exec();
$this->loadModel('action')->create('todo', $todoID, 'finished', '', "TASK:$taskID");
/* If the todo comes from a feedback, update the feedback information. */
$todo = $this->dao->findByID($todoID)->from(TABLE_TODO)->fetch();
if($this->config->edition != 'open' and $todo->type == 'feedback' and $todo->objectID) $this->loadModel('feedback')->updateStatus('todo', $todo->idvalue, 'done');
}
/* If the task comes from a design, update the task information. */
if(!empty($task->design))
{
$design = $this->loadModel('design')->getByID($task->design);
$this->dao->update(TABLE_TASK)->set('designVersion')->eq($design->version)->where('id')->eq($taskID)->exec();
}
/* If the task comes from a story, update the stage of the story. */
if($task->story) $this->loadModel('story')->setStage($task->story);
/* If the current task has test subtasks, create test subtasks and update the task information. */
if(!empty($testTasks))
{
$this->taskTao->createTestChildTasks($taskID, $testTasks);
$this->computeWorkingHours($taskID);
$this->computeBeginAndEnd($taskID);
$this->dao->update(TABLE_TASK)->set('`parent`')->eq(-1)->where('id')->eq($taskID)->exec();
}
if(dao::isError()) return false;
/* Update file information and create score. */
$this->file->updateObjectID($this->post->uid, $taskID, 'task');
$this->score->create('task', 'create', $taskID);
}
return true;
}
/**
* 更新看板中的任务泳道数据。
* Update the task lane data in Kanban.
*
* @param object $execution
* @param object $rawData
* @param int $taskID
* @access public
* @return void
*/
public function updateKanbanData(object $execution, object $task, int $laneID, int $oldColumnID): void
{
$this->loadModel('kanban');
/* Get kanban id, lane id and column id. */
$kanbanID = $execution->type == 'kanban' ? $execution->id : $task->execution;
$laneID = empty($laneID) ? 0 : $laneID;
$columnID = $this->kanban->getColumnIDByLaneID($laneID, 'wait');
if(empty($columnID)) $columnID = $oldColumnID;
/* If both of lane id and column id are not empty, add task to the kanban cell. */
if($laneID and $columnID) $this->kanban->addKanbanCell($kanbanID, $laneID, $columnID, 'task', $task->id);
/* If lane id or column id is empty, update the task type lane of the kanban. */
if(!$laneID or !$columnID) $this->kanban->updateLane($kanbanID, 'task');
}
/**
* 设置任务的附件。
* Set attachments for tasks.
*
* @param array $taskFiles
* @param int $taskID
* @access public
* @return array
*/
public function setTaskFiles(array $taskFiles, int $taskID): array
{
/* If taskFiles is not empty, create task files. */
if(!empty($taskFiles))
{
foreach($taskFiles as $taskFile)
{
$taskFile->objectID = $taskID;
$this->dao->insert(TABLE_FILE)->data($taskFile)->exec();
}
}
/* If taskFiles is empty, get task files. */
else
{
$taskFileTitle = $this->loadModel('file')->saveUpload('task', $taskID);
$taskFiles = $this->dao->select('*')->from(TABLE_FILE)->where('id')->in(array_keys($taskFileTitle))->fetchAll('id');
foreach($taskFiles as $fileID => $taskFile) unset($taskFiles[$fileID]->id);
}
return $taskFiles;
}
/**
* 创建关联需求的测试类型的子任务。
* Create a subtask for the test type story with the story.
*
* @param int $taskID
* @param object[] $testTasks
* @access public
* @return void
*/
public function createTestChildTasks(int $taskID, array $testTasks): void
{
$this->loadModel('action');
/* Get the stories of the test tasks. */
$testStoryIdList = array_keys($testTasks);
$testStories = $this->dao->select('id,title,version,module')->from(TABLE_STORY)->where('id')->in($testStoryIdList)->fetchAll('id');
foreach($testStoryIdList as $i => $storyID)
{
/* If the story id is not exist, skip it. */
if(!isset($testStories[$storyID])) continue;
/* Construct a task and create it. */
$task = $testTasks[$storyID];
$task->parent = $taskID;
$task->storyVersion = $testStories[$storyID]->version;
$task->name = $this->lang->task->lblTestStory . " #{$storyID} " . $testStories[$storyID]->title;
$task->module = $testStories[$storyID]->module;
$this->dao->insert(TABLE_TASK)->data($task)->exec();
/* Get task id and create a action. */
$childTaskID = $this->dao->lastInsertID();
$this->action->create('task', $childTaskID, 'Opened');
}
}
}
+285 -9
View File
@@ -13,8 +13,45 @@ declare(strict_types=1);
class taskTao extends taskModel
{
/**
* Compute progress of a task.
* 创建一个任务。
* Create a task.
*
* @param object $task
* @access protected
* @return int|bool
*/
protected function doCreate(object $task): int|bool
{
/* Insert task data. */
$this->dao->insert(TABLE_TASK)->data($task)
->checkIF($task->estimate != '', 'estimate', 'float')
->autoCheck()
->batchCheck($this->config->task->create->requiredFields, 'notempty')
->checkFlow()
->exec();
if(dao::isError()) return false;
/* Get task id. */
$taskID = (int)$this->dao->lastInsertID();
/* Insert task desc data. */
$taskSpec = new stdClass();
$taskSpec->task = $taskID;
$taskSpec->version = $task->version;
$taskSpec->name = $task->name;
$taskSpec->estStarted = $task->estStarted ? $task->estStarted : null;
$taskSpec->deadline = $task->deadline ? $task->deadline : null;
$this->dao->insert(TABLE_TASKSPEC)->data($taskSpec)->autoCheck()->exec();
if(dao::isError()) return false;
return $taskID;
}
/**
* 获取任务的进度。
* Compute progress of a task.
*
* @param object $task
* @access protected
@@ -28,8 +65,8 @@ class taskTao extends taskModel
}
/**
* Compute progress of task list, include its' children.
* 计算任务列表中每个任务的进度,包括子任务。
* Compute progress of task list, include its' children.
*
* @param object[] $tasks
* @access protected
@@ -49,8 +86,8 @@ class taskTao extends taskModel
}
/**
* Fetch tasks under execution by executionID(Todo).
* 获取执行下的任务。
* Fetch tasks under execution by executionID(Todo).
*
* @param int $executionID
* @param int $productID
@@ -110,6 +147,7 @@ class taskTao extends taskModel
}
/**
* 通过任务类型查找用户的任务。
* Fetch user tasks by type.
*
* @param string $account
@@ -153,8 +191,8 @@ class taskTao extends taskModel
}
/**
* Get task team by id list.
* 通过任务ID列表查询任务团队信息。
* Get task team by id list.
*
* @param array $taskIdList
* @access protected
@@ -166,8 +204,8 @@ class taskTao extends taskModel
}
/**
* Get task list by report.
* 根据报表条件查询任务.
* Get task list by report.
*
* @param string $field
* @param string $condition
@@ -182,8 +220,8 @@ class taskTao extends taskModel
}
/**
* Get the assignedTo for the multiply linear task.
* 获取多人串行任务的指派人。
* Get the assignedTo for the multiply linear task.
*
* @param string|array $members
* @param object $task
@@ -216,8 +254,8 @@ class taskTao extends taskModel
}
/**
* Change the hierarchy of tasks to a parent-child structure.
* 将任务的层级改为父子结构。
* Change the hierarchy of tasks to a parent-child structure.
*
* @param object[] $tasks
* @param object[] $parentTasks
@@ -245,8 +283,8 @@ class taskTao extends taskModel
}
/**
* Compute the status of the current task.
* 计算当前任务的状态。
* Compute the status of the current task.
*
* @param object $currentTask
* @param object $oldTask
@@ -259,15 +297,18 @@ class taskTao extends taskModel
*/
protected function computeCurrentTaskStatus(object $currentTask, object $oldTask, object $task, bool $autoStatus, bool $hasEfforts, array $members): object
{
/* If the status is not automatic, return the current task. */
if(!$autoStatus) return $currentTask;
if($currentTask->consumed == 0 and $hasEfforts)
/* If consumed of the current task is empty and current task has no efforts, the current task status should be wait. */
if($currentTask->consumed == 0 and !$hasEfforts)
{
if(!isset($task->status)) $currentTask->status = 'wait';
$currentTask->finishedBy = null;
$currentTask->finishedDate = null;
}
/* If neither consumed nor left of the current task is empty, the current task status should be doing. */
if($currentTask->consumed > 0 && $currentTask->left > 0)
{
$currentTask->status = 'doing';
@@ -275,9 +316,11 @@ class taskTao extends taskModel
$currentTask->finishedDate = null;
}
/* If consumed of the current task is not empty and left of the current task is empty, the current task status should be done or doing. */
if($currentTask->consumed > 0 and $currentTask->left == 0)
{
$finisedUsers = $this->getFinishedUsers($oldTask->id, $members);
/* If the number of finisher is less than the number of team members , the current task status should be doing. */
if(count($finisedUsers) != count($members))
{
if(strpos('cancel,pause', $oldTask->status) === false or ($oldTask->status == 'closed' and $oldTask->reason == 'done'))
@@ -287,6 +330,7 @@ class taskTao extends taskModel
$currentTask->finishedDate = null;
}
}
/* If status of old task is wait or doing or pause, the current task status should be done. */
elseif(strpos('wait,doing,pause', $oldTask->status) !== false)
{
$currentTask->status = 'done';
@@ -332,4 +376,236 @@ class taskTao extends taskModel
->where('id')->eq($taskID)
->exec();
}
/**
* 根据条件移除创建任务的必填项。
* Remove required fields for creating tasks based on conditions.
*
* @param object $task
* @param bool $selectTestStory
* @access protected
* @return void
*/
protected function removeCreateRequiredFields(object $task, bool $selectTestStory): void
{
/* Get create required fields and the execution of the task. */
$requiredFields = "," . $this->config->task->create->requiredFields . ",";
$execution = $this->dao->findByID($task->execution)->from(TABLE_PROJECT)->fetch();
/* If the lifetime if the execution is ops and the attribute of execution is request or review, remove story from required fields. */
if($execution->lifetime == 'ops' or in_array($execution->attribute, array('request', 'review')))
{
$requiredFields = str_replace(",story,", ',', "$requiredFields");
}
/* If the type of the task is test and select story is true, remove some required fields. */
if($task->type == 'test' and $selectTestStory)
{
$requiredFields = str_replace(array(",estimate,", ",story,", ",estStarted,", ",deadline,", ",module,"), ',', "$requiredFields");
}
$this->config->task->create->requiredFields = trim($requiredFields, ',');
}
/**
* 在批量创建之前移除post数据中重复的数据。
* Remove the duplicate data before batch create tasks.
*
* @param int $executionID
* @param object $tasks
* @access protected
* @return object|false
*/
protected function removeDuplicate4BatchCreate(int $executionID, object $tasks): object|false
{
$storyIDs = array();
$taskNames = array();
foreach($tasks->story as $key => $storyID)
{
/* 过滤事务型和任务名称为空的数据。 */
if(empty($tasks->name[$key])) continue;
if($tasks->type[$key] == 'affair') continue;
if($tasks->type[$key] == 'ditto' and isset($tasks->type[$key - 1]) and $tasks->type[$key - 1] == 'affair') continue;
if($storyID == 'ditto') $storyID = $preStory;
$preStory = $storyID;
if(!isset($tasks->story[$key - 1]) and $key > 1 and !empty($tasks->name[$key - 1]))
{
$storyIDs[] = 0;
$taskNames[] = $tasks->name[$key - 1];
}
/* 判断Post传过来的任务有没有重复数据。 */
$hasExistsName = in_array($tasks->name[$key], $taskNames);
if($hasExistsName and in_array($storyID, $storyIDs))
{
dao::$errors['message'][] = sprintf($this->lang->duplicate, $this->lang->task->common) . ' ' . $tasks->name[$key];
return false;
}
$storyIDs[] = $storyID;
$taskNames[] = $tasks->name[$key];
}
/* 去重并赋值。 */
$result = $this->loadModel('common')->removeDuplicate('task', $tasks, "execution=$executionID and story " . helper::dbIN($storyIDs));
return $result['data'];
}
/**
* 批量创建任务之前构造数据。
* Construct data before batch create tasks.
*
* @param object $execution
* @param object $tasks
* @param int $index
* @param array $dittoFields
* @param array $extendFields
* @access protected
* @return array
*/
protected function constructData4BatchCreate(object $execution, object $tasks, int $index, array $dittoFields, array $extendFields): array
{
extract($dittoFields);
$now = helper::now();
$data[$index] = new stdclass();
$data[$index]->story = (int)$story;
$data[$index]->type = $type;
$data[$index]->module = (int)$module;
$data[$index]->assignedTo = $assignedTo;
$data[$index]->color = $tasks->color[$index];
$data[$index]->name = $tasks->name[$index];
$data[$index]->desc = nl2br($tasks->desc[$index]);
$data[$index]->pri = $tasks->pri[$index];
$data[$index]->estimate = $tasks->estimate[$index] ? $tasks->estimate[$index] : 0;
$data[$index]->left = $tasks->estimate[$index] ? $tasks->estimate[$index] : 0;
$data[$index]->project = $execution->project;
$data[$index]->execution = $execution->id;
$data[$index]->estStarted = $estStarted;
$data[$index]->deadline = $deadline;
$data[$index]->status = 'wait';
$data[$index]->openedBy = $this->app->user->account;
$data[$index]->openedDate = $now;
$data[$index]->parent = $tasks->parent[$index];
$data[$index]->vision = isset($tasks->vision[$index]) ? $tasks->vision[$index] : 'rnd';
if($story) $data[$index]->storyVersion = (int)$this->dao->findById($data[$index]->story)->from(TABLE_STORY)->fetch('version');
if($assignedTo) $data[$index]->assignedDate = $now;
if(strpos($this->config->task->create->requiredFields, 'estStarted') !== false and empty($estStarted)) $data[$index]->estStarted = '';
if(strpos($this->config->task->create->requiredFields, 'deadline') !== false and empty($deadline)) $data[$index]->deadline = '';
if(isset($tasks->lanes[$index])) $data[$index]->laneID = $tasks->lanes[$index];
/* 附加工作流字段。 */
foreach($extendFields as $extendField)
{
$data[$index]->{$extendField->field} = $tasks->{$extendField->field}[$index];
if(is_array($data[$index]->{$extendField->field})) $data[$index]->{$extendField->field} = join(',', $data[$index]->{$extendField->field});
$data[$index]->{$extendField->field} = htmlSpecialString($data[$index]->{$extendField->field});
}
return $data;
}
/**
* 批量创建前检查必填项
* Check required fields before batch create tasks.
*
* @param object $execution
* @param object[] $data
* @access protected
* @return object[]|false
*/
protected function checkRequired4BatchCreate(object $execution, array $data): array|false
{
/* 设置必填项。 */
$requiredFields = ',' . $this->config->task->create->requiredFields . ',';
if($execution->lifetime == 'ops' or $execution->attribute == 'request' or $execution->attribute == 'review') $requiredFields = str_replace(',story,', ',', $requiredFields);
$requiredFields = trim($requiredFields, ',');
/* check data. */
foreach($data as $i => $task)
{
/* 检查任务是否开启了起止日期必填的配置(limitTaskDate)。 */
if(!empty($this->config->limitTaskDate))
{
$this->checkEstStartedAndDeadline($execution->id, $task->estStarted, $task->deadline);
if(dao::isError()) return false;
}
/* 检查任务截止日期是否为空以及是否小于预计开始日期。 */
if(!helper::isZeroDate($task->deadline) and $task->deadline < $task->estStarted)
{
dao::$errors['message'][] = $this->lang->task->error->deadlineSmall;
return false;
}
/* 检查任务预计是否为数字类型。 */
if($task->estimate and !preg_match("/^[0-9]+(.[0-9]{1,3})?$/", $task->estimate))
{
dao::$errors['message'][] = $this->lang->task->error->estimateNumber;
return false;
}
/* 验证必填字段。 */
$requiredFields = array_filter(explode(',', $requiredFields));
foreach($requiredFields as $field)
{
if(empty($task->$field))
{
dao::$errors['message'][] = sprintf($this->lang->error->notempty, $this->lang->task->$field);
return false;
}
}
if($task->estimate) $task->estimate = (float)$task->estimate;
}
return $data;
}
/**
* 拆分已消耗的任务。
* Split the consumed task.
*
* @param object $parentTask
* @access protected
* @return bool
*/
protected function splitConsumedTask(object $parentTask): bool
{
$clonedTask = clone $parentTask;
$clonedTask->parent = $parentTask->id;
unset($clonedTask->id);
$this->dao->insert(TABLE_TASK)->data($clonedTask)->autoCheck()->exec();
if(dao::isError()) return false;
$clonedTaskID = $this->dao->lastInsertID();
$this->dao->update(TABLE_EFFORT)->set('objectID')->eq($clonedTaskID)
->where('objectID')->eq($parentTask->id)
->andWhere('objectType')->eq('task')
->exec();
if(dao::isError()) return false;
return true;
}
/**
* 拆分之后更新父任务的parent、lastEditedBy、lastEditedDate字段。
* Update the parent's parent, lastEditedBy, and lastEditedDate fields after split.
*
* @param int $parentID
* @access protected
* @return void
*/
protected function updateParentAfterSplit(int $parentID): void
{
$task = new stdclass();
$task->parent = '-1';
$task->lastEditedBy = $this->app->user->account;
$task->lastEditedDate = helper::now();
$this->dao->update(TABLE_TASK)->data($task)->where('id')->eq($parentID)->exec();
}
}
@@ -6,7 +6,7 @@ su('admin');
/**
title=taskModel->getTaskEstimate();
title=taskModel->getTaskEfforts();
cid=1
pid=1
@@ -19,4 +19,4 @@ $waitstart = array('assignedTo' => 'user92','consumed' => '10');
$task = new taskTest();
$task->startTest($taskID,$waitstart);
r($task->getTaskEstimateTest($taskID)) && p('0:task') && e('55'); // 查看任务预计
r($task->getTaskEffortsTest($taskID)) && p('0:task') && e('55'); // 查看任务预计
+3 -3
View File
@@ -652,7 +652,7 @@ class taskTest
}
/**
* Test get task estimate.
* Test get task efforts.
*
* @param int $taskID
* @param string $account
@@ -660,9 +660,9 @@ class taskTest
* @access public
* @return object
*/
public function getTaskEstimateTest($taskID, $account = '', $append = '')
public function getTaskEffortsTest($taskID, $account = '', $append = '')
{
$object = $this->objectModel->getTaskEstimate($taskID, $account, $append);
$object = $this->objectModel->getTaskEfforts($taskID, $account, $append);
if(dao::isError())
{
$error = dao::getError();
+744 -1
View File
@@ -11,6 +11,16 @@ declare(strict_types=1);
*/
class taskZen extends task
{
/**
* 执行数组。
* Execution's [id] => 'name' pairs.
*
* @var array
* @access private
*/
private $executionPairs = array();
/**
* 准备创建数据。
* Prepare edit data.
@@ -53,7 +63,6 @@ class taskZen extends task
->setIF($oldTask->parent < 0, 'left', $oldTask->left)
->setIF($oldTask->name != $postData->name || $oldTask->estStarted != $postData->estStarted || $oldTask->deadline != $postData->deadline, 'version', $oldTask->version + 1)
->add('lastEditedDate', $now)
->stripTags($this->config->task->editor->edit['id'], $this->config->allowedTags)
->join('mailto', ',')
@@ -255,4 +264,738 @@ class taskZen extends task
$this->view->users = $this->loadModel('user')->getPairs();
$this->display();
}
/**
* 设置任务二级导航。
* Set the task secondary navigation.
*
* @param int $executionID
* @access protected
* @return int
*/
protected function setMenu(int $executionID): int
{
$execution = $this->execution->getById($executionID);
/* If the admin denied modification of closed executions, only query not closed executions. */
$queryMode = $execution && common::canModify('execution', $execution) ? 'all' : 'noclosed';
/* Get executions the current user can access. */
$this->executionPairs = $this->execution->getPairs(0, 'all', $queryMode);
/* Call checkAccess method to judge the user can access the execution or not, if not return the first one he can access. */
$executionID = $this->execution->checkAccess($executionID, $this->executionPairs);
/* Set Menu. */
$this->execution->setMenu($executionID);
if($this->app->tab == 'project') $this->project->setMenu($this->session->project);
return $executionID;
}
/**
* 通过传入的对象ID设置任务信息。
* Set task information through the incoming object ID.
*
* @param int $storyID
* @param int $moduleID
* @param int $taskID
* @param int $todoID
* @param int $bugID
* @access private
* @return object
*/
private function setTaskInfoByObjectID(int $storyID, int $moduleID, int $taskID, int $todoID, int $bugID): object
{
$task = $this->config->task->create->template;
$task->module = $moduleID;
/* If exist task, copy task information by task id. */
if($taskID > 0)
{
$task = $this->task->getByID($taskID);
$executionID = $task->execution;
/* Emptying consumed hours when copy task. */
if($task->mode == 'multi')
{
foreach($task->team as $teamMember) $teamMember->consumed = 0;
}
}
/* If exist todo, copy todo information by todo id. */
if($todoID > 0)
{
$todo = $this->loadModel('todo')->getById($todoID);
$task->name = $todo->name;
$task->pri = $todo->pri;
$task->desc = $todo->desc;
}
/* If exist bug, copy bug information by bug id. */
if($bugID > 0)
{
$bug = $this->loadModel('bug')->getById($bugID);
$task->name = $bug->title;
$task->pri = !empty($bug->pri) ? $bug->pri : '3';
$task->assignedTo = array($bug->assignedTo);
}
/* If exist story, copy story module by story id. */
if($storyID)
{
$task->story = $storyID;
$task->module = $this->dao->findByID($storyID)->from(TABLE_STORY)->fetch('module');
}
elseif(!$moduleID)
{
$task->module = (int)$this->cookie->lastTaskModule;
}
return $task;
}
/**
* 展示看板相关变量。
* Show related variable about the Kanban.
*
* @param int $executionID
* @param array $output
* @access private
* @return void
*/
private function showKanbanRelatedVars(int $executionID, array $output): void
{
$this->loadModel('kanban');
$regionID = isset($output['regionID']) ? (int)$output['regionID'] : 0;
$laneID = isset($output['laneID']) ? (int)$output['laneID'] : 0;
$regionPairs = $this->kanban->getRegionPairs($executionID, 0, 'execution');
$regionID = $regionID ? $regionID : key($regionPairs);
$lanePairs = $this->kanban->getLanePairsByRegion($regionID, 'task');
$laneID = $laneID ? $laneID : key($lanePairs);
$this->view->regionID = $regionID;
$this->view->laneID = $laneID;
$this->view->regionPairs = $regionPairs;
$this->view->lanePairs = $lanePairs;
}
/**
* 展示地盘待处理区块的ID。
* Show the ID of the block to be processed on the my.
*
* @access private
* @return void
*/
private function showAssignedToMeBlockID(): void
{
/* Get block id of assinge to me. */
$blockID = 0;
if(isonlybody())
{
$blockID = $this->dao->select('id')->from(TABLE_BLOCK)
->where('block')->eq('assingtome')
->andWhere('module')->eq('my')
->andWhere('account')->eq($this->app->user->account)
->orderBy('order_desc')
->fetch('id');
}
$this->view->blockID = $blockID;
}
/**
* 展示执行相关数据。
* Show execution related data.
*
* @param object $execution
* @access private
* @return void
*/
private function showExecutionData(object $execution): void
{
$projectID = $execution ? $execution->project : 0;
$lifetimeList = array();
$attributeList = array();
if(!empty($projectID))
{
$executionKey = 0;
$executions = $this->execution->getByProject($projectID, 'all', 0, true);
$executionList = $this->execution->getByIdList(array_keys($executions));
foreach($executionList as $executionItem)
{
if(!common::canModify('execution', $executionItem)) $executionKey = $executionItem->id;
if($executionKey) unset($executions[$executionKey]);
if(!$executionKey) continue;
$lifetimeList[$executionKey] = $executionItem->lifetime;
$attributeList[$executionKey] = $executionItem->attribute;
}
}
else
{
$executions = $this->executionPairs;
$executionList = $this->execution->getByIdList(array_keys($executions));
foreach($executionList as $executionItem)
{
$executionKey = $executionItem->id;
$lifetimeList[$executionKey] = $executionItem->lifetime;
$attributeList[$executionKey] = $executionItem->attribute;
}
}
$this->view->projectID = $projectID;
$this->view->executions = $executions;
$this->view->lifetimeList = $lifetimeList;
$this->view->attributeList = $attributeList;
$this->view->productID = $this->loadModel('product')->getProductIDByProject($projectID);
$this->view->features = $this->execution->getExecutionFeatures($execution);
$this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
$this->view->members = $this->user->getTeamMemberPairs($execution->id, 'execution', 'nodeleted');
}
/**
* 处理创建任务的请求数据。
* Process the request data for the creation task.
*
* @param int $executionID
* @param object $formData
* @access private
* @return object
*/
private function prepareTask4Create(int $executionID, object $formData): object
{
$rawData = $formData->rawdata;
$execution = $this->dao->findById($rawData->execution)->from(TABLE_EXECUTION)->fetch();
$team = !empty($rawData->team) ? array_filter($rawData->team) : array();
$task = $formData->setDefault('execution', $executionID)
->setDefault('project', $this->task->getProjectID($executionID))
->setIF($rawData->estimate != false, 'left', $rawData->estimate)
->setIF(isset($rawData->story), 'storyVersion', isset($rawData->story) ? $this->loadModel('story')->getVersion($rawData->story) : 0)
->setIF(empty($rawData->multiple) || count($team) < 1, 'mode', '')
->setIF($execution and ($execution->lifetime == 'ops' or in_array($execution->attribute, array('request', 'review'))), 'story', 0)
->stripTags($this->config->task->editor->create['id'], $this->config->allowedTags)
->join('mailto', ',')
->get();
/* Processing image link. */
return $this->loadModel('file')->processImgURL($task, $this->config->task->editor->create['id'], $rawData->uid);
}
/**
* 检查传入的创建数据是否符合要求。
* Check if the incoming creation data meets the requirements.
*
* @param int $executionID
* @param float $estimate
* @param string $estStarted
* @param string $deadline
* @access private
* @return bool
*/
private function checkCreate(int $executionID, float $estimate, string $estStarted, string $deadline): bool
{
/* Check if the estimate is positive. */
if($estimate < 0)
{
dao::$errors['estimate'] = $this->lang->task->error->recordMinus;
return false;
}
/* If the task start and end date must be between the execution start and end date, check if the task start and end date accord with the conditions. */
if(!empty($this->config->limitTaskDate))
{
$this->task->checkEstStartedAndDeadline($executionID, $estStarted, $deadline);
if(dao::isError()) return false;
}
/* Check start and end date. */
if(!helper::isZeroDate($deadline) and $estStarted > $deadline)
{
dao::$errors['deadline'] = $this->lang->task->error->deadlineSmall;
return false;
}
return true;
}
/**
* 检查规定时间内是否创建了同名任务。
* Check whether a task with the same name is created within the specified time.
*
* @param object $task
* @access private
* @return int
*/
private function checkDuplicateName($task): int
{
/* Check duplicate task. */
if($task->type != 'affair' and $task->name)
{
$result = $this->loadModel('common')->removeDuplicate('task', $task, "execution={$task->execution} and story=" . (int)$task->story . (isset($task->feedback) ? " and feedback=" . (int)$task->feedback : ''));
if($result['stop']) return zget($result, 'duplicate', 0);
}
return 0;
}
/**
* 处理关联需求的测试子任务的请求数据。
* Process request data for test subtasks related to stories.
*
* @param int $executionID
* @param object $formData
* @access private
* @return array|bool
*/
private function prepareTestTasks4Create(int $executionID, object $formData): array|bool
{
/* Set data for the type of test task that has linked stories. */
$testTasks = array();
$rawData = $formData->rawdata;
foreach($rawData->testStory as $i => $storyID)
{
if(empty($storyID)) continue;
/* Process the ditto option as a concrete value. */
$estStarted = !isset($rawData->testEstStarted[$i]) || (isset($rawData->estStartedDitto[$i]) && $rawData->estStartedDitto[$i] == 'on') ? $estStarted : $rawData->testEstStarted[$i];
$deadline = !isset($rawData->testDeadline[$i]) || (isset($rawData->deadlineDitto[$i]) && $rawData->deadlineDitto[$i] == 'on') ? $deadline : $rawData->testDeadline[$i];
$assignedTo = !isset($rawData->testAssignedTo[$i]) || $rawData->testAssignedTo[$i] == 'ditto' ? $assignedTo : $rawData->testAssignedTo[$i];
/* Set task data. */
$task = new stdclass();
$task->execution = $executionID;
$task->story = $storyID;
$task->pri = $rawData->testPri[$i];
$task->estStarted = $estStarted;
$task->deadline = $deadline;
$task->assignedTo = $assignedTo;
$task->estimate = (float)$rawData->testEstimate[$i];
$task->left = (float)$rawData->testEstimate[$i];
$testTasks[$storyID] = $task;
}
return $testTasks;
}
/**
* 检查关联需求的测试类型任务数据格式是否符合要求。
* Check if the test type task data format of the linked stories meets the requirements.
*
* @param object[] $tasks
* @access private
* @return bool
*/
private function checkTestTasks(array $tasks): bool
{
foreach($tasks as $task)
{
/* Check if the estimate is positive. */
if($task->estimate < 0)
{
dao::$errors[] = "ID: {$task->story} {$this->lang->task->error->recordMinus}";
return false;
}
/* If the task start and end date must be between the execution start and end date, check if the task start and end date accord with the conditions. */
if(!empty($this->config->limitTaskDate))
{
$this->checkEstStartedAndDeadline($task->execuiton, $task->estStarted, $task->deadline);
if(dao::isError())
{
foreach(dao::getError() as $field => $error)
{
dao::$errors[] = "ID: {$task->story} {$error}";
return false;
}
}
}
/* Check start and end date. */
if($task->estStarted > $task->deadline)
{
dao::$errors[] = "ID: {$task->story} {$this->lang->task->error->deadlineSmall}";
return false;
}
/* Checking the required fields of task data. */
$this->dao->insert(TABLE_TASK)->data($task)->batchCheck($this->config->task->create->requiredFields, 'notempty');
if(dao::isError())
{
foreach(dao::getError() as $field => $error)
{
dao::$errors[] = "ID: {$task->story} {$error}";
return false;
}
}
}
return true;
}
/**
* 处理创建任务后的返回信息。
* The information returned after processing the creation task.
*
* @param int $taskID
* @param object $execution
* @param string $afterChoice continueAdding|toTaskList|toStoryList
* @access protected
* @return array|bool
*/
protected function responseAfterCreate(object $task, object $execution, string $afterChoice): array|bool
{
/* If there is a database error, return the error message. */
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
/* Return task id when call the API. */
$taskID = $task->id;
if($this->viewType == 'json' or (defined('RUN_MODE') && RUN_MODE == 'api')) return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $taskID);
$response['result'] = 'success';
$response['message'] = $this->lang->saveSuccess;
/* Send Webhook notifications. */
$message = $this->executeHooks($taskID);
if($message) $response['message'] = $message;
/* Processing the return information of pop-up windows. */
if(isonlybody())
{
/* If it is Kanban execution, refresh the Kanban statically through callback. */
if($this->app->tab == 'execution' or $this->config->vision == 'lite')
{
$kanbanData = $this->getKanbanData($execution);
$response['closeModal'] = true;
$response['callback'] = $execution->type == 'kanban' ? "parent.updateKanban($kanbanData, 0)" : "parent.updateKanban(\"task\", $kanbanData)";
return $response;
}
$response['locate'] = 'parent';
return $response;
}
/* Locate the browser. */
if($this->app->getViewType() == 'xhtml')
{
$response['locate'] = $this->createLink('task', 'view', "taskID=$taskID", 'html');
return $response;
}
/* Process the return information for selecting a jump after creation. */
return $this->getLocateAfterCreate($task, $execution->id, $afterChoice);
}
/**
* 处理创建后选择跳转的返回信息。
* Process the return information for selecting a jump after creation.
*
* @param object $task
* @param int $executionID
* @param string $afterChoice
* @access private
* @return array
*/
private function getLocateAfterCreate(object $task, int $executionID, string $afterChoice): array
{
/* Set the universal return value. */
$response['result'] = 'success';
$response['message'] = $this->lang->saveSuccess;
$response['locate'] = $this->createLink('execution', 'browse', "executionID=$executionID&tab=task");
/* Set the response to continue adding task to story. */
$executionID = $task->execution;
if($afterChoice == 'continueAdding')
{
$storyID = $task->story ? $task->story : 0;
$moduleID = $task->module ? $task->module : 0;
$response['message'] = $this->lang->task->successSaved . $this->lang->task->afterChoices['continueAdding'];
$response['locate'] = $this->createLink('task', 'create', "executionID=$executionID&storyID=$storyID&moduleID=$moduleID");
}
/* Set the response to return task list. */
elseif($afterChoice == 'toTaskList')
{
setcookie('moduleBrowseParam', 0, 0, $this->config->webRoot, '', $this->config->cookieSecure, false);
$response['locate'] = $this->createLink('execution', 'task', "executionID=$executionID&status=unclosed&param=0&orderBy=id_desc");
}
/* Set the response to return story list. */
elseif($afterChoice == 'toStoryList')
{
$response['locate'] = $this->createLink('execution', 'story', "executionID=$executionID");
if($this->config->vision == 'lite')
{
$projectID = $this->execution->getProjectID($executionID);
$response['locate'] = $this->createLink('projectstory', 'story', "projectID=$projectID");
}
}
return $response;
}
/**
* 展示需求相关变量。
* Show requirements related variables.
*
* @param int $executionID
* @access protected
* @return void
*/
protected function showStoryVars(int $executionID): void
{
$stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', '', '', 'active');
$testStoryIdList = $this->loadModel('story')->getTestStories(array_keys($stories), $executionID);
$testStories = array();
foreach($stories as $testStoryID => $storyTitle)
{
if(empty($testStoryID) or isset($testStoryIdList[$testStoryID])) continue;
$testStories[$testStoryID] = $storyTitle;
}
$this->view->testStories = $testStories;
$this->view->testStoryIdList = $testStoryIdList;
$this->view->stories = $stories;
}
/**
* 展示创建任务的相关变量。
* Show the variables associated with the creation task.
*
* @param object $execution
* @param int $storyID
* @param int $moduleID
* @param int $taskID
* @param int $todoID
* @param int $bugID
* @param array $output
* @access protected
* @return void
*/
protected function showCreateVars(object $execution, int $storyID, int $moduleID, int $taskID, int $todoID, int $bugID, array $output): void
{
/* Get information about the task. */
$executionID = $execution->id;
$task = $this->setTaskInfoByObjectID($storyID, $moduleID, $taskID, $todoID, $bugID);
/* Get module information. */
$showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : '';
$moduleOptionMenu = $this->tree->getTaskOptionMenu($executionID, 0, 0, $showAllModule ? 'allModule' : '');
if(!$storyID and !isset($moduleOptionMenu[$task->module])) $task->module = 0;
/* Display relevant variables. */
$this->showAssignedToMeBlockID();
$this->showExecutionData($execution);
$this->showStoryVars($executionID);
if($execution->type == 'kanban') $this->showKanbanRelatedVars($executionID, $output);
/* Set Custom fields. */
foreach(explode(',', $this->config->task->customCreateFields) as $field) $customFields[$field] = $this->lang->task->$field;
$this->view->title = $execution->name . $this->lang->colon . $this->lang->task->create;
$this->view->customFields = $customFields;
$this->view->showAllModule = $showAllModule;
$this->view->moduleOptionMenu = $moduleOptionMenu;
$this->view->showFields = $this->config->task->custom->createFields;
$this->view->gobackLink = (isset($output['from']) and $output['from'] == 'global') ? $this->createLink('execution', 'task', "executionID=$executionID") : '';
$this->view->execution = $execution;
$this->view->task = $task;
$this->view->storyID = $storyID;
$this->display();
}
/**
* 准备创建任务前的数据信息。
* Prepare the data information before creating the task.
*
* @param int $executionID
* @param float $estimate
* @param string $estStarted
* @param string $deadline
* @param bool $selectTestStory
* @access protected
* @return bool|array
*/
protected function prepareCreate(int $executionID, float $estimate, string $estStarted, string $deadline, bool $selectTestStory): bool|array
{
/* Check if the incoming creation data meets the requirements. */
$result = $this->checkCreate($executionID, $estimate, $estStarted, $deadline);
if(!$result) return false;
/* Process the request data for the creation task. */
$formData = form::data($this->config->task->form->create);
$task = $this->prepareTask4Create($executionID, $formData);
/* Prepare to create the data for the test subtask and to check the data format. */
$testTasks = array();
if($selectTestStory and $task->type == 'test')
{
$testTasks = $this->prepareTestTasks4Create($executionID, $formData);
$result = $this->checkTestTasks($testTasks);
if(!$result) return false;
}
/* Check whether a task with the same name is created within the specified time. */
$existTaskID = $this->checkDuplicateName($task);
return array($task, $testTasks, $existTaskID);
}
/**
* 根据一级菜单设置任务模块的导航。
* According to the main menu, set the navbar of the task module.
*
* @param int $executionID
* @param int $projectID
* @access protected
* @return void
*/
protected function setMenuByTab(int $executionID, int $projectID = 0): void
{
$this->execution->setMenu($executionID);
if($this->app->tab == 'project') $this->loadModel('project')->setMenu($projectID);
}
/**
* 为表单获取自定义字段。
* Get task's custom fields for form.
*
* @param object $execution
* @param string $action
* @access protected
* return array
*/
protected function getCustomFields(object $execution, string $action): array
{
/* 设置自定义字段列表。 */
$customFormField = 'custom' . ucfirst($action). 'Fields';
foreach(explode(',', $this->config->task->{$customFormField}) as $field)
{
if($execution->type == 'stage' and strpos('estStarted,deadline', $field) !== false) continue;
$customFields[$field] = $this->lang->task->$field;
}
/* 设置已勾选的自定义字段。 */
$showFields = $this->config->task->custom->{$action . 'Fields'};
if($execution->lifetime == 'ops' or $execution->attribute == 'request' or $execution->attribute == 'review')
{
unset($customFields['story']);
$showFields = str_replace(',story,', ',', ",$showFields,");
$showFields = trim($showFields, ',');
}
return array($customFields, $showFields);
}
/**
* 构建批量创建任务的表单数据。
* Build batch create form.
*
* @param object $execution
* @param int $storyID
* @param int $moduleID
* @param int $taskID
* @param array $output
* @access protected
* @return void
*/
protected function buildBatchCreateForm(object $execution, int $storyID, int $moduleID, int $taskID, array $output): void
{
/* 获取区域和泳道下拉数据,并设置区域和泳道的默认值。*/
if($execution->type == 'kanban') $this->taskZen->showKanbanRelatedVars($execution->id, $output);
/* 任务拆解。 */
if($taskID)
{
$task = $this->dao->findById($taskID)->from(TABLE_TASK)->fetch();
$this->view->parentTitle = $task->name;
$this->view->parentPri = $task->pri;
}
/* 需求批量分解任务。 */
$story = $this->story->getByID($storyID);
$moduleID = $story ? $story->module : $moduleID;
$moduleParam = $story ? $moduleID : 0;
/* 获取模块下拉数据。 */
$showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : '';
$modules = $this->loadModel('tree')->getTaskOptionMenu($execution->id, 0, 0, $showAllModule ? 'allModule' : '');
$stories = $this->story->getExecutionStoryPairs($execution->id, 0, 'all', $moduleParam, 'short', 'active');
/* Set Custom. */
list($customFields, $showFields) = $this->getCustomFields($execution, 'batchCreate');
$this->view->title = $execution->name . $this->lang->colon . $this->lang->task->batchCreate;
$this->view->execution = $execution;
$this->view->modules = $modules;
$this->view->parent = $taskID;
$this->view->storyID = $storyID;
$this->view->story = $story;
$this->view->moduleID = $moduleID;
$this->view->stories = $stories;
$this->view->storyTasks = $this->task->getStoryTaskCounts(array_keys($stories), $execution->id);
$this->view->members = $this->loadModel('user')->getTeamMemberPairs($execution->id, 'execution', 'nodeleted');
$this->view->taskConsumed = isset($task) ? $task->consumed : 0;
$this->view->customFields = $customFields;
$this->view->showFields = $showFields;
$this->display();
}
/**
* 获取重定向链接。
* Get redirected link.
*
* @param object $execution
* @access protected
* @return string
*/
protected function getRedirectedLink(object $execution): string
{
if($this->app->tab == 'my')
{
$link = $this->createLink('my', 'work', 'mode=task');
}
elseif($this->app->tab == 'project' and $execution->multiple)
{
$link = $this->createLink('project', 'execution', "browseType=all&projectID={$execution->project}");
}
else
{
$link = $this->createLink('execution', 'browse', "executionID=$execution->id");
}
return $link;
}
/**
* 任务的数据更新之后,获取对应看板的数据。
* Get R&D kanban's or task kanban's data after task's data is updated.
*
* @param object $execution
* @access protected
* @return string
*/
protected function getKanbanData(object $execution): string
{
$this->loadModel('kanban');
$execLaneType = $this->session->execLaneType ? $this->session->execLaneType : 'all';
$execGroupBy = $this->session->execGroupBy ? $this->session->execGroupBy : 'default';
$rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : '';
$taskSearchValue = $this->session->taskSearchValue ? $this->session->taskSearchValue : '';
/* 处理专业研发看板。 */
if($execution->type == 'kanban')
{
$kanbanData = $this->kanban->getRDKanban($execution->id, $execLaneType, 'id_desc', 0, $execGroupBy, $rdSearchValue);
$kanbanData = json_encode($kanbanData);
return $kanbanData;
}
/* 处理任务看板。 */
$kanbanData = $this->kanban->getExecutionKanban($execution->id, $execLaneType, $execGroupBy, $taskSearchValue);
$kanbanType = $execLaneType == 'all' ? 'task' : key($kanbanData);
$kanbanData = json_encode($kanbanData[$kanbanType]);
return $kanbanData;
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ class todo extends control
$todo = $this->todoZen->prepareCreateData($todoData, $uid);
if(!$todo) return print(js::error(dao::getError()));
$todoID = $this->todo->create($todo, $formData);
$todoID = $this->todo->create($todo);
if($todoID === false) return print(js::error(dao::getError()));
$todo->id = $todoID;
+4 -4
View File
@@ -82,8 +82,8 @@ class todoTao extends todoModel
{
$this->dao->insert(TABLE_TODO)->data($todo)
->autoCheck()
->checkIF(!in_array($todo->type, $this->config->todo->moduleList), $this->config->todo->create->requiredFields, 'notempty')
->checkIF(in_array($todo->type, $this->config->todo->moduleList) && $todo->objectID == 0, 'objectID', 'notempty')
->check($this->config->todo->create->requiredFields, 'notempty')
->checkIF(isset($todo->type) && in_array($todo->type, $this->config->todo->moduleList), 'objectID', 'notempty')
->exec();
return (int)$this->dao->lastInsertID();
@@ -102,8 +102,8 @@ class todoTao extends todoModel
{
$this->dao->update(TABLE_TODO)->data($todo)
->autoCheck()
->checkIF(isset($todo->type) && in_array($todo->type, array('custom', 'feedback')), $this->config->todo->edit->requiredFields, 'notempty')
->checkIF(isset($todo->type) && in_array($todo->type, $this->config->todo->moduleList) && $todo->objectID == 0, 'objectID', 'notempty')
->check($this->config->todo->edit->requiredFields, 'notempty')
->checkIF(isset($todo->type) && in_array($todo->type, $this->config->todo->moduleList), 'objectID', 'notempty')
->where('id')->eq($todoID)
->exec();
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
include dirname(__FILE__, 2) . '/todo.class.php';
su('admin');
function initData()
{
zdTable('todo')->config('fetch')->gen(10);
}
/**
title=测试完成待办 todoModel->finish();
timeout=0
cid=1
- 查询id=1的todo并且验证状态
- 属性id @1
- 属性status @wait
- 查询id=9的todo并且验证状态
- 属性id @9
- 属性status @done
*/
initData();
global $tester;
$tester->loadModel('todo')->todoTao;
r($tester->todo->fetch(1)) && p('id,status') && e('1,wait'); // 查询id=1的todo并且验证状态
r($tester->todo->fetch(9)) && p('id,status') && e('9,done'); // 查询id=9的todo并且验证状态
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
su('admin');
function initData()
{
zdTable('todo')->config('getcountbyaccount')->gen(20);
}
/**
title=测试 todoModel->getCount();
timeout=0
cid=1
- 获取用户admin的所有待办个数 @4
- 获取用户user1的所有待办个数 @3
- 获取用户user2的所有待办个数 @3
- 获取不存在的用户所有待办个数 @0
- 获取用户admin的vision=>rand所有待办个数 @4
*/
initData();
global $tester;
$tester->loadModel('todo')->todoTao;
$accountList = array('admin', 'user1', 'user2', 'user10001');
r($tester->todo->getCountByAccount($accountList[0])) && p() && e('4'); // 获取用户admin的所有待办个数
r($tester->todo->getCountByAccount($accountList[1])) && p() && e('3'); // 获取用户user1的所有待办个数
r($tester->todo->getCountByAccount($accountList[2])) && p() && e('3'); // 获取用户user2的所有待办个数
r($tester->todo->getCountByAccount($accountList[3])) && p() && e('0'); // 获取不存在的用户所有待办个数
r($tester->todo->getCountByAccount($accountList[0],'rnd')) && p() && e('4'); // 获取用户admin的vision=>rand所有待办个数
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . "/test/lib/init.php";
include dirname(__FILE__, 4) . '/task/test/task.class.php';
su('admin');
function initData()
{
zdTable('todo')->config('getprojectlist')->gen(10);
}
/**
title=测试 todoModel->getCount();
timeout=0
cid=1
- 验证task获得的键值对的个数 @4
*/
initData();
global $tester;
$tester->loadModel('todo');
$taskData1 = array('id' => 10, 'name' => '开发任务一', 'type' => 'devel');
$taskData2 = array('id' => 11, 'name' => '开发任务二', 'type' => 'devel');
$taskData3 = array('id' => 12, 'name' => '开发任务三', 'type' => 'devel');
$taskData4 = array('id' => 13, 'name' => '开发任务四', 'type' => 'devel');
$task = new taskTest();
$task->createObject($taskData1,101);
$task->createObject($taskData2,102);
$task->createObject($taskData3,103);
$task->createObject($taskData4,104);
$ids = range(1,10);
$list = $tester->todo->getByList($ids);
$projectIDList = array_column($list, 'objectID');
$tester->loadModel('todo')->todoTao;
$projectList = $tester->todo->getProjectList('zt_task', $projectIDList);
r(count($projectList)) && p() && e('4'); //验证task获得的键值对的个数
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
su('admin');
function initData ()
{
zdTable('todo')->gen(1);
}
/**
title=测试 todoTao::insert();
timeout=0
cid=1
- 正常插入待办数据 @2
- 正常插入task类型待办数据 @3
- 插入没有名称的待办数据 @0
- 插入bug类型待办,没有bugID的情况 @0
*/
initData();
global $tester;
$tester->loadModel('todo');
$todo = new stdClass();
$todo->account = 'admin';
$todo->date = date('Y-m-d');
$todo->begin = '1000';
$todo->end = '1400';
$todo->type = 'custom';
$todo->name = '我的待办名字';
$todo->desc = '我的待办描述';
$todo->status = 'wait';
$todoNoName = clone $todo;
$todoNoName->name = '';
$todoBug = clone $todo;
$todoBug->type = 'bug';
$todoBug->name = 'BUG待办';
$todoBug->objectID = 0;
$todoTask = clone $todo;
$todoTask->type = 'task';
$todoTask->name = 'TASK待办';
$todoTask->objectID = 1;
r($tester->todo->insert($todo)) && p() && e(2); // 正常插入待办数据
r($tester->todo->insert($todoTask)) && p() && e(3); // 正常插入task类型待办数据
r($tester->todo->insert($todoNoName)) && p() && e(0); // 插入没有名称的待办数据
r($tester->todo->insert($todoBug)) && p() && e(0); // 插入bug类型待办,没有bugID的情况
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
su('admin');
function initData ()
{
zdTable('todo')->gen(1);
}
/**
title=测试 todoTao::updateRow();
timeout=0
cid=1
- 正常更新待办数据 @1
- 正常更新task类型待办数据 @1
- 更新没有名称的待办数据 @0
- 更新bug类型待办,没有bugID的情况 @0
*/
initData();
global $tester;
$tester->loadModel('todo');
$todo = new stdClass();
$todo->account = 'admin';
$todo->date = date('Y-m-d');
$todo->begin = '1000';
$todo->end = '1400';
$todo->type = 'custom';
$todo->name = '我的待办名字';
$todo->desc = '我的待办描述';
$todo->status = 'wait';
$todoNoName = clone $todo;
$todoNoName->name = '';
$todoBug = clone $todo;
$todoBug->type = 'bug';
$todoBug->name = 'BUG待办';
$todoBug->objectID = 0;
$todoTask = clone $todo;
$todoTask->type = 'task';
$todoTask->name = 'TASK待办';
$todoTask->objectID = 1;
r($tester->todo->updateRow(1, $todo)) && p() && e('1'); // 正常更新待办数据
r($tester->todo->updateRow(2, $todoTask)) && p() && e('1'); // 正常更新task类型待办数据
r($tester->todo->updateRow(3, $todoNoName)) && p() && e('0'); // 更新没有名称的待办数据
r($tester->todo->updateRow(4, $todoBug)) && p() && e('0'); // 更新bug类型待办,没有bugID的情况
@@ -0,0 +1,35 @@
title: zt_fetch
author: liuchenglong
version: "1.0"
fields:
- field: id
range: 1-100
- field: account
range: 'admin'
- field: date
range: 1-9
prefix: '2023-04-2'
- field: begin
range: 1000-1100
- field: end
range: 1200-1300
- field: type
range: 'custom'
- field: cycle
range: 0
- field: pri
range: 3
- field: name
range: 1-5
prefix: '我的待办'
- field: desc
range: 1-5
prefix: '这是待办描述'
- field: status
range: 'wait,doing,done'
- field: finishedDate
range: 1-9
prefix: '2023-04-2'
- field: closedDate
range: 1-9
prefix: '2023-04-2'
@@ -0,0 +1,38 @@
title: zt_getcountbyaccount
author: liuchenglong
version: "1.0"
fields:
- field: id
range: 1-100
- field: account
range: 'admin,user1,user2'
- field: date
range: 1-9
prefix: '2023-04-2'
- field: begin
range: 1000-1100
- field: end
range: 1200-1300
- field: type
range: 'custom'
- field: cycle
range: 0
- field: pri
range: 3
- field: name
range: 1-5
prefix: '我的待办'
- field: desc
range: 1-5
prefix: '这是待办描述'
- field: status
range: 'wait,doing,done'
- field: vision
range: 'rnd,litle'
- field: finishedDate
range: 1-9
prefix: '2023-04-2'
- field: closedDate
range: 1-9
prefix: '2023-04-2'
@@ -0,0 +1,42 @@
title: zt_getprojectlist
author: liuchenglong
version: "1.0"
fields:
- field: id
range: 1-100
- field: account
range: 'admin,user1,user2'
- field: objectID
range: 10-20
- field: date
range: 1-9
prefix: '2023-04-2'
- field: begin
range: 1000-1100
- field: end
range: 1200-1300
- field: type
range: 'task'
- field: cycle
range: 0
- field: pri
range: 3
- field: name
range: 1-5
prefix: '我的待办'
- field: desc
range: 1-5
prefix: '这是待办描述'
- field: status
range: 'wait,doing,done'
- field: vision
range: 'rnd,litle'
- field: finishedDate
range: 1-9
prefix: '2023-04-2'
- field: closedDate
range: 1-9
prefix: '2023-04-2'
-4
View File
@@ -99,8 +99,6 @@ fields:
format: "YYYY-MM-DD"
- field: closedBy
range: "[]{50},admin{30}"
- field: closedDate
range:
- field: duplicateBug
range: 0{55},1-5,0{40},10-15
- field: linkBug
@@ -127,7 +125,5 @@ fields:
range: 0
- field: lastEditedBy
range:
- field: lastEditedDate
range:
- field: deleted
range: 0{40},1{10},0{50}
+1 -10
View File
@@ -100,7 +100,7 @@ fields:
format: ""
- field: status
note: "当前状态"
range: draft,active,closed,changed
range: draft,active,closed,changing
prefix: ""
postfix: ""
loop: 0
@@ -181,9 +181,6 @@ fields:
postfix: ""
loop: 0
format: ""
- field: lastEditedDate
note: "最后修改日期"
range: ""
- field: reviewedBy
note: "评审者"
fields:
@@ -194,9 +191,6 @@ fields:
prefix: ""
postfix: ""
format: ""
- field: reviewedDate
note: "评审日期"
range: ""
- field: closedBy
note: "关闭者"
fields:
@@ -208,9 +202,6 @@ fields:
postfix: ""
loop: 0
format: ""
- field: closedDate
note: "关闭日期"
range: ""
- field: closedReason
note: "关闭原因"
range: done,subdivided,duplicate,postponed,willnotdo,cancel,bydesign
+24 -26
View File
@@ -359,7 +359,7 @@ class yaml
if($pos !== false) $runFileName = mb_substr($runFileName, $pos+1);
$runFileDir = dirname($_SERVER['SCRIPT_FILENAME']);
return array($runFileDir, $runFileName);
}
@@ -379,7 +379,7 @@ class yaml
$scriptPath = sprintf("%s%s%s.php", $runFileDir, DS, $runFileName);
$yamlPath = sprintf("%s%sdata%s%s_%s.yaml", $runFileDir, DS, DS, $this->tableName, $runFileName);
if($this->checkNeedReGenerateSql($sqlPath, $scriptPath, $yamlPath))
if($rows && $this->checkNeedReGenerateSql($sqlPath, $scriptPath, $yamlPath))
{
$runtimeRoot = dirname(dirname(__FILE__)) . '/runtime/';
$zdPath = $runtimeRoot . 'zd';
@@ -437,11 +437,6 @@ class yaml
*/
function insertDB($sqlPath, $tableName, $isClear = true)
{
$tableSqlDir = dirname($sqlPath);
if(!is_dir($tableSqlDir)) mkdir($tableSqlDir, 0777, true);
$dumpCommand = "mysqldump -u%s -p%s -h%s -P%s %s %s > {$tableSqlDir}/{$tableName}.sql 2>/dev/null";
$tableName = $this->config->db->prefix . $tableName;
$dbName = $this->config->db->name;
$dbHost = $this->config->db->host;
@@ -449,10 +444,11 @@ class yaml
$dbUser = $this->config->db->user;
$dbPWD = $this->config->db->password;
$command = "mysql -u%s -p%s -h%s -P%s --default-character-set=utf8 -D%s < %s";
$execInsert = sprintf($command, $dbUser, $dbPWD, $dbHost, $dbPort, $dbName, $sqlPath);
$execDump = sprintf($dumpCommand, $dbUser, $dbPWD, $dbHost, $dbPort, $dbName, $tableName);
$tableSqlDir = dirname($sqlPath);
if(!is_dir($tableSqlDir)) mkdir($tableSqlDir, 0777, true);
$dumpCommand = "mysqldump -u%s -p%s -h%s -P%s %s %s > {$tableSqlDir}/{$tableName}.sql 2>/dev/null";
$execDump = sprintf($dumpCommand, $dbUser, $dbPWD, $dbHost, $dbPort, $dbName, $tableName);
system($execDump);
if($isClear === true)
@@ -461,36 +457,38 @@ class yaml
system(sprintf("mysql -u%s -p%s -h%s -P%s %s -e 'truncate %s' 2>/dev/null", $dbUser, $dbPWD, $dbHost, $dbPort, $dbName, $tableName));
}
$stderr = '';
$this->execWithStderr($execInsert, $stderr);
if(empty($stderr)) return;
if(!file_exists($sqlPath)) return;
$errors = explode(PHP_EOL, $stderr);
$errors = array_filter($errors, function($error)
{
return !empty($error) && !strpos($error, 'Using a password on the command line interface can be insecure');
});
if(!empty($errors)) echo implode(PHP_EOL, $errors) . PHP_EOL;
$command = "mysql -u%s -p%s -h%s -P%s --default-character-set=utf8 -D%s < %s";
$execInsert = sprintf($command, $dbUser, $dbPWD, $dbHost, $dbPort, $dbName, $sqlPath);
$this->execWithStderr($execInsert);
}
/**
* 执行系统命令,并捕捉stderror
* 执行系统命令,并捕捉错误。
* exec command and catch stderror.
*
* @param string $cmd
* @param string $stderr
* @access private
* @return string
* @return void
*/
private function execWithStderr($cmd, &$stderr=null)
private function execWithStderr($cmd)
{
$proc = proc_open($cmd, array(2 => array('pipe', 'w')), $pipes);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($proc);
if(empty($stderr)) return;
$errors = explode(PHP_EOL, $stderr);
$errors = array_filter($errors, function($error)
{
return !empty($error) && strpos($error, 'Using a password on the command line interface can be insecure') !== false;
});
if(!empty($errors)) echo implode(PHP_EOL, $errors) . PHP_EOL;
}
/**