zen.php 92 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169
  1. <?php
  2. /**
  3. * The zen file of story module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Wang Yidong<yidong@easycorp.ltd>
  8. * @package story
  9. * @link https://www.zentao.net
  10. */
  11. class storyZen extends story
  12. {
  13. /**
  14. * 设置创建需求页面的导航。
  15. * Set menu for create story.
  16. *
  17. * @param int $productID
  18. * @param int $objectID
  19. * @param string $extra
  20. * @access protected
  21. * @return int[]
  22. */
  23. protected function setMenuForCreate($productID, $objectID, $extra = '')
  24. {
  25. /* Get product id according to the project id when lite vision todo transfer story */
  26. if($this->config->vision == 'lite' && $productID == 0)
  27. {
  28. $products = $this->product->getProductPairsByProject($objectID);
  29. if(!empty($products)) $productID = key($products);
  30. }
  31. /* Get objectID by tab. */
  32. if(empty($objectID))
  33. {
  34. if($this->app->tab == 'project') $objectID = (int)$this->session->project;
  35. if($this->app->tab == 'execution') $objectID = (int)$this->session->execution;
  36. }
  37. /* Set menu by tab. */
  38. if($this->app->tab == 'product')
  39. {
  40. $extra = str_replace(array(',', ' '), array('&', ''), $extra);
  41. parse_str($extra, $output);
  42. if(!empty($output['from']) && $output['from'] == 'global')
  43. {
  44. $product = $this->product->getById($productID);
  45. if(!$product || $product->deleted || $product->shadow)
  46. {
  47. $newProduct = $this->product->getOrderedProducts('noclosed');
  48. if(!empty($newProduct)) $productID = (int) key($newProduct);
  49. }
  50. }
  51. $this->product->setMenu($productID);
  52. }
  53. if($this->app->tab == 'execution')
  54. {
  55. $this->execution->setMenu($objectID);
  56. $this->view->executionID = $objectID;
  57. }
  58. if($this->app->tab == 'project')
  59. {
  60. $projectID = $objectID;
  61. if(!$this->session->multiple)
  62. {
  63. $projectID = $this->session->project;
  64. $objectID = $this->execution->getNoMultipleID($projectID);
  65. }
  66. $projects = $this->project->getPairsByProgram();
  67. $projectID = $this->project->checkAccess($projectID, $projects);
  68. $this->view->projectID = $projectID;
  69. $this->project->setMenu($projectID);
  70. }
  71. return array($productID, $objectID);
  72. }
  73. /**
  74. * 为批量创建需求页面设置导航。
  75. * Set menu for batch create.
  76. *
  77. * @param int $productID
  78. * @param string $branch
  79. * @param int $executionID
  80. * @param string $extra
  81. * @param string $storyType
  82. * @access protected
  83. * @return void
  84. */
  85. protected function setMenuForBatchCreate($productID, $branch = '', $executionID = 0, $extra = '', $storyType = 'story')
  86. {
  87. $this->view->hiddenProduct = false;
  88. $this->view->hiddenPlan = false;
  89. /* Set menu. */
  90. if($this->app->tab == 'project' and $this->config->vision == 'lite')
  91. {
  92. $this->project->setMenu($this->session->project);
  93. $this->view->projectID = $this->session->project;
  94. return;
  95. }
  96. if(empty($executionID))
  97. {
  98. if($this->app->tab == 'project')
  99. {
  100. $this->project->setMenu($this->session->project);
  101. $this->view->projectID = $this->session->project;
  102. }
  103. else
  104. {
  105. $this->product->setMenu($productID, $branch);
  106. }
  107. return;
  108. }
  109. $execution = $this->dao->findById((int)$executionID)->from(TABLE_EXECUTION)->fetch();
  110. $this->view->execution = $execution;
  111. if($execution->type == 'project')
  112. {
  113. $this->project->setMenu($executionID);
  114. $this->lang->navGroup->story = 'project';
  115. $model = $execution->model == 'waterfallplus' ? 'waterfall' : $execution->model;
  116. if($execution->model == 'agileplus') $model = 'scrum';
  117. $this->lang->product->menu = $this->lang->{$model}->menu;
  118. $this->view->projectID = $executionID;
  119. }
  120. else
  121. {
  122. $this->execution->setMenu($executionID);
  123. $this->lang->navGroup->story = 'execution';
  124. if($execution->type == 'kanban')
  125. {
  126. $this->loadModel('kanban');
  127. $output = $this->story->parseExtra($extra);
  128. $regionPairs = $this->kanban->getRegionPairs($executionID, 0, 'execution');
  129. $regionID = !empty($output['regionID']) ? $output['regionID'] : key($regionPairs);
  130. $lanePairs = $this->kanban->getLanePairsByRegion((int)$regionID, $storyType);
  131. $laneID = !empty($output['laneID']) ? $output['laneID'] : key($lanePairs);
  132. $this->view->regionID = $regionID;
  133. $this->view->laneID = $laneID;
  134. $this->view->regionPairs = $regionPairs;
  135. $this->view->lanePairs = $lanePairs;
  136. }
  137. if($this->app->tab == 'execution') $this->view->executionID = $executionID;
  138. if($this->app->tab == 'project') $this->view->projectID = $executionID;
  139. }
  140. if($this->app->tab != 'project' && $this->app->tab != 'execution') return;
  141. /* Hidden some fields of projects without products. */
  142. $project = $this->dao->findById($executionID)->from(TABLE_PROJECT)->fetch();
  143. if($project->project) $project = $this->dao->findById((int)$project->project)->from(TABLE_PROJECT)->fetch();
  144. if($project->hasProduct) return;
  145. $this->view->hiddenProduct = true;
  146. if($project->model !== 'scrum') $this->view->hiddenPlan = true;
  147. if(!$project->multiple) $this->view->hiddenPlan = true;
  148. }
  149. /**
  150. * 为批量编辑页面设置导航。
  151. * Set menu for batch edit page.
  152. *
  153. * @param int $productID
  154. * @param int $executionID
  155. * @param string $storyType story|requirement
  156. * @param string $from work|contribute
  157. * @access protected
  158. * @return void
  159. */
  160. protected function setMenuForBatchEdit($productID, $executionID = 0, $storyType = 'story', $from = '')
  161. {
  162. $this->view->hiddenPlan = false;
  163. if($this->app->tab == 'product')
  164. {
  165. $this->product->setMenu($productID);
  166. return;
  167. }
  168. if($this->app->tab == 'execution')
  169. {
  170. $this->execution->setMenu($executionID);
  171. $this->view->executionID = $this->session->execution;
  172. return;
  173. }
  174. if($this->app->tab == 'qa')
  175. {
  176. $this->loadModel('qa')->setMenu($productID);
  177. return;
  178. }
  179. if($this->app->tab == 'my')
  180. {
  181. $this->loadModel('my');
  182. if($from == 'work' || $from == 'contribute')
  183. {
  184. $this->lang->my->menu->{$from}['subModule'] = $storyType;
  185. $this->lang->my->menu->{$from}['subMenu']->{$storyType}['subModule'] = 'story';
  186. }
  187. return;
  188. }
  189. if($this->app->tab == 'project')
  190. {
  191. $project = $this->dao->findByID($executionID)->from(TABLE_PROJECT)->fetch();
  192. if($project->type == 'project')
  193. {
  194. if(!($project->model == 'scrum' and !$project->hasProduct and $project->multiple)) $this->view->hiddenPlan = true;
  195. $this->project->setMenu($executionID);
  196. $this->view->projectID = $this->session->project;
  197. }
  198. else
  199. {
  200. if(!$project->hasProduct and !$project->multiple) $this->view->hiddenPlan = true;
  201. $this->execution->setMenu($executionID);
  202. $this->view->projectID = $this->session->execution;
  203. }
  204. }
  205. }
  206. /**
  207. * 设置批量关闭需求页面的导航。
  208. * Set menu for batch close.
  209. *
  210. * @param int $productID
  211. * @param int $executionID
  212. * @param string $storyType story|requirement
  213. * @param string $from work|contribute
  214. * @access protected
  215. * @return void
  216. */
  217. protected function setMenuForBatchClose($productID, $executionID = 0, $storyType = 'story', $from = '')
  218. {
  219. /* The stories of a product. */
  220. if($this->app->tab == 'product' && $productID)
  221. {
  222. $this->product->setMenu($productID);
  223. $product = $this->product->getByID($productID);
  224. $this->view->title = $product->name . $this->lang->hyphen . $this->lang->story->batchClose;
  225. }
  226. /* The stories of a execution. */
  227. elseif($this->app->tab == 'execution' && $executionID)
  228. {
  229. $this->lang->story->menu = $this->lang->execution->menu;
  230. $this->lang->story->menuOrder = $this->lang->execution->menuOrder;
  231. $this->execution->setMenu($executionID);
  232. $execution = $this->execution->getByID($executionID);
  233. $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->story->batchClose;
  234. $this->view->executionID = $executionID;
  235. }
  236. elseif($this->app->tab == 'project')
  237. {
  238. $this->project->setMenu(!empty($this->session->project) ? $this->session->project : $executionID);
  239. $this->view->title = $this->lang->story->batchClose;
  240. $this->view->projectID = $this->session->project;
  241. }
  242. else
  243. {
  244. if($from == 'work' || $from == 'contribute')
  245. {
  246. $this->lang->my->menu->{$from}['subModule'] = $storyType;
  247. $this->lang->my->menu->{$from}['subMenu']->{$storyType}['subModule'] = 'story';
  248. }
  249. $this->lang->story->menu = $this->lang->my->menu;
  250. $this->lang->story->menuOrder = $this->lang->my->menuOrder;
  251. $this->view->title = $this->lang->story->batchClose;
  252. }
  253. }
  254. /**
  255. * 如果是看板执行,设置界面中要用到关于看板的视图变量。
  256. * Set view vars for kanban.
  257. *
  258. * @param int $objectID
  259. * @param array $kanbanSetting
  260. * @param string $storyType
  261. * @access protected
  262. * @return void
  263. */
  264. protected function setViewVarsForKanban($objectID, $kanbanSetting, $storyType = 'story')
  265. {
  266. if(empty($objectID)) return;
  267. $execution = $this->dao->findById($objectID)->from(TABLE_EXECUTION)->fetch();
  268. if($execution->type != 'kanban') return ;
  269. /* 如果是看板执行,设置看板的view变量。 */
  270. $regionPairs = $this->loadModel('kanban')->getRegionPairs($execution->id, 0, 'execution');
  271. $regionID = !empty($kanbanSetting['regionID']) ? $kanbanSetting['regionID'] : key($regionPairs);
  272. $lanePairs = $this->kanban->getLanePairsByRegion((int)$regionID, $storyType);
  273. $laneID = !empty($kanbanSetting['laneID']) ? $kanbanSetting['laneID'] : key($lanePairs);
  274. $this->view->executionType = 'kanban';
  275. $this->config->story->form->create['region']['options'] = $regionPairs;
  276. $this->config->story->form->create['region']['default'] = $regionID;
  277. $this->config->story->form->create['region']['title'] = $this->lang->kanbancard->region;
  278. $this->config->story->form->create['lane']['options'] = $lanePairs;
  279. $this->config->story->form->create['lane']['default'] = $laneID;
  280. $this->config->story->form->create['lane']['title'] = $this->lang->kanbancard->lane;
  281. }
  282. /**
  283. * 初始化创建需求的一些字段的数据。
  284. * Init story for create.
  285. *
  286. * @param int $planID
  287. * @param int $storyID
  288. * @param int $bugID
  289. * @param int $todoID
  290. * @param string $extra feedback扩展使用
  291. * @access public
  292. * @return object
  293. */
  294. public function initStoryForCreate($planID, $storyID, $bugID, $todoID, $extra = '')
  295. {
  296. $initStory = new stdclass();
  297. $initStory->source = '';
  298. $initStory->sourceNote = '';
  299. $initStory->pri = 3;
  300. $initStory->estimate = '';
  301. $initStory->title = '';
  302. $initStory->spec = '';
  303. $initStory->verify = '';
  304. $initStory->keywords = '';
  305. $initStory->mailto = '';
  306. $initStory->color = '';
  307. $initStory->plan = $planID;
  308. if($storyID > 0) $initStory = $this->getInitStoryByStory($storyID, $initStory);
  309. if($bugID > 0) $initStory = $this->getInitStoryByBug($bugID, $initStory);
  310. if($todoID > 0) $initStory = $this->getInitStoryByTodo($todoID, $initStory);
  311. return $initStory;
  312. }
  313. /**
  314. * 根据复制的需求,初始化创建需求的一些字段数据。
  315. * Get init story by copied story.
  316. *
  317. * @param int $storyID
  318. * @param object $initStory
  319. * @access protected
  320. * @return object
  321. */
  322. protected function getInitStoryByStory($storyID, $initStory)
  323. {
  324. if(empty($storyID)) return $initStory;
  325. $story = $this->story->getByID($storyID);
  326. $initStory->plan = $story->plan;
  327. $initStory->module = $story->module;
  328. $initStory->source = $story->source;
  329. $initStory->sourceNote = $story->sourceNote;
  330. $initStory->color = $story->color;
  331. $initStory->pri = $story->pri;
  332. $initStory->estimate = $story->estimate;
  333. $initStory->title = $story->title;
  334. $initStory->spec = $story->spec;
  335. $initStory->grade = $story->grade;
  336. $initStory->verify = $story->verify;
  337. $initStory->keywords = $story->keywords;
  338. $initStory->mailto = $story->mailto;
  339. $initStory->category = $story->category;
  340. $initStory->feedbackBy = $story->feedbackBy;
  341. $initStory->notifyEmail = $story->notifyEmail;
  342. $initStory->parent = $story->parent;
  343. $initStory->files = $story->files;
  344. if($this->config->edition != 'open')
  345. {
  346. $extendFields = $this->loadModel('flow')->getExtendFields($story->type, 'create');
  347. foreach($extendFields as $field) $initStory->{$field->field} = $story->{$field->field};
  348. }
  349. foreach($initStory->files as $file)
  350. {
  351. $file->name = $file->title;
  352. $file->url = $this->createLink('file', 'download', "fileID={$file->id}");
  353. }
  354. return $initStory;
  355. }
  356. /**
  357. * 根据来源Bug,初始化创建需求的一些字段数据。
  358. * Get init story by bug.
  359. *
  360. * @param int $bugID
  361. * @param object $initStory
  362. * @access protected
  363. * @return object
  364. */
  365. protected function getInitStoryByBug($bugID, $initStory)
  366. {
  367. if(empty($bugID)) return $initStory;
  368. $bug = $this->loadModel('bug')->getByID($bugID);
  369. $initStory->source = 'bug';
  370. $initStory->title = $bug->title;
  371. $initStory->keywords = $bug->keywords;
  372. $initStory->spec = $bug->steps;
  373. $initStory->pri = !empty($bug->pri) ? $bug->pri : '3';
  374. $initStory->mailto = $bug->mailto;
  375. if($bug->mailto and strpos($bug->mailto, $bug->openedBy) === false) $initStory->mailto = $bug->mailto . $bug->openedBy . ',';
  376. if(!empty($bug->files)) $initStory->files = $bug->files;
  377. return $initStory;
  378. }
  379. /**
  380. * 根据来源待办,初始化创建产品的一些字段数据。
  381. * Get init story by todo.
  382. *
  383. * @param int $todoID
  384. * @param object $initStory
  385. * @access protected
  386. * @return object
  387. */
  388. protected function getInitStoryByTodo($todoID, $initStory)
  389. {
  390. if(empty($todoID)) return $initStory;
  391. $todo = $this->loadModel('todo')->getByID($todoID);
  392. $initStory->source = 'todo';
  393. $initStory->title = $todo->name;
  394. $initStory->spec = $todo->desc;
  395. $initStory->pri = $todo->pri;
  396. return $initStory;
  397. }
  398. /**
  399. * 获取产品和分支列表。
  400. * Get products and branches for create.
  401. *
  402. * @param int $productID
  403. * @param int $objectID
  404. * @access protected
  405. * @return array
  406. */
  407. protected function getProductsAndBranchesForCreate($productID, $objectID)
  408. {
  409. $products = array();
  410. $branches = array();
  411. if($objectID != 0)
  412. {
  413. $onlyNoClosed = empty($this->config->CRProduct) ? 'noclosed' : '';
  414. $products = $this->product->getProductPairsByProject($objectID, $onlyNoClosed);
  415. $productID = (!empty($productID) && isset($products[$productID])) ? $productID : key($products);
  416. $product = $this->product->getById((int)$productID);
  417. if($product && $product->type != 'normal')
  418. {
  419. $productBranches = $this->loadModel('execution')->getBranchByProduct(array($productID), $objectID, 'noclosed|withMain');
  420. if(isset($productBranches[$productID])) $branches = $productBranches[$productID];
  421. }
  422. }
  423. else
  424. {
  425. $productList = $this->product->getOrderedProducts('noclosed');
  426. foreach($productList as $product) $products[$product->id] = $product->name;
  427. $product = $this->product->getById($productID);
  428. if(!isset($products[$product->id])) $products[$product->id] = $product->name;
  429. if($product->type != 'normal') $branches = $this->loadModel('branch')->getPairs($productID, 'active');
  430. }
  431. $this->product->checkAccess((int)$productID, $products);
  432. return array($products, $branches);
  433. }
  434. /**
  435. * 获取产品列表,并排序,将我负责的产品排前面。
  436. * Get products for edit.
  437. *
  438. * @access protected
  439. * @return array
  440. */
  441. protected function getProductsForEdit()
  442. {
  443. $account = $this->app->user->account;
  444. $myProducts = array();
  445. $othersProducts = array();
  446. $products = $this->loadModel('product')->getList();
  447. foreach($products as $product)
  448. {
  449. if($product->status != 'closed' and $product->PO == $account) $myProducts[$product->id] = $product->name;
  450. if($product->status != 'closed' and $product->PO != $account) $othersProducts[$product->id] = $product->name;
  451. }
  452. $products = $myProducts + $othersProducts;
  453. return $products;
  454. }
  455. /**
  456. * 获取创建需求的表单字段。
  457. * Get form fields for create
  458. *
  459. * @param int $productID
  460. * @param string $branch
  461. * @param int $objectID
  462. * @param object $initStory
  463. * @param string $storyType
  464. * @access protected
  465. * @return array
  466. */
  467. protected function getFormFieldsForCreate($productID, $branch, $objectID, $initStory, $storyType = 'story')
  468. {
  469. $account = $this->app->user->account;
  470. $fields = $this->config->story->form->create;
  471. /* 准备数据。*/
  472. list($products, $branches) = $this->getProductsAndBranchesForCreate($productID, $objectID);
  473. if($objectID)
  474. {
  475. $branch = is_array($branches) && !empty($branches) ? (string)key($branches) : $branch;
  476. $productID = (!empty($productID) && isset($products[$productID])) ? $productID : key($products);
  477. }
  478. $branch = strpos($branch, ',') !== false ? current(explode(',', $branch)) : $branch;
  479. $product = $this->product->getByID($productID);
  480. $users = $this->user->getPairs('pdfirst|noclosed|nodeleted');
  481. $stories = $this->story->getParentStoryPairs($productID, '', $storyType);
  482. $grades = $this->story->getGradePairs($storyType);
  483. $plans = $this->loadModel('productplan')->getPairs($productID, $branch == 0 ? '' : $branch, 'unexpired|noclosed', true);
  484. $plans = array_map(function($planName){return str_replace(FUTURE_TIME, $this->lang->story->undetermined, $planName);}, $plans);
  485. $forceReview = $this->story->checkForceReview($storyType);
  486. $needReview = ($account == $product->PO || $objectID > 0 || $this->config->{$storyType}->needReview == 0 || !$forceReview);
  487. $reviewers = $this->story->getProductReviewers($productID);
  488. $requiredFields = $this->config->{$storyType}->create->requiredFields;
  489. /* 追加字段的name、title属性,展开user数据。 */
  490. foreach($fields as $field => $attr)
  491. {
  492. if(isset($attr['options']) and $attr['options'] == 'users') $fields[$field]['options'] = $users;
  493. if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field;
  494. if(!isset($fields[$field]['title'])) $fields[$field]['title'] = zget($this->lang->story, $field);
  495. if(strpos($requiredFields, $field) !== false) $fields[$field]['required'] = true;
  496. }
  497. /* 设置下拉菜单内容。 */
  498. $fields['product']['options'] = $products;
  499. $fields['branch']['options'] = $branches;
  500. $fields['branches']['options'] = $branches;
  501. $fields['plan']['options'] = $plans;
  502. $fields['plans']['options'] = $plans;
  503. $fields['grade']['options'] = $grades;
  504. $fields['grade']['default'] = current($grades);
  505. $fields['reviewer']['options'] = $reviewers;
  506. $fields['parent']['options'] = array_filter($stories);
  507. /* 设置默认值。 */
  508. foreach($initStory as $field => $defaultValue)
  509. {
  510. if(isset($fields[$field])) $fields[$field]['default'] = $defaultValue;
  511. }
  512. if(empty($fields['product']['default'])) $fields['product']['default'] = $productID;
  513. if(empty($fields['branch']['default'])) $fields['branch']['default'] = (int)$branch;
  514. if(empty($fields['branches']['default'])) $fields['branches']['default'] = (int)$branch;
  515. if(empty($fields['plans']['default'])) $fields['plans']['default'] = zget($initStory, 'plan', 0);
  516. if(empty($needReview)) $fields['reviewer']['default'] = $product->PO;
  517. if($forceReview) $fields['reviewer']['required'] = true;
  518. /* 删除不需要的字段。 */
  519. if(empty($branches)) unset($fields['branch'], $fields['branches'], $fields['modules'], $fields['plans']);
  520. if($this->config->vision == 'or') unset($fields['plan']);
  521. $this->view->productID = $productID;
  522. $this->view->product = $product;
  523. $this->view->branch = $branch;
  524. $this->view->branches = $branches;
  525. $this->view->objectID = $objectID;
  526. $this->view->forceReview = $forceReview;
  527. $this->view->needReview = $needReview;
  528. $this->view->gradeRule = $this->config->{$storyType}->gradeRule;
  529. return $fields;
  530. }
  531. /**
  532. * 为编辑页面获取字段配置。
  533. * Get form fields for edit.
  534. *
  535. * @param int $storyID
  536. * @access protected
  537. * @return array
  538. */
  539. protected function getFormFieldsForEdit($storyID)
  540. {
  541. $fields = $this->config->story->form->edit;
  542. /* 准备数据。*/
  543. $story = $this->view->story;
  544. $parent = $this->story->fetchByID($story->parent);
  545. $grades = $this->story->getGradeOptions($parent, $story->type, (array)$story->grade);
  546. $product = $this->view->product;
  547. $users = $this->loadModel('user')->getPairs('pofirst|nodeleted|noclosed', "$story->assignedTo,$story->openedBy,$story->closedBy");
  548. $stories = $this->story->getParentStoryPairs($story->product, $story->parent, $story->type, $storyID);
  549. $plans = $this->loadModel('productplan')->getPairs($story->product, $story->branch == 0 ? 'all' : $story->branch, '', true);
  550. $reviewerList = $this->story->getReviewerPairs($story->id, $story->version);
  551. $reviewers = $product->reviewer;
  552. if(!$reviewers and $product->acl != 'open') $reviewers = $this->user->getProductViewListUsers($product);
  553. $reviewers = $this->user->getPairs('noclosed|nodeleted', array_keys($reviewerList), 0, $reviewers);
  554. $products = $this->getProductsForEdit();
  555. if($this->app->tab == 'project' or $this->app->tab == 'execution')
  556. {
  557. $objectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution;
  558. $products = $this->product->getProductPairsByProject($objectID);
  559. /* 如果是无产品项目,则所属产品显示需求本身的所属产品。*/
  560. /* If there is no product item, the product shows the product of story itself.*/
  561. $object = $this->loadModel('execution')->getByID($objectID);
  562. if(!$object->hasProduct) $products = array($product->id => $product->name);
  563. $this->view->objectID = $objectID;
  564. }
  565. $branches = $this->loadModel('branch')->getList($product->id, isset($objectID) ? $objectID : 0, 'all');
  566. $branchTagOption = array();
  567. foreach($branches as $branchInfo) $branchTagOption[$branchInfo->id] = $branchInfo->name . ($branchInfo->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : '');
  568. $moduleOptionMenu = $this->loadModel('tree')->getOptionMenu($story->product, 'story', 0, (string)$story->branch);
  569. if($product->type == 'normal' and !empty($story->branch)) $moduleOptionMenu += $this->tree->getModulesName(array($story->module));
  570. $storyBranch = $story->branch > 0 ? $story->branch : '0';
  571. $branch = $product->type == 'branch' ? $storyBranch : 'all';
  572. $productStories = $this->story->getProductStoryPairs($story->product, $branch, 0, 'all', 'id_desc', 0, '', $story->type);
  573. /* 追加字段的name、title属性,展开user数据。 */
  574. foreach($fields as $field => $attr)
  575. {
  576. if(isset($attr['options']) and $attr['options'] == 'users') $fields[$field]['options'] = $users;
  577. if(isset($story->$field)) $fields[$field]['default'] = $story->$field;
  578. if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field;
  579. if(!isset($fields[$field]['title'])) $fields[$field]['title'] = zget($this->lang->story, $field);
  580. }
  581. /* 设置下拉菜单内容。 */
  582. if(isset($stories[$storyID])) unset($stories[$storyID]);
  583. $fields['product']['options'] = $products;
  584. $fields['branch']['options'] = $branchTagOption;
  585. $fields['module']['options'] = $moduleOptionMenu;
  586. $fields['plan']['options'] = $plans;
  587. $fields['reviewer']['options'] = $reviewers;
  588. $fields['parent']['options'] = array_filter($stories);
  589. $fields['grade']['options'] = $grades;
  590. $fields['duplicateStory']['options'] = $productStories;
  591. $fields['assignedTo']['options'] += array('closed' => 'Closed');
  592. $fields['stage']['options'] = $this->lang->{$story->type}->stageList;
  593. /* 设置默认值。 */
  594. if(empty($fields['reviewer']['default'])) $fields['reviewer']['default'] = implode(',', array_keys($reviewerList));
  595. if($story->type == 'story') unset($fields['stage']['options']['inroadmap'], $fields['stage']['options']['incharter']);
  596. $this->view->users = $users;
  597. $this->view->storyReviewers = array_keys($reviewerList);
  598. $this->view->gradeRule = $this->config->{$story->type}->gradeRule;
  599. return $fields;
  600. }
  601. /**
  602. * 获取批量创建需求的表单字段。
  603. * Get form fields for batch create.
  604. *
  605. * @param int $productID
  606. * @param string $branch
  607. * @param int $executionID
  608. * @access protected
  609. * @return array
  610. * @param string $storyType
  611. */
  612. protected function getFormFieldsForBatchCreate($productID, $branch, $executionID = 0, $storyType = '')
  613. {
  614. $product = $this->loadModel('product')->getByID($productID);
  615. $fields = $this->config->story->form->batchCreate;
  616. foreach(explode(',', trim($this->config->{$storyType}->create->requiredFields, ',')) as $field) $fields[$field]['required'] = true;
  617. if($executionID)
  618. {
  619. $productBranches = $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct(array($productID), $executionID, 'noclosed|withMain') : array();
  620. $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array();
  621. $branch = empty($branches) ? '' : key($branches);
  622. if(isset($this->view->execution->type) && $this->view->execution->type == 'kanban')
  623. {
  624. $fields['region']['options'] = zget($this->view, 'regionPairs', array());
  625. $fields['lane']['options'] = zget($this->view, 'lanePairs', array());
  626. $fields['region']['default'] = zget($this->view, 'regionID', 0);
  627. $fields['lane']['default'] = zget($this->view, 'laneID', 0);
  628. }
  629. }
  630. else
  631. {
  632. $branches = $product->type != 'normal' ? $this->loadModel('branch')->getPairs($productID, 'active') : array();
  633. }
  634. $branch = current(explode(',', (string)$branch));
  635. $modules = $this->tree->getOptionMenu($productID, 'story', 0, $branch === 'all' ? '0' : $branch);
  636. $plans = $this->loadModel('productplan')->getPairs($productID, ($branch === 'all' or empty($branch)) ? '' : $branch, 'unexpired|noclosed', true);
  637. $reviewers = $this->story->getProductReviewers($productID);
  638. $users = $this->user->getPairs('pdfirst|noclosed|nodeleted');
  639. $stories = $this->story->getParentStoryPairs($productID, '', $storyType);
  640. $grades = $this->story->getGradePairs($storyType);
  641. /* 追加字段的label属性。 */
  642. foreach($fields as $field => $attr)
  643. {
  644. if(!isset($attr['label']))
  645. {
  646. if(strpos(',region,lane,', ",$field,") !== false)
  647. {
  648. $this->app->loadLang('kanban');
  649. $fields[$field]['label'] = zget($this->lang->kanbancard, $field);
  650. continue;
  651. }
  652. $fields[$field]['label'] = zget($this->lang->story, $field);
  653. }
  654. }
  655. /* 设置下拉菜单内容。 */
  656. $fields['branch']['options'] = $branches;
  657. if($product->type == 'normal') unset($fields['branch']);
  658. $fields['grade']['default'] = key($grades);
  659. $fields['module']['options'] = $modules;
  660. $fields['grade']['options'] = $grades;
  661. $fields['reviewer']['options'] = $reviewers;
  662. $fields['assignedTo']['options'] = $users;
  663. $fields['mailto']['options'] = $users;
  664. $fields['parent']['options'] = array_filter($stories);
  665. if(!empty($fields['plan'])) $fields['plan']['options'] = $plans;
  666. if($this->story->checkForceReview($storyType)) $fields['reviewer']['required'] = true;
  667. if(empty($branches)) unset($fields['branch']);
  668. if(!empty($this->view->hiddenPlan)) unset($fields['plan']);
  669. if($this->config->edition != 'ipd' && $storyType == 'epic') $fields['parent']['hidden'] = true;
  670. $this->view->branchID = $branch;
  671. $this->view->gradeRule = $this->config->{$storyType}->gradeRule;
  672. return $fields;
  673. }
  674. /**
  675. * 获取变更需求的表单字段。
  676. * Get form fields for change story.
  677. *
  678. * @param int $storyID
  679. * @access protected
  680. * @return array
  681. */
  682. protected function getFormFieldsForChange($storyID)
  683. {
  684. $story = $this->view->story;
  685. $fields = $this->config->story->form->change;
  686. unset($fields['relievedTwins']);
  687. unset($fields['deleteFiles']);
  688. unset($fields['renameFiles']);
  689. unset($fields['docs']);
  690. unset($fields['oldDocs']);
  691. unset($fields['docVersions']);
  692. foreach(array_keys($fields) as $field)
  693. {
  694. if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field;
  695. if(!isset($fields[$field]['title'])) $fields[$field]['title'] = $field == 'reviewer' ? $this->lang->story->reviewers : zget($this->lang->story, $field);
  696. }
  697. $reviewerAndResultPairs = $this->story->getReviewerPairs($storyID, $story->version);
  698. $reviewer = array_keys($reviewerAndResultPairs);
  699. $fields['reviewer']['options'] = $this->story->getProductReviewers($story->product, $reviewer);
  700. $fields['reviewer']['default'] = $reviewer;
  701. $fields['title']['default'] = $story->title;
  702. $fields['color']['default'] = $story->color;
  703. $fields['spec']['default'] = $story->spec;
  704. $fields['verify']['default'] = $story->verify;
  705. $fields['status']['default'] = $story->status;
  706. $forceReview = $this->story->checkForceReview($story->type);
  707. if($forceReview) $fields['reviewer']['required'] = true;
  708. $this->view->forceReview = $forceReview;
  709. $this->view->needReview = ($this->app->user->account == $this->view->product->PO || $this->config->{$story->type}->needReview == 0 || !$forceReview) && empty($reviewer);
  710. $fields['comment'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '', 'name' => 'comment', 'title' => $this->lang->comment);
  711. $requiredFields = $this->config->{$story->type}->change->requiredFields;
  712. if(strpos(",{$requiredFields},", ',comment,') !== false) $fields['comment']['required'] = true;
  713. if(strpos(",{$requiredFields},", ',spec,') !== false) $fields['spec']['required'] = true;
  714. if(strpos(",{$requiredFields},", ',verify,') !== false) $fields['verify']['required'] = true;
  715. return $fields;
  716. }
  717. /**
  718. * 获取评审需求的表单字段。
  719. * Get form fields for review story.
  720. *
  721. * @param int $storyID
  722. * @access protected
  723. * @return array
  724. */
  725. protected function getFormFieldsForReview($storyID)
  726. {
  727. $story = $this->view->story;
  728. $fields = $this->config->story->form->review;
  729. $users = $this->loadModel('user')->getPairs('nodeleted|noclosed', "$story->lastEditedBy,$story->openedBy");
  730. $resultList = $this->lang->{$story->type}->reviewResultList;
  731. if($story->status == 'reviewing')
  732. {
  733. if($story->version == 1) unset($resultList['revert']);
  734. if($story->version > 1) unset($resultList['reject']);
  735. }
  736. foreach($fields as $field => $attr)
  737. {
  738. if(isset($attr['options']) and $attr['options'] == 'users') $fields[$field]['options'] = $users;
  739. if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field;
  740. if(!isset($fields[$field]['title'])) $fields[$field]['title'] = zget($this->lang->story, $field);
  741. }
  742. $fields['result']['options'] = $resultList;
  743. $fields['reviewedDate']['default'] = helper::now();
  744. $fields['assignedTo']['default'] = $story->assignedTo;
  745. $fields['pri']['default'] = $story->pri;
  746. $fields['estimate']['default'] = $story->estimate ? $story->estimate : 0;
  747. $fields['status']['default'] = $story->status;
  748. $fields['closedReason']['required'] = true;
  749. $fields['duplicateStory']['required'] = true;
  750. $fields['comment'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '', 'name' => 'comment', 'title' => $this->lang->comment, 'width' => 'full');
  751. if(strpos($this->config->{$story->type}->review->requiredFields, 'reviewedDate') !== false) $fields['reviewedDate']['required'] = true;
  752. if(strpos($this->config->{$story->type}->review->requiredFields, 'comment') !== false) $fields['comment']['required'] = true;
  753. $this->view->users = $users;
  754. return $fields;
  755. }
  756. /**
  757. * Set form options for batch edit.
  758. *
  759. * @param int $productID
  760. * @param int $executionID
  761. * @param array $stories
  762. * @access protected
  763. * @return void
  764. */
  765. protected function setFormOptionsForBatchEdit($productID, $executionID, $stories)
  766. {
  767. $this->loadModel('product');
  768. $this->loadModel('tree');
  769. if($productID and !$executionID)
  770. {
  771. $product = $this->product->getByID($productID);
  772. $options = $this->getFormOptionsForSingleProduct($productID, $executionID, $product);
  773. $branchProduct = $options['branchProduct'];
  774. $modules = $options['modules'];
  775. $branchTagOption = $options['branchTagOption'];
  776. $products = $options['products'];
  777. $plans = $options['plans'];
  778. }
  779. else
  780. {
  781. /* Get product id list by the stories. */
  782. $branchProduct = false;
  783. $branchTagOption = array();
  784. $plans = array();
  785. $modules = array();
  786. $productIdList = array();
  787. foreach($stories as $story) $productIdList[$story->product] = $story->product;
  788. $products = $this->product->getByIdList($productIdList);
  789. foreach($products as $storyProduct)
  790. {
  791. $options = $this->getFormOptionsForSingleProduct($storyProduct->id, $executionID, $storyProduct);
  792. if($options['branchProduct'] && !$branchProduct) $branchProduct = true;
  793. if($options['branchProduct']) $branchTagOption[$storyProduct->id] = $options['branchTagOption'][$storyProduct->id];
  794. $modules += $options['modules'];
  795. $plans += $options['plans'];
  796. if(empty($plans[$storyProduct->id])) $plans[$storyProduct->id][0] = $plans[$storyProduct->id];
  797. }
  798. }
  799. /* Append module when change product type. */
  800. $moduleList = array(0 => '/');
  801. $productStoryList = array();
  802. foreach($stories as $story)
  803. {
  804. $moduleList[$story->id] = array();
  805. if(isset($modules[$story->product][$story->branch])) $moduleList[$story->id] = $modules[$story->product][$story->branch];
  806. if(empty($moduleList[$story->id]) and isset($modules[$story->product])) $moduleList[$story->id] = zget($modules[$story->product], 0, array()) + $this->tree->getModulesName(array($story->module));
  807. if($story->status == 'closed')
  808. {
  809. $storyBranch = $story->branch > 0 ? $story->branch : '0';
  810. $branch = $products[$story->product]->type == 'branch' ? $storyBranch : 'all';
  811. if(!isset($productStoryList[$story->product][$story->branch])) $productStoryList[$story->product][$story->branch] = $this->story->getProductStoryPairs($story->product, $branch, 0, 'all', 'id_desc', 0, '', $story->type);
  812. }
  813. if(!empty($story->plan))
  814. {
  815. foreach(explode(',', $story->plan) as $planID)
  816. {
  817. if(empty($planID)) continue;
  818. if(!isset($plans[$story->product][$story->branch][$planID]))
  819. {
  820. $plan = $this->dao->select('id,title,begin,end')->from(TABLE_PRODUCTPLAN)->where('id')->eq($planID)->fetch();
  821. $plans[$story->product][$story->branch][$planID] = $plan->title . ' [' . $plan->begin . '~' . $plan->end . ']';
  822. }
  823. }
  824. }
  825. }
  826. $urStageOptions = array();
  827. foreach($this->lang->requirement->stageList as $stageKey => $stageLang) $urStageOptions[] = array('text' => $stageLang, 'value' => $stageKey);
  828. $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed');
  829. $this->view->branchTagOption = $branchTagOption;
  830. $this->view->moduleList = $moduleList;
  831. $this->view->productStoryList = $productStoryList;
  832. $this->view->plans = $plans;
  833. $this->view->branchProduct = $branchProduct;
  834. $this->view->urStageOptions = $urStageOptions;
  835. }
  836. /**
  837. * Get form options for single product.
  838. *
  839. * @param int $productID
  840. * @param int $executionID
  841. * @param object $product
  842. * @access private
  843. * @return array
  844. */
  845. private function getFormOptionsForSingleProduct($productID, $executionID, $product)
  846. {
  847. $this->loadModel('branch');
  848. $this->loadModel('productplan');
  849. $this->loadModel('tree');
  850. $branchProduct = $product->type == 'normal' ? false : true;
  851. $branches = array(0);
  852. $branchTagOption = array();
  853. if($branchProduct)
  854. {
  855. $branches = $this->branch->getList($productID, $executionID, 'all');
  856. foreach($branches as $branchInfo) $branchTagOption['branch' . $branchInfo->id] = $branchInfo->name . ($branchInfo->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : '');
  857. $branches = array_keys($branches);
  858. }
  859. $modulePairs = $this->tree->getOptionMenu($productID, 'story', 0, $branches);
  860. $modules = array($productID => $modulePairs);
  861. $branchTagOption = array($productID => $branchTagOption);
  862. $products = array($productID => $product);
  863. $plans = array($productID => $this->productplan->getBranchPlanPairs($productID, $branches, '', true));
  864. return array('branchProduct' => $branchProduct, 'modules' => $modules, 'branchTagOption' => $branchTagOption, 'products' => $products, 'plans' => $plans);
  865. }
  866. /**
  867. * Get stories by post checked id list.
  868. *
  869. * @access protected
  870. * @return array|false
  871. */
  872. protected function getStoriesByChecked()
  873. {
  874. $storyIdList = $this->post->storyIdList;
  875. if(empty($storyIdList)) return false;
  876. /* Get edited stories. */
  877. $storyIdList = array_unique($storyIdList);
  878. foreach($storyIdList as $index => $storyID)
  879. {
  880. /* 处理选中的子需求的ID,截取-后的子需求ID。*/
  881. /* Process selected child story ID. */
  882. if(strpos((string)$storyID, '-') !== false) $storyIdList[$index] = substr($storyID, strpos($storyID, '-') + 1);
  883. }
  884. $stories = $this->story->getByList($storyIdList);
  885. if(empty($stories)) return false;
  886. /* Filter twins. */
  887. $twins = '';
  888. $frozenStories = '';
  889. foreach($stories as $id => $story)
  890. {
  891. if(!empty($story->frozen))
  892. {
  893. $frozenStories .= "#$id ";
  894. unset($stories[$id]);
  895. continue;
  896. }
  897. if(empty($story->twins)) continue;
  898. $twins .= "#$id ";
  899. unset($stories[$id]);
  900. }
  901. if(!empty($twins)) $this->view->twinsTip = sprintf($this->lang->story->batchEditTip, $twins);
  902. if(!empty($frozenStories)) $this->view->frozenTips = sprintf($this->lang->story->frozenTips, $frozenStories, $this->lang->story->edit);
  903. return $stories;
  904. }
  905. /**
  906. * 设置模块字段的表单字段。
  907. * Set module form field.
  908. *
  909. * @param array $fields
  910. * @param int $moduleID
  911. * @access protected
  912. * @return array
  913. */
  914. protected function setModuleField($fields, $moduleID)
  915. {
  916. $productID = $this->view->productID;
  917. $branch = $this->view->branch;
  918. $optionMenu = $this->tree->getOptionMenu($productID, 'story', 0, $branch === 'all' ? '0' : (string)$branch, 'nodeleted');
  919. $moduleID = $moduleID ? $moduleID : (int)$this->cookie->lastStoryModule;
  920. $moduleID = $moduleID ? $moduleID : $fields['module']['default'];
  921. $moduleID = isset($optionMenu[$moduleID]) ? $moduleID : 0;
  922. $fields['module']['options'] = $optionMenu;
  923. $fields['module']['default'] = $moduleID;
  924. $fields['modules']['options'] = $optionMenu;
  925. $fields['modules']['default'] = $moduleID;
  926. $this->view->moduleID = $moduleID;
  927. return $fields;
  928. }
  929. /**
  930. * 根据配置,删除非必要的表单字段配置。
  931. * Remove form fields for create.
  932. *
  933. * @param array $fields
  934. * @param string $storyType
  935. * @access protected
  936. * @return array
  937. */
  938. protected function removeFormFieldsForCreate($fields, $storyType = 'story')
  939. {
  940. $objectID = $this->view->objectID;
  941. /* Hidden some fields of projects without products. */
  942. $hiddenProduct = $hiddenPlan = false;
  943. $teamUsers = array();
  944. if($this->app->tab === 'project' || $this->app->tab === 'execution')
  945. {
  946. $project = $this->dao->findById((int)$objectID)->from(TABLE_PROJECT)->fetch();
  947. if(!empty($project->project)) $project = $this->dao->findById((int)$project->project)->from(TABLE_PROJECT)->fetch();
  948. if(empty($project->hasProduct))
  949. {
  950. $teamUsers = $this->project->getTeamMemberPairs($project->id);
  951. $hiddenProduct = true;
  952. if($project->model !== 'scrum' or !$project->multiple) $hiddenPlan = true;
  953. }
  954. }
  955. if($storyType != 'story') unset($fields['branches'], $fields['modules'], $fields['plans']);
  956. if($hiddenPlan) unset($fields['plan']);
  957. if($hiddenProduct)
  958. {
  959. $fields['product']['control'] = 'hidden';
  960. $fields['reviewer']['options'] = $teamUsers;
  961. $fields['assignedTo']['options'] = $teamUsers;
  962. }
  963. return $fields;
  964. }
  965. /**
  966. * 隐藏不显示的字段。
  967. * Hide form fields for edi.
  968. *
  969. * @param array $fields
  970. * @param string $storyType
  971. * @access protected
  972. * @return array
  973. */
  974. protected function hiddenFormFieldsForEdit($fields, $storyType)
  975. {
  976. $product = $this->view->product;
  977. $hiddenProduct = $hiddenPlan = false;
  978. $teamUsers = array();
  979. if($product->shadow)
  980. {
  981. $this->loadModel('project');
  982. $project = $this->project->getByShadowProduct($product->id);
  983. $teamUsers = $this->project->getTeamMemberPairs($project->id);
  984. $hiddenProduct = true;
  985. if($project->model !== 'scrum') $hiddenPlan = true;
  986. if(!$project->multiple)
  987. {
  988. $hiddenPlan = true;
  989. unset($this->lang->story->stageList[''], $this->lang->story->stageList['wait'], $this->lang->story->stageList['planned']);
  990. }
  991. }
  992. if($hiddenPlan) $fields['plan']['className'] = 'hidden';
  993. if($hiddenProduct)
  994. {
  995. $fields['product']['className'] = 'hidden';
  996. $fields['reviewer']['options'] = $teamUsers;
  997. $fields['assignedTo']['options'] = $teamUsers;
  998. }
  999. return $fields;
  1000. }
  1001. /**
  1002. * 根据配置,删除非必要的表单字段配置。
  1003. * Remove form fields for batch create.
  1004. *
  1005. * @param array $fields
  1006. * @param bool $hiddenPlan
  1007. * @param string $executionType
  1008. * @param int $executionID
  1009. * @access protected
  1010. * @return array
  1011. */
  1012. protected function removeFormFieldsForBatchCreate($fields, $hiddenPlan, $executionType, $executionID = 0)
  1013. {
  1014. if($hiddenPlan) unset($fields['plan']);
  1015. if($executionType != 'kanban')
  1016. {
  1017. unset($fields['region']);
  1018. unset($fields['lane']);
  1019. }
  1020. if($this->app->tab == 'project' || $this->app->tab == 'execution')
  1021. {
  1022. $fields['parent']['hidden'] = true;
  1023. $project = $this->dao->findById((int)$executionID)->from(TABLE_PROJECT)->fetch();
  1024. if(!empty($project->project)) $project = $this->dao->findById((int)$project->project)->from(TABLE_PROJECT)->fetch();
  1025. if(empty($project->hasProduct))
  1026. {
  1027. $teamUsers = $this->project->getTeamMemberPairs($project->id);
  1028. $fields['reviewer']['options'] = $teamUsers;
  1029. $fields['assignedTo']['options'] = $teamUsers;
  1030. }
  1031. }
  1032. return $fields;
  1033. }
  1034. /**
  1035. * 获取指派给我的Block编号。
  1036. * Get assign me block id.
  1037. *
  1038. * @access protected
  1039. * @return int
  1040. */
  1041. protected function getAssignMeBlockID()
  1042. {
  1043. if(!isonlybody()) return 0;
  1044. return (int)$this->dao->select('id')->from(TABLE_BLOCK)->where('module')->eq('assigntome')
  1045. ->andWhere('module')->eq('my')
  1046. ->andWhere('account')->eq($this->app->user->account)
  1047. ->andWhere('vision')->eq($this->config->vision)
  1048. ->orderBy('order_desc')
  1049. ->limit(1)
  1050. ->fetch('id');
  1051. }
  1052. /**
  1053. * 构建创建需求数据。
  1054. * Build story for create
  1055. *
  1056. * @param int $executionID
  1057. * @param int $bugID
  1058. * @param string $storyType
  1059. * @access protected
  1060. * @return object|false
  1061. */
  1062. protected function buildStoryForCreate($executionID, $bugID, $storyType = 'story')
  1063. {
  1064. $fields = $this->config->story->form->create;
  1065. foreach(explode(',', trim($this->config->{$storyType}->create->requiredFields, ',')) as $field)
  1066. {
  1067. if($field == 'files')
  1068. {
  1069. if(empty($_FILES['files']['name'][0]))
  1070. {
  1071. dao::$errors['files'][] = sprintf($this->lang->error->notempty, $this->lang->files);
  1072. }
  1073. continue;
  1074. }
  1075. $fields[$field]['required'] = true;
  1076. }
  1077. if(!isset($_POST['plan'])) $this->config->{$storyType}->create->requiredFields = str_replace(',plan,', ',', ",{$this->config->story->create->requiredFields},");
  1078. if(!empty($_POST['modules']) && !empty($fields['module']['required']))
  1079. {
  1080. /* Check empty module in the product with multi-branches. */
  1081. $fields['module']['required'] = false;
  1082. $this->config->{$storyType}->create->requiredFields = str_replace(',module,', ',', ",{$this->config->{$storyType}->create->requiredFields},");
  1083. foreach($_POST['modules'] as $key => $moduleID)
  1084. {
  1085. if(!$key) continue;
  1086. if(empty($moduleID)) dao::$errors["modules[{$key}]"][] = sprintf($this->lang->error->notempty, $this->lang->story->module);
  1087. }
  1088. }
  1089. if(!empty($_POST['estimate']) && $_POST['estimate'] < 0) dao::$errors['estimate'] = sprintf($this->lang->story->errorRecordMinus, $this->lang->story->estimateAB);
  1090. if(dao::isError()) return false;
  1091. $storyData = form::data($fields)
  1092. ->setIF($this->post->assignedTo, 'assignedDate', helper::now())
  1093. ->setIF($this->post->plan > 0 && $storyType == 'story', 'stage', 'planned')
  1094. ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'feedbackBy', '')
  1095. ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'notifyEmail', '')
  1096. ->setIF($executionID > 0, 'stage', 'projected')
  1097. ->setIF($bugID > 0, 'fromBug', $bugID)
  1098. ->setIF(!$this->post->estimate, 'estimate', 0)
  1099. ->setIF($storyType, 'type', $storyType)
  1100. ->get();
  1101. if(isset($_POST['reviewer'])) $_POST['reviewer'] = array_filter($_POST['reviewer']);
  1102. if(!$this->post->needNotReview and empty($_POST['reviewer']))
  1103. {
  1104. dao::$errors['reviewer'] = sprintf($this->lang->error->notempty, $this->lang->story->reviewer);
  1105. return false;
  1106. }
  1107. /* Need and force review, then set status to reviewing. */
  1108. if($storyData->status != 'draft' and $this->story->checkForceReview($storyType) and !$this->post->needNotReview) $storyData->status = 'reviewing';
  1109. return $this->loadModel('file')->processImgURL($storyData, $this->config->story->editor->create['id'], $this->post->uid);
  1110. }
  1111. /**
  1112. * 构建编辑需求数据。
  1113. * Build story for edit
  1114. *
  1115. * @param int $storyID
  1116. * @access protected
  1117. * @return object|false
  1118. */
  1119. protected function buildStoryForEdit($storyID)
  1120. {
  1121. $storyPlan = array();
  1122. $oldStory = $this->story->getByID($storyID);
  1123. if(!$oldStory) return false;
  1124. if(!empty($_POST['lastEditedDate']) and $oldStory->lastEditedDate != $this->post->lastEditedDate) dao::$errors[] = $this->lang->error->editedByOther;
  1125. if(strpos('draft,changing', $oldStory->status) !== false and $this->story->checkForceReview($oldStory->type) and empty($_POST['reviewer'])) dao::$errors[] = $this->lang->story->notice->reviewerNotEmpty;
  1126. if(!empty($_POST['plan'])) $storyPlan = is_array($_POST['plan']) ? array_filter($_POST['plan']) : array($_POST['plan']);
  1127. if(count($storyPlan) > 1 && $oldStory->type == 'story')
  1128. {
  1129. $oldStoryPlan = !empty($oldStory->plan) ? array_filter(explode(',', $oldStory->plan)) : array();
  1130. $oldPlanDiff = array_diff($storyPlan, $oldStoryPlan);
  1131. $storyPlanDiff = array_diff($oldStoryPlan, $storyPlan);
  1132. if(!empty($oldPlanDiff) or !empty($storyPlanDiff)) dao::$errors['plan'] = $this->lang->story->notice->changePlan;
  1133. }
  1134. if(strpos(',draft,changing,', $oldStory->status) !== false)
  1135. {
  1136. if(isset($_POST['reviewer'])) $_POST['reviewer'] = array_filter($_POST['reviewer']);
  1137. if(!$this->post->needNotReview and empty($_POST['reviewer'])) dao::$errors['reviewer'] = $this->lang->story->errorEmptyReviewedBy;
  1138. }
  1139. if(!empty($_POST['estimate']) && $_POST['estimate'] < 0) dao::$errors['estimate'] = sprintf($this->lang->story->errorRecordMinus, $this->lang->story->estimateAB);
  1140. if(dao::isError()) return false;
  1141. $hasProduct = $this->dao->select('t2.hasProduct')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  1142. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  1143. ->where('t1.product')->eq($oldStory->product)
  1144. ->andWhere('t2.deleted')->eq(0)
  1145. ->fetch('hasProduct');
  1146. $_POST['product'] = (!empty($hasProduct) && !$hasProduct) ? $oldStory->product : $this->post->product;
  1147. $now = helper::now();
  1148. $fields = $this->config->story->form->edit;
  1149. $storyData = form::data($fields, $storyID)
  1150. ->add('id', $storyID)
  1151. ->add('lastEditedBy', $this->app->user->account)
  1152. ->add('lastEditedDate', $now)
  1153. ->add('demand', $oldStory->demand)
  1154. ->setDefault('deleteFiles', array())
  1155. ->setDefault('reviewedBy', $oldStory->reviewedBy)
  1156. ->setDefault('renameFiles', array())
  1157. ->setDefault('product', $oldStory->product)
  1158. ->setDefault('branch', $oldStory->branch)
  1159. ->setDefault('stage', $oldStory->stage)
  1160. ->setDefault('stagedBy', $oldStory->stagedBy)
  1161. ->setIF($this->post->assignedTo != $oldStory->assignedTo, 'assignedDate', $now)
  1162. ->setIF($this->post->closedBy && $oldStory->closedDate == '', 'closedDate', $now)
  1163. ->setIF($this->post->closedReason && $oldStory->closedDate == '', 'closedDate', $now)
  1164. ->setIF($this->post->closedBy || $this->post->closedReason != false, 'status', 'closed')
  1165. ->setIF($this->post->closedReason && $this->post->closedBy == false, 'closedBy', $this->app->user->account)
  1166. ->setIF($this->post->stage == 'released', 'releasedDate', $now)
  1167. ->setIF(!$this->post->grade, 'grade', $oldStory->grade)
  1168. ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'feedbackBy', '')
  1169. ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'notifyEmail', '')
  1170. ->setIF(!empty($_POST['plan'][0]) and $oldStory->stage == 'wait', 'stage', 'planned')
  1171. ->setIF(!isset($_POST['title']), 'title', $oldStory->title)
  1172. ->setIF(!isset($_POST['spec']), 'spec', $oldStory->spec)
  1173. ->setIF(!isset($_POST['verify']), 'verify', $oldStory->verify)
  1174. ->setIF(!isset($_POST['estimate']), 'estimate', $oldStory->estimate)
  1175. ->get();
  1176. if($this->post->linkStories) $storyData->linkStories = implode(',', array_unique($this->post->linkStories));
  1177. if($this->post->linkRequirements) $storyData->linkRequirements = implode(',', array_unique($this->post->linkRequirements));
  1178. if($oldStory->product != $storyData->product) $storyData->root = $storyID;
  1179. return $this->loadModel('file')->processImgURL($storyData, $this->config->story->editor->edit['id'], $this->post->uid);
  1180. }
  1181. /**
  1182. * 构建变更需求数据。
  1183. * Build story for change
  1184. *
  1185. * @param int $storyID
  1186. * @access protected
  1187. * @return object|false
  1188. */
  1189. protected function buildStoryForChange($storyID)
  1190. {
  1191. $oldStory = $this->story->getByID($storyID);
  1192. if(!empty($_POST['lastEditedDate']) and $oldStory->lastEditedDate != $this->post->lastEditedDate) dao::$errors[] = $this->lang->error->editedByOther;
  1193. if(strpos($this->config->{$oldStory->type}->change->requiredFields, 'spec') !== false and !$this->post->spec) dao::$errors['spec'][] = sprintf($this->lang->error->notempty, $this->lang->story->spec);
  1194. if(strpos($this->config->{$oldStory->type}->change->requiredFields, 'verify') !== false and !$this->post->spec) dao::$errors['verify'][] = sprintf($this->lang->error->notempty, $this->lang->story->verify);
  1195. if(strpos($this->config->{$oldStory->type}->change->requiredFields, 'comment') !== false and !$this->post->comment) dao::$errors['comment'] = sprintf($this->lang->error->notempty, $this->lang->comment);
  1196. if(isset($_POST['reviewer'])) $_POST['reviewer'] = array_filter($_POST['reviewer']);
  1197. if(!$this->post->needNotReview and empty($_POST['reviewer'])) dao::$errors['reviewer'] = $this->lang->story->errorEmptyReviewedBy;
  1198. if(dao::isError()) return false;
  1199. $now = helper::now();
  1200. $fields = $this->config->story->form->change;
  1201. $story = form::data($fields, $storyID)
  1202. ->setDefault('lastEditedBy', $this->app->user->account)
  1203. ->setDefault('deleteFiles', array())
  1204. ->setDefault('lastEditedDate', $now)
  1205. ->setDefault('version', $oldStory->version)
  1206. ->get();
  1207. $specChanged = false;
  1208. $oldStoryReviewers = array_keys($this->story->getReviewerPairs($storyID, $oldStory->version));
  1209. $_POST['reviewer'] = isset($_POST['reviewer']) ? $_POST['reviewer'] : array();
  1210. $reviewerHasChanged = (array_diff($oldStoryReviewers, $_POST['reviewer']) || array_diff($_POST['reviewer'], $oldStoryReviewers));
  1211. if($story->spec != $oldStory->spec or $story->verify != $oldStory->verify or $story->title != $oldStory->title or $this->loadModel('file')->getCount() or $reviewerHasChanged or !empty($story->deleteFiles)) $specChanged = true;
  1212. $story->reviewerHasChanged = $reviewerHasChanged;
  1213. if($specChanged)
  1214. {
  1215. $story->version = (int)$oldStory->version + 1;
  1216. $story->reviewedBy = '';
  1217. $story->changedBy = $this->app->user->account;
  1218. $story->changedDate = $now;
  1219. $story->closedBy = '';
  1220. $story->closedReason = '';
  1221. if($oldStory->reviewedBy) $story->reviewedDate = null;
  1222. if($oldStory->closedBy) $story->closedDate = null;
  1223. }
  1224. else
  1225. {
  1226. $story->status = $oldStory->status;
  1227. }
  1228. if(!isset($_POST['relievedTwins'])) unset($story->relievedTwins);
  1229. return $this->loadModel('file')->processImgURL($story, $this->config->story->editor->change['id'], $this->post->uid);
  1230. }
  1231. /**
  1232. * 构建需求转化任务的数据。
  1233. * Build data for batchToTask.
  1234. *
  1235. * @param int $executionID
  1236. * @param int $projectID
  1237. * @access protected
  1238. * @return array|false
  1239. */
  1240. protected function buildDataForBatchToTask($executionID, $projectID = 0)
  1241. {
  1242. $this->loadModel('task');
  1243. $now = helper::now();
  1244. $fields = $this->config->story->form->batchToTask;
  1245. $requiredFields = "," . $this->config->task->create->requiredFields . ",";
  1246. foreach(explode(',', trim($requiredFields, ',')) as $field)
  1247. {
  1248. if(isset($fields[$field])) $fields[$field]['required'] = true;
  1249. }
  1250. $syncFields = zget($_POST, 'syncFields', '');
  1251. $stories = array();
  1252. if(!empty($syncFields)) $stories = empty($_POST['story']) ? array() : $this->story->getByList($_POST['story']);
  1253. $this->lang->story->estimate = $this->lang->task->estimateAB;
  1254. $this->lang->story->deadline = $this->lang->task->deadline;
  1255. $this->lang->story->estStarted = $this->lang->task->estStarted;
  1256. $tasks = form::batchData($fields)->get();
  1257. $taskNames = array();
  1258. foreach($tasks as $task)
  1259. {
  1260. $task->project = $projectID;
  1261. $task->execution = $executionID;
  1262. $task->left = $task->estimate;
  1263. if($task->assignedTo) $task->assignedDate = $now;
  1264. if($task->story)
  1265. {
  1266. $story = zget($stories, $task->story, null);
  1267. if($story)
  1268. {
  1269. $task->storyVersion = $story->version;
  1270. if(strpos(",{$syncFields},", ',spec,') !== false) $task->desc = $story->spec;
  1271. if(strpos(",{$syncFields},", ',mailto,') !== false) $task->mailto = $story->mailto;
  1272. }
  1273. }
  1274. if(in_array($task->name, $taskNames)) dao::$errors['message'][] = sprintf($this->lang->duplicate, $this->lang->task->common) . ' ' . $task->name;
  1275. if(!helper::isZeroDate($task->deadline) and $task->deadline < $task->estStarted) dao::$errors['message'][] = $this->lang->task->error->deadlineSmall;
  1276. if($task->estimate and !preg_match("/^[0-9]+(.[0-9]+)?$/", (string)$task->estimate)) dao::$errors['message'][] = $this->lang->task->error->estimateNumber;
  1277. if(!empty($this->config->limitTaskDate)) $this->task->checkEstStartedAndDeadline($executionID, (string)$task->estStarted, (string)$task->deadline);
  1278. $taskNames[] = $task->name;
  1279. }
  1280. if(dao::isError()) return false;
  1281. return $tasks;
  1282. }
  1283. /**
  1284. * 处理编辑需求数据。
  1285. * Process data for edit.
  1286. *
  1287. * @param int $storyID
  1288. * @param object $story
  1289. * @access protected
  1290. * @return void
  1291. */
  1292. protected function processDataForEdit($storyID, $story)
  1293. {
  1294. $oldStory = $this->story->fetchByID($storyID);
  1295. if($oldStory->type == 'story' and !isset($story->linkStories)) $story->linkStories = '';
  1296. if($oldStory->type == 'requirement' and !isset($story->linkRequirements)) $story->linkRequirements = '';
  1297. if($oldStory->status == 'changing' and $story->status == 'draft') $story->status = 'changing';
  1298. if(isset($_POST['plan']) and is_array($_POST['plan'])) $story->plan = trim(implode(',', $_POST['plan']), ',');
  1299. if(isset($_POST['branch']) and $_POST['branch'] == 0) $story->branch = 0;
  1300. if(isset($story->stage) and $oldStory->stage != $story->stage) $story->stagedBy = (strpos('tested|verified|rejected|pending|released|closed', $story->stage) !== false) ? $this->app->user->account : '';
  1301. }
  1302. /**
  1303. * 构建评审需求数据。
  1304. * Build story for review
  1305. *
  1306. * @param int $storyID
  1307. * @access protected
  1308. * @return object|false
  1309. */
  1310. protected function buildStoryForReview($storyID)
  1311. {
  1312. $oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch();
  1313. $now = helper::now();
  1314. $fields = $this->config->story->form->review;
  1315. foreach(explode(',', trim($this->config->{$oldStory->type}->review->requiredFields, ',')) as $field)
  1316. {
  1317. if($field == 'comment' && !$this->post->comment)
  1318. {
  1319. dao::$errors['comment'][] = sprintf($this->lang->error->notempty, $this->lang->comment);
  1320. return false;
  1321. }
  1322. if($field == 'reviewedDate' && !$this->post->reviewedDate)
  1323. {
  1324. dao::$errors['reviewedDate'] = sprintf($this->lang->error->notempty, $this->lang->story->reviewedDate);
  1325. return false;
  1326. }
  1327. if(isset($fields[$field])) $fields[$field]['required'] = true;
  1328. }
  1329. if($this->post->result == false)
  1330. {
  1331. dao::$errors['result'][] = $this->lang->story->mustChooseResult;
  1332. return false;
  1333. }
  1334. $result = $this->post->result;
  1335. $closedReason = $this->post->closedReason;
  1336. $storyData = form::data($fields, $storyID)
  1337. ->setDefault('lastEditedBy', $this->app->user->account)
  1338. ->setDefault('lastEditedDate', $now)
  1339. ->removeIF($result != 'reject', 'closedReason,duplicateStory')
  1340. ->removeIF($result == 'reject' && $closedReason != 'duplicate', 'duplicateStory')
  1341. ->get();
  1342. if($oldStory->assignedTo != $storyData->assignedTo) $storyData->assignedDate = $now;
  1343. $storyData->reviewedBy = implode(',', array_unique(explode(',', $oldStory->reviewedBy . ',' . $this->app->user->account)));
  1344. if($result == 'reject' && empty($closedReason)) dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->story->rejectedReason);
  1345. if($result == 'reject' && $closedReason == 'duplicate' && empty($storyData->duplicateStory)) dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory);
  1346. if(!empty($storyData->estimate) && $storyData->estimate < 0) dao::$errors['estimate'] = sprintf($this->lang->story->errorRecordMinus, $this->lang->story->estimate);
  1347. if(dao::isError()) return false;
  1348. return $this->loadModel('file')->processImgURL($storyData, $this->config->story->editor->review['id'], $this->post->uid);
  1349. }
  1350. /**
  1351. * 构建批量创建需求数据。
  1352. * Build stories for batch create.
  1353. *
  1354. * @param int $productID
  1355. * @param string $storyType
  1356. * @access protected
  1357. * @return array
  1358. */
  1359. protected function buildStoriesForBatchCreate($productID, $storyType)
  1360. {
  1361. $forceReview = $this->story->checkForceReview($storyType);
  1362. $fields = $this->config->story->form->batchCreate;
  1363. $account = $this->app->user->account;
  1364. $now = helper::now();
  1365. $saveDraft = $this->post->status == 'draft';
  1366. if($forceReview) $fields['reviewer']['required'] = true;
  1367. $stories = form::batchData($fields)->get();
  1368. foreach($stories as $i => $story)
  1369. {
  1370. if(!empty($story->title) && mb_strlen($story->title) > 255)
  1371. {
  1372. dao::$errors["title[$i]"] = sprintf($this->lang->story->error->length, 255);
  1373. }
  1374. if(!empty($story->estimate) && $story->estimate < 0) dao::$errors["estimate[$i]"] = sprintf($this->lang->story->errorRecordMinus, $this->lang->story->estimate);
  1375. $story->type = $storyType;
  1376. $story->status = (empty($story->reviewer) && !$forceReview) ? 'active' : 'reviewing';
  1377. $story->status = $saveDraft ? 'draft' : $story->status;
  1378. $story->product = $productID;
  1379. $story->openedBy = $account;
  1380. $story->vision = $this->config->vision;
  1381. $story->openedDate = $now;
  1382. $story->version = 1;
  1383. if(in_array($this->app->tab, array('project', 'execution')))
  1384. {
  1385. $story->stage = 'projected';
  1386. }
  1387. !empty($story->assignedTo) && $story->assignedDate = $now;
  1388. if($this->post->uploadImage && $this->post->uploadImage[$i]) $story->uploadImage = $this->post->uploadImage[$i];
  1389. }
  1390. return $stories;
  1391. }
  1392. /**
  1393. * 构建批量编辑需求数据。
  1394. * Build stories for batch edit page.
  1395. *
  1396. * @access protected
  1397. * @return array
  1398. */
  1399. protected function buildStoriesForBatchEdit()
  1400. {
  1401. $fields = $this->config->story->form->batchEdit;
  1402. $account = $this->app->user->account;
  1403. $now = helper::now();
  1404. $fields['duplicateStory'] = array('type' => 'int', 'required' => false, 'default' => 0);
  1405. $stories = form::batchData($fields)->get();
  1406. $oldStories = $this->story->getByList(array_keys($stories));
  1407. if($this->config->edition == 'ipd') $roadmaps = $this->loadModel('roadmap')->getList();
  1408. foreach($stories as $storyID => $story)
  1409. {
  1410. $oldStory = $oldStories[$storyID];
  1411. $story->lastEditedBy = $this->app->user->account;
  1412. $story->lastEditedDate = $now;
  1413. $story->stage = empty($story->stage) ? $oldStory->stage : $story->stage;
  1414. $story->status = empty($story->status) ? $oldStory->status : $story->status;
  1415. $story->branch = $story->branch === '' ? $oldStory->branch : str_replace('branch', '', $story->branch);
  1416. if(empty($story->roadmap)) $story->roadmap = $oldStory->roadmap;
  1417. if($oldStory->assignedTo != $story->assignedTo) $story->assignedDate = $now;
  1418. if($oldStory->parent < 0) $story->plan = '';
  1419. if($story->stage != $oldStory->stage) $story->stagedBy = (strpos('|tested|verified|rejected|pending|released|closed|', "|{$story->stage}|") !== false) ? $account : '';
  1420. if($story->closedBy && helper::isZeroDate($oldStory->closedDate)) $story->closedDate = $now;
  1421. if($story->closedReason && helper::isZeroDate($oldStory->closedDate)) $story->closedDate = $now;
  1422. if($story->closedBy || $story->closedReason) $story->status = 'closed';
  1423. if($story->closedReason && empty($story->closedBy)) $story->closedBy = $account;
  1424. if($story->closedBy && empty($story->closedReason)) dao::$errors['closedReason'] = sprintf($this->lang->error->notempty, $this->lang->story->closedReason);
  1425. if($story->closedReason == 'done' && empty($story->stage)) dao::$errors['stage'] = sprintf($this->lang->error->notempty, $this->lang->story->stage);
  1426. if($story->closedReason == 'duplicate' && empty($story->duplicateStory)) dao::$errors['duplicateStory'] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory);
  1427. if($this->config->vision == 'or' && $this->config->edition == 'ipd')
  1428. {
  1429. if($story->stage == 'wait' && !empty($story->roadmap) && isset($roadmaps[$story->roadmap])) $story->stage = $roadmaps[$story->roadmap]->status == 'launched' ? 'incharter' : 'inroadmap';
  1430. if(empty($story->roadmap)) $story->stage = 'wait';
  1431. }
  1432. if(!empty($story->estimate) && $story->estimate < 0) dao::$errors["estimate[{$storyID}]"] = sprintf($this->lang->story->errorRecordMinus, $this->lang->story->estimate);
  1433. }
  1434. return $stories;
  1435. }
  1436. /**
  1437. * 构建批量关闭需求数据,跳过有子需求的父需求以及已经关闭了的需求。
  1438. * Build stories for batch close.
  1439. *
  1440. * @access protected
  1441. * @return array
  1442. */
  1443. protected function buildStoriesForBatchClose()
  1444. {
  1445. $account = $this->app->user->account;
  1446. $now = helper::now();
  1447. $data = form::batchData()->get();
  1448. $oldStories = $this->story->getByList(array_keys($data));
  1449. $stories = array();
  1450. foreach($data as $storyID => $story)
  1451. {
  1452. $oldStory = $oldStories[$storyID];
  1453. if($oldStory->parent == -1) continue; /* Skip the story which has any child story. */
  1454. if($oldStory->status == 'closed') continue; /* Skip the story which has been closed. */
  1455. $story->lastEditedBy = $account;
  1456. $story->lastEditedDate = $now;
  1457. $story->closedBy = $account;
  1458. $story->closedDate = $now;
  1459. $story->assignedTo = 'closed';
  1460. $story->assignedDate = $now;
  1461. $story->status = 'closed';
  1462. $story->stage = 'closed';
  1463. if($story->closedReason != 'done') $story->plan = '';
  1464. if($story->closedReason == 'duplicate' && empty($story->duplicateStory)) dao::$errors["duplicateStory[{$storyID}]"] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory);
  1465. $stories[$storyID] = $story;
  1466. }
  1467. return $stories;
  1468. }
  1469. /**
  1470. * 如果是在弹窗中打开页面,获取跳转地址。
  1471. * Get response when open in modal.
  1472. *
  1473. * @param string $message
  1474. * @access protected
  1475. * @return array|false
  1476. */
  1477. protected function getResponseInModal($message = '')
  1478. {
  1479. if(!isInModal()) return false;
  1480. $execution = $this->execution->getByID((int)$this->session->execution);
  1481. if($this->app->tab == 'execution' and $execution->type == 'kanban')
  1482. {
  1483. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => "refreshKanban()", 'closeModal' => true));
  1484. }
  1485. else
  1486. {
  1487. return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
  1488. }
  1489. }
  1490. /**
  1491. * 获取编辑需求后的跳转地址。
  1492. * Get location when after edit a story.
  1493. *
  1494. * @param int $storyID
  1495. * @param string $storyType
  1496. * @access protected
  1497. * @return string
  1498. */
  1499. protected function getAfterEditLocation($storyID, $storyType)
  1500. {
  1501. $module = $storyType;
  1502. $params = "storyID=$storyID&version=0&param=0&storyType=$storyType";
  1503. $method = 'view';
  1504. if($this->app->tab == 'project')
  1505. {
  1506. $project = $this->loadModel('project')->getByID($this->session->project);
  1507. if(empty($project->multiple))
  1508. {
  1509. $module = 'execution';
  1510. $method = 'storyView';
  1511. $params = "storyID=$storyID&project={$this->session->project}";
  1512. }
  1513. else
  1514. {
  1515. $module = 'projectstory';
  1516. $params = "storyID=$storyID&project={$this->session->project}";
  1517. }
  1518. }
  1519. return $this->createLink($module, $method, $params);
  1520. }
  1521. /**
  1522. * 获取创建需求后的跳转地址。
  1523. * Get location when after create story.
  1524. *
  1525. * @param int $productID
  1526. * @param string $branch
  1527. * @param int $objectID
  1528. * @param int $storyID
  1529. * @param string $storyType
  1530. * @param string $extra feedback扩展使用
  1531. * @access public
  1532. * @return string
  1533. */
  1534. public function getAfterCreateLocation($productID, $branch, $objectID, $storyID, $storyType, $extra = '')
  1535. {
  1536. if($this->app->getViewType() == 'xhtml') return $this->createLink('story', 'view', "storyID=$storyID", 'html');
  1537. if($objectID)
  1538. {
  1539. helper::setcookie('storyModuleParam', '0', 0);
  1540. $object = $this->loadModel('project')->fetchByID($objectID);
  1541. if(empty($_SESSION['storyList'])) return $this->createLink($this->app->tab == 'project' && $object->type == 'project' ? 'projectstory' : 'execution', 'story', "objectID=$objectID");
  1542. return $this->session->storyList;
  1543. }
  1544. if($this->app->tab == 'product')
  1545. {
  1546. $storyProductID = $this->story->fetchByID($storyID)->product;
  1547. return $this->createLink('product', 'browse', "productID=$storyProductID&branch=$branch&browseType=unclosed&param=0&storyType=$storyType");
  1548. }
  1549. helper::setcookie('storyModule', '0', 0);
  1550. $branchID = $this->post->branch ? $this->post->branch : $branch;
  1551. if(empty($_SESSION['storyList'])) return $this->createLink('product', 'browse', "productID=$productID&branch=$branchID&browseType=&param=0&storyType=$storyType&orderBy=id_desc");
  1552. if(!empty($_POST['branches']) and count($_POST['branches']) > 1) return preg_replace('/branch=(\d+|[A-Za-z]+)/', 'branch=all', $this->session->storyList);
  1553. return $this->session->storyList;
  1554. }
  1555. /**
  1556. * 获取批量创建需求后的跳转地址。
  1557. * Get after batch create location.
  1558. *
  1559. * @param int $productID
  1560. * @param string $branch
  1561. * @param int $executionID
  1562. * @param int $storyID
  1563. * @param string $storyType
  1564. * @access protected
  1565. * @return string
  1566. */
  1567. protected function getAfterBatchCreateLocation($productID, $branch, $executionID, $storyID, $storyType)
  1568. {
  1569. if($storyID)
  1570. {
  1571. $locateLink = $this->session->storyList ? $this->session->storyList : $this->createLink('projectstory', 'view', "storyID=$storyID&projectID={$this->app->project}");
  1572. if($this->app->tab == 'product')
  1573. {
  1574. $story = $this->story->fetchById($storyID);
  1575. $locateLink = $this->createLink($story->type, 'view', "storyID=$storyID&version=0&param=0&storyType=$storyType");
  1576. }
  1577. return $locateLink;
  1578. }
  1579. if($executionID)
  1580. {
  1581. helper::setcookie('storyModuleParam', '0', 0);
  1582. return $this->session->storyList;
  1583. }
  1584. if($this->app->tab == 'product')
  1585. {
  1586. return $this->createLink('product', 'browse', "productID=$productID&branch=$branch&browseType=unclosed&queryID=0&storyType=$storyType");
  1587. }
  1588. helper::setcookie('storyModule', '0', 0);
  1589. if($this->session->storyList) return $this->session->storyList;
  1590. return $this->createLink('product', 'browse', "productID=$productID&branch=$branch&browseType=unclosed&queryID=0&storyType=$storyType");
  1591. }
  1592. /**
  1593. * 获取变更需求后的跳转地址。
  1594. * Get after change story location.
  1595. *
  1596. * @param int $storyID
  1597. * @param string $storyType
  1598. * @access protected
  1599. * @return string
  1600. */
  1601. protected function getAfterChangeLocation($storyID, $storyType = 'story')
  1602. {
  1603. if($this->app->tab == 'execution') return helper::createLink('execution', 'storyView', "storyID=$storyID");
  1604. if($this->app->tab != 'project') return helper::createLink($storyType, 'view', "storyID=$storyID&version=0&param=0&storyType=$storyType");
  1605. if($this->app->tab == 'project')
  1606. {
  1607. $module = 'projectstory';
  1608. $method = 'view';
  1609. $params = "storyID=$storyID";
  1610. if(!$this->session->multiple)
  1611. {
  1612. $module = 'story';
  1613. $params .= "&version=0&param={$this->session->project}&storyType=$storyType";
  1614. }
  1615. return helper::createLink($module, $method, $params);
  1616. }
  1617. }
  1618. /**
  1619. * 获取评审需求后的跳转地址。
  1620. * Get after review location.
  1621. *
  1622. * @param int $storyID
  1623. * @param string $storyType
  1624. * @param string $from
  1625. * @access protected
  1626. * @return string
  1627. */
  1628. protected function getAfterReviewLocation($storyID, $storyType = 'story', $from = '')
  1629. {
  1630. if($from == 'project')
  1631. {
  1632. $project = $this->project->getByID($this->session->project);
  1633. if($project && !$project->multiple) return helper::createLink('execution', 'storyView', "storyID=$storyID");
  1634. return helper::createLink('projectstory', 'view', "storyID={$storyID}&projectID={$this->session->project}");
  1635. }
  1636. if($from != 'execution') return helper::createLink($storyType, 'view', "storyID={$storyID}&version=0&param=0&storyType={$storyType}");
  1637. $module = 'execution';
  1638. $method = 'storyView';
  1639. $params = "storyID=$storyID";
  1640. return helper::createLink($module, $method, $params);
  1641. }
  1642. /**
  1643. * 根据上传图片,批量创建需求时,获取初始化需求数据。
  1644. * Get data from upload images.
  1645. *
  1646. * @param int $productID
  1647. * @param int $moduleID
  1648. * @param int $planID
  1649. * @access protected
  1650. * @return array
  1651. */
  1652. protected function getDataFromUploadImages($productID, $moduleID = 0, $planID = 0)
  1653. {
  1654. /* Clear title when switching products and set the session for the current product. */
  1655. if($productID != $this->cookie->preProductID) unset($_SESSION['storyImagesFile']);
  1656. helper::setcookie('preProductID', (string)$productID);
  1657. $defaultStory = array('title' => '', 'spec' => '', 'module' => $moduleID, 'plan' => $planID, 'pri' => (string)$this->config->story->defaultPriority, 'estimate' => '', 'branch' => $this->view->branchID);
  1658. $batchStories = array();
  1659. $count = $this->config->story->batchCreate;
  1660. for($batchIndex = 0; $batchIndex < $count; $batchIndex++) $batchStories[] = $defaultStory;
  1661. if(empty($_SESSION['storyImagesFile'])) return $batchStories;
  1662. $files = $this->session->storyImagesFile;
  1663. $batchStories = array();
  1664. foreach($files as $fileName => $file)
  1665. {
  1666. $defaultStory['title'] = $file['title'];
  1667. $defaultStory['uploadImage'] = $fileName;
  1668. $batchStories[] = $defaultStory;
  1669. }
  1670. return $batchStories;
  1671. }
  1672. /**
  1673. * Get custom fields list.
  1674. *
  1675. * @param object $config
  1676. * @param string $storyType
  1677. * @param bool $hiddenPlan
  1678. * @param object $product
  1679. * @access protected
  1680. * @return array
  1681. */
  1682. protected function getCustomFields(&$config, $storyType, $hiddenPlan, $product)
  1683. {
  1684. $customFields = array();
  1685. /* Attach multi-branch or multi-platform field. */
  1686. if($product->type != 'normal') $customFields[$product->type] = $this->lang->product->branchName[$product->type];
  1687. foreach(explode(',', $config->story->list->customBatchCreateFields) as $field) $customFields[$field] = $this->lang->story->$field;
  1688. if($hiddenPlan) unset($customFields['plan']);
  1689. if($this->app->tab == 'project' || $this->app->tab == 'execution') unset($customFields['parent']);
  1690. if($product->type != 'normal')
  1691. {
  1692. $config->{$storyType}->custom->batchCreateFields = sprintf($config->story->custom->batchCreateFields, $product->type);
  1693. }
  1694. else
  1695. {
  1696. $config->{$storyType}->custom->batchCreateFields = trim(sprintf($config->story->custom->batchCreateFields, ''), ',');
  1697. }
  1698. return $customFields;
  1699. }
  1700. /**
  1701. * Get show fields list.
  1702. *
  1703. * @param string $fieldListStr
  1704. * @param bool $hiddenPlan
  1705. * @param object $product
  1706. * @access protected
  1707. * @return array
  1708. * @param string $storyType
  1709. */
  1710. protected function getShowFields($fieldListStr, $storyType, $product)
  1711. {
  1712. $showFields = $fieldListStr;
  1713. if($product->type == 'normal')
  1714. {
  1715. $showFields = str_replace(array(0 => ",branch,", 1 => ",platform,"), '', ",$showFields,");
  1716. $showFields = trim($showFields, ',');
  1717. }
  1718. return $showFields;
  1719. }
  1720. /**
  1721. * Build story post data for activating the story.
  1722. *
  1723. * @param int $storyID
  1724. * @access protected
  1725. * @return object
  1726. */
  1727. protected function buildStoryForActivate($storyID)
  1728. {
  1729. $postData = form::data($this->config->story->form->activate, $storyID)->get();
  1730. $story = $this->loadModel('file')->processImgURL($postData, $this->config->story->editor->activate['id'], $this->post->uid);
  1731. return $story;
  1732. }
  1733. /**
  1734. * Build story post data for submitReview the story.
  1735. *
  1736. * @param int $storyID
  1737. * @access protected
  1738. * @return object|false
  1739. */
  1740. protected function buildStoryForSubmitReview($storyID)
  1741. {
  1742. if(isset($_POST['reviewer'])) $_POST['reviewer'] = array_filter($_POST['reviewer']);
  1743. if(!$this->post->needNotReview and empty($_POST['reviewer']))
  1744. {
  1745. dao::$errors['reviewer'] = $this->lang->story->errorEmptyReviewedBy;
  1746. return false;
  1747. }
  1748. return form::data($this->config->story->form->submitReview, $storyID)->get();
  1749. }
  1750. /**
  1751. * Get linked objects. e.g. bugs,cases,linkedMRs,linkedCommits,twins,reviewers,relations.
  1752. *
  1753. * @param object $story
  1754. * @access protected
  1755. * @return void
  1756. */
  1757. protected function getLinkedObjects($story)
  1758. {
  1759. $linkedStories = isset($story->linkStoryTitles) ? array_keys($story->linkStoryTitles) : array();
  1760. $this->view->bugs = $this->dao->select('id,title,status,pri,severity')->from(TABLE_BUG)->where('story')->eq($story->id)->andWhere('deleted')->eq(0)->fetchAll();
  1761. $this->view->fromBug = $story->fromBug ? $this->dao->select('id,title')->from(TABLE_BUG)->where('id')->eq($story->fromBug)->fetch() : '';
  1762. $this->view->cases = $this->dao->select('id,title,status,pri')->from(TABLE_CASE)->where('story')->eq($story->id)->andWhere('deleted')->eq(0)->fetchAll();
  1763. $this->view->linkedMRs = $this->loadModel('mr')->getLinkedMRPairs($story->id, 'story');
  1764. $this->view->linkedCommits = $this->loadModel('repo')->getCommitsByObject($story->id, 'story');
  1765. $this->view->modulePath = $this->tree->getParents($story->module);
  1766. $this->view->storyModule = empty($story->module) ? '' : $this->tree->getById($story->module);
  1767. $this->view->storyProducts = $this->dao->select('id,product')->from(TABLE_STORY)->where('id')->in($linkedStories)->fetchPairs();
  1768. $this->view->twins = !empty($story->twins) ? $this->story->getByList($story->twins) : array();
  1769. $this->view->reviewers = $this->story->getReviewerPairs($story->id, $story->version);
  1770. $this->view->relations = $this->story->getStoryRelation($story->id, $story->type);
  1771. }
  1772. /**
  1773. * Set hidden fields for view. like: hiddenPlan.
  1774. *
  1775. * @param object $product
  1776. * @access protected
  1777. * @return void
  1778. */
  1779. protected function setHiddenFieldsForView($product)
  1780. {
  1781. $this->view->hiddenPlan = false;
  1782. if(empty($product->shadow)) return;
  1783. $projectInfo = $this->dao->select('t2.model, t2.multiple')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  1784. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  1785. ->where('t1.product')->eq($product->id)
  1786. ->andWhere('t2.type')->eq('project')
  1787. ->fetch();
  1788. if($projectInfo->model == 'waterfall') $this->view->hiddenPlan = true;
  1789. if($projectInfo->model == 'kanban') $this->view->hiddenPlan = true;
  1790. if(!$projectInfo->multiple) $this->view->hiddenPlan = true;
  1791. }
  1792. /**
  1793. * Convert child ID.
  1794. *
  1795. * @param array $storyIdList
  1796. * @access protected
  1797. * @return array
  1798. */
  1799. protected function convertChildID($storyIdList)
  1800. {
  1801. $storyIdList = array_unique($storyIdList);
  1802. $storyIdList = array_filter($storyIdList);
  1803. foreach($storyIdList as $index => $storyID)
  1804. {
  1805. /* 处理选中的子需求的ID,截取-后的子需求ID。*/
  1806. /* Process selected child story ID. */
  1807. if(strpos((string)$storyID, '-') !== false) $storyIdList[$index] = substr($storyID, strpos($storyID, '-') + 1);
  1808. }
  1809. return $storyIdList;
  1810. }
  1811. /**
  1812. * 处理过滤条件显示内容。
  1813. * Process filter title.
  1814. *
  1815. * @param string $browseType
  1816. * @param int $param
  1817. * @access public
  1818. * @return string
  1819. */
  1820. protected function processFilterTitle($browseType, $param)
  1821. {
  1822. $filter = '';
  1823. $fieldsType = 'execution';
  1824. $searchConfig = $this->session->executionStorysearchParams;
  1825. $searchFields = $this->session->executionStoryForm;
  1826. $featureBar = $this->lang->execution->featureBar['story'];
  1827. if($this->app->tab == 'project' && $this->session->multiple)
  1828. {
  1829. $this->app->loadLang('projectstory');
  1830. $this->app->loadConfig('product');
  1831. $fieldsType = 'product';
  1832. $searchConfig = $this->session->projectstorysearchParams;
  1833. $searchFields = $this->session->projectstoryForm;
  1834. $featureBar = array_merge($this->lang->projectstory->featureBar['story'], $this->lang->projectstory->moreSelects['story']['more']);
  1835. }
  1836. $fieldParams = array();
  1837. if($searchConfig && !empty($searchConfig['params'])) $fieldParams = $searchConfig['params'];
  1838. if($browseType != 'bysearch' && $browseType != 'bymodule' && $browseType != 'byproduct')
  1839. {
  1840. $statusName = zget($featureBar, $browseType, '');
  1841. $filter = sprintf($this->lang->story->report->tpl->feature, $statusName);
  1842. if(!$param) return $filter;
  1843. return $filter . zget($this->lang->search->andor, 'and') . sprintf($this->lang->story->report->tpl->search, $this->config->$fieldsType->search['fields']['module'], '=', zget($fieldParams['module']['values'], $param));
  1844. }
  1845. if($browseType == 'byproduct' && $param)
  1846. {
  1847. $productName = $this->loadModel('product')->getById($param)->name;
  1848. return sprintf($this->lang->story->report->tpl->feature, $productName);
  1849. }
  1850. if($browseType == 'bymodule' && $param)
  1851. {
  1852. if($filter) $filter .= ', ';
  1853. return $filter . sprintf($this->lang->story->report->tpl->search, $this->config->$fieldsType->search['fields']['module'], '=', zget($fieldParams['module']['values'], $param));
  1854. }
  1855. $leftConditions = array();
  1856. $rightConditions = array();
  1857. $fieldNames = $searchConfig['fields'];
  1858. if(!$searchFields) return '';
  1859. $this->app->loadLang('search');
  1860. $groupAndOr = 'and';
  1861. $users = $this->loadModel('user')->getPairs('noletter');
  1862. foreach($searchFields as $index => $field)
  1863. {
  1864. if($index == 6) $groupAndOr = $field['groupAndOr'];
  1865. if(!isset($field['field'])) continue;
  1866. if(isset($field['value']) && $field['value'] === '') continue;
  1867. if(!empty($fieldParams[$field['field']]['values']))
  1868. {
  1869. if($fieldParams[$field['field']]['values'] == 'users') $fieldParams[$field['field']]['values'] = $users;
  1870. $field['value'] = zget($fieldParams[$field['field']]['values'], $field['value']);
  1871. }
  1872. $fieldName = zget($fieldNames, $field['field']);
  1873. $operator = zget($this->lang->search->operators, $field['operator']);
  1874. if($index == 0 || $index == 3) $field['andOr'] = '';
  1875. if($index < 3) $leftConditions[] = zget($this->lang->search->andor, $field['andOr']) . sprintf($this->lang->story->report->tpl->search, $fieldName, $operator, $field['value']);
  1876. elseif($index < 6) $rightConditions[] = zget($this->lang->search->andor, $field['andOr']) . sprintf($this->lang->story->report->tpl->search, $fieldName, $operator, $field['value']);
  1877. }
  1878. if(empty($leftConditions) && empty($rightConditions)) return '';
  1879. if(empty($leftConditions)) return implode('', $rightConditions);
  1880. if(empty($rightConditions)) return implode('', $leftConditions);
  1881. return sprintf($this->lang->story->report->tpl->multi, implode('', $leftConditions), zget($this->lang->search->andor, $groupAndOr), implode('', $rightConditions));
  1882. }
  1883. /**
  1884. * 检查数据是否有变化。
  1885. * Check if data has changed.
  1886. *
  1887. * @param int $storyID
  1888. * @param object $storyData
  1889. * @access public
  1890. * @return bool
  1891. */
  1892. public function checkDataChanged($storyID, $storyData)
  1893. {
  1894. if($this->post->comment) return true;
  1895. if(!empty($storyData->docs)) return true;
  1896. if(!empty($_FILES['files']['name'][0]) || !empty($_POST['renameFiles']) || !empty($_POST['deleteFiles'])) return true;
  1897. $oldStory = $this->story->getByID($storyID);
  1898. if(!empty($oldStory->docs) && empty($_POST['oldDocs'])) return true;
  1899. $changes = common::createChanges($oldStory, $storyData);
  1900. foreach($changes as $index => $change)
  1901. {
  1902. if(in_array($change['field'], array('status', 'version', 'reviewedBy', 'changedBy', 'changedDate', 'reviewedDate', 'docs'))) unset($changes[$index]);
  1903. }
  1904. if(!empty($changes)) return true;
  1905. $reviewers = $this->story->getReviewerPairs($storyID, $oldStory->version);
  1906. $oldStory->reviewer = array_keys($reviewers);
  1907. $diff = array_diff($oldStory->reviewer, $storyData->reviewer) || array_diff($storyData->reviewer, $oldStory->reviewer);
  1908. if($diff) return true;
  1909. $docVersions = !empty($_POST['docVersions']) ? $_POST['docVersions'] : array();
  1910. foreach($docVersions as $docID => $version)
  1911. {
  1912. if(empty($oldStory->docVersions[$docID]) || $oldStory->docVersions[$docID] != $version) return true;
  1913. }
  1914. return false;
  1915. }
  1916. }