model.php 93 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237
  1. <?php
  2. /**
  3. * The model file of product 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 Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package product
  9. * @link https://www.zentao.net
  10. */
  11. class productModel extends model
  12. {
  13. /**
  14. * 检查是否有权限访问该产品。
  15. * Check privilege.
  16. *
  17. * @param int $product
  18. * @access public
  19. * @return bool
  20. * @param int $productID
  21. */
  22. public function checkPriv($productID)
  23. {
  24. if(common::isTutorialMode()) return true;
  25. return !empty($productID) && ($this->app->user->admin || (strpos(",{$this->app->user->view->products},", ",{$productID},") !== false));
  26. }
  27. /**
  28. * 根据id获取产品。
  29. * Get product by id.
  30. *
  31. * @param int $productID
  32. * @access public
  33. * @return object|false
  34. */
  35. public function getByID($productID)
  36. {
  37. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProduct();
  38. $product = $this->fetchById($productID);
  39. if(!$product) return false;
  40. return $this->loadModel('file')->replaceImgURL($product, 'desc');
  41. }
  42. /**
  43. * 根据id列表获取产品列表。
  44. * Get products by id list.
  45. *
  46. * @param array $productIdList
  47. * @access public
  48. * @return array
  49. */
  50. public function getByIdList($productIdList)
  51. {
  52. return $this->dao->select('*')->from(TABLE_PRODUCT)->where('id')->in($productIdList)->fetchAll('id', false);
  53. }
  54. /**
  55. * 根据权限控制范围获取产品。
  56. * Get products by acl.
  57. *
  58. * @param string $acl
  59. * @access public
  60. * @return array
  61. */
  62. public function getListByAcl($acl)
  63. {
  64. return $this->dao->select('id, program, PO, QD, RD, feedback, ticket, acl, whitelist, reviewer, PMT, createdBy')->from(TABLE_PRODUCT)->where('acl')->in($acl)->fetchAll('id');
  65. }
  66. /**
  67. * 通过session 中的搜索条件查询产品列表。
  68. * Get product list by search.
  69. *
  70. * @param int $queryID
  71. * @access public
  72. * @return array
  73. */
  74. public function getListBySearch($queryID = 0)
  75. {
  76. $this->loadModel('search')->setQuery('product', $queryID);
  77. return $this->dao->select('*')->from(TABLE_PRODUCT)->where($this->session->productQuery)
  78. ->andWhere('deleted')->eq(0)
  79. ->andWhere('shadow')->eq(0)
  80. ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->products)->fi()
  81. ->beginIF($this->config->vision != 'or')->andWhere("FIND_IN_SET('{$this->config->vision}', vision)")->fi()
  82. ->filterTpl('skip')
  83. ->orderBy('order_asc')
  84. ->fetchAll('id', false);
  85. }
  86. /**
  87. * 获取产品键值对列表。
  88. * Get product pairs.
  89. *
  90. * @param string $mode all|noclosed
  91. * @param int $programID
  92. * @param string|array $append
  93. * @param string|int $shadow all|0|1
  94. * @return int[]
  95. */
  96. public function getPairs($mode = '', $programID = 0, $append = '', $shadow = 0)
  97. {
  98. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProductPairs();
  99. return $this->productTao->fetchPairs($mode, $programID, $append, $shadow);
  100. }
  101. /**
  102. * 根据项目获取关联的产品键值对列表。
  103. * Get product pairs by project.
  104. *
  105. * @param int|array $projectID
  106. * @param string $status all|noclosed
  107. * @param string $append
  108. * @param bool $noDeleted
  109. * @param bool $noShadow
  110. * @access public
  111. * @return array
  112. */
  113. public function getProductPairsByProject($projectID = 0, $status = 'all', $append = '', $noDeleted = true, $noShadow = false)
  114. {
  115. if(empty($projectID)) return array();
  116. return $this->getProducts($projectID, $status, '', false, $append, $noDeleted, $noShadow);
  117. }
  118. /**
  119. * 获取所有与某类型的项目所关联的产品。
  120. * Get product pairs by project model.
  121. *
  122. * @param string $model all|scrum|waterfall|kanban
  123. * @access public
  124. * @return array
  125. */
  126. public function getPairsByProjectModel($model = 'all')
  127. {
  128. return $this->dao->select('t3.id, t3.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  129. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  130. ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id')
  131. ->where('t3.deleted')->eq(0)
  132. ->beginIF(!$this->app->user->admin)->andWhere('t3.id')->in($this->app->user->view->products)->fi()
  133. ->andWhere("FIND_IN_SET('{$this->config->vision}', t3.vision)")
  134. ->beginIF($model != 'all')->andWhere('t2.model')->eq($model)->fi()
  135. ->fetchPairs('id', 'name');
  136. }
  137. /**
  138. * 获取关联项目的产品。
  139. * Get products by project.
  140. *
  141. * @param int|array $projectID
  142. * @param string $status all|noclosed
  143. * @param string $orderBy
  144. * @param bool $withBranch
  145. * @param string|array $append
  146. * @param bool $noDeleted
  147. * @param bool $noShadow
  148. * @access public
  149. * @return int[]
  150. */
  151. public function getProducts($projectID = 0, $status = 'all', $orderBy = '', $withBranch = true, $append = '', $noDeleted = true, $noShadow = false)
  152. {
  153. /* 如果是新手教程模式,直接返回测试数据。*/
  154. if(commonModel::isTutorialMode())
  155. {
  156. $this->loadModel('tutorial');
  157. if(!$withBranch) return $this->tutorial->getProductPairs();
  158. return $this->tutorial->getExecutionProducts();
  159. }
  160. /* 初始化变量。 */
  161. $projectProducts = $this->productTao->getProductsByProjectID($projectID, $append, $status, $orderBy, $noDeleted, $noShadow);
  162. $products = array();
  163. /* 如果不返回分支信息,则返回 id=>name 的键值对。 */
  164. if(!$withBranch)
  165. {
  166. foreach($projectProducts as $product) $products[$product->id] = $product->deleted ? $product->name . "({$this->lang->product->deleted})" : $product->name;
  167. return $products;
  168. }
  169. /* 将分支 ID 、计划 ID 合并到产品数据中。*/
  170. foreach($projectProducts as $product)
  171. {
  172. if(!isset($products[$product->id]))
  173. {
  174. $products[$product->id] = $product;
  175. $products[$product->id]->branches = array();
  176. $products[$product->id]->plans = array();
  177. }
  178. $products[$product->id]->branches[$product->branch] = $product->branch;
  179. if($product->plan) $products[$product->id]->plans[$product->plan] = $product->plan;
  180. unset($product->branch, $product->plan);
  181. }
  182. return $products;
  183. }
  184. /**
  185. * 通过项目id查询关联的产品id。
  186. * Get product id list or first id by project.
  187. *
  188. * @param int $projectID
  189. * @param bool $isFirst
  190. * @access public
  191. * @return object[]|int
  192. */
  193. public function getProductIDByProject($projectID, $isFirst = true)
  194. {
  195. $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)
  196. ->where('1=1')
  197. ->beginIF(!$this->app->user->admin)->andWhere('product')->in($this->app->user->view->products)->fi()
  198. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  199. ->fetchPairs();
  200. if($isFirst === false) return $products;
  201. return empty($products) ? 0 : current($products);
  202. }
  203. /**
  204. * 根据项目id获取其对应的影子产品。
  205. * Get shadow product by project id.
  206. *
  207. * @param int $projectID
  208. * @access public
  209. * @return object|false
  210. */
  211. public function getShadowProductByProject($projectID)
  212. {
  213. return $this->dao->select('products.*')->from(TABLE_PRODUCT)->alias('products')
  214. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('relations')->on('products.id = relations.product')
  215. ->where('products.shadow')->eq(1)
  216. ->andWhere('relations.project')->eq($projectID)
  217. ->fetch();
  218. }
  219. /**
  220. * 遍历产品列表,将产品线拼接到产品名上。
  221. * Iterate products, concatenating product line onto product name.
  222. *
  223. * @param array $products
  224. * @access public
  225. * @return array
  226. */
  227. public function concatProductLine($products)
  228. {
  229. $lines = $this->getLinePairs();
  230. $productsWithLine = array();
  231. $productsNoLine = array();
  232. foreach($products as $product)
  233. {
  234. if(array_key_exists($product->line, $lines))
  235. {
  236. $lineName = $lines[$product->line];
  237. if(in_array($this->config->systemMode, array('ALM', 'PLM'))) $product->name = $lineName . '/' . $product->name;
  238. $productsWithLine[] = $product;
  239. }
  240. else
  241. {
  242. $productsNoLine[] = $product;
  243. }
  244. }
  245. return array_merge($productsWithLine, $productsNoLine);
  246. }
  247. /**
  248. * 获取排序后的产品列表,顺序为:我的产品、其他人的产品、关闭的产品。
  249. * Get ordered products.
  250. *
  251. * @param string $status
  252. * @param int $num
  253. * @param int $projectID
  254. * @param string|int $shadow all|0|1
  255. * @access public
  256. * @return array
  257. */
  258. public function getOrderedProducts($status, $num = 0, $projectID = 0, $shadow = 0)
  259. {
  260. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getProductStats();
  261. $products = array();
  262. if($projectID)
  263. {
  264. $pairs = $this->getProducts($projectID, $status == 'normal' ? 'noclosed' : '');
  265. $products = $this->getByIdList(array_keys($pairs));
  266. }
  267. else
  268. {
  269. $products = $this->getList(0, $status, $num, 0, $shadow);
  270. }
  271. if(empty($products)) return $products;
  272. $productList = $this->concatProductLine($products);
  273. $currentUser = $this->app->user->account;
  274. $orderedProducts = $mineProducts = $othersProducts = $closedProducts = array();
  275. foreach($productList as $product)
  276. {
  277. if(!$this->app->user->admin and !$this->checkPriv($product->id)) continue;
  278. if($product->status == 'normal' and $product->PO == $currentUser) $mineProducts[$product->id] = $product;
  279. if($product->status == 'normal' and $product->PO != $currentUser) $othersProducts[$product->id] = $product;
  280. if($product->status == 'closed') $closedProducts[$product->id] = $product;
  281. }
  282. $orderedProducts = $mineProducts + $othersProducts + $closedProducts;
  283. if(empty($num)) return $orderedProducts;
  284. return array_slice($orderedProducts, 0, $num, true);
  285. }
  286. /**
  287. * 获取多分支产品和多平台产品。
  288. * Get Multi-branch and Multi-platform product pairs.
  289. *
  290. * @param int $programID
  291. * @access public
  292. * @return array
  293. */
  294. public function getMultiBranchPairs($programID = 0)
  295. {
  296. return $this->dao->select('id')->from(TABLE_PRODUCT)
  297. ->where('deleted')->eq(0)
  298. ->beginIF($programID)->andWhere('program')->eq($programID)->fi()
  299. ->andWhere('type')->in('branch,platform')
  300. ->fetchPairs();
  301. }
  302. /**
  303. * 获取以所属项目集分组的产品列表。
  304. * Get products group by program.
  305. *
  306. * @access public
  307. * @return array
  308. */
  309. public function getProductsGroupByProgram()
  310. {
  311. $products = $this->getList(0, 'noclosed');
  312. $programIdList = array_unique(array_column($products, 'program'));
  313. $programs = helper::hasFeature('program') ? $this->loadModel('program')->getPairsByList($programIdList) : array();
  314. $productGroup = array();
  315. foreach($products as $product) $productGroup[$product->program][$product->id] = zget($programs, $product->program, '') . '/' . $product->name;
  316. return $productGroup;
  317. }
  318. /**
  319. * 用对象数据创建产品
  320. * Create a product.
  321. *
  322. * @param object $product
  323. * @param string $lineName
  324. * @access public
  325. * @return int|false
  326. */
  327. public function create($product, $lineName = '')
  328. {
  329. /* Insert product and get the product ID. */
  330. $this->lang->error->unique = $this->lang->error->repeat;
  331. $this->dao->insert(TABLE_PRODUCT)->data($product)->autoCheck()
  332. ->checkIF((!empty($product->name) && isset($product->program)), 'name', 'unique', "`program` = " . zget($product, 'program') . " AND `deleted` = '0'")
  333. ->checkIF((!empty($product->code) && isset($product->program)), 'code', 'unique', "`program` = " . zget($product, 'program') . " AND `deleted` = '0'")
  334. ->batchCheck($this->config->product->create->requiredFields, 'notempty')
  335. ->checkFlow()
  336. ->exec();
  337. if(dao::isError()) return false;
  338. $productID = $this->dao->lastInsertID();
  339. /* Fix order and line fields for product. */
  340. $fixData = new stdclass();
  341. $fixData->order = $productID * 5;
  342. if(!empty($lineName))
  343. {
  344. $lineID = $this->productTao->createLine(!empty($product->program) ? (int)$product->program : 0, $lineName);
  345. if($lineID) $fixData->line = $lineID;
  346. }
  347. $this->dao->update(TABLE_PRODUCT)->data($fixData)->where('id')->eq($productID)->exec();
  348. /* Update and create linked data. */
  349. $this->loadModel('action')->create('product', $productID, 'opened');
  350. $uid = empty($this->post->uid) ? '' : $this->post->uid;
  351. $this->loadModel('file')->updateObjectID($uid, $productID, 'product');
  352. $this->productTao->createMainLib($productID);
  353. if($product->whitelist) $this->loadModel('personnel')->updateWhitelist(explode(',', $product->whitelist), 'product', $productID);
  354. if($product->acl != 'open') $this->loadModel('user')->updateUserView(array($productID), 'product');
  355. return $productID;
  356. }
  357. /**
  358. * 更新产品数据。
  359. * Update a product.
  360. *
  361. * @param int $productID
  362. * @param object $product
  363. * @access public
  364. * @return array|false
  365. */
  366. public function update($productID, $product)
  367. {
  368. $oldProduct = $this->fetchByID($productID);
  369. $this->lang->error->unique = $this->lang->error->repeat;
  370. $result = $this->productTao->doUpdate($product, $productID, zget($product, 'program', $oldProduct->program));
  371. if(!$result) return false;
  372. /* Update objectID field of file recode, that upload by editor. */
  373. $this->loadModel('file')->updateObjectID($this->post->uid, $productID, 'product');
  374. $whitelist = explode(',', $product->whitelist);
  375. $this->loadModel('personnel')->updateWhitelist($whitelist, 'product', $productID);
  376. if($oldProduct->acl != $product->acl and $product->acl != 'open') $this->loadModel('user')->updateUserView(array($productID), 'product');
  377. if($product->type == 'normal' and $oldProduct->type != 'normal') $this->loadModel('branch')->unlinkBranch4Project(array($productID));
  378. if($product->type != 'normal' and $oldProduct->type == 'normal') $this->loadModel('branch')->linkBranch4Project($productID);
  379. /* Save action and changes. */
  380. $changes = common::createChanges($oldProduct, $product);
  381. if($changes)
  382. {
  383. $actionID = $this->loadModel('action')->create('product', $productID, 'edited');
  384. $this->action->logHistory($actionID, $changes);
  385. }
  386. return $changes;
  387. }
  388. /**
  389. * 批量更新产品信息。
  390. * Batch update products.
  391. *
  392. * @param array $products
  393. * @access public
  394. * @return array
  395. */
  396. public function batchUpdate($products)
  397. {
  398. if(empty($products)) return array();
  399. /* 初始化变量。*/
  400. $oldProducts = $this->getByIdList(array_keys($products));
  401. $allChanges = array();
  402. $updateViewProducts = array();
  403. $unlinkProducts = array();
  404. $linkProducts = array();
  405. /* 根据产品ID,循环更新的产品信息. */
  406. $this->loadModel('personnel');
  407. $this->lang->error->unique = $this->lang->error->repeat;
  408. foreach($products as $productID => $product)
  409. {
  410. $oldProduct = $oldProducts[$productID];
  411. $programID = (int)zget($product, 'program', $oldProduct->program);
  412. $result = $this->productTao->doUpdate($product, $productID, $programID);
  413. if(!$result) return array('result' => 'fail', 'message' => 'product#' . $productID . dao::getError(true));
  414. /* When acl is open, white list set empty. */
  415. if($product->acl == 'open') $this->personnel->updateWhitelist(array(), 'product', $productID);
  416. /* 如果产品类型或权限有修改,则标记该产品,用做后面处理。*/
  417. if($oldProduct->acl != $product->acl and $product->acl != 'open') $updateViewProducts[] = $productID;
  418. if($product->type == 'normal' and $oldProduct->type != 'normal') $unlinkProducts[] = $productID;
  419. if($product->type != 'normal' and $oldProduct->type == 'normal') $linkProducts[] = $productID;
  420. $allChanges[$productID] = common::createChanges($oldProduct, $product);
  421. }
  422. /* 对标记的产品,批量做对应的后续处理。*/
  423. if(!empty($updateViewProducts)) $this->loadModel('user')->updateUserView($updateViewProducts, 'product');
  424. if(!empty($unlinkProducts)) $this->loadModel('branch')->unlinkBranch4Project($unlinkProducts);
  425. if(!empty($linkProducts)) $this->loadModel('branch')->linkBranch4Project($linkProducts);
  426. /* Save actions. */
  427. $this->loadModel('action');
  428. foreach($allChanges as $productID => $changes)
  429. {
  430. if(empty($changes)) continue;
  431. $actionID = $this->action->create('product', $productID, 'Edited');
  432. $this->action->logHistory($actionID, $changes);
  433. }
  434. return $allChanges;
  435. }
  436. /**
  437. * 关闭产品。
  438. * Close product.
  439. *
  440. * @param int $productID
  441. * @param object $product must have status field.
  442. * @param string|false $comment
  443. * @access public
  444. * @return array|false
  445. */
  446. public function close($productID, $product, $comment = '')
  447. {
  448. $oldProduct = $this->getByID($productID);
  449. if(empty($product)) return false;
  450. $this->dao->update(TABLE_PRODUCT)->data($product)->autoCheck()
  451. ->checkFlow()
  452. ->where('id')->eq($productID)
  453. ->exec();
  454. if(dao::isError()) return false;
  455. $changes = common::createChanges($oldProduct, $product);
  456. if(!empty($comment) or !empty($changes))
  457. {
  458. $actionID = $this->loadModel('action')->create('product', $productID, 'Closed', $comment);
  459. $this->action->logHistory($actionID, $changes);
  460. }
  461. return $changes;
  462. }
  463. /**
  464. * 激活产品。
  465. * Activate a product.
  466. *
  467. * @param int $productID
  468. * @param object $product must have status field.
  469. * @param string|false $comment
  470. * @access public
  471. * @return array|false
  472. */
  473. public function activate($productID, $product, $comment = '')
  474. {
  475. $oldProduct = $this->getByID($productID);
  476. if(empty($product)) return false;
  477. $this->dao->update(TABLE_PRODUCT)->data($product)->autoCheck()
  478. ->checkFlow()
  479. ->where('id')->eq($productID)
  480. ->exec();
  481. if(dao::isError()) return false;
  482. $changes = common::createChanges($oldProduct, $product);
  483. if(!empty($comment) or !empty($changes))
  484. {
  485. $actionID = $this->loadModel('action')->create('product', $productID, 'Activated', $comment);
  486. $this->action->logHistory($actionID, $changes);
  487. }
  488. return $changes;
  489. }
  490. /**
  491. * 更新排序。
  492. * Sort order field.
  493. *
  494. * @param array $sortedIdList
  495. * @access public
  496. * @return void
  497. */
  498. public function updateOrder($sortedIdList)
  499. {
  500. /* Remove programID. */
  501. $sortedIdList = array_values(array_filter(array_map(function($id){return (is_numeric($id) and $id > 0) ? $id : null;}, $sortedIdList)));
  502. if(empty($sortedIdList)) return;
  503. /* Get the list of products before sorting. */
  504. $products = $this->dao->select('`order`, id')->from(TABLE_PRODUCT)->where('id')->in($sortedIdList)->orderBy('order_asc')->fetchPairs('order', 'id');
  505. /* Update order by sorted id list. */
  506. foreach($products as $order => $id)
  507. {
  508. $newID = array_shift($sortedIdList);
  509. if($id == $newID) continue;
  510. $this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($order)->where('id')->eq($newID)->exec();
  511. }
  512. }
  513. /**
  514. * 添加或更新产品线。
  515. * Add or update product line.
  516. *
  517. * @param array $lines
  518. * @access public
  519. * @return array
  520. */
  521. public function manageLine($lines)
  522. {
  523. /* Init product line object. */
  524. $line = new stdclass();
  525. $line->type = 'line';
  526. $line->parent = 0;
  527. $line->grade = 1;
  528. /* Get the max order number. */
  529. $maxOrder = (int)$this->dao->select("`order`")->from(TABLE_MODULE)->where('type')->eq('line')->orderBy('`order`_desc')->limit(1)->fetch('order');
  530. foreach($lines as $programID => $lineNameList)
  531. {
  532. foreach($lineNameList as $lineID => $lineName)
  533. {
  534. $isInsert = is_numeric($lineID);
  535. if(!$isInsert) $lineID = str_replace('id', '', $lineID);
  536. /* Build product line data. */
  537. $line->name = strip_tags(trim($lineName));
  538. $line->root = $programID;
  539. if($isInsert)
  540. {
  541. $maxOrder += 10; //Reserve for extension. Increment the order number by 10.
  542. $line->order = $maxOrder;
  543. /* Insert product line. */
  544. $this->dao->insert(TABLE_MODULE)->data($line)->exec();
  545. $lineID = $this->dao->lastInsertID();
  546. /* Compute product line path and update it. */
  547. $path = ",$lineID,";
  548. $this->dao->update(TABLE_MODULE)->set('path')->eq($path)->where('id')->eq($lineID)->exec();
  549. }
  550. else
  551. {
  552. unset($line->order);
  553. $this->dao->update(TABLE_MODULE)->data($line)->where('id')->eq($lineID)->exec();
  554. }
  555. if(!dao::isError()) $this->productTao->syncProgramToProduct($programID, (int)$lineID);
  556. }
  557. }
  558. if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
  559. return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => true);
  560. }
  561. /**
  562. * 获取需求列表。
  563. * Get stories.
  564. *
  565. * @param int|array $productID
  566. * @param string $branch
  567. * @param string $browseType bymodule|unclosed|allstory|assignedtome|openedbyme|reviewbyme|draftstory|reviewedbyme|assignedbyme|closedbyme|activestory|changingstory|reviewingstory|willclose|closedstory
  568. * @param int $queryID
  569. * @param int $moduleID
  570. * @param string $type requirement|story
  571. * @param string $sort
  572. * @param object $pager
  573. * @access public
  574. * @return array
  575. */
  576. public function getStories($productID, $branch, $browseType, $queryID, $moduleID, $type = 'story', $sort = 'id_desc', $pager = null)
  577. {
  578. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getStories();
  579. /* Set modules and browse type. */
  580. $modules = $moduleID ? $this->loadModel('tree')->getAllChildID($moduleID) : '0';
  581. $browseType = $browseType == 'bybranch' ? 'bymodule' : $browseType;
  582. $browseType = ($browseType == 'bymodule' and $this->session->storyBrowseType and $this->session->storyBrowseType != 'bysearch') ? $this->session->storyBrowseType : $browseType;
  583. /* Get stories by browseType. */
  584. if(!isset($this->story)) $this->loadModel('story');
  585. if($browseType == 'assignedtome') return $this->story->getByAssignedTo($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
  586. if($browseType == 'openedbyme') return $this->story->getByOpenedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
  587. if($browseType == 'reviewedbyme') return $this->story->getByReviewedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
  588. if($browseType == 'reviewbyme') return $this->story->getByReviewBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
  589. if($browseType == 'closedbyme') return $this->story->getByClosedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
  590. if($browseType == 'assignedbyme') return $this->story->getByAssignedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
  591. if($browseType == 'unplan') return $this->story->getByPlan($productID, $queryID, $modules, '', $type, $sort, $pager);
  592. if($browseType == 'bymodule') return $this->story->getProductStories($productID, $branch, $modules, 'all', $type, $sort, true, '', $pager);
  593. if($browseType == 'bysearch') return $this->story->getBySearch($productID, $branch, $queryID, $sort, 0, $type, '', '', $pager);
  594. if($browseType == 'willclose') return $this->story->get2BeClosed($productID, $branch, $modules, $type, $sort, $pager);
  595. if($browseType == 'fromfeedback') return $this->story->getFeedbackStories($productID, $branch, $modules, $type, $sort, $pager);
  596. /* all or allstory. */
  597. if(strpos($browseType, 'all') !== false) return $this->story->getProductStories($productID, $branch, $modules, 'all', $type, $sort, true, '', $pager);
  598. if($browseType == 'unclosed')
  599. {
  600. $unclosedStatus = $this->lang->story->statusList;
  601. unset($unclosedStatus['closed']);
  602. $stories = $this->story->getProductStories($productID, $branch, $modules, array_keys($unclosedStatus), $type, $sort, true, '', $pager);
  603. foreach($stories as $storyID => $story)
  604. {
  605. if($story->type != 'story' && $story->stage == 'closed') unset($stories[$storyID]);
  606. }
  607. $this->app->loadClass('pager', true);
  608. $pager = new pager(count($stories), $pager ? $pager->recPerPage : 20, $pager ? $pager->pageID : 1);
  609. return $stories ? current(array_chunk($stories, $pager->recPerPage, true)) : array();
  610. }
  611. /* Set default function called, when browseType is (draftstory, activestory, changingstory, reviewingstory, closedstory, developingstory, launchedstory). */
  612. return $this->story->getByStatus($productID, $branch, $modules, substr($browseType, 0, -5), $type, $sort, $pager);
  613. }
  614. /**
  615. * 批量获取需求的阶段数据。
  616. * Batch get story stage.
  617. *
  618. * @param array $stories.
  619. * @access public
  620. * @return array
  621. */
  622. public function batchGetStoryStage($stories)
  623. {
  624. /* Set story id list. */
  625. $storyIdList = array();
  626. foreach($stories as $story) $storyIdList[$story->id] = $story->id;
  627. return $this->loadModel('story')->batchGetStoryStage($storyIdList);
  628. }
  629. /**
  630. * 在需求列表页面构建搜索表单。
  631. * Build search form for story list page.
  632. *
  633. * @param int $productID
  634. * @param array $products
  635. * @param int $queryID
  636. * @param string $actionURL
  637. * @param string $storyType
  638. * @param string $branch
  639. * @param int $projectID
  640. * @access public
  641. * @return void
  642. */
  643. public function buildSearchForm($productID, $products, $queryID, $actionURL, $storyType = 'story', $branch = '', $projectID = 0)
  644. {
  645. $searchConfig = $this->config->product->search;
  646. $searchConfig['queryID'] = $queryID;
  647. $searchConfig['actionURL'] = $actionURL;
  648. /* Get product data. */
  649. if(empty($productID) && empty($products))
  650. {
  651. $products = $this->getPairs('all', 0, '', 'all');
  652. $productID = key($products);
  653. $showAll = true;
  654. }
  655. $product = ($this->app->tab == 'project' && empty($productID)) || !empty($showAll) ? $products : array();
  656. if(empty($product) && isset($products[$productID])) $product = array($productID => $products[$productID]);
  657. $searchConfig['params']['product']['values'] = array('' => '') + $product + array('all' => $this->lang->product->allProduct);
  658. /* Get module data. */
  659. $projectID = ($this->app->tab == 'project' && empty($projectID)) ? $this->session->project : $projectID;
  660. $searchConfig['params']['module']['values'] = empty($showAll) ? $this->productTao->getModulesForSearchForm($productID, $products, $branch, (int)$projectID) : $this->loadModel('tree')->getAllModulePairs('story');
  661. $gradePairs = $this->loadModel('story')->getGradePairs($storyType, 'all');
  662. if($projectID || $storyType == 'all')
  663. {
  664. $project = $this->loadModel('project')->fetchById($projectID);
  665. $gradePairs = array();
  666. $gradeList = $this->loadModel('story')->getGradeList('');
  667. $storyTypes = isset($project->storyType) ? $project->storyType : 'epic,story,requirement';
  668. foreach($gradeList as $grade)
  669. {
  670. if(strpos($storyTypes, $grade->type) === false) continue;
  671. $key = (string)$grade->type . (string)$grade->grade;
  672. $gradePairs[$key] = $grade->name;
  673. }
  674. asort($gradePairs);
  675. $this->lang->story->title = $this->lang->story->name;
  676. }
  677. elseif($storyType != 'story')
  678. {
  679. $replacement = $storyType == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon;
  680. $this->lang->story->title = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->title);
  681. $this->lang->story->create = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->create);
  682. $searchConfig['fields']['title'] = $this->lang->story->title;
  683. }
  684. $searchConfig['params']['grade']['values'] = $gradePairs;
  685. if($this->config->edition == 'ipd') $searchConfig['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID);
  686. /* Get product plan data. */
  687. $productIdList = ($this->app->tab == 'project' && empty($productID)) || !empty($showAll) ? array_keys($products) : array($productID);
  688. $branchParam = ($this->app->tab == 'project' && empty($productID)) || !empty($showAll) ? '' : $branch;
  689. $searchConfig['params']['plan']['values'] = $this->loadModel('productplan')->getPairs($productIdList, (empty($branchParam) || $branchParam == 'all') ? '' : $branchParam);
  690. /* Get branch data. */
  691. if($productID && empty($showAll))
  692. {
  693. $productInfo = $this->getByID($productID);
  694. if(!empty($productInfo->shadow))
  695. {
  696. unset($searchConfig['fields']['product']);
  697. unset($searchConfig['params']['product']);
  698. }
  699. if($productInfo->type == 'normal' || $this->app->tab == 'assetlib')
  700. {
  701. unset($searchConfig['fields']['branch']);
  702. unset($searchConfig['params']['branch']);
  703. }
  704. else
  705. {
  706. $searchConfig['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productInfo->type]);
  707. $searchConfig['params']['branch']['values'] = array('' => '', '0' => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($productID, 'noempty');
  708. }
  709. }
  710. else
  711. {
  712. unset($searchConfig['fields']['branch']);
  713. unset($searchConfig['params']['branch']);
  714. }
  715. /* Reset the stage list for epic and requirement. */
  716. if($storyType != 'all')
  717. {
  718. $this->app->loadLang($storyType);
  719. $searchConfig['params']['stage']['values'] = $this->lang->$storyType->stageList;
  720. }
  721. $this->loadModel('search')->setSearchParams($searchConfig);
  722. }
  723. /**
  724. * 在产品列表页面构建搜索表单。
  725. * Build search form for product list page.
  726. *
  727. * @param int $queryID
  728. * @param string $actionURL
  729. * @access public
  730. * @return void
  731. */
  732. public function buildProductSearchForm($queryID, $actionURL)
  733. {
  734. $this->config->product->all->search['queryID'] = $queryID;
  735. $this->config->product->all->search['actionURL'] = $actionURL;
  736. if(in_array($this->config->systemMode, array('ALM', 'PLM')))
  737. {
  738. if(helper::hasFeature('program')) $this->config->product->all->search['params']['program']['values'] = $this->loadModel('program')->getTopPairs('noclosed');
  739. $this->config->product->all->search['params']['line']['values'] = $this->getLinePairs();
  740. }
  741. $this->loadModel('search')->setSearchParams($this->config->product->all->search);
  742. }
  743. /**
  744. * 根据产品列表获取关联的项目列表。
  745. * Get project pairs by product.
  746. *
  747. * @param array $productIdList
  748. * @access public
  749. * @return array
  750. */
  751. public function getProjectPairsByProductIdList($productIdList)
  752. {
  753. return $this->dao->select('t2.id, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  754. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  755. ->where('t1.product')->in($productIdList)
  756. ->andWhere('t2.type')->eq('project')
  757. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
  758. ->andWhere('t2.deleted')->eq('0')
  759. ->orderBy('t1.product,order_asc')
  760. ->fetchPairs('id', 'name');
  761. }
  762. /**
  763. * 获取关联某产品的项目键值对列表。
  764. * Get project pairs by product.
  765. *
  766. * @param int $productID
  767. * @param string $branch 'all'|''|int
  768. * @param string|array $appendProject
  769. * @param string $status all|noclosed|closed
  770. * @param string $param multiple|
  771. * @access public
  772. * @return array
  773. */
  774. public function getProjectPairsByProduct($productID, $branch = '0', $appendProject = '', $status = '', $param = '')
  775. {
  776. $product = $this->getByID($productID);
  777. if(empty($product)) return array();
  778. $appendProject = $this->productTao->formatAppendParam($appendProject);
  779. return $this->dao->select('t2.id, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  780. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  781. ->where('(t1.product')->eq($productID)
  782. ->andWhere('t2.type')->eq('project')
  783. ->andWhere('t2.deleted')->eq('0')
  784. ->beginIF($status == 'noclosed')->andWhere('t2.status')->ne('closed')->fi()
  785. ->beginIF($status == 'closed')->andWhere('t2.status')->eq('closed')->fi()
  786. ->beginIF(strpos($param, 'multiple') !== false)->andWhere('t2.multiple')->ne('0')->fi()
  787. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
  788. ->beginIF($product->type != 'normal' and $branch !== '' and $branch != 'all')->andWhere('t1.branch')->in($branch)->fi()
  789. ->markRight(1)
  790. ->beginIF($appendProject)->orWhere('t2.id')->in($appendProject)->fi()
  791. ->orderBy('order_asc')
  792. ->fetchPairs('id', 'name');
  793. }
  794. /**
  795. * 获取与该产品关联的项目列表。
  796. * Get project list by product.
  797. *
  798. * @param int $productID
  799. * @param string $browseType all|undone|wait|doing|done
  800. * @param string $branch
  801. * @param bool $involved $this->cookie->involved or $involved
  802. * @param string $orderBy
  803. * @param object|null $pager
  804. * @access public
  805. * @return array
  806. */
  807. public function getProjectListByProduct($productID, $browseType = 'all', $branch = '0', $involved = false, $orderBy = 'order_desc', $pager = null)
  808. {
  809. $branch = $branch ? $branch : '0';
  810. if(!$involved) $projectList = $this->productTao->fetchAllProductProjects($productID, $browseType, $branch, $orderBy, $pager);
  811. if($involved) $projectList = $this->productTao->fetchInvolvedProductProjects($productID, $browseType, $branch, $orderBy, $pager);
  812. /* Determine how to display the name of the program. */
  813. $programList = $this->loadModel('program')->getParentPairs('', 'all');
  814. foreach($projectList as $id => $project)
  815. {
  816. $programName = $project->parent ? zget($programList, $project->parent, '') : '';
  817. $projectList[$id]->programName = preg_replace('/\//', '', $programName, 1);
  818. }
  819. return $projectList;
  820. }
  821. /**
  822. * 根据产品,获取与该产品关联的项目的统计信息。
  823. * Get project stats by product.
  824. *
  825. * @param int $productID
  826. * @param string $browseType
  827. * @param string $branch
  828. * @param bool $involved $this->cookie->involved or $involved
  829. * @param string $orderBy
  830. * @param object $pager
  831. * @access public
  832. * @return int[]
  833. */
  834. public function getProjectStatsByProduct($productID, $browseType = 'all', $branch = '0', $involved = false, $orderBy = 'order_desc', $pager = null)
  835. {
  836. $projects = $this->getProjectListByProduct($productID, $browseType, $branch, $involved, $orderBy, $pager);
  837. if(empty($projects)) return array();
  838. return $this->program->appendStatToProjects($projects);
  839. }
  840. /**
  841. * 获取关联了某产品的执行列表。
  842. * Get executions by product and project.
  843. *
  844. * @param int $productID
  845. * @param string $branch
  846. * @param int $projectID
  847. * @param string $mode stagefilter|noclosed|multiple
  848. * @param array $unAllowedStage
  849. * @access public
  850. * @return array
  851. */
  852. public function getExecutionPairsByProduct($productID, $branch = '', $projectID = 0, $mode = '', $unAllowedStage = array())
  853. {
  854. if(empty($productID)) return array();
  855. /* Determine executions order. */
  856. $projects = $this->loadModel('project')->getByIdList(array($projectID));
  857. $hasWaterfall = in_array('waterfall', helper::arrayColumn($projects, 'model'));
  858. $hasWaterfallplus = in_array('waterfallplus', helper::arrayColumn($projects, 'model'));
  859. $waterfallOrderBy = ($hasWaterfall || $hasWaterfallplus) ? 't2.begin_asc,t2.id_asc' : 't2.begin_desc,t2.id_desc';
  860. $executions = $this->dao->select('t2.id,t2.name,t2.project,t2.grade,t2.path,t2.parent,t2.attribute,t2.multiple,t3.name as projectName')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  861. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project = t2.id')
  862. ->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project = t3.id')
  863. ->where('t1.product')->eq($productID)
  864. ->andWhere('t2.type')->in('sprint,kanban,stage')
  865. ->beginIF($projectID)->andWhere('t2.project')->in($projectID)->fi()
  866. ->beginIF($branch !== '' and strpos($branch, 'all') === false)->andWhere('t1.branch')->in($branch)->fi()
  867. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi()
  868. ->beginIF(strpos($mode, 'noclosed') !== false)->andWhere('t2.status')->ne('closed')->fi()
  869. ->beginIF(strpos($mode, 'multiple') !== false)->andWhere('t2.multiple')->eq('1')->fi()
  870. ->beginIF(!empty($unAllowedStage))->andWhere('t2.attribute')->notIn($unAllowedStage)->fi() //针对瀑布项目,过滤掉不允许的阶段
  871. ->andWhere('t2.deleted')->eq('0')
  872. ->orderBy($waterfallOrderBy)
  873. ->fetchAll('id');
  874. if(empty($executions)) return array();
  875. if($projectID) $executions = $this->loadModel('execution')->resetExecutionSorts($executions);
  876. return $this->productTao->buildExecutionPairs($executions, $mode, empty($projectID));
  877. }
  878. /**
  879. * 获取产品路线图数据。
  880. * Get roadmap of product.
  881. *
  882. * @param int $productID
  883. * @param string $branch all|0|1
  884. * @param int $count
  885. * @access public
  886. * @return array
  887. */
  888. public function getRoadmap($productID, $branch = '0', $count = 0)
  889. {
  890. /* Get group roadmap data. */
  891. list($groupRoadmap, $return) = $this->getGroupRoadmapData($productID, $branch, $count);
  892. if($return) return $groupRoadmap;
  893. /* Get last 5 roadmap. */
  894. $lastKeys = array_slice(array_keys($groupRoadmap), 0, 5);
  895. $lastRoadmap = array();
  896. $lastRoadmap['total'] = 0;
  897. foreach($lastKeys as $key)
  898. {
  899. if($key == '2030')
  900. {
  901. $lastRoadmap[$this->lang->productplan->future] = $groupRoadmap[$key];
  902. }
  903. else
  904. {
  905. $lastRoadmap[$key] = $groupRoadmap[$key];
  906. }
  907. foreach($groupRoadmap[$key] as $branchRoadmap) $lastRoadmap['total'] += (count($branchRoadmap, 1) - count($branchRoadmap));
  908. }
  909. return $lastRoadmap;
  910. }
  911. /**
  912. * Process roadmap.
  913. *
  914. * @param array $roadmap
  915. * @param string $branch
  916. * @access public
  917. * @return array
  918. * @param mixed[] $roadmapGroups
  919. */
  920. public function processRoadmap($roadmapGroups, $branch)
  921. {
  922. $newRoadmap = array();
  923. foreach($roadmapGroups as $branchRoadmap)
  924. {
  925. foreach($branchRoadmap as $branchID => $roadmap)
  926. {
  927. if($branch != $branchID) continue;
  928. foreach($roadmap as $roadmapItem) $newRoadmap[] = $roadmapItem;
  929. }
  930. }
  931. krsort($newRoadmap);
  932. return $newRoadmap;
  933. }
  934. /**
  935. * 使用产品ID获取产品统计信息。
  936. * Get product stat data by product ID.
  937. *
  938. * @param int $productID
  939. * @param string $storyType
  940. * @access public
  941. * @return object|bool
  942. */
  943. public function getStatByID($productID, $storyType = 'story')
  944. {
  945. /* Check privilege. */
  946. if(!$this->checkPriv($productID))
  947. {
  948. $this->accessDenied($this->lang->product->accessDenied);
  949. return false;
  950. }
  951. /* Get product. */
  952. $product = $this->getByID($productID);
  953. if(empty($product)) return false;
  954. /* Attach statistic data. */
  955. $product->stories = $this->getStoryStatusCountByID($productID, $storyType);
  956. $product->plans = $this->productTao->getStatCountByID(TABLE_PRODUCTPLAN, $productID);
  957. $product->builds = $this->productTao->getStatCountByID(TABLE_BUILD, $productID);
  958. $product->cases = $this->productTao->getStatCountByID(TABLE_CASE, $productID);
  959. $product->bugs = $this->productTao->getStatCountByID(TABLE_BUG, $productID);
  960. $product->docs = $this->productTao->getStatCountByID(TABLE_DOC, $productID);
  961. $product->releases = $this->productTao->getStatCountByID(TABLE_RELEASE, $productID);
  962. $product->projects = $this->productTao->getStatCountByID(TABLE_PROJECTPRODUCT, $productID);
  963. $product->executions = $this->productTao->getStatCountByID('executions', $productID);
  964. $product->progress = $this->productTao->getStatCountByID('progress', $productID);
  965. $product->storyDeliveryRate = 0;
  966. $storyDeliveryRate = $this->loadModel('metric')->getResultByCode('rate_of_delivery_story_in_product', array('product' => $productID));
  967. if($storyDeliveryRate)
  968. {
  969. $storyDeliveryRate = current($storyDeliveryRate);
  970. $product->storyDeliveryRate = $storyDeliveryRate['value'] * 100;
  971. }
  972. return $product;
  973. }
  974. /**
  975. * 获取产品统计信息。
  976. * Get product stats.
  977. *
  978. * @param array $productIdList
  979. * @param string $orderBy order_asc|program_asc
  980. * @param object|null $pager
  981. * @param string $storyType requirement|story
  982. * @param int $programID
  983. * @access public
  984. * @return array
  985. */
  986. public function getStats($productIdList, $orderBy = 'order_asc', $pager = null, $storyType = 'story', $programID = 0)
  987. {
  988. /* Call the getProductStats method of the tutorial module if you are in tutorial mode.*/
  989. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProductStats();
  990. if(empty($productIdList)) return array();
  991. $stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager);
  992. $latestReleases = $this->productTao->getStatisticByType($productIdList, 'latestReleases');
  993. $projectCountPairs = $this->productTao->getProjectCountPairs($productIdList);
  994. $executionCountPairs = $this->productTao->getExecutionCountPairs($productIdList);
  995. $coveragePairs = $this->getCaseCoveragePairs($productIdList);
  996. foreach($stats as $product)
  997. {
  998. $product->projects = zget($projectCountPairs, $product->id, 0);
  999. $product->executions = zget($executionCountPairs, $product->id, 0);
  1000. $product->coverage = zget($coveragePairs, $product->id, 0);
  1001. $latestRelease = isset($latestReleases[$product->id]) ? $latestReleases[$product->id][0] : null;
  1002. $product->latestRelease = $latestRelease ? $latestRelease->name : '';
  1003. $product->latestReleaseDate = $latestRelease ? $latestRelease->date : '';
  1004. }
  1005. return $stats;
  1006. }
  1007. /**
  1008. * 获取看板页面的产品统计信息。
  1009. * Get stats for product kanban.
  1010. *
  1011. * @param string $browseType
  1012. * @access public
  1013. * @return array
  1014. */
  1015. public function getStats4Kanban($browseType = 'my')
  1016. {
  1017. /* Get base data. */
  1018. $productList = $this->getList();
  1019. foreach($productList as $id => $product)
  1020. {
  1021. if($browseType == 'my')
  1022. {
  1023. if($product->PO != $this->app->user->account) unset($productList[$id]);
  1024. }
  1025. else
  1026. {
  1027. if($product->PO == $this->app->user->account) unset($productList[$id]);
  1028. }
  1029. }
  1030. $projectList = $this->loadModel('program')->getProjectStats(0, 'doing');
  1031. $productIdList = array_keys($productList);
  1032. $projectProduct = $this->productTao->getProjectProductList($productList);
  1033. $planList = $this->productTao->getPlanList($productIdList);
  1034. $executionList = $this->productTao->getExecutionList(array_keys($projectList), $productIdList);
  1035. $releaseList = $this->productTao->getReleaseList($productIdList);
  1036. /* Filter latest executions. */
  1037. $projectLatestExecutions = array();
  1038. $latestExecutionList = array();
  1039. $today = helper::today();
  1040. foreach($executionList as $projectID => $executions)
  1041. {
  1042. foreach($executions as &$execution)
  1043. {
  1044. /* Calculate delayed execution. */
  1045. if($execution->status != 'done' && $execution->status != 'closed' && $execution->status != 'suspended')
  1046. {
  1047. $delay = helper::diffDate($today, $execution->end);
  1048. if($delay > 0) $execution->delay = $delay;
  1049. }
  1050. }
  1051. /* Used to computer execution progress. */
  1052. $latestExecutionList[key($executions)] = current($executions);
  1053. /* Used for display in page. */
  1054. $projectLatestExecutions[$projectID] = current($executions);
  1055. }
  1056. /* Build result. */
  1057. $statsData = array($productList, $planList, $projectList, $projectProduct, $projectLatestExecutions, $releaseList);
  1058. /* Convert predefined HTML entities to characters. */
  1059. $statsData = $this->convertHtmlSpecialChars($statsData);
  1060. return $statsData;
  1061. }
  1062. /**
  1063. * 获取项目集下的产品线。
  1064. * Get product line pairs by program.
  1065. *
  1066. * @param int|array $programID
  1067. * @param bool $filterRoot
  1068. * @access public
  1069. * @return string[]
  1070. * @param int|mixed[] $programIdList
  1071. */
  1072. public function getLinePairs($programIdList = 0, $filterRoot = false)
  1073. {
  1074. return $this->dao->select('id,name')->from(TABLE_MODULE)
  1075. ->where('type')->eq('line')
  1076. ->beginIF(helper::hasFeature('program') && ($programIdList || $filterRoot))->andWhere('root')->in($programIdList)->fi()
  1077. ->andWhere('deleted')->eq(0)
  1078. ->orderBy('order_asc')
  1079. ->fetchPairs('id', 'name');
  1080. }
  1081. /*
  1082. * 根据项目集编号获取产品线列表。
  1083. * Get product lines by program id list.
  1084. *
  1085. * @param array $programIdList
  1086. * @access public
  1087. * @return object[]
  1088. */
  1089. /**
  1090. * @param mixed[] $programIdList
  1091. */
  1092. public function getLines($programIdList = array())
  1093. {
  1094. return $this->dao->select('*')->from(TABLE_MODULE)
  1095. ->where('type')->eq('line')
  1096. ->beginIF($programIdList)->andWhere('root')->in($programIdList)->fi()
  1097. ->andWhere('deleted')->eq(0)
  1098. ->orderBy('`order`')
  1099. ->fetchAll();
  1100. }
  1101. /**
  1102. * 获取产品需求的摘要信息。
  1103. * Get the summary of product's stories.
  1104. *
  1105. * @param array $stories
  1106. * @param string $storyType story|requirement
  1107. * @access public
  1108. * @return string
  1109. */
  1110. public function summary($stories, $storyType = 'story')
  1111. {
  1112. $totalEstimate = 0.0;
  1113. $storyIdList = array();
  1114. $rateCount = 0;
  1115. $total = 0;
  1116. $SRTotal = 0;
  1117. $URTotal = 0;
  1118. $ERTotal = 0;
  1119. $idList = array();
  1120. foreach($stories as $story) $idList[] = $story->id;
  1121. $childTypes = $this->dao->select('parent, type')->from(TABLE_STORY)->where('parent')->in($idList)->andWhere('deleted')->eq('0')->fetchGroup('parent', 'type');
  1122. foreach($stories as $story)
  1123. {
  1124. $total ++;
  1125. if($story->type == 'story') $SRTotal += 1;
  1126. if($story->type == 'requirement') $URTotal += 1;
  1127. if($story->type == 'epic') $ERTotal += 1;
  1128. /* 产品软需列表仅统计叶子需求的规模。
  1129. 如果是产品用需、业需列表,则追加统计子需求类型和父需求不一样的父需求规模。
  1130. 如果是项目需求列表,则只统计叶子需求的规模。
  1131. */
  1132. $isLeafStory = !isset($childTypes[$story->id]) || (isset($childTypes[$story->id]) && !isset($childTypes[$story->id][$story->type]));
  1133. if(($storyType == $story->type && $isLeafStory) || ($storyType == 'all' && $story->isParent == '0'))
  1134. {
  1135. $totalEstimate += $story->estimate;
  1136. $story->needSummaryEstimate = true;
  1137. }
  1138. if($story->type != 'story') continue;
  1139. if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed'))))
  1140. {
  1141. $storyIdList[] = $story->id;
  1142. $rateCount ++;
  1143. }
  1144. }
  1145. $casesCount = count($this->productTao->filterNoCasesStory($storyIdList));
  1146. $rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 4);
  1147. if($storyType == 'all') return sprintf($this->lang->product->allSummary, $total, $totalEstimate, $rate * 100 . "%");
  1148. if($storyType == 'epic') return sprintf($this->lang->product->epicSummary, $ERTotal, $totalEstimate);
  1149. if($storyType == 'requirement') return sprintf($this->lang->product->requirementSummary, $URTotal, $totalEstimate);
  1150. return sprintf($this->lang->product->storySummary, $SRTotal, $totalEstimate, $rate * 100 . "%");
  1151. }
  1152. /**
  1153. * 判断某操作是否可点击。
  1154. * Judge an action is clickable.
  1155. *
  1156. * @param object $product
  1157. * @param string $action
  1158. * @access public
  1159. * @return bool
  1160. */
  1161. public static function isClickable($product, $action)
  1162. {
  1163. $action = strtolower($action);
  1164. if($action == 'close') return $product->status != 'closed';
  1165. if($action == 'activate') return $product->status == 'closed';
  1166. return true;
  1167. }
  1168. /**
  1169. * Setting parameters for link.
  1170. *
  1171. * @param string $module
  1172. * @param string $link
  1173. * @param int $projectID
  1174. * @param int $productID
  1175. * @access public
  1176. * @return void
  1177. */
  1178. public function setParamsForLink($module, $link, $projectID, $productID)
  1179. {
  1180. return $module == 'programplan' ? sprintf($link, $projectID, $productID) : sprintf($link, $productID);
  1181. }
  1182. /**
  1183. * 将预定义的HTML实体转换为字符。
  1184. * Convert predefined HTML entities to characters.
  1185. *
  1186. * @param array $statsData
  1187. * @return array
  1188. */
  1189. public function convertHtmlSpecialChars($statsData)
  1190. {
  1191. if(empty($statsData)) return array();
  1192. foreach($statsData as $key => $data)
  1193. {
  1194. if(empty($data)) continue;
  1195. if($key === 'productList' || $key === 'projectList') array_map(function($item){return $item->name = htmlspecialchars_decode($item->name, ENT_QUOTES);}, $data);
  1196. if($key === 'planList')
  1197. {
  1198. foreach($data as $plan)
  1199. {
  1200. if(empty($plan)) continue;
  1201. array_map(function($planItem){return $planItem->title = htmlspecialchars_decode($planItem->title, ENT_QUOTES);}, $plan);
  1202. }
  1203. }
  1204. }
  1205. return $statsData;
  1206. }
  1207. /**
  1208. * 把访问的产品ID等状态信息保存到session和cookie中。
  1209. * Save the product id user last visited to session and cookie.
  1210. *
  1211. * @param int $productID
  1212. * @param array $products
  1213. * @access public
  1214. * @return int
  1215. */
  1216. public function checkAccess($productID, $products)
  1217. {
  1218. if(commonModel::isTutorialMode()) return $productID;
  1219. $productID = $this->getAccessibleProductID($productID, $products);
  1220. $this->session->set('product', $productID, $this->app->tab);
  1221. /* If preProductID changed, then reset preBranch. */
  1222. if($this->cookie->preProductID != $this->session->product)
  1223. {
  1224. $this->cookie->set('preBranch', 0);
  1225. helper::setcookie('preBranch', '0');
  1226. }
  1227. helper::setcookie('preProductID', (string)$productID);
  1228. return $productID;
  1229. }
  1230. /**
  1231. * 获取可访问的产品ID。
  1232. * Get accessible product ID.
  1233. *
  1234. * @param int $productID
  1235. * @param array $products
  1236. * @access protected
  1237. * @return int
  1238. */
  1239. protected function getAccessibleProductID($productID, $products)
  1240. {
  1241. if(empty($productID))
  1242. {
  1243. if($this->cookie->preProductID and isset($products[$this->cookie->preProductID])) $productID = (int)$this->cookie->preProductID;
  1244. if(empty($productID) && empty($this->session->product)) $productID = (int)key($products);
  1245. }
  1246. /* 产品ID已经被删除,不存在于产品列表中。*/
  1247. /* Product ID does not exist in products list, it may be deleted. */
  1248. /* Confirm if product exist. */
  1249. $product = $this->getByID($productID);
  1250. if(empty($product) or $product->deleted == 1) $productID = (int)key($products);
  1251. /* If product is invisible for current user, respond access denied message. */
  1252. if($productID && !$this->checkPriv($productID))
  1253. {
  1254. $productID = (int)key($products);
  1255. $this->accessDenied($this->lang->product->accessDenied);
  1256. }
  1257. return (int)$productID;
  1258. }
  1259. /**
  1260. * 输出访问被拒绝提示信息。
  1261. * Show accessDenied response.
  1262. *
  1263. * @param string $tips
  1264. * @access public
  1265. * @return void
  1266. */
  1267. public function accessDenied($tips)
  1268. {
  1269. if(commonModel::isTutorialMode()) return true;
  1270. return $this->app->control->sendError($tips, helper::createLink('product', 'all'));
  1271. }
  1272. /**
  1273. * 设置导航菜单。
  1274. * Set menu.
  1275. *
  1276. * @param int $productID
  1277. * @param string|int $branch all|''|int
  1278. * @param string $extra requirement|story
  1279. * @access public
  1280. * @return bool
  1281. */
  1282. public function setMenu($productID = 0, $branch = '', $extra = '')
  1283. {
  1284. if(!commonModel::isTutorialMode() and $productID != 0 and !$this->checkPriv($productID))
  1285. {
  1286. $this->accessDenied($this->lang->product->accessDenied);
  1287. return true;
  1288. }
  1289. /* 用真实数据替换导航配置的占位符,并删除无用配置项。 */
  1290. $params = array('branch' => $branch);
  1291. common::setMenuVars('product', $productID, $params);
  1292. if(strpos($extra, 'requirement') !== false) unset($this->lang->product->moreSelects['willclose']);
  1293. $product = $this->getByID($productID);
  1294. if(!$product) return false;
  1295. /* 设置1.5级导航数据。*/
  1296. $this->lang->switcherMenu = $this->getSwitcher($productID, $extra, $branch);
  1297. /* 设置导航中分支的显示数据。*/
  1298. /* 如果产品类型是正常的,隐藏导航中分支的显示。*/
  1299. if($product->type == 'normal')
  1300. {
  1301. unset($this->lang->product->menu->settings['subMenu']->branch);
  1302. return true;
  1303. }
  1304. /* 如果产品类型是多分支、多平台的,将真实数据替换@branch@匹配符。*/
  1305. $branchLink = $this->lang->product->menu->settings['subMenu']->branch['link'];
  1306. $this->lang->product->menu->settings['subMenu']->branch['link'] = str_replace('@branch@', $this->lang->product->branchName[$product->type], $branchLink);
  1307. $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  1308. return true;
  1309. }
  1310. /**
  1311. * 删除产品。
  1312. * Delete product by ID.
  1313. *
  1314. * @param int $productID
  1315. * @access protected
  1316. * @return bool
  1317. */
  1318. protected function deleteByID($productID)
  1319. {
  1320. $this->delete(TABLE_PRODUCT, $productID);
  1321. $this->dao->update(TABLE_DOCLIB)->set('deleted')->eq(1)->where('product')->eq($productID)->exec();
  1322. $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('product')->eq($productID)->exec();
  1323. return !dao::isError();
  1324. }
  1325. /**
  1326. * 格式化数据表格输出数据。
  1327. * Format data for list.
  1328. *
  1329. * @param object $product
  1330. * @param array $users
  1331. * @access public
  1332. * @return object
  1333. */
  1334. public function formatDataForList($product, $users)
  1335. {
  1336. $product->type = 'product';
  1337. $product->productLine = $product->lineName;
  1338. $product->PO = !empty($product->PO) ? zget($users, $product->PO) : '';
  1339. $product->PMT = !empty($product->PMT) ? zget($users, $product->PMT) : '';
  1340. $product->createdBy = !empty($product->createdBy) ? zget($users, $product->createdBy) : '';
  1341. $product->createdDate = substr($product->createdDate, 0, 10);
  1342. if($this->config->vision == 'or') return $product;
  1343. $product->testCaseCoverage = $product->coverage;
  1344. $product->epicCompleteRate = $product->totalEpics == 0 ? 0 : round($product->finishedEpics / $product->totalEpics, 3) * 100;
  1345. $product->requirementCompleteRate = $product->totalRequirements == 0 ? 0 : round($product->finishedRequirements / $product->totalRequirements, 3) * 100;
  1346. $product->storyCompleteRate = $product->totalStories == 0 ? 0 : round($product->finishedStories / $product->totalStories, 3) * 100;
  1347. $product->bugFixedRate = ($product->unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100;
  1348. return $product;
  1349. }
  1350. /**
  1351. * 构建路线图页面数据。
  1352. * Build roadmap for UI.
  1353. *
  1354. * @param array $roadmaps
  1355. * @param int $branchKey
  1356. * @access public
  1357. * @return array
  1358. */
  1359. public function buildRoadmapForUI($roadmaps, $branchKey = 0)
  1360. {
  1361. unset($roadmaps['total']);
  1362. $data = array();
  1363. foreach($roadmaps as $year => $yearRoadmaps)
  1364. {
  1365. if(!isset($yearRoadmaps[$branchKey])) continue;
  1366. foreach($yearRoadmaps[$branchKey] as $roadmapData)
  1367. {
  1368. $yearNodes = array();
  1369. foreach($roadmapData as $roadmap)
  1370. {
  1371. $isPlan = (isset($roadmap->begin) && isset($roadmap->end));
  1372. $moduleName = $isPlan ? 'productplan' : 'release';
  1373. $node = array();
  1374. $node['href'] = common::hasPriv($moduleName, 'view') ? helper::createLink($moduleName, 'view', "id={$roadmap->id}") : '###';
  1375. $node['version'] = $isPlan ? $roadmap->title : $roadmap->name;
  1376. $node['date'] = $isPlan ? $roadmap->begin . '~' . $roadmap->end : (empty($roadmap->releasedDate) ? $roadmap->date : $roadmap->releasedDate);
  1377. $node['marker'] = !empty($roadmap->marker);
  1378. $yearNodes[] = $node;
  1379. }
  1380. $count = count($yearNodes);
  1381. $cols = 5;
  1382. $lines = array();
  1383. for($i = 0; $i * $cols < $count; $i ++) $lines[] = array_slice($yearNodes, $i * $cols, $cols);
  1384. $data[$year] = array_reverse($lines);
  1385. }
  1386. }
  1387. return $data;
  1388. }
  1389. /**
  1390. * 删除一个产品线。
  1391. * Delete a product line.
  1392. *
  1393. * @param int $lineID
  1394. * @access public
  1395. * @return bool
  1396. */
  1397. public function deleteLine($lineID)
  1398. {
  1399. $this->dao->update(TABLE_MODULE)->set('deleted')->eq(1)->where('id')->eq($lineID)->exec();
  1400. $this->loadModel('action')->create('productline', $lineID, 'deleted', '', ACTIONMODEL::CAN_UNDELETED);
  1401. $this->dao->update(TABLE_PRODUCT)->set('line')->eq('0')->where('line')->eq($lineID)->exec();
  1402. return !dao::isError();
  1403. }
  1404. /**
  1405. * 获取产品列表。
  1406. * Get products list.
  1407. *
  1408. * @param int $programID
  1409. * @param string $status all|noclosed|involved|review|normal|closed
  1410. * @param int $limit
  1411. * @param int $line
  1412. * @param string|int $shadow all | 0 | 1
  1413. * @param string $fields * or fieldList, such as id,name,program
  1414. * @access protected
  1415. * @return array
  1416. */
  1417. protected function getList($programID = 0, $status = 'all', $limit = 0, $line = 0, $shadow = 0, $fields = '*')
  1418. {
  1419. $fields = explode(',', $fields);
  1420. $fields = trim(implode(',t1.', $fields), ',');
  1421. $programField = $programID ? '' : 't2.order,';
  1422. $programOrder = $programID ? '' : 't2.order_asc,';
  1423. return $this->dao->select("DISTINCT t1.{$fields},{$programField}t1.line,t1.order")->from(TABLE_PRODUCT)->alias('t1')
  1424. ->beginIF(!$programID)->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id')->fi() // 如果指定了项目集,则不需要关联项目集表。
  1425. ->beginIF($status == 'involved')->leftJoin(TABLE_PROJECTPRODUCT)->alias('t3')->on('t3.product = t1.id')->fi()
  1426. ->beginIF($status == 'involved')->leftJoin(TABLE_TEAM)->alias('t4')->on("t4.root = t3.project and t4.type='project'")->fi()
  1427. ->where('t1.deleted')->eq(0)
  1428. ->beginIF($shadow !== 'all')->andWhere('t1.shadow')->eq((int)$shadow)->fi()
  1429. ->beginIF($programID)->andWhere('t1.program')->eq($programID)->fi()
  1430. ->beginIF($line > 0)->andWhere('t1.line')->eq($line)->fi()
  1431. ->beginIF(strpos($status, 'feedback') === false && !$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi()
  1432. ->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)")
  1433. ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi()
  1434. ->beginIF(strpos($status, 'nowait') !== false)->andWhere('t1.status')->ne('wait')->fi()
  1435. ->beginIF(strpos($status, '|') === false && !in_array($status, array('all', 'noclosed', 'involved', 'review'), true))->andWhere('t1.status')->in($status)->fi()
  1436. ->beginIF($status == 'involved')
  1437. ->andWhere('t1.PO', true)->eq($this->app->user->account)
  1438. ->orWhere('t1.QD')->eq($this->app->user->account)
  1439. ->orWhere('t1.RD')->eq($this->app->user->account)
  1440. ->orWhere('t1.createdBy')->eq($this->app->user->account)
  1441. ->orWhere('t4.account')->eq($this->app->user->account)
  1442. ->markRight(1)
  1443. ->fi()
  1444. ->beginIF($status == 'review')
  1445. ->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
  1446. ->andWhere('t1.reviewStatus')->eq('doing')
  1447. ->fi()
  1448. ->filterTpl('skip')
  1449. ->orderBy("{$programOrder}t1.line_desc, t1.order_asc")
  1450. ->beginIF($limit > 0)->limit($limit)->fi()
  1451. ->fetchAll('id');
  1452. }
  1453. /**
  1454. * 获取用于统计的产品列表。
  1455. * Get products list for statistic.
  1456. *
  1457. * @param int[] $productIdList
  1458. * @param bool $appendProgram
  1459. * @param string $orderBy
  1460. * @param object|null $pager
  1461. * @access public
  1462. * @return array
  1463. */
  1464. public function getStatsProducts($productIdList, $appendProgram, $orderBy, $pager = null)
  1465. {
  1466. if($orderBy == 'program_asc')
  1467. {
  1468. $products = $this->productTao->getPagerProductsWithProgramIn($productIdList, $pager);
  1469. }
  1470. else
  1471. {
  1472. $products = $this->productTao->getPagerProductsIn($productIdList, $pager, $orderBy);
  1473. }
  1474. /* Fetch product lines. */
  1475. $linePairs = $this->getLinePairs();
  1476. foreach($products as $product) $product->lineName = zget($linePairs, $product->line, '');
  1477. if(!$appendProgram) return $products;
  1478. $programIdList = array();
  1479. foreach($products as $product) $programIdList[] = $product->program;
  1480. $programs = $this->loadModel('program')->getBaseDataList(array_unique($programIdList));
  1481. $finishClosedStory = $this->dao->select('product, count(1) as finish')->from(TABLE_STORY)
  1482. ->where('deleted')->eq(0)
  1483. ->andWhere('status')->eq('closed')
  1484. ->andWhere('closedReason')->eq('done')
  1485. ->groupBy('product')
  1486. ->fetchPairs('product', 'finish');
  1487. $launchedStory = $this->dao->select('product, count(1) as launched')->from(TABLE_STORY)
  1488. ->where('deleted')->eq(0)
  1489. ->andWhere('status')->eq('launched')
  1490. ->groupBy('product')
  1491. ->fetchPairs('product', 'launched');
  1492. $developingStory = $this->dao->select('product, count(1) as developing')->from(TABLE_STORY)
  1493. ->where('deleted')->eq(0)
  1494. ->andWhere('status')->eq('developing')
  1495. ->groupBy('product')
  1496. ->fetchPairs('product', 'developing');
  1497. foreach($products as $productID => $product)
  1498. {
  1499. $product->programName = isset($programs[$product->program]) ? $programs[$product->program]->name : '';
  1500. $product->programPM = isset($programs[$product->program]) ? $programs[$product->program]->PM : '';
  1501. $products[$productID]->finishClosedStories = isset($finishClosedStory[$productID]) ? $finishClosedStory[$productID] : 0;
  1502. $products[$productID]->launchedStories = isset($launchedStory[$productID]) ? $launchedStory[$productID] : 0;
  1503. $products[$productID]->developingStories = isset($developingStory[$productID]) ? $developingStory[$productID] : 0;
  1504. }
  1505. return $products;
  1506. }
  1507. /**
  1508. * 获取用于数据统计的研发需求和用户需求列表。
  1509. * Get dev stories and user requirements for statistics.
  1510. *
  1511. * @param array $productIdList
  1512. * @param string $storyType
  1513. * @access public
  1514. * @return array[]
  1515. */
  1516. public function getStatsStoriesAndRequirements($productIdList, $storyType)
  1517. {
  1518. $this->loadModel('story');
  1519. $stories = $this->story->getStoriesCountByProductIDs($productIdList, 'story');
  1520. $requirements = $this->story->getStoriesCountByProductIDs($productIdList);
  1521. /* Padding the stories to sure all products have records. */
  1522. $defaultStory = array_keys($this->lang->story->statusList);
  1523. foreach($productIdList as $productID)
  1524. {
  1525. if(!isset($stories[$productID])) $stories[$productID] = $defaultStory;
  1526. if(!isset($requirements[$productID])) $requirements[$productID] = $defaultStory;
  1527. }
  1528. /* Collect count for each status of stories. */
  1529. foreach($stories as $key => $story)
  1530. {
  1531. foreach(array_keys($this->lang->story->statusList) as $status) $story[$status] = isset($story[$status]) ? $story[$status]->count : 0;
  1532. $stories[$key] = $story;
  1533. }
  1534. /* Collect count for each status of requirements. */
  1535. foreach($requirements as $key => $requirement)
  1536. {
  1537. foreach(array_keys($this->lang->story->statusList) as $status) $requirement[$status] = isset($requirement[$status]) ? $requirement[$status]->count : 0;
  1538. $requirements[$key] = $requirement;
  1539. }
  1540. /* Story type is 'requirement'. */
  1541. if($storyType == 'requirement') $stories = $requirements;
  1542. return array($stories, $requirements);
  1543. }
  1544. /**
  1545. * 获取多个产品关联需求用例覆盖率的键值对。
  1546. * Get K-V pairs of product ID and test case coverage.
  1547. *
  1548. * @param int[] $productIdList
  1549. * @access public
  1550. * @return array
  1551. */
  1552. public function getCaseCoveragePairs($productIdList)
  1553. {
  1554. if(empty($productIdList)) return array();
  1555. /* Get storie list by product ID list. */
  1556. $storyList = $this->loadModel('story')->getStoriesByProductIdList($productIdList, 'story', false);
  1557. /* Get case count of each story. */
  1558. $storyIdList = array();
  1559. $productStoryList = array();
  1560. foreach($storyList as $story)
  1561. {
  1562. $storyIdList[] = $story->id;
  1563. if(!isset($productStoryList[$story->product])) $productStoryList[$story->product] = array();
  1564. $productStoryList[$story->product][] = $story->id;
  1565. }
  1566. $caseCountPairs = $this->productTao->getCaseCountByProductIdList($productIdList);
  1567. /* Calculate coverage. */
  1568. $coveragePairs = array();
  1569. foreach($productStoryList as $productID => $list)
  1570. {
  1571. $total = count($list);
  1572. $totalCovered = 0;
  1573. foreach($list as $storyID)
  1574. {
  1575. if(isset($caseCountPairs[$storyID])) $totalCovered++;
  1576. }
  1577. $coveragePairs[$productID] = $total ? round($totalCovered * 100 / $total) : 0;
  1578. }
  1579. return $coveragePairs;
  1580. }
  1581. /**
  1582. * 从产品统计数据中统计项目集。
  1583. * Statistics program data from statistics data of product.
  1584. *
  1585. * @param array $productStats
  1586. * @access public
  1587. * @return array
  1588. */
  1589. public function statisticProgram($productStats)
  1590. {
  1591. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProductStats(true);
  1592. $programStructure = array();
  1593. foreach($productStats as $product)
  1594. {
  1595. $programStructure[$product->program][$product->line]['products'][$product->id] = $product;
  1596. /* Generate line data. */
  1597. if($product->line && $this->config->vision != 'or')
  1598. {
  1599. $programStructure[$product->program][$product->line]['lineName'] = $product->lineName;
  1600. $programStructure[$product->program][$product->line] = $this->productTao->statisticProductData('line', $programStructure, $product);
  1601. }
  1602. /* Generate program data. */
  1603. if($product->program && $this->config->vision != 'or')
  1604. {
  1605. $programStructure[$product->program]['programName'] = $product->programName;
  1606. $programStructure[$product->program]['programPM'] = $product->programPM;
  1607. $programStructure[$product->program]['id'] = $product->program;
  1608. $programStructure[$product->program] = $this->productTao->statisticProductData('program', $programStructure, $product);
  1609. }
  1610. }
  1611. return $programStructure;
  1612. }
  1613. /**
  1614. * 通过产品ID查询产品关联需求的各状态统计总数。每一个需求状态对应一个数量。
  1615. * Get stories total count of each status by product ID. Every story status has a count value.
  1616. *
  1617. * @param int $productID
  1618. * @param string $storyType
  1619. * @access public
  1620. * @return object[]
  1621. */
  1622. public function getStoryStatusCountByID($productID, $storyType = 'story')
  1623. {
  1624. /* 通过产品ID获取每一个需求状态对应的数量。Get count of each story status of the product. */
  1625. $statusCountList = $this->loadModel('story')->getStoriesCountByProductID($productID, $storyType);
  1626. foreach($statusCountList as $status => $stat) $statusCountList[$status] = $stat->count;
  1627. /* 确保每一种需求状态都是有值的。Padding the stories to make sure all status have records. */
  1628. foreach(array_keys($this->lang->story->statusList) as $status)
  1629. {
  1630. if(!isset($statusCountList[$status])) $statusCountList[$status] = 0;
  1631. }
  1632. return $statusCountList;
  1633. }
  1634. /**
  1635. * 获取产品路线图的分组数据。
  1636. * Get group roadmap data of product.
  1637. *
  1638. * @param int $productID
  1639. * @param string $branch all|0|1
  1640. * @param int $count
  1641. * @access public
  1642. * @return [array, bool]
  1643. */
  1644. public function getGroupRoadmapData($productID, $branch, $count)
  1645. {
  1646. $roadmap = array();
  1647. $return = false;
  1648. /* Get product plans. */
  1649. $planList = $this->loadModel('productplan')->getList($productID, $branch);
  1650. /* Filter the valid plans, then get the ordered and parents plans. */
  1651. list($orderedPlans, $parentPlans) = $this->productTao->filterOrderedAndParentPlans($planList);
  1652. /* Get roadmaps of product plans. */
  1653. list($roadmap, $total, $return) = $this->getRoadmapOfPlans($orderedPlans, $parentPlans, $branch, $count);
  1654. if($return) return array($roadmap, $return);
  1655. /* Get roadmpas of product releases. */
  1656. $releases = $this->loadModel('release')->getList($productID, $branch);
  1657. list($roadmap, $subTotal, $return) = $this->getRoadmapOfReleases($roadmap, $releases, $branch, $count);
  1658. if($return) return array($roadmap, $return);
  1659. $total += $subTotal;
  1660. /* Re-group with branch ID. */
  1661. $groupRoadmap = array();
  1662. foreach($roadmap as $year => $branchRoadmap)
  1663. {
  1664. foreach($branchRoadmap as $branch => $roadmapItems) $groupRoadmap[$year][$branch][] = array_reverse($roadmapItems);
  1665. }
  1666. return array($groupRoadmap, $return);
  1667. }
  1668. /**
  1669. * 获取发布的路线图数据。
  1670. * Get roadmap of releases.
  1671. *
  1672. * @param array $roadmap
  1673. * @param array $parents
  1674. * @param string $branch
  1675. * @param int $count
  1676. * @access public
  1677. * @return [array, int, bool]
  1678. * @param mixed[] $releases
  1679. */
  1680. public function getRoadmapOfReleases($roadmap, $releases, $branch, $count)
  1681. {
  1682. $total = 0;
  1683. $return = false;
  1684. $today = helper::today();
  1685. $orderedReleases = array();
  1686. /* Collect releases. */
  1687. foreach($releases as $release)
  1688. {
  1689. if(helper::isZeroDate($release->releasedDate) || $release->releasedDate > $today || $release->status != 'normal') continue;
  1690. $orderedReleases[$release->releasedDate][] = $release;
  1691. }
  1692. krsort($orderedReleases);
  1693. foreach($orderedReleases as $releases)
  1694. {
  1695. krsort($releases);
  1696. foreach($releases as $release)
  1697. {
  1698. $year = substr($release->date, 0, 4);
  1699. $branchIdList = trim($release->branch, ',');
  1700. $branchIdList = empty($branchIdList) ? array(0) : explode(',', $branchIdList);
  1701. $branchIdList = array_unique($branchIdList);
  1702. foreach($branchIdList as $branchID)
  1703. {
  1704. if($branchID === '') continue;
  1705. $roadmap[$year][$branchID][] = $release;
  1706. }
  1707. $total++;
  1708. /* Exceed required count .*/
  1709. if($count > 0 and $total >= $count)
  1710. {
  1711. $roadmap = $this->processRoadmap($roadmap, $branch);
  1712. $return = true;
  1713. return array($roadmap, $total, $return);
  1714. }
  1715. }
  1716. }
  1717. if($count > 0)
  1718. {
  1719. $roadmap = $this->processRoadmap($roadmap, $branch);
  1720. $return = true;
  1721. }
  1722. return array($roadmap, $total, $return);
  1723. }
  1724. /**
  1725. * 获取计划的路线图数据。
  1726. * Get roadmap of plans.
  1727. *
  1728. * @param array $orderedPlans
  1729. * @param array $parents
  1730. * @param string $branch
  1731. * @param int $count
  1732. * @access public
  1733. * @return [array, int, bool]
  1734. */
  1735. public function getRoadmapOfPlans($orderedPlans, $parents, $branch, $count)
  1736. {
  1737. $return = false;
  1738. $total = 0;
  1739. $roadmap = array();
  1740. foreach($orderedPlans as $plans)
  1741. {
  1742. krsort($plans);
  1743. foreach($plans as $plan)
  1744. {
  1745. /* Attach parent plan. */
  1746. if($plan->parent > 0 and isset($parents[$plan->parent])) $plan->title = $parents[$plan->parent] . ' / ' . $plan->title;
  1747. $year = substr($plan->end, 0, 4);
  1748. $branchIdList = explode(',', trim($plan->branch, ','));
  1749. $branchIdList = array_unique($branchIdList);
  1750. foreach($branchIdList as $branchID)
  1751. {
  1752. if($branchID === '') continue;
  1753. $roadmap[$year][$branchID][] = $plan;
  1754. }
  1755. $total++;
  1756. /* Exceed requested count. */
  1757. if($count > 0 and $total >= $count)
  1758. {
  1759. $roadmap = $this->processRoadmap($roadmap, $branch);
  1760. $return = true;
  1761. break;
  1762. }
  1763. }
  1764. }
  1765. return array($roadmap, $total, $return);
  1766. }
  1767. /**
  1768. * 获取产品1.5级导航下拉项跳转链接。
  1769. * Get product link for 1.5 level navigation.
  1770. *
  1771. * @param string $module
  1772. * @param string $method
  1773. * @param string $extra
  1774. * @param bool $branch
  1775. * @access public
  1776. * @return string
  1777. */
  1778. public function getProductLink($module, $method, $extra = '', $branch = false)
  1779. {
  1780. $branchID = $branch ? "%s" : 'all';
  1781. $branchParam = $branch ? "&branch=%s" : '';
  1782. $extraParam = $extra ? "&extras=$extra" : '';
  1783. $params = explode(',', $extra);
  1784. $method = strtolower($method);
  1785. if($module == 'project' && $method == 'bug') return helper::createLink($module, $method, "projectID={$params[0]}&productID=%s{$branchParam}");
  1786. if($module == 'bug' && $method == 'view') return helper::createLink('bug', 'browse', "productID=%s&branch={$branchID}&extra=$extra");
  1787. if($module == 'bug' && $method == 'report') return helper::createLink('bug', 'browse', "productID=%s{$branchParam}");
  1788. if($module == 'qa' && $method == 'index') return helper::createLink('bug', 'browse', "productID=%s{$branchParam}");
  1789. if($module == 'story' && $method == 'report') return helper::createLink($module, $method, "productID=%s&branch={$branchID}&stroyType=$module");
  1790. if($module == 'testcase' && $method == 'browse') return helper::createLink($module, $method, "productID=%s&branch={$branchID}" . ($extra ? "&browseType=$extra" : ''));
  1791. if($module == 'testreport' && $method == 'create') return helper::createLink($module, $method, "objectID=&objectType=testtask&extra=%s");
  1792. if($module == 'testtask' && $method == 'browse') return helper::createLink($module, $method, "productID=%s&branch={$branchID}");
  1793. if($module == 'testsuite' && $method != 'create') return helper::createLink('testsuite', 'browse', "productID=%s");
  1794. if($module == 'product' && $method == 'project') return helper::createLink($module, $method, "status=all&productID=%s{$branchParam}");
  1795. if($module == 'product' && $method == 'dynamic') return helper::createLink($module, $method, "productID=%s&type=$extra");
  1796. if($module == 'project' && $method == 'testcase') return helper::createLink($module, $method, "projectID={$params[0]}&productID=%s&branch={$branchID}&browseType={$params[1]}");
  1797. if($module == 'testtask' && $method == 'browseunits') return helper::createLink($module, 'browseUnits', "productID=%s&browseType=newest&orderBy=id_desc&recTotal=0&recPerPage=0&pageID=1" . ($this->app->tab == 'project' ? "&projectID={$this->session->project}" : ''));
  1798. if($module == 'testtask' && $method == 'unitcases') return helper::createLink($module, 'browseUnits', "productID=%s&browseType=newest&orderBy=id_desc&recTotal=0&recPerPage=0&pageID=1" . ($this->app->tab == 'project' ? "&projectID={$this->session->project}" : ''));
  1799. if(in_array($module, array('story', 'epic', 'requirement')) && in_array($method, array('create', 'edit', 'batchcreate', 'view'))) return helper::createLink('product', 'browse', "productID=%s&branch={$branchID}&browseType=" . ($this->config->vision == 'or' ? 'assignedtome' : 'unclosed') . "&param=0&storyType=$module");
  1800. if($module == 'product' && $method == 'browse') return helper::createLink('product', 'browse', "productID=%s&branch={$branchID}&browseType=" . ($this->config->vision == 'or' ? 'assignedtome' : 'unclosed') . "&param=0&storyType=$extra");
  1801. if($module == 'story' && $method == 'storyview') return helper::createLink('product', 'browse', "productID=%s&branch={$branchID}");
  1802. if($this->config->edition != 'open')
  1803. {
  1804. $flow = $this->loadModel('workflow')->getByModule($module);
  1805. if($flow && in_array($flow->app, array('scrum', 'waterfall', 'kanbanProject'))) $flow->app = 'project';
  1806. if(!empty($flow) && $flow->buildin == '0') return helper::createLink('flow', 'ajaxSwitchBelong', "objectID=%s&moduleName=$module") . "#app=$flow->app";
  1807. }
  1808. if($module == 'execution' && in_array($method, array('bug', 'testcase'))) return helper::createLink($module, $method, "executionID={$params[0]}&productID=%s{$branchParam}");
  1809. if($module == 'product' && in_array($method, array('doc', 'view'))) return helper::createLink($module, $method, "productID=%s");
  1810. if($module == 'product' && in_array($method, array('create', 'showimport'))) return helper::createLink($module, 'browse', "productID=%s&type=$extra");
  1811. if($module == 'bug' && in_array($method, array('edit', 'view'))) return helper::createLink($module, 'browse', "productID=%s");
  1812. if($module == 'ticket' && in_array($method, array('browse', 'view', 'edit', 'createstory'))) return helper::createLink('ticket', 'browse', "browseType=byProduct&productID=%s");
  1813. if($module == 'testreport' && in_array($method, array('edit', 'browse'))) return helper::createLink($module, 'browse', "objectID=%s");
  1814. if($module == 'feedback' && $this->config->vision == 'lite') return helper::createLink('feedback', 'browse', "browseType=byProduct&productID=%s");
  1815. if($module == 'tree' && !empty($extra)) return helper::createLink('tree', $method, "productID=%s&view={$extra}&currentModuleID=0{$branchParam}");
  1816. if($module == 'design') return helper::createLink('design', 'browse', "productID=%s");
  1817. if($module == 'execution') return helper::createLink('execution', $method, "objectID=$extra&productID=%s");
  1818. if($module == 'feedback') return helper::createLink('feedback', 'admin', "browseType=byProduct&productID=%s");
  1819. if($module == 'programplan') return helper::createLink('programplan', 'browse', "projectID=%s&productID=%s&type=" . ($extra ? $extra : 'gantt'));
  1820. if($module == 'project') return helper::createLink('project', $method, "objectID=$extra&productID=%s");
  1821. if($module == 'tree') return helper::createLink('tree', $method, "productID=%s&view=story&currentModuleID=0{$branchParam}");
  1822. if($module == 'ticket') return helper::createLink('ticket', $method, 'productID=%s');
  1823. if($module == 'testtask') return helper::createLink('testtask', 'browse', "productID=%s&branch={$branchID}");
  1824. if(in_array($module, array('productplan', 'release', 'roadmap')) && $method != 'create') return helper::createLink($module, 'browse', "productID=%s{$branchParam}");
  1825. if($module == 'api' || $module == 'doc') return helper::createLink('doc', 'productSpace', "objectID=%s");
  1826. if($module == 'productplan' || $module == 'release') return helper::createLink($module, $method, "productID=%s{$branchParam}");
  1827. if($module == 'testcase' && in_array($method, array('groupcase', 'zerocase')) && $this->app->tab == 'project')
  1828. {
  1829. $projectID = $extra;
  1830. if(strpos($extra, 'projectID') !== false)
  1831. {
  1832. parse_str($extra, $output);
  1833. $projectID = zget($output, 'projectID', 0);
  1834. }
  1835. return helper::createLink($module, $method, "productID=%s&branch={$branchID}&groupBy=&projectID=$projectID") . "#app=project";
  1836. }
  1837. if($module == 'testcase' && !in_array($method, array('browse', 'browsescene', 'groupcase', 'zerocase', 'create'))) return helper::createLink($module, 'browse', "productID=%s");
  1838. if($module == 'story' && $this->config->vision == 'or') return helper::createLink('story', 'create', "productID=%s&branch=0&moduleID=0&storyID=0&objectID=0&bugID=0&planID0&todoID=0&extra=&storyType=requirement");
  1839. return helper::createLink($module, $method, "productID=%s{$branchParam}{$extraParam}");
  1840. }
  1841. /**
  1842. * 刷新产品的统计信息。
  1843. * Refresh stats info of products.
  1844. *
  1845. * @param bool $refreshAll
  1846. * @access public
  1847. * @return bool
  1848. */
  1849. public function refreshStats($refreshAll = false)
  1850. {
  1851. $updateTime = zget($this->app->config->global, 'productStatsTime', '');
  1852. if($updateTime && time() - strtotime($updateTime) < $this->config->product->refreshInterval && !$refreshAll) return true;
  1853. /*
  1854. * If productStatsTime is before two weeks ago, refresh all products directly.
  1855. * Else only refresh the latest products in action table.
  1856. */
  1857. $productActions = array();
  1858. $products = array();
  1859. if($updateTime < date('Y-m-d', strtotime('-14 days')) || $refreshAll)
  1860. {
  1861. $products = $this->dao->select('id')->from(TABLE_PRODUCT)->fetchPairs('id');
  1862. }
  1863. else
  1864. {
  1865. $productActions = $this->dao->select('distinct product')->from(TABLE_ACTION)
  1866. ->where('date')->ge($updateTime)
  1867. ->andWhere('product')->notin(array(',0,', ',,'))
  1868. ->fetchPairs('product');
  1869. if(empty($productActions)) return true;
  1870. foreach($productActions as $productAction)
  1871. {
  1872. foreach(explode(',', trim($productAction, ',')) as $product) $products[$product] = $product;
  1873. }
  1874. }
  1875. $now = helper::now();
  1876. /* 1. Get summary of products to be refreshed. */
  1877. $stats = $this->productTao->getProductStats($products);
  1878. /* 2. Refresh stats to db. */
  1879. foreach($stats as $productID=> $product)
  1880. {
  1881. $this->dao->update(TABLE_PRODUCT)->data($product)->where('id')->eq($productID)->exec();
  1882. }
  1883. /* 3. Update projectStatsTime in config. */
  1884. $this->loadModel('setting')->setItem('system.common.global.productStatsTime', $now);
  1885. $this->app->config->global->productStatsTime = $now;
  1886. /* 4. Clear actions older than 30 days. */
  1887. $this->loadModel('action')->cleanActions();
  1888. return !dao::isError();
  1889. }
  1890. /*
  1891. * 获取1.5级导航数据。
  1892. * Get product switcher.
  1893. *
  1894. * @param int $productID
  1895. * @param string $extra
  1896. * @access public
  1897. * @return string
  1898. */
  1899. /**
  1900. * @param int $productID
  1901. * @param string $extra
  1902. */
  1903. public function getSwitcher($productID = 0, $extra = '')
  1904. {
  1905. /* 获取产品名称,产品类型。 */
  1906. $currentProductName = $this->lang->productCommon;
  1907. if($productID)
  1908. {
  1909. $currentProduct = $this->getByID($productID);
  1910. $currentProductName = $currentProduct->name;
  1911. $this->session->set('currentProductType', $currentProduct->type);
  1912. }
  1913. /* Init locateModule and locateMethod for report and story. */
  1914. list($locateModule, $locateMethod) = $this->productTao->computeLocate4DropMenu();
  1915. /* 生成异步获取产品下拉菜单的链接。*/
  1916. $this->loadModel('index');
  1917. $fromModule = $this->app->tab == 'qa' ? 'qa' : '';
  1918. $dropMenuModule = $this->app->tab == 'qa' ? 'product' : $this->app->tab;
  1919. $dropMenuMethod = in_array("{$this->app->moduleName}-{$this->app->methodName}", $this->config->index->oldPages) ? 'ajaxGetOldDropMenu' : 'ajaxGetDropMenu';
  1920. $dataRide = in_array("{$this->app->moduleName}-{$this->app->methodName}", $this->config->index->oldPages) ? 'searchList' : 'dropmenu' ;
  1921. $dropMenuLink = helper::createLink($dropMenuModule, $dropMenuMethod, "objectID=$productID&module=$locateModule&method=$locateMethod&extra=$extra&from=$fromModule");
  1922. /* 构建产品1.5级导航数据。*/
  1923. $output = "<div class='btn-group header-btn' id='swapper'><button data-toggle='dropdown' type='button' class='btn' id='currentItem' title='{$currentProductName}'><span class='text'>{$currentProductName}</span> <span class='caret' style='margin-bottom: -1px'></span></button><div id='dropMenu' class='dropdown-menu search-list' data-ride='{$dataRide}' data-url='$dropMenuLink'>";
  1924. $output .= '<div class="input-control search-box has-icon-left has-icon-right search-example"><input type="search" class="form-control search-input" /><label class="input-control-icon-left search-icon"><i class="icon icon-search"></i></label><a class="input-control-icon-right search-clear-btn"><i class="icon icon-close icon-sm"></i></a></div>';
  1925. $output .= "</div></div>";
  1926. return $output;
  1927. }
  1928. /**
  1929. * Build search config for story list.
  1930. *
  1931. * @param int $productID
  1932. * @param string $storyType
  1933. * @access public
  1934. * @return array
  1935. */
  1936. public function buildSearchConfig($productID, $storyType)
  1937. {
  1938. $searchConfig = $this->config->product->search;
  1939. $this->app->loadLang($storyType);
  1940. unset($searchConfig['fields']['product']);
  1941. unset($searchConfig['params']['product']);
  1942. /* Get module data. */
  1943. $searchConfig['params']['module']['values'] = $this->productTao->getModulesForSearchForm($productID, $products = array());
  1944. $searchConfig['params']['grade']['values'] = $this->loadModel('story')->getGradePairs($storyType, 'all');
  1945. $searchConfig['params']['plan']['values'] = $this->loadModel('productplan')->getPairs(array($productID), '');
  1946. $searchConfig['params']['stage']['values'] = $this->lang->$storyType->stageList;
  1947. /* Get branch data. */
  1948. if($productID)
  1949. {
  1950. $productInfo = $this->getByID($productID);
  1951. if($productInfo->type == 'normal')
  1952. {
  1953. unset($searchConfig['fields']['branch']);
  1954. unset($searchConfig['params']['branch']);
  1955. }
  1956. else
  1957. {
  1958. $searchConfig['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productInfo->type]);
  1959. $searchConfig['params']['branch']['values'] = array('' => '', '0' => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($productID, 'noempty');
  1960. }
  1961. }
  1962. if($this->config->edition == 'ipd') $searchConfig['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID);
  1963. if($storyType != 'story')
  1964. {
  1965. $replacement = $storyType == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon;
  1966. $this->lang->story->title = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->title);
  1967. $this->lang->story->create = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->create);
  1968. $searchConfig['fields']['title'] = $this->lang->story->title;
  1969. }
  1970. return $searchConfig;
  1971. }
  1972. }