* @package execution * @link https://www.zentao.net */ class executionZen extends execution { /** * 展示Bug列表的相关变量。 * Show the bug list related variables. * * @param object $execution * @param object $project * @param int $productID * @param string $branch * @param array $products * @param string $orderBy * @param string $type * @param string $build * @param int $param * @param array $bugs * @param object $pager * @access protected * @return void */ protected function assignBugVars(object $execution, object $project, int $productID, string $branch, array $products, string $orderBy, string $type, int $param, string $build, array $bugs, object $pager) { $this->loadModel('product'); $this->loadModel('tree'); $moduleID = $type == 'bymodule' ? $param : 0; /* Get module tree.*/ $extra = array('projectID' => $execution->id, 'orderBy' => $orderBy, 'type' => 'byModule', 'build' => $build, 'branchID' => $branch); if($execution->id and empty($productID) and count($products) > 1) { $moduleTree = $this->tree->getBugTreeMenu($execution->id, $productID, 0, array('treeModel', 'createBugLink'), $extra); } elseif(!empty($products)) { $productID = empty($productID) ? reset($products)->id : $productID; $moduleTree = $this->tree->getTreeMenu((int)$productID, 'bug', 0, array('treeModel', 'createBugLink'), $extra + array('branchID' => $branch, 'productID' => $productID), $branch); } else { $moduleTree = array(); } $tree = $moduleID ? $this->tree->getByID($moduleID) : ''; $showModule = !empty($this->config->execution->bug->showModule) ? $this->config->execution->bug->showModule : ''; $build = !empty($build) ? $this->loadModel('build')->getById((int)$build) : null; /* Assign. */ $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->bug; $this->view->project = $project; $this->view->orderBy = $orderBy; $this->view->type = $type; $this->view->pager = $pager; $this->view->bugs = $bugs; $this->view->summary = $this->loadModel('bug')->summary($bugs); $this->view->moduleTree = $moduleTree; $this->view->moduleID = $moduleID; $this->view->modulePairs = $showModule ? $this->tree->getModulePairs($productID, 'bug', $showModule) : array(); $this->view->buildID = $build ? $build->id : 0; $this->view->productID = $productID; $this->view->product = $this->product->getByID($productID); $this->view->branchID = empty($this->view->build->branch) ? $branch : $this->view->build->branch; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->param = $param; $this->view->defaultProduct = (empty($productID) and !empty($products)) ? current(array_keys($products)) : $productID; $this->view->builds = $this->loadModel('build')->getBuildPairs(array($productID)); $this->view->projectPairs = $this->loadModel('project')->getPairsByProgram(); $this->view->switcherObjectID = $productID; } /** * 展示看板的相关变量。 * Show the variables associated with the kanban. * * @param int $executionID * @access protected * @return void */ protected function assignKanbanVars(int $executionID) { /* Get user list. */ $userList = array(); $users = $this->loadModel('user')->getPairs('noletter|nodeleted'); $avatarPairs = $this->user->getAvatarPairs('all'); foreach($avatarPairs as $account => $avatar) { if(!isset($users[$account])) continue; $userList[$account]['realname'] = $users[$account]; $userList[$account]['avatar'] = $avatar; } $userList['closed']['account'] = 'Closed'; $userList['closed']['realname'] = 'Closed'; $userList['closed']['avatar'] = ''; /* Get execution linked products. */ $productID = 0; $branchID = 0; $products = $this->loadModel('product')->getProducts($executionID); $productNames = array(); if($products) { $productID = key($products); $branches = $this->loadModel('branch')->getPairs($productID, '', $executionID); if($branches) $branchID = key($branches); } foreach($products as $product) $productNames[$product->id] = $product->name; /* Get execution linked plans. */ $plans = $this->execution->getPlans(array_keys($products), 'skipParent', $executionID); $allPlans = array(); if(!empty($plans)) { foreach($plans as $plan) $allPlans += $plan; } if(!isset($_SESSION['limitedExecutions'])) $this->execution->getLimitedExecution(); $this->view->users = $users; $this->view->userList = $userList; $this->view->productID = $productID; $this->view->branchID = $branchID; $this->view->productNames = $productNames; $this->view->productNum = count($products); $this->view->allPlans = $allPlans; $this->view->isLimited = !$this->app->user->admin && strpos(",{$this->session->limitedExecutions},", ",{$executionID},") !== false; } /** * 展示维护产品相关变量。 * Show the manage products related variables. * * @param object $execution * @access protected * @return void */ protected function assignManageProductsVars(object $execution) { $branches = $this->project->getBranchesByProject($execution->id); $linkedProductIdList = empty($branches) ? array() : array_keys($branches); $allProducts = $this->loadModel('product')->getProductPairsByProject($execution->project, 'all', implode(',', $linkedProductIdList)); $linkedProducts = $this->product->getProducts($execution->id, 'all', '', true, $linkedProductIdList); $linkedBranches = array(); $executionStories = $this->project->getStoriesByProject($execution->id); /* If the story of the product which linked the execution, you don't allow to remove the product. */ $unmodifiableProducts = array(); $unmodifiableBranches = array(); $linkedStoryIDList = array(); $linkedBranchIdList = array(); foreach($linkedProducts as $productID => $linkedProduct) { $linkedBranches[$productID] = array(); if(!isset($allProducts[$productID])) $allProducts[$productID] = $linkedProduct->name; foreach($branches[$productID] as $branchID => $branch) { $linkedBranches[$productID][$branchID] = $branchID; $linkedBranchIdList[$branchID] = $branchID; if(!empty($executionStories[$productID][$branchID])) { array_push($unmodifiableProducts, $productID); array_push($unmodifiableBranches, $branchID); $linkedStoryIDList[$productID][$branchID] = $executionStories[$productID][$branchID]->storyIDList; } } } $this->view->title = $this->lang->execution->manageProducts . $this->lang->hyphen . $execution->name; $this->view->execution = $execution; $this->view->linkedProducts = $linkedProducts; $this->view->unmodifiableProducts = $unmodifiableProducts; $this->view->unmodifiableBranches = $unmodifiableBranches; $this->view->linkedBranches = $linkedBranches; $this->view->linkedStoryIDList = $linkedStoryIDList; $this->view->allProducts = $allProducts; $this->view->branchGroups = $this->execution->getBranchByProduct(array_keys($allProducts), $execution->project, 'ignoreNormal|noclosed', $linkedBranchIdList); $this->view->allBranches = $this->execution->getBranchByProduct(array_keys($allProducts), $execution->project, 'ignoreNormal'); $this->display(); } /** * 展示需求关联的任务、Bug、用例的数量以及统计需求信息。 * Show the number of tasks, bugs cases linked with the stories, and statistics story information. * * @param int $executionID * @param array $stories * @param string $storyType * @access protected * @return void */ protected function assignCountForStory(int $executionID, array $stories, string $storyType): void { /* Get related tasks, bugs, cases count of each story. */ $storyIdList = array(); foreach($stories as $story) { $storyIdList[$story->id] = $story->id; if(empty($story->children)) continue; foreach($story->children as $child) $storyIdList[$child->id] = $child->id; } $this->view->stories = $stories; $this->view->storyTasks = $this->loadModel('task')->getStoryTaskCounts($storyIdList, $executionID); $this->view->storyBugs = $this->loadModel('bug')->getStoryBugCounts($storyIdList, $executionID); $this->view->storyCases = $this->loadModel('testcase')->getStoryCaseCounts($storyIdList); $this->view->summary = $this->loadModel('product')->summary($stories, 'all'); } /** * 展示需求列表的相关变量。 * Show the story list related variables. * * @param object $execution * @param array $products * @param int $productID * @param string $type * @param string $storyType * @param int $param * @param string $orderBy * @param object $pager * @access protected * @return void */ protected function assignRelationForStory(object $execution, array $products, int $productID, string $type, string $storyType, int $param, string $orderBy, object $pager): void { $plans = $this->execution->getPlans(array_keys($products), 'skipParent|withMainPlan|unexpired|noclosed|sortedByDate', $execution->id); $allPlans = array(); if(!empty($plans)) { foreach($plans as $plan) $allPlans += $plan; } if($this->cookie->storyProductParam) $this->view->product = $this->loadModel('product')->getById((int)$this->cookie->storyProductParam); if($this->cookie->storyBranchParam) { $branchID = $this->cookie->storyBranchParam; if(strpos($branchID, ',') !== false) list($productID, $branchID) = explode(',', $branchID); $this->view->branch = $this->loadModel('branch')->getById($branchID, $productID); } $executionProductList = $this->loadModel('product')->getProducts($execution->id); $multiBranch = false; foreach($executionProductList as $executionProduct) { if($executionProduct->type != 'normal') { $multiBranch = true; break; } } $project = $this->loadModel('project')->getByID($execution->project); if(!($execution->type == 'stage' && in_array($execution->attribute, array('mix', 'request', 'design'))) && $project->multiple) $project->storyType = 'story'; $productPairs = $this->loadModel('product')->getProductPairsByProject($execution->id); // Get execution's product. if(empty($productID)) $productID = (int)key($productPairs); $this->assignModuleForStory($type, $param, $storyType, $execution, $productID); $gradeList = $this->loadModel('story')->getGradeList(''); $gradeGroup = array(); foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; /* Assign. */ $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->story; $this->view->storyType = $storyType; $this->view->param = $param; $this->view->type = $this->session->executionStoryBrowseType; $this->view->orderBy = $orderBy; $this->view->pager = $pager; $this->view->product = $this->product->getById($productID); $this->view->allPlans = $allPlans; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->multiBranch = $multiBranch; $this->view->execution = $execution; $this->view->gradeMenu = $this->loadModel('story')->getGradeMenu($storyType, $project); $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); $this->view->gradeGroup = $gradeGroup; $this->view->showGrades = isset($this->config->execution->showGrades) ? $this->config->execution->showGrades : $this->story->getDefaultShowGrades($this->view->gradeMenu); $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable. $this->view->branchPairs = $this->loadModel('branch')->getPairs($productID, 'withClosed'); $this->view->linkedTaskStories = $this->loadModel('story')->getIdListWithTask($execution->id); } /** * 展示需求列表的模块变量。 * Show the story list module variables. * * @param string $type * @param int $param * @param string $storyType * @param object $execution * @param int $productID * @access protected * @return void */ protected function assignModuleForStory(string $type, int $param, string $storyType, object $execution, int $productID): void { $this->loadModel('tree'); if($this->cookie->storyModuleParam) $this->view->module = $this->loadModel('tree')->getById((int)$this->cookie->storyModuleParam); $showModule = !empty($this->config->execution->story->showModule) ? $this->config->execution->story->showModule : ''; $modulePairs = $showModule ? $this->tree->getModulePairs($type == 'byproduct' ? $param : 0, 'story', $showModule) : array(); $createModuleLink = $storyType == 'story' ? 'createStoryLink' : 'createRequirementLink'; if(!$execution->hasProduct && !$execution->multiple) { $moduleTree = $this->tree->getTreeMenu($productID, 'story', 0, array('treeModel', $createModuleLink), array('executionID' => $execution->id, 'productID' => $productID), '', "¶m=$param&storyType=$storyType"); } else { $moduleTree = $this->tree->getProjectStoryTreeMenu($execution->id, 0, array('treeModel', $createModuleLink), array('storyType' => $storyType)); } $this->view->moduleTree = $moduleTree; $this->view->modulePairs = $modulePairs; $this->view->moduleID = $type == 'bymodule' ? $param : 0; } /** * 展示任务看板的相关变量。 * Show the task Kanban related variables. * * @param object $execution * @access protected * @return void */ protected function assignTaskKanbanVars(object $execution) { /* Get user list. */ $userList = array(); $users = $this->loadModel('user')->getPairs('noletter|nodeleted'); $avatarPairs = $this->user->getAvatarPairs('all'); foreach($avatarPairs as $account => $avatar) { if(!isset($users[$account])) continue; $userList[$account]['realname'] = $users[$account]; $userList[$account]['avatar'] = $avatar; } $userList['closed']['account'] = 'Closed'; $userList['closed']['realname'] = 'Closed'; $userList['closed']['avatar'] = ''; /* Get execution linked products. */ $productID = 0; $productNames = array(); $products = $this->loadModel('product')->getProducts($execution->id); if($products) $productID = key($products); foreach($products as $product) $productNames[$product->id] = $product->name; $plans = $this->execution->getPlans(array_keys($products), 'skipParent|withMainPlan|unexpired|noclosed|sortedByDate', $execution->id); $allPlans = array(); if(!empty($plans)) { foreach($plans as $plan) $allPlans += $plan; } if(!isset($_SESSION['limitedExecutions'])) $this->execution->getLimitedExecution(); $project = $this->project->getByID($execution->project); $this->view->hasFrozenStories = $this->project->hasFrozenObject($execution->project, 'SRS'); $this->view->title = $this->lang->execution->kanban; $this->view->userList = $userList; $this->view->realnames = $users; $this->view->productID = $productID; $this->view->productNames = $productNames; $this->view->productNum = count($products); $this->view->allPlans = $allPlans; $this->view->hiddenPlan = $project->model !== 'scrum'; $this->view->execution = $execution; $this->view->project = $project; $this->view->canBeChanged = common::canModify('execution', $execution); $this->view->isLimited = !$this->app->user->admin && strpos(",{$this->session->limitedExecutions},", ",{$execution->id},") !== false; } /** * 展示用例列表的相关变量。 * Show the case list related variables. * * @param int $executionID * @param int $productID * @param string $branchID * @param int $moduleID * @param int $param * @param string $orderBy * @param string $type * @param object $pager * @access protected * @return void */ protected function assignTestcaseVars(int $executionID, int $productID, string $branchID, int $moduleID, int $param, string $orderBy, string $type, object $pager) { $this->loadModel('tree'); /* Get cases. */ $cases = $this->loadModel('testcase')->getExecutionCases($type, $executionID, $productID, $branchID, $moduleID, $param, $orderBy, $pager); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'testcase', false); $cases = $this->testcase->appendData($cases, 'case'); $cases = $this->loadModel('story')->checkNeedConfirm($cases); /* Get module tree.*/ if($executionID and empty($productID)) { $moduleTree = $this->tree->getCaseTreeMenu($executionID, $productID, 0, array('treeModel', 'createCaseLink')); } else { $moduleTree = $this->tree->getTreeMenu($productID, 'case', 0, array('treeModel', 'createCaseLink'), array('projectID' => $executionID, 'productID' => $productID, 'branchID' => $branchID), $branchID); } $tree = $moduleID ? $this->tree->getByID($moduleID) : ''; $showModule = !empty($this->config->execution->testcase->showModule) ? $this->config->execution->testcase->showModule : ''; $this->view->cases = $cases; $this->view->scenes = $this->testcase->getSceneMenu($productID, $moduleID);; $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->title = $this->lang->execution->testcase; $this->view->executionID = $executionID; $this->view->productID = $productID; $this->view->product = $this->loadModel('product')->getByID((int)$productID); $this->view->orderBy = $orderBy; $this->view->pager = $pager; $this->view->type = $type; $this->view->branchID = $branchID; $this->view->branchTagOption = $this->loadModel('branch')->getPairs($productID, 'withClosed'); $this->view->recTotal = $pager->recTotal; $this->view->showBranch = $this->loadModel('branch')->showBranch($productID); $this->view->stories = array( 0 => '') + $this->loadModel('story')->getPairs($productID); $this->view->moduleTree = $moduleTree; $this->view->moduleID = $moduleID; $this->view->moduleName = $moduleID ? $tree->name : $this->lang->tree->all; $this->view->modulePairs = $showModule ? $this->tree->getModulePairs($productID, 'bug', $showModule) : array(); } /** * 展示测试单的相关变量。 * Show the testtask related variables. * * @param array $tasks * @access protected * @return void */ protected function assignTesttaskVars(array $tasks) { /* Compute rowspan. */ $productGroup = array(); $waitCount = 0; $testingCount = 0; $blockedCount = 0; $doneCount = 0; foreach($tasks as $task) { $productGroup[$task->product][] = $task; if($task->status == 'wait') $waitCount ++; if($task->status == 'doing') $testingCount ++; if($task->status == 'blocked') $blockedCount ++; if($task->status == 'done') $doneCount ++; if($task->build == 'trunk' || empty($task->buildName)) $task->buildName = $this->lang->trunk; } $lastProduct = ''; foreach($tasks as $taskID => $task) { $task->rawStatus = $task->status; $task->status = $this->processStatus('testtask', $task); $task->rowspan = 0; if($lastProduct !== $task->product) { $lastProduct = $task->product; if(!empty($productGroup[$task->product])) $task->rowspan = count($productGroup[$task->product]); } } $this->view->waitCount = $waitCount; $this->view->testingCount = $testingCount; $this->view->blockedCount = $blockedCount; $this->view->doneCount = $doneCount; $this->view->tasks = $tasks; } /** * 展示执行详情的相关变量。 * Show the view related variables. * * @param int $executionID * @access protected * @return void */ protected function assignViewVars(int $executionID) { $this->executeHooks($executionID); $userPairs = array(); $userList = array(); $users = $this->loadModel('user')->getList('all'); foreach($users as $user) { $userList[$user->account] = $user; $userPairs[$user->account] = $user->realname; } /* Get linked branches. */ $branches = $this->project->getBranchesByProject($executionID); $linkedProductIdList = empty($branches) ? '' : array_keys($branches); $products = $this->loadModel('product')->getProducts($executionID, 'all', '', true, $linkedProductIdList, false); $linkedBranches = array(); foreach($products as $product) { if(isset($product->branches)) { foreach($product->branches as $branchID) $linkedBranches[$branchID] = $branchID; } } $this->view->users = $userPairs; $this->view->userList = $userList; $this->view->products = $products; $this->view->branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products), 'ignoreNormal', $linkedBranches); $this->view->planGroups = $this->execution->getPlans(array_keys($products)); $this->view->actions = $this->loadModel('action')->getList('execution', $executionID); $this->view->dynamics = $this->loadModel('action')->getDynamic('all', 'all', 'date_desc', 50, 'all', 'all', $executionID); $this->view->teamMembers = $this->execution->getTeamMembers($executionID); $this->view->docLibs = $this->loadModel('doc')->getLibsByObject('execution', $executionID); $this->view->statData = $this->execution->statRelatedData($executionID); } /** * 构造任务分组视图数据。 * Build task group data. * * @param string $groupBy story|status|pri|assignedTo|finishedBy|closedBy|type * @param array $tasks * @param array $users * @access protected * @return array */ protected function buildGroupTasks(string $groupBy = 'story', array $tasks = array(), array $users = array()): array { $groupTasks = array(); $groupByList = array(); foreach($tasks as $task) { if($groupBy == 'story') { $groupTasks[$task->story][] = $task; $groupByList[$task->story] = $task->storyTitle; } elseif($groupBy == 'status') { $statusList = arrayUnion($this->lang->task->statusList, array('changed' => $this->lang->task->storyChange)); $groupTasks[$statusList[$task->status]][] = $task; } elseif($groupBy == 'assignedTo') { if(isset($task->team)) { $groupTasks = $this->buildGroupMultiTask($groupBy, $task, $users, $groupTasks); } else { $groupTasks[$task->assignedToRealName][] = $task; } } elseif($groupBy == 'finishedBy') { if(isset($task->team)) { $task->consumed = $task->estimate = $task->left = 0; $groupTasks = $this->buildGroupMultiTask($groupBy, $task, $users, $groupTasks); } else { $groupTasks[$users[$task->finishedBy]][] = $task; } } elseif($groupBy == 'closedBy') { $groupTasks[$users[$task->closedBy]][] = $task; } elseif($groupBy == 'type') { $groupTasks[$this->lang->task->typeList[$task->type]][] = $task; } else { $groupTasks[$task->$groupBy][] = $task; } } /* Process closed data when group by assignedTo. */ if($groupBy == 'assignedTo' && isset($groupTasks['Closed'])) { $closedTasks = $groupTasks['Closed']; unset($groupTasks['Closed']); $groupTasks['closed'] = $closedTasks; } return array($groupTasks, $groupByList); } /** * 构建多人任务的分组视图数据。 * Build group data for multiple task. * * @param string $groupBy * @param object $task * @param array $users * @param array $groupTasks * @access protected * @return array */ protected function buildGroupMultiTask(string $groupBy, object $task, array $users, array $groupTasks): array { foreach($task->team as $team) { if($team->left != 0 && $groupBy == 'finishedBy') { $task->estimate += $team->estimate; $task->consumed += $team->consumed; $task->left += $team->left; continue; } $cloneTask = clone $task; $cloneTask->{$groupBy} = $team->account; $cloneTask->estimate = $team->estimate; $cloneTask->consumed = $team->consumed; $cloneTask->left = $team->left; if($team->left == 0 || $groupBy == 'finishedBy') $cloneTask->status = 'done'; $realname = zget($users, $team->account); $cloneTask->assignedToRealName = $realname; $groupTasks[$realname][] = $cloneTask; } if($groupBy == 'finishedBy' && !empty($task->left)) $groupTasks[$users[$task->finishedBy]][] = $task; return $groupTasks; } /** * 构建需求列表的搜索表单数据。 * Build the search form data to story list. * * @param object $execution * @param int $productID * @param array $products * @param int $queryID * @param string $actionURL * @access protected * @return void */ protected function buildStorySearchForm(object $execution, int $productID, array $products, int $queryID, string $actionURL): void { $modules = array(); $productModules = array(); $executionModules = $this->loadModel('tree')->getTaskTreeModules($execution->id, true); if($productID) { $product = $products[$productID]; $productModules = $this->tree->getOptionMenu($productID, 'story', 0, $product->branches); } else { foreach($products as $product) $productModules += $this->tree->getOptionMenu($product->id, 'story', 0, $product->branches); } if(commonModel::isTutorialMode()) { $modules = $this->loadModel('tutorial')->getModulePairs(); } else { foreach($productModules as $branchID => $moduleList) { foreach($moduleList as $moduleID => $moduleName) { if($moduleID && !isset($executionModules[$moduleID])) continue; $modules[$moduleID] = ((count($products) >= 2 && $moduleID) ? $product->name : '') . $moduleName; } } } $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products)); $this->execution->buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, 'executionStory', $execution); } /** * 将导入的Bug转为任务。 * Change imported bugs to the tasks. * * @param object $execution * @param array $postData * @access protected * @return array */ protected function buildTasksForImportBug(object $execution, array $postData) { $this->loadModel('task'); $tasks = array(); $bugs = $this->loadModel('bug')->getByIdList(array_keys($postData)); $showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : ''; $modules = $this->loadModel('tree')->getTaskOptionMenu($execution->id, 0, $showAllModule ? 'allModule' : ''); $now = helper::now(); $requiredFields = str_replace(array(',story,', ',module,'), ',', ',' . $this->config->task->create->requiredFields . ','); $requiredFields = trim($requiredFields, ','); foreach($postData as $bugID => $task) { $bug = zget($bugs, $bugID, ''); if(empty($bug)) continue; unset($task->id); $task->bug = $bug; $task->project = $execution->project; $task->execution = $execution->id; $task->story = $bug->story; $task->storyVersion = $bug->storyVersion; $task->module = isset($modules[$bug->module]) ? $bug->module : 0; $task->fromBug = $bugID; $task->name = $bug->title; $task->type = 'devel'; $task->consumed = 0; $task->status = 'wait'; $task->openedDate = $now; $task->openedBy = $this->app->user->account; $task->estStarted = $task->estStarted ? $task->estStarted : null; $task->deadline = $task->deadline ? $task->deadline : null; if($task->estimate !== '') $task->left = $task->estimate; if(!empty($task->assignedTo)) $task->assignedDate = $now; /* Check task required fields. */ foreach(explode(',', $requiredFields) as $field) { if(empty($field)) continue; if(!isset($task->$field)) continue; if(!empty($task->$field)) continue; dao::$errors["{$field}[{$bugID}]"] = 'ID: ' . $bugID . sprintf($this->lang->error->notempty, $this->lang->task->$field); return false; } if(!preg_match("/^[0-9]+(.[0-9]+)?$/", (string)$task->estimate) and !empty($task->estimate)) { dao::$errors["{$field}[{$bugID}]"] = 'ID: ' . $bugID . $this->lang->task->error->estimateNumber; return false; } if(!helper::isZeroDate($task->deadline) && $task->estStarted > $task->deadline) { dao::$errors["deadline[{$bugID}]"] = 'ID: ' . $bugID . $this->lang->task->error->deadlineSmall; return false; } if(!empty($this->config->limitTaskDate)) { $this->task->checkEstStartedAndDeadline($execution->id, (string)$task->estStarted, (string)$task->deadline); if(dao::isError()) return false; } $tasks[$bugID] = $task; } return $tasks; } /** * 构建导入Bug的搜索表单数据。 * Build the search form data to import the Bug. * * @param object $execution * @param int $queryID * @param array $products * @param array $executions * @param array $projects * @access protected * @return void */ protected function buildImportBugSearchForm(object $execution, int $queryID, array $products, array $executions, array $projects) { $project = $this->loadModel('project')->getByID($execution->project); $this->config->bug->search['actionURL'] = $this->createLink('execution', 'importBug', "executionID=$execution->id&browseType=bySearch¶m=myQueryID"); $this->config->bug->search['queryID'] = $queryID; if(!empty($products)) { $this->config->bug->search['params']['product']['values'] = array(''=>'') + $products + array('all'=>$this->lang->execution->aboveAllProduct); } else { $this->config->bug->search['params']['product']['values'] = array(''=>''); } if(!$execution->multiple) { unset($this->config->bug->search['fields']['execution'], $this->config->bug->search['params']['execution']); } else { $this->config->bug->search['params']['execution']['values'] = array(''=>'') + $executions + array('all'=>$this->lang->execution->aboveAllExecution); } $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairs(array_keys($products)); $this->config->bug->search['module'] = 'importBug'; $this->config->bug->search['params']['confirmed']['values'] = $this->lang->bug->confirmedList; $this->loadModel('tree'); $bugModules = array(); foreach($products as $productID => $productName) { $productModules = $this->tree->getOptionMenu($productID, 'bug', 0, 'all'); foreach($productModules as $moduleID => $moduleName) { if(empty($moduleID)) { $bugModules[$moduleID] = $moduleName; continue; } $bugModules[$moduleID] = $productName . $moduleName; } } $this->config->bug->search['params']['module']['values'] = $bugModules; $this->config->bug->search['params']['project']['values'] = $projects; $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs(array_keys($products), 'all', 'withbranch|releasetag'); unset($this->config->bug->search['fields']['resolvedBy']); unset($this->config->bug->search['fields']['closedBy']); unset($this->config->bug->search['fields']['status']); unset($this->config->bug->search['fields']['toTask']); unset($this->config->bug->search['fields']['toStory']); unset($this->config->bug->search['fields']['resolution']); unset($this->config->bug->search['fields']['resolvedBuild']); unset($this->config->bug->search['fields']['resolvedDate']); unset($this->config->bug->search['fields']['closedDate']); unset($this->config->bug->search['fields']['branch']); if(empty($execution->multiple) && empty($execution->hasProduct)) unset($this->config->bug->search['fields']['plan']); if(empty($project->hasProduct)) { unset($this->config->bug->search['fields']['product']); if($project->model !== 'scrum') unset($this->config->bug->search['fields']['plan']); } unset($this->config->bug->search['params']['resolvedBy']); unset($this->config->bug->search['params']['closedBy']); unset($this->config->bug->search['params']['status']); unset($this->config->bug->search['params']['toTask']); unset($this->config->bug->search['params']['toStory']); unset($this->config->bug->search['params']['resolution']); unset($this->config->bug->search['params']['resolvedBuild']); unset($this->config->bug->search['params']['resolvedDate']); unset($this->config->bug->search['params']['closedDate']); unset($this->config->bug->search['params']['branch']); $this->loadModel('search')->setSearchParams($this->config->bug->search); } /** * 检查创建的表单数据。 * Check the form data for create. * * @access protected * @return bool */ protected function checkPostForCreate(): bool { if(empty($_POST['project'])) { dao::$errors['project'] = $this->lang->execution->projectNotEmpty; return false; } $projectID = (int)$_POST['project']; $project = $this->loadModel('project')->fetchByID($projectID); $this->execution->checkBeginAndEndDate($projectID, $_POST['begin'], $_POST['end'], isset($_POST['parent']) ? (int)$this->post->parent : $projectID); if(dao::isError()) return false; /* Judge workdays is legitimate. */ $workdays = helper::diffDate($_POST['end'], $_POST['begin']) + 1; if(isset($_POST['days']) && $_POST['days'] > $workdays) { dao::$errors['days'] = sprintf($this->lang->project->workdaysExceed, $workdays); return false; } if(!empty($_POST['products'])) { $_POST['products'] = array_filter($_POST['products']); $this->app->loadLang('project'); $multipleProducts = $this->loadModel('product')->getMultiBranchPairs(); foreach($_POST['products'] as $index => $productID) { if(empty($_POST['branch'][$index])) continue; $branches = implode(',', $_POST['branch'][$index]); if(isset($multipleProducts[$productID]) && $branches == '') { dao::$errors["branch[{$index}][]"] = $this->lang->project->error->emptyBranch; return false; } } } /* Determine whether to add a sprint or a stage according to the model of the execution. */ if($project->model == 'waterfall' || $project->model == 'waterfallplus') { if(empty($_POST['products'])) dao::$errors['products[0]'] = $this->lang->project->errorNoProducts; if(dao::isError()) return false; if(isset($this->config->setPercent) && $this->config->setPercent == 1) { $this->execution->checkWorkload('create', (int)$this->post->percent, $project); if(dao::isError()) return false; } } return true; } /** * 构造创建执行的数据。 * Build the data for create execution. * * @access protected * @return object|false */ protected function buildExecutionForCreate(): object|false { $now = helper::now(); $project = $this->loadModel('project')->fetchByID((int)$_POST['project']); $type = 'sprint'; if($project) $type = zget($this->config->execution->modelList, $project->model, 'sprint'); $fields = $this->config->execution->form->create; foreach(explode(',', trim($this->config->execution->create->requiredFields, ',')) as $field) $fields[$field]['required'] = true; if(!isset($_POST['code'])) $fields['code']['required'] = false; if(!isset($_POST['percent'])) $fields['percent']['required'] = false; $this->config->execution->create->requiredFields = implode(',', array_keys(array_filter(array_map(function($config){return $config['required'] == true;}, $fields)))); $this->correctErrorLang(); $execution = form::data($fields) ->setDefault('openedBy', $this->app->user->account) ->setDefault('openedDate', $now) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->setDefault('type', $type) ->setDefault('team', $this->post->name) ->setDefault('parent', $this->post->project) ->setDefault('isTpl', $project->isTpl) ->setIF($this->post->parent, 'parent', $this->post->parent) ->setIF($this->post->heightType == 'auto', 'displayCards', 0) ->setIF($this->post->acl == 'open', 'whitelist', '') ->setIF($this->post->type, 'type', $this->post->type) ->get(); if(!empty($execution->parent) && ($execution->project == $execution->parent)) $execution->hasProduct = $project->hasProduct; if(!empty($execution->parent) && empty($execution->attribute)) { /* 子阶段与父阶段同类型。 */ $parent = $this->execution->fetchByID((int)$execution->parent); $execution->attribute = $parent->attribute; } if($this->post->heightType == 'custom' && !$this->loadModel('kanban')->checkDisplayCards($execution->displayCards)) return false; if(!$this->checkPostForCreate()) return false; /* Set planDuration and realDuration. */ if(in_array($this->config->edition, array('max', 'ipd'))) { $execution->planDuration = $this->loadModel('programplan')->getDuration($execution->begin, $execution->end); if(!empty($execution->realBegan) && !empty($execution->realEnd)) $execution->realDuration = $this->programplan->getDuration($execution->realBegan, $execution->realEnd); } return $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->create['id'], $this->post->uid); } /** * 检查累积流图的日期。 * Check Cumulative flow diagram date. * * @param string $begin * @param string $end * @param string $minDate * @param string $maxDate * @access protected * @return bool */ protected function checkCFDDate(string $begin, string $end, string $minDate, string $maxDate): bool { $dateError = array(); if(empty($begin)) $dateError[] = sprintf($this->lang->error->notempty, $this->lang->execution->charts->cfd->begin); if(empty($end)) $dateError[] = sprintf($this->lang->error->notempty, $this->lang->execution->charts->cfd->end); if(empty($dateError)) { if($begin < $minDate) $dateError[] = sprintf($this->lang->error->gt, $this->lang->execution->charts->cfd->begin, $minDate); if($begin > $maxDate) $dateError[] = sprintf($this->lang->error->lt, $this->lang->execution->charts->cfd->begin, $maxDate); if($end < $minDate) $dateError[] = sprintf($this->lang->error->gt, $this->lang->execution->charts->cfd->end, $minDate); if($end > $maxDate) $dateError[] = sprintf($this->lang->error->lt, $this->lang->execution->charts->cfd->end, $maxDate); } foreach($dateError as $index => $error) { dao::$errors = str_replace(array('。', '.'), array('', ''), $error); return false; } if($begin >= $end) { dao::$errors = $this->lang->execution->charts->cfd->errorBegin; return false; } if(date("Y-m-d", strtotime("-3 months", strtotime($end))) > $begin) { dao::$errors = $this->lang->execution->charts->cfd->errorDateRange; return false; } return true; } /** * 处理版本列表展示数据。 * Process build list display data. * * @param array $buildList * @param string $executionID * @access protected * @return object[] */ protected function processBuildListData(array $buildList, int $executionID = 0): array { $this->loadModel('build'); $productIdList = array(); foreach($buildList as $build) $productIdList[$build->product] = $build->product; /* Get branch name. */ $showBranch = false; $branchGroups = $this->loadModel('branch')->getByProducts($productIdList); $builds = array(); foreach($buildList as $build) { $build->branchName = ''; if(isset($branchGroups[$build->product])) { $showBranch = true; $branchPairs = $branchGroups[$build->product]; foreach(explode(',', trim($build->branch, ',')) as $branchID) { if(isset($branchPairs[$branchID])) $build->branchName .= "{$branchPairs[$branchID]},"; } $build->branchName = trim($build->branchName, ','); } if($build->scmPath && $build->filePath) { $build->rowspan = 2; $buildInfo = clone $build; $buildInfo->pathType = 'scmPath'; $buildInfo->path = $build->scmPath; $builds[] = $buildInfo; $buildInfo = clone $build; $buildInfo->pathType = 'filePath'; $buildInfo->path = $build->filePath; $builds[] = $buildInfo; } else { $build->pathType = empty($build->scmPath) ? 'filePath' : 'scmPath'; $build->path = empty($build->scmPath) ? $build->filePath : $build->scmPath; $builds[] = $build; } if(!$build->system) $build->system = ''; } if(!$showBranch) unset($this->config->build->dtable->fieldList['branch']); unset($this->config->build->dtable->fieldList['execution']); return $builds; } /** * 构建产品下拉选择数据。 * Build product drop-down select data. * * @param int $executionID * @param int $productID * @param object[] $products * @access protected * @return array */ protected function buildProductSwitcher(int $executionID, int $productID, array $products) { $productOption = array(); $branchOption = array(); $programIdList = array(); if(count($products) > 1) $productOption[0] = $this->lang->product->all; foreach($products as $productData) $programIdList[$productData->program] = $productData->program; $programPairs = $this->loadModel('program')->getPairsByList($programIdList); $linePairs = $this->loadModel('product')->getLinePairs($programIdList); foreach($products as $productData) { $programName = isset($programPairs[$productData->program]) ? $programPairs[$productData->program] . ' / ' : ''; $lineName = isset($linePairs[$productData->line]) ? $linePairs[$productData->line] . ' / ' : ''; $productOption[$productData->id] = $programName . $lineName . $productData->name; } $product = $this->product->getById((int)$productID); if($product and $product->type != 'normal') { /* Display status of branch. */ $branches = $this->loadModel('branch')->getList($productID, $executionID, 'all'); foreach($branches as $branchInfo) { $branchOption[$branchInfo->id] = $branchInfo->name . ($branchInfo->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : ''); } } return array($productOption, $branchOption); } /** * 构建执行团队成员信息。 * Build execution team member information. * * @param array $currentMembers * @param array $members2Import * @param array $deptUsers * @param int $days * @access protected * @return array */ protected function buildMembers(array $currentMembers, array $members2Import, array $deptUsers, int $days): array { $teamMembers = array(); foreach($currentMembers as $account => $member) { $member->memberType = 'default'; $teamMembers[$account] = $member; } foreach($members2Import as $account => $member2Import) { $member2Import->memberType = 'import'; $member2Import->days = $days; $member2Import->limited = 'no'; $teamMembers[$account] = $member2Import; } $roles = $this->loadModel('user')->getUserRoles(array_keys($deptUsers)); foreach($deptUsers as $deptAccount => $userName) { if(isset($currentMembers[$deptAccount]) || isset($members2Import[$deptAccount])) continue; $deptMember = new stdclass(); $deptMember->memberType = 'dept'; $deptMember->account = $deptAccount; $deptMember->role = zget($roles, $deptAccount, ''); $deptMember->days = $days; $deptMember->hours = $this->config->execution->defaultWorkhours; $deptMember->limited = 'no'; $teamMembers[$deptAccount] = $deptMember; } for($j = 0; $j < 5; $j ++) { $newMember = new stdclass(); $newMember->memberType = 'add'; $newMember->account = ''; $newMember->role = ''; $newMember->days = $days; $newMember->hours = $this->config->execution->defaultWorkhours; $newMember->limited = 'no'; $teamMembers[] = $newMember; } return $teamMembers; } /** * 构造待更新的团队成员数据。 * Construct the team member data to be updated. * * @param object $execution * @access protected * @return array */ protected function buildMembersForManageMembers(object $execution) { $members = form::batchData()->get(); $oldMemberDays = $this->dao->select('`account`, `days`')->from(TABLE_TEAM)->where('root')->eq($execution->id)->andWhere('type')->eq('execution')->fetchPairs('account', 'days'); foreach($members as $rowIndex => $member) { $member->root = $execution->id; if($member->hours > 24) dao::$errors["hours[$rowIndex]"] = $this->lang->execution->errorHours; if(!empty($execution->days)) { if(isset($oldMemberDays[$member->account]) && $member->days == $oldMemberDays[$member->account]) continue; if($member->days > $execution->days) dao::$errors["days[$rowIndex]"] = sprintf($this->lang->execution->daysGreaterProject, $execution->days); } } return $members; } /** * 根据过滤规则,筛选任务分组数据。 * Filter task group data based on the filter rules. * * @param array $groupTasks * @param string $groupBy * @param string $filter * @param int $allCount * @param array $tasks * @access protected * @return array */ protected function filterGroupTasks(array $groupTasks, string $groupBy, string $filter, int $allCount, array $tasks): array { if($filter == 'all') return array($groupTasks, $allCount); if($groupBy == 'story' && $filter == 'linked' && isset($groupTasks[0])) { $allCount -= count($groupTasks[0]); unset($groupTasks[0]); } elseif($groupBy == 'pri' && $filter == 'noset') { foreach($groupTasks as $pri => $tasks) { if($pri) { $allCount -= count($tasks); unset($groupTasks[$pri]); } } } elseif($groupBy == 'assignedTo' && $filter == 'undone') { $multiTaskCount = array(); foreach($groupTasks as $assignedTo => $tasks) { foreach($tasks as $i => $task) { if($task->status != 'wait' && $task->status != 'doing') { if($task->mode == 'multi' && !isset($multiTaskCount[$task->id])) { $multiTaskCount[$task->id] = true; $allCount -= 1; } elseif($task->mode != 'multi') { $allCount -= 1; } unset($groupTasks[$assignedTo][$i]); } } } } elseif(($groupBy == 'finishedBy' || $groupBy == 'closedBy') && isset($tasks[''])) { $allCount -= count($tasks['']); unset($tasks['']); } return array($groupTasks, $allCount); } /** * 设置最近五次执行。 * Set the recent five executions. * * @param int $executionID * @access protected * @return void */ protected function setRecentExecutions(int $executionID) { if($this->session->multiple) { $recentExecutions = isset($this->config->execution->recentExecutions) ? explode(',', $this->config->execution->recentExecutions) : array(); array_unshift($recentExecutions, $executionID); $recentExecutions = array_slice(array_unique($recentExecutions), 0, 5); $recentExecutions = implode(',', $recentExecutions); $this->loadModel('setting'); if(empty($this->config->execution->recentExecutions) || $this->config->execution->recentExecutions != $recentExecutions) $this->setting->updateItem($this->app->user->account . 'common.execution.recentExecutions', $recentExecutions); if(empty($this->config->execution->lastExecution) || $this->config->execution->lastExecution != $executionID) $this->setting->updateItem($this->app->user->account . 'common.execution.lastExecution', $executionID); } } /** * 设置任务页面的Cookie和Session。 * Set task page storage. * * @access protected * @return void */ protected function setTaskPageStorage(int $executionID, string $orderBy, string $browseType, int $param = 0) { helper::setcookie('preExecutionID', (string)$executionID); helper::setcookie('executionTaskOrder', $orderBy); if($this->cookie->preExecutionID != $executionID) { helper::setcookie('moduleBrowseParam', '0'); helper::setcookie('productBrowseParam', '0'); } if($browseType == 'bymodule') { helper::setcookie('moduleBrowseParam', (string)$param); helper::setcookie('productBrowseParam', '0'); } elseif($browseType == 'byproduct') { helper::setcookie('moduleBrowseParam', '0'); helper::setcookie('productBrowseParam', (string)$param); } else { $this->session->set('taskBrowseType', $browseType); } if($browseType == 'bymodule' && $this->session->taskBrowseType == 'bysearch') $this->session->set('taskBrowseType', 'unclosed'); } /** * 构建执行看板的数据。 * Build the data to execution Kanban. * * @param array $projectIdList * @param array $executions * @access protected * @return void */ protected function buildExecutionKanbanData(array $projectIdList, array $executions) { $projectCount = 0; $statusCount = array(); $myExecutions = array(); $kanbanGroup = array(); $teams = $this->execution->getMembersByIdList(toIntArray($this->app->user->view->sprints)); foreach($projectIdList as $projectID) { foreach(array_keys($this->lang->execution->statusList) as $status) { if(!isset($statusCount[$status])) $statusCount[$status] = 0; foreach($executions as $execution) { if($execution->status == $status) { if(isset($teams[$execution->id][$this->app->user->account])) $myExecutions[$status][$execution->id] = $execution; if($execution->project == $projectID) $kanbanGroup[$projectID][$status][$execution->id] = $execution; } } $statusCount[$status] += isset($kanbanGroup[$projectID][$status]) ? count($kanbanGroup[$projectID][$status]) : 0; /* Max 2 closed executions. */ if($status == 'closed') { list($myExecutions, $kanbanGroup) = $this->processExecutionKanbanData($myExecutions, $kanbanGroup, $projectID, $status); } } if(empty($kanbanGroup[$projectID])) continue; $projectCount ++; } return array($projectCount, $statusCount, $myExecutions, $kanbanGroup); } /** * 获取可以导入到执行中的Bug。 * * @param int $executionID * @param array $productIdList * @param string $browseType * @param int $queryID * @param object $pager * @access protected * @return array */ protected function getImportBugs(int $executionID, array $productIdList, string $browseType, int $queryID, object $pager): array { $this->loadModel('bug'); $bugs = array(); if($browseType != "bysearch") { $bugs = $this->bug->getActiveAndPostponedBugs($productIdList, $executionID, $pager); } else { if($queryID) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('importBugQuery', $query->sql); $this->session->set('importBugForm', $query->form); } else { $this->session->set('importBugQuery', ' 1 = 1'); } } else { if($this->session->importBugQuery === false) $this->session->set('importBugQuery', ' 1 = 1'); } $bugQuery = str_replace("`product` = 'all'", "`product`" . helper::dbIN($productIdList), $this->session->importBugQuery); // Search all execution. $bugs = $this->execution->getSearchBugs($productIdList, $executionID, $bugQuery, 'id_desc', $pager); } return $bugs; } /** * 获取打印看板的数据。 * Get printed kanban data. * * @param int $executionID * @param array $stories * @access protected * @return array */ protected function getPrintKanbanData(int $executionID, array $stories): array { $kanbanTasks = $this->execution->getKanbanTasks($executionID, "id"); $kanbanBugs = $this->loadModel('bug')->getExecutionBugs($executionID); $users = array(); $taskAndBugs = array(); foreach($kanbanTasks as $task) { $status = $task->status; $users[] = $task->assignedTo; $taskAndBugs[$status]["task{$task->id}"] = $task; } foreach($kanbanBugs as $bug) { $status = $bug->status; $status = $status == 'active' ? 'wait' : ($status == 'resolved' ? ($bug->resolution == 'postponed' ? 'cancel' : 'done') : $status); $users[] = $bug->assignedTo; $taskAndBugs[$status]["bug{$bug->id}"] = $bug; } $dataList = array(); $contents = array('story', 'wait', 'doing', 'done', 'cancel'); foreach($contents as $content) { if($content != 'story' and !isset($taskAndBugs[$content])) continue; $dataList[$content] = $content == 'story' ? $stories : $taskAndBugs[$content]; } return array($dataList, $users); } /** * 处理执行看板数据。 * Process execution kanban data. * * @param array $myExecutions * @param array $kanbanGroup * @param int $projectID * @param string $status * @access protected * @return array */ protected function processExecutionKanbanData(array $myExecutions, array $kanbanGroup, int $projectID, string $status): array { if(isset($myExecutions[$status]) and count($myExecutions[$status]) > 2) { foreach($myExecutions[$status] as $executionID => $execution) { unset($myExecutions[$status][$executionID]); $myExecutions[$status][$execution->closedDate] = $execution; } krsort($myExecutions[$status]); $myExecutions[$status] = array_slice($myExecutions[$status], 0, 2, true); } if(isset($kanbanGroup[$projectID][$status]) and count($kanbanGroup[$projectID][$status]) > 2) { foreach($kanbanGroup[$projectID][$status] as $executionID => $execution) { unset($kanbanGroup[$projectID][$status][$executionID]); $kanbanGroup[$projectID][$status][$execution->closedDate] = $execution; } krsort($kanbanGroup[$projectID][$status]); $kanbanGroup[$projectID][$status] = array_slice($kanbanGroup[$projectID][$status], 0, 2); } return array($myExecutions, $kanbanGroup); } /** * 处理打印的看板数据。 * Process printed Kanban data. * * @param int $executionID * @param array $dataList * @access protected * @return array */ protected function processPrintKanbanData(int $executionID, array $dataList): array { $prevKanbans = $this->execution->getPrevKanban($executionID); foreach($dataList as $type => $data) { if(isset($prevKanbans[$type])) { $prevData = $prevKanbans[$type]; foreach($prevData as $id) { if(isset($data[$id])) unset($dataList[$type][$id]); } } } return $dataList; } /** * Check if the product has multiple branch and check if the execution has a product with multiple branch. * * @param int $productID * @param int $executionID * @return bool */ protected function hasMultipleBranch(int $productID, int $executionID): bool { /* Check if the product is multiple branch. */ $multiBranchProduct = false; if($productID) { $product = $this->loadModel('product')->getByID($productID); if($product->type != 'normal') $multiBranchProduct = true; } else { $executionProductList = $this->loadModel('product')->getProducts($executionID); foreach($executionProductList as $executionProduct) { if($executionProduct->type != 'normal') { $multiBranchProduct = true; break; } } } return $multiBranchProduct; } /** * 通过模块,方法和类型生成执行的链接。 * Generate the link of execution by module, method and type. * * @param string $module * @param string $method * @param mixed $type * @access protected * @return string */ protected function getLink(string $module, string $method, string $type = ''): string { $executionModules = array('task', 'testcase', 'build', 'bug', 'case', 'testtask', 'testreport', 'doc'); if(in_array($module, array('task', 'testcase', 'story', 'testtask')) && in_array($method, array('view', 'edit', 'batchedit', 'create', 'batchcreate', 'report', 'batchrun', 'groupcase'))) $method = $module; if(in_array($module, $executionModules) && in_array($method, array('view', 'edit', 'create'))) $method = $module; if(in_array($module, array_merge($executionModules, array('story', 'product')))) $module = 'execution'; if($module == 'execution' && $method == 'create') return ''; if($this->config->edition != 'open') { $flow = $this->loadModel('workflow')->getByModule($module); if(!empty($flow) && in_array($flow->app, array('scrum', 'waterfall', 'kanbanProject'))) $flow->app = 'project'; if(!empty($flow) && $flow->buildin == '0') return helper::createLink('flow', 'ajaxSwitchBelong', "objectID=%s&moduleName=$module") . "#app=$flow->app"; } $link = helper::createLink($module, $method, "executionID=%s"); if($module == 'execution' && ($method == 'index' || $method == 'all')) { $link = helper::createLink($module, 'task', "executionID=%s"); } elseif($module == 'execution' && $method == 'storyview') { $link = helper::createLink($module, 'story', "executionID=%s"); } elseif($module == 'execution' && in_array($method, array('kanban', 'cfd'))) { $link = helper::createLink($module, 'view', "executionID=%s"); } elseif($module == 'execution' && $method == 'autoschedule') { $link = helper::createLink('task', 'autoSchedule', "executionID=%s"); } elseif($module == 'bug' && $method == 'create' && $this->app->tab == 'execution') { $link = helper::createLink($module, $method, "productID=0&branch=0&executionID=%s"); } elseif(in_array($module, array('bug', 'case', 'testtask', 'testreport')) && strpos(',view,edit,', ",$method,") !== false) { $link = helper::createLink('execution', $module, "executionID=%s"); } elseif($module == 'repo' && $method == 'review') { $link = helper::createLink('repo', 'review', "repoID=0&browseType=all&executionID=%s") . '#app=execution'; } elseif($module == 'repo') { $link = helper::createLink('repo', 'browse', "repoID=0&branchID=&executionID=%s"); } elseif($module == 'mr') { $link = helper::createLink('mr', 'browse', "repoID=0&mode=status¶m=opened&objectID=%s") . '#app=execution'; } elseif($module == 'doc') { $link = helper::createLink('doc', $method, "type=execution&objectID=%s&from=execution"); } elseif($module == 'pssp') { $link = helper::createLink($module, 'browse', "projectID=%s") . "#app={$this->app->tab}"; } elseif(in_array($module, array('issue', 'risk', 'opportunity', 'auditplan', 'nc', 'meeting'))) { $link = helper::createLink($module, 'browse', "executionID=%s&from=execution"); } elseif(($module == 'testreport' && $method == 'create') || ($module == 'execution' && $method == 'cases')) { $link = helper::createLink('execution', 'testtask', "executionID=%s"); } elseif($method == 'zerocase') { $link = helper::createLink('execution', 'testcase', "executionID=%s"); } elseif($method == 'createrelation') { $link = helper::createLink('execution', 'relation', "executionID=%s"); } elseif($module == 'project' && $method == 'deliverablechecklist') { $link = helper::createLink('project', 'deliverableChecklist', "projectID=%s") . "#app={$this->app->tab}"; } if($type != '') $link .= "&type=$type"; return $link; } /** * 设置cookie和session。 * Set the cookie and session. * * @param string $executionID * @param string $type all|byModule|byProduct|byBranch|bySearch * @param string $param * @param string $orderBy * @access protected * @return int */ protected function setStorageForStory(string $executionID, string $type, string $param, string $orderBy): int { $productID = 0; if($this->cookie->storyPreExecutionID != $executionID) { $_COOKIE['storyModuleParam'] = $_COOKIE['storyProductParam'] = $_COOKIE['storyBranchParam'] = 0; helper::setcookie('storyModuleParam', '0'); helper::setcookie('storyProductParam', '0'); helper::setcookie('storyBranchParam', '0'); } if($type == 'bymodule') { $module = $this->loadModel('tree')->getByID((int)$param); $productID = isset($module->root) ? $module->root : 0; helper::setcookie('storyModuleParam', $param); helper::setcookie('storyProductParam', '0'); helper::setcookie('storyBranchParam', '0'); } elseif($type == 'byproduct') { $productID = $param; helper::setcookie('storyModuleParam', '0'); helper::setcookie('storyProductParam', $param); helper::setcookie('storyBranchParam', '0'); } elseif($type == 'bybranch') { helper::setcookie('storyModuleParam', '0'); helper::setcookie('storyProductParam', '0'); helper::setcookie('storyBranchParam', $param); } $this->session->set('executionStoryBrowseType', $type); $this->session->set('storyBrowseType', $type, 'execution'); $uri = $this->app->getURI(true); $this->session->set('storyList', $uri, $this->app->tab); $this->session->set('executionStoryList', $uri, 'execution'); helper::setcookie('executionStoryOrder', $orderBy); helper::setcookie('storyPreExecutionID', $executionID); if($this->app->tab == 'project' && $this->cookie->storyProductParam != $productID) helper::setcookie('storyProductParam', '0'); return (int)$productID; } /** * Set the more link of user and return users with echo role. * * @param object[]|object $execution * @return array */ protected function setUserMoreLink(mixed $execution = null): array { $appendPo = $appendPm = $appendQd = $appendRd = array(); if(is_array($execution)) { $appendPo = $appendPm = $appendQd = $appendRd = array(); foreach($execution as $item) { $appendPo[$item->PO] = $item->PO; $appendPm[$item->PM] = $item->PM; $appendQd[$item->QD] = $item->QD; $appendRd[$item->RD] = $item->RD; } } elseif(is_object($execution)) { $appendPo[$execution->PO] = $execution->PO; $appendPm[$execution->PM] = $execution->PM; $appendQd[$execution->QD] = $execution->QD; $appendRd[$execution->RD] = $execution->RD; } $this->loadModel('user'); $pmUsers = $this->user->getPairs('noclosed|nodeleted|pmfirst', $appendPm, $this->config->maxCount); if(!empty($this->config->user->moreLink)) $this->config->moreLinks["PM"] = $this->config->user->moreLink; $poUsers = $this->user->getPairs('noclosed|nodeleted|pofirst', $appendPo, $this->config->maxCount); if(!empty($this->config->user->moreLink)) $this->config->moreLinks["PO"] = $this->config->user->moreLink; $qdUsers = $this->user->getPairs('noclosed|nodeleted|qdfirst', $appendQd, $this->config->maxCount); if(!empty($this->config->user->moreLink)) $this->config->moreLinks["QD"] = $this->config->user->moreLink; $rdUsers = $this->user->getPairs('noclosed|nodeleted|devfirst', $appendRd, $this->config->maxCount); if(!empty($this->config->user->moreLink)) $this->config->moreLinks["RD"] = $this->config->user->moreLink; return array($pmUsers, $poUsers, $qdUsers, $rdUsers); } /** * 初始化创建执行的字段。 * Init execution fields for create. * * @param int $projectID * @param array $output * @access protected * @return object */ protected function initFieldsForCreate(int $projectID, array $output = array()): object { $execution = new stdclass(); $execution->project = $projectID; $execution->type = zget($output, 'type', 'sprint'); $execution->name = ''; $execution->code = ''; $execution->team = ''; $execution->acl = 'private'; $execution->whitelist = ''; $this->view->execution = $execution; return $execution; } /** * 通过复制执行ID设置字段值。 * Set execution fields by copy execution id. * * @param object $fields * @param int $copyExecutionID * @access protected * @return object */ protected function setFieldsByCopyExecution(object $fields, int $copyExecutionID): object { if(empty($copyExecutionID)) return $fields; $copyExecution = $this->execution->fetchByID($copyExecutionID); $fields->project = (int)$copyExecution->project; $fields->type = $copyExecution->type; $fields->name = $copyExecution->name; $fields->code = $copyExecution->code; $fields->team = $copyExecution->team; $fields->acl = $copyExecution->acl; $fields->whitelist = $copyExecution->whitelist; $this->view->execution = $copyExecution; return $fields; } /** * 获取已关联的产品。 * Get can link products. * * @param int $copyExecutionID * @param int $planID * @param object|null $project * @access protected * @return array */ protected function getLinkedProducts(int $copyExecutionID, int $planID, object|null $project): array { $products = array(); if($copyExecutionID) $products = $this->loadModel('product')->getProducts($copyExecutionID); if($planID) { $plan = $this->loadModel('productplan')->fetchByID($planID); if($plan->begin == $this->config->productplan->future) $plan->begin = ''; if($plan->end == $this->config->productplan->future) $plan->end = ''; $products = $this->dao->select('t1.id, t1.name, t1.type, t2.branch')->from(TABLE_PRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.product') ->where('t1.id')->eq($plan->product) ->fetchAll('id'); $this->view->plan = $plan; } if(isset($project->hasProduct) && empty($project->hasProduct)) { $product = $this->loadModel('product')->getShadowProductByProject($project->id); $products = array($product->id => $product->name); } return $products; } /** * 获取已关联的分支。 * Set linked branches. * * @param array $products * @param int $copyExecutionID * @param int $planID * @param object|null $project * @access protected * @return void */ protected function setLinkedBranches(array $products, int $copyExecutionID, int $planID, object|null $project) { $projectID = empty($project) ? 0 : $project->id; if(!empty($copyExecutionID)) { $branches = $this->project->getBranchesByProject($copyExecutionID); $plans = $this->loadModel('productplan')->getGroupByProduct(array_keys($products), 'skipparent|unexpired'); $branchGroups = $this->execution->getBranchByProduct(array_keys($products), $projectID); } if(!empty($project) and $project->stageBy == 'project') { $products = $this->loadModel('product')->getProducts($project->id); $branches = $this->project->getBranchesByProject($projectID); $plans = $this->loadModel('productplan')->getGroupByProduct(array_keys($products), 'skipparent|unexpired'); } if($products and isset($branches)) { $linkedBranches = array(); foreach($products as $productIndex => $product) { $productPlans[$productIndex] = array(); foreach($branches[$productIndex] as $branchID => $branch) { $linkedBranches[$productIndex][$branchID] = $branchID; if(isset($plans[$productIndex][$branchID])) { foreach($plans[$productIndex][$branchID] as $plan) $productPlans[$productIndex][$branchID][$plan->id] = $plan->title; } } } $this->view->productPlans = $productPlans; $this->view->linkedBranches = $linkedBranches; } if(!empty($planID)) { $plan = $this->loadModel('productplan')->fetchByID($planID); $productPlan = $this->productplan->getPairs($plan->product, $plan->branch, 'unexpired|withMainPlan', true); $linkedBranches = array(); $linkedBranches[$plan->product][$plan->branch] = $plan->branch; $this->view->linkedBranches = $linkedBranches; } $this->view->productPlan = isset($productPlan) ? $productPlan : array(); $this->view->branchGroups = isset($branchGroups) ? $branchGroups : $this->execution->getBranchByProduct(array_keys($products), $projectID); if(isset($project->hasProduct) and empty($project->hasProduct)) { $shadowProduct = $this->loadModel('product')->getShadowProductByProject($project->id); $this->view->productPlan = $this->loadModel('productplan')->getPairs($shadowProduct->id ?? 0, '0,0', 'noclosed,unexpired', true); } } /** * 根据项目获取可关联的产品。 * Get all products for create. * * @param object|null $project * @access protected * @return void */ protected function getAllProductsForCreate(object|null $project) : array { if(empty($project)) return array(); $allProducts = $this->loadModel('product')->getProductPairsByProject($project->id, 'noclosed'); if(!empty($project->hasProduct)) $allProducts = array(0 => '') + $allProducts; return $allProducts; } /** * 设置可复制的执行。 * Set copy projects. * * @param object|null $project * @access protected * @return void */ protected function setCopyProjects(object|null $project) { $parentProject = 0; $projectModel = ''; if($project) { $parentProject = $project->parent; $projectModel = $project->model; if($projectModel == 'agileplus') $projectModel = array('scrum', 'agileplus'); if($projectModel == 'waterfallplus') $projectModel = array('waterfall', 'waterfallplus'); } $copyProjects = $this->loadModel('project')->getPairsByProgram($parentProject, 'noclosed', false, 'order_asc', '', $projectModel, 'multiple'); $copyProjectID = empty($project) ? key($copyProjects) : $project->id; $this->view->copyProjects = $copyProjects; $this->view->copyProjectID = $copyProjectID; $this->view->copyExecutions = empty($copyProjectID) ? array() : $this->execution->getList($copyProjectID, 'all', 'all', 0, 0, 0, null, false); } /** * 修正executionCommon公共语言项。 * Correct execution common lang. * * @param object $project * @param string $type * @access protected * @return bool */ protected function correctExecutionCommonLang(object $project, string $type): bool { if(empty($project)) return false; if($project->model == 'kanban') { global $lang; $executionLang = $lang->execution->common; $executionCommonLang = $lang->executionCommon; $lang->executionCommon = $lang->execution->kanban; $lang->execution->common = $lang->execution->kanban; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; $lang->execution->common = $executionLang; $lang->executionCommon = $executionCommonLang; $lang->execution->typeList['sprint'] = $executionCommonLang; } elseif($project->model == 'waterfall' || $project->model == 'waterfallplus') { $this->app->loadLang('stage'); global $lang; $lang->executionCommon = $lang->execution->stage; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; } if(isset($project->hasProduct) and empty($project->hasProduct)) $this->lang->execution->PO = $this->lang->common->story . $this->lang->execution->owner; return true; } /** * 修正错误提示语言。 * Correct error lang. * * @access protected * @return void */ protected function correctErrorLang(): void { $this->lang->execution->team = $this->lang->execution->teamName; $this->lang->error->unique = $this->lang->error->repeat; /* Redefines the language entries for the fields in the project table. */ foreach(explode(',', $this->config->execution->create->requiredFields) as $field) { if(isset($this->lang->execution->$field)) $this->lang->project->$field = $this->lang->execution->$field; } /* Replace required language. */ if($this->app->tab == 'project') { $this->lang->project->name = $this->lang->execution->name; $this->lang->project->code = $this->lang->execution->code; } else { $this->lang->project->name = $this->lang->execution->execName; $this->lang->project->code = $this->lang->execution->execCode; } } /** * 创建执行后,显示提示页面。 * Display after created. * * @param int $projectID * @param int $executionID * @param int $planID * @param string $confirm * @access protected * @return void */ protected function displayAfterCreated(int $projectID, int $executionID, int $planID, string $confirm = 'no') { $execution = $this->execution->fetchByID($executionID); if(!empty($planID) and $execution->lifetime != 'ops') { if($confirm == 'yes') { $this->execution->linkStories($executionID); } else { $executionProductList = $this->loadModel('product')->getProducts($executionID); $multiBranchProduct = false; array_map(function($executionProduct) use(&$multiBranchProduct){if($executionProduct->type != 'normal') $multiBranchProduct = true;}, $executionProductList); $storyType = ''; $project = $this->loadModel('project')->fetchByID($projectID); if($project) { $this->loadModel('story'); foreach(explode(',', $project->storyType) as $type) $storyType .= $this->lang->story->typeList[$type] . ', '; } if(empty($storyType)) $storyType = $this->lang->story->common; $importPlanStoryTips = sprintf($multiBranchProduct ? $this->lang->execution->importBranchPlanStory : $this->lang->execution->importPlanStory, trim($storyType, ', ')); if($execution->type == 'stage') $importPlanStoryTips = str_replace($this->lang->executionCommon, $this->lang->execution->stage, $importPlanStoryTips); $confirmURL = inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=$planID&confirm=yes"); $cancelURL = inlink('create', "projectID=$projectID&executionID=$executionID"); return $this->send(array('result' => 'success', 'open' => array('confirm' => $importPlanStoryTips, 'url' => $confirmURL, 'canceled' => $cancelURL))); } } if(!empty($projectID) and $execution->type == 'kanban' and $this->app->tab == 'project') return $this->send(array('result' => 'success', 'load' => $this->createLink('project', 'index', "projectID=$projectID"))); if(!empty($projectID) and $execution->type == 'kanban') return $this->send(array('result' => 'success', 'load' => inlink('kanban', "executionID=$executionID"))); $this->view->title = $this->lang->execution->tips; $this->view->executionID = $executionID; $this->view->execution = $this->execution->fetchByID($executionID); $this->display('execution', 'tips'); } /** * 更新迭代关联的计划。 * Update linked plans. * * @param int $executionID * @param string $newPlans * @param string $confirm * @return void */ protected function updateLinkedPlans(int $executionID, string $newPlans = '', string $confirm = 'no') { $projectID = $this->dao->select('project')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('project'); if(!empty($newPlans) and $confirm == 'yes') { $newPlans = explode(',', $newPlans); $this->loadModel('productplan')->linkProject($executionID, $newPlans); $this->productplan->linkProject($projectID, $newPlans); return $this->send(array('result' => 'success', 'load' => inlink('view', "executionID=$executionID"))); } elseif(!empty($newPlans)) { $executionProductList = $this->loadModel('product')->getProducts($executionID); /* 无论是迭代ID还是项目ID都会查到一个与之对应的产品。*/ $multiBranchProduct = false; foreach($executionProductList as $executionProduct) { if($executionProduct->type != 'normal') { $multiBranchProduct = true; break; } } $storyType = ''; $project = $this->loadModel('project')->fetchByID($projectID); if($project) { $this->loadModel('story'); foreach(explode(',', $project->storyType) as $type) $storyType .= $this->lang->story->typeList[$type] . ', '; } if(empty($storyType)) $storyType = $this->lang->story->common; $linkPlanMsg = sprintf($multiBranchProduct ? $this->lang->execution->importBranchEditPlanStory : $this->lang->execution->importEditPlanStory, trim($storyType, ', ')); $confirmURL = inlink('edit', "executionID=$executionID&action=edit&extra=&newPlans=$newPlans&confirm=yes"); $cancelURL = inlink('view', "executionID=$executionID"); return $this->send(array('result' => 'success', 'load' => array('confirm' => $linkPlanMsg, 'confirmed' => $confirmURL, 'canceled' => $cancelURL))); } } /** * Check if the execution can be linked to plan stories. * * @param int $executionID * @param array $oldPlans * @return void */ protected function checkLinkPlan(int $executionID, array $oldPlans) { $oldPlans = explode(',', implode(',' ,$oldPlans)); $newPlans = array(); if(isset($_POST['plans'])) { foreach((array)$_POST['plans'] as $plans) { foreach($plans as $planID) { if(array_search($planID, $oldPlans) === false) $newPlans[$planID] = $planID; } } } $newPlans = array_filter($newPlans); if(!empty($newPlans)) { $newPlans = implode(',', $newPlans); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => inlink('edit', "executionID=$executionID&action=edit&extra=&newPlans=$newPlans&confirm=no"))); } } /** * 获取执行关联的对象,如关联的产品、分支、需求等。 * Get linked objects of this execution. * * @param object $execution * @return object */ protected function getLinkedObjects(object $execution): object { $this->loadModel('project'); $this->loadModel('product'); $this->loadModel('productplan'); $branches = $this->project->getBranchesByProject($execution->id); $linkedProductIdList = empty($branches) ? '' : array_keys($branches); $allProducts = $this->product->getProducts($execution->project, 'noclosed', '', false, $linkedProductIdList); $productPlans = $linkedBranches = $linkedBranchList = array(); $linkedProducts = $this->product->getProducts($execution->id, 'all', '', true, $linkedProductIdList, false); $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts), 'skipparent|unexpired'); $executionStories = $this->project->getStoriesByProject($execution->id); /* If the story of the product which linked the execution, you don't allow to remove the product. */ $unmodifiableProducts = $unmodifiableBranches = $linkedStoryIDList = array(); foreach($linkedProducts as $productID => $linkedProduct) { if(!isset($allProducts[$productID])) $allProducts[$productID] = $linkedProduct->deleted ? $linkedProduct->name . "({$this->lang->product->deleted})" : $linkedProduct->name; $productPlans[$productID] = array(); foreach($branches[$productID] as $branchID => $branch) { if(isset($plans[$productID][$branchID])) { foreach($plans[$productID][$branchID] as $plan) $productPlans[$productID][$branchID][$plan->id] = $plan->title; } $linkedBranchList[$branchID] = $branchID; $linkedBranches[$productID][$branchID] = $branchID; if($branchID != BRANCH_MAIN && isset($plans[$productID][BRANCH_MAIN])) { foreach($plans[$productID][BRANCH_MAIN] as $plan) $productPlans[$productID][BRANCH_MAIN][$plan->id] = $plan->title; } if(!empty($executionStories[$productID][$branchID])) { array_push($unmodifiableProducts, $productID); array_push($unmodifiableBranches, $branchID); $linkedStoryIDList[$productID][$branchID] = $executionStories[$productID][$branchID]->storyIDList; } } if(!empty($linkedProduct->plans)) { foreach($linkedProduct->plans as $linkedPlans) { $planList = explode(',', $linkedPlans); $planList = array_filter($planList); foreach($planList as $planID) { if(isset($plans[$productID][$planID])) continue; $productPlans[$productID][$branchID][$planID] = ''; } } } } $productPlan = array(); if(!empty($planID)) { $plan = $this->productplan->fetchByID((int)$planID); $productPlan = $this->productplan->getPairs($plan->product, $plan->branch, 'unexpired|withMainPlan', true); } $productPlansOrder = array(); foreach($productPlans as $productID => $branchPlans) { foreach($branchPlans as $branchID => $plans) { $orderPlans = $this->loadModel('productplan')->getByIDList(array_keys($plans)); $orderPlans = $this->productplan->relationBranch($orderPlans); $orderPlansMap = array_keys($orderPlans); foreach($orderPlansMap as $planMapID) { if(empty($plans[$planMapID])) $productPlans[$productID][$branchID][$planMapID] = $orderPlans[$planMapID]->title; $productPlansOrder[$productID][$branchID][$planMapID] = $productPlans[$productID][$branchID][$planMapID]; } } } $linkedObjects = new stdclass(); $linkedObjects->currentPlan = !empty($planID) ? $planID : 0; $linkedObjects->productPlan = $productPlan; $linkedObjects->allProducts = $allProducts; $linkedObjects->linkedProducts = $linkedProducts; $linkedObjects->productPlans = array_filter($productPlansOrder); $linkedObjects->linkedBranches = $linkedBranches; $linkedObjects->linkedBranchList = $linkedBranchList; $linkedObjects->linkedStoryIDList = $linkedStoryIDList; $linkedObjects->unmodifiableProducts = $unmodifiableProducts; $linkedObjects->unmodifiableBranches = $unmodifiableBranches; return $linkedObjects; } /** * 获取创建执行后的跳转地址。 * Get location after create. * * @param int $projectID * @param int $executionID * @param string $model * @access protected * @return string */ protected function getAfterCreateLocation(int $projectID, int $executionID, string $model = ''): string { if($this->app->tab == 'doc') return $this->createLink('doc', 'projectSpace', "objectID=$executionID"); if(!empty($_POST['plans'])) return inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=1&confirm=no"); if(!empty($projectID) and $model == 'kanban') { if($this->app->tab == 'project') return $this->config->vision != 'lite' ? $this->createLink('project', 'index', "projectID=$projectID") : $this->createLink('project', 'execution', "status=all&projectID=$projectID"); return inlink('kanban', "executionID=$executionID"); } $execution = $this->execution->fetchByID($executionID); if(!empty($execution->isTpl)) return inlink('task', "executionID=$executionID"); return inlink('create', "projectID=$projectID&executionID=$executionID"); } }