zen.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <?php
  2. /**
  3. * The zen file of producrplan module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Wang Yidong <yidong@easycorp.ltd>
  8. * @package producrplan
  9. * @link https://www.zentao.net
  10. */
  11. class productplanZen extends productplan
  12. {
  13. /**
  14. * 构建批量编辑计划数据。
  15. * Build plans for batch edit.
  16. *
  17. * @param int $productID
  18. * @access protected
  19. * @return array|string
  20. */
  21. protected function buildPlansForBatchEdit()
  22. {
  23. $plans = form::batchData($this->config->productplan->form->batchEdit)->get();
  24. $oldPlans = $this->productplan->getByIDList(array_keys($plans));
  25. $futureConfig = $this->config->productplan->future;
  26. foreach($plans as $planID => $plan)
  27. {
  28. $oldPlan = $oldPlans[$planID];
  29. $parentID = $oldPlan->parent;
  30. if(empty($plan->begin)) $plan->begin = $oldPlan->begin;
  31. if(empty($plan->end)) $plan->end = $oldPlan->end;
  32. if(empty($plan->status)) $plan->status = $oldPlan->status;
  33. if($plan->begin > $plan->end && !empty($plan->end)) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginGeEnd, $planID);;
  34. if($plan->begin == '') $plan->begin = $this->config->productplan->future;
  35. if($plan->end == '') $plan->end = $this->config->productplan->future;
  36. if(!empty($_POST['future'][$planID]))
  37. {
  38. $plan->begin = $this->config->productplan->future;
  39. $plan->end = $this->config->productplan->future;
  40. }
  41. /* Determine whether the begin and end dates of the parent plan and the child plan are correct. */
  42. if($parentID > 0)
  43. {
  44. $parent = zget($plans, $parentID, $this->productplan->getByID($parentID));
  45. 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);
  46. 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);
  47. }
  48. elseif($parentID == -1 && $plan->begin != $futureConfig)
  49. {
  50. $childPlans = $this->productplan->getChildren($plan->id);
  51. $minBegin = $plan->begin;
  52. $maxEnd = $plan->end;
  53. foreach($childPlans as $childID => $childPlan)
  54. {
  55. $childPlan = isset($plans[$childID]) ? $plans[$childID] : $childPlan;
  56. if($childPlan->begin < $minBegin && $minBegin != $this->config->productplan->future) $minBegin = $childPlan->begin;
  57. if($childPlan->end > $maxEnd && $maxEnd != $this->config->productplan->future) $maxEnd = $childPlan->end;
  58. }
  59. if($minBegin < $plan->begin && $minBegin != $futureConfig) return dao::$errors["begin[{$planID}]"] = sprintf($this->lang->productplan->beginGreaterChildTip, $planID, $plan->begin, $minBegin);
  60. if($maxEnd > $plan->end && $maxEnd != $futureConfig) return dao::$errors["end[{$planID}]"] = sprintf($this->lang->productplan->endLessThanChildTip, $planID, $plan->end, $maxEnd);
  61. }
  62. if($plan->branch == '') $plan->branch = 0;
  63. }
  64. return $plans;
  65. }
  66. /**
  67. * 设置计划看板页面数据。
  68. * Set kanban page data.
  69. *
  70. * @param object $product
  71. * @param string $branchID
  72. * @param string $orderBy
  73. * @access protected
  74. * @return void
  75. */
  76. protected function assignKanbanData($product, $branchID, $orderBy)
  77. {
  78. $branches = array();
  79. $branchPairs = array();
  80. $planCount = 0;
  81. if(!in_array($orderBy, array_keys($this->lang->productplan->orderList))) $orderBy = key($this->lang->productplan->orderList);
  82. if($product->type == 'normal')
  83. {
  84. $planGroup = $this->productplan->getList($product->id, '0', 'all', null, $orderBy, 'skipparent');
  85. $this->view->planCount = count(array_filter($planGroup));
  86. }
  87. else
  88. {
  89. $planGroup = $this->productplan->getGroupByProduct(array($product->id), 'skipparent', $orderBy);
  90. $branches = $this->loadModel('branch')->getPairs($product->id, 'active');
  91. foreach($branches as $id => $name)
  92. {
  93. $plans = isset($planGroup[$product->id][$id]) ? array_filter($planGroup[$product->id][$id]) : array();
  94. $branchPairs[$id] = $name . ' ' . count($plans);
  95. $planCount += count($plans);
  96. }
  97. $this->view->branches = array('all' => $this->lang->productplan->allAB . ' ' . $planCount) + $branchPairs;
  98. }
  99. $this->view->kanbanList = $this->loadModel('kanban')->getPlanKanban($product, $branchID, $planGroup);
  100. }
  101. /**
  102. * 构造计划列表页面数据。
  103. * Build data for browse page.
  104. *
  105. * @param array $plans
  106. * @param array $branchOption
  107. * @access protected
  108. * @return array
  109. */
  110. protected function buildDataForBrowse($plans, $branchOption)
  111. {
  112. if(empty($plans)) return $plans;
  113. foreach($plans as $plan)
  114. {
  115. $plan->branchName = '';
  116. if($this->session->currentProductType != 'normal')
  117. {
  118. foreach(explode(',', $plan->branch) as $branchID) $plan->branchName .= $branchOption[$branchID] . ',';
  119. $plan->branchName = trim($plan->branchName, ',');
  120. }
  121. if($plan->begin == $this->config->productplan->future) $plan->begin = $this->lang->productplan->future;
  122. if($plan->end == $this->config->productplan->future) $plan->end = $this->lang->productplan->future;
  123. $plan->actions = $this->buildActionsList($plan);
  124. $plan->projects = array_values($plan->projects);
  125. $plan->desc = strip_tags($plan->desc);
  126. }
  127. return $plans;
  128. }
  129. /**
  130. * 构造计划列表页面操作。
  131. * Build actions for browse page.
  132. *
  133. * @param object $plan
  134. * @access protected
  135. * @return array
  136. */
  137. protected function buildActionsList($plan)
  138. {
  139. $actions = array();
  140. if(common::hasPriv('productplan', 'start')) $actions[] = 'start';
  141. if(common::hasPriv('productplan', 'finish')) $actions[] = 'finish';
  142. if(common::hasPriv('productplan', 'close')) $actions[] = 'close';
  143. if(common::hasPriv('productplan', 'activate')) $actions[] = 'activate';
  144. if(common::hasPriv('execution', 'create')) $actions[] = 'createExecution';
  145. if(count($actions) > 0) $actions[] = 'divider';
  146. if(common::hasPriv('productplan', 'linkStory')) $actions[] = 'linkStory';
  147. if(common::hasPriv('productplan', 'linkBug')) $actions[] = 'linkBug';
  148. if(common::hasPriv('productplan', 'edit')) $actions[] = 'edit';
  149. if(common::hasPriv('productplan', 'create')) $actions[] = 'create';
  150. if(common::hasPriv('productplan', 'delete')) $actions[] = 'delete';
  151. return $actions;
  152. }
  153. /**
  154. * 统计父计划、子计划和独立计划的总数。
  155. * Get the total count of parent plan, child plan and indepentdent plan.
  156. *
  157. * @param array $planList
  158. * @access protected
  159. * @return string
  160. */
  161. protected function getSummary($planList)
  162. {
  163. $totalParent = $totalChild = $totalIndependent = 0;
  164. foreach($planList as $plan)
  165. {
  166. if($plan->parent == -1) $totalParent ++;
  167. if($plan->parent > 0) $totalChild ++;
  168. if($plan->parent == 0) $totalIndependent ++;
  169. }
  170. return sprintf($this->lang->productplan->summary, count($planList), $totalParent, $totalChild, $totalIndependent);
  171. }
  172. /**
  173. * 设置计划详情页面session信息。
  174. * Set session for view page.
  175. *
  176. * @param int $planID
  177. * @param string $type
  178. * @param string $orderBy
  179. * @param int $pageID
  180. * @param int $recPerPage
  181. * @access protected
  182. * @return void
  183. */
  184. protected function setSessionForViewPage($planID, $type, $orderBy, $pageID, $recPerPage)
  185. {
  186. if(in_array($type, array('story', 'bug')) && ($orderBy != 'order_desc' || $pageID != 1 || $recPerPage != 100))
  187. {
  188. if($type == 'story')
  189. {
  190. $this->session->set('storyList', $this->app->getURI(true), 'product');
  191. }
  192. elseif($type == 'bug')
  193. {
  194. $this->session->set('bugList', $this->app->getURI(true), 'qa');
  195. }
  196. else
  197. {
  198. $this->session->set('storyList', $this->createLink('productplan', 'view', "planID={$planID}&type={$type}"), $type == 'story' ? 'product' : 'qa');
  199. }
  200. }
  201. }
  202. /**
  203. * 设置详情页面的属性。
  204. * Set attributes for view page.
  205. *
  206. * @param object $plan
  207. * @access protected
  208. * @return void
  209. */
  210. protected function assignViewData($plan)
  211. {
  212. if($plan->parent > 0) $this->view->parentPlan = $this->productplan->getById($plan->parent);
  213. if($plan->parent == '-1') $this->view->childrenPlans = $this->productplan->getChildren($plan->id);
  214. $gradeList = $this->loadModel('story')->getGradeList('');
  215. $gradeGroup = array();
  216. foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
  217. $this->view->plan = $plan;
  218. $this->view->gradeGroup = $gradeGroup;
  219. $this->view->actions = $this->loadModel('action')->getList('productplan', $plan->id);
  220. $this->view->users = $this->loadModel('user')->getPairs('noletter');
  221. $this->view->plans = $this->productplan->getPairs($plan->product, $plan->branch, '', true);
  222. $this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product);
  223. if($this->app->getViewType() == 'json')
  224. {
  225. unset($this->view->storyPager);
  226. unset($this->view->bugPager);
  227. }
  228. }
  229. /**
  230. * 构建关联需求页面的搜索表单。
  231. * Build search form for link story page.
  232. *
  233. * @param object $plan
  234. * @param int $queryID
  235. * @param string $orderBy
  236. * @access protected
  237. * @return void
  238. */
  239. protected function buildLinkStorySearchForm($plan, $queryID, $orderBy)
  240. {
  241. $this->app->loadLang('story');
  242. $products = $this->loadModel('product')->getProductPairsByProject((int)$this->session->project);
  243. /* Build search form. */
  244. $this->config->product->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$plan->id&type=story&orderBy=$orderBy&link=true&param=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
  245. $this->config->product->search['queryID'] = $queryID;
  246. $this->config->product->search['style'] = 'simple';
  247. $this->config->product->search['fields']['title'] = $this->lang->productplan->storyTitle;
  248. $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject);
  249. $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true);
  250. $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all');
  251. $storyStatusList = $this->lang->story->statusList;
  252. unset($storyStatusList['closed']);
  253. $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList);
  254. $product = $this->loadModel('product')->getByID($plan->product);
  255. if($product->type == 'normal')
  256. {
  257. unset($this->config->product->search['fields']['branch']);
  258. unset($this->config->product->search['params']['branch']);
  259. }
  260. else
  261. {
  262. $branches = $this->loadModel('branch')->getPairsByIdList(explode(',', trim($plan->branch, ',')));
  263. $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  264. $this->config->product->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches;
  265. }
  266. $gradeList = $this->loadModel('story')->getGradeList('');
  267. foreach($gradeList as $grade)
  268. {
  269. if(!$this->config->URAndSR && $grade->type == 'requirement') continue;
  270. if(!$this->config->enableER && $grade->type == 'epic') continue;
  271. $key = (string)$grade->type . (string)$grade->grade;
  272. $gradePairs[$key] = $grade->name;
  273. }
  274. asort($gradePairs);
  275. $this->config->product->search['params']['grade']['values'] = $gradePairs;
  276. unset($this->config->product->search['fields']['product']);
  277. $this->loadModel('search')->setSearchParams($this->config->product->search);
  278. }
  279. /**
  280. * 构造关联bug页面的搜索表单。
  281. * Build search form for link bug page.
  282. *
  283. * @param object $plan
  284. * @param int $queryID
  285. * @param string $orderBy
  286. * @access protected
  287. * @return void
  288. */
  289. protected function buildBugSearchForm($plan, $queryID, $orderBy)
  290. {
  291. $this->config->bug->search['actionURL'] = $this->createLink('productplan', 'view', "planID={$plan->id}&type=bug&orderBy={$orderBy}&link=true&param=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
  292. $this->config->bug->search['queryID'] = $queryID;
  293. $this->config->bug->search['style'] = 'simple';
  294. $modulePairs = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, 'all');
  295. $this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true);
  296. $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($plan->product, $plan->branch);
  297. $this->config->bug->search['params']['module']['values'] = $modulePairs;
  298. $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs(array($plan->product), 'all', 'releasetag');
  299. $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values'];
  300. $this->config->bug->search['params']['module']['values'] = $modulePairs;
  301. $this->config->bug->search['params']['project']['values'] = $this->product->getProjectPairsByProduct($plan->product, $plan->branch);
  302. unset($this->config->bug->search['fields']['product']);
  303. $product = $this->loadModel('product')->getByID($plan->product);
  304. if($product->type == 'normal')
  305. {
  306. unset($this->config->bug->search['fields']['branch']);
  307. unset($this->config->bug->search['params']['branch']);
  308. }
  309. else
  310. {
  311. $branches = $this->loadModel('branch')->getPairsByIdList(explode(',', trim($plan->branch, ',')));
  312. $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  313. $this->config->bug->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches;
  314. }
  315. $this->loadModel('search')->setSearchParams($this->config->bug->search);
  316. }
  317. /**
  318. * 构造需求列表的摘要信息。
  319. * Get the summary of product's stories.
  320. *
  321. * @param array $stories
  322. * @access public
  323. * @return string
  324. */
  325. public function buildViewSummary($stories)
  326. {
  327. $totalEstimate = 0.0;
  328. $storyIdList = array();
  329. $rateCount = 0;
  330. $SRTotal = 0;
  331. $URTotal = 0;
  332. $ERTotal = 0;
  333. foreach($stories as $story)
  334. {
  335. if($story->type == 'story') $SRTotal += 1;
  336. if($story->type == 'requirement') $URTotal += 1;
  337. if($story->type == 'epic') $ERTotal += 1;
  338. if($story->isParent == '0') $totalEstimate += $story->estimate;
  339. if($story->type != 'story') continue;
  340. if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed'))))
  341. {
  342. $storyIdList[] = $story->id;
  343. $rateCount ++;
  344. }
  345. }
  346. $casesCount = count($this->loadModel('product')->filterNoCasesStory($storyIdList));
  347. $rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 2);
  348. return sprintf($this->lang->productplan->storySummary, $ERTotal, $URTotal, $SRTotal, $totalEstimate, $rate * 100 . "%");
  349. }
  350. /**
  351. * 构造计划详情页面的操作菜单。
  352. * Build operate menu for plan detail page.
  353. *
  354. * @param object $plan
  355. * @access public
  356. * @return array
  357. */
  358. public function buildViewActions($plan)
  359. {
  360. $params = "planID=$plan->id";
  361. $canEdit = common::hasPriv('productplan', 'edit');
  362. $canStart = common::hasPriv('productplan', 'start') && $this->productplan->isClickable($plan, 'start');
  363. $canFinish = common::hasPriv('productplan', 'finish') && $this->productplan->isClickable($plan, 'finish');
  364. $canClose = common::hasPriv('productplan', 'close') && $this->productplan->isClickable($plan, 'close');
  365. $canActivate = common::hasPriv('productplan', 'activate') && $this->productplan->isClickable($plan, 'activate');
  366. $canCreateChild = common::hasPriv('productplan', 'create') && $this->productplan->isClickable($plan, 'create');
  367. $canDelete = common::hasPriv('productplan', 'delete') && $this->productplan->isClickable($plan, 'delete');
  368. $menu = array();
  369. 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)');
  370. 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)');
  371. 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');
  372. 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)');
  373. 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}"));
  374. if($canEdit) $menu[] = array('icon' => 'edit text-primary', 'class' => 'ghost', 'text' => $this->lang->edit, 'url' => helper::createLink('productplan', 'edit', $params));
  375. 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)');
  376. return $menu;
  377. }
  378. /**
  379. * 对计划需求按照父子关系重新排序。
  380. * Reorder stories by parent-child relationship.
  381. *
  382. * @access public
  383. * @return void
  384. */
  385. public function reorderStories()
  386. {
  387. /* 获取不分页的 SQL 语句,为重新按照父子关系排序做准备。*/
  388. $sql = $this->dao->get();
  389. if(strpos($sql, 'LIMIT')) $sql = substr($sql, 0, strpos($sql, 'LIMIT'));
  390. /* 取出用户重新排序的关键字段。*/
  391. $stories = array();
  392. $query = $this->dao->query($sql);
  393. while($story = $query->fetch()) $stories[$story->id] = $story->parent;
  394. /* 对需求重新按照父子关系排序,保证进入需求详情后上一页下一页的URL符合预期。 */
  395. $objectList = $this->loadModel('story')->reorderStories($stories);
  396. if($objectList)
  397. {
  398. $this->session->set('storyBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab);
  399. $this->session->set('epicBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab);
  400. $this->session->set('requirementBrowseList', array('sql' => $sql, 'idkey' => 'id', 'objectList' => $objectList), $this->app->tab);
  401. }
  402. }
  403. }