* @package producrplan * @link https://www.zentao.net */ class productplanZen extends productplan { /** * 构建批量编辑计划数据。 * Build plans for batch edit. * * @param int $productID * @access protected * @return array|string */ protected function buildPlansForBatchEdit() { $plans = form::batchData($this->config->productplan->form->batchEdit)->get(); $oldPlans = $this->productplan->getByIDList(array_keys($plans)); $futureConfig = $this->config->productplan->future; foreach($plans as $planID => $plan) { $oldPlan = $oldPlans[$planID]; $parentID = $oldPlan->parent; if(empty($plan->begin)) $plan->begin = $oldPlan->begin; if(empty($plan->end)) $plan->end = $oldPlan->end; if(empty($plan->status)) $plan->status = $oldPlan->status; if($plan->begin > $plan->end && !empty($plan->end)) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginGeEnd, $planID);; if($plan->begin == '') $plan->begin = $this->config->productplan->future; if($plan->end == '') $plan->end = $this->config->productplan->future; if(!empty($_POST['future'][$planID])) { $plan->begin = $this->config->productplan->future; $plan->end = $this->config->productplan->future; } /* Determine whether the begin and end dates of the parent plan and the child plan are correct. */ if($parentID > 0) { $parent = zget($plans, $parentID, $this->productplan->getByID($parentID)); if($parent->begin != $futureConfig && $plan->begin != $futureConfig && $plan->begin < $parent->begin) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginLessThanParentTip, $planID, $plan->begin, $parent->begin); if($parent->end != $futureConfig && $plan->end != $futureConfig && $plan->end > $parent->end) return dao::$errors["end[{$planID}]"] = sprintf($this->lang->productplan->endGreatThanParentTip, $planID, $plan->end, $parent->end); } elseif($parentID == -1 && $plan->begin != $futureConfig) { $childPlans = $this->productplan->getChildren($plan->id); $minBegin = $plan->begin; $maxEnd = $plan->end; foreach($childPlans as $childID => $childPlan) { $childPlan = isset($plans[$childID]) ? $plans[$childID] : $childPlan; if($childPlan->begin < $minBegin && $minBegin != $this->config->productplan->future) $minBegin = $childPlan->begin; if($childPlan->end > $maxEnd && $maxEnd != $this->config->productplan->future) $maxEnd = $childPlan->end; } if($minBegin < $plan->begin && $minBegin != $futureConfig) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginGreaterChildTip, $planID, $plan->begin, $minBegin); if($maxEnd > $plan->end && $maxEnd != $futureConfig) return dao::$errors["end[{$planID}]"] = sprintf($this->lang->productplan->endLessThanChildTip, $planID, $plan->end, $maxEnd); } if($plan->branch == '') $plan->branch = 0; } return $plans; } /** * 设置计划看板页面数据。 * Set kanban page data. * * @param object $product * @param string $branchID * @param string $orderBy * @access protected * @return void */ protected function assignKanbanData($product, $branchID, $orderBy) { $branches = array(); $branchPairs = array(); $planCount = 0; if(!in_array($orderBy, array_keys($this->lang->productplan->orderList))) $orderBy = key($this->lang->productplan->orderList); if($product->type == 'normal') { $planGroup = $this->productplan->getList($product->id, '0', 'all', null, $orderBy, 'skipparent'); $this->view->planCount = count(array_filter($planGroup)); } else { $planGroup = $this->productplan->getGroupByProduct(array($product->id), 'skipparent', $orderBy); $branches = $this->loadModel('branch')->getPairs($product->id, 'active'); foreach($branches as $id => $name) { $plans = isset($planGroup[$product->id][$id]) ? array_filter($planGroup[$product->id][$id]) : array(); $branchPairs[$id] = $name . ' ' . count($plans); $planCount += count($plans); } $this->view->branches = array('all' => $this->lang->productplan->allAB . ' ' . $planCount) + $branchPairs; } $this->view->kanbanList = $this->loadModel('kanban')->getPlanKanban($product, $branchID, $planGroup); } /** * 构造计划列表页面数据。 * Build data for browse page. * * @param array $plans * @param array $branchOption * @access protected * @return array */ protected function buildDataForBrowse($plans, $branchOption) { if(empty($plans)) return $plans; foreach($plans as $plan) { $plan->branchName = ''; if($this->session->currentProductType != 'normal') { foreach(explode(',', $plan->branch) as $branchID) $plan->branchName .= $branchOption[$branchID] . ','; $plan->branchName = trim($plan->branchName, ','); } if($plan->begin == $this->config->productplan->future) $plan->begin = $this->lang->productplan->future; if($plan->end == $this->config->productplan->future) $plan->end = $this->lang->productplan->future; $plan->actions = $this->buildActionsList($plan); $plan->projects = array_values($plan->projects); $plan->desc = strip_tags($plan->desc); } return $plans; } /** * 构造计划列表页面操作。 * Build actions for browse page. * * @param object $plan * @access protected * @return array */ protected function buildActionsList($plan) { $actions = array(); if(common::hasPriv('productplan', 'start')) $actions[] = 'start'; if(common::hasPriv('productplan', 'finish')) $actions[] = 'finish'; if(common::hasPriv('productplan', 'close')) $actions[] = 'close'; if(common::hasPriv('productplan', 'activate')) $actions[] = 'activate'; if(common::hasPriv('execution', 'create')) $actions[] = 'createExecution'; if(count($actions) > 0) $actions[] = 'divider'; if(common::hasPriv('productplan', 'linkStory')) $actions[] = 'linkStory'; if(common::hasPriv('productplan', 'linkBug')) $actions[] = 'linkBug'; if(common::hasPriv('productplan', 'edit')) $actions[] = 'edit'; if(common::hasPriv('productplan', 'create')) $actions[] = 'create'; if(common::hasPriv('productplan', 'delete')) $actions[] = 'delete'; return $actions; } /** * 统计父计划、子计划和独立计划的总数。 * Get the total count of parent plan, child plan and indepentdent plan. * * @param array $planList * @access protected * @return string */ protected function getSummary($planList) { $totalParent = $totalChild = $totalIndependent = 0; foreach($planList as $plan) { if($plan->parent == -1) $totalParent ++; if($plan->parent > 0) $totalChild ++; if($plan->parent == 0) $totalIndependent ++; } return sprintf($this->lang->productplan->summary, count($planList), $totalParent, $totalChild, $totalIndependent); } /** * 设置计划详情页面session信息。 * Set session for view page. * * @param int $planID * @param string $type * @param string $orderBy * @param int $pageID * @param int $recPerPage * @access protected * @return void */ protected function setSessionForViewPage($planID, $type, $orderBy, $pageID, $recPerPage) { if(in_array($type, array('story', 'bug')) && ($orderBy != 'order_desc' || $pageID != 1 || $recPerPage != 100)) { if($type == 'story') { $this->session->set('storyList', $this->app->getURI(true), 'product'); } elseif($type == 'bug') { $this->session->set('bugList', $this->app->getURI(true), 'qa'); } else { $this->session->set('storyList', $this->createLink('productplan', 'view', "planID={$planID}&type={$type}"), $type == 'story' ? 'product' : 'qa'); } } } /** * 设置详情页面的属性。 * Set attributes for view page. * * @param object $plan * @access protected * @return void */ protected function assignViewData($plan) { if($plan->parent > 0) $this->view->parentPlan = $this->productplan->getById($plan->parent); if($plan->parent == '-1') $this->view->childrenPlans = $this->productplan->getChildren($plan->id); $gradeList = $this->loadModel('story')->getGradeList(''); $gradeGroup = array(); foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; $this->view->plan = $plan; $this->view->gradeGroup = $gradeGroup; $this->view->actions = $this->loadModel('action')->getList('productplan', $plan->id); $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->plans = $this->productplan->getPairs($plan->product, $plan->branch, '', true); $this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product); if($this->app->getViewType() == 'json') { unset($this->view->storyPager); unset($this->view->bugPager); } } /** * 构建关联需求页面的搜索表单。 * Build search form for link story page. * * @param object $plan * @param int $queryID * @param string $orderBy * @access protected * @return void */ protected function buildLinkStorySearchForm($plan, $queryID, $orderBy) { $this->app->loadLang('story'); $products = $this->loadModel('product')->getProductPairsByProject((int)$this->session->project); /* Build search form. */ $this->config->product->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$plan->id&type=story&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; $this->config->product->search['fields']['title'] = $this->lang->productplan->storyTitle; $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject); $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true); $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all'); $storyStatusList = $this->lang->story->statusList; unset($storyStatusList['closed']); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList); $product = $this->loadModel('product')->getByID($plan->product); if($product->type == 'normal') { unset($this->config->product->search['fields']['branch']); unset($this->config->product->search['params']['branch']); } else { $branches = $this->loadModel('branch')->getPairsByIdList(explode(',', trim($plan->branch, ','))); $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->product->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches; } $gradeList = $this->loadModel('story')->getGradeList(''); foreach($gradeList as $grade) { if(!$this->config->URAndSR && $grade->type == 'requirement') continue; if(!$this->config->enableER && $grade->type == 'epic') continue; $key = (string)$grade->type . (string)$grade->grade; $gradePairs[$key] = $grade->name; } asort($gradePairs); $this->config->product->search['params']['grade']['values'] = $gradePairs; unset($this->config->product->search['fields']['product']); $this->loadModel('search')->setSearchParams($this->config->product->search); } /** * 构造关联bug页面的搜索表单。 * Build search form for link bug page. * * @param object $plan * @param int $queryID * @param string $orderBy * @access protected * @return void */ protected function buildBugSearchForm($plan, $queryID, $orderBy) { $this->config->bug->search['actionURL'] = $this->createLink('productplan', 'view', "planID={$plan->id}&type=bug&orderBy={$orderBy}&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; $modulePairs = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, 'all'); $this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true); $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($plan->product, $plan->branch); $this->config->bug->search['params']['module']['values'] = $modulePairs; $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs(array($plan->product), 'all', 'releasetag'); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; $this->config->bug->search['params']['module']['values'] = $modulePairs; $this->config->bug->search['params']['project']['values'] = $this->product->getProjectPairsByProduct($plan->product, $plan->branch); unset($this->config->bug->search['fields']['product']); $product = $this->loadModel('product')->getByID($plan->product); if($product->type == 'normal') { unset($this->config->bug->search['fields']['branch']); unset($this->config->bug->search['params']['branch']); } else { $branches = $this->loadModel('branch')->getPairsByIdList(explode(',', trim($plan->branch, ','))); $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->bug->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches; } $this->loadModel('search')->setSearchParams($this->config->bug->search); } /** * 构造需求列表的摘要信息。 * Get the summary of product's stories. * * @param array $stories * @access public * @return string */ public function buildViewSummary($stories) { $totalEstimate = 0.0; $storyIdList = array(); $rateCount = 0; $SRTotal = 0; $URTotal = 0; $ERTotal = 0; foreach($stories as $story) { if($story->type == 'story') $SRTotal += 1; if($story->type == 'requirement') $URTotal += 1; if($story->type == 'epic') $ERTotal += 1; if($story->isParent == '0') $totalEstimate += $story->estimate; if($story->type != 'story') continue; if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed')))) { $storyIdList[] = $story->id; $rateCount ++; } } $casesCount = count($this->loadModel('product')->filterNoCasesStory($storyIdList)); $rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 2); return sprintf($this->lang->productplan->storySummary, $ERTotal, $URTotal, $SRTotal, $totalEstimate, $rate * 100 . "%"); } /** * 构造计划详情页面的操作菜单。 * Build operate menu for plan detail page. * * @param object $plan * @access public * @return array */ public function buildViewActions($plan) { $params = "planID=$plan->id"; $canEdit = common::hasPriv('productplan', 'edit'); $canStart = common::hasPriv('productplan', 'start') && $this->productplan->isClickable($plan, 'start'); $canFinish = common::hasPriv('productplan', 'finish') && $this->productplan->isClickable($plan, 'finish'); $canClose = common::hasPriv('productplan', 'close') && $this->productplan->isClickable($plan, 'close'); $canActivate = common::hasPriv('productplan', 'activate') && $this->productplan->isClickable($plan, 'activate'); $canCreateChild = common::hasPriv('productplan', 'create') && $this->productplan->isClickable($plan, 'create'); $canDelete = common::hasPriv('productplan', 'delete') && $this->productplan->isClickable($plan, 'delete'); $menu = array(); if($canStart) $menu[] = array('icon' => 'play text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->startAB, 'data-url' => helper::createLink('productplan', 'start', $params), 'data-action' => 'start', 'onclick' => 'ajaxConfirmLoad(this)'); if($canFinish) $menu[] = array('icon' => 'checked text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->finishAB, 'data-url' => helper::createLink('productplan', 'finish', $params), 'data-action' => 'finish', 'onclick' => 'ajaxConfirmLoad(this)'); if($canClose) $menu[] = array('icon' => 'off text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->closeAB, 'url' => helper::createLink('productplan', 'close', $params, '', true), 'data-toggle' => 'modal'); if($canActivate) $menu[] = array('icon' => 'magic text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->activateAB, 'data-url' => helper::createLink('productplan', 'activate', $params), 'data-action' => 'activate', 'onclick' => 'ajaxConfirmLoad(this)'); if($canCreateChild) $menu[] = array('icon' => 'split text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->children, 'url' => helper::createLink('productplan', 'create', "product={$plan->product}&branch={$plan->branch}&parent={$plan->id}")); if($canEdit) $menu[] = array('icon' => 'edit text-primary', 'class' => 'ghost', 'text' => $this->lang->edit, 'url' => helper::createLink('productplan', 'edit', $params)); if($canDelete) $menu[] = array('icon' => 'trash text-primary', 'class' => 'ghost', 'text' => $this->lang->delete, 'data-url' => helper::createLink('productplan', 'delete', $params), 'data-action' => 'delete', 'onclick' => 'ajaxConfirmLoad(this)'); return $menu; } /** * 对计划需求按照父子关系重新排序。 * Reorder stories by parent-child relationship. * * @access public * @return void */ public function reorderStories() { /* 获取不分页的 SQL 语句,为重新按照父子关系排序做准备。*/ $sql = $this->dao->get(); if(strpos($sql, 'LIMIT')) $sql = substr($sql, 0, strpos($sql, 'LIMIT')); /* 取出用户重新排序的关键字段。*/ $stories = array(); $query = $this->dao->query($sql); while($story = $query->fetch()) $stories[$story->id] = $story->parent; /* 对需求重新按照父子关系排序,保证进入需求详情后上一页下一页的URL符合预期。 */ $objectList = $this->loadModel('story')->reorderStories($stories); if($objectList) { $this->session->set('storyBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab); $this->session->set('epicBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab); $this->session->set('requirementBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab); } } }