| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- <?php
- /**
- * The zen file of producrplan module of ZenTaoPMS.
- *
- * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
- * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
- * @author Wang Yidong <yidong@easycorp.ltd>
- * @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);
- }
- }
- }
|