tao.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. <?php
  2. /**
  3. * The tao file of execution module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Shujie Tian <tianshujie@easysoft.ltd>
  8. * @package execution
  9. * @link https://www.zentao.net
  10. */
  11. class executionTao extends executionModel
  12. {
  13. /**
  14. * 根据给定条件构建执行键值对。
  15. * Build execution id:name pairs through the conditions.
  16. *
  17. * @param string $mode all|noclosed|stagefilter|withdelete|multiple|leaf|order_asc|noprefix|withobject|hideMultiple
  18. * @param array $allExecutions
  19. * @param array $executions
  20. * @param array $parents
  21. * @param array $projectPairs
  22. * @param string $projectModel
  23. * @access protected
  24. * @return array
  25. */
  26. protected function buildExecutionPairs($mode = '', $allExecutions = array(), $executions = array(), $parents = array(), $projectPairs = array(), $projectModel = '')
  27. {
  28. $executionPairs = array();
  29. $noMultiples = array();
  30. foreach($executions as $execution)
  31. {
  32. if(strpos($mode, 'leaf') !== false && isset($parents[$execution->id])) continue; // Only show leaf.
  33. if(strpos($mode, 'noclosed') !== false && ($execution->status == 'done' or $execution->status == 'closed')) continue;
  34. if(strpos($mode, 'stagefilter') !== false && isset($projectModel) && in_array($projectModel, array('waterfall', 'waterfallplus')) && in_array($execution->attribute, array('request', 'design', 'review'))) continue; // Some stages of waterfall && waterfallplus not need.
  35. if(empty($execution->multiple)) $noMultiples[$execution->id] = $execution->project;
  36. /* Set execution name. */
  37. $paths = array_slice(explode(',', trim($execution->path, ',')), 1);
  38. $executionName = '';
  39. foreach($paths as $path)
  40. {
  41. if(isset($allExecutions[$path])) $executionName .= '/' . $allExecutions[$path]->name;
  42. }
  43. if(strpos($mode, 'withobject') !== false) $executionName = zget($projectPairs, $execution->project, '') . $executionName;
  44. if(strpos($mode, 'noprefix') !== false) $executionName = ltrim($executionName, '/');
  45. $executionPairs[$execution->id] = $executionName;
  46. }
  47. if($noMultiples)
  48. {
  49. if(strpos($mode, 'hideMultiple') !== false)
  50. {
  51. foreach($noMultiples as $executionID => $projectID) $executionPairs[$executionID] = '';
  52. }
  53. else
  54. {
  55. $this->app->loadLang('project');
  56. $noMultipleProjects = $this->dao->select('id, name')->from(TABLE_PROJECT)->where('id')->in($noMultiples)->fetchPairs('id', 'name');
  57. foreach($noMultiples as $executionID => $projectID)
  58. {
  59. if(isset($noMultipleProjects[$projectID])) $executionPairs[$executionID] = $noMultipleProjects[$projectID] . "({$this->lang->project->disableExecution})";
  60. }
  61. }
  62. }
  63. /* If the executionPairs is empty, to make sure there's an execution in the executionPairs. */
  64. if(empty($executionPairs) and isset($executions[0]))
  65. {
  66. $firstExecution = $executions[0];
  67. $executionPairs[$firstExecution->id] = $firstExecution->name;
  68. }
  69. return $executionPairs;
  70. }
  71. /**
  72. * 构造批量更新执行的数据。
  73. * Build bathc update execution data.
  74. *
  75. * @param object $postData
  76. * @param array $oldExecutions
  77. * @access public
  78. * @return array
  79. */
  80. public function buildBatchUpdateExecutions($postData, $oldExecutions)
  81. {
  82. $this->loadModel('user');
  83. $this->loadModel('project');
  84. $this->app->loadLang('programplan');
  85. $executions = array();
  86. $nameList = array();
  87. $codeList = array();
  88. $parents = array();
  89. foreach($oldExecutions as $oldExecution) $parents[$oldExecution->id] = $oldExecution->parent;
  90. /* Replace required language. */
  91. if($this->app->tab == 'project')
  92. {
  93. $projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($this->session->project)->fetch('model');
  94. if(empty($this->session->project) || $projectModel == 'scrum')
  95. {
  96. $this->lang->project->name = $this->lang->execution->name;
  97. $this->lang->project->code = $this->lang->execution->code;
  98. }
  99. else
  100. {
  101. $this->lang->project->name = str_replace($this->lang->project->common, $this->lang->project->stage, $this->lang->project->name);
  102. $this->lang->project->code = str_replace($this->lang->project->common, $this->lang->project->stage, $this->lang->project->code);
  103. }
  104. }
  105. else
  106. {
  107. $this->lang->project->name = $this->lang->execution->name;
  108. $this->lang->project->code = $this->lang->execution->code;
  109. }
  110. $this->lang->error->unique = $this->lang->error->repeat;
  111. $extendFields = $this->getFlowExtendFields();
  112. foreach($postData->id as $executionID)
  113. {
  114. $executionName = $postData->name[$executionID];
  115. if(isset($postData->code)) $executionCode = $postData->code[$executionID];
  116. $executionID = (int)$executionID;
  117. $executions[$executionID] = new stdClass();
  118. $executions[$executionID]->id = $executionID;
  119. $executions[$executionID]->name = $executionName;
  120. $executions[$executionID]->PM = $postData->PM[$executionID];
  121. $executions[$executionID]->PO = $postData->PO[$executionID];
  122. $executions[$executionID]->QD = $postData->QD[$executionID];
  123. $executions[$executionID]->RD = $postData->RD[$executionID];
  124. $executions[$executionID]->begin = $postData->begin[$executionID];
  125. $executions[$executionID]->end = $postData->end[$executionID];
  126. $executions[$executionID]->team = $postData->team[$executionID];
  127. $executions[$executionID]->desc = htmlspecialchars_decode($postData->desc[$executionID]);
  128. $executions[$executionID]->days = $postData->days[$executionID];
  129. $executions[$executionID]->lastEditedBy = $this->app->user->account;
  130. $executions[$executionID]->lastEditedDate = helper::now();
  131. if(isset($postData->code)) $executions[$executionID]->code = $executionCode;
  132. if(isset($postData->project)) $executions[$executionID]->project = zget($postData->project, $executionID, 0);
  133. if(isset($postData->attribute[$executionID])) $executions[$executionID]->attribute = zget($postData->attribute, $executionID, '');
  134. if(isset($postData->lifetime[$executionID])) $executions[$executionID]->lifetime = $postData->lifetime[$executionID];
  135. $oldExecution = $oldExecutions[$executionID];
  136. $projectID = isset($executions[$executionID]->project) ? (int)$executions[$executionID]->project : (int)$oldExecution->project;
  137. $project = dao::isError() ? '' : $this->project->getByID($projectID);
  138. /* Check unique code for edited executions. */
  139. if(isset($postData->code) && empty($executionCode) && strpos(",{$this->config->execution->edit->requiredFields},", ',code,') !== false)
  140. {
  141. dao::$errors["code[$executionID]"] = sprintf($this->lang->error->notempty, $this->lang->execution->execCode);
  142. }
  143. elseif(isset($postData->code) and $executionCode)
  144. {
  145. if(isset($codeList[$executionCode]))
  146. {
  147. dao::$errors["code[$executionID]"] = sprintf($this->lang->error->unique, $this->lang->execution->execCode, $executionCode);
  148. }
  149. $codeList[$executionCode] = $executionCode;
  150. }
  151. /* Name check. */
  152. $parentID = $parents[$executionID];
  153. if(isset($nameList[$executionName]) && !empty($executionName))
  154. {
  155. foreach($nameList[$executionName] as $repeatID)
  156. {
  157. if($parentID == $parents[$repeatID])
  158. {
  159. $type = $oldExecution->type == 'stage' ? 'stage' : 'agileplus';
  160. $repeatTip = $parentID == $projectID ? $this->lang->programplan->error->sameName : sprintf($this->lang->execution->errorNameRepeat, strtolower(zget($this->lang->programplan->typeList, $type)));
  161. dao::$errors["name[$executionID]"] = $repeatTip;
  162. }
  163. }
  164. }
  165. $nameList[$executionName][] = $executionID;
  166. /* Attribute check. */
  167. if(isset($postData->attribute) && isset($project->model) && in_array($project->model, array('waterfall', 'waterfallplus')))
  168. {
  169. $this->app->loadLang('stage');
  170. $attribute = isset($executions[$executionID]->attribute) ? $executions[$executionID]->attribute : $oldExecution->attribute;
  171. if(isset($attributeList[$parentID]))
  172. {
  173. $parentAttr = $attributeList[$parentID];
  174. }
  175. else
  176. {
  177. $parentAttr = dao::isError() ? $attribute : $this->dao->select('attribute')->from(TABLE_PROJECT)->where('id')->eq($parentID)->fetch('attribute');
  178. }
  179. if($parentAttr && $parentAttr != $attribute && $parentAttr != 'mix')
  180. {
  181. $parentAttr = zget($this->lang->stage->typeList, $parentAttr);
  182. dao::$errors["attribute[$executionID]"] = sprintf($this->lang->execution->errorAttrMatch, $parentAttr);
  183. }
  184. $attributeList[$executionID] = $attribute;
  185. }
  186. /* Judge workdays is legitimate. */
  187. $workdays = helper::diffDate($postData->end[$executionID], $postData->begin[$executionID]) + 1;
  188. if(isset($postData->days[$executionID]) and $postData->days[$executionID] > $workdays)
  189. {
  190. $this->app->loadLang('project');
  191. dao::$errors["days[{$executionID}]"] = sprintf($this->lang->project->workdaysExceed, $workdays);
  192. }
  193. /* Parent stage begin and end check. */
  194. if(isset($executions[$parentID]))
  195. {
  196. $begin = $executions[$executionID]->begin;
  197. $end = $executions[$executionID]->end;
  198. $parentBegin = $executions[$parentID]->begin;
  199. $parentEnd = $executions[$parentID]->end;
  200. if($begin < $parentBegin)
  201. {
  202. dao::$errors["begin[$executionID]"] = sprintf($this->lang->execution->errorLesserParent, $parentBegin);
  203. }
  204. if($end > $parentEnd)
  205. {
  206. dao::$errors["end[$executionID]"] = sprintf($this->lang->execution->errorGreaterParent, $parentEnd);
  207. }
  208. }
  209. foreach($extendFields as $extendField)
  210. {
  211. $executions[$executionID]->{$extendField->field} = $postData->{$extendField->field}[$executionID];
  212. if(in_array($extendField->type, array('date', 'datetime')) && empty($postData->{$extendField->field}[$executionID]))
  213. {
  214. $executions[$executionID]->{$extendField->field} = null;
  215. continue;
  216. }
  217. if(is_array($executions[$executionID]->{$extendField->field})) $executions[$executionID]->{$extendField->field} = implode(',', $executions[$executionID]->{$extendField->field});
  218. $executions[$executionID]->{$extendField->field} = htmlSpecialString($executions[$executionID]->{$extendField->field});
  219. }
  220. if(empty($executions[$executionID]->begin)) dao::$errors["begin[{$executionID}]"] = sprintf($this->lang->error->notempty, $this->lang->execution->begin);
  221. if(empty($executions[$executionID]->end)) dao::$errors["end[{$executionID}]"] = sprintf($this->lang->error->notempty, $this->lang->execution->end);
  222. /* Project begin and end check. */
  223. if(!empty($executions[$executionID]->begin) and !empty($executions[$executionID]->end))
  224. {
  225. if($executions[$executionID]->begin > $executions[$executionID]->end)
  226. {
  227. dao::$errors["end[{$executionID}]"] = sprintf($this->lang->execution->errorLesserPlan, $executions[$executionID]->end, $executions[$executionID]->begin);
  228. }
  229. if($project and $executions[$executionID]->begin < $project->begin)
  230. {
  231. dao::$errors["begin[{$executionID}]"] = sprintf($this->lang->execution->errorLesserProject, $project->begin);
  232. }
  233. if($project and $executions[$executionID]->end > $project->end)
  234. {
  235. dao::$errors["end[{$executionID}]"] = sprintf($this->lang->execution->errorGreaterProject, $project->end);
  236. }
  237. }
  238. }
  239. return $executions;
  240. }
  241. /**
  242. * 获取燃尽图相关数据。
  243. * Get burn related data.
  244. *
  245. * @param array $executionIdList
  246. * @access protected
  247. * @return array
  248. */
  249. protected function fetchBurnData($executionIdList)
  250. {
  251. $today = helper::today();
  252. $burns = $this->dao->select("execution, '$today' AS date, sum(estimate) AS `estimate`, sum(`left`) AS `left`, SUM(consumed) AS `consumed`")->from(TABLE_TASK)
  253. ->where('execution')->in($executionIdList)
  254. ->andWhere('deleted')->eq('0')
  255. ->andWhere('isParent')->eq('0')
  256. ->andWhere('status')->ne('cancel')
  257. ->groupBy('execution')
  258. ->fetchAll('execution');
  259. $closedLefts = $this->dao->select('execution, sum(`left`) AS `left`')->from(TABLE_TASK)
  260. ->where('execution')->in($executionIdList)
  261. ->andWhere('deleted')->eq('0')
  262. ->andWhere('isParent')->eq('0')
  263. ->andWhere('status')->eq('closed')
  264. ->groupBy('execution')
  265. ->fetchAll('execution');
  266. $finishedEstimates = $this->dao->select("execution, sum(`estimate`) AS `estimate`")->from(TABLE_TASK)
  267. ->where('execution')->in($executionIdList)
  268. ->andWhere('deleted')->eq('0')
  269. ->andWhere('isParent')->eq('0')
  270. ->andWhere('status', true)->eq('done')
  271. ->orWhere('status')->eq('closed')
  272. ->markRight(1)
  273. ->groupBy('execution')
  274. ->fetchAll('execution');
  275. $storyPoints = $this->dao->select('t1.project, sum(t2.estimate) AS `storyPoint`')->from(TABLE_PROJECTSTORY)->alias('t1')
  276. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  277. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t2.product = t3.id')
  278. ->where('t1.project')->in($executionIdList)
  279. ->andWhere('t2.deleted')->eq(0)
  280. ->andWhere('t2.status')->ne('closed')
  281. ->andWhere('t2.stage')->in('wait,planned,projected,developing')
  282. ->andWhere('t2.isParent')->eq('0')
  283. ->groupBy('project')
  284. ->fetchAll('project');
  285. return array($burns, $closedLefts, $finishedEstimates, $storyPoints);
  286. }
  287. /**
  288. * 获取燃尽图数据。
  289. * Get burn data.
  290. *
  291. * @param int $executionID
  292. * @param string $date
  293. * @param int $taskCount
  294. * @access protected
  295. * @return object|bool
  296. */
  297. protected function getBurnByExecution($executionID, $date = '', $taskCount = 0)
  298. {
  299. return $this->dao->select('*')->from(TABLE_BURN)
  300. ->where('execution')->eq($executionID)
  301. ->beginIF($date)->andWhere('date')->eq($date)->fi()
  302. ->beginIF($taskCount)->andWhere('task')->eq($taskCount)->fi()
  303. ->fetch();
  304. }
  305. /**
  306. * 获取执行团队成员数量。
  307. * Get execution team member count.
  308. *
  309. * @param array $executionIdList
  310. * @access protected
  311. * @return void
  312. */
  313. protected function getMemberCountGroup($executionIdList)
  314. {
  315. return $this->dao->select('t1.root,count(t1.id) as teams')->from(TABLE_TEAM)->alias('t1')
  316. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
  317. ->where('t1.root')->in($executionIdList)
  318. ->andWhere('t1.type')->ne('project')
  319. ->andWhere('t2.deleted')->eq(0)
  320. ->groupBy('t1.root')
  321. ->fetchAll('root');
  322. }
  323. /**
  324. * 通过产品ID列表获取执行id:name的键值对。
  325. * Get the pair for execution id:name from the product id list.
  326. *
  327. * @param array $productIdList
  328. * @access protected
  329. * @return array
  330. */
  331. protected function getPairsByProduct($productIdList)
  332. {
  333. return $this->dao->select('t1.project, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  334. ->leftJoin(TABLE_EXECUTION)->alias('t2')
  335. ->on('t1.project = t2.id')
  336. ->where('t1.product')->in($productIdList)
  337. ->andWhere('t2.type')->in('sprint,stage,kanban')
  338. ->fetchPairs('project');
  339. }
  340. /**
  341. * 获取执行关联的产品信息。
  342. * Get product information of the linked execution.
  343. *
  344. * @param int $executionID
  345. * @access protected
  346. * @return array
  347. */
  348. protected function getProductList($executionID)
  349. {
  350. $executions = $this->dao->select('t1.id,t2.product,t3.name')->from(TABLE_EXECUTION)->alias('t1')
  351. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id=t2.project')
  352. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t2.product=t3.id')
  353. ->where('t1.project')->eq($executionID)
  354. ->andWhere('t1.type')->in('kanban,sprint,stage')
  355. ->fetchAll();
  356. $productList = array();
  357. $linkedProducts = array();
  358. foreach($executions as $execution)
  359. {
  360. if(!isset($productList[$execution->id]))
  361. {
  362. $productList[$execution->id] = new stdclass();
  363. $productList[$execution->id]->product = '';
  364. $productList[$execution->id]->productName = '';
  365. }
  366. if(isset($linkedProducts[$execution->id][$execution->product])) continue;
  367. $productList[$execution->id]->product .= $execution->product . ',';
  368. $productList[$execution->id]->productName .= $execution->name . ',';
  369. $linkedProducts[$execution->id][$execution->product] = $execution->product;
  370. }
  371. return $productList;
  372. }
  373. /**
  374. * 通过搜索获取Bug列表数据。
  375. * Get bugs by search in execution.
  376. *
  377. * @param array $productIdList
  378. * @param int $executionID
  379. * @param string $sql
  380. * @param string $orderBy
  381. * @param object|null $pager
  382. * @access public
  383. * @return object[]
  384. */
  385. public function getSearchBugs($productIdList, $executionID, $sql = '1=1', $orderBy = 'id_desc', $pager = null)
  386. {
  387. return $this->dao->select('*')->from(TABLE_BUG)
  388. ->where($sql)
  389. ->andWhere('status')->eq('active')
  390. ->andWhere('toTask')->eq(0)
  391. ->andWhere('tostory')->eq(0)
  392. ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi()
  393. ->beginIF(empty($productIdList))->andWhere('execution')->eq($executionID)->fi()
  394. ->andWhere('deleted')->eq(0)
  395. ->orderBy($orderBy)
  396. ->page($pager)
  397. ->fetchAll('id');
  398. }
  399. /**
  400. * 将执行ID保存到session中。
  401. * Save the execution ID to the session.
  402. *
  403. * @param int $executionID
  404. * @access protected
  405. * @return void
  406. */
  407. protected function saveSession($executionID)
  408. {
  409. $this->session->set('execution', $executionID, $this->app->tab);
  410. $this->setProjectSession($executionID);
  411. }
  412. /**
  413. * 设置看板执行的菜单。
  414. * Set kanban menu.
  415. *
  416. * @access public
  417. * @return void
  418. */
  419. public function setKanbanMenu()
  420. {
  421. global $lang;
  422. $lang->executionCommon = $lang->execution->kanban;
  423. include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
  424. $this->lang->execution->menu = new stdclass();
  425. $this->lang->execution->menu->kanban = array('link' => "{$this->lang->kanban->common}|execution|kanban|executionID=%s", 'subModule' => 'task', 'alias' => 'importtask');
  426. $this->lang->execution->menu->CFD = array('link' => "{$this->lang->execution->CFD}|execution|cfd|executionID=%s");
  427. $this->lang->execution->menu->build = array('link' => "{$this->lang->build->common}|execution|build|executionID=%s", 'alias' => 'bug', 'subModule' => 'projectbuild,build,bug');
  428. $this->lang->execution->menu->settings = array('link' => "{$this->lang->settings}|execution|view|executionID=%s", 'subModule' => 'personnel', 'alias' => 'edit,manageproducts,team,whitelist,addwhitelist,managemembers', 'class' => 'dropdown dropdown-hover');
  429. $this->lang->execution->dividerMenu = '';
  430. $this->lang->execution->menu->settings['subMenu'] = new stdclass();
  431. $this->lang->execution->menu->settings['subMenu']->view = array('link' => "{$this->lang->overview}|execution|view|executionID=%s", 'subModule' => 'view', 'alias' => 'edit,start,suspend,putoff,close');
  432. $this->lang->execution->menu->settings['subMenu']->products = array('link' => "{$this->lang->productCommon}|execution|manageproducts|executionID=%s");
  433. $this->lang->execution->menu->settings['subMenu']->team = array('link' => "{$this->lang->team->common}|execution|team|executionID=%s", 'alias' => 'managemembers');
  434. $this->lang->execution->menu->settings['subMenu']->whitelist = array('link' => "{$this->lang->whitelist}|execution|whitelist|executionID=%s", 'subModule' => 'personnel', 'alias' => 'addwhitelist');
  435. }
  436. /**
  437. * 更新今日的累计流图数据。
  438. * Update today's cumulative flow graph data.
  439. *
  440. * @param int $executionID
  441. * @param string $type
  442. * @param string $colName
  443. * @param array $laneGroup
  444. * @access protected
  445. * @return void
  446. */
  447. protected function updateTodayCFDData($executionID, $type, $colName, $laneGroup)
  448. {
  449. $cfd = new stdclass();
  450. $cfd->count = 0;
  451. $cfd->date = helper::today();
  452. $cfd->type = $type;
  453. foreach($laneGroup as $columnGroup)
  454. {
  455. foreach($columnGroup as $columnCard)
  456. {
  457. $cards = trim($columnCard->cards, ',');
  458. $cfd->count += $cards ? count(explode(',', $cards)) : 0;
  459. }
  460. }
  461. $cfd->name = $colName;
  462. $cfd->execution = $executionID;
  463. $this->dao->replace(TABLE_CFD)->data($cfd)->exec();
  464. }
  465. /**
  466. * 获取任务分组数据。
  467. * Get task group data.
  468. *
  469. * @param int $executionID
  470. * @access protected
  471. * @return array
  472. */
  473. protected function getTaskGroups($executionID)
  474. {
  475. $executionID = (int)$executionID;
  476. $tasks = $this->dao->select('*')->from(TABLE_TASK)
  477. ->where('execution')->eq($executionID)
  478. ->andWhere('deleted')->eq(0)
  479. ->andWhere('parent')->eq(0)
  480. ->orderBy('id_desc')
  481. ->fetchAll();
  482. $childTasks = $this->dao->select('*')->from(TABLE_TASK)
  483. ->where('execution')->eq($executionID)
  484. ->andWhere('deleted')->eq(0)
  485. ->andWhere('parent')->ne(0)
  486. ->orderBy('id_desc')
  487. ->fetchGroup('parent');
  488. $taskGroups = array();
  489. foreach($tasks as $task) $taskGroups[$task->module][$task->story][$task->id] = $this->appendTaskChildren($task, $childTasks);
  490. return $taskGroups;
  491. }
  492. /**
  493. * 递归获取任务数量。
  494. * Get task count.
  495. *
  496. * @param array $tasks
  497. * @access protected
  498. * @return int
  499. *
  500. */
  501. protected function getTaskCount($tasks)
  502. {
  503. $taskCount = count($tasks);
  504. foreach($tasks as $task)
  505. {
  506. if(!empty($task->children)) $taskCount += $this->getTaskCount($task->children);
  507. }
  508. return $taskCount;
  509. }
  510. /**
  511. * 追加子任务到任务
  512. * Append children to task
  513. *
  514. * @param object $task
  515. * @param array $childTasks
  516. * @return object
  517. */
  518. protected function appendTaskChildren($task, $childTasks)
  519. {
  520. if(empty($childTasks[$task->id])) return $task;
  521. $task->children = array();
  522. foreach($childTasks[$task->id] as $child)
  523. {
  524. $child = $this->appendTaskChildren($child, $childTasks);
  525. $task->children[] = $child;
  526. }
  527. return $task;
  528. }
  529. /**
  530. * 处理树状图需求类型数据。
  531. * Process tree chart demand type data.
  532. *
  533. * @param object $node
  534. * @param array $storyGroups
  535. * @param array $taskGroups
  536. * @param int $executionID
  537. * @access protected
  538. * @return object
  539. */
  540. protected function processStoryNode($node, $storyGroups, $taskGroups, $executionID)
  541. {
  542. $node->type = 'module';
  543. $stories = isset($storyGroups[$node->root][$node->id]) ? $storyGroups[$node->root][$node->id] : array();
  544. $node->children = $this->buildStoryTree($stories, $taskGroups, $executionID, $node);
  545. /* Append for task of no story && node is not root. */
  546. if($node->id && isset($taskGroups[$node->id][0]))
  547. {
  548. $taskItems = $this->formatTasksForTree($taskGroups[$node->id][0]);
  549. $node->tasksCount = $this->getTaskCount($taskItems);
  550. foreach($taskItems as $taskItem) $node->children[] = $taskItem;
  551. }
  552. return $node;
  553. }
  554. /**
  555. * 构造需求树。
  556. * Build story tree.
  557. *
  558. * @param array $stories
  559. * @param array $taskGroups
  560. * @param int $executionID
  561. * @param object $node
  562. * @param int $parentID
  563. * @access protected
  564. * @return array
  565. */
  566. protected function buildStoryTree($stories, $taskGroups, $executionID, $node, $parentID = 0)
  567. {
  568. static $users, $avatarPairs;
  569. if(empty($users)) $users = $this->loadModel('user')->getPairs('noletter');
  570. if(empty($avatarPairs)) $avatarPairs = $this->loadModel('user')->getAvatarPairs();
  571. $children = array();
  572. foreach($stories as $story)
  573. {
  574. if($story->parent != $parentID && ($parentID || (!$parentID && isset($stories[$story->parent])))) continue;
  575. $avatarAccount = empty($story->assignedTo) ? zget($story, 'openedBy', '') : $story->assignedTo;
  576. $userAvatar = zget($avatarPairs, $avatarAccount);
  577. $userAvatar = $userAvatar && $avatarAccount != 'closed' ? "<img src='{$userAvatar}'/>" : strtoupper(mb_substr($avatarAccount, 0, 1, 'utf-8'));
  578. $storyItem = new stdclass();
  579. $storyItem->type = $story->type;
  580. $storyItem->id = 'story' . $story->id;
  581. $storyItem->title = $story->title;
  582. $storyItem->color = $story->color;
  583. $storyItem->pri = $story->pri;
  584. $storyItem->storyId = $story->id;
  585. $storyItem->grade = $story->grade;
  586. $storyItem->openedBy = zget($users, $story->openedBy);
  587. $storyItem->assignedTo = zget($users, $story->assignedTo);
  588. $storyItem->url = helper::createLink('execution', 'storyView', "storyID=$story->id&execution=$executionID");
  589. $storyItem->taskCreateUrl = helper::createLink('task', 'batchCreate', "executionID={$executionID}&story={$story->id}");
  590. $storyItem->avatarAccount = zget($users, $avatarAccount);
  591. $storyItem->avatar = $userAvatar;
  592. $storyTasks = isset($taskGroups[$node->id][$story->id]) ? $taskGroups[$node->id][$story->id] : array();
  593. if(!empty($storyTasks))
  594. {
  595. $taskItems = $this->formatTasksForTree($storyTasks, $stories);
  596. $storyItem->tasksCount = $this->getTaskCount($taskItems);
  597. $storyItem->children = $taskItems;
  598. }
  599. else
  600. {
  601. $storyItemChildren = $this->buildStoryTree($stories, $taskGroups, $executionID, $node, $story->id);
  602. if($storyItemChildren) $storyItem->children = $storyItemChildren;
  603. }
  604. $children[] = $storyItem;
  605. }
  606. return $children;
  607. }
  608. /**
  609. * 处理树状图任务类型数据。
  610. * Process tree chart task type data.
  611. *
  612. * @param object $node
  613. * @param array $taskGroups
  614. * @access protected
  615. * @return object
  616. */
  617. protected function processTaskNode($node, $taskGroups)
  618. {
  619. $node->type = 'module';
  620. $node->tasksCount = 0;
  621. if(!isset($taskGroups[$node->id])) return $node;
  622. foreach($taskGroups[$node->id] as $tasks)
  623. {
  624. $taskItems = $this->formatTasksForTree($tasks);
  625. $node->tasksCount += $this->getTaskCount($taskItems);
  626. foreach($taskItems as $taskItem) $node->children[$taskItem->id] = $taskItem;
  627. }
  628. $node->children = array_values($node->children);
  629. return $node;
  630. }
  631. /**
  632. * Update team of execution when it edited.
  633. *
  634. * @param int $executionID
  635. * @param object $oldExecution
  636. * @param object $execution
  637. * @return void
  638. */
  639. protected function updateTeam($executionID, $oldExecution, $execution)
  640. {
  641. if(defined('RUN_MODE') && RUN_MODE == 'api') return;
  642. /* Get team and language item. */
  643. $this->loadModel('user');
  644. $team = $this->user->getTeamMemberPairs($executionID, 'execution');
  645. $members = isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array();
  646. array_push($members, $execution->PO, $execution->QD, $execution->PM, $execution->RD);
  647. $members = array_unique($members);
  648. $roles = $this->user->getUserRoles(array_values($members));
  649. /* Add the member who joined the team. */
  650. $changedAccounts = array();
  651. $teamMembers = array();
  652. foreach($members as $account)
  653. {
  654. if(empty($account) or isset($team[$account])) continue;
  655. $member = new stdclass();
  656. $member->root = $executionID;
  657. $member->account = $account;
  658. $member->join = helper::today();
  659. $member->role = zget($roles, $account, '');
  660. $member->days = zget($execution, 'days', 0);
  661. $member->type = 'execution';
  662. $member->hours = $this->config->execution->defaultWorkhours;
  663. $this->dao->replace(TABLE_TEAM)->data($member)->exec();
  664. $changedAccounts[$account] = $account;
  665. $teamMembers[$account] = $member;
  666. }
  667. /* Remove the member who left the team. */
  668. $this->dao->delete()->from(TABLE_TEAM)
  669. ->where('root')->eq($executionID)
  670. ->andWhere('type')->eq('execution')
  671. ->andWhere('account')->in(array_keys($team))
  672. ->andWhere('account')->notin(array_values($members))
  673. ->andWhere('account')->ne($oldExecution->openedBy)
  674. ->exec();
  675. if(isset($execution->project) and $execution->project) $this->addProjectMembers((int)$execution->project, $teamMembers);
  676. /* Fix bug#3074, Update views for team members. */
  677. if($execution->acl != 'open') $this->updateUserView($executionID, 'sprint', $changedAccounts);
  678. }
  679. /**
  680. * 格式化任务树的数据。
  681. * Format tasks for tree.
  682. *
  683. * @param array $tasks
  684. * @param array $stories
  685. * @access protected
  686. * @return array
  687. */
  688. protected function formatTasksForTree($tasks, $stories = array())
  689. {
  690. static $users, $avatarPairs;
  691. if(empty($users)) $users = $this->loadModel('user')->getPairs('noletter');
  692. if(empty($avatarPairs)) $avatarPairs = $this->loadModel('user')->getAvatarPairs();
  693. $taskItems = array();
  694. foreach($tasks as $task)
  695. {
  696. $avatarAccount = empty($task->assignedTo) ? zget($task, 'openedBy', '') : $task->assignedTo;
  697. $userAvatar = zget($avatarPairs, $avatarAccount);
  698. $userAvatar = $userAvatar && $avatarAccount != 'closed' ? "<img src='{$userAvatar}' />" : strtoupper(mb_substr($avatarAccount, 0, 1, 'utf-8'));
  699. $story = zget($stories, $task->story, '');
  700. $taskItem = new stdclass();
  701. $taskItem->type = 'task';
  702. $taskItem->id = $task->id;
  703. $taskItem->title = $task->name;
  704. $taskItem->color = $task->color;
  705. $taskItem->pri = (int)$task->pri;
  706. $taskItem->status = $task->status;
  707. $taskItem->parent = $task->parent;
  708. $taskItem->isParent = $task->isParent;
  709. $taskItem->estStarted = $task->estStarted;
  710. $taskItem->realStarted = $task->realStarted;
  711. $taskItem->estimate = $task->estimate;
  712. $taskItem->consumed = $task->consumed;
  713. $taskItem->left = $task->left;
  714. $taskItem->openedBy = zget($users, $task->openedBy);
  715. $taskItem->assignedTo = zget($users, $task->assignedTo);
  716. $taskItem->url = helper::createLink('task', 'view', "task=$task->id");
  717. $taskItem->storyChanged = $story && $story->status == 'active' && $story->version > $story->taskVersion;
  718. $taskItem->avatarAccount = zget($users, $avatarAccount);
  719. $taskItem->avatar = $userAvatar;
  720. if(!empty($task->children)) $taskItem->children = $this->formatTasksForTree($task->children, $stories);
  721. $taskItems[] = $taskItem;
  722. }
  723. return $taskItems;
  724. }
  725. /**
  726. * 添加执行的团队。
  727. * Add execution members.
  728. *
  729. * @param int $executionID
  730. * @param array $postMembers e.g. array('admin', 'dev1')
  731. * @access protected
  732. * @return void
  733. */
  734. protected function addExecutionMembers($executionID, $postMembers)
  735. {
  736. $execution = $this->fetchByID($executionID);
  737. if(empty($execution)) return;
  738. array_push($postMembers, $execution->PO, $execution->QD, $execution->PM, $execution->RD, $execution->openedBy);
  739. $members = array_filter(array_unique($postMembers));
  740. $roles = $this->loadModel('user')->getUserRoles(array_values($members));
  741. $today = helper::today();
  742. $teamMembers = array();
  743. $oldTeams = $this->dao->select('account')->from(TABLE_TEAM)->where('root')->eq($executionID)->andWhere('type')->eq('execution')->fetchPairs();
  744. foreach($members as $account)
  745. {
  746. if(isset($oldTeams[$account])) continue;
  747. $member = new stdClass();
  748. $member->root = $executionID;
  749. $member->type = 'execution';
  750. $member->account = $account;
  751. $member->role = zget($roles, $account, '');
  752. $member->join = $today;
  753. $member->days = $execution->days;
  754. $member->hours = $this->config->execution->defaultWorkhours;
  755. $this->dao->insert(TABLE_TEAM)->data($member)->exec();
  756. $teamMembers[$account] = $member;
  757. }
  758. if($execution->acl != 'open') $this->updateUserView($executionID, 'sprint');
  759. $this->addProjectMembers($execution->project, $teamMembers);
  760. }
  761. /**
  762. * 关联创建执行主库
  763. * Create main lib for product
  764. *
  765. * @param int productID
  766. * @access protected
  767. * @return int|false
  768. */
  769. protected function createMainLib($projectID, $executionID, $type = 'sprint')
  770. {
  771. if($projectID < 0 || $executionID <= 0) return false;
  772. $existedLibID = (int)$this->dao->select('id')->from(TABLE_DOCLIB)->where('execution')->eq($executionID)
  773. ->andWhere('type')->eq('execution')
  774. ->andWhere('main')->eq('1')
  775. ->fetch('id');
  776. if($existedLibID) return $existedLibID;
  777. $this->app->loadLang('doc');
  778. if($type == 'sprint') $this->app->loadLang('project');
  779. $lib = new stdclass();
  780. $lib->project = $projectID;
  781. $lib->execution = $executionID;
  782. $lib->name = $type == 'stage' ? str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->doclib->main['execution']) : $this->lang->doclib->main['execution'];
  783. $lib->type = 'execution';
  784. $lib->main = '1';
  785. $lib->acl = 'default';
  786. $lib->addedBy = $this->app->user->account;
  787. $lib->addedDate = helper::now();
  788. $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
  789. if(dao::isError()) return false;
  790. return $this->dao->lastInsertID();
  791. }
  792. }