From c9c340d0e8848329e1bbec6b8dcd15a6be9a331c Mon Sep 17 00:00:00 2001 From: tianshujie Date: Sat, 6 May 2023 11:33:40 +0800 Subject: [PATCH] * Split the function that create the task. --- module/task/config.php | 3 + module/task/config/form.php | 20 + module/task/control.php | 331 +--------- module/task/model.php | 572 ++++++++-------- module/task/tao.php | 92 ++- ...gettaskestimate.php => gettaskefforts.php} | 4 +- module/task/test/task.class.php | 6 +- module/task/zen.php | 613 +++++++++++++++++- 8 files changed, 1015 insertions(+), 626 deletions(-) rename module/task/test/model/{gettaskestimate.php => gettaskefforts.php} (73%) diff --git a/module/task/config.php b/module/task/config.php index 09eca6121d..ed781a954d 100644 --- a/module/task/config.php +++ b/module/task/config.php @@ -1,4 +1,7 @@ task = new stdclass(); $config->task->batchCreate = 10; diff --git a/module/task/config/form.php b/module/task/config/form.php index 0914fe827a..69f37052bd 100644 --- a/module/task/config/form.php +++ b/module/task/config/form.php @@ -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); diff --git a/module/task/control.php b/module/task/control.php index eb50716d46..dd4eb89522 100755 --- a/module/task/control.php +++ b/module/task/control.php @@ -27,31 +27,29 @@ 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 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')); - $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); + /* 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,298 +58,35 @@ 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¶m=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' : ''); + /* Shows the variables needed to create the task page. */ + $this->taskZen->showCreateVars($execution, $storyID, $moduleID, $taskID, $todoID, $bugID, $extra); - /* 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(); } @@ -1129,7 +864,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; diff --git a/module/task/model.php b/module/task/model.php index a7dc3da4b8..33f7cb6211 100755 --- a/module/task/model.php +++ b/module/task/model.php @@ -14,278 +14,121 @@ 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; } + /** + * 计算多人任务工时。 + * 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; + } + /** * Create a batch task. * @@ -811,59 +654,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,7 +672,7 @@ class taskModel extends model if($taskStatus == 'doing') { - $efforts = $this->getTaskEstimate($taskID); + $efforts = $this->getTaskEfforts($taskID); $doingUsers = array(); foreach($efforts as $i => $effort) { @@ -1012,7 +802,7 @@ class taskModel extends model } elseif($effortID) { - $efforts = $this->getTaskEstimate($taskID, '', $effortID); + $efforts = $this->getTaskEfforts($taskID, '', $effortID); $prevTeam = null; $thisTeam = null; @@ -2342,7 +2132,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 +2482,7 @@ class taskModel extends model } /** - * Get task estimate. + * Get task efforts. * * @param int $taskID * @param string $account @@ -2701,7 +2491,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 +2732,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 +4016,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'); + } + } } diff --git a/module/task/tao.php b/module/task/tao.php index afba92f783..1d48db64df 100644 --- a/module/task/tao.php +++ b/module/task/tao.php @@ -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,34 @@ 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, ','); + } } diff --git a/module/task/test/model/gettaskestimate.php b/module/task/test/model/gettaskefforts.php similarity index 73% rename from module/task/test/model/gettaskestimate.php rename to module/task/test/model/gettaskefforts.php index f5546dbde0..1d80d236c9 100755 --- a/module/task/test/model/gettaskestimate.php +++ b/module/task/test/model/gettaskefforts.php @@ -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'); // 查看任务预计 \ No newline at end of file +r($task->getTaskEffortsTest($taskID)) && p('0:task') && e('55'); // 查看任务预计 diff --git a/module/task/test/task.class.php b/module/task/test/task.class.php index 519286db12..76e0026874 100644 --- a/module/task/test/task.class.php +++ b/module/task/test/task.class.php @@ -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(); diff --git a/module/task/zen.php b/module/task/zen.php index 3c34051fcd..6b1daa3bb9 100644 --- a/module/task/zen.php +++ b/module/task/zen.php @@ -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,606 @@ 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; + } + + /** + * 初始化任务信息。 + * Initialize task information. + * + * @param int $storyID + * @param int $moduleID + * @param int $taskID + * @param int $todoID + * @param int $bugID + * @access protected + * @return object + */ + protected function initTaskData(int $storyID, int $moduleID, int $taskID, int $todoID, int $bugID): object + { + /* Init task data. */ + $task = new stdClass(); + $task->module = 0; + $task->mode = ''; + $task->assignedTo = ''; + $task->name = ''; + $task->story = 0; + $task->type = ''; + $task->pri = 3; + $task->estimate = ''; + $task->desc = ''; + $task->estStarted = null; + $task->deadline = null; + $task->mailto = ''; + $task->color = ''; + + return $this->setTaskInfoByObjectID($task, $storyID, $moduleID, $taskID, $todoID, $bugID); + } + + /** + * 通过传入的对象ID初始化任务信息。 + * Set task information by incoming object id. + * + * @param object $task + * @param int $storyID + * @param int $moduleID + * @param int $taskID + * @param int $todoID + * @param int $bugID + * @access protected + * @return object + */ + protected function setTaskInfoByObjectID(object $task, int $storyID, int $moduleID, int $taskID, int $todoID, int $bugID): object + { + $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 array $output + * @access protected + * @return void + */ + protected function showKanbanRelatedVars(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($execution->id, 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。 + * Set the ID of the block to be processed on the my. + * + * @access protected + * @return void + */ + protected function setAssignedToMeBlockID(): 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; + } + + /** + * 设置执行相关数据。 + * Set execution related data. + * + * @param object $execution + * @access protected + * @return void + */ + protected function setExecutionData(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. */ + if($this->viewType == 'json' or (defined('RUN_MODE') && RUN_MODE == 'api')) return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $taskID); + + $taskID = $task->id; + $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¶m=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 string $extra + * @access protected + * @return void + */ + protected function showCreateVars(object $execution, int $storyID, int $moduleID, int $taskID, int $todoID, int $bugID, string $extra) + { + $extra = str_replace(array(',', ' '), array('&', ''), $extra); + parse_str($extra, $output); + + $executionID = $execution->id; + $task = $this->initTaskData($storyID, $moduleID, $taskID, $todoID, $bugID); + + $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; + + $this->setAssignedToMeBlockID(); + $this->setExecutionData($execution); + $this->showStoryVars($executionID); + if($execution->type == 'kanban') $this->taskZen->showKanbanRelatedVars($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; + } + + /** + * 准备创建任务前的数据信息。 + * 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); + } }