control.php 94 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235
  1. <?php
  2. /**
  3. * The control file of story module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package story
  9. * @version $Id: control.php 5145 2013-07-15 06:47:26Z chencongzhi520@gmail.com $
  10. * @link https://www.zentao.net
  11. */
  12. class story extends control
  13. {
  14. /**
  15. * The construct function, load product, tree, user auto.
  16. *
  17. * @param string $module
  18. * @param string $method
  19. * @access public
  20. * @return void
  21. */
  22. public function __construct($module = '', $method = '')
  23. {
  24. parent::__construct($module, $method);
  25. $this->loadModel('product');
  26. $this->loadModel('project');
  27. $this->loadModel('execution');
  28. $this->loadModel('tree');
  29. $this->loadModel('user');
  30. $this->loadModel('action');
  31. $this->loadModel('epic');
  32. $this->loadModel('requirement');
  33. if($this->app->rawModule == 'projectstory') $this->app->tab = 'project';
  34. }
  35. /**
  36. * Create a story.
  37. *
  38. * @param int $productID
  39. * @param string $branch
  40. * @param int $moduleID
  41. * @param int $storyID
  42. * @param int $objectID projectID|executionID
  43. * @param int $bugID
  44. * @param int $planID
  45. * @param int $todoID
  46. * @param string $extra for example feedbackID=0
  47. * @param string $storyType requirement|story
  48. * @access public
  49. * @return void
  50. */
  51. public function create($productID = 0, $branch = '', $moduleID = 0, $storyID = 0, $objectID = 0, $bugID = 0, $planID = 0, $todoID = 0, $extra = '', $storyType = 'story')
  52. {
  53. /* Set menu. */
  54. $this->story->replaceURLang($storyType);
  55. $copyStoryID = $storyID;
  56. list($productID, $objectID) = $this->storyZen->setMenuForCreate($productID, $objectID, $extra);
  57. if($productID == 0 && $objectID == 0) return $this->locate($this->createLink('product', 'create'));
  58. if($productID == 0 && $objectID != 0) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$objectID"));
  59. if(!empty($_POST))
  60. {
  61. if(isset($_POST['module'])) $moduleID = $this->post->module;
  62. if(isset($_POST['modules'])) $moduleID = reset($_POST['modules']);
  63. helper::setcookie('lastStoryModule', $moduleID, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false);
  64. /* API will post projectID or executionID. */
  65. if($this->post->project) $objectID = (int)$this->post->project;
  66. if($this->post->execution) $objectID = (int)$this->post->execution;
  67. /* API only has productID. */
  68. if(!isset($_POST['product'])) $_POST['product'] = $productID;
  69. /* Get story data from post. */
  70. $storyData = $this->storyZen->buildStoryForCreate($objectID, $bugID, $storyType);
  71. if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  72. /* Insert story data. */
  73. $createFunction = (!empty($storyData->branches) && $storyData->type == 'story') ? 'createTwins' : 'create'; // 只有软件需求才有孪生需求
  74. $storyID = $this->story->{$createFunction}($storyData, $objectID, $bugID, $extra, $todoID);
  75. if(empty($storyID) || dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  76. if(!empty($_POST['fileList']))
  77. {
  78. $fileList = $this->post->fileList;
  79. if($fileList) $fileList = json_decode($fileList, true);
  80. $this->loadModel('file')->saveDefaultFiles($fileList, 'story', $storyID, 1);
  81. }
  82. $productID = $this->post->product ? $this->post->product : $productID;
  83. $message = $this->executeHooks($storyID);
  84. if(empty($message)) $message = $this->post->status == 'draft' ? $this->lang->story->saveDraftSuccess : $this->lang->saveSuccess;
  85. if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $message, 'id' => $storyID));
  86. /* Get response when create in modal. */
  87. $response = $this->storyZen->getResponseInModal($message);
  88. if($response) return $this->send($response);
  89. $response = array('result' => 'success', 'message' => $message);
  90. if($this->post->newStory)
  91. {
  92. $response['message'] = $message . $this->lang->story->newStory;
  93. $response['load'] = $this->createLink('story', 'create', "productID=$productID&branch=$branch&moduleID=$moduleID&story=$copyStoryID&objectID=$objectID&bugID=$bugID&planID=$planID&todoID=$todoID&extra=$extra&storyType=$storyType");
  94. return $this->send($response);
  95. }
  96. $response['load'] = $this->storyZen->getAfterCreateLocation((int)$productID, $branch, $objectID, $storyID, $storyType, $extra);
  97. return $this->send($response);
  98. }
  99. /* Init vars. */
  100. $initStory = $this->storyZen->initStoryForCreate($planID, $copyStoryID, $bugID, $todoID, $extra);
  101. /* Get form fields. */
  102. $this->storyZen->setViewVarsForKanban($objectID, $this->story->parseExtra($extra), $storyType);
  103. $fields = $this->storyZen->getFormFieldsForCreate($productID, $branch, $objectID, $initStory, $storyType);
  104. $fields = $this->storyZen->setModuleField($fields, $moduleID);
  105. $fields = $this->storyZen->removeFormFieldsForCreate($fields, $storyType);
  106. $this->view->title = $this->view->product->name . $this->lang->hyphen . $this->lang->story->create;
  107. $this->view->fields = $fields;
  108. $this->view->blockID = $this->storyZen->getAssignMeBlockID();
  109. $this->view->type = $storyType;
  110. $this->view->story = $initStory;
  111. $this->view->forceReview = $this->story->checkForceReview($storyType);
  112. $extras = str_replace(array(',', ' ', '*'), array('&', '', '-'), $extra);
  113. parse_str($extras, $params);
  114. if(!isset($params['needNotReview'])) $extra .= ',needNotReview={needNotReview}';
  115. $this->view->needNotReview = $params['needNotReview'] ?? !$this->view->forceReview;
  116. $this->view->loadUrl = $this->createLink($storyType, 'create', "productID={product}&branch={branch}&moduleID=$moduleID&story=$storyID&objectID=$objectID&bugID=$bugID&planID=$planID&todoID=$todoID&extra=$extra&storyType=$storyType");
  117. $this->display();
  118. }
  119. /**
  120. * Create a batch stories.
  121. *
  122. * @param int $productID
  123. * @param string $branch
  124. * @param int $moduleID
  125. * @param int $storyID
  126. * @param int $executionID projectID|executionID
  127. * @param int $plan
  128. * @param string $storyType requirement|story
  129. * @param string $extra for example feedbackID=0
  130. * @access public
  131. * @return void
  132. */
  133. public function batchCreate($productID = 0, $branch = '', $moduleID = 0, $storyID = 0, $executionID = 0, $plan = 0, $storyType = 'story', $extra = '')
  134. {
  135. $this->story->replaceURLang($storyType);
  136. if(!empty($_POST))
  137. {
  138. $stories = $this->storyZen->buildStoriesForBatchCreate($productID, $storyType);
  139. if(empty($stories)) return $this->sendError($this->lang->story->errorEmptyStory, true);
  140. if(dao::isError()) return $this->sendError(dao::getError());
  141. $storyIdList = $this->story->batchCreate($stories);
  142. if(dao::isError()) return $this->sendError(dao::getError(), true);
  143. /* Project or execution linked stories. */
  144. if($executionID)
  145. {
  146. $lanes = array();
  147. $products = array();
  148. foreach($storyIdList as $i => $newStoryID)
  149. {
  150. if(isset($stories[$i]) && !empty($stories[$i]->lane)) $lanes[$newStoryID] = $stories[$i]->lane;
  151. $products[$newStoryID] = $productID;
  152. }
  153. if($this->session->project && $executionID != $this->session->project) $this->execution->linkStory($this->session->project, $storyIdList);
  154. $this->execution->linkStory($executionID, $storyIdList, $extra, $lanes, $storyType);
  155. }
  156. if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $stories));
  157. if(isInModal()) return $this->send($this->storyZen->getResponseInModal($this->lang->saveSuccess));
  158. $locateLink = $this->storyZen->getAfterBatchCreateLocation($productID, $branch, $executionID, $storyID, $storyType);
  159. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $locateLink));
  160. }
  161. $this->storyZen->setMenuForBatchCreate($productID, $branch, $executionID, $extra, $storyType);
  162. if($productID == 0 && $executionID != 0) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$executionID"));
  163. $product = $this->product->getByID($productID);
  164. if($product) $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  165. /* The 'batchCreateFields' of global variable $config will be changed and used by the following business logic. */
  166. $customFields = $this->storyZen->getCustomFields($this->config, $storyType, $this->view->hiddenPlan, $product);
  167. $showFields = $this->storyZen->getShowFields($this->config->{$storyType}->custom->batchCreateFields, $storyType, $product);
  168. $fields = $this->storyZen->getFormFieldsForBatchCreate($productID, $branch, $executionID, $storyType);
  169. $fields = $this->storyZen->removeFormFieldsForBatchCreate($fields, $this->view->hiddenPlan, isset($this->view->execution) ? $this->view->execution->type : '', $executionID);
  170. if($storyID)
  171. {
  172. $story = $this->story->getByID($storyID);
  173. if($story)
  174. {
  175. if(isset($fields['parent'])) $fields['parent']['default'] = $storyID;
  176. $gradeOptions = $this->story->getGradeOptions($story, $storyType);
  177. if(empty($gradeOptions)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorNoGradeSplit, 'locate' => $this->session->storyList)));
  178. $fields['grade']['options'] = $gradeOptions;
  179. $fields['grade']['default'] = key($gradeOptions);
  180. }
  181. $this->view->story = $story;
  182. $this->view->storyTitle = isset($story->title) ? $story->title : '';
  183. /* Check can subdivide or not. */
  184. if(!$this->story->checkCanSubdivide($story, !empty($product->shadow))) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorNotSubdivide)));
  185. /* Check can split requirement/story or not. */
  186. if($storyType != $story->type && !$this->story->checkCanSplit($story)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorCannotSplit)));
  187. }
  188. $this->view->title = $product->name . $this->lang->hyphen . ($storyID ? $this->lang->story->subdivide : $this->lang->story->batchCreate);
  189. $this->view->customFields = $customFields;
  190. $this->view->showFields = $showFields;
  191. $this->view->product = $product;
  192. $this->view->branch = $branch;
  193. $this->view->productID = $productID;
  194. $this->view->storyID = $storyID;
  195. $this->view->moduleID = $moduleID;
  196. $this->view->executionID = $executionID;
  197. $this->view->type = $storyType;
  198. $this->view->fields = $fields;
  199. $this->view->planID = $plan;
  200. $this->view->maxGradeGroup = $this->story->getMaxGradeGroup('all');
  201. $this->view->stories = $this->storyZen->getDataFromUploadImages($productID, $moduleID, $plan);
  202. $this->view->storyTitle = isset($story->title) ? $story->title : '';
  203. $this->view->forceReview = $this->story->checkForceReview($storyType);
  204. $this->display();
  205. }
  206. /**
  207. * The common action when edit or change a story.
  208. *
  209. * @param int $storyID
  210. * @param int $projectID
  211. * @access public
  212. * @return void
  213. */
  214. public function commonAction($storyID, $projectID = 0)
  215. {
  216. /* Get data. */
  217. $story = $this->story->getByID($storyID);
  218. if(!$story) return $this->send(array('result' => 'fail', 'message' => $this->lang->story->noStory));
  219. $this->story->replaceURLang($story->type);
  220. /* Set menu. */
  221. if($this->app->tab == 'project')
  222. {
  223. if(empty($projectID)) $projectID = $this->session->project;
  224. $this->loadModel('project')->setMenu($projectID);
  225. $this->view->projectID = $projectID;
  226. }
  227. elseif($this->app->tab == 'execution')
  228. {
  229. if(empty($projectID)) $projectID = $this->session->execution;
  230. $this->loadModel('execution')->setMenu($projectID);
  231. $this->view->executionID = $projectID;
  232. }
  233. elseif($this->app->tab == 'qa')
  234. {
  235. $this->loadModel('qa')->setMenu($story->product);
  236. }
  237. else
  238. {
  239. $this->product->setMenu($story->product, $story->branch);
  240. }
  241. $product = $this->product->getByID($story->product);
  242. if($product->shadow)
  243. {
  244. $products = $this->product->getPairs('', 0, '', 'all');
  245. }
  246. else
  247. {
  248. $products = $this->product->getPairs();
  249. }
  250. /* Assign. */
  251. $this->view->product = $product;
  252. $this->view->productID = $this->view->product->id;
  253. $this->view->products = $products;
  254. $this->view->story = $story;
  255. $this->view->moduleOptionMenu = $this->tree->getOptionMenu($story->product, 'story', 0, (string)$story->branch);
  256. $this->view->plans = $this->loadModel('productplan')->getPairs($story->product, 0, '', true);
  257. $this->view->actions = $this->action->getList('story', $storyID);
  258. }
  259. /**
  260. * Edit a story.
  261. *
  262. * @param int $storyID
  263. * @param string $kanbanGroup
  264. * @param string $storyType story|requirement
  265. * @access public
  266. * @return void
  267. */
  268. public function edit($storyID, $kanbanGroup = 'default', $storyType = 'story')
  269. {
  270. $this->loadModel('file');
  271. $this->app->loadLang('bug');
  272. $this->commonAction($storyID);
  273. $this->story->getAllChildId($storyID);
  274. if(!empty($_POST))
  275. {
  276. $storyData = $this->storyZen->buildStoryForEdit($storyID);
  277. if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  278. $this->storyZen->processDataForEdit($storyID, $storyData);
  279. $this->story->update($storyID, $storyData, $this->post->comment);
  280. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  281. $message = $this->executeHooks($storyID);
  282. if(empty($message)) $message = $this->lang->saveSuccess;
  283. if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
  284. $response = $this->storyZen->getResponseInModal($message);
  285. if($response) return $this->send($response);
  286. $response = array('result' => 'success', 'message' => $message);
  287. $response['load'] = $this->storyZen->getAfterEditLocation($storyID, $storyType);
  288. return $this->send($response);
  289. }
  290. $story = $this->story->getByID($storyID);
  291. if($story->type == 'requirement') $this->lang->story->notice->reviewerNotEmpty = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->notice->reviewerNotEmpty);
  292. $fields = $this->storyZen->getFormFieldsForEdit($storyID);
  293. $fields = $this->storyZen->hiddenFormFieldsForEdit($fields, $storyType);
  294. $this->view->title = $this->lang->story->edit . "STORY" . $this->lang->hyphen . $this->view->story->title;
  295. $this->view->story = $story;
  296. $this->view->showGrade = !empty($this->config->showStoryGrade);
  297. $this->view->twins = empty($story->twins) ? array() : $this->story->getByList($story->twins);
  298. $this->view->fields = $fields;
  299. $this->view->branches = $this->view->product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($story->product);
  300. $this->view->lastReviewer = $this->story->getLastReviewer($story->id);
  301. $this->display();
  302. }
  303. /**
  304. * Batch edit story.
  305. *
  306. * @param int $productID
  307. * @param int $executionID
  308. * @param string $branch
  309. * @param string $storyType
  310. * @param string $from
  311. * @access public
  312. * @return void
  313. */
  314. public function batchEdit($productID = 0, $executionID = 0, $branch = '', $storyType = 'story', $from = '')
  315. {
  316. $this->story->replaceURLang($storyType);
  317. $this->storyZen->setMenuForBatchEdit($productID, $executionID, $storyType, $from);
  318. if($this->config->vision != 'or') unset($this->config->story->form->batchEdit['roadmap']);
  319. /* Load model. */
  320. $this->loadModel('productplan');
  321. if($this->post->title)
  322. {
  323. $stories = $this->storyZen->buildStoriesForBatchEdit();
  324. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  325. $this->story->batchUpdate($stories);
  326. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  327. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->session->storyList));
  328. }
  329. $stories = $this->storyZen->getStoriesByChecked();
  330. if(!$stories) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->batchEditError, 'locate' => $this->session->storyList)));
  331. /* Set Custom*/
  332. foreach(explode(',', $this->config->story->list->customBatchEditFields) as $field) $customFields[$field] = $this->lang->story->$field;
  333. $product = $this->product->getByID($productID);
  334. if($product && $product->type == 'normal') unset($customFields['branch']);
  335. $this->view->customFields = $customFields;
  336. $this->view->showFields = $this->config->{$storyType}->custom->batchEditFields;
  337. $this->storyZen->setFormOptionsForBatchEdit($productID, $executionID, $stories);
  338. foreach($stories as $story) $story->branch = 'branch' . $story->branch;
  339. $this->view->title = $this->lang->story->batchEdit;
  340. $this->view->productID = $productID;
  341. $this->view->branch = $branch;
  342. $this->view->storyType = $storyType;
  343. $this->view->stories = $stories;
  344. $this->view->executionID = $executionID;
  345. $this->view->from = $from;
  346. $this->display();
  347. }
  348. /**
  349. * Change a story.
  350. *
  351. * @param int $storyID
  352. * @param string $from
  353. * @param string $storyType story|requirement
  354. * @access public
  355. * @return void
  356. */
  357. public function change($storyID, $from = '', $storyType = 'story')
  358. {
  359. if(!empty($_POST))
  360. {
  361. $storyData = $this->storyZen->buildStoryForChange($storyID);
  362. if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  363. $location = $this->storyZen->getAfterChangeLocation($storyID, $storyType);
  364. $dataChanged = $this->storyZen->checkDataChanged($storyID, $storyData);
  365. if(!$dataChanged) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $location));
  366. $changes = $this->story->change($storyID, $storyData);
  367. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  368. if($this->post->comment != '' or !empty($changes))
  369. {
  370. $action = !empty($changes) ? 'Changed' : 'Commented';
  371. $actionID = $this->action->create('story', $storyID, $action, $this->post->comment);
  372. $this->action->logHistory($actionID, $changes);
  373. /* Record submit review action. */
  374. $story = $this->story->fetchByID($storyID);
  375. if($story->status == 'reviewing') $this->action->create('story', $storyID, 'submitReview');
  376. }
  377. $message = $this->executeHooks($storyID);
  378. if(empty($message)) $message = $this->lang->saveSuccess;
  379. if(defined('RUN_MODE') and RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
  380. $response = $this->storyZen->getResponseInModal($message);
  381. if($response) return $this->send($response);
  382. return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location));
  383. }
  384. $this->commonAction($storyID);
  385. $story = $this->view->story;
  386. $this->story->getAffectedScope($story);
  387. $gradeGroup = array();
  388. $gradeList = $this->story->getGradeList('');
  389. foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
  390. /* Assign. */
  391. $this->view->title = $this->lang->story->change . "STORY" . $this->lang->hyphen . $story->title;
  392. $this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $story->assignedTo);
  393. $this->view->fields = $this->storyZen->getFormFieldsForChange($storyID);
  394. $this->view->gradeGroup = $gradeGroup;
  395. $this->view->lastReviewer = $this->story->getLastReviewer($story->id);
  396. $this->display();
  397. }
  398. /**
  399. * 激活需求。
  400. * Activate a story.
  401. *
  402. * @param int $storyID
  403. * @param string $storyType story|requirement
  404. * @access public
  405. * @return void
  406. */
  407. public function activate($storyID, $storyType = 'story')
  408. {
  409. if(!empty($_POST))
  410. {
  411. $postData = $this->storyZen->buildStoryForActivate($storyID);
  412. $this->story->activate($storyID, $postData);
  413. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  414. $this->executeHooks($storyID);
  415. if(isInModal())
  416. {
  417. $execution = $this->execution->getByID((int)$this->session->execution);
  418. if($this->app->tab == 'execution' and $execution->type == 'kanban')
  419. {
  420. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => "refreshKanban()", 'closeModal' => true));
  421. }
  422. else
  423. {
  424. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
  425. }
  426. }
  427. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
  428. }
  429. $this->commonAction($storyID);
  430. /* Assign. */
  431. $this->view->title = $this->lang->story->activate . "STORY" . $this->lang->hyphen . $this->view->story->title;
  432. $this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $this->view->story->closedBy);
  433. $this->display();
  434. }
  435. /**
  436. * View a story.
  437. *
  438. * @param int $storyID
  439. * @param int $version
  440. * @param int $param executionID|projectID
  441. * @param string $storyType story|requirement
  442. * @access public
  443. * @return void
  444. */
  445. public function view($storyID, $version = 0, $param = 0, $storyType = 'story')
  446. {
  447. $this->config->morphUpdate = false;
  448. $uri = $this->app->getURI(true);
  449. $tab = $this->app->tab;
  450. $story = $this->story->getById($storyID, $version, true);
  451. $product = $this->product->getByID((int)$story->product);
  452. $isAPI = defined('RUN_MODE') && RUN_MODE == 'api';
  453. if(!isInModal() && $tab == 'product' && !empty($product->shadow) && !$isAPI) return $this->send(array('result' => 'success', 'open' => array('url' => $uri, 'app' => 'project')));
  454. if(!$story || (isset($story->type) && $story->type != $storyType))
  455. {
  456. $locateModule = $this->config->vision == 'lite' ? 'project' : 'product';
  457. $locateMethod = ($this->config->edition == 'ipd' && $this->config->vision == 'or') ? 'browse' : 'index';
  458. return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink($locateModule, $locateMethod))));
  459. }
  460. if(!$this->app->user->admin and strpos(",{$this->app->user->view->products},", ",$story->product,") === false) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->product->accessDenied, 'locate' => $this->createLink('my', 'index'))));
  461. $this->session->set('productList', $uri . "#app={$tab}", 'product');
  462. if(!empty($story->fromBug)) $this->session->set('bugList', $uri, 'qa');
  463. $version = empty($version) ? $story->version : $version;
  464. $story = $this->story->mergeReviewer($story, true);
  465. $this->app->loadLang('bug');
  466. $this->story->replaceURLang($story->type);
  467. $this->storyZen->getLinkedObjects($story);
  468. $this->storyZen->setHiddenFieldsForView($product);
  469. if($product->type != 'normal') $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
  470. /* Set menu. */
  471. if($this->app->tab == 'project')
  472. {
  473. if($product->shadow)
  474. {
  475. $project = $this->loadModel('project')->getByShadowProduct($product->id);
  476. $projectID = $project->id;
  477. }
  478. else
  479. {
  480. $projectID = $param ? $param : $this->session->project;
  481. $project = $this->loadModel('project')->fetchByID((int)$projectID);
  482. }
  483. $this->view->projectID = $projectID;
  484. $this->view->project = $project;
  485. if(!$project->multiple)
  486. {
  487. $executionID = $param ? $param : $this->session->execution;
  488. $this->project->setMenu((int)$project->id);
  489. $this->view->executionID = $executionID;
  490. $this->view->execution = $this->loadModel('execution')->fetchByID($executionID);
  491. }
  492. else
  493. {
  494. $this->project->setMenu((int)$projectID);
  495. }
  496. }
  497. elseif($this->app->tab == 'execution')
  498. {
  499. $executionID = $param ? $param : $this->session->execution;
  500. $this->loadModel('execution')->setMenu($executionID);
  501. $this->view->executionID = $executionID;
  502. $this->view->execution = $this->execution->fetchByID($executionID);
  503. }
  504. elseif($this->app->tab == 'qa')
  505. {
  506. $this->loadModel('qa')->setMenu($story->product);
  507. }
  508. else
  509. {
  510. $this->product->setMenu($story->product, $story->branch);
  511. }
  512. $gradeGroup = array();
  513. $gradeList = $this->story->getGradeList('');
  514. foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
  515. $this->view->title = "STORY #$story->id $story->title - $product->name";
  516. $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id);
  517. $this->view->users = $this->user->getPairs('noletter');
  518. $this->view->executions = $this->execution->getPairs(0, 'all', 'nocode');
  519. $this->view->version = $version;
  520. $this->view->preAndNext = $this->loadModel('common')->getPreAndNextObject($story->type, $storyID);
  521. $this->view->builds = $this->loadModel('build')->getStoryBuilds($storyID);
  522. $this->view->releases = $this->loadModel('release')->getStoryReleases($storyID);
  523. $this->view->story = $story;
  524. $this->view->product = $product;
  525. $this->view->maxGradeGroup = $this->story->getMaxGradeGroup();
  526. $this->view->gradePairs = $this->story->getGradePairs($story->type, 'all');
  527. $this->view->gradeGroup = $gradeGroup;
  528. $this->view->roadmaps = $this->config->edition == 'ipd' ? array(0 => '') + $this->loadModel('roadmap')->getPairs() : array();
  529. $this->view->demand = $this->config->edition == 'ipd' ? $this->loadModel('demand')->getByID($story->demand) : new stdclass();
  530. $this->view->showGrade = !empty($this->config->showStoryGrade);
  531. $this->view->actions = $this->action->getList('story', $storyID);
  532. $this->view->branch = $story->branch;
  533. $this->view->project = isset($projectID) ? $this->loadModel('project')->getById($projectID) : null;
  534. $this->display();
  535. }
  536. /**
  537. * 需求详情,延后鉴权。
  538. * View a story, delay auth.
  539. *
  540. * @param int $storyID
  541. * @access public
  542. * @return void
  543. */
  544. public function storyView($storyID)
  545. {
  546. $story = $this->story->fetchByID($storyID);
  547. if(common::hasPriv($story->type, 'view'))
  548. {
  549. echo $this->fetch($story->type, 'view', "storyID=$storyID");
  550. }
  551. else
  552. {
  553. $vars = "module={$story->type}&method=view";
  554. if(isset($this->server->http_referer))
  555. {
  556. $referer = helper::safe64Encode($this->server->http_referer);
  557. $vars .= "&referer=$referer";
  558. }
  559. $denyLink = helper::createLink('user', 'deny', $vars);
  560. return $this->send(array('result' => 'success', 'load' => $denyLink));
  561. }
  562. }
  563. /**
  564. * Delete a story.
  565. *
  566. * @param int $storyID
  567. * @param string $confirm yes|no
  568. * @param string $from taskkanban
  569. * @param string $storyType story|requirement
  570. * @access public
  571. * @return void
  572. */
  573. public function delete($storyID, $confirm = 'no', $from = '', $storyType = 'story')
  574. {
  575. $story = $this->story->fetchById($storyID);
  576. if($story->parent < 0) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert('{$this->lang->story->cannotDeleteParent}');"));
  577. if($confirm == 'no')
  578. {
  579. if($story->type != 'story')
  580. {
  581. $replacement = $story->type == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon;
  582. $this->lang->story->confirmDelete = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->confirmDelete);
  583. }
  584. $confirmURL = $this->createLink($storyType, 'delete', "story=$storyID&confirm=yes&from=$from&storyType=$storyType");
  585. return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$this->lang->story->confirmDelete}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});"));
  586. }
  587. else
  588. {
  589. $this->dao->update(TABLE_STORY)->set('deleted')->eq(1)->where('id')->eq($storyID)->exec();
  590. $this->loadModel('action')->create($story->type, $storyID, 'deleted', '', actionModel::CAN_UNDELETED);
  591. if($story->parent > 0)
  592. {
  593. $this->story->updateParentStatus($story->id);
  594. $this->action->create('story', $story->parent, 'deleteChildrenStory', '', $storyID);
  595. }
  596. $this->story->setStage($story->id);
  597. $this->executeHooks($storyID);
  598. if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success'));
  599. if($this->app->tab == 'execution' and $from == 'taskkanban') return $this->send(array('result' => 'success', 'closeModal' => true, 'callback' => "refreshKanban()"));
  600. $locateLink = $this->session->storyList ? $this->session->storyList : $this->createLink('product', 'browse', "productID={$story->product}");
  601. $locateLink = isInModal() ? true : $locateLink;
  602. return $this->send(array('result' => 'success', 'load' => $locateLink, 'closeModal' => true));
  603. }
  604. }
  605. /**
  606. * Review a story.
  607. *
  608. * @param int $storyID
  609. * @param string $from product|project
  610. * @param string $storyType story|requirement
  611. * @access public
  612. * @return void
  613. */
  614. public function review($storyID, $from = 'product', $storyType = 'story')
  615. {
  616. if(!empty($_POST))
  617. {
  618. $storyData = $this->storyZen->buildStoryForReview($storyID);
  619. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  620. $this->story->review($storyID, $storyData, (string)$this->post->comment);
  621. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  622. $message = $this->executeHooks($storyID);
  623. if(empty($message)) $message = $this->lang->saveSuccess;
  624. if(isInModal())
  625. {
  626. if($this->app->tab == 'execution') $this->loadModel('kanban')->updateLane($this->session->execution, 'story', $storyID);
  627. return $this->send($this->storyZen->getResponseInModal($message));
  628. }
  629. if(defined('RUN_MODE') and RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
  630. $location = $this->storyZen->getAfterReviewLocation($storyID, $storyType, $from);
  631. return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location));
  632. }
  633. $this->commonAction($storyID);
  634. $story = $this->view->story;
  635. if(!empty($reviewers[$this->app->user->account]))
  636. {
  637. return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$this->lang->hasReviewed}'}).then((res) => {loadCurrentPage()});"));
  638. }
  639. $reviewers = $this->story->getReviewerPairs($storyID, $story->version);
  640. $this->story->getAffectedScope($story);
  641. $gradeGroup = array();
  642. $gradeList = $this->story->getGradeList('');
  643. foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
  644. $this->view->title = $this->lang->story->review . "STORY" . $this->lang->hyphen . $story->title;
  645. $this->view->fields = $this->storyZen->getFormFieldsForReview($storyID);
  646. $this->view->reviewers = $reviewers;
  647. $this->view->gradeGroup = $gradeGroup;
  648. $this->view->isLastOne = count(array_diff(array_keys($reviewers), explode(',', $story->reviewedBy))) <= 1;
  649. $this->display();
  650. }
  651. /**
  652. * Batch review stories.
  653. *
  654. * @param string $result
  655. * @param string $reason
  656. * @param string $storyType story|requirement
  657. * @access public
  658. * @return void
  659. */
  660. public function batchReview($result, $reason = '', $storyType = 'story')
  661. {
  662. if(!$this->post->storyIdList) return $this->send(array('result' => 'success', 'load' => $this->session->storyList));
  663. $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
  664. $message = $this->story->batchReview($storyIdList, $result, $reason);
  665. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  666. $this->loadModel('score')->create('ajax', 'batchOther');
  667. $response = array();
  668. $response['result'] = 'success';
  669. $response['load'] = false;
  670. if($message) $response['callback'] = "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$message}'}).then((res) => {loadCurrentPage()});";
  671. if(empty($message)) $response['load'] = true;
  672. return $this->send($response);
  673. }
  674. /**
  675. * Recall the story review or story change.
  676. *
  677. * @param int $storyID
  678. * @param string $from list
  679. * @param string $confirm no|yes
  680. * @param string $storyType story|requirement
  681. * @access public
  682. * @return void
  683. */
  684. public function recall($storyID, $from = 'list', $confirm = 'no', $storyType = 'story')
  685. {
  686. $story = $this->story->fetchById($storyID);
  687. if($confirm == 'no')
  688. {
  689. $confirmTips = $story->status == 'changing' ? $this->lang->story->confirmRecallChange : $this->lang->story->confirmRecallReview;
  690. $confirmURL = $this->createLink($story->type, 'recall', "storyID=$storyID&from=$from&confirm=yes&storyType=$storyType");
  691. return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message:'{$confirmTips}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});"));
  692. }
  693. if($story->status == 'changing') $this->story->recallChange($storyID);
  694. if($story->status == 'reviewing') $this->story->recallReview($storyID);
  695. $action = $story->status == 'changing' ? 'recalledChange' : 'Recalled';
  696. $this->loadModel('action')->create('story', $storyID, $action);
  697. if($from == 'modal') return $this->send(array('result' => 'success', 'load' => 'modal'));
  698. $locateLink = $this->session->storyList ? $this->session->storyList : $this->createLink('product', 'browse', "productID={$story->product}");
  699. if($from == 'view')
  700. {
  701. $module = $story->type;
  702. $method = 'view';
  703. $params = "storyID=$storyID&version=0&param=0&storyType=$storyType";
  704. if($this->app->tab == 'project')
  705. {
  706. $module = 'projectstory';
  707. $method = 'view';
  708. $params = "storyID=$storyID";
  709. }
  710. elseif($this->app->tab == 'execution')
  711. {
  712. $module = 'execution';
  713. $method = 'storyView';
  714. $params = "storyID=$storyID";
  715. }
  716. $locateLink = $this->createLink($module, $method, $params);
  717. if(strpos(',qa,doc,', ",{$this->app->tab},") !== false) $locateLink = true;
  718. }
  719. return $this->send(array('result' => 'success', 'load' => $locateLink, 'closeModal' => true));
  720. }
  721. /**
  722. * Submit review.
  723. *
  724. * @param int $storyID
  725. * @param string $storyType story|requirement
  726. * @access public
  727. * @return void
  728. */
  729. public function submitReview($storyID, $storyType = 'story')
  730. {
  731. if($_POST)
  732. {
  733. $storyData = $this->storyZen->buildStoryForSubmitReview($storyID);
  734. if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  735. $changes = $this->story->submitReview($storyID, $storyData);
  736. if(dao::isError()) return print(js::error(dao::getError()));
  737. if($changes)
  738. {
  739. $actionID = $this->loadModel('action')->create('story', $storyID, 'submitReview');
  740. $this->action->logHistory($actionID, $changes);
  741. }
  742. if(isInModal()) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => 'loadCurrentPage()'));
  743. $module = $this->app->tab == 'project' ? 'projectstory' : 'story';
  744. $params = $this->app->tab == 'project' ? "storyID=$storyID&project={$this->session->project}" : "storyID=$storyID&version=0&param=0&storyType=$storyType";
  745. return $this->send(array('result' => 'success', 'load' => $this->createLink($module, 'view', $params), 'message' => $this->lang->saveSuccess));
  746. }
  747. /* Get story and product. */
  748. $story = $this->story->fetchById($storyID);
  749. $product = $this->product->getById($story->product);
  750. /* Get reviewers. */
  751. $reviewers = $product->reviewer;
  752. if(!$reviewers and $product->acl != 'open') $reviewers = $this->loadModel('user')->getProductViewListUsers($product);
  753. /* Get story reviewer. */
  754. $reviewerList = $this->story->getReviewerPairs($story->id, $story->version);
  755. $story->reviewer = array_keys($reviewerList);
  756. $this->view->story = $story;
  757. $this->view->actions = $this->action->getList('story', $storyID);
  758. $this->view->reviewers = $this->user->getPairs('noclosed|nodeleted', '', 0, $reviewers);
  759. $this->view->users = $this->user->getPairs('noclosed|noletter');
  760. $this->view->needReview = (($this->app->user->account == $product->PO or $this->config->{$storyType}->needReview == 0 or !$this->story->checkForceReview($storyType)) and empty($story->reviewer)) ? "checked='checked'" : "";
  761. $this->view->lastReviewer = $this->story->getLastReviewer($story->id);
  762. $this->display();
  763. }
  764. /**
  765. * 关闭需求。
  766. * Close the story.
  767. *
  768. * @param int $storyID
  769. * @param string $from taskkanban
  770. * @param string $storyType story|requirement
  771. * @access public
  772. * @return void
  773. */
  774. public function close($storyID, $from = '', $storyType = 'story')
  775. {
  776. $story = $this->story->getById($storyID);
  777. $this->commonAction($storyID);
  778. if(!empty($_POST))
  779. {
  780. $postData = form::data($this->config->story->form->close, $storyID)
  781. ->stripTags($this->config->story->editor->close['id'], $this->config->allowedTags)
  782. ->removeIF($this->post->closedReason != 'duplicate', 'duplicateStory')
  783. ->get();
  784. if(strpos($this->config->{$story->type}->change->requiredFields, 'comment') !== false and !$this->post->comment) $this->send(array('result' => 'fail', 'message' => array('comment' => sprintf($this->lang->error->notempty, $this->lang->comment))));
  785. $postData = $this->loadModel('file')->processImgURL($postData, $this->config->story->editor->close['id'], $this->post->uid);
  786. $changes = $this->story->close($storyID, $postData);
  787. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  788. $this->executeHooks($storyID);
  789. if(isInModal())
  790. {
  791. $execution = $this->execution->getByID((int)$this->session->execution);
  792. if(($this->app->tab == 'execution' && $execution->type == 'kanban') || $from == 'taskkanban')
  793. {
  794. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "refreshKanban()"));
  795. }
  796. else
  797. {
  798. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => 'table'));
  799. }
  800. }
  801. if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
  802. $module = $this->app->tab == 'project' ? 'projectstory' : 'story';
  803. $params = $this->app->tab == 'project' ? "storyID=$storyID&project={$this->session->project}" : "storyID=$storyID&version=0&param=0&storyType=$storyType";
  804. return $this->send(array('result' => 'success', 'load' => $this->createLink($module, 'view', $params), 'message' => $this->lang->saveSuccess, 'closeModal' => true));
  805. }
  806. /* Get story and product. */
  807. $product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fields('name, id, `type`')->fetch();
  808. $this->story->replaceURLang($story->type);
  809. /* Set the closed reason options and remove subdivided options. */
  810. $reasonList = $this->lang->{$storyType}->reasonList;
  811. if($story->status == 'draft') unset($reasonList['cancel']);
  812. unset($reasonList['subdivided']);
  813. $this->view->title = $this->lang->story->close . "STORY" . $this->lang->hyphen . $story->title;
  814. $this->view->product = $product;
  815. $this->view->story = $story;
  816. $this->view->actions = $this->action->getList('story', $storyID);
  817. $this->view->users = $this->loadModel('user')->getPairs();
  818. $this->view->reasonList = $reasonList;
  819. $this->display();
  820. }
  821. /**
  822. * 批量关闭需求。
  823. * Batch close the stories.
  824. *
  825. * @param int $productID
  826. * @param int $executionID
  827. * @param string $storyType story|requirement
  828. * @param string $from contribute|work
  829. * @access public
  830. * @return void
  831. */
  832. public function batchClose($productID = 0, $executionID = 0, $storyType = 'story', $from = '')
  833. {
  834. if(!$this->post->storyIdList) return $this->send(array('result' => 'success', 'load' => $this->session->storyList));
  835. $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
  836. $this->story->replaceURLang($storyType);
  837. if($this->post->comment)
  838. {
  839. $stories = $this->storyZen->buildStoriesForBatchClose(); /* Get the stories which need to be closed. */
  840. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  841. $this->story->batchClose($stories);
  842. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  843. $this->loadModel('score')->create('ajax', 'batchOther');
  844. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->session->storyList));
  845. }
  846. $this->storyZen->setMenuForBatchClose($productID, $executionID, $storyType, $from);
  847. /* Get the skipped and already closed stories, and the count of stories which have a twin. */
  848. $stories = $this->story->getByList($storyIdList);
  849. $closedStory = array();
  850. $ignoreTwins = array();
  851. $twinsCount = array();
  852. $storyCount = count($stories);
  853. foreach($stories as $story)
  854. {
  855. if(!empty($ignoreTwins) and isset($ignoreTwins[$story->id]))
  856. {
  857. unset($stories[$story->id]);
  858. continue;
  859. }
  860. if($story->status == 'closed')
  861. {
  862. $closedStory[] = $story->id;
  863. unset($stories[$story->id]);
  864. }
  865. if(!empty($story->twins))
  866. {
  867. $twinsCount[$story->id] = 0;
  868. foreach(explode(',', trim($story->twins, ',')) as $twinID)
  869. {
  870. $twinsCount[$story->id] ++;
  871. $ignoreTwins[$twinID] = $twinID;
  872. }
  873. }
  874. }
  875. if($storyCount == count($closedStory)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->notice->closed, 'locate' => $this->session->storyList)));
  876. $errorTips = '';
  877. if($closedStory) $errorTips .= sprintf($this->lang->story->closedStory, implode(',', $closedStory));
  878. $this->view->productID = $productID;
  879. $this->view->stories = $stories;
  880. $this->view->storyType = $storyType;
  881. $this->view->twinsCount = $twinsCount;
  882. $this->view->errorTips = $errorTips;
  883. $this->display();
  884. }
  885. /**
  886. * Batch change the module of story.
  887. *
  888. * @param int $moduleID
  889. * @param string $storyType story|requirement
  890. * @access public
  891. * @return void
  892. */
  893. public function batchChangeModule($moduleID, $storyType = 'story')
  894. {
  895. if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
  896. $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
  897. $allChanges = $this->story->batchChangeModule($storyIdList, $moduleID);
  898. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  899. foreach($allChanges as $storyID => $changes)
  900. {
  901. $actionID = $this->action->create('story', $storyID, 'Edited');
  902. $this->action->logHistory($actionID, $changes);
  903. }
  904. $this->loadModel('score')->create('ajax', 'batchOther');
  905. return $this->send(array('result' => 'success', 'load' => true));
  906. }
  907. /**
  908. * Batch stories convert to tasks.
  909. *
  910. * @param int $executionID
  911. * @param int $projectID
  912. * @access public
  913. * @return void
  914. */
  915. public function batchToTask($executionID = 0, $projectID = 0)
  916. {
  917. if($this->app->tab == 'execution' and $executionID) $this->loadModel('execution')->setMenu($executionID);
  918. if($this->app->tab == 'project' and $executionID) $this->loadModel('execution')->setMenu($executionID);
  919. if(!empty($_POST['name']))
  920. {
  921. $response['result'] = 'success';
  922. $response['message'] = $this->lang->story->successToTask;
  923. $tasks = $this->storyZen->buildDataForBatchToTask($executionID, $projectID);
  924. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  925. $taskIdList = $this->story->batchToTask($executionID, $tasks);
  926. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  927. if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $taskIdList));
  928. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->createLink('execution', 'task', "executionID=$executionID")));
  929. }
  930. if(!$this->post->storyIdList) return $this->locate($this->session->storyList);
  931. $stories = $this->story->getByList($_POST['storyIdList']);
  932. $activeStories = array();
  933. $storyPairs = array();
  934. $hasParent = false;
  935. $hasERUR = false;
  936. foreach($stories as $story)
  937. {
  938. if(strpos(',draft,reviewing,changing,closed,', ",{$story->status},") !== false) continue;
  939. if($story->type != 'story')
  940. {
  941. $hasERUR = true;
  942. continue;
  943. }
  944. if($story->isParent == '1')
  945. {
  946. $hasParent = true;
  947. continue;
  948. }
  949. $activeStories[$story->id] = $story;
  950. $storyPairs[$story->id] = $story->title;
  951. }
  952. if(empty($activeStories)) return $this->sendError($this->lang->story->noStoryToTask, $this->session->storyList . "#app={$this->app->tab}");
  953. $execution = $this->execution->fetchByID($executionID);
  954. if($execution->multiple) $manageLink = common::hasPriv('execution', 'manageMembers') ? $this->createLink('execution', 'manageMembers', "execution={$execution->id}") : '';
  955. if(!$execution->multiple) $manageLink = common::hasPriv('project', 'manageMembers') ? $this->createLink('project', 'manageMembers', "projectID={$execution->project}") : '';
  956. $this->view->title = $this->lang->story->batchToTask;
  957. $this->view->executionID = $executionID;
  958. $this->view->projectID = $projectID;
  959. $this->view->manageLink = $manageLink;
  960. $this->view->syncFields = empty($_POST['fields']) ? array() : $_POST['fields'];
  961. $this->view->hourPointValue = empty($_POST['hourPointValue']) ? 0 : $_POST['hourPointValue'];
  962. $this->view->taskType = empty($_POST['type']) ? '' : $_POST['type'];
  963. $this->view->stories = $activeStories;
  964. $this->view->storyPairs = $storyPairs;
  965. $this->view->hasParent = $hasParent;
  966. $this->view->hasERUR = $hasERUR;
  967. $this->view->modules = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, 'allModule');
  968. $this->view->members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted');
  969. $this->view->storyTasks = $this->loadModel('task')->getStoryTaskCounts(array_keys($stories), $executionID);
  970. $this->display();
  971. }
  972. /**
  973. * Batch change the plan of story.
  974. *
  975. * @param int $planID
  976. * @param int $oldPlanID
  977. * @access public
  978. * @return void
  979. */
  980. public function batchChangePlan($planID, $oldPlanID = 0)
  981. {
  982. if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
  983. $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
  984. $allChanges = $this->story->batchChangePlan($storyIdList, $planID, $oldPlanID);
  985. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  986. foreach($allChanges as $storyID => $changes)
  987. {
  988. $actionID = $this->action->create('story', $storyID, 'Edited');
  989. $this->action->logHistory($actionID, $changes);
  990. }
  991. $this->loadModel('score')->create('ajax', 'batchOther');
  992. return $this->send(array('result' => 'success', 'load' => true));
  993. }
  994. /**
  995. * Batch change branch.
  996. *
  997. * @param int $branchID
  998. * @param string $confirm yes|no
  999. * @param string $storyIdList
  1000. * @param string $storyType story|requirement
  1001. * @access public
  1002. * @return void
  1003. */
  1004. public function batchChangeBranch($branchID, $confirm = '', $storyIdList = '', $storyType = 'story')
  1005. {
  1006. if(empty($storyIdList) and empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
  1007. if(!empty($_POST['storyIdList'])) $storyIdList = $this->post->storyIdList;
  1008. if(is_string($storyIdList)) $storyIdList = array_filter(explode(',', $storyIdList));
  1009. $storyIdList = $this->storyZen->convertChildID($storyIdList);
  1010. $plans = $this->loadModel('productplan')->getPlansByStories($storyIdList);
  1011. if(empty($confirm))
  1012. {
  1013. $stories = $this->story->getByList($storyIdList);
  1014. $normalStotyIdList = '';
  1015. $conflictStoryIdList = '';
  1016. $conflictStoryArray = array();
  1017. /* Determine whether there is a conflict between the branch of the story and the linked plan. */
  1018. foreach($storyIdList as $storyID)
  1019. {
  1020. if($stories[$storyID]->branch == $branchID) continue;
  1021. if($branchID == BRANCH_MAIN) continue;
  1022. if(!isset($plans[$storyID])) continue;
  1023. foreach($plans[$storyID] as $plan)
  1024. {
  1025. if($plan->branch == $branchID) continue;
  1026. $conflictStoryIdList .= "[{$storyID}]";
  1027. $conflictStoryArray[] = $storyID;
  1028. break;
  1029. }
  1030. }
  1031. /* Prompt the user whether to continue to modify the conflicting stories branch. */
  1032. if($conflictStoryIdList)
  1033. {
  1034. $normalStotyIdList = array_diff($storyIdList, $conflictStoryArray);
  1035. $normalStotyIdList = implode(',', $normalStotyIdList);
  1036. $storyIdList = implode(',', $storyIdList);
  1037. $confirmURL = $this->createLink('story', 'batchChangeBranch', "branchID=$branchID&confirm=yes&storyIdList=$storyIdList&storyType=$storyType");
  1038. $cancelURL = $this->createLink('story', 'batchChangeBranch', "branchID=$branchID&confirm=no&storyIdList=$normalStotyIdList&storyType=$storyType");
  1039. return $this->send(array('result' => 'success', 'load' => array('confirm' => sprintf($this->lang->story->confirmChangeBranch, $conflictStoryIdList), 'confirmed' => $confirmURL, 'canceled' => $cancelURL)));
  1040. }
  1041. }
  1042. $allChanges = $this->story->batchChangeBranch($storyIdList, $branchID, $confirm, $plans);
  1043. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  1044. foreach($allChanges as $storyID => $changes)
  1045. {
  1046. $actionID = $this->action->create('story', $storyID, 'Edited');
  1047. $this->action->logHistory($actionID, $changes);
  1048. }
  1049. if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther');
  1050. return $this->send(array('result' => 'success', 'load' => true));
  1051. }
  1052. /**
  1053. * Batch change the grade of story.
  1054. *
  1055. * @param int $grade
  1056. * @param string $storyType story|requirement|epic
  1057. * @access public
  1058. * @return void
  1059. */
  1060. public function batchChangeGrade($grade, $storyType = 'story')
  1061. {
  1062. if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
  1063. $storyIdList = array_unique($this->post->storyIdList);
  1064. $message = $this->story->batchChangeGrade($storyIdList, $grade, $storyType);
  1065. $response = array();
  1066. $response['result'] = 'success';
  1067. $response['load'] = false;
  1068. if($message) $response['callback'] = "zui.Modal.alert('{$message}').then((res) => {loadCurrentPage()});";
  1069. if(empty($message)) $response['load'] = true;
  1070. return $this->send($response);
  1071. }
  1072. /**
  1073. * Batch change the stage of story.
  1074. *
  1075. * @param string $stage
  1076. * @access public
  1077. * @return void
  1078. */
  1079. public function batchChangeStage($stage)
  1080. {
  1081. if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
  1082. $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
  1083. $message = $this->story->batchChangeStage($storyIdList, $stage);
  1084. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  1085. $this->loadModel('score')->create('ajax', 'batchOther');
  1086. $response = array();
  1087. $response['result'] = 'success';
  1088. $response['load'] = false;
  1089. if($message) $response['callback'] = "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$message}'}).then((res) => {loadCurrentPage()});";
  1090. if(empty($message)) $response['load'] = true;
  1091. return $this->send($response);
  1092. }
  1093. /**
  1094. * 批量修改需求的父需求。
  1095. * Batch change the parent of story.
  1096. *
  1097. * @param int $productID
  1098. * @param string $storyType story|requirement
  1099. * @access public
  1100. * @return void
  1101. */
  1102. public function batchChangeParent($productID = 0, $storyType = 'story')
  1103. {
  1104. if($_POST)
  1105. {
  1106. $storyIdList = $this->cookie->checkedItem;
  1107. $result = $this->story->batchChangeParent($storyIdList, (int)$this->post->parent, $storyType);
  1108. if($result) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert('$result');", 'closeModal' => true, 'load' => true));
  1109. return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true));
  1110. }
  1111. $this->view->parents = $this->story->getParentStoryPairs($productID, '', $storyType);
  1112. $this->display();
  1113. }
  1114. /**
  1115. * 需求的指派给页面。
  1116. * Assign the story to a user.
  1117. *
  1118. * @param int $storyID
  1119. * @access public
  1120. * @return void
  1121. */
  1122. public function assignTo($storyID)
  1123. {
  1124. if(!empty($_POST))
  1125. {
  1126. $story = form::data($this->config->story->form->assignTo, $storyID)->get();
  1127. $this->story->assign($storyID, $story);
  1128. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  1129. $message = $this->executeHooks($storyID);
  1130. $response = $this->storyZen->getResponseInModal($message);
  1131. if($response) return $this->send($response);
  1132. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
  1133. }
  1134. /* Get story and product to render the view. */
  1135. $story = $this->story->getById($storyID);
  1136. $product = $this->product->getByID($story->product);
  1137. $users = $this->config->vision == 'lite' ? $this->user->getTeamMemberPairs($this->session->project) : $this->user->getPairs('nodeleted|noclosed|pofirst|noletter');
  1138. $this->view->title = zget($product, 'name', $story->title) . $this->lang->hyphen . $this->lang->story->assign;
  1139. $this->view->story = $story;
  1140. $this->view->actions = $this->action->getList('story', $storyID);
  1141. $this->view->users = $users;
  1142. $this->display();
  1143. }
  1144. /**
  1145. * Batch assign to.
  1146. *
  1147. * @param string $storyType story|requirement
  1148. * @access public
  1149. * @return void
  1150. * @param string $assignedTo
  1151. */
  1152. public function batchAssignTo($storyType = 'story', $assignedTo = '')
  1153. {
  1154. if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
  1155. if(empty($assignedTo)) $assignedTo = $this->post->assignedTo;
  1156. $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
  1157. $oldStories = $this->story->getByList($storyIdList);
  1158. $allChanges = $this->story->batchAssignTo($storyIdList, $assignedTo);
  1159. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  1160. $ignoreStories = array();
  1161. $assignedTwins = array();
  1162. foreach($allChanges as $storyID => $changes)
  1163. {
  1164. $oldStory = $oldStories[$storyID];
  1165. if($oldStory->status == 'closed') $ignoreStories[] = "#{$storyID}";
  1166. /* Sync twins. */
  1167. if(empty($oldStory->twins)) continue;
  1168. $twins = array_unique(array_filter(explode(',', $oldStory->twins)));
  1169. foreach($twins as $i => $twinID)
  1170. {
  1171. if(in_array($twinID, $storyIdList) || isset($assignedTwins[$twinID])) unset($twins[$i]);
  1172. }
  1173. $this->story->syncTwins($storyID, implode(',', $twins), $changes, 'Assigned');
  1174. foreach($twins as $assignedID) $assignedTwins[$assignedID] = $assignedID;
  1175. }
  1176. $this->loadModel('score')->create('ajax', 'batchOther');
  1177. $response = array('result' => 'success', 'load' => true);
  1178. if($ignoreStories) $response['ignoreMessage'] = sprintf($this->lang->story->ignoreClosedStory, implode(',', $ignoreStories));
  1179. return $this->send($response);
  1180. }
  1181. /**
  1182. * 查看需求的相关任务。
  1183. * Tasks of a story.
  1184. *
  1185. * @param int $storyID
  1186. * @param int $executionID
  1187. * @access public
  1188. * @return void
  1189. */
  1190. public function tasks($storyID, $executionID = 0)
  1191. {
  1192. $tasks = $this->loadModel('task')->getListByStory($storyID, $executionID);
  1193. $this->view->tasks = $tasks;
  1194. $this->view->users = $this->user->getPairs('noletter');
  1195. $this->view->summary = $this->execution->summary($tasks);
  1196. $this->view->story = $this->story->getById($storyID);
  1197. $this->display();
  1198. }
  1199. /**
  1200. * 查看需求的相关缺陷。
  1201. * Bugs of a story.
  1202. *
  1203. * @param int $storyID
  1204. * @param int $executionID
  1205. * @access public
  1206. * @return void
  1207. */
  1208. public function bugs($storyID, $executionID = 0)
  1209. {
  1210. $this->view->bugs = $this->loadModel('bug')->getStoryBugs($storyID, $executionID);
  1211. $this->view->users = $this->user->getPairs('noletter');
  1212. $this->view->story = $this->story->getById($storyID);
  1213. $this->display();
  1214. }
  1215. /**
  1216. * 查看需求的相关用例。
  1217. * Cases of a story.
  1218. *
  1219. * @param int $storyID
  1220. * @access public
  1221. * @return void
  1222. */
  1223. public function cases($storyID)
  1224. {
  1225. $this->view->cases = $this->loadModel('testcase')->getStoryCases($storyID);
  1226. $this->view->users = $this->user->getPairs('noletter');
  1227. $this->view->resultList = $this->lang->testcase->resultList;
  1228. $this->view->story = $this->story->getById($storyID);
  1229. $this->display();
  1230. }
  1231. /**
  1232. * 展示用户需求关联的软件需求,或软件需求关联的用户需求。
  1233. * Show the URs/SRs of the SR/UR.
  1234. *
  1235. * @param int $storyID
  1236. * @param string $storyType story|requirement
  1237. * @access public
  1238. * @return void
  1239. */
  1240. public function relation($storyID, $storyType = 'story')
  1241. {
  1242. $story = $this->story->getById($storyID);
  1243. $storyType = $storyType != $story->type ? $story->type : $storyType;
  1244. $selectFields = array('id', 'pri', 'title', 'plan', 'openedBy', 'assignedTo', 'estimate', 'status');
  1245. $this->view->relation = $this->story->getStoryRelation($storyID, $storyType, $selectFields);
  1246. $this->view->users = $this->user->getPairs('noletter');
  1247. $this->view->storyType = $storyType;
  1248. $this->view->story = $story;
  1249. $this->display();
  1250. }
  1251. /**
  1252. * 需求详情页关联需求。
  1253. * Link story in view.
  1254. *
  1255. * @param int $storyID
  1256. * @param string $type link|remove
  1257. * @param int $linkedStoryID
  1258. * @param string $browseType ''|bySearch
  1259. * @param int $queryID 0|
  1260. * @param int $recTotal
  1261. * @param int $recPerPage
  1262. * @param int $pageID
  1263. * @param string $orderBy
  1264. * @access public
  1265. * @return void
  1266. */
  1267. public function linkStory($storyID, $type = 'link', $linkedStoryID = 0, $browseType = '', $queryID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1, $orderBy = 'id_desc')
  1268. {
  1269. $this->commonAction($storyID);
  1270. $story = $this->story->getById($storyID);
  1271. if($type == 'remove')
  1272. {
  1273. $this->story->unlinkStory($storyID, $linkedStoryID);
  1274. return $this->send(array('result' => 'success', 'load' => true));
  1275. }
  1276. if($_POST)
  1277. {
  1278. $this->story->linkStories($storyID, $this->post->stories);
  1279. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  1280. return $this->send(array('result' => 'success', 'callback' => "const modal = zui.Modal.query('.modal-dialog[data-auto-load=\"story:$storyID\"]'); if(typeof(modal) !== 'undefined') modal.render(); else loadCurrentPage();", 'closeModal' => true));
  1281. }
  1282. /* Get story, product, products, and queryID. */
  1283. $products = $this->product->getPairs('', 0, '', 'all');
  1284. /* Build search form. */
  1285. $actionURL = $this->createLink('story', 'linkStory', "storyID=$storyID&type=$type&linkedStoryID=$linkedStoryID&browseType=bySearch&queryID=myQueryID&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID");
  1286. $this->product->buildSearchForm($story->product, $products, $queryID, $actionURL, 'all', (string)$story->branch);
  1287. /* Load pager. */
  1288. $this->app->loadClass('pager', true);
  1289. $pager = new pager($recTotal, $recPerPage, $pageID);
  1290. /* Get stories to link. */
  1291. $stories2Link = $this->story->getStories2Link($storyID, $browseType, $queryID, $pager, $orderBy);
  1292. /* Assign. */
  1293. $this->view->title = $this->lang->story->linkStory . "STORY" . $this->lang->hyphen .$this->lang->story->linkStory;
  1294. $this->view->type = $type;
  1295. $this->view->pager = $pager;
  1296. $this->view->stories2Link = $stories2Link;
  1297. $this->view->maxGradeGroup = $this->story->getMaxGradeGroup();
  1298. $this->view->users = $this->loadModel('user')->getPairs('noletter');
  1299. $this->view->storyID = $storyID;
  1300. $this->view->linkedStoryID = $linkedStoryID;
  1301. $this->view->browseType = $browseType;
  1302. $this->view->queryID = $queryID;
  1303. $this->view->orderBy = $orderBy;
  1304. $this->display();
  1305. }
  1306. /**
  1307. * 需求的父需求变更时,子需求确认变更。
  1308. * Confirm the change of the parent story.
  1309. *
  1310. * @param int $storyID
  1311. * @access public
  1312. * @return void
  1313. */
  1314. public function processStoryChange($storyID)
  1315. {
  1316. $story = $this->story->fetchByID($storyID);
  1317. if($story->parent <= 0 && $this->config->edition == 'ipd') return $this->fetch('demand', 'processDemandChange', "objectID=$storyID&type=story");
  1318. $parent = $this->story->fetchByID($story->parent);
  1319. $this->dao->update(TABLE_STORY)->set('parentVersion')->eq($parent->version)->where('id')->eq($storyID)->exec();
  1320. return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true));
  1321. }
  1322. /**
  1323. * 获取执行的需求列表信息用于html下拉列表。
  1324. * AJAX: get stories of a execution in html select.
  1325. *
  1326. * @param int $executionID
  1327. * @param int $productID
  1328. * @param int|string $branch 0|all|integer
  1329. * @param int $moduleID
  1330. * @param int $storyID
  1331. * @param string $pageType batch
  1332. * @param string $type full|short
  1333. * @param string $status all|unclosed
  1334. * @access public
  1335. * @return void
  1336. */
  1337. public function ajaxGetExecutionStories($executionID, $productID = 0, $branch = 0, $moduleID = 0, $storyID = 0, $pageType = '', $type = 'full', $status = 'all')
  1338. {
  1339. if($moduleID)
  1340. {
  1341. $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
  1342. $moduleID = $this->tree->getAllChildID($moduleID);
  1343. }
  1344. $stories = $this->story->getExecutionStoryPairs($executionID, $productID, $branch, $moduleID, $type, $status, 'story', false);
  1345. if($this->app->getViewType() === 'json')
  1346. {
  1347. return print(json_encode($stories));
  1348. }
  1349. elseif($this->app->getViewType() == 'mhtml')
  1350. {
  1351. return print(html::select('story', empty($stories) ? array() : $stories, $storyID, 'onchange=setStoryRelated()'));
  1352. }
  1353. elseif($pageType == 'batch')
  1354. {
  1355. $storyList = array();
  1356. foreach($stories as $id => $name) $storyList[] = array('value' => $id, 'text' => $name);
  1357. return $this->send($storyList);
  1358. }
  1359. else
  1360. {
  1361. $items = $this->story->addGradeLabel($stories);
  1362. return print(json_encode($items));
  1363. }
  1364. }
  1365. /**
  1366. * 获取产品的需求列表信息用于html下拉列表。
  1367. * AJAX: get the stories of a product in html select.
  1368. *
  1369. * @param int $productID
  1370. * @param int $branch
  1371. * @param int $moduleID
  1372. * @param int $storyID
  1373. * @param string $onlyOption
  1374. * @param string $status ''|all|noclosed|changing|active|draft|closed|reviewing
  1375. * @param int $limit
  1376. * @param string $type full|all
  1377. * @param int $hasParent 0|1
  1378. * @param int $objectID projectID|executionID
  1379. * @param int $isHTML 0|1
  1380. * @access public
  1381. * @return void
  1382. */
  1383. public function ajaxGetProductStories($productID, $branch = 0, $moduleID = 0, $storyID = 0, $onlyOption = 'false', $status = '', $limit = 0, $type = 'full', $hasParent = 1, $objectID = 0, $isHTML = 1)
  1384. {
  1385. $hasParent = $hasParent >= 1 ? true : false;
  1386. $modules = array();
  1387. if($moduleID)
  1388. {
  1389. $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
  1390. $modules = $this->tree->getAllChildID($moduleID);
  1391. }
  1392. $storyStatus = $this->story->getStatusList($status);
  1393. if($storyStatus == 'active') $storyStatus = 'active,reviewing';
  1394. if($objectID)
  1395. {
  1396. $stories = $this->story->getExecutionStoryPairs($objectID, $productID, $branch, $modules, $type, 'all', 'story', $hasParent);
  1397. }
  1398. else
  1399. {
  1400. $stories = $this->story->getProductStoryPairs($productID, $branch, $modules, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent);
  1401. }
  1402. if(!in_array($this->app->tab, array('execution', 'project')) and empty($stories)) $stories = $this->story->getProductStoryPairs($productID, $branch, 0, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent);
  1403. if($storyID && !isset($stories[$storyID]))
  1404. {
  1405. $story = $this->story->fetchByID($storyID);
  1406. if(!$moduleID || in_array($story->module, $modules)) $stories = arrayUnion($stories, array($storyID => $storyID . ':' . $story->title));
  1407. }
  1408. if($isHTML == 0)
  1409. {
  1410. $storyList = array();
  1411. foreach($stories as $storyID => $storyName) $storyList[] = array('value' => $storyID, 'text' => $storyName);
  1412. $storyList = $this->story->addGradeLabel($stories);
  1413. return $this->send($storyList);
  1414. }
  1415. $items = $this->story->addGradeLabel($stories);
  1416. return print(json_encode($items));
  1417. }
  1418. /**
  1419. * AJAX: search stories of a product as json
  1420. *
  1421. * @param string $key #storyID|storyID|keyword
  1422. * @param int $productID
  1423. * @param int $branch
  1424. * @param int $moduleID
  1425. * @param int $storyID
  1426. * @param string $status noclosed
  1427. * @param int $limit 50|0
  1428. * @access public
  1429. * @return void
  1430. */
  1431. public function ajaxSearchProductStories($key, $productID, $branch = 0, $moduleID = 0, $storyID = 0, $status = 'noclosed', $limit = 50)
  1432. {
  1433. if($moduleID)
  1434. {
  1435. $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
  1436. $moduleID = $this->tree->getAllChildID($moduleID);
  1437. }
  1438. $storyStatus = array();
  1439. if($status == 'noclosed')
  1440. {
  1441. $storyStatus = $this->lang->story->statusList;
  1442. unset($storyStatus['closed']);
  1443. $storyStatus = array_keys($storyStatus);
  1444. }
  1445. $stories = $this->story->getProductStoryPairs($productID, $branch, $moduleID, $storyStatus);
  1446. $result = array();
  1447. $resultCount = 0;
  1448. foreach ($stories as $id => $story)
  1449. {
  1450. if($limit > 0 && $resultCount > $limit) break;
  1451. if(('#' . $id) === $key || stripos($story, $key) !== false)
  1452. {
  1453. $result[$id] = $story;
  1454. $resultCount++;
  1455. }
  1456. }
  1457. if($resultCount < 1) $result['info'] = $this->lang->noResultsMatch;
  1458. echo json_encode($result);
  1459. }
  1460. /**
  1461. * 获取关闭需求页面重复需求的下拉列表。
  1462. * AJAX: get the duplicated stories of a story.
  1463. *
  1464. * @param int $storyID
  1465. * @param int $productID
  1466. * @access public
  1467. * @return void
  1468. */
  1469. public function ajaxGetDuplicatedStories($storyID, $productID)
  1470. {
  1471. $product = $this->dao->findById($productID)->from(TABLE_PRODUCT)->fields('name, id, `type`')->fetch();
  1472. $story = $this->story->fetchByID($storyID);
  1473. $storyBranch = $story->branch > 0 ? $story->branch : '0';
  1474. $branch = $product->type == 'branch' ? $storyBranch : 'all';
  1475. $productStories = $this->story->getProductStoryPairs($story->product, $branch, 0, 'all', 'id_desc', 0, '', $story->type);
  1476. $children = $this->story->getAllChildId($storyID);
  1477. $productStories = array_diff_key($productStories, array_flip($children));
  1478. if(isset($productStories[$storyID])) unset($productStories[$storyID]);
  1479. $items = array();
  1480. foreach($productStories as $storyID => $storyName) $items[] = array('text' => $storyName, 'value' => $storyID);
  1481. return print(json_encode($items));
  1482. }
  1483. /**
  1484. * 获取用例可关联的需求下拉列表。
  1485. * AJAX: get the stories of a case.
  1486. *
  1487. * @param int $productID
  1488. * @param int $moduleID
  1489. * @param int $branch
  1490. * @param int $storyID
  1491. * @access public
  1492. * @return void
  1493. */
  1494. public function ajaxGetCaseStories($productID = 0, $moduleID = 0, $branch = '', $storyID = 0)
  1495. {
  1496. if($storyID)
  1497. {
  1498. $story = $this->story->fetchByID($storyID);
  1499. if(empty($moduleID)) $moduleID = $story->module;
  1500. }
  1501. $modules = array();
  1502. if($moduleID)
  1503. {
  1504. $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
  1505. $modules = $this->tree->getAllChildID($moduleID);
  1506. }
  1507. $stories = $this->story->getProductStoryPairs($productID, $branch, $modules, 'active,reviewing', 'id_desc', 0, '', 'story', false);
  1508. if($this->app->tab != 'qa' && $this->app->tab != 'product' && $this->app->tab != 'my')
  1509. {
  1510. $projectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution;
  1511. if($projectID) $stories = $this->story->getExecutionStoryPairs($projectID, $productID, $branch, $modules, 'full', 'all', 'story', false);
  1512. }
  1513. if(!in_array($this->app->tab, array('execution', 'project')) and empty($stories)) $stories = $this->story->getProductStoryPairs($productID, $branch, 0, 'active,reviewing', 'id_desc', 0, '', 'story', false);
  1514. if($storyID && !isset($stories[$storyID])) $stories = arrayUnion($stories, array($storyID => $storyID . ':' . $story->title));
  1515. $stories = $this->story->addGradeLabel($stories);
  1516. return print(json_encode($stories));
  1517. }
  1518. /**
  1519. * 获取需求详情和操作日志。
  1520. * AJAX: get the actions and detail of the story for web app.
  1521. *
  1522. * @param int $storyID
  1523. * @access public
  1524. * @return void
  1525. */
  1526. public function ajaxGetDetail($storyID)
  1527. {
  1528. $this->view->actions = $this->action->getList('story', $storyID);
  1529. $this->view->story = $this->story->getByID($storyID);
  1530. $this->display();
  1531. }
  1532. /**
  1533. * 获取需求层级下拉列表及默认值。
  1534. * AJAX: get the grade of a story.
  1535. *
  1536. * @param int $storyID
  1537. * @param string $type
  1538. * @param int $grade
  1539. * @access public
  1540. * @return void
  1541. */
  1542. public function ajaxGetGrade($storyID, $type = 'story', $grade = 0)
  1543. {
  1544. $story = $this->story->fetchByID($storyID);
  1545. $gradeOptions = $this->story->getGradeOptions($story, $type, (array)$grade);
  1546. $items = array();
  1547. foreach($gradeOptions as $grade => $name) $items[] = array('text' => $name, 'value' => $grade);
  1548. return $this->send(array('items' => array_values($items), 'default' => key($gradeOptions)));
  1549. }
  1550. /**
  1551. * 检查需求的等级是否超出系统设置。
  1552. * AJAX: check the grade of a story.
  1553. *
  1554. * @param int $storyID
  1555. * @param int $newGrade
  1556. * @access public
  1557. * @return bool
  1558. */
  1559. public function ajaxCheckGrade($storyID, $newGrade)
  1560. {
  1561. $oldStory = $this->story->fetchByID($storyID);
  1562. $story = clone $oldStory;
  1563. $story->grade = $newGrade;
  1564. if($story->grade > $oldStory->grade)
  1565. {
  1566. if(!$this->story->checkGrade($story, $oldStory)) return print(json_encode(array('result' => false, 'message' => dao::getError())));
  1567. }
  1568. return print(json_encode(array('result' => true)));
  1569. }
  1570. /**
  1571. * 获取需求的信息。
  1572. * AJAX: get module of a story.
  1573. *
  1574. * @param int $storyID
  1575. * @param string $pageType batch
  1576. * @access public
  1577. * @return mixed
  1578. */
  1579. public function ajaxGetInfo($storyID, $pageType = '')
  1580. {
  1581. $story = $this->story->getByID($storyID);
  1582. if(empty($story)) return;
  1583. $storyInfo['moduleID'] = $story->module;
  1584. $storyInfo['estimate'] = $story->estimate;
  1585. $storyInfo['pri'] = $story->pri;
  1586. $storyInfo['spec'] = html_entity_decode($story->spec, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
  1587. $storyInfo['status'] = $story->status;
  1588. if($pageType == 'batch') return $this->send(array('storyInfo' => $storyInfo));
  1589. return print(json_encode($storyInfo));
  1590. }
  1591. /**
  1592. * 获取某产品下的父需求列表。
  1593. * AJAX: get the parent story.
  1594. *
  1595. * @param int $productID
  1596. * @param int $storyID
  1597. * @access public
  1598. * @return string
  1599. */
  1600. public function ajaxGetParentStory($productID, $storyID = 0)
  1601. {
  1602. if($storyID)
  1603. {
  1604. $story = $this->story->fetchByID($storyID);
  1605. $stories = $this->story->getParentStoryPairs($story->product, $story->parent, $story->type, $storyID);
  1606. }
  1607. else
  1608. {
  1609. $stories = $this->story->getParentStoryPairs($productID);
  1610. }
  1611. $items = array();
  1612. foreach($stories as $storyID => $storyTitle)
  1613. {
  1614. if(empty($storyID)) continue;
  1615. $items[] = array('text' => $storyTitle, 'value' => $storyID);
  1616. }
  1617. return $this->send($items);
  1618. }
  1619. /**
  1620. * 查看需求的报告。
  1621. * The report page.
  1622. *
  1623. * @param int $productID
  1624. * @param int $branchID
  1625. * @param string $storyType
  1626. * @param string $browseType
  1627. * @param int $moduleID
  1628. * @param string $chartType
  1629. * @param int $projectID
  1630. * @access public
  1631. * @return void
  1632. */
  1633. public function report($productID, $branchID, $storyType = 'story', $browseType = 'unclosed', $moduleID = 0, $chartType = 'pie', $projectID = 0)
  1634. {
  1635. $this->loadModel('report');
  1636. $this->view->charts = array();
  1637. $this->view->datas = array();
  1638. if(!empty($_POST))
  1639. {
  1640. foreach($this->post->charts as $chart)
  1641. {
  1642. $chartFunc = 'getDataOf' . $chart;
  1643. $chartData = $this->story->$chartFunc($storyType);
  1644. $chartOption = $this->lang->story->report->$chart;
  1645. if(!empty($chartType)) $chartOption->type = $chartType;
  1646. $this->story->mergeChartOption($chart);
  1647. $this->view->charts[$chart] = $chartOption;
  1648. $this->view->datas[$chart] = $this->report->computePercent($chartData);
  1649. }
  1650. }
  1651. $this->story->replaceURLang($storyType);
  1652. $product = $this->product->getByID($productID);
  1653. if(strpos('project,execution', $this->app->tab) !== false)
  1654. {
  1655. $project = $this->dao->findByID($projectID)->from(TABLE_PROJECT)->fetch();
  1656. if($project->type == 'project')
  1657. {
  1658. $this->loadModel('project')->setMenu($projectID);
  1659. if($project and $project->model == 'waterfall') unset($this->lang->story->report->charts['storiesPerPlan']);
  1660. $this->view->projectID = $projectID;
  1661. }
  1662. else
  1663. {
  1664. $this->loadModel('execution')->setMenu($projectID);
  1665. $this->view->executionID = $projectID;
  1666. }
  1667. if(!$project->hasProduct)
  1668. {
  1669. unset($this->lang->story->report->charts['storiesPerProduct']);
  1670. if(!$project->multiple) unset($this->lang->story->report->charts['storiesPerPlan']);
  1671. }
  1672. }
  1673. else
  1674. {
  1675. $this->product->setMenu($productID, $branchID);
  1676. }
  1677. $this->view->title = $product->name . $this->lang->hyphen . $this->lang->story->reportChart;
  1678. $this->view->productID = $productID;
  1679. $this->view->branchID = $branchID;
  1680. $this->view->browseType = $browseType;
  1681. $this->view->storyType = $storyType;
  1682. $this->view->moduleID = $moduleID;
  1683. $this->view->chartType = $chartType;
  1684. $this->view->projectID = $projectID;
  1685. $this->view->checkedCharts = $this->post->charts ? implode(',', $this->post->charts) : '';
  1686. $this->display();
  1687. }
  1688. /**
  1689. * 导出需求数据。
  1690. * Get the data of the stories to export.
  1691. *
  1692. * @param int $productID
  1693. * @param string $orderBy
  1694. * @param int $executionID
  1695. * @param string $browseType
  1696. * @param string $storyType requirement|story
  1697. * @access public
  1698. * @return void
  1699. */
  1700. public function export($productID, $orderBy, $executionID = 0, $browseType = '', $storyType = 'story')
  1701. {
  1702. $this->story->replaceURLang($storyType);
  1703. /* format the fields of every story in order to export data. */
  1704. if($_POST)
  1705. {
  1706. $this->loadModel('transfer');
  1707. $postData = form::data($this->config->transfer->form->export)->get();
  1708. $this->session->set("{$storyType}TransferParams", array('productID' => $productID, 'executionID' => $executionID));
  1709. /* 给评审过的人员添加下拉选择,方便再次导入时转换成待评审人员。*/
  1710. /* Add a drop-down selection to the reviewer to facilitate the conversion to the reviewer during import. */
  1711. $this->config->story->dtable->fieldList['reviewedBy']['control'] = 'multiple';
  1712. $this->config->story->dtable->fieldList['reviewedBy']['dataSource'] = array('module' => 'story', 'method' => 'getProductReviewers', 'params' => array('productID' => (int)$productID));
  1713. if($executionID) $this->lang->story->title = $this->lang->story->name;
  1714. /* Create field lists. */
  1715. if(!$productID or $browseType == 'bysearch')
  1716. {
  1717. $this->config->story->dtable->fieldList['branch']['dataSource'] = array('module' => 'branch', 'method' => 'getAllPairs');
  1718. $this->config->story->dtable->fieldList['module']['dataSource']['method'] = 'getAllModulePairs';
  1719. $this->config->story->dtable->fieldList['module']['dataSource']['params'] = 'story';
  1720. $this->config->story->dtable->fieldList['project']['dataSource'] = array('module' => 'project', 'method' => 'getPairsByIdList', 'params' => $executionID);
  1721. $this->config->story->dtable->fieldList['execution']['dataSource'] = array('module' => 'execution', 'method' => 'getPairs', 'params' => $executionID);
  1722. $products = $this->loadModel('product')->getPairs('all', 0, '', 'all');
  1723. $productIdList = array_keys($products);
  1724. $this->config->story->dtable->fieldList['plan']['dataSource'] = array('module' => 'productplan', 'method' => 'getPairs', 'params' => array($productIdList));
  1725. }
  1726. $this->fetch('transfer', 'export', "model=$storyType");
  1727. }
  1728. $project = null;
  1729. $hasBranch = false;
  1730. if($executionID)
  1731. {
  1732. $execution = $this->loadModel('execution')->getByID($executionID);
  1733. $fileName = $execution->name . $this->lang->dash . $this->lang->common->story;
  1734. $project = $execution;
  1735. if($execution->type == 'execution') $project = $this->project->getById($execution->project);
  1736. $this->lang->story->title = $this->lang->story->name;
  1737. $products = $this->loadModel('product')->getProducts($executionID);
  1738. foreach($products as $product)
  1739. {
  1740. if($product->type != 'normal') $hasBranch = true;
  1741. }
  1742. }
  1743. else
  1744. {
  1745. $productName = $this->lang->product->all;
  1746. if($productID)
  1747. {
  1748. $product = $this->product->getById($productID);
  1749. $productName = $product->name;
  1750. if($product->shadow) $project = $this->project->getByShadowProduct($productID);
  1751. if($product->type != 'normal') $hasBranch = true;
  1752. }
  1753. if(isset($this->lang->product->featureBar['browse'][$browseType]))
  1754. {
  1755. $browseType = $this->lang->product->featureBar['browse'][$browseType];
  1756. }
  1757. else
  1758. {
  1759. $browseType = isset($this->lang->product->moreSelects[$browseType]) ? $this->lang->product->moreSelects[$browseType] : '';
  1760. }
  1761. $fileName = $productName . $this->lang->dash . $browseType . $this->lang->common->story;
  1762. }
  1763. /* Unset branch field. */
  1764. if(!$hasBranch) $this->config->story->exportFields = str_replace(', branch', '', $this->config->story->exportFields);
  1765. /* If or vision, unset plan field. */
  1766. if($this->config->vision == 'or') $this->config->story->exportFields = str_replace(', plan,', ',', $this->config->story->exportFields);
  1767. /* Unset product field when in single project. */
  1768. if(isset($project->hasProduct) && !$project->hasProduct)
  1769. {
  1770. $filterFields = array(', product,', ', branch,');
  1771. if($project->model != 'scrum') $filterFields[] = ', plan,';
  1772. $this->config->story->exportFields = str_replace($filterFields, ',', $this->config->story->exportFields);
  1773. }
  1774. /* Append workflow field. */
  1775. if($this->config->edition != 'open')
  1776. {
  1777. $exportFlowFields = $this->loadModel('workflowfield')->getExportFields($storyType);
  1778. foreach($exportFlowFields as $field => $name)
  1779. {
  1780. $this->config->story->exportFields .= ",{$field}";
  1781. $this->lang->story->{$field} = $name;
  1782. }
  1783. }
  1784. $this->view->fileName = $fileName;
  1785. $this->view->allExportFields = $this->config->story->exportFields;
  1786. $this->view->customExport = true;
  1787. $this->view->storyType = $storyType;
  1788. $this->display();
  1789. }
  1790. /**
  1791. * 通过AJAX方式获取用户需要处理的需求。
  1792. * AJAX: get stories of a user in html select.
  1793. *
  1794. * @param int $userID
  1795. * @param string $id the id of the select control.
  1796. * @param int $appendID
  1797. * @param string $storyType
  1798. * @access public
  1799. * @return string
  1800. */
  1801. public function ajaxGetUserStories($userID = 0, $id = '', $appendID = 0, $storyType = 'story')
  1802. {
  1803. if(empty($userID)) $userID = $this->app->user->id;
  1804. $user = $this->loadModel('user')->getById($userID, 'id');
  1805. $stories = $this->story->getUserStoryPairs($user->account, 10, $storyType, '', $appendID);
  1806. $items = array();
  1807. foreach($stories as $storyID => $storyTitle) $items[] = array('text' => $storyTitle, 'value' => $storyID);
  1808. $fieldName = $id ? "stories[$id]" : $storyType;
  1809. return print(json_encode(array('name' => $fieldName, 'items' => $items)));
  1810. }
  1811. /**
  1812. * 用AJAX方式获取需求的指派给。
  1813. * AJAX: get story assignee.
  1814. *
  1815. * @param string $type create|review|change
  1816. * @param int $storyID
  1817. * @param array|string $assignees
  1818. * @access public
  1819. * @return mixed
  1820. */
  1821. public function ajaxGetAssignedTo($type = '', $storyID = 0, $assignees = '')
  1822. {
  1823. $users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
  1824. if($type == 'create')
  1825. {
  1826. $selectUser = is_array($assignees) ? current($assignees) : '';
  1827. return print(html::select('assignedTo', $users, $selectUser, "class='from-control picker-select'"));
  1828. }
  1829. if($type == 'review')
  1830. {
  1831. $moduleName = $this->app->rawModule;
  1832. $story = $this->story->getByID($storyID);
  1833. $reviewers = $this->story->getReviewerPairs($storyID, $story->version);
  1834. $isChanged = $story->changedBy ? true : false;
  1835. $superReviewers = trim(zget($this->config->{$moduleName}, 'superReviewers', ''), ',');
  1836. $isSuperReviewer = strpos(",{$superReviewers},", ",{$this->app->user->account},") !== false;
  1837. if(count($reviewers) == 1)
  1838. {
  1839. $selectUser = $isChanged ? $story->changedBy : $story->openedBy;
  1840. }
  1841. else
  1842. {
  1843. unset($reviewers[$this->app->user->account]);
  1844. foreach($reviewers as $account => $result)
  1845. {
  1846. if(empty($result))
  1847. {
  1848. $selectUser = $account;
  1849. break;
  1850. }
  1851. else
  1852. {
  1853. $selectUser = $isChanged ? $story->changedBy : $story->openedBy;
  1854. }
  1855. }
  1856. }
  1857. if($isSuperReviewer !== false) $selectUser = $isChanged ? $story->changedBy : $story->openedBy;
  1858. return print(html::select('assignedTo', $users, $selectUser, "class='from-control picker-select'"));
  1859. }
  1860. if($type == 'change')
  1861. {
  1862. $selectUser = is_array($assignees) ? current($assignees) : '';
  1863. return print(html::select('assignedTo', $users, $selectUser, "class='from-control picker-select'"));
  1864. }
  1865. return false;
  1866. }
  1867. /**
  1868. * 移除需求的孪生需求。
  1869. * AJAX: Deleted story twin.
  1870. *
  1871. * @access public
  1872. * @return void
  1873. */
  1874. public function ajaxRelieveTwins()
  1875. {
  1876. $twinID = !empty($_POST['twinID']) ? $_POST['twinID'] : 0;
  1877. $story = $this->story->getByID((int)$twinID);
  1878. $twins = explode(',', trim($story->twins, ','));
  1879. if(empty($story->twins)) return $this->send(array('result' => 'fail'));
  1880. $this->story->relieveTwins($story->product, (int)$twinID);
  1881. if(!dao::isError()) $this->loadModel('action')->create('story', (int)$twinID, 'relieved');
  1882. return $this->send(array('result' => 'success', 'twinsCount' => count($twins)-1));
  1883. }
  1884. /**
  1885. * 获取需求ID和需求概要信息键值对的html信息。批量关闭时如果原因是重复了,需要选择重复的需求。
  1886. * AJAX: Get the html with story pairs to choose the duplicated story.
  1887. *
  1888. * @param int $storyID
  1889. * @access public
  1890. * @return void
  1891. */
  1892. public function ajaxGetDuplicatedStory($storyID)
  1893. {
  1894. $story = $this->story->getByID($storyID);
  1895. $stories = $this->story->getProductStoryPairs($story->product, $story->branch, 0, 'all', 'id_desc', 0, '', $story->type);
  1896. $items = array();
  1897. foreach($stories as $id => $storyTitle)
  1898. {
  1899. if(empty($id)) continue;
  1900. if($id == $storyID) continue;
  1901. $items[] = array('text' => $storyTitle, 'value' => $id, 'keys' => $storyTitle);
  1902. }
  1903. return print(json_encode($items));
  1904. }
  1905. /**
  1906. * 创建代码分支。
  1907. * Create repo branch.
  1908. *
  1909. * @param int $storyID
  1910. * @param int $repoID
  1911. * @access public
  1912. * @return void
  1913. */
  1914. public function createBranch($storyID, $repoID = 0)
  1915. {
  1916. return print($this->fetch('repo', 'createBranch', array('objectID' => $storyID, 'repoID' => $repoID)));
  1917. }
  1918. /**
  1919. * 取消代码分支的关联。
  1920. * Unlink code branch.
  1921. *
  1922. * @access public
  1923. * @return void
  1924. */
  1925. public function unlinkBranch()
  1926. {
  1927. return print($this->fetch('repo', 'unlinkBranch'));
  1928. }
  1929. }