* @package execution * @version $Id: control.php 5094 2013-07-10 08:46:15Z chencongzhi520@gmail.com $ * @link https://www.zentao.net */ class execution extends control { /** * The global $executions. * * @var array * @access public */ public $executions; /** * The global $objectType. * * @var string * @access public */ public $objectType = 'execution'; /** * Construct function, Set executions. * * @param string $moduleName * @param string $methodName * @access public * @return void */ public function __construct($moduleName = '', $methodName = '') { parent::__construct($moduleName, $methodName); $this->loadModel('project'); if($this->app->upgrading || !isset($this->app->user)) return false; $mode = $this->app->tab == 'execution' ? 'multiple' : ''; if((defined('RUN_MODE') and RUN_MODE == 'api') or $this->viewType == 'json') $mode = ''; $this->executions = $this->execution->getPairs(0, 'all', "nocode,noprefix,{$mode}"); $skipCreateStep = array('computeburn', 'ajaxgetdropmenu', 'executionkanban', 'ajaxgetteammembers', 'all', 'ajaxgetcopyprojectexecutions'); if(in_array($this->methodName, $skipCreateStep)) return false; if($this->executions || $this->methodName == 'index' || $this->methodName == 'create' || $this->app->getViewType() == 'mhtml') return false; if($this->app->tab == 'project' && in_array($this->app->rawMethod, array('linkstory', 'importplanstories', 'unlinkstory', 'export'))) return false; if(($this->methodName == 'story' || $this->methodName == 'task') && $this->app->tab == 'doc') return false; $this->locate($this->createLink('execution', 'create')); } /** * 执行任务列表页。 * Browse task list for a execution. * * @param int $executionID * @access public * @return void */ public function browse(int $executionID = 0) { $this->locate($this->createLink($this->moduleName, 'task', "executionID=$executionID")); } /** * 设置页面公共数据。 * Common actions. * * @param int $executionID * @param string $extra * @access public * @return object|false */ public function commonAction(int $executionID = 0, string $extra = ''): object|false { /* Get executions and products info. */ $executionID = $this->execution->checkAccess($executionID, $this->executions); $execution = $this->execution->getById($executionID); $products = $this->loadModel('product')->getProducts($executionID); $childExecutions = $this->execution->getChildExecutions($executionID); $teamMembers = $this->execution->getTeamMembers($executionID); $actions = $this->loadModel('action')->getList($this->objectType, $executionID); $project = $this->loadModel('project')->fetchByID($execution->project); /* Set menu. */ $this->execution->setMenu($executionID, 0, $extra); /* Assign view data. */ $this->view->projectID = $execution->project; $this->view->hidden = !empty($project->hasProduct) ? '' : 'hide'; $this->view->executions = $this->executions; $this->view->execution = $execution; $this->view->executionID = $executionID; $this->view->childExecutions = $childExecutions; $this->view->products = $products; $this->view->teamMembers = $teamMembers; $this->view->actions = $actions; return $execution; } /** * 执行下任务列表页。 * Tasks of a execution. * * @param int $executionID * @param string $status * @param string $param * @param string $orderBy * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function task(int $executionID = 0, string $status = 'unclosed', int $param = 0, string $orderBy = '', int $recTotal = 0, int $recPerPage = 100, int $pageID = 1, string $from = 'execution', int $blockID = 0) { $this->app->loadLang('doc'); if(($from == 'doc' || $from == 'ai') && empty($this->executions)) return $this->send(array('result' => 'fail', 'message' => $this->lang->doc->tips->noExecution)); if(!isset($_SESSION['limitedExecutions'])) $this->execution->getLimitedExecution(); /* Save to session. */ $this->session->set('taskList', $this->app->getURI(true), 'execution'); /* Set browse type. */ $browseType = strtolower($status); $execution = $this->commonAction($executionID, $status); $executionID = $execution->id; if($execution->type == 'kanban' && $this->config->vision != 'lite' && $this->app->getViewType() != 'json' && $from != 'doc') $this->locate($this->createLink('execution', 'kanban', "executionID=$executionID")); /* Save the recently five executions visited in the cookie. */ $this->executionZen->setRecentExecutions($executionID); /* Append id for second sort and process the order by field. */ if(!$orderBy) $orderBy = $this->cookie->executionTaskOrder ? $this->cookie->executionTaskOrder : 'status,id_desc'; $sort = common::appendOrder($orderBy); $this->executionZen->setTaskPageStorage($executionID, $sort, $browseType, (int)$param); /* Set queryID, moduleID and productID. */ $queryID = $moduleID = $productID = 0; if($browseType == 'bysearch') $queryID = (int)$param; if($browseType == 'bymodule') $moduleID = (int)$param; if($browseType == 'byproduct') $productID = (int)$param; if(!in_array($browseType, array('bysearch', 'bymodule', 'byproduct'))) { $moduleID = $this->cookie->moduleBrowseParam ? $this->cookie->moduleBrowseParam : 0; $productID = $this->cookie->productBrowseParam ? $this->cookie->productBrowseParam : 0; } /* Load pager and get tasks. */ $this->app->loadClass('pager', true); if($this->app->getViewType() == 'mhtml' || $this->app->getViewType() == 'xhtml') $recPerPage = 10; $pager = new pager($recTotal, $recPerPage, $pageID); $tasks = $this->execution->getTasks((int)$productID, $executionID, $this->executions, $browseType, $queryID, (int)$moduleID, $sort, $pager); /* team member pairs. */ $memberPairs = array(); foreach($this->view->teamMembers as $key => $member) $memberPairs[$key] = $member->realname; $memberPairs = $this->loadModel('user')->setCurrentUserFirst($memberPairs); if($this->config->edition != 'open') $taskRelatedObject = $this->loadModel('custom')->getRelatedObjectList(array_keys($tasks), 'task', 'byRelation', true); /* Append branches to task. */ $this->loadModel('task'); $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($this->view->products)); foreach($tasks as $task) { $task->name = htmlspecialchars_decode($task->name); if($task->mode == 'multi' && strpos('done,closed,cancel', $task->status) === false) { $task->assignedTo = ''; $taskTeam = $this->task->getTeamByTask($task->id); foreach($taskTeam as $teamMember) { if($this->app->user->account == $teamMember->account && $teamMember->status != 'done') { $task->assignedTo = $this->app->user->account; break; } } } if(isset($branchGroups[$task->product][$task->branch])) $task->branch = $branchGroups[$task->product][$task->branch]; if($task->needConfirm) $task->status = $task->rawStatus = 'changed'; if($this->config->edition != 'open') $task->relatedObject = zget($taskRelatedObject, $task->id, 0); } $showAllModule = empty($this->config->execution->task->allModule) ? '' : 'allModule'; $showModule = !empty($this->config->execution->task->showModule) ? $this->config->execution->task->showModule : ''; /* Build the search form. */ $modules = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, $showModule); $actionURL = $this->createLink('execution', 'task', "executionID=$executionID&status=bySearch¶m=myQueryID&orderBy=$orderBy&recTotal=0&recPerPage=100&pageID=1&from=$from&blockID=$blockID"); $this->execution->buildTaskSearchForm($executionID, $this->executions, $queryID, $actionURL); $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->task; $this->view->tasks = $tasks; $this->view->pager = $pager; $this->view->orderBy = $orderBy; $this->view->browseType = $browseType; $this->view->status = $status; $this->view->param = $param; $this->view->moduleID = $moduleID; $this->view->modules = $modules; $this->view->modulePairs = $showModule ? $this->tree->getModulePairs($executionID, 'task', $showModule) : array(); $this->view->moduleTree = $this->tree->getTaskTreeMenu($executionID, (int)$productID, 0, array('treeModel', 'createTaskLink'), $showAllModule); $this->view->memberPairs = $memberPairs; $this->view->execution = $execution; $this->view->executionID = $executionID; $this->view->productID = $productID; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->features = $this->execution->getExecutionFeatures($execution); $this->view->from = $from; $this->view->blockID = $blockID; $this->view->idList = ''; if($from === 'doc') { $docBlock = $this->loadModel('doc')->getDocBlock($blockID); $this->view->docBlock = $docBlock; if($docBlock) { $content = json_decode($docBlock->content, true); if(isset($content['idList'])) $this->view->idList = $content['idList']; } } $this->display(); } /** * 任务分组视图。 * Browse tasks in group. * * @param int $executionID * @param string $groupBy story|status|pri|assignedTo|finishedBy|closedBy|type * @param string $filter * @access public * @return void */ public function grouptask(int $executionID = 0, string $groupBy = 'story', string $filter = '') { $execution = $this->commonAction($executionID); $executionID = $execution->id; /* Execution of the ops type don't show group by story. */ if(empty($groupBy))$groupBy = 'story'; if($groupBy == 'story' && $execution->lifetime == 'ops') { $groupBy = 'status'; unset($this->lang->execution->groups['story']); } $sort = common::appendOrder($groupBy); $tasks = $this->loadModel('task')->getExecutionTasks($executionID, 0, 'all', array(), $sort); $groupBy = str_replace('`', '', $groupBy); $groupTasks = array(); $allCount = 0; foreach($tasks as $task) { if($task->mode == 'multi') $task->assignedToRealName = $this->lang->task->team; $groupTasks[] = $task; $allCount ++; if(isset($task->children)) { foreach($task->children as $child) { $groupTasks[] = $child; $allCount ++; } unset($task->children); } } /* Get users and build task group data. */ $users = $this->loadModel('user')->getPairs('noletter'); $tasks = $groupTasks; list($groupTasks, $groupByList) = $this->executionZen->buildGroupTasks($groupBy, $groupTasks, array('' => '') + $users); /* Remove task by filter and group. */ $filter = (empty($filter) && isset($this->lang->execution->groupFilter[$groupBy])) ? key($this->lang->execution->groupFilter[$groupBy]) : $filter; list($groupTasks, $allCount) = $this->executionZen->filterGroupTasks($groupTasks, $groupBy, $filter, $allCount, $tasks); if(!isset($_SESSION['limitedExecutions'])) $this->execution->getLimitedExecution(); /* Assign. */ $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->task; $this->view->members = $this->execution->getTeamMembers($executionID); $this->view->tasks = $groupTasks; $this->view->groupByList = $groupByList; $this->view->browseType = 'group'; $this->view->groupBy = $groupBy; $this->view->orderBy = $groupBy; $this->view->executionID = $executionID; $this->view->users = $users; $this->view->features = $this->execution->getExecutionFeatures($execution); $this->view->filter = $filter; $this->view->allCount = $allCount; $this->view->execution = $execution; $this->view->isLimited = !$this->app->user->admin && strpos(",{$this->session->limitedExecutions},", ",{$executionID},") !== false; $this->display(); } /** * Import tasks undoned from other executions. * * @param int $executionID * @param int $fromExecution * @param string $orderBy * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function importTask(int $toExecution, int $fromExecution = 0, string $orderBy = 'id_desc', int $recPerPage = 20, int $pageID = 1) { if(!empty($_POST)) { $taskIdList = form::data($this->config->execution->form->importTask)->get('taskIdList'); $dateExceed = $this->execution->importTask($toExecution, $taskIdList); $result = array('load' => true); if($dateExceed) $result['message'] = sprintf($this->lang->task->error->dateExceed, implode(',', $dateExceed)); return $this->sendSuccess($result); } $execution = $this->commonAction($toExecution); $toExecution = $execution->id; $project = $this->loadModel('project')->getByID($execution->project); $branches = $this->execution->getBranches($toExecution); $tasks = $this->execution->getTasks2Imported($toExecution, $branches, $orderBy); $executions = $this->execution->getToImport(array_keys($tasks), $execution->type, $project->model); unset($executions[$toExecution]); $tasks2Imported = array(); if($fromExecution == 0) { foreach($executions as $executionID => $executionName) $tasks2Imported = array_merge($tasks2Imported, $tasks[$executionID]); } else { $tasks2Imported = zget($tasks, $fromExecution, array()); } /* Pager. */ $this->app->loadClass('pager', true); $pager = new pager(count($tasks2Imported), $recPerPage, $pageID); $tasks2ImportedList = array_chunk($tasks2Imported, $pager->recPerPage, true); if(!empty($tasks2ImportedList)) $tasks2ImportedList = isset($tasks2ImportedList[$pageID - 1]) ? $tasks2ImportedList[$pageID - 1] : current($tasks2ImportedList); $tasks2ImportedList = $this->loadModel('task')->processTasks($tasks2ImportedList); $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->importTask; $this->view->pager = $pager; $this->view->orderBy = $orderBy; $this->view->executions = $executions; $this->view->fromExecution = $fromExecution; $this->view->tasks2Imported = $tasks2ImportedList; $this->view->memberPairs = $this->loadModel('user')->getPairs('noletter|pofirst'); $this->display(); } /** * 导入Bug。 * Import from Bug. * * @param int $executionID * @param string $browseType * @param int $param * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function importBug(int $executionID = 0, string $browseType = 'all', int $param = 0, int $recTotal = 0, int $recPerPage = 30, int $pageID = 1) { $this->session->set('bugList', $this->app->getURI(true), 'execution'); $this->execution->setMenu($executionID); $execution = $this->execution->getByID($executionID); if(!empty($_POST)) { $postData = form::batchData($this->config->execution->form->importBug)->get(); $tasks = $this->executionZen->buildTasksForImportBug($execution, $postData); if(dao::isError()) return $this->sendError(dao::getError()); $this->execution->importBug($tasks); if(dao::isError()) return $this->sendError(dao::getError()); return $this->sendSuccess(array('load' => true, 'closeModal' => true)); } /* Get users, products, executions, project and projects.*/ $users = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted'); $products = $this->loadModel('product')->getProductPairsByProject($executionID); $executions = !empty($products) ? $this->execution->getPairsByProduct(array_keys($products)) : array($executionID => $execution->name); $project = $this->loadModel('project')->getByID($execution->project); !empty($products) ? $projects = $this->product->getProjectPairsByProductIDList(array_keys($products)) : $projects[$project->id] = $project->name; /* Set browseType, productID, moduleID and queryID. */ $browseType = strtolower($browseType); $queryID = ($browseType == 'bysearch') ? (int)$param : 0; /* Load pager. */ $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); /* Get bugs.*/ $bugs = $this->executionZen->getImportBugs($executionID, array_keys($products), $browseType, $queryID, $pager); /* Build the search form. */ $this->executionZen->buildImportBugSearchForm($execution, $queryID, $products, $executions, $projects); if($this->app->tab == 'project') $this->view->projectID = $execution->project; /* Assign. */ $this->view->title = $executions[$executionID] . $this->lang->hyphen . $this->lang->execution->importBug; $this->view->pager = $pager; $this->view->bugs = $bugs; $this->view->browseType = $browseType; $this->view->param = $param; $this->view->users = $users; $this->view->execution = $execution; $this->view->executionID = $executionID; $this->display(); } /** * 需求列表。 * Browse stories of a execution. * * @param int $executionID * @param string $storyType story|requirement * @param string $orderBy * @param string $type all|byModule|byProduct|byBranch|bySearch * @param int $param * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function story(int $executionID = 0, string $storyType = 'story', string $orderBy = 'order_desc', string $type = 'all', int $param = 0, int $recTotal = 0, int $recPerPage = 50, int $pageID = 1, string $from = 'execution', int $blockID = 0) { /* Load these models. */ $this->loadModel('story'); $this->loadModel('requirement'); $this->loadModel('epic'); $this->loadModel('product'); $this->app->loadLang('doc'); if(($from == 'doc' || $from == 'ai') && empty($this->executions)) return $this->send(array('result' => 'fail', 'message' => $this->lang->doc->tips->noExecution)); /* Change for requirement story title. */ $this->lang->story->linkStory = str_replace($this->lang->URCommon, $this->lang->SRCommon, $this->lang->story->linkStory); if($storyType == 'requirement') { $this->story->replaceURLang($storyType); $this->config->product->search['fields']['title'] = $this->lang->story->title; unset($this->config->product->search['fields']['plan']); unset($this->config->product->search['params']['plan']); unset($this->config->product->search['fields']['stage']); unset($this->config->product->search['params']['stage']); } /* Process the order by field. */ if(!$orderBy) $orderBy = $this->cookie->executionStoryOrder ? $this->cookie->executionStoryOrder : 'pri'; $param = (int)$param; $type = strtolower($type); $productID = $this->executionZen->setStorageForStory((string)$executionID, $type, (string)$param, $orderBy); /* Append id for second sort. */ $sort = common::appendOrder($orderBy); if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort); $execution = $this->commonAction($executionID); $executionID = $execution->id; $project = $this->loadModel('project')->getByID($execution->project); /* Build the search form. */ $products = $this->product->getProducts($executionID); $actionURL = $this->createLink('execution', 'story', "executionID=$executionID&storyType=$storyType&orderBy=$orderBy&type=bySearch&queryID=myQueryID&recTotal=0&recPerPage=&pageID=1&from=$from&blockID=$blockID"); $this->executionZen->buildStorySearchForm($execution, $productID, $products, $type == 'bysearch' ? $param : 0, $actionURL); /* Load pager. */ $this->app->loadClass('pager', true); if($this->app->getViewType() == 'xhtml') $recPerPage = 10; $pager = new pager($recTotal, $recPerPage, $pageID); $stories = $this->story->getExecutionStories($executionID, 0, $sort, $type, (string)$param, $project->storyType ?? 'story', '', $pager); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false); if(!empty($stories)) $stories = $this->story->mergeReviewer($stories); if($this->config->edition == 'ipd') $stories = $this->loadModel('story')->getAffectObject($stories, 'story'); $this->executionZen->assignCountForStory($executionID, $stories, $storyType); $this->executionZen->assignRelationForStory($execution, $products, $productID, $type, $storyType, $param, $orderBy, $pager); $this->view->from = $from; $this->view->blockID = $blockID; $this->view->idList = ''; if($from === 'doc') { $docBlock = $this->loadModel('doc')->getDocBlock($blockID); $this->view->docBlock = $docBlock; if($docBlock) { $content = json_decode($docBlock->content, true); if(isset($content['idList'])) $this->view->idList = $content['idList']; } $this->view->executions = $this->execution->getPairs(); } $this->view->productID = $productID; $this->view->project = $project; $this->view->linkedProductCount = count($products); $this->view->hasFrozenStories = $this->project->hasFrozenObject($project->id, 'SRS'); $this->display(); } /** * 查看需求详情。 * View a story. * * @param int $storyID * @param int $executionID * @access public * @return void */ public function storyView(int $storyID, int $executionID = 0) { $this->session->set('productList', $this->app->getURI(true), 'product'); $story = $this->loadModel('story')->getByID($storyID); echo $this->fetch('story', 'view', "storyID=$storyID&version=$story->version¶m=" . ($executionID ? $executionID : $this->session->execution) . "&storyType={$story->type}"); } /** * Bug列表。 * Browse bugs of a execution. * * @param int $executionID * @param int $productID * @param int $branchID * @param string $orderBy * @param string $build * @param string $type * @param int $param * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function bug(int $executionID = 0, int $productID = 0, string $branch = 'all', string $orderBy = 'status,id_desc', string $build = '', string $type = 'all', int $param = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { /* Save session and load model. */ $this->loadModel('bug'); $this->session->set('bugList', $this->app->getURI(true), 'execution'); $this->session->set('buildList', $this->app->getURI(true), 'execution'); $type = strtolower($type); $execution = $this->commonAction($executionID); $project = $this->loadModel('project')->getByID($execution->project); $executionID = $execution->id; $products = $this->product->getProducts($execution->id); $param = in_array($type, array('bysearch', 'bymodule')) ? (int)$param : 0; if(count($products) === 1) $productID = current($products)->id; /* Set the product drop-down and search fields. */ $productOption = array(); $branchOption = array(); $showBranch = $this->loadModel('branch')->showBranch($productID); if($execution->hasProduct) { list($productOption, $branchOption) = $this->executionZen->buildProductSwitcher($executionID, $productID, $products); unset($this->config->bug->search['fields']['product']); unset($this->config->bug->search['params']['product']); if($project->model != 'scrum') { unset($this->config->bug->search['fields']['plan']); unset($this->config->bug->search['params']['plan']); } } /* Load pager and get bugs, user. */ $this->app->loadClass('pager', true); if($this->app->getViewType() == 'xhtml') $recPerPage = 10; $pager = new pager($recTotal, $recPerPage, $pageID); $sort = common::appendOrder($orderBy); $bugs = $this->bug->getExecutionBugs($executionID, $productID, $branch, $build, $type, $param, $sort, '', $pager); $bugs = $this->bug->batchAppendDelayedDays($bugs); /* Build the search form. */ $actionURL = $this->createLink('execution', 'bug', "executionID=$executionID&productID=$productID&branch=$branch&orderBy=$orderBy&build=$build&type=bysearch&queryID=myQueryID"); $this->execution->buildBugSearchForm($products, $param, $actionURL); $bugs = $this->bug->processBuildForBugs($bugs); $this->executionZen->assignBugVars($execution, $project, $productID, $branch, $products, $orderBy, $type, $param, $build, $bugs, $pager); $storyIdList = $taskIdList = array(); foreach($bugs as $bug) { if($bug->story) $storyIdList[$bug->story] = $bug->story; if($bug->task) $taskIdList[$bug->task] = $bug->task; if($bug->toTask) $taskIdList[$bug->toTask] = $bug->toTask; } $this->view->showBranch = $showBranch; $this->view->productOption = $productOption; $this->view->branchOption = $branchOption; $this->view->plans = $this->loadModel('productplan')->getForProducts(array_keys($products)); $this->view->tasks = $this->loadModel('task')->getPairsByIdList($taskIdList); $this->view->stories = $this->loadModel('story')->getPairsByList($storyIdList); $this->view->moduleID = $type == 'bymodule' ? $param : 0; $this->view->switcherParams = "executionID={$executionID}&productID={$productID}¤tMethod=bug"; $this->view->switcherText = isset($products[$productID]) ? $products[$productID]->name : $this->lang->product->all; if(empty($project->hasProduct)) $this->config->excludeSwitcherList[] = 'execution-bug'; $this->display(); } /** * 用例列表。 * Execution case list. * * @param int $executionID * @param int $productID * @param string $branchID * @param string $type * @param int $moduleID * @param string $orderBy * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function testcase(int $executionID = 0, int $productID = 0, string $branchID = 'all', string $type = 'all', int $param = 0, int $moduleID = 0, string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { $this->commonAction($executionID); $uri = $this->app->getURI(true); $this->session->set('caseList', $uri, 'execution'); $this->session->set('bugList', $uri, 'execution'); $products = $this->product->getProducts($executionID); if(count($products) == 1) $productID = key($products); $execution = $this->execution->getByID($executionID); $productOption = array(); $branchOption = array(); if($execution->hasProduct) { list($productOption, $branchOption) = $this->executionZen->buildProductSwitcher($executionID, $productID, $products); } /* Load pager. */ $this->app->loadClass('pager', true); $pager = pager::init($recTotal, $recPerPage, $pageID); unset($this->config->testcase->dtable->fieldList['title']['nestedToggle']); if($productID && $products[$productID]->type == 'normal') unset($this->config->testcase->dtable->fieldList['branch']); /* Build the search form. */ $actionURL = $this->createLink('execution', 'testcase', "executionID=$executionID&productID=$productID&branchID=$branchID&type=bysearch&queryID=myQueryID&moduleID=0&orderBy=$orderBy"); $this->execution->buildCaseSearchForm($products, $param, $actionURL, $executionID); $this->executionZen->assignTestcaseVars($executionID, $productID, $branchID, $moduleID, $param, $orderBy, $type, $pager); $this->view->execution = $execution; $this->view->productOption = $productOption; $this->view->branchOption = $branchOption; $this->view->switcherParams = "executionID={$executionID}&productID={$productID}¤tMethod=testcase"; $this->view->switcherText = isset($products[$productID]) ? $products[$productID]->name : $this->lang->product->all; $this->view->switcherObjectID = $productID; if(empty($execution->hasProduct)) $this->config->excludeSwitcherList[] = 'execution-testcase'; $this->display(); } /** * 测试报告列表。 * List of test reports for the execution. * * @param int $executionID * @param string $objectType project|execution|product * @param string $extra * @param string $orderBy * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function testreport(int $executionID = 0, string $objectType = 'execution', string $extra = '', string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { echo $this->fetch('testreport', 'browse', "objectID=$executionID&objectType=$objectType&extra=$extra&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); } /** * 版本列表。 * Browse builds of a execution. * * @param int $executionID * @param string $type all|product|bysearch * @param int $param * @param string $orderBy * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function build(int $executionID = 0, string $type = 'all', int $param = 0, string $orderBy = 't1.date_desc,t1.id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { $execution = $this->commonAction($executionID); $executionID = (int)$execution->id; /* Get products' list. */ $products = $this->product->getProducts($executionID, 'all', '', false); /* Build the search form. */ $type = strtolower($type); $this->project->buildProjectBuildSearchForm($products, $type == 'bysearch' ? (int)$param : 0, $executionID, $param, 'execution'); /* Load pager. */ $this->app->loadClass('pager', true); $pager = pager::init($recTotal, $recPerPage, $pageID); /* Get builds. */ $this->loadModel('build'); if($type == 'bysearch') { $builds = $this->loadModel('build')->getExecutionBuildsBySearch($executionID, (int)$param, $pager); } else { $builds = $this->loadModel('build')->getExecutionBuilds($executionID, $type, (string)$param, $orderBy, $pager); } /* Set view data. */ $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->build; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->builds = $this->executionZen->processBuildListData($builds, $executionID); $this->view->product = $type == 'product' ? $param : ''; $this->view->products = $products; $this->view->system = $this->loadModel('system')->getPairs(); $this->view->type = $type; $this->view->param = $param; $this->view->orderBy = $orderBy; $this->view->pager = $pager; $this->display(); } /** * 测试单列表页面。 * Browse test tasks of execution. * * @param int $executionID * @param int $productID * @param string $orderBy * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function testtask(int $executionID = 0, int $productID = 0, string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { $this->loadModel('testtask'); $this->app->loadLang('testreport'); /* Save session. */ $this->session->set('testtaskList', $this->app->getURI(true), 'execution'); $this->session->set('buildList', $this->app->getURI(true), 'execution'); $execution = $this->commonAction($executionID); $executionID = $execution->id; $sort = str_replace('taskID', 'id', $orderBy); /* Load pager. */ $this->app->loadClass('pager', true); $pager = pager::init($recTotal, $recPerPage, $pageID); $tasks = $this->testtask->getExecutionTasks($executionID, $productID, 'execution', 'product_asc,' . $sort, $pager); $this->executionZen->assignTesttaskVars($tasks); $this->view->title = $this->executions[$executionID] . $this->lang->hyphen . $this->lang->testtask->common; $this->view->execution = $execution; $this->view->project = $this->loadModel('project')->getByID($execution->project); $this->view->executionID = $executionID; $this->view->productID = $productID; $this->view->executionName = $this->executions[$executionID]; $this->view->pager = $pager; $this->view->orderBy = $orderBy; $this->view->users = $this->loadModel('user')->getPairs('noclosed|noletter'); $this->view->products = $this->loadModel('product')->getProducts($executionID, 'all', '', false); $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable. $this->display(); } /** * 燃尽图。 * Browse burndown chart of a execution. * * @param int $executionID * @param string $type noweekend|withweekend * @param string $interval * @param string $burnBy left|estimate|storyPoint * @access public * @return void */ public function burn(int $executionID = 0, string $type = 'noweekend', string $interval = '', string $burnBy = 'left') { $execution = $this->commonAction($executionID); $executionID = $execution->id; /* Determine whether to display delay data. */ if(((strpos('closed,suspended', $execution->status) === false && helper::today() > $execution->end) || ($execution->status == 'closed' && substr($execution->closedDate, 0, 10) > $execution->end) || ($execution->status == 'suspended' && $execution->suspendedDate > $execution->end)) && strpos($type, 'delay') === false) $type .= ',withdelay'; /* Get burn date list and interval. */ $deadline = $execution->status == 'closed' ? substr($execution->closedDate, 0, 10) : $execution->suspendedDate; $deadline = strpos('closed,suspended', $execution->status) === false ? helper::today() : $deadline; $endDate = strpos($type, 'withdelay') !== false ? $deadline : $execution->end; list($dateList, $interval) = $this->execution->getDateList($execution->begin, $endDate, $type, $interval, 'Y-m-d', $execution->end); $executionEnd = strpos($type, 'withdelay') !== false ? $execution->end : ''; $burnBy = $this->cookie->burnBy ? $this->cookie->burnBy : $burnBy; $chartData = $this->execution->buildBurnData($executionID, $dateList, $burnBy, $executionEnd); $allDateList = date::getDateList($execution->begin, $endDate, 'Y-m-d', $type, $this->config->execution->weekend); $dayList = array_fill(1, (int)floor(count($allDateList) / $this->config->execution->maxBurnDay) + 5, ''); foreach($dayList as $key => $val) $dayList[$key] = $this->lang->execution->interval . ($key + 1) . $this->lang->day; unset($this->lang->TRActions); /* Shows the variables needed to burn page. */ $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->burn; $this->view->tabID = 'burn'; $this->view->burnBy = $burnBy; $this->view->executionID = $executionID; $this->view->executionName = $execution->name; $this->view->type = $type; $this->view->interval = $interval; $this->view->chartData = $chartData; $this->view->dayList = array('full' => $this->lang->execution->interval . '1' . $this->lang->day) + $dayList; $this->display(); } /** * 更新燃尽图数据。 * Compute burndown data. * * @param string $reload * @access public * @return void */ public function computeBurn(string $reload = 'no') { $burns = $this->execution->computeBurn(); if($reload == 'yes') return $this->send(array('load' => true, 'result' => 'success')); return print(json_encode($burns)); } /** * 累计流图。 * Cumulative flow diagram. * * @param int $executionID * @param string $type story|bug|task * @param string $withWeekend * @param string $begin * @param string $end * @access public * @return void */ public function cfd(int $executionID = 0, string $type = 'story', string $withWeekend = 'false', string $begin = '', string $end = '') { $execution = $this->commonAction($executionID); $executionID = $execution->id; $this->app->loadClass('date'); $minDate = !helper::isZeroDate($execution->openedDate) ? date('Y-m-d', strtotime($execution->openedDate)) : date('Y-m-d', strtotime($execution->begin)); $maxDate = !helper::isZeroDate($execution->closedDate) ? date('Y-m-d', strtotime($execution->closedDate)) : helper::today(); if($execution->lifetime == 'ops' or in_array($execution->attribute, array('request', 'review'))) $type = 'task'; if(!empty($_POST)) { $begin = htmlspecialchars($this->post->begin, ENT_QUOTES); $end = htmlspecialchars($this->post->end, ENT_QUOTES); $this->executionZen->checkCFDDate($begin, $end, $minDate, $maxDate); if(dao::isError()) return $this->sendError(dao::getError()); $this->execution->computeCFD($executionID); $this->execution->updateCFDData($executionID, $begin); return $this->send(array('result' => 'success', 'locate' => $this->createLink('execution', 'cfd', "executionID=$executionID&type=$type&withWeekend=$withWeekend&begin=" . helper::safe64Encode(urlencode($begin)) . "&end=" . helper::safe64Encode(urlencode($end))))); } if($begin && $end) { $begin = urldecode(helper::safe64Decode($begin)); $end = urldecode(helper::safe64Decode($end)); } else { list($begin, $end) = $this->execution->getBeginEnd4CFD($execution); } $dateList = date::getDateList($begin, $end, 'Y-m-d', $withWeekend == 'false'? 'noweekend' : ''); $chartData = $this->execution->buildCFDData($executionID, $dateList, $type); if(isset($chartData['line'])) $chartData['line'] = array_reverse($chartData['line']); $this->view->title = $this->lang->execution->CFD; $this->view->type = $type; $this->view->execution = $execution; $this->view->withWeekend = $withWeekend; $this->view->executionName = $execution->name; $this->view->executionID = $executionID; $this->view->chartData = $chartData; $this->view->features = $this->execution->getExecutionFeatures($execution); $this->view->begin = $begin; $this->view->end = $end; $this->view->minDate = $minDate; $this->view->maxDate = $maxDate; $this->display(); } /** * 计算累计流图数据。 * Compute cfd data. * * @param string $reload * @param int $executionID * @access public * @return void */ public function computeCFD(string $reload = 'no', int $executionID = 0) { $this->execution->computeCFD($executionID); if($reload == 'yes') return $this->sendSuccess(array('load' => true)); } /** * 修改燃尽图的首天工时 * Fix burn for first date. * * @param int $executionID * @access public * @return void */ public function fixFirst(int $executionID) { $execution = $this->execution->getByID($executionID); if($_POST) { if(!preg_match("/^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|[1-9]\d*$/", (string)$this->post->estimate) || $this->post->estimate <= 0) { dao::$errors['estimate'] = sprintf($this->lang->execution->errorFloat, $this->lang->execution->estimate); return $this->sendError(dao::getError()); } $burn = $this->execution->getBurnByExecution($executionID, $execution->begin, 0); $left = empty($burn) ? 0 : $burn->left; $withLeft = $this->post->withLeft ? $this->post->withLeft : 0; if($withLeft) $left = $this->post->estimate; $burnData = form::data($this->config->execution->form->fixfirst) ->add('task', 0) ->add('execution', $executionID) ->add('date', $execution->begin) ->add('left', $left) ->add('consumed', empty($burn) ? 0 : $burn->consumed) ->get(); if(!$burnData->left) $burnData->left = 0; if(is_numeric($burnData->estimate)) $this->execution->fixFirst($burnData); return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true)); } $this->view->firstBurn = $this->execution->getBurnByExecution($executionID, $execution->begin); $this->view->execution = $execution; $this->display(); } /** * 执行团队列表。 * Browse team of a execution. * * @param int $executionID * @access public * @return void */ public function team(int $executionID = 0) { $execution = $this->commonAction($executionID); $deptID = $this->app->user->admin ? 0 : $this->app->user->dept; $teamMembers = array(); foreach($this->view->teamMembers as $member) { $member->days = $member->days . $this->lang->execution->day; $member->hours = helper::formatHours($member->hours) . $this->lang->execution->workHour; $member->total = helper::formatHours($member->totalHours) . $this->lang->execution->workHour; $member->actions = array(); if(common::hasPriv('execution', 'unlinkMember', $member) && common::canModify('execution', $execution)) $member->actions = array('unlink'); $teamMembers[] = $member; } $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->team; $this->view->deptUsers = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id'); $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable. $this->view->recTotal = count($this->view->teamMembers); $this->view->teamMembers = $teamMembers; $this->display(); } /** * Create a execution. * * @param int $projectID * @param int $executionID * @param int $copyExecutionID * @param int $planID * @param string $confirm * @param int $productID * @param string $extra * * @access public * @return void */ public function create(int $projectID = 0, int $executionID = 0, int $copyExecutionID = 0, int $planID = 0, string $confirm = 'no', int $productID = 0, string $extra = '') { $project = empty($projectID) ? null : $this->loadModel('project')->fetchByID($projectID); if(!empty($project->isTpl)) dao::$filterTpl = 'never'; if($this->app->tab == 'doc') unset($this->lang->doc->menu->execution['subMenu']); if($this->app->tab == 'project') $this->project->setMenu($projectID); $extra = str_replace(array(',', ' '), array('&', ''), $extra); parse_str($extra, $output); $execution = $this->executionZen->initFieldsForCreate($projectID, $output); if($copyExecutionID) $this->executionZen->setFieldsByCopyExecution($execution, $copyExecutionID); $projectID = isset($execution->project) ? $execution->project : $projectID; if($executionID) return $this->executionZen->displayAfterCreated($projectID, $executionID, $planID, $confirm); $allProjects = $this->project->getPairsByModel('all', 'noclosed,multiple'); $this->loadModel('project')->checkAccess($projectID, $allProjects); if(empty($projectID)) $projectID = key($allProjects) ? key($allProjects) : 0; /* 过滤掉阶段已打基线的项目。*/ $executionList = $this->project->getExecutionList(array_keys($allProjects)); foreach($executionList as $executionInfo) { if(!empty($executionInfo->frozen)) unset($allProjects[$executionInfo->project]); } $project = empty($projectID) ? null : $this->loadModel('project')->fetchByID($projectID); $project ? $this->executionZen->correctExecutionCommonLang($project, $execution->type) : $project = null; $products = $this->executionZen->getLinkedProducts($copyExecutionID, $planID, $project); $this->executionZen->setLinkedBranches($products, $copyExecutionID, $planID, $project); if(!empty($_POST)) { /* Filter empty plans. */ if(isset($_POST['attribute']) and $_POST['attribute'] == 'review') unset($_POST['plans']); if(!empty($_POST['plans'])) { foreach($_POST['plans'] as $key => $planItem) $_POST['plans'][$key] = array_filter($_POST['plans'][$key]); $_POST['plans'] = array_filter($_POST['plans']); } $execution = $this->executionZen->buildExecutionForCreate(); if(!$execution) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $executionID = $this->execution->create($execution, isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array()); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->loadModel('action')->create($this->objectType, $executionID, 'opened', '', $project->hasProduct ? implode(',', $_POST['products']) : ''); if(!empty($projectID) and strpos(',kanban,agileplus,waterfallplus,ipd,', ",$project->model,") !== false and $execution->type == 'kanban') { $execution = $this->execution->fetchByID($executionID); $this->loadModel('kanban')->createRDKanban($execution); } $message = $this->executeHooks($executionID); if(empty($message)) $message = $this->lang->saveSuccess; if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $message, 'id' => $executionID)); $location = $this->executionZen->getAfterCreateLocation($projectID, $executionID, $project->model); return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location)); } list($this->view->pmUsers, $this->view->poUsers, $this->view->qdUsers, $this->view->rdUsers) = $this->executionZen->setUserMoreLink(); $this->executionZen->setCopyProjects($project); $isStage = isset($output['type']) && $output['type'] == 'stage'; if(!$isStage && !empty($this->view->execution) && $this->view->execution->type == 'stage') $isStage = true; if(!empty($project) && in_array($project->model, $this->config->project->waterfallList)) { if(in_array($project->model, $this->config->project->waterfallList) && !isset($output['type'])) $isStage = true; $parentStage = isset($output['parentStage']) ? $output['parentStage'] : 0; $type = isset($output['type']) ? $output['type'] : ''; if($parentStage) { $parent = $this->execution->fetchById((int)$parentStage); if($parent->attribute != 'mix') { $this->app->loadLang('stage'); foreach($this->lang->stage->typeList as $type => $label) { if($type != $parent->attribute) unset($this->lang->stage->typeList[$type]); } } } else { unset($this->lang->execution->typeList['kanban']); unset($this->lang->execution->typeList['sprint']); } $this->view->parentStage = $parentStage; $this->view->parentStages = $this->loadModel('programplan')->getParentStageList($projectID, 0, 0, 'withparent|noclosed|' . ($isStage ? 'stage' : 'notstage')); } $this->view->title = $this->app->tab == 'execution' ? $this->lang->execution->createExec : $this->lang->execution->create; $this->view->gobackLink = (isset($output['from']) and $output['from'] == 'global') ? $this->createLink('execution', 'all') : ''; $this->view->allProducts = array_filter($this->executionZen->getAllProductsForCreate($project)); $this->view->allProjects = $allProjects; $this->view->multiBranchProducts = $this->loadModel('product')->getMultiBranchPairs(); $this->view->products = $products; $this->view->teams = $this->execution->getCanCopyObjects((int)$projectID); $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed'); $this->view->copyExecutionID = $copyExecutionID; $this->view->productID = $productID; $this->view->projectID = $projectID; $this->view->from = $this->app->tab; $this->view->isStage = $isStage; $this->view->isKanban = isset($output['type']) && $output['type'] == 'kanban'; $this->view->project = $project; $this->view->planID = $planID; $this->display(); } /** * 编辑一个执行。 * Edit a execution. * * @param int $executionID * @param string $action * @param string $extra * @param string $newPlans * @param string $confirm * @access public * @return void */ public function edit(int $executionID, string $action = 'edit', string $extra = '', string $newPlans = '', string $confirm = 'no') { /* Update linked plans if confirmed and new plans added. */ $this->executionZen->updateLinkedPlans($executionID, $newPlans, $confirm); $this->loadModel('product'); $this->loadModel('programplan'); $this->loadModel('productplan'); $this->app->loadLang('program'); $this->app->loadLang('stage'); $execution = $this->execution->getById($executionID); $project = $this->project->getById($execution->project); $branches = $this->project->getBranchesByProject($executionID); $linkedProductIdList = empty($branches) ? '' : array_keys($branches); /* Set menu. */ $this->execution->setMenu($executionID); if(!empty($_POST)) { $oldProducts = $this->product->getProducts($executionID, 'all', '', true, $linkedProductIdList); $oldPlans = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->andWhere('plan')->ne(0)->fetchPairs('plan'); $oldExecution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch(); /* Get the data from the post. */ $formData = form::data(null, $executionID) ->add('id', $executionID) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setIF($this->post->heightType == 'auto', 'displayCards', 0) ->setIF(helper::isZeroDate($this->post->begin), 'begin', '') ->setIF(helper::isZeroDate($this->post->end), 'end', '') ->setIF($this->post->status == 'closed' && $oldExecution->status != 'closed', 'closedDate', helper::now()) ->setIF($this->post->status == 'closed' && $oldExecution->status != 'closed', 'realEnd', helper::today()) ->setIF($this->post->status == 'suspended' && $oldExecution->status != 'suspended', 'suspendedDate', helper::today()) ->setIF($oldExecution->type == 'stage', 'project', $oldExecution->project) ->setDefault('project', $oldExecution->project) ->setDefault('team', $this->post->name) ->setDefault('branch', $this->post->branch) ->setDefault('attribute', $oldExecution->attribute) ->get(); $changes = $this->execution->update($executionID, $formData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->execution->updateProducts($executionID, $formData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->loadModel('action'); if($action == 'undelete') { $this->dao->update(TABLE_EXECUTION)->set('deleted')->eq('0')->where('id')->eq($executionID)->exec(); $this->dao->update(TABLE_ACTION)->set('extra')->eq(actionModel::BE_UNDELETED)->where('id')->eq($extra)->exec(); $this->action->create($this->objectType, $executionID, 'undeleted'); } $oldProducts = array_keys($oldProducts); $newProducts = $this->product->getProducts($executionID, 'all', '', true, $linkedProductIdList); $newProducts = array_keys($newProducts); $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts)); $products = $diffProducts ? implode(',', $newProducts) : ''; if($changes or $diffProducts) { $actionID = $this->action->create($this->objectType, $executionID, 'edited', '', $products); $this->action->logHistory($actionID, $changes); } if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->programplan->computeProgress($executionID, 'edit'); /* Redirect to confirm page if the execution can link plan stories. */ $this->executionZen->checkLinkPlan($executionID, $oldPlans); $message = $this->executeHooks($executionID); if($message) $this->lang->saveSuccess = $message; if($formData->status == 'doing') $this->loadModel('common')->syncPPEStatus($executionID); /* If link from no head then reload. */ if(isInModal()) { $kanbanLoad = array('selector' => '#main>*'); return $this->sendSuccess(array('closeModal' => true, 'callback' => $execution->type == 'kanban' ? 'loadCurrentPage(' . json_encode($kanbanLoad) . ');' : 'loadCurrentPage()')); } return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => inlink('view', "executionID=$executionID"))); } $executions = $this->executions; unset($executions[$executionID]); /* Remove current execution from the executions. */ /* Get linked objects of this execution. */ $linkedObjects = $this->executionZen->getLinkedObjects($execution); list($pmUsers, $poUsers, $qdUsers, $rdUsers) = $this->executionZen->setUserMoreLink($execution); if(!$project->hasProduct) $this->lang->execution->PO = $this->lang->common->story . $this->lang->execution->owner; if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) { $parentStage = $this->project->getByID($execution->parent, 'stage'); $this->view->enableOptionalAttr = (empty($parentStage) or (!empty($parentStage) and $parentStage->attribute == 'mix')); } if($this->app->tab == 'project' and $project->model == 'waterfallplus') { $productID = $this->product->getProductIDByProject($executionID); $parentStageList = $this->programplan->getParentStageList($execution->project, $executionID, $productID); unset($parentStageList[0]); } if(empty($project->hasProduct)) { $linkedProduct = current($linkedObjects->linkedProducts); $linkedPlans = !empty($linkedProduct->plans) ? array_values($linkedProduct->plans) : array(); } $this->view->title = $this->lang->execution->edit . $this->lang->hyphen . $execution->name; $this->view->executions = $executions; $this->view->execution = $execution; $this->view->project = $project; $this->view->poUsers = $poUsers; $this->view->pmUsers = $pmUsers; $this->view->qdUsers = $qdUsers; $this->view->rdUsers = $rdUsers; $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed'); $this->view->groups = $this->loadModel('group')->getPairs(); $this->view->branches = $branches; $this->view->allProducts = $linkedObjects->allProducts; $this->view->multiBranchProducts = $this->product->getMultiBranchPairs(); $this->view->linkedProducts = $linkedObjects->linkedProducts; $this->view->linkedBranches = $linkedObjects->linkedBranches; $this->view->linkedStoryIDList = $linkedObjects->linkedStoryIDList; $this->view->unmodifiableProducts = $linkedObjects->unmodifiableProducts; $this->view->unmodifiableBranches = $linkedObjects->unmodifiableBranches; $this->view->productPlans = $linkedObjects->productPlans; $this->view->productPlan = $linkedObjects->productPlan; $this->view->currentPlan = empty($project->hasProduct) ? $linkedPlans : $linkedObjects->currentPlan; $this->view->branchGroups = $this->execution->getBranchByProduct(array_keys($linkedObjects->linkedProducts), $execution->project, 'noclosed', $linkedObjects->linkedBranchList); $this->view->teamMembers = $this->execution->getTeamMembers($executionID); $this->view->allProjects = $this->project->getPairsByModel($project->model, 'noclosed|multiple', $project->id); $this->view->parentStageList = isset($parentStageList) ? $parentStageList : array(); $this->view->isStage = isset($project->model) && in_array($project->model, array('waterfall', 'waterfallplus')); $this->view->unclosedTasks = $this->loadModel('task')->getUnclosedTasksByExecution($executionID); $this->display(); } /** * 批量编辑多个执行。 * Batch edit the executions. * * @param int $executionID * @access public * @return void */ public function batchEdit(int $executionID = 0) { $this->app->loadLang('stage'); $this->app->loadLang('programplan'); if($this->app->tab == 'project') { $projectID = $this->session->project; $project = $this->project->getById($projectID); $this->project->setMenu($projectID); $this->view->project = $project; if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->lang->execution->common = $this->lang->execution->stage; if($project->model == 'ipd') $this->config->execution->list->customBatchEditFields = 'days,team,desc,PO,QD,PM,RD'; } else { $this->config->excludeDropmenuList[] = 'execution-batchedit'; } if($this->post->name) { $postData = fixer::input('post')->get(); $allChanges = $this->execution->batchUpdate($postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->executeHooks(key($this->post->id)); if(!empty($allChanges)) { foreach($allChanges as $executionID => $changes) { if(empty($changes)) continue; $actionID = $this->loadModel('action')->create($this->objectType, $executionID, 'Edited'); $this->action->logHistory($actionID, $changes); } } return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->session->executionList)); } $projectModel = $this->app->tab == 'project' ? $project->model : 'all'; $allProjects = $this->project->getPairsByModel($projectModel, 'noclosed', isset($projectID) ? $projectID : 0); if(!$this->post->executionIDList) { /* Use a fallback link to locate in case session has no related data. */ $locateLink = !empty($this->session->executionList) ? $this->session->executionList : $this->createLink('execution', 'all'); return $this->locate($locateLink); } $executionIDList = $this->post->executionIDList; $executions = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->in($executionIDList)->fetchAll('id', false); $relatedProjects = $this->dao->select('id,project')->from(TABLE_PROJECT)->where('id')->in($executionIDList)->fetchPairs(); /* 获取执行所属的项目列表。*/ $projects = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->in($relatedProjects)->fetchAll('id'); /* 获取执行所属的项目列表中每个项目的项目信息。*/ $frozenStages = ''; foreach($executions as $key => $execution) { if(!empty($execution->frozen)) { $frozenStages .= "#{$execution->id} "; unset($executions[$key]); } } if(empty($executions) && !empty($frozenStages)) return $this->send(array('result' => 'fail', 'load' => array('alert' => sprintf($this->lang->execution->frozenTip, $frozenStages), 'load' => true))); list($pmUsers, $poUsers, $qdUsers, $rdUsers) = $this->executionZen->setUserMoreLink($executions); /* Set custom fields. */ foreach(explode(',', $this->config->execution->list->customBatchEditFields) as $field) { $fieldTitle = $this->app->tab == 'execution' ? str_replace($this->lang->executionCommon, $this->lang->execution->common, $this->lang->execution->$field) : $this->lang->execution->$field; if($field == 'lifetime') $fieldTitle = $this->app->tab == 'execution' ? $this->lang->execution->execType : $this->lang->execution->type; if($field == 'team') $fieldTitle = $this->lang->execution->teamName; $customFields[$field] = $fieldTitle; } $parentIdList = array(); foreach($executions as $execution) { if($execution->parent == 0) continue; $parentIdList[] = $execution->parent; } $this->view->customFields = $customFields; $this->view->showFields = $this->config->execution->custom->batchEditFields; $this->view->title = $this->lang->execution->batchEdit; $this->view->executions = $executions; $this->view->allProjects = $allProjects; $this->view->projects = $projects; $this->view->pmUsers = $pmUsers; $this->view->poUsers = $poUsers; $this->view->qdUsers = $qdUsers; $this->view->rdUsers = $rdUsers; $this->view->from = $this->app->tab; $this->view->parents = $this->execution->getByIdList($parentIdList); $this->view->frozenStages = $frozenStages; $this->display(); } /** * 批量设置执行状态。 * Batch change status. * * @param string $status * @param int $projectID * @access public * @return void */ public function batchChangeStatus(string $status, int $projectID = 0) { if($_SERVER['REQUEST_METHOD'] !== 'POST') return $this->sendError($this->lang->error->unsupportedReq); $executionIdList = $this->post->executionIDList; if(is_string($executionIdList)) $executionIdList = explode(',', $executionIdList); $message = $this->execution->batchChangeStatus($executionIdList, $status); if(empty($message['byChild']) && empty($message['byDeliverable'])) return $this->sendSuccess(array('load' => true)); $alertMsg = ''; if($status == 'wait') { $project = $this->loadModel('project')->getById($projectID); /* In execution-all list or waterfall, waterfallplus project's execution list. */ if(empty($project) or (!empty($project) and strpos($project->model, 'waterfall') !== false)) { $executionLang = (empty($project) or (!empty($project) and $project->model == 'waterfallplus')) ? $this->lang->execution->common : $this->lang->stage->common; $alertMsg = sprintf($this->lang->execution->hasStartedTaskOrSubStage, $executionLang, $message['byChild']); } if(!empty($project) and strpos('agileplus,scrum', $project->model) !== false) { $executionLang = $project->model == 'scrum' ? $this->lang->executionCommon : $this->lang->execution->common; $alertMsg = sprintf($this->lang->execution->hasStartedTask, $executionLang, $message['byChild']); } } if($status == 'suspended') $alertMsg = sprintf($this->lang->execution->hasSuspendedOrClosedChildren, $message['byChild']); if($status == 'closed') { if(!empty($message['byChild'])) { $alertMsg .= sprintf($this->lang->execution->hasNotClosedChildren, $message['byChild']); } elseif(!empty($message['byDeliverable'])) { $alertMsg .= sprintf($this->lang->execution->cannotCloseByDeliverable, $message['byDeliverable']); } } return $this->send(array('load' => array('alert' => $alertMsg), 'result' => 'success')); } /** * 开始一个执行。 * Start the execution. * * @param int $executionID * @param string $from * @access public * @return void */ public function start(int $executionID, string $from = 'execution') { $execution = $this->commonAction($executionID); $executionID = $execution->id; if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; $this->extendRequireFields($executionID); if(!empty($_POST)) { $postData = fixer::input('post') ->add('id', $executionID) ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->stripTags($this->config->execution->editor->start['id'], $this->config->allowedTags) ->get(); $this->execution->start($executionID, $postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $project = $this->loadModel('project')->getById($execution->project); if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->loadModel('programplan')->computeProgress($executionID, 'start'); $this->loadModel('common')->syncPPEStatus($executionID); $this->executeHooks($executionID); $response['closeModal'] = true; $response['load'] = true; if($from == 'kanban') $response['callback'] = "changeStatus('doing')"; return $this->sendSuccess($response); } $this->view->title = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->start; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList($this->objectType, $executionID); $this->display(); } /** * 延期一个迭代。 * Delay the execution. * * @param int $executionID * @param string $from * @access public * @return void */ public function putoff(int $executionID, string $from = 'execution') { $execution = $this->commonAction($executionID); $executionID = $execution->id; $this->extendRequireFields($executionID); if(!empty($_POST)) { $postData = fixer::input('post') ->add('id', $executionID) ->stripTags($this->config->execution->editor->putoff['id'], $this->config->allowedTags) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->get(); $this->execution->putoff($executionID, $postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->executeHooks($executionID); $response['closeModal'] = true; $response['load'] = true; if($from == 'kanban') $response['callback'] = "changeStatus('doing')"; return $this->sendSuccess($response); } $this->view->title = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->putoff; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList($this->objectType, $executionID); $this->display(); } /** * 挂起一个执行。 * Suspend a execution. * * @param int $executionID * @param string $from * @access public * @return void */ public function suspend(int $executionID, string $from = 'execution') { $execution = $this->commonAction($executionID); $executionID = $execution->id; if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; $this->extendRequireFields($executionID); if(!empty($_POST)) { $postData = fixer::input('post') ->add('id', $executionID) ->setDefault('status', 'suspended') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setDefault('suspendedDate', helper::today()) ->stripTags($this->config->execution->editor->suspend['id'], $this->config->allowedTags) ->get(); $this->execution->computeBurn($executionID); $this->execution->suspend($executionID, $postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $project = $this->loadModel('project')->getById($execution->project); if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->loadModel('programplan')->computeProgress($executionID, 'suspend'); $this->executeHooks($executionID); $response['closeModal'] = true; $response['load'] = true; if($from == 'kanban') $response['callback'] = "changeStatus('suspended')"; return $this->sendSuccess($response); } $this->view->title = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->suspend; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList($this->objectType, $executionID); $this->display(); } /** * 激活一个执行。 * Activate a execution. * * @param int $executionID * @param string $from * @access public * @return void */ public function activate(int $executionID, string $from = 'execution') { $execution = $this->commonAction($executionID); $executionID = $execution->id; if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; $this->extendRequireFields($executionID); if(!empty($_POST)) { $postData = fixer::input('post') ->add('id', $executionID) ->setDefault('realEnd', null) ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setDefault('closedBy', '') ->setDefault('closedDate', null) ->stripTags($this->config->execution->editor->activate['id'], $this->config->allowedTags) ->get(); $this->execution->activate($executionID, $postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $project = $this->loadModel('project')->getById($execution->project); if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->loadModel('programplan')->computeProgress($executionID, 'activate'); $this->executeHooks($executionID); $response['closeModal'] = true; $response['load'] = true; if($from == 'kanban') $response['callback'] = "changeStatus('doing')"; return $this->sendSuccess($response); } $newBegin = date('Y-m-d'); $dateDiff = helper::diffDate($newBegin, $execution->begin); $newEnd = date('Y-m-d', strtotime($execution->end) + $dateDiff * 24 * 3600); $this->view->title = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->activate; $this->view->execution = $execution; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList($this->objectType, $executionID); $this->view->newBegin = $newBegin; $this->view->newEnd = $newEnd; $this->display(); } /** * 关闭迭代。 * Close execution. * * @param int $executionID * @param string $from * @access public * @return void */ public function close(int $executionID, string $from = 'execution') { $execution = $this->commonAction($executionID); $executionID = $execution->id; if($execution->type == 'kanban') $this->lang->executionCommon = $this->lang->execution->kanban; $this->extendRequireFields($executionID); if(!empty($_POST)) { $now = helper::now(); $postData = fixer::input('post') ->add('id', $executionID) ->setDefault('status', 'closed') ->setDefault('closedBy', $this->app->user->account) ->setDefault('closedDate', $now) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->stripTags($this->config->execution->editor->close['id'], $this->config->allowedTags) ->get(); $this->execution->computeBurn($executionID); $this->execution->close($executionID, $postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $project = $this->loadModel('project')->getById($execution->project); if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) { $result = $this->loadModel('programplan')->computeProgress($executionID, 'close'); if(is_array($result)) return $this->send($result); } $this->executeHooks($executionID); $response['closeModal'] = true; $response['load'] = true; if($from == 'kanban') $response['callback'] = "changeStatus('closed')"; return $this->sendSuccess($response); } $this->view->title = $this->view->execution->name . $this->lang->hyphen .$this->lang->execution->close; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->from = $from; $this->view->actions = $this->loadModel('action')->getList($this->objectType, $executionID); $this->view->unclosedTasks = $this->loadModel('task')->getUnclosedTasksByExecution($executionID); $this->display(); } /** * 执行详情。 * View a execution. * * @param int $executionID * @access public * @return void */ public function view(int $executionID) { /* Check execution permission. */ $accessExecutionID = $this->execution->checkAccess((int)$executionID, $this->executions); if($this->app->apiVersion == 'v2' && $accessExecutionID != $executionID) return $this->sendError($this->lang->execution->accessDenied); $executionID = $accessExecutionID; $execution = $this->execution->getByID($executionID, true); $type = $this->config->vision == 'lite' ? 'kanban' : 'stage,sprint,kanban'; if($execution->type == 'stage') { $childExecutions = $this->execution->getChildExecutions($executionID); if(!empty($childExecutions)) return $this->app->control->sendError($this->lang->execution->errorParentExecution, helper::createLink('execution', 'all')); } if(empty($execution) || strpos($type, $execution->type) === false) return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->config->vision == 'lite' ? $this->createLink('project', 'index') : $this->createLink('execution', 'all')))); if($execution->type == 'kanban' and defined('RUN_MODE') and RUN_MODE == 'api') return print($this->fetch('execution', 'kanban', "executionID=$executionID")); /* Load lang and set session. */ $this->app->loadLang('program'); $this->app->loadLang('bug'); $this->app->session->set('teamList', $this->app->getURI(true), 'execution'); $this->execution->setMenu($execution->id); /* Execution not found to prevent searching for. */ if(!isset($this->executions[$execution->id])) $this->executions = $this->execution->getPairs($execution->project, 'all', 'nocode'); if(!$this->loadModel('common')->checkPrivByObject('execution', $executionID)) return $this->execution->accessDenied(); $execution->projectInfo = $this->loadModel('project')->getByID($execution->project); $programList = $execution->projectInfo ? array_filter(explode(',', $execution->projectInfo->path)) : array(); array_pop($programList); if(empty($execution->projectInfo->hasProduct)) $this->lang->execution->PO = $this->lang->common->story . $this->lang->execution->owner; $this->executionZen->assignViewVars($executionID); $this->view->title = $this->lang->execution->view; $this->view->execution = $execution; $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable. $this->view->type = $type; $this->view->features = $this->execution->getExecutionFeatures($execution); $this->view->project = $execution->projectInfo; $this->view->programList = $this->loadModel('program')->getPairsByList($programList); $this->display(); } /** * 研发看板。 * Research and development Kanban. * * @param int $executionID * @param string $browseType all|story|bug|task * @param string $orderBy * @param string $groupBy default|pri|category|module|source|assignedTo|type|story|severity * @access public * @return void */ public function kanban(int $executionID, string $browseType = 'all', string $orderBy = 'id_asc', string $groupBy = 'default') { $this->app->loadLang('bug'); $this->app->loadLang('epic'); $this->app->loadLang('requirement'); $this->app->loadLang('kanban'); if($this->config->vision != 'lite') $this->lang->execution->menu = new stdclass(); $execution = $this->execution->fetchByID($executionID); if(!$execution || $execution->deleted) return $this->sendError($this->lang->notFound, $this->createLink('execution', 'all')); $execution = $this->commonAction($executionID); if($execution->type != 'kanban') return $this->locate(inlink('view', "executionID={$execution->id}")); /* Save the recently five executions visited in the cookie. */ $this->executionZen->setRecentExecutions($execution->id); /* Set Session. */ if(empty($groupBy)) $groupBy = 'default'; $this->session->set('execGroupBy', $groupBy); $this->session->set('storyList', $this->app->getURI(true), 'execution'); $this->session->set('rdSearchValue', ''); /* Get kanban data and set actions. */ $executionActions = array(); foreach($this->config->execution->statusActions as $action) { if($this->execution->isClickable($execution, $action)) $executionActions[] = $action; } /* Set lane type. */ $features = $this->execution->getExecutionFeatures($execution); if(!$features['story'] and !$features['qa']) $browseType = 'task'; if(!$features['story']) unset($this->lang->kanban->group->task['story']); $this->session->set('execLaneType', $browseType); $taskToOpen = $this->cookie->taskToOpen ? $this->cookie->taskToOpen : 0; helper::setcookie('taskToOpen', 0, 0); $this->executionZen->assignKanbanVars($executionID); $this->view->title = $this->lang->kanban->view; $this->view->execution = $execution; $this->view->executionList = $this->loadModel('project')->getExecutionList(array($execution->project)); $this->view->executionID = $executionID; $this->view->kanbanList = $this->loadModel('kanban')->getRDKanban($executionID, $browseType, $orderBy, 0, $groupBy); $this->view->browseType = $browseType; $this->view->orderBy = $orderBy; $this->view->groupBy = $groupBy; $this->view->projectID = $execution->project; $this->view->project = $this->loadModel('project')->getByID($execution->project); $this->view->features = $features; $this->view->executionActions = $executionActions; $this->view->kanban = $this->lang->execution->kanban; $this->view->taskToOpen = $taskToOpen; $this->display(); } /** * 任务看板 * Task kanban. * * @param int $executionID * @param string $browseType story|bug|task|all * @param string $orderBy * @param string $groupBy default|pri|category|module|source|assignedTo|type|story|severity * @access public * @return void */ public function taskKanban(int $executionID, string $browseType = 'all', string $orderBy = 'order_asc', string $groupBy = '') { if(!$this->loadModel('common')->checkPrivByObject('execution', $executionID)) return $this->execution->accessDenied(); /* Load model and language. */ $this->app->loadLang('epic'); $this->app->loadLang('requirement'); $this->app->loadLang('task'); $this->app->loadLang('bug'); $this->loadModel('kanban'); if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) helper::header('X-UA-Compatible', 'IE=EmulateIE7'); // Compatibility IE8. $this->execution->setMenu($executionID); $execution = $this->execution->getByID($executionID); $features = $this->execution->getExecutionFeatures($execution); if($execution->lifetime == 'ops') { $browseType = $browseType == 'all' ? 'task' : $browseType; unset($this->lang->kanban->type['all']); unset($this->lang->kanban->type['story']); unset($this->lang->kanban->type['bug']); unset($this->lang->kanban->type['parentStory']); } elseif(!$features['story']) { $browseType = 'task'; unset($this->lang->kanban->group->task['story'], $this->lang->kanban->type['story'], $this->lang->kanban->type['parentStory']); } if(!$features['qa']) unset($this->lang->kanban->type['bug']); /* Save to session. */ $uri = $this->app->getURI(true); $groupBy = empty($groupBy) ? 'default' : $groupBy; $this->session->set('taskList', $uri, 'execution'); $this->session->set('storyList', $uri, 'storyList'); $this->session->set('bugList', $uri, 'qa'); $this->session->set('taskSearchValue', ''); $this->session->set('execGroupBy', $groupBy); $this->session->set('execLaneType', $browseType); /* Get kanban data. */ $orderBy = $groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy]) ? 'id_asc' : $orderBy; $this->kanban->createLaneIfNotExist($execution); list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); /* Show lanes of the attribute: no story and bug in request, no bug in design. */ if(!isset($this->lang->execution->menu->story)) unset($kanbanGroup['story']); if(!isset($this->lang->execution->menu->qa)) unset($kanbanGroup['bug']); $this->executionZen->assignTaskKanbanVars($execution); if($this->app->tab == 'project') $this->view->projectID = $execution->project; $this->view->storyOrder = $orderBy; $this->view->orderBy = 'id_asc'; $this->view->executionID = $executionID; $this->view->browseType = $browseType; $this->view->features = $features; $this->view->kanbanGroup = $kanbanGroup; $this->view->links = $links; $this->view->groupBy = $groupBy; $this->display(); } /** * 执行看板。 * Execution kanban. * * @access public * @return void */ public function executionKanban() { $projects = $this->loadModel('project')->getPairsByProgram(0, 'noclosed'); $executions = $this->execution->getStatData(0, 'all', 0, 0, false, 'withchild', 'id_desc'); foreach($executions as $execution) { $execution->name = htmlspecialchars_decode($execution->name); $execution->team = htmlspecialchars_decode($execution->team); } list($projectCount, $statusCount, $myExecutions, $kanbanGroup) = $this->executionZen->buildExecutionKanbanData(array_keys($projects), $executions); $kanbanGroup = empty($myExecutions) ? $kanbanGroup : array($myExecutions) + $kanbanGroup; $kanbanList = array(); $lanes = array(); $items = array(); $columnCards = array(); $columns = array(); foreach($kanbanGroup as $laneKey => $laneData) { $laneTitle = zget($projects, $laneKey, $this->lang->execution->myExecutions); $laneKey ++; $lanes[] = array('name' => $laneKey, 'title' => $laneTitle); foreach(array('wait', 'doing', 'suspended', 'closed') as $columnKey) { $cardList = !empty($laneData[$columnKey]) ? $laneData[$columnKey] : array(); foreach($cardList as $card) { $items[$laneKey][$columnKey][] = array('id' => $card->id, 'name' => $card->id, 'title' => $card->name, 'status' => $card->status, 'delay' => !empty($card->delay) ? $card->delay : 0, 'progress' => $card->progress); if(!isset($columnCards[$columnKey])) $columnCards[$columnKey] = 0; $columnCards[$columnKey] ++; } } } foreach(array('wait', 'doing', 'suspended', 'closed') as $columnKey) { $columns[] = array('name' => $columnKey, 'title' => $this->lang->execution->kanbanColType[$columnKey], 'cards' => !empty($columnCards[$columnKey]) ? $columnCards[$columnKey] : 0); } if(!empty($kanbanGroup)) { $groupData['key'] = 'executionKanban'; $groupData['data']['lanes'] = $lanes; $groupData['data']['cols'] = $columns; $groupData['data']['items'] = $items; $kanbanList[] = array('items' => array($groupData), 'key' => 'executionKanban', 'heading' => array('title' => $this->lang->execution->executionKanban)); } $this->view->title = $this->lang->execution->executionKanban; $this->view->kanbanList = $kanbanList; $this->display(); } /** * 设置看板配置。 * Set Kanban. * * @param int $executionID * @access public * @return void */ public function setKanban(int $executionID) { $execution = $this->execution->getByID($executionID); if($_POST) { $executionData = form::data($this->config->execution->form->setkanban) ->setIF($this->post->heightType == 'auto', 'displayCards', 0) ->setDefault('minColWidth', $execution->minColWidth) ->remove('heightType') ->get(); if(!isset($_POST['heightType']) || $this->post->heightType != 'custom' || $this->loadModel('kanban')->checkDisplayCards($executionData->displayCards)) { $this->execution->setKanban($executionID, $executionData); } if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); return $this->sendSuccess(array('load' => true, 'closeModal' => true)); } $this->view->title = $this->lang->execution->setKanban; $this->view->execution = $execution; $this->view->laneCount = $this->loadModel('kanban')->getLaneCount($executionID, $execution->type); $this->view->heightType = $execution->displayCards > 2 ? 'custom' : 'auto'; $this->view->displayCards = $execution->displayCards ? $execution->displayCards : ''; $this->display(); } /** * 用树状图的形式查看迭代/执行的视图。 * Show the view of the execution in a tree mode. * * @param int $executionID * @param string $type * @access public * @return void */ public function tree(int $executionID, string $type = 'task') { $this->execution->setMenu($executionID); /* Save this URI to session for locate it back. */ $uri = $this->app->getURI(true); $this->app->session->set('taskList', $uri, 'execution'); $this->app->session->set('storyList', $uri, 'execution'); $this->app->session->set('executionList', $uri, 'execution'); $this->app->session->set('caseList', $uri, 'qa'); $this->app->session->set('bugList', $uri, 'qa'); $tree = $this->execution->getTree($executionID); if($type === 'json') return print(helper::jsonEncode4Parse($tree, JSON_HEX_QUOT | JSON_HEX_APOS)); $execution = $this->execution->getById($executionID); $project = $this->project->getById($execution->project); if($execution->lifetime == 'ops') unset($this->lang->execution->treeLevel['story']); if($this->app->tab == 'project') $this->view->projectID = $execution->project; $gradeGroup = $this->loadModel('story')->getGradeGroup(); if(!isset($_SESSION['limitedExecutions'])) $this->execution->getLimitedExecution(); $this->view->title = $this->lang->execution->tree; $this->view->execution = $execution; $this->view->executionID = $executionID; $this->view->level = $type; $this->view->tree = $this->execution->buildTree($tree, (bool)$project->hasProduct, $gradeGroup); $this->view->features = $this->execution->getExecutionFeatures($execution); $this->view->isLimited = !$this->app->user->admin && strpos(",{$this->session->limitedExecutions},", ",{$executionID},") !== false; $this->display(); } /** * 看板看板。 * Print kanban. * * @param int $executionID * @param string $orderBy * @access public * @return void */ public function printKanban(int $executionID, string $orderBy = 'id_asc') { $this->view->title = $this->lang->execution->printKanban; $this->execution->setMenu($executionID); if($_POST) { /* Get stories information. */ $order = 1; $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, $orderBy); $storySpecs = $this->story->getStorySpecs(array_keys($stories)); foreach($stories as $story) $story->order = $order ++; /* Get printed kanban data. */ list($dataList, $users) = $this->executionZen->getPrintKanbanData($executionID, $stories); $originalDataList = $dataList; if($this->post->content == 'increment') $dateList = $this->executionZen->processPrintKanbanData($executionID, $dataList); /* Close the page when there is no data. */ $hasData = false; foreach($dataList as $data) { if(empty($data)) continue; $hasData = true; break; } if(!$hasData) return $this->sendSuccess(array('message' => $this->lang->execution->noPrintData, 'load' => true)); $this->execution->saveKanbanData($executionID, $originalDataList); /* Get date list. */ if($this->post->content == 'all') { $executionInfo = $this->execution->getByID($executionID); list($dateList) = $this->execution->getDateList($executionInfo->begin, $executionInfo->end, 'noweekend'); } $this->view->hasBurn = $this->post->content == 'all'; $this->view->datas = $dataList; $this->view->chartData = $this->post->content == 'all' ? $this->execution->buildBurnData($executionID, $dateList) : array(); $this->view->storySpecs = $storySpecs; $this->view->realnames = $this->loadModel('user')->getRealNameAndEmails($users); $this->view->executionID = $executionID; return $this->display(); } $this->view->executionID = $executionID; $this->display(); } /** * 需求看板。 * Story kanban. * * @param int $executionID * @access public * @return void */ public function storyKanban(int $executionID) { $this->app->loadLang('kanban'); $this->session->set('executionStoryBrowseType', 'unclosed'); $execution = $this->commonAction($executionID); $executionID = $execution->id; $stories = $this->loadModel('story')->getExecutionStories($executionID); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false); $stories = array_filter($stories, function($story) {return in_array($story->stage, $this->config->execution->storyKanbanCols);}); foreach($stories as $story) unset($story->type); /* Get execution's product. */ $productID = 0; $productPairs = $this->loadModel('product')->getProductPairsByProject($executionID); if($productPairs) { $productID = key($productPairs); } else { return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$executionID")); } $this->app->session->set('executionStoryList', $this->app->getURI(true), 'execution'); $this->view->title = $this->lang->execution->storyKanban; $this->view->kanbanData = $this->story->getKanbanGroupData($stories); $this->view->realnames = $this->loadModel('user')->getPairs('noletter'); $this->view->executionID = $executionID; $this->view->execution = $execution; $this->view->productID = $productID; $this->view->total = count($stories); $this->view->product = $this->product->getByID($productID); $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable. $this->display(); } /** * 删除一个执行。 * Delete a execution. * * @param int $executionID * @param string $confirm yes|no * @access public * @return void */ public function delete(int $executionID, string $confirm = 'no') { if($confirm == 'no') { /* Get the number of unfinished tasks and unresolved bugs. */ $unfinishedTasks = $this->loadModel('task')->getUnfinishTasks($executionID); $unresolvedBugs = $this->loadModel('bug')->getActiveBugs(0, '', (string)$executionID, array()); $unfinishedTaskCount = count($unfinishedTasks); $unresolvedBugCount = count($unresolvedBugs); /* Set prompt information. */ $tips = ''; if($unfinishedTaskCount) $tips = sprintf($this->lang->execution->unfinishedTask, $unfinishedTaskCount); if($unresolvedBugCount) $tips .= sprintf($this->lang->execution->unresolvedBug, $unresolvedBugCount); if($tips) $tips = $this->lang->execution->unfinishedExecution . $tips; $execution = $this->execution->fetchByID($executionID); if($execution->type == 'stage') { if($tips) $tips = str_replace($this->lang->executionCommon, $this->lang->project->stage, $tips); $this->lang->execution->confirmDelete = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->execution->confirmDelete); } elseif($execution->type == 'kanban') { global $lang; $lang->executionCommon = $lang->execution->kanban; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; } $tips = $tips . sprintf($this->lang->execution->confirmDelete, $execution->name); return $this->send(array('callback' => "confirmDeleteExecution({$executionID}, \"{$tips}\")")); } else { /* Delete an execution and update the information. */ $execution = $this->execution->fetchByID($executionID); $this->execution->delete(TABLE_EXECUTION, $executionID); $this->execution->updateUserView($executionID); $project = $this->loadModel('project')->getByID($execution->project); if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->loadModel('programplan')->computeProgress($executionID); $this->session->set('execution', ''); $message = $this->executeHooks($executionID); if($message) $this->lang->saveSuccess = $message; if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess)); $locate = true; if($execution->type == 'kanban') { $locate = $this->config->vision == 'lite' ? $this->createLink('project', 'execution', "status=all&projectID=$execution->project") : $this->createLink('execution', 'all'); } if($this->app->tab == 'project' && $this->session->executionList) $locate = $this->session->executionList; return $this->send(array('result' => 'success', 'load' => $locate)); } } /** * 维护关联产品。 * Manage products. * * @param int $executionID * @access public * @return void */ public function manageProducts(int $executionID) { /* Use first execution if executionID does not exist. */ if(!isset($this->executions[$executionID])) $executionID = key($this->executions); $execution = $this->execution->getByID($executionID); $project = $this->project->getByID($execution->project); if(!$project->hasProduct) return $this->sendError($this->lang->project->cannotManageProducts, true); if($project->model == 'waterfall' || $project->model == 'waterfallplus') return $this->sendError(sprintf($this->lang->execution->cannotManageProducts, zget($this->lang->project->modelList, $project->model)), true); /* Set menu. */ $this->execution->setMenu($execution->id); if($_SERVER['REQUEST_METHOD'] == 'POST') { $oldProducts = $this->loadModel('product')->getProducts($executionID); $oldProducts = array_keys($oldProducts); $postData = form::data()->get(); $this->execution->updateProducts($executionID, $postData); if(dao::isError()) return $this->sendError(dao::getError()); $newProducts = $this->product->getProducts($executionID); $newProducts = array_keys($newProducts); $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts)); if($diffProducts) $this->loadModel('action')->create($this->objectType, $executionID, 'Managed', '', !empty($_POST['products']) ? implode(',', $_POST['products']) : ''); return $this->sendSuccess(array('load' => true, 'closeModal' => true)); } $this->executionZen->assignManageProductsVars($execution); } /** * 维护执行的团队成员。 * Manage members of the execution. * * @param int $executionID * @param int $team2Import the team to import. * @param int $dept * @access public * @return void */ public function manageMembers(int $executionID = 0, int $team2Import = 0, int $dept = 0) { /* Set menu. */ $this->execution->setMenu($executionID); $execution = $this->execution->getByID($executionID); if(!empty($_POST)) { $memberDataList = $this->executionZen->buildMembersForManageMembers($execution); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->execution->manageMembers($execution, $memberDataList); if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError())); $this->execution->getLimitedExecution(); if(isInModal()) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "renderTaskAssignedTo($executionID);")); $this->execution->getLimitedExecution(); return $this->sendSuccess(array('load' => $this->createLink('execution', 'team', "executionID={$executionID}"))); } $this->loadModel('dept'); $deptUsers = empty($dept) ? array() : $this->dept->getDeptUserPairs($dept); $currentMembers = $this->execution->getTeamMembers($executionID); $members2Import = $this->execution->getMembers2Import($team2Import, array_keys($currentMembers)); /* Append users for get users. */ $appendUsers = array(); foreach($currentMembers as $account => $member) $appendUsers[$account] = $account; foreach($members2Import as $member) $appendUsers[$member->account] = $member->account; foreach($deptUsers as $deptAccount => $userName) $appendUsers[$deptAccount] = $deptAccount; if(!empty($this->config->user->moreLink)) $this->config->moreLinks["accounts[]"] = $this->config->user->moreLink; if($execution->type == 'kanban') $this->lang->execution->copyTeamTitle = str_replace($this->lang->execution->common, $this->lang->execution->kanban, $this->lang->execution->copyTeamTitle); $users = $this->loadModel('user')->getPairs('noclosed|nodeleted|devfirst', $appendUsers); $userItems = array(); foreach($users as $account => $realName) $userItems[$account] = array('value' => $account, 'text' => $realName, 'keys' => $account, 'disabled' => false); $this->view->title = $this->lang->execution->manageMembers . $this->lang->hyphen . $execution->name; $this->view->execution = $execution; $this->view->users = $users; $this->view->userItems = $userItems; $this->view->roles = $this->user->getUserRoles(array_keys($this->view->users)); $this->view->dept = $dept; $this->view->depts = $this->dept->getOptionMenu(); $this->view->teams2Import = $this->loadModel('personnel')->getCopiedObjects($executionID, 'sprint', true); $this->view->team2Import = $team2Import; $this->view->teamMembers = $this->executionZen->buildMembers($currentMembers, $members2Import, $deptUsers, $execution->days); $this->display(); } /** * 移除团队成员。 * Unlink a memeber. * * @param int $executionID * @param int $userID * @access public * @return void */ public function unlinkMember(int $executionID, int $userID) { $user = $this->loadModel('user')->getById($userID, 'id'); $this->execution->unlinkMember($executionID, $user->account); if(dao::isError()) return $this->sendError(dao::getError()); $this->execution->getLimitedExecution(); $changes = array(array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => $user->realname)); $actionID = $this->loadModel('action')->create('execution', $executionID, 'managedTeam'); $this->action->logHistory($actionID, $changes); return $this->sendSuccess(array('message' => '', 'load' => helper::createLink('execution', 'team', "executionID={$executionID}"))); } /** * 在迭代中关联需求。 * Link stories in the execution. * * @param int $objectID * @param string $browseType * @param int $param * @param string $orderBy * @param int $recPerPage * @param int $pageID * @param string $extra * @param string $storyType * @access public * @return void */ public function linkStory(int $objectID = 0, string $browseType = '', int $param = 0, string $orderBy = 'id_desc', int $recPerPage = 50, int $pageID = 1, string $extra = '', string $storyType = 'story') { $this->loadModel('story'); $this->loadModel('product'); $this->loadModel('tree'); $this->loadModel('branch'); /* Save old objectID to use later. */ $originObjectID = $objectID; /* Use the projectID of Kanban as objectID if vision is lite. */ if($this->config->vision == 'lite') { $kanban = $this->project->getByID($objectID, 'kanban'); if($kanban) $objectID = $kanban->project; } $products = $this->product->getProducts($objectID); if(empty($products)) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$objectID")); /* 通过objectID获取符合项目、执行、阶段和看板类型的对象。*/ $object = $this->project->getByID($objectID, 'project,sprint,stage,kanban'); $browseLink = $this->createLink('execution', 'story', "executionID=$objectID"); if($this->app->tab == 'project' and $object->multiple) $browseLink = $this->createLink('projectstory', 'story', "objectID=$objectID"); if($object->type == 'kanban' && !$object->hasProduct) $this->lang->productCommon = $this->lang->project->common; $this->session->set('storyList', $this->app->getURI(true), $this->app->tab); if($object->type == 'project') $this->project->setMenu($object->id); if(in_array($object->type, array('sprint', 'stage', 'kanban'))) $this->execution->setMenu($object->id); if(!empty($_POST)) { if($object->type != 'project' and $object->project != 0) $this->execution->linkStory($object->project, $this->post->stories ? $this->post->stories : array(), '', array(), $storyType); $this->execution->linkStory($objectID, $this->post->stories ? $this->post->stories : array(), $extra, array(), $storyType); if(isInModal()) return $this->sendSuccess(array('closeModal' => true, 'load' => true, 'callback' => "refreshKanban()")); return $this->sendSuccess(array('load' => $browseLink)); } /* Set modules and branches. */ $modules = array(); $branchIDList = array(BRANCH_MAIN); $branches = $this->project->getBranchesByProject($objectID); $productType = 'normal'; if(commonModel::isTutorialMode()) { $modules = $this->loadModel('tutorial')->getModulePairs(); } else { foreach($products as $product) { $productModules = $this->tree->getOptionMenu($product->id, 'story', 0, array_keys($branches[$product->id])); foreach($productModules as $branchModules) { foreach($branchModules as $moduleID => $moduleName) $modules[$moduleID] = ((count($products) >= 2 and $moduleID != 0) ? $product->name : '') . $moduleName; } if($product->type != 'normal') { $productType = $product->type; if(isset($branches[$product->id])) { foreach($branches[$product->id] as $branchID => $branch) $branchIDList[$branchID] = $branchID; } } } } /* Build the search form. */ $actionURL = $this->createLink($this->app->rawModule, 'linkStory', "objectID=$objectID&browseType=bySearch&queryID=myQueryID&orderBy=$orderBy&recPerPage=$recPerPage&pageID=$pageID&extra=$extra&storyType=$storyType"); $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products)); $queryID = ($browseType == 'bySearch') ? (int)$param : 0; $this->execution->buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, 'linkStory', $object); $project = (strpos('sprint,stage,kanban', $object->type) !== false) ? $this->loadModel('project')->getByID($object->project) : $object; $storyType = (($object->type == 'stage' && in_array($object->attribute, array('mix', 'request', 'design'))) || $object->type == 'project' || !$object->multiple) ? ($project->storyType ?? 'story') : 'story'; if($browseType == 'bySearch') $allStories = $this->story->getBySearch('all', '', $queryID, $orderBy, $objectID, $storyType); if($browseType != 'bySearch') $allStories = $this->story->getProductStories(implode(',', array_keys($products)), $branchIDList, '0', 'active,launched', $storyType, $orderBy, true, '', null); $linkedStories = $this->story->getExecutionStoryPairs($objectID, 0, 'all', 0, 'full', 'all', $storyType); foreach($allStories as $id => $story) { if(isset($linkedStories[$story->id])) unset($allStories[$id]); if(!isset($modules[$story->module])) { $storyModule = $this->tree->getModulesName((array)$story->module); $productName = count($products) > 1 ? $products[$story->product]->name : ''; $modules[$story->module] = $productName . zget($storyModule, $story->module, ''); } $story->estimate = $story->estimate . $this->config->hourUnit; } $gradeGroup = array(); $gradeList = $this->story->getGradeList(''); foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; /* Set the pager. */ $this->app->loadClass('pager', true); $pager = new pager(count($allStories), $recPerPage, $pageID); $allStories = array_chunk($allStories, $pager->recPerPage); $productPairs = array(); foreach($products as $id => $product) $productPairs[$id] = $product->name; $this->view->title = $object->name . $this->lang->hyphen . $this->lang->execution->linkStory; $this->view->objectID = $originObjectID; $this->view->param = $param; $this->view->extra = $extra; $this->view->object = $object; $this->view->orderBy = $orderBy; $this->view->gradeGroup = $gradeGroup; $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->productPairs = $productPairs; $this->view->allStories = empty($allStories) ? $allStories : $allStories[$pageID - 1]; $this->view->pager = $pager; $this->view->browseType = $browseType; $this->view->productType = $productType; $this->view->modules = $modules; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->branchGroups = $branchGroups; $this->view->browseLink = $browseLink; $this->view->project = $project; $this->view->executionID = $object->id; $this->view->projectID = $object->id; $this->view->storyType = $storyType; if($this->config->edition == 'ipd') $this->view->roadmaps = $this->loadModel('roadmap')->getPairs(array_keys($productPairs)); $this->display(); } /** * 取消关联需求(移除需求)。 * Unlink a story. * * @param int $executionID * @param int $storyID * @param string $confirm yes|no * @param string $from taskkanban * @param int $laneID * @param int $columnID * @access public * @return void */ public function unlinkStory(int $executionID, int $storyID, string $confirm = 'no', string $from = '', int $laneID = 0, int $columnID = 0) { if($confirm == 'no') { $confirmURL = $this->createLink('execution', 'unlinkstory', "executionID=$executionID&storyID=$storyID&confirm=yes&from=$from&laneID=$laneID&columnID=$columnID"); $tip = $this->app->rawModule == 'projectstory' ? $this->lang->execution->confirmUnlinkExecutionStory : $this->lang->execution->confirmUnlinkStory; $story = $this->loadModel('story')->getByID($storyID); if($story->type == 'requirement') $tip = str_replace($this->lang->SRCommon, $this->lang->URCommon, $tip); return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$tip}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});")); } $execution = $this->execution->getByID($executionID); $this->execution->unlinkStory($executionID, $storyID, $laneID, $columnID); if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError())); if($execution->type == 'kanban') { /* Fix bug #29171. */ $executions = $this->dao->select('*')->from(TABLE_EXECUTION)->where('parent')->eq($execution->parent)->fetchAll('id'); $executionStories = $this->dao->select('project,story')->from(TABLE_PROJECTSTORY)->where('story')->eq($storyID)->andWhere('project')->in(array_keys($executions))->fetchAll(); if(empty($executionStories)) $this->execution->unlinkStory($execution->parent, $storyID, $laneID, $columnID); } if($this->app->tab == 'execution' and $execution->type == 'kanban' or $from == 'taskkanban') return $this->send(array('result' => 'success', 'closeModal' => true, 'callback' => "refreshKanban()")); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true)); } /** * 批量取消关联需求(移除需求)。 * Batch unlink story. * * @param int $executionID * @access public * @return void */ public function batchUnlinkStory(int $executionID) { if(isset($_POST['storyIdList'])) { $storyIdList = $this->post->storyIdList; $_POST = array(); $this->loadModel('gitlab'); foreach($storyIdList as $storyID) { /* Delete related issue in gitlab. */ $relation = $this->gitlab->getRelationByObject('story', (int)$storyID); if(!empty($relation)) $this->gitlab->deleteIssue('story', (int)$storyID, $relation->issueID); $this->execution->unlinkStory($executionID, (int)$storyID); } } if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther'); $execution = $this->execution->getByID($executionID); return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->execution->confirmBatchUnlinkStory, 'locate' => $this->createLink('execution', 'story', "executionID=$executionID") . ($execution->multiple ? '' : '#app=project')))); } /** * 获取执行动态。 * Get the dynamic of execution. * * @param int $executionID * @param string $type * @param string $param * @param int $recTotal * @param string $date * @param string $direction next|pre * @access public * @return void */ public function dynamic(int $executionID = 0, string $type = 'today', string $param = '', int $recTotal = 0, string $date = '', string $direction = 'next') { if(empty($type)) $type = 'today'; /* Save session. */ $uri = $this->app->getURI(true); $this->session->set('productList', $uri, 'product'); $this->session->set('productPlanList', $uri, 'product'); $this->session->set('releaseList', $uri, 'product'); $this->session->set('storyList', $uri, 'product'); $this->session->set('executionList', $uri, 'execution'); $this->session->set('taskList', $uri, 'execution'); $this->session->set('buildList', $uri, 'execution'); $this->session->set('bugList', $uri, 'qa'); $this->session->set('caseList', $uri, 'qa'); $this->session->set('testtaskList', $uri, 'qa'); $this->session->set('reportList', $uri, 'qa'); /* use first execution if executionID does not exist. */ $executionID = $this->execution->checkAccess($executionID, $this->executions); $this->execution->setMenu($executionID); $execution = $this->execution->getByID($executionID); /* Set the user and type. */ $account = 'all'; if($type == 'account') { $user = $this->loadModel('user')->getById((int)$param, 'id'); /* 通过 id 字段获取用户。另一种是通过 account 获取。*/ if($user) $account = $user->account; } /* 获取这个时间段的操作日志列表。*/ $period = $type == 'account' ? 'all' : $type; $orderBy = $direction == 'next' ? 'date_desc' : 'date_asc'; $date = empty($date) ? '' : date('Y-m-d', (int)$date); $actions = $this->loadModel('action')->getDynamicByExecution($executionID, $account, $period, $orderBy, 50, $date, $direction); $dateGroups = $this->action->buildDateGroup($actions, $direction); if(empty($recTotal) && $dateGroups) $recTotal = $this->action->getDynamicCount($period); $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->dynamic; $this->view->userIdPairs = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted|useid'); $this->view->accountPairs = $this->loadModel('user')->getPairs('noletter|nodeleted'); $this->view->executionID = $executionID; $this->view->type = $type; $this->view->orderBy = $orderBy; $this->view->account = $account; $this->view->param = $param; $this->view->dateGroups = $dateGroups; /* 将日志按照日期分组,以日期为索引,将日志列表作为值。*/ $this->view->direction = $direction; $this->view->recTotal = $recTotal; $this->display(); } /** * 通过执行ID获取产品。 * AJAX: get products of a execution in html select. * * @param int $executionID * @access public * @return void */ public function ajaxGetProducts(int $executionID) { $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false); return print(html::select('product', $products, '', 'class="form-control"')); } /** * 通过执行ID/项目ID和指派给获取团队成员。 * AJAX: get team members of the execution. * * @param int $objectID * @param string $assignedTo * @access public * @return void */ public function ajaxGetMembers(int $objectID, string $assignedTo = '') { $object = $this->execution->fetchByID($objectID); $users = $this->loadModel('user')->getTeamMemberPairs($objectID, $object->type == 'project' ? 'project' : 'execution'); if($this->app->getViewType() === 'json') return print(json_encode($users)); $items = array(); foreach($users as $account => $realName) $items[] = array('value' => $account, 'text' => $realName, 'keys' => $account); return print(json_encode($items)); } /** * 通过项目ID或执行ID获取团队成员。 * AJAX: get team members by projectID/executionID. * * @param int $objectID * @access public * @return string */ public function ajaxGetTeamMembers(int $objectID) { $objectType = $this->dao->select('`type`')->from(TABLE_PROJECT)->where('id')->eq($objectID)->fetch('type'); if($objectType != 'project') $objectType = 'execution'; $members = $this->loadModel('user')->getTeamMemberPairs($objectID, $objectType); $selectedMembers = array(); foreach($members as $account => $realName) $selectedMembers[] = $account; return print(json_encode($selectedMembers)); } /** * 在用户创建完执行后,给用户发送一个后续操作的提示(比如设置团队、关联软件需求等)方便引导用户使用。 * Show tips to guide the user to do something after the execution created successfully. * * @param int $executionID * @access public * @return void */ public function tips(int $executionID) { $execution = $this->execution->getById($executionID); $this->view->executionID = $executionID; $this->view->execution = $execution; $this->view->projectID = $execution->project; $this->display(); } /** * 获取执行下拉列表用于切换不同的执行。 * Drop menu page. * * @param int $executionID 已经打开的页面对应的执行ID * @param string $module 链接里要访问的模块 * @param string $method 链接里要访问的方法 * @param string $extra 给链接传入的额外的参数 * @access public * @return void */ public function ajaxGetDropMenu(int $executionID, string $module = 'execution', string $method = 'task', string $extra = '') { $execution = $this->execution->fetchByID($executionID); if(!empty($execution->isTpl)) dao::$filterTpl = 'never'; $this->view->link = $this->executionZen->getLink($module, $method, $extra); $this->view->module = $module; $this->view->method = $method; $this->view->executionID = $executionID; $this->view->extra = $extra; $projects = $this->loadModel('program')->getProjectList(0, 'all', 0, 'order_asc', '', true); /* 获取所有项目的列表。*/ $executionGroups = $this->dao->select('*')->from(TABLE_EXECUTION) /* 按照项目分组,获取有权限访问的执行列表。*/ ->where('deleted')->eq('0') ->andWhere('multiple')->eq('1') ->andWhere('type')->in(!empty($execution->isTpl) ? 'sprint,stage' : 'sprint,stage,kanban') ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->beginIF(!empty($execution->isTpl))->andWhere('isTpl')->eq('1')->fi() ->andWhere('project')->in(array_keys($projects)) ->orderBy('order_asc') ->fetchGroup('project', 'id'); $teams = $this->dao->select('root,account')->from(TABLE_TEAM) /* 按照执行ID分组,获取有权限访问的执行的团队信息。*/ ->where('root')->in($this->app->user->view->sprints) ->andWhere('type')->eq('execution') ->fetchGroup('root', 'account'); $projectPairs = array(); /* 项目ID为索引,项目名称为值的数组 [projectID => projectName]。 */ $orderedExecutions = array(); $parentExecutions = array(); foreach($projects as $project) { $executions = zget($executionGroups, $project->id, array()); if(isset($project->model) and $project->model == 'waterfall') ksort($executions); $topExecutions = array(); foreach($executions as $execution) { if($execution->grade == 1) $topExecutions[$execution->id] = $execution->id; if($execution->grade > 1 && $execution->parent) $parentExecutions[$execution->parent][$execution->id] = $execution; } /* 获取排序后的执行列表,并给每个执行设置团队信息。*/ $executions = $this->execution->resetExecutionSorts($executions, $topExecutions, $parentExecutions); foreach($executions as $execution) { $execution->teams = zget($teams, $execution->id, array()); $orderedExecutions[$execution->id] = $execution; } $projectPairs[$project->id] = $project->name; } $executionNameList = $this->execution->getFullNameList($orderedExecutions); $projectExecutions = array(); /* 项目对应的执行列表 [projectID => execution]。*/ foreach($orderedExecutions as $execution) { if(isset($parentExecutions[$execution->id])) continue; $execution->name = $executionNameList[$execution->id]; $projectExecutions[$execution->project][] = $execution; } $this->view->projects = $projectPairs; /* 项目ID为索引,项目名称为值的数组 [projectID => projectName]。 */ $this->view->projectExecutions = $projectExecutions; /* 项目对应的执行列表 [projectID => execution]。*/ $this->view->executionID = $executionID; $this->view->execution = $execution; $this->display(); } /** * 将执行列表内的执行进行排序。 * Update the order of the executions. * * @access public * @return void */ public function updateOrder() { if($_SERVER['REQUEST_METHOD'] !== 'POST') return $this->sendError($this->lang->error->unsupportedReq); $idList = explode(',', trim($this->post->executions, ',')); $orderBy = $this->post->orderBy; if(strpos($orderBy, 'order') === false) return false; $executions = $this->dao->select('id,`order`')->from(TABLE_EXECUTION)->where('id')->in($idList)->orderBy($orderBy)->fetchPairs('order', 'id'); foreach($executions as $order => $id) { $newID = array_shift($idList); if($id == $newID) continue; $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($order)->where('id')->eq($newID)->exec(); if(dao::isError()) return $this->sendError(dao::getError()); } return $this->sendSuccess(); } /** * 需求排序。 * Sort stories. * * @param int $executionID * @access public * @return void */ public function storySort(int $executionID) { $storyIdList = json_decode($this->post->storyIdList, true); $storyIdList = array_flip($storyIdList); $orderBy = $this->post->orderBy; krsort($storyIdList); $order = $this->dao->select('`order`')->from(TABLE_PROJECTSTORY)->where('story')->in($storyIdList)->andWhere('project')->eq($executionID)->orderBy('order_asc')->fetch('order'); foreach($storyIdList as $storyID) { $this->dao->update(TABLE_PROJECTSTORY)->set('`order`')->eq($order)->where('story')->eq($storyID)->andWhere('project')->eq($executionID)->exec(); $order++; } return $this->sendSuccess(); } /** * 在执行需求列表内进行需求估算。 * Story estimate. * * @param int $executionID * @param int $storyID * @param int $round The round of estimate for stroy 估算的轮次 * @access public * @return void */ public function storyEstimate(int $executionID, int $storyID, int $round = 0) { $this->loadModel('story'); if($_POST) { $this->story->saveEstimateInfo($storyID); if(dao::isError()) return $this->sendError(dao::getError()); $this->loadModel('action')->create('story', $storyID, 'estimated', '', $executionID); $link = $this->createLink('execution', 'storyEstimate', "executionID={$executionID}&storyID={$storyID}&round={$round}"); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => "loadModal(\"$link\", '#storyEstimateModal');")); } $estimateInfo = $this->story->getEstimateInfo($storyID, $round); $team = $this->execution->getTeamMembers($executionID); /* If user has been removed from team and has estimate record, join him into team.*/ if(!empty($estimateInfo->estimate)) { foreach($estimateInfo->estimate as $account => $estimate) { if(!in_array($account, array_keys($team))) { $team[$account] = new stdclass(); $team[$account]->account = $account; } } } $this->view->estimateInfo = $estimateInfo; $this->view->round = !empty($estimateInfo->round) ? $estimateInfo->round : 0; $this->view->rounds = $this->story->getEstimateRounds($storyID); $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->team = $team; $this->view->executionID = $executionID; $this->view->storyID = $storyID; $this->view->story = $this->story->getById($storyID); $this->display(); } /** * 查看执行列表。 * All execution. * * @param string $status * @param string $orderBy * @param int $productID * @param string $param * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function all(string $status = 'undone', string $orderBy = 'order_asc', int $productID = 0, string $param = '', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { $this->app->loadLang('my'); $this->app->loadLang('stage'); $this->app->loadLang('programplan'); $this->loadModel('product'); $this->loadModel('project'); $this->loadModel('datatable'); $from = $this->app->tab; if($from == 'execution') $this->session->set('executionList', $this->app->getURI(true), 'execution'); if($this->app->viewType == 'mhtml') { $executionID = $this->execution->checkAccess(0, $this->executions); $this->execution->setMenu($executionID); } /* Load pager and get tasks. */ $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); $queryID = ($status == 'bySearch') ? (int)$param : 0; $actionURL = $this->createLink('execution', 'all', "status=bySearch&orderBy=$orderBy&productID=$productID¶m=myQueryID"); $this->execution->buildSearchForm($queryID, $actionURL); $sort = $orderBy; if(strpos($sort, 'rawID_') !== false) $sort = str_replace('rawID_', 'id_', $sort); if(strpos($sort, 'nameCol_') !== false) $sort = str_replace('nameCol_', 'name_', $sort); $executionStats = $this->execution->getStatData(0, $status, $productID, 0, false, $queryID, $sort, $pager); $this->view->title = $this->lang->execution->allExecutions; $this->view->executionStats = $executionStats; $this->view->allExecutionsNum = $this->execution->getExecutionCounts(0, 'all'); $this->view->productList = $this->loadModel('product')->getProductPairsByProject(0); $this->view->productID = $productID; $this->view->pager = $pager; $this->view->orderBy = $orderBy; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->projects = array('') + $this->project->getPairsByProgram(); $this->view->status = $status; $this->view->from = $from; $this->view->param = $param; $this->view->showBatchEdit = $this->cookie->showExecutionBatchEdit; $this->view->avatarList = $this->user->getAvatarPairs(''); $this->display(); } /** * 显示白名单信息。 * Get white list personnel. * * @param int $executionID * @param string $module * @param string $objectType * @param string $orderby * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ public function whitelist(int $executionID = 0, string $module='execution', string $objectType = 'sprint', string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { /* Set the menu. If the executionID = 0, use the indexMenu instead. */ $execution = $this->commonAction($executionID); if(!empty($execution->acl) and $execution->acl != 'private') return $this->sendError($this->lang->whitelistNotNeed, $this->createLink('execution', 'task', "executionID=$executionID")); echo $this->fetch('personnel', 'whitelist', "objectID=$executionID&module=$module&browseType=$objectType&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); } /** * 添加用户到白名单。 * Adding users to the white list. * * @param int $executionID * @param int $deptID * @param int $copyID * @access public * @return void */ public function addWhitelist(int $executionID = 0, int $deptID = 0, int $copyID = 0) { /* use first execution if executionID does not exist. */ if(!isset($this->executions[$executionID])) $executionID = key($this->executions); /* Set the menu. If the executionID = 0, use the indexMenu instead. */ $this->execution->setMenu($executionID); $execution = $this->execution->getByID($executionID); if(!empty($execution->acl) and $execution->acl != 'private') return $this->sendError($this->lang->whitelistNotNeed, $this->createLink('execution', 'task', "executionID=$executionID")); echo $this->fetch('personnel', 'addWhitelist', "objectID=$executionID&dept=$deptID©ID=$copyID&objectType=sprint&module=execution"); } /* * 从白名单中移除用户。 * Removing users from the white list. * * @param int $id * @param string $confirm * @access public * @return void */ public function unbindWhitelist(int $id = 0, string $confirm = 'no') { echo $this->fetch('personnel', 'unbindWhitelist', "id=$id&confirm=$confirm"); } /** * 执行列表导出数据。 * Export execution. * * @param string $status * @param int $productID * @param string $orderBy * @param string $from * @access public * @return void */ public function export(string $status, int $productID, string $orderBy, string $from) { $lowerStatus = strtolower($status); if($_POST) { $executionLang = $this->lang->execution; $executionConfig = $this->config->execution; $projectExecutionDtable = $this->config->project->execution->dtable->fieldList; $projectID = $from == 'project' ? $this->session->project : 0; if($projectID) $this->project->setMenu($projectID); $project = $this->project->getByID($projectID); /* Create field lists. */ $fields = $this->post->exportFields ? $this->post->exportFields : explode(',', $executionConfig->list->exportFields); foreach($fields as $key => $fieldName) { if($fieldName == 'type' and $this->app->tab == 'project' and ($project->model == 'agileplus' or $project->model == 'waterfallplus')) $fields['method'] = $executionLang->method; $fieldName = trim($fieldName); $fields[$fieldName] = $from == 'project' && !empty($projectExecutionDtable[$fieldName]) ? $projectExecutionDtable[$fieldName]['title'] : zget($executionLang, $fieldName); if($fieldName == 'type') $fields['type'] = $this->lang->typeAB; unset($fields[$key]); } if(isset($fields['id'])) $fields['id'] = $this->lang->idAB; unset($fields[$from == 'project' ? 'projectName' : 'productName']); $users = $this->loadModel('user')->getPairs('noletter'); $showTask = ($this->app->tab == 'project' && (bool)$this->cookie->showTask); if($showTask && $lowerStatus == 'bysearch') { $executionIdList = $this->loadModel('programplan')->getStageList($projectID, $productID, 'all', 'order'); $tasks = $this->programplan->getGanttTasks($projectID, array_keys($executionIdList), $lowerStatus, 0, null); $executionTasks = array(); $executionIdList = array(); foreach($tasks as $task) { $executionIdList[$task->execution] = $task->execution; if(!isset($executionTasks[$task->execution])) $executionTasks[$task->execution] = array(); $executionTasks[$task->execution][$task->id] = $task; } $executions = $this->execution->getByIdList($executionIdList); $executions = $this->execution->batchProcessExecution($executions, $projectID, $productID, true, '', $executionTasks); $executionStats = array_values($executions); } else { $executionStats = $this->execution->getStatData($projectID, $lowerStatus == 'byproduct' ? 'all' : $status, $productID, 0, (bool)$showTask, 'withchild', $orderBy); } $executionStats = $this->flattenObjectArray($executionStats); $rows = array(); $checkedItem = $this->cookie->checkedItem; foreach($executionStats as $i => $execution) { if($this->post->exportType == 'selected' && strpos(",$checkedItem,", ",pid{$execution->id},") === false) continue; $execution->PM = zget($users, $execution->PM); $execution->status = !empty($execution->delay) ? $executionLang->delayed : $this->processStatus('execution', $execution); $execution->progress .= '%'; $execution->name = isset($execution->title) ? $execution->title : $execution->name; $execution->type = zget($this->lang->execution->typeList, $execution->type, $this->lang->executionCommon); if(isset($executionStats[$execution->parent])) $execution->name = $executionStats[$execution->parent]->name . '/' . $execution->name; if($this->app->tab == 'project' and ($project->model == 'agileplus' or $project->model == 'waterfallplus')) $execution->method = zget($executionLang->typeList, $execution->type); $rows[] = $execution; if(empty($execution->tasks)) continue; foreach($execution->tasks as $task) { if($task->isParent) { $task->name = '[' . $this->lang->task->parentAB . '] ' . $task->name; } elseif($task->parent > 0) { $task->name = '[' . $this->lang->task->childrenAB . '] ' . $task->name; } $task->type = $this->lang->task->common; $task->status = zget($this->lang->task->statusList, $task->status); $task->totalEstimate = $task->estimate; $task->totalConsumed = $task->consumed; $task->totalLeft = $task->left; $task->progress = (($task->consumed + $task->left) == 0 ? 0 : round($task->consumed / ($task->consumed + $task->left), 4) * 100) . '%'; $task->begin = !helper::isZeroDate($task->estStarted) ? $task->estStarted : ''; $task->end = !helper::isZeroDate($task->deadline) ? $task->deadline : ''; $task->realBegan = !helper::isZeroDate($task->realStarted) ? $task->realStarted : ''; $task->realEnd = !helper::isZeroDate($task->finishedDate) ? $task->finishedDate : ''; $task->PM = zget($users, $task->assignedTo); $rows[] = $task; } } if($this->config->edition != 'open') list($fields, $executionStats) = $this->loadModel('workflowfield')->appendDataFromFlow($fields, $executionStats); $this->post->set('fields', $fields); $this->post->set('rows', $rows); $this->post->set('kind', $this->lang->execution->common); $this->fetch('file', 'export2' . $this->post->fileType, $_POST); } $executionConcept = $this->lang->execution->common; if($this->app->tab == 'project') { $project = $this->project->getByID($this->session->project); if(!empty($project->model) && $project->model == 'waterfall') $executionConcept = $this->lang->project->stage; if(!empty($project->model) && $project->model == 'scrum') $executionConcept = $this->lang->executionCommon; $fileName = isset($project->name) ? $project->name . ' - ' . $executionConcept : $executionConcept; } if($lowerStatus != 'bysearch') $fileName = (in_array($status, array('all', 'undone', 'delayed')) ? zget($this->lang->execution, $status, '') : zget($this->lang->execution->statusList, $status, '')) . $executionConcept; $this->view->fileName = !empty($fileName) ? $fileName : $executionConcept; $this->display(); } /** * 在迭代中查看迭代的文档。 * Doc for compatible. * * @param int $executionID * @access public * @return void */ public function doc(int $executionID = 0, int $libID = 0, int $moduleID = 0, string $browseType = 'all', string $orderBy = 'order_asc', int $param = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1, string $mode = 'list', int $docID = 0, string $search = '') { $this->commonAction($executionID); echo $this->fetch('doc', 'app', "type=execution&spaceID=$executionID&libID=$libID&moduleID=$moduleID&docID=$docID&mode=$mode&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID&filterType=$browseType&search=$search&noSpace=true"); } /** * 将计划中的需求关联到此项目或迭代/执行或看板中。 * Import the stories to execution/project/kanban by product plan. * * @param int $executionID * @param int $planID * @param int $productID * @param string $extra * @access public * @return void */ public function importPlanStories(int $executionID, int $planID, int $productID = 0, string $extra = '') { $execution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch(); $planStories = array(); $planStory = $this->loadModel('story')->getPlanStories($planID); $draftCount = 0; if(!empty($planStory)) { $projectProducts = $this->loadModel('project')->getBranchesByProject($executionID); foreach($planStory as $id => $story) { $projectBranches = zget($projectProducts, $story->product, array()); if($story->status != 'active' or (!empty($story->branch) and !empty($projectBranches) and !isset($projectBranches[$story->branch]))) { $draftCount++; unset($planStory[$id]); } } $planStories = array_keys($planStory); if($execution->project != 0) $this->execution->linkStory($execution->project, $planStories); /* Link story to the project of the execution. */ $this->execution->linkStory($executionID, $planStories, $extra); /* Link story to the execution. */ } $moduleName = 'execution'; if($execution->type == 'project') $moduleName = 'projectstory'; if($execution->type == 'kanban') { global $lang; $lang->executionCommon = $lang->execution->kanban; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; } /* Check if the product is multiple branch. */ $multiBranchProduct = $this->executionZen->hasMultipleBranch($productID, $executionID); if($draftCount != 0) { $importPlanStoryTips = $multiBranchProduct ? $this->lang->execution->haveBranchDraft : $this->lang->execution->haveDraft; $haveDraft = sprintf($importPlanStoryTips, $draftCount); if(!$execution->multiple || $moduleName == 'projectstory') $haveDraft = str_replace($this->lang->executionCommon, $this->lang->projectCommon, $haveDraft); return $this->send(array('closeModal' => true, 'load' => true, 'result' => 'fail', 'message' => $haveDraft)); } return $this->sendSuccess(array('closeModal' => true, 'load' => true)); } /** * 在执行/迭代的树状图中查看某个需求的详情。 * View a story in the tree view of the execution. * * @param int $storyID * @param int $version The version of story. * * @access public * @return void */ public function treeStory(int $storyID, int $version = 0) { $this->loadModel('requirement'); $this->loadModel('epic'); $story = $this->loadModel('story')->getById($storyID, $version, true); $product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fields('name, id, `type`, shadow')->fetch(); $this->view->story = $story; $this->view->product = $product; $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id); $this->view->plan = $this->dao->findById($story->plan)->from(TABLE_PRODUCTPLAN)->fields('title')->fetch('title'); $this->view->bugs = $this->dao->select('id,title,status')->from(TABLE_BUG)->where('story')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll(); $this->view->cases = $this->dao->select('id,title')->from(TABLE_CASE)->where('story')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll(); $this->view->modulePath = $this->loadModel('tree')->getParents($story->module); $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->executions = $this->loadModel('execution')->getPairs(0, 'all', 'nocode'); $this->view->actions = $this->loadModel('action')->getList('story', $storyID); $this->view->version = $version == 0 ? $story->version : $version; $this->view->executionID = $this->session->execution; $this->display(); } /** * 在执行/迭代的树状图中查看某个任务的详情。 * View a task in the tree view of the execution. * * @param int $taskID * * @access public * @return void */ public function treeTask(int $taskID) { $this->loadModel('task'); $task = $this->task->getById($taskID, true); if($task->fromBug != 0) { $bug = $this->loadModel('bug')->getById($task->fromBug); $task->bugSteps = ''; if($bug) { $task->bugSteps = $this->loadModel('file')->setImgSize($bug->steps); foreach($bug->files as $file) $task->files[] = $file; } $this->view->fromBug = $bug; } else { $story = $this->loadModel('story')->getById($task->story); $task->storySpec = empty($story) ? '' : $this->loadModel('file')->setImgSize($story->spec); $task->storyVerify = empty($story) ? '' : $this->loadModel('file')->setImgSize($story->verify); $task->storyFiles = $this->loadModel('file')->getByObject('story', $task->story); } if($task->team) $this->lang->task->assign = $this->lang->task->transfer; /* Update action. */ if($task->assignedTo == $this->app->user->account) $this->loadModel('action')->read('task', $taskID); $this->view->task = $task; $this->view->execution = $this->execution->getById($task->execution); $this->view->actions = $this->loadModel('action')->getList('task', $taskID); $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->display(); } /** * 获取泳道分组下拉。 * Get the lane group drop-down by ajax. * * @param string $type all|syory|task|bug * @param string $group * @access public * @return void */ public function ajaxGetGroup($type, $group = 'default') { $this->app->loadLang('kanban'); return print(html::select("group", $this->lang->kanban->group->$type, $group, 'class="form-control chosen" data-max_drop_width="215"')); } /** * 更新看板数据。 * Update kanban by ajax. * * @param int $executionID * @param int $enterTime * @param string $browseType all|story|task|bug * @param string $groupBy default|pri|category|module|source|assignedTo|type|story|severity * @param string $from taskkanban|execution * @param string $searchValue * @param string $orderBy * @access public * @return void */ public function ajaxUpdateKanban(int $executionID = 0, int $enterTime = 0, string $browseType = '', string $groupBy = '', string $from = 'taskkanban', string $searchValue = '', string $orderBy = 'id_asc') { $this->loadModel('kanban'); if($groupBy == 'story' and $browseType == 'task' and !isset($this->lang->kanban->orderList[$orderBy])) $orderBy = 'pri_asc'; if($from == 'execution') $this->session->set('taskSearchValue', $searchValue); if($from == 'RD') $this->session->set('rdSearchValue', $searchValue); $lastEditedTime = $this->execution->getLaneMaxEditedTime($executionID); $enterTime = date('Y-m-d H:i:s', $enterTime); if(in_array(true, array(empty($lastEditedTime), strtotime($lastEditedTime) < 0, $lastEditedTime > $enterTime, $groupBy != 'default', !empty($searchValue)))) { if($from == 'taskkanban') { list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $searchValue, $orderBy); } else { $kanbanGroup = $this->kanban->getRDKanban($executionID, $browseType, $orderBy, 0, $groupBy, $searchValue); } return print(json_encode($kanbanGroup)); } } /** * 创建执行时,根据项目ID获取可以复制的执行。 * When the create an execution, get execution list that can be copied by the project ID. * * @param int $projectID * @param int $copyExecutionID * @access public * @return void */ public function ajaxGetCopyProjectExecutions(int $projectID = 0, int $copyExecutionID = 0) { $project = $this->loadModel('project')->fetchByID($projectID); if($project) $this->executionZen->correctExecutionCommonLang($project, zget($this->config->execution->modelList, $project->model, 'scrum')); $this->view->executions = $this->execution->getList($projectID, 'all', 'all', 0, 0, 0, null, false); $this->view->copyExecutionID = $copyExecutionID; $this->display(); } /** * 获取专业研发看板的数据。 * Get execution kanban data by ajax. * * @param int $executionID * @access public * @return void */ public function ajaxGetExecutionKanban(int $executionID) { $execution = $this->execution->getByID($executionID); if($this->app->tab == 'execution' and $execution->type == 'kanban') { $execLaneType = $this->session->execLaneType ? $this->session->execLaneType : 'all'; $execGroupBy = $this->session->execGroupBy ? $this->session->execGroupBy : 'default'; $rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : ''; $kanbanData = $this->loadModel('kanban')->getRDKanban($executionID, $execLaneType, 'id_desc', 0, $execGroupBy, $rdSearchValue); echo json_encode($kanbanData); } } /** * 展示燃尽图。 * Display burn chart. * * @param int $executionID * @access public * @return void */ public function ajaxGetBurn(int $executionID) { $execution = $this->execution->getByID($executionID, true); $type = 'noweekend'; if(((strpos('closed,suspended', $execution->status) === false && helper::today() > $execution->end) || ($execution->status == 'closed' && substr($execution->closedDate ?? '', 0, 10) > $execution->end) || ($execution->status == 'suspended' && $execution->suspendedDate > $execution->end)) && strpos($type, 'delay') === false) { $type .= ',withdelay'; } $deadline = $execution->status == 'closed' ? substr($execution->closedDate ?? '', 0, 10) : $execution->suspendedDate; $deadline = strpos('closed,suspended', $execution->status) === false ? helper::today() : $deadline; $endDate = strpos($type, 'withdelay') !== false ? $deadline : $execution->end; list($dateList, $interval) = $this->execution->getDateList($execution->begin, $endDate, $type, 0, 'Y-m-d', $execution->end); $executionEnd = strpos($type, 'withdelay') !== false ? $execution->end : ''; $this->view->chartData = $this->execution->buildBurnData($executionID, $dateList, 'left', $executionEnd); $this->view->execution = $execution; $this->display(); } /** * 展示累计流图。 * Display cumulative flow diagram. * * @param int $executionID * @access public * @return void */ public function ajaxGetCFD(int $executionID) { $execution = $this->execution->getById($executionID, true); $this->app->loadClass('date'); list($begin, $end) = $this->execution->getBeginEnd4CFD($execution); $dateList = date::getDateList($begin, $end, 'Y-m-d', 'noweekend'); $chartData = $this->execution->buildCFDData($executionID, $dateList, 'task'); if(isset($chartData['line'])) $chartData['line'] = array_reverse($chartData['line']); $this->view->begin = helper::safe64Encode(urlencode($begin)); $this->view->end = helper::safe64Encode(urlencode($end)); $this->view->execution = $execution; $this->view->chartData = $chartData; $this->display(); } /** * Ajax get product drop menu. * * @param int $executionID * @param int $productID * @access public * @return void */ public function ajaxSwitcherMenu(int $executionID, int $productID, string $currentMethod = '') { $link = helper::createLink('execution', $currentMethod, "executionID=$executionID&productID={id}"); if($currentMethod == 'zerocase') $link = helper::createLink('testcase', 'zerocase', "productID={id}&branch=0&orderBy=id_desc&objectID=$executionID"); $this->view->link = $link; $this->view->productID = $productID; $this->view->products = $this->loadModel('product')->getProducts($executionID); $this->view->executionID = $executionID; $this->view->currentMethod = $currentMethod; $this->display(); } /** * Ajax get execution drop menu. * 通过Ajax获取执行下拉菜单。 * * @param int $projectID * @param int $executionID * @param string $method * @access public * @return void */ public function ajaxGetExecutionSwitcherMenu(int $projectID, int $executionID, string $method = '') { $this->view->link = helper::createLink('execution', $method, 'executionID={id}'); $this->view->executionID = $executionID; $this->view->executions = $this->execution->getList($projectID, 'kanban'); $this->display(); } /** * Flatten Object Array. * * @param array $array * @access public * @return void */ public function flattenObjectArray(array $array = array()) { $this->loadModel('task'); $result = array(); foreach($array as $object) { $result[$object->id] = $object; $tasks = zget($object, 'tasks', array()); if(empty($tasks)) continue; $object->tasks = $this->task->mergeChildIntoParent($tasks); } return $result; } /** * AJAX: 获取有未关闭任务的执行。 * AJAX: get executions that have unclosed tasks. * * @param string $executionIdList * @access public * @return string */ public function ajaxGetNonClosableExecutions(string $executionIdList) { $executionIdList = str_replace('pid', '', $executionIdList); $tasks = $this->loadModel('task')->getUnclosedTasksByExecution(explode(',', $executionIdList)); $tasks = array_filter($tasks); $executions = !empty($tasks) ? $this->execution->getPairsByList(array_keys($tasks)) : array(); return print(json_encode(array_values($executions))); } }