model.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. <?php
  2. /**
  3. * The model file of build module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package build
  9. * @version $Id: model.php 4970 2013-07-02 05:58:11Z wyd621@gmail.com $
  10. * @link https://www.zentao.net
  11. */
  12. ?>
  13. <?php
  14. class buildModel extends model
  15. {
  16. /**
  17. * 通过版本ID获取版本信息。
  18. * Get build info.
  19. *
  20. * @param int $buildID
  21. * @param bool $setImgSize
  22. * @access public
  23. * @return object|bool
  24. */
  25. public function getByID($buildID, $setImgSize = false)
  26. {
  27. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getBuild();
  28. $build = $this->dao->select('t1.*, t2.name as executionName, t3.name as productName, t3.type as productType')
  29. ->from(TABLE_BUILD)->alias('t1')
  30. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id')
  31. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id')
  32. ->where('t1.id')->eq((int)$buildID)
  33. ->fetch();
  34. if(!$build) return false;
  35. $build = $this->joinChildBuilds($build);
  36. $build = $this->loadModel('file')->replaceImgURL($build, 'desc');
  37. $build->files = $this->file->getByObject('build', $buildID);
  38. if($setImgSize) $build->desc = $this->file->setImgSize($build->desc);
  39. $build->isChild = false;
  40. if($this->app->rawMethod == 'edit')
  41. {
  42. if($this->dao->select('id')->from(TABLE_BUILD)->where("FIND_IN_SET($buildID, `builds`)")->fetch()) $build->isChild = true;
  43. if(!$build->isChild && $this->dao->select('id')->from(TABLE_RELEASE)->where("FIND_IN_SET($buildID, `build`)")->fetch()) $build->isChild = true;
  44. }
  45. return $build;
  46. }
  47. /**
  48. * 通过版本ID列表获取版本信息。
  49. * Get builds by id list.
  50. *
  51. * @param array $idList
  52. * @access public
  53. * @return array
  54. */
  55. public function getByList($idList)
  56. {
  57. return $this->dao->select('*')->from(TABLE_BUILD)->where('id')->in($idList)->fetchAll('id', false);
  58. }
  59. /**
  60. * 通过项目ID获取版本信息。
  61. * Get builds of a project.
  62. *
  63. * @param int $projectID
  64. * @param string $type
  65. * @param string $param
  66. * @param string $orderBy
  67. * @param object $pager
  68. * @access public
  69. * @return array
  70. */
  71. public function getProjectBuilds($projectID = 0, $type = 'all', $param = '', $orderBy = 't1.date_desc,t1.id_desc', $pager = null)
  72. {
  73. $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where("FIND_IN_SET({$projectID}, project)")->fetchPairs('shadow', 'shadow');
  74. $builds = $this->dao->select('t1.*, t2.name as productName')
  75. ->from(TABLE_BUILD)->alias('t1')
  76. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
  77. ->where('t1.deleted')->eq(0)
  78. ->andWhere('t1.project')->ne(0)
  79. ->beginIF(!empty($shadows))->andWhere('t1.id')->notIN($shadows)->fi()
  80. ->beginIF($projectID)->andWhere('t1.project')->eq((int)$projectID)->fi()
  81. ->beginIF($type == 'product' && $param)->andWhere('t1.product')->eq((int)$param)->fi()
  82. ->beginIF($type == 'bysearch')->andWhere($param)->fi()
  83. ->beginIF($type == 'review')->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")->fi()
  84. ->orderBy($orderBy)
  85. ->page($pager)
  86. ->fetchAll('id');
  87. $executionIdList = array();
  88. foreach($builds as $build)
  89. {
  90. $build->builds = $this->getByList(explode(',', $build->builds));
  91. if(!empty($build->builds))
  92. {
  93. foreach($build->builds as $child)
  94. {
  95. if(!isset($executionIdList[$child->execution])) $executionIdList[$child->execution] = $child->execution;
  96. }
  97. }
  98. if(!isset($executionIdList[$build->execution])) $executionIdList[$build->execution] = $build->execution;
  99. }
  100. $executions = $this->loadModel('execution')->getByIdList($executionIdList, 'all');
  101. foreach($builds as $build)
  102. {
  103. $build->executionDeleted = $build->execution ? $executions[$build->execution]->deleted : 0;
  104. $build->executionName = $build->execution ? $executions[$build->execution]->name : '';
  105. if(!empty($build->builds))
  106. {
  107. foreach($build->builds as $child) $child->executionName = $child->execution ? $executions[$child->execution]->name : '';
  108. }
  109. }
  110. return $builds;
  111. }
  112. /**
  113. * 根据搜索条件获取项目版本。
  114. * Get builds of a project by search.
  115. *
  116. * @param int $projectID
  117. * @param int $queryID
  118. * @param string $orderBy
  119. * @param object $pager
  120. * @access public
  121. * @return array
  122. */
  123. public function getProjectBuildsBySearch($projectID, $queryID, $orderBy = 't1.date_desc,t1.id_desc', $pager = null)
  124. {
  125. /* If there are saved query conditions, reset the session. */
  126. if((int)$queryID)
  127. {
  128. $query = $this->loadModel('search')->getQuery($queryID);
  129. if($query)
  130. {
  131. $this->session->set('projectBuildQuery', $query->sql);
  132. $this->session->set('projectBuildForm', $query->form);
  133. }
  134. }
  135. if($this->session->projectBuildQuery == false) $this->session->set('projectBuildQuery', ' 1 = 1');
  136. $buildQuery = $this->session->projectBuildQuery;
  137. /* Distinguish between repeated fields. */
  138. $fields = array('id' => '`id`', 'name' => '`name`', 'product' => '`product`', 'desc' => '`desc`', 'project' => '`project`');
  139. foreach($fields as $field)
  140. {
  141. if(strpos($buildQuery, $field) !== false)
  142. {
  143. $buildQuery = str_replace($field, "t1." . $field, $buildQuery);
  144. }
  145. }
  146. return $this->getProjectBuilds($projectID, 'bysearch', (string)$buildQuery, $orderBy, $pager);
  147. }
  148. /**
  149. * 根据执行ID获取版本信息。
  150. * Get builds of a execution.
  151. *
  152. * @param int $executionID
  153. * @param string $type all|product|bysearch
  154. * @param string $param productID|buildQuery
  155. * @param string $orderBy
  156. * @param object $pager
  157. * @access public
  158. * @return array
  159. */
  160. public function getExecutionBuilds($executionID, $type = '', $param = '', $orderBy = 't1.date_desc,t1.id_desc', $pager = null)
  161. {
  162. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getBuilds();
  163. return $this->dao->select('t1.*, t2.name as executionName, t3.name as productName')
  164. ->from(TABLE_BUILD)->alias('t1')
  165. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id')
  166. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id')
  167. ->where('t1.deleted')->eq(0)
  168. ->beginIF($executionID)->andWhere('t1.execution')->eq((int)$executionID)->fi()
  169. ->beginIF($type == 'product' && $param)->andWhere('t1.product')->eq((int)$param)->fi()
  170. ->beginIF($type == 'bysearch')->andWhere($param)->fi()
  171. ->beginIF($type == 'review')->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")->fi()
  172. ->orderBy($orderBy)
  173. ->page($pager)
  174. ->fetchAll('id');
  175. }
  176. /**
  177. * 通过条件获取执行下版本列表。
  178. * Get builds of a execution by search.
  179. *
  180. * @param int $executionID
  181. * @param int $queryID
  182. * @param object $pager
  183. * @access public
  184. * @return object[]
  185. */
  186. public function getExecutionBuildsBySearch($executionID, $queryID, $pager = null)
  187. {
  188. /* If there are saved query conditions, reset the session. */
  189. if($queryID)
  190. {
  191. $query = $this->loadModel('search')->getQuery($queryID);
  192. if($query)
  193. {
  194. $this->session->set('executionBuildQuery', $query->sql);
  195. $this->session->set('executionBuildForm', $query->form);
  196. }
  197. }
  198. if($this->session->executionBuildQuery === false) $this->session->set('executionBuildQuery', ' 1 = 1');
  199. $buildQuery = $this->session->executionBuildQuery;
  200. /* Distinguish between repeated fields. */
  201. $fields = array('id' => '`id`', 'name' => '`name`', 'product' => '`product`', 'desc' => '`desc`');
  202. foreach($fields as $field)
  203. {
  204. if(strpos($this->session->executionBuildQuery, $field) !== false)
  205. {
  206. $buildQuery = str_replace($field, "t1." . $field, $buildQuery);
  207. }
  208. }
  209. return $this->getExecutionBuilds($executionID, 'bysearch', (string)$buildQuery, 't1.date_desc,t1.id_desc', $pager);
  210. }
  211. /**
  212. * 根据需求ID获取版本列表。
  213. * Get builds by story ID.
  214. *
  215. * @param int $storyID
  216. * @access public
  217. * @return array
  218. */
  219. public function getStoryBuilds($storyID)
  220. {
  221. if(empty($storyID)) return array();
  222. return $this->dao->select('*')->from(TABLE_BUILD)
  223. ->where('deleted')->eq(0)
  224. ->andWhere("CONCAT(',', stories, ',')")->like("%,$storyID,%")
  225. ->orderBy('id_desc')
  226. ->fetchAll('id');
  227. }
  228. /**
  229. * 通过条件获取版本id:name的键值对。
  230. * Get build pairs by condition.
  231. *
  232. * @param array|int $productIdList
  233. * @param string|int $branch
  234. * @param string $params noempty|notrunk|noterminate|withbranch|hasproject|noDeleted|singled|noreleased|releasedtag, can be a set of them
  235. * @param int $objectID
  236. * @param string $objectType
  237. * @param string $buildIdList
  238. * @param bool $replace
  239. * @access public
  240. * @return array
  241. * @param int $system
  242. */
  243. public function getBuildPairs($productIdList, $branch = 'all', $params = 'noterminate, nodone', $objectID = 0, $objectType = 'execution', $buildIdList = '', $replace = true, $system = 0)
  244. {
  245. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getBuildPairs();
  246. $sysBuilds = array();
  247. if(strpos($params, 'notrunk') === false) $sysBuilds = array('trunk' => $this->lang->trunk);
  248. if(is_string($buildIdList)) $buildIdList = str_replace('trunk', '0', $buildIdList);
  249. $shadows = array();
  250. if(!empty($productIdList)) $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where('product')->in($productIdList)->fetchPairs('shadow', 'shadow'); // Get the buildID under the shadow product.
  251. if($objectType == 'project') $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where("FIND_IN_SET({$objectID}, project)")->fetchPairs('shadow', 'shadow');
  252. $selectedBuilds = $this->buildTao->selectedBuildPairs($buildIdList, $productIdList, $params, $objectID, $objectType);
  253. $allBuilds = $this->buildTao->fetchBuilds($productIdList, $params, $objectID, $objectType, $shadows, $system);
  254. /* Set builds and filter done executions and terminate releases. */
  255. list($builds, $excludedReleaseIdList) = $this->setBuildDateGroup($allBuilds, $branch, $params);
  256. if(empty($builds) && empty($shadows)) return $sysBuilds + $selectedBuilds;
  257. /* if the build has been released and replace is true, replace build name with release name. */
  258. if($replace)
  259. {
  260. $releases = $this->getRelatedReleases($productIdList, $buildIdList, $shadows, $objectType, $objectID, $params);
  261. $builds = $this->replaceNameWithRelease($allBuilds, $builds, $releases, $branch, $params, $excludedReleaseIdList);
  262. }
  263. krsort($builds);
  264. $buildPairs = array();
  265. foreach($builds as $childBuilds) $buildPairs += $childBuilds;
  266. return $sysBuilds + $buildPairs + $selectedBuilds;
  267. }
  268. /**
  269. * 根据版本日期分组设置版本信息。
  270. * Set build date group.
  271. *
  272. * @param array $allBuilds
  273. * @param string|int $branch
  274. * @param string $params nodone|noterminate|withexecution|withbranch
  275. * @access public
  276. * @return array
  277. */
  278. public function setBuildDateGroup($allBuilds, $branch, $params)
  279. {
  280. $this->app->loadLang('branch');
  281. $deletedProjects = $this->dao->select('id, deleted')->from(TABLE_PROJECT)->where('type')->eq('project')->andWhere('deleted')->eq('1')->fetchPairs();
  282. $deletedExecutions = $this->dao->select('id, deleted')->from(TABLE_EXECUTION)->where('type')->in('sprint,stage,kanban')->andWhere('deleted')->eq('1')->fetchPairs();
  283. $branchPairs = $this->dao->select('id,name')->from(TABLE_BRANCH)->fetchPairs();
  284. $builds = array();
  285. $excludedReleaseIdList = array();
  286. foreach($allBuilds as $id => $build)
  287. {
  288. if($build->branch === '') $build->branch = 0;
  289. $isDeleted = false;
  290. if(strpos($params, 'withexecution') !== false && $build->execution && isset($deletedExecutions[$build->execution])) $isDeleted = true;
  291. if($build->project && !$build->execution && isset($deletedProjects[$build->project])) $isDeleted = true;
  292. $isDone = empty($build->releaseID) && strpos($params, 'nodone') !== false && !empty($build->objectStatus) && $build->objectStatus === 'done';
  293. $isTerminate = strpos($params, 'noterminate') !== false && $build->releaseStatus === 'terminate';
  294. $isNotInBranch = $branch !== '' && $branch !== 'all' && strpos(",{$build->branch},", ",{$branch},") === false;
  295. if(in_array(true, array($isDone, $isTerminate, $isDeleted, $isNotInBranch)))
  296. {
  297. $excludedReleaseIdList[] = $build->releaseID;
  298. continue;
  299. }
  300. if($build->deleted == 1) $build->name .= ' (' . $this->lang->build->deleted . ')';
  301. if(!empty($build->branch))
  302. {
  303. $branchName = '';
  304. foreach(explode(',', $build->branch) as $buildBranch)
  305. {
  306. if(empty($buildBranch))
  307. {
  308. $branchName .= $this->lang->branch->main;
  309. }
  310. else
  311. {
  312. $branchName .= isset($branchPairs[$buildBranch]) ? $branchPairs[$buildBranch] : '';
  313. }
  314. $branchName .= ',';
  315. }
  316. $branchName = trim($branchName, ',');
  317. }
  318. else
  319. {
  320. $branchName = $this->lang->branch->main;
  321. }
  322. $buildName = $build->name;
  323. if(strpos($params, 'withbranch') !== false && $build->productType != 'normal') $buildName = $branchName . '/' . $buildName;
  324. $builds[$build->date][$id] = $buildName;
  325. }
  326. return array($builds, $excludedReleaseIdList);
  327. }
  328. /**
  329. * 将版本名称替换为发布名称。
  330. * Replace the build name with release name.
  331. *
  332. * @param array $allBuilds
  333. * @param array $builds
  334. * @param array $releases
  335. * @param string|int $branch
  336. * @param string $params separate|noterminate|withbranch|releasetag|noreleased
  337. * @param array $excludedReleaseIdList
  338. * @access public
  339. * @return array
  340. */
  341. public function replaceNameWithRelease($allBuilds, $builds, $releases, $branch, $params, $excludedReleaseIdList)
  342. {
  343. $this->app->loadLang('branch');
  344. $branches = strpos($params, 'separate') === false ? "0,$branch" : $branch;
  345. foreach($releases as $release)
  346. {
  347. if(strpos($params, 'noterminate') !== false && in_array($release->id, $excludedReleaseIdList)) continue;
  348. if($branch !== 'all')
  349. {
  350. $inBranch = false;
  351. foreach(explode(',', trim($release->branch, ',')) as $branchID)
  352. {
  353. if($branchID === '') continue;
  354. if(strpos(",{$branches},", ",{$branchID},") !== false) $inBranch = true;
  355. }
  356. if(!$inBranch) continue;
  357. }
  358. /* Set release name based on the condition. */
  359. $releaseName = $release->name;
  360. $branchName = $release->branchName ? $release->branchName : $this->lang->branch->main;
  361. if($release->productType != 'normal') $releaseName = (strpos($params, 'withbranch') !== false ? $branchName . '/' : '') . $releaseName;
  362. if(strpos($params, 'releasetag') !== false) $releaseName = $releaseName . " [{$this->lang->build->released}]";
  363. $builds[$release->date][$release->shadow] = $releaseName;
  364. if(strpos($params, 'noreplace') === false) // 勾选所有版本时,展示所有发布,不替换版本名称。
  365. {
  366. foreach(explode(',', trim($release->build, ',')) as $buildID)
  367. {
  368. if(!isset($allBuilds[$buildID])) continue;
  369. $build = $allBuilds[$buildID];
  370. if(strpos($params, 'noreleased') !== false) unset($builds[$build->date][$buildID]);
  371. }
  372. }
  373. }
  374. return $builds;
  375. }
  376. /**
  377. * 获取关联的发布。
  378. * Get releated release.
  379. *
  380. * @param array|int $productIdList
  381. * @param string|int $buildIdList
  382. * @param array|bool $shadows
  383. * @param string $objectType
  384. * @param int $objectID
  385. * @param string $params
  386. * @access public
  387. * @return array
  388. */
  389. public function getRelatedReleases($productIdList, $buildIdList = '', $shadows = false, $objectType = '', $objectID = 0, $params = '')
  390. {
  391. $releases = $this->dao->select('DISTINCT t1.id,t1.shadow,t1.product,t1.branch,t1.build,t1.name,t1.date,t1.status,t3.name as branchName,t4.type as productType')->from(TABLE_RELEASE)->alias('t1')
  392. ->leftJoin(TABLE_RELEASERELATED)->alias('t5')->on("t1.id=t5.release AND t5.objectType='build'")
  393. ->leftJoin(TABLE_BUILD)->alias('t2')->on('t5.objectID=t2.id')
  394. ->leftJoin(TABLE_RELEASERELATED)->alias('t6')->on("t1.id=t6.release AND t6.objectType='branch'")
  395. ->leftJoin(TABLE_BRANCH)->alias('t3')->on('t6.objectID=t3.id')
  396. ->leftJoin(TABLE_PRODUCT)->alias('t4')->on('t1.product=t4.id')
  397. ->where('t1.product')->in($productIdList)
  398. ->beginIF($objectType === 'project' && $objectID)->andWhere("(FIND_IN_SET('$objectID', t1.project)")->orWhere('t1.project')->eq('0')->markRight(1)->fi()
  399. ->beginIF($objectType === 'execution' && $objectID)->andWhere('t2.execution')->eq($objectID)->fi()
  400. ->beginIF(strpos($params, 'nowaitrelease') !== false)->andWhere('t1.status')->ne('wait')->fi()
  401. ->beginIF(strpos($params, 'nofail') !== false)->andWhere('t1.status')->ne('fail')->fi()
  402. ->andWhere('((t1.deleted')->eq(0)
  403. ->andWhere('t1.shadow')->ne(0)
  404. ->markRight(true)
  405. ->beginIF(!empty($buildIdList))->orWhere('t2.id')->in($buildIdList)->fi()
  406. ->markRight(true)
  407. ->fetchAll('id');
  408. if($shadows === false) $shadows = $this->dao->select('shadow')->from(TABLE_RELEASE)->where('product')->in($productIdList)->fetchPairs('shadow', 'shadow'); // Get the buildID under the shadow product.
  409. if($shadows)
  410. {
  411. /* Append releases of only shadow and not link build. */
  412. $releases += $this->dao->select('DISTINCT t1.id,t1.shadow,t1.product,t1.branch,t1.build,t1.name,t1.date,t2.name as branchName,t3.type as productType')->from(TABLE_RELEASE)->alias('t1')
  413. ->leftJoin(TABLE_RELEASERELATED)->alias('t4')->on("t1.id=t4.release AND t4.objectType='branch'")
  414. ->leftJoin(TABLE_BRANCH)->alias('t2')->on('t4.objectID=t2.id')
  415. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id')
  416. ->where('t1.shadow')->in($shadows)
  417. ->beginIF($objectType === 'project' && $objectID)->andWhere("(FIND_IN_SET('$objectID', t1.project)")->orWhere('t1.project')->eq('0')->markRight(1)->fi()
  418. ->andWhere('t1.build')->eq(0)
  419. ->andWhere('t1.deleted')->eq(0)
  420. ->fetchAll('id');
  421. }
  422. return $releases;
  423. }
  424. /**
  425. * 获取最后一次创建的版本信息。
  426. * Get the last build.
  427. *
  428. * @param int $executionID
  429. * @param int $projectID
  430. * @access public
  431. * @return object|false
  432. */
  433. public function getLast($executionID = 0, $projectID = 0)
  434. {
  435. return $this->dao->select('id, name')->from(TABLE_BUILD)
  436. ->where('deleted')->eq(0)
  437. ->andWhere('execution')->eq($executionID)
  438. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  439. ->orderBy('date DESC,id DESC')
  440. ->fetch();
  441. }
  442. /**
  443. * 创建一个版本。
  444. * Create a build.
  445. *
  446. * @param object $build
  447. * @access public
  448. * @return int|false
  449. */
  450. public function create($build)
  451. {
  452. /* Integrated version merging branch. */
  453. if($this->post->isIntegrated == 'yes')
  454. {
  455. $build->execution = 0;
  456. $branchPairs = $this->dao->select('branch')->from(TABLE_BUILD)->where('id')->in($build->builds)->fetchPairs();
  457. $relationBranch = array();
  458. foreach($branchPairs as $branches)
  459. {
  460. foreach(explode(',', $branches) as $branch)
  461. {
  462. if(!isset($relationBranch[$branch])) $relationBranch[$branch] = $branch;
  463. }
  464. }
  465. if($relationBranch) $build->branch = implode(',', $relationBranch);
  466. $this->config->build->create->requiredFields = str_replace('execution,', '', $this->config->build->create->requiredFields);
  467. }
  468. else
  469. {
  470. $product = $this->loadModel('product')->getByID((int)$build->product);
  471. if(!empty($product) && $product->type != 'normal'&& $this->post->branch === false)
  472. {
  473. $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  474. dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch);
  475. }
  476. }
  477. if(dao::isError()) return false;
  478. /* Process and insert build data. */
  479. $requiredFields = $this->config->build->create->requiredFields;
  480. $project = $this->loadModel('project')->getByID((int)$build->project);
  481. if($project && !$project->hasProduct) $requiredFields = str_replace('product,', '', $requiredFields);
  482. $build = $this->loadModel('file')->processImgURL($build, $this->config->build->editor->create['id'], (string)$this->post->uid);
  483. $this->dao->insert(TABLE_BUILD)->data($build)
  484. ->autoCheck()
  485. ->batchCheck($requiredFields, 'notempty')
  486. ->check('name', 'unique', "product = {$build->product} AND branch = '{$build->branch}' AND deleted = '0'")
  487. ->checkFlow()
  488. ->exec();
  489. if(dao::isError()) return false;
  490. /* Set file linkage and score info. */
  491. $buildID = $this->dao->lastInsertID();
  492. $this->file->updateObjectID($this->post->uid, $buildID, 'build');
  493. $this->file->saveUpload('build', $buildID);
  494. $this->loadModel('score')->create('build', 'create', $buildID);
  495. $this->loadModel('action')->create('build', $buildID, 'opened');
  496. return $buildID;
  497. }
  498. /**
  499. * 更新一个版本。
  500. * Update a build.
  501. *
  502. * @param int $buildID
  503. * @param object $build
  504. * @access public
  505. * @return array|false
  506. */
  507. public function update($buildID, $build)
  508. {
  509. $oldBuild = $this->fetchByID($buildID);
  510. $product = $this->loadModel('product')->getByID((int) $build->product);
  511. $branch = $this->post->branch === false || ($product && $product->type) == 'normal' ? 0 : $oldBuild->branch;
  512. if(empty($oldBuild->execution)) $build = $this->processBuildForUpdate($build, $oldBuild);
  513. $product = $this->loadModel('product')->getByID($build->product);
  514. if(!empty($product) && $product->type != 'normal' && !isset($_POST['branch']) && isset($_POST['product']))
  515. {
  516. $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  517. dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch);
  518. }
  519. if(dao::isError()) return false;
  520. $project = $this->loadModel('project')->getByID((int)$oldBuild->project);
  521. $requiredFields = $this->config->build->edit->requiredFields;
  522. if(!$project->hasProduct) $requiredFields = str_replace('product,', '', $requiredFields);
  523. $build = $this->loadModel('file')->processImgURL($build, $this->config->build->editor->edit['id'], (string)$this->post->uid);
  524. $this->dao->update(TABLE_BUILD)->data($build, 'deleteFiles,renameFiles,files')
  525. ->autoCheck()
  526. ->batchCheck($requiredFields, 'notempty')
  527. ->where('id')->eq($buildID)
  528. ->check('name', 'unique', "id != $buildID AND product = {$build->product} AND branch = '{$build->branch}' AND deleted = '0'")
  529. ->checkFlow()
  530. ->exec();
  531. if(dao::isError()) return false;
  532. if(isset($build->branch) && $oldBuild->branch != $build->branch) $this->dao->update(TABLE_RELEASE)->set('branch')->eq($build->branch)->where('build')->eq($buildID)->exec();
  533. if(dao::isError()) return false;
  534. $oldBuild->files = $this->file->getByObject('build', $buildID);
  535. $this->file->processFileDiffsForObject('build', $oldBuild, $build);
  536. return common::createChanges($oldBuild, $build);
  537. }
  538. /**
  539. * 更新关联Bug的解决原因。
  540. * Update linked bug to resolved.
  541. *
  542. * @param object $build
  543. * @param array $bugIdList
  544. * @param array $resolvedByList
  545. * @access public
  546. * @return bool
  547. */
  548. public function updateLinkedBug($build, $bugIdList = array(), $resolvedByList = array())
  549. {
  550. $bugs = empty($bugIdList) ? array() : $this->dao->select('*')->from(TABLE_BUG)->where('id')->in($bugIdList)->fetchAll();
  551. if(!$bugs) return false;
  552. $this->loadModel('action');
  553. $now = helper::now();
  554. foreach($bugs as $bug)
  555. {
  556. if($bug->status == 'resolved' || $bug->status == 'closed') continue;
  557. if(helper::isZeroDate($bug->activatedDate)) unset($bug->activatedDate);
  558. if(helper::isZeroDate($bug->closedDate)) unset($bug->closedDate);
  559. $bug->resolvedBy = zget($resolvedByList, $bug->id, '');
  560. $bug->resolvedDate = $now;
  561. $bug->status = 'resolved';
  562. $bug->confirmed = 1;
  563. $bug->assignedDate = $now;
  564. $bug->assignedTo = $bug->openedBy;
  565. $bug->lastEditedBy = $this->app->user->account;
  566. $bug->lastEditedDate = $now;
  567. $bug->resolution = 'fixed';
  568. $bug->resolvedBuild = $build->id;
  569. $bug->deadline = !empty($bug->deadline) ? $bug->deadline : null;
  570. $this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bug->id)->exec();
  571. $this->action->create('bug', $bug->id, 'Resolved', '', 'fixed', $bug->resolvedBy);
  572. }
  573. return !dao::isError();
  574. }
  575. /**
  576. * 版本关联需求。
  577. * Link stories to a build.
  578. *
  579. * @param int $buildID
  580. * @param array $storyIdList
  581. * @access public
  582. * @return void
  583. */
  584. public function linkStory($buildID, $storyIdList)
  585. {
  586. if(empty($storyIdList)) return false;
  587. $build = $this->getByID($buildID);
  588. foreach($storyIdList as $i => $storyID)
  589. {
  590. if(strpos(",{$build->stories},", ",{$storyID},") !== false) unset($storyIdList[$i]);
  591. }
  592. $build->stories .= ',' . implode(',', $storyIdList);
  593. $this->dao->update(TABLE_BUILD)->set('stories')->eq($build->stories)->where('id')->eq((int)$buildID)->exec();
  594. $this->loadModel('action');
  595. foreach($storyIdList as $storyID) $this->action->create('story', (int)$storyID, 'linked2build', '', $buildID);
  596. $this->action->create('build', $buildID, 'linkstory', '', implode(',', $storyIdList));
  597. return !dao::isError();
  598. }
  599. /**
  600. * 解除需求关联。
  601. * Unlink story.
  602. *
  603. * @param int $buildID
  604. * @param int $storyID
  605. * @access public
  606. * @return bool
  607. */
  608. public function unlinkStory($buildID, $storyID)
  609. {
  610. $build = $this->getByID($buildID);
  611. $build->stories = trim(str_replace(",$storyID,", ',', ",$build->stories,"), ',');
  612. if($build->stories) $build->stories = ',' . $build->stories;
  613. $this->dao->update(TABLE_BUILD)->set('stories')->eq($build->stories)->where('id')->eq($buildID)->exec();
  614. $this->loadModel('action')->create('story', $storyID, 'unlinkedfrombuild', '', $buildID, '', false);
  615. return !dao::isError();
  616. }
  617. /**
  618. * 批量解除需求关联。
  619. * Batch unlink story.
  620. *
  621. * @param int $buildID
  622. * @param array $storyIDList
  623. * @access public
  624. * @return bool
  625. */
  626. public function batchUnlinkStory($buildID, $storyIDList)
  627. {
  628. if(empty($storyIDList)) return true;
  629. $build = $this->getByID($buildID);
  630. $build->stories = ",$build->stories,";
  631. foreach($storyIDList as $storyID) $build->stories = str_replace(",$storyID,", ',', $build->stories);
  632. $build->stories = trim($build->stories, ',');
  633. $this->dao->update(TABLE_BUILD)->set('stories')->eq($build->stories)->where('id')->eq((int)$buildID)->exec();
  634. $this->loadModel('action');
  635. foreach($storyIDList as $storyID) $this->action->create('story', (int)$storyID, 'unlinkedfrombuild', '', $buildID);
  636. return !dao::isError();
  637. }
  638. /**
  639. * 版本关联Bug。
  640. * Link bugs.
  641. *
  642. * @param int $buildID
  643. * @param array $bugIdList
  644. * @param array $resolvedList
  645. * @access public
  646. * @return bool
  647. */
  648. public function linkBug($buildID, $bugIdList, $resolvedList = array())
  649. {
  650. $build = $this->getByID($buildID);
  651. if(!$build) return false;
  652. foreach($bugIdList as $i => $bugID)
  653. {
  654. if(strpos(",{$build->bugs},", ",{$bugID},") !== false) unset($bugIdList[$i]);
  655. }
  656. $build->bugs .= ',' . implode(',', $bugIdList);
  657. $this->updateLinkedBug($build, $bugIdList, $resolvedList);
  658. $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$buildID)->exec();
  659. $this->loadModel('action');
  660. foreach($bugIdList as $bugID) $this->action->create('bug', (int)$bugID, 'linked2bug', '', $buildID);
  661. return !dao::isError();
  662. }
  663. /**
  664. * 解除Bug跟版本的关联关系。
  665. * Unlink bug.
  666. *
  667. * @param int $buildID
  668. * @param int $bugID
  669. * @access public
  670. * @return void
  671. */
  672. public function unlinkBug($buildID, $bugID)
  673. {
  674. $build = $this->getByID($buildID);
  675. if(!$build) return;
  676. $build->bugs = trim(str_replace(",$bugID,", ',', ",$build->bugs,"), ',');
  677. if($build->bugs) $build->bugs = ',' . $build->bugs;
  678. $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq($buildID)->exec();
  679. $this->loadModel('action')->create('bug', $bugID, 'unlinkedfrombuild', '', $buildID, '', false);
  680. }
  681. /**
  682. * 批量解除Bug跟版本的关联关系。
  683. * Batch unlink bug.
  684. *
  685. * @param int $buildID
  686. * @param array $bugIdList
  687. * @access public
  688. * @return bool
  689. */
  690. public function batchUnlinkBug($buildID, $bugIdList)
  691. {
  692. if(empty($bugIdList)) return true;
  693. $build = $this->getByID($buildID);
  694. if(!$build) return false;
  695. $build->bugs = ",$build->bugs,";
  696. foreach($bugIdList as $bugID) $build->bugs = str_replace(",$bugID,", ',', $build->bugs);
  697. $build->bugs = trim($build->bugs, ',');
  698. $this->dao->update(TABLE_BUILD)->set('bugs')->eq($build->bugs)->where('id')->eq((int)$buildID)->exec();
  699. $this->loadModel('action');
  700. foreach($bugIdList as $unlinkBugID) $this->action->create('bug', (int)$unlinkBugID, 'unlinkedfrombuild', '', $buildID);
  701. return !dao::isError();
  702. }
  703. /**
  704. * 更新子版本关联的Bug。
  705. * Bugs and stories associated with child builds.
  706. *
  707. * @param object $build
  708. * @access public
  709. * @return object
  710. */
  711. public function joinChildBuilds($build)
  712. {
  713. $build->allBugs = $build->bugs;
  714. $build->allStories = $build->stories;
  715. $childBuilds = $this->getByList(explode(',', $build->builds));
  716. foreach($childBuilds as $childBuild)
  717. {
  718. if($childBuild->bugs) $build->allBugs .= ",{$childBuild->bugs}";
  719. if($childBuild->stories) $build->allStories .= ",{$childBuild->stories}";
  720. }
  721. $build->allBugs = explode(',', $build->allBugs);
  722. $build->allBugs = implode(',', array_unique(array_filter($build->allBugs)));
  723. $build->allStories = explode(',', $build->allStories);
  724. $build->allStories = implode(',', array_unique(array_filter($build->allStories)));
  725. return $build;
  726. }
  727. /**
  728. * 检查按钮是否可用。
  729. * Adjust the action is clickable.
  730. *
  731. * @param object $buikd
  732. * @param string $action
  733. * @param string $module
  734. * @access public
  735. * @return bool
  736. * @param object $build
  737. */
  738. public static function isClickable($build, $action, $module = 'bug')
  739. {
  740. $action = strtolower($action);
  741. if($module == 'testtask' && $action == 'create') return !$build->executionDeleted;
  742. return true;
  743. }
  744. /**
  745. * 为区块获取版本数据。
  746. * Get build's data for block.
  747. *
  748. * @param int $projectID
  749. * @param string $orderBy
  750. * @param int $limit
  751. * @access public
  752. * @return object[]
  753. */
  754. public function getBuildBlockData($projectID = 0, $orderBy = 'id_desc', $limit = 10)
  755. {
  756. return $this->dao->select('*')->from(TABLE_BUILD)
  757. ->where('deleted')->eq('0')
  758. ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi()
  759. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  760. ->orderBy($orderBy)
  761. ->limit($limit)
  762. ->fetchAll();
  763. }
  764. /**
  765. * 处理版本编辑前没有执行的情况。
  766. * Process build for update when the build has no execution.
  767. *
  768. * @param object $build
  769. * @param object $oldBuild
  770. * @access public
  771. * @return object
  772. */
  773. public function processBuildForUpdate($build, $oldBuild)
  774. {
  775. if(!empty($oldBuild->execution)) return $build;
  776. $buildBranch = array();
  777. foreach(explode(',', trim($build->branch, ',')) as $branchID) $buildBranch[$branchID] = $branchID;
  778. /* Get delete builds. */
  779. $deleteBuilds = array();
  780. $newBuilds = isset($build->builds) ? explode(',', $build->builds) : array();
  781. foreach($newBuilds as $oldBuildID)
  782. {
  783. if(empty($oldBuildID)) continue;
  784. if(!in_array($oldBuildID, $newBuilds)) $deleteBuilds[$oldBuildID] = $oldBuildID;
  785. }
  786. /* Delete the branch when the branch of the deleted build has no linked stories. */
  787. $storyBranches = $this->dao->select('branch')->from(TABLE_STORY)->where('id')->in($oldBuild->stories)->fetchPairs('branch');
  788. $branches = $this->dao->select('branch')->from(TABLE_BUILD)->where('id')->in($newBuilds + $deleteBuilds)->fetchPairs();
  789. foreach($branches as $branch)
  790. {
  791. foreach(explode(',', $branch) as $branchID)
  792. {
  793. if(empty($branchID)) continue;
  794. if(in_array($branchID, $deleteBuilds) && isset($storyBranches[$branchID])) continue;
  795. if(in_array($branchID, $newBuilds) && isset($buildBranch[$branchID])) continue;
  796. if(in_array($branchID, $deleteBuilds)) unset($buildBranch[$branchID]);
  797. if(in_array($branchID, $newBuilds)) $buildBranch[$branchID] = $branchID;
  798. }
  799. }
  800. $build->branch = implode(',', $buildBranch);
  801. return $build;
  802. }
  803. /**
  804. * 获取版本关联的bug列表。
  805. * Get bug list of build.
  806. *
  807. * @param string $bugIdList
  808. * @param string $orderBy
  809. * @param object $pager
  810. * @access public
  811. * @return array
  812. */
  813. public function getBugList($bugIdList, $orderBy = '', $pager = null)
  814. {
  815. return $this->dao->select('*')->from(TABLE_BUG)
  816. ->where('id')->in($bugIdList)
  817. ->andWhere('deleted')->eq(0)
  818. ->beginIF($orderBy)->orderBy($orderBy)->fi()
  819. ->page($pager)
  820. ->fetchAll();
  821. }
  822. /**
  823. * 获取版本关联的story列表。
  824. * Get story list of build.
  825. *
  826. * @param string $storyIdList
  827. * @param string $orderBy
  828. * @param object $pager
  829. * @access public
  830. * @return array
  831. * @param int $branch
  832. */
  833. public function getStoryList($storyIdList, $branch = 0, $orderBy = '', $pager = null)
  834. {
  835. $stories = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY)
  836. ->where('id')->in($storyIdList)
  837. ->andWhere('deleted')->eq(0)
  838. ->beginIF($orderBy)->orderBy($orderBy)->fi()
  839. ->page($pager)
  840. ->fetchAll('id');
  841. $stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->in(array_keys($stories))->andWhere('branch')->eq($branch)->fetchPairs('story', 'stage');
  842. foreach($stages as $storyID => $stage) $stories[$storyID]->stage = $stage;
  843. return $stories;
  844. }
  845. /**
  846. * 给版本中的发布增加标识。
  847. * Add label for the release in the builds.
  848. *
  849. * @param int $productID
  850. * @param array $builds
  851. * @access protected
  852. * @return array
  853. */
  854. public function addReleaseLabelForBuilds($productID, $builds)
  855. {
  856. $releases = $this->getRelatedReleases(array($productID));
  857. $buildItems = array();
  858. $buildsPinyin = common::convert2Pinyin($builds);
  859. foreach($builds as $buildID => $buildName) $buildItems[$buildID] = array('value' => $buildID, 'text' => $buildName, 'keys' => $buildID . $buildName . (!empty($buildsPinyin[$buildID]) ? $buildsPinyin[$buildID] : ''));
  860. foreach($releases as $release)
  861. {
  862. if(isset($buildItems[$release->shadow])) $buildItems[$release->shadow]['content'] = array('html' => "<div class='flex clip'>{$buildItems[$release->shadow]['text']}</div><label class='label bg-primary-50 text-primary ml-1 flex-none'>{$this->lang->release->common}</label>", 'class' => 'w-full flex nowrap');
  863. }
  864. return array_values($buildItems);
  865. }
  866. }