model.php 122 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707
  1. <?php
  2. /**
  3. * The model file of action module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package action
  9. * @version $Id: model.php 5028 2013-07-06 02:59:41Z wyd621@gmail.com $
  10. * @link https://www.zentao.net
  11. */
  12. ?>
  13. <?php
  14. class actionModel extends model
  15. {
  16. const BE_UNDELETED = 0; // The deleted object has been undeleted.
  17. const CAN_UNDELETED = 1; // The deleted object can be undeleted.
  18. const BE_HIDDEN = 2; // The deleted object has been hidden.
  19. const MAXCOUNT = 1000;
  20. /**
  21. * 创建一个操作记录。
  22. * Create a action.
  23. *
  24. * @param string $objectType
  25. * @param int $objectID
  26. * @param string $actionType
  27. * @param string|bool $comment
  28. * @param string|int|float $extra the extra info of this action, according to different modules and actions, can set different extra.
  29. * @param string $actor
  30. * @param bool $autoDelete
  31. * @access public
  32. * @return int|bool
  33. */
  34. public function create($objectType, $objectID, $actionType, $comment = '', $extra = '', $actor = '', $autoDelete = true)
  35. {
  36. if(strtolower($actionType) == 'commented' && empty($comment)) return false;
  37. $actor = $actor ? $actor : (!empty($this->app->user->account) ? $this->app->user->account : 'system');
  38. $actionType = strtolower($actionType);
  39. $actor = ($actionType == 'openedbysystem' || $actionType == 'closedbysystem') ? '' : $actor;
  40. if($actor == 'guest' && $actionType == 'logout') return false;
  41. $objectType = str_replace('`', '', $objectType);
  42. $extra = (string)$extra;
  43. $action = new stdclass();
  44. $action->objectType = strtolower($objectType);
  45. $action->objectID = $objectID;
  46. $action->actor = $actor;
  47. $action->action = $actionType;
  48. $action->date = helper::now();
  49. $action->extra = (string)$extra;
  50. if(!$this->app->upgrading) $action->vision = $this->config->vision;
  51. if($objectType == 'story' && in_array($actionType, array('reviewpassed', 'reviewrejected', 'reviewclarified', 'reviewreverted', 'synctwins'))) $action->actor = $this->lang->action->system;
  52. /* 使用purifier处理注解。 */
  53. /* Use purifier to process comment. Fix bug #2683. */
  54. if(empty($comment)) $comment = '';
  55. $action->comment = fixer::stripDataTags($comment);
  56. $uid = $this->post->uid;
  57. if(is_string($uid)) $uid = array($uid);
  58. if(!is_array($uid)) $uid = array();
  59. $this->loadModel('file');
  60. foreach($uid as $value)
  61. {
  62. $action = $this->file->processImgURL($action, 'comment', $value);
  63. if($autoDelete) $this->file->autoDelete($value);
  64. }
  65. /* 获取对象的产品项目以及执行。 */
  66. /* Get product project and execution for this object. */
  67. $relation = $this->getRelatedFields($action->objectType, $objectID, $actionType, $extra);
  68. $action->product = $relation['product'];
  69. $action->project = (int)$relation['project'];
  70. $action->execution = (int)$relation['execution'];
  71. $this->dao->insert(TABLE_ACTION)->data($action)->autoCheck()->exec();
  72. $actionID = $this->dao->lastInsertID();
  73. $hasProductTable = true;
  74. $hasRecentTable = true;
  75. if($this->app->upgrading)
  76. {
  77. $fromVersion = $this->loadModel('setting')->getItem('owner=system&module=common&section=global&key=version');
  78. if(is_numeric($fromVersion[0]) && version_compare($fromVersion, '18.6', '<')) $hasRecentTable = false;
  79. if(strpos($fromVersion, 'pro') !== false) $hasRecentTable = false;
  80. if(strpos($fromVersion, 'biz') !== false && version_compare($fromVersion, 'biz8.6', '<')) $hasRecentTable = false;
  81. if(strpos($fromVersion, 'max') !== false && version_compare($fromVersion, 'max4.6', '<')) $hasRecentTable = false;
  82. if(strpos($fromVersion, 'ipd') !== false && version_compare($fromVersion, 'ipd1.0.1', '<')) $hasRecentTable = false;
  83. if(is_numeric($fromVersion[0]) && version_compare($fromVersion, '21.7.1', '<=')) $hasProductTable = false;
  84. if(strpos($fromVersion, 'pro') !== false) $hasProductTable = false;
  85. if(strpos($fromVersion, 'biz') !== false && version_compare($fromVersion, 'biz12.0', '<=')) $hasProductTable = false;
  86. if(strpos($fromVersion, 'max') !== false && version_compare($fromVersion, 'max7.0', '<=')) $hasProductTable = false;
  87. if(strpos($fromVersion, 'ipd') !== false && version_compare($fromVersion, 'ipd4.0', '<=')) $hasProductTable = false;
  88. }
  89. if($hasProductTable)
  90. {
  91. $productIdList = array_filter(explode(',', $action->product));
  92. foreach($productIdList as $productID) $this->dao->insert(TABLE_ACTIONPRODUCT)->set('action')->eq($actionID)->set('product')->eq($productID)->exec();
  93. }
  94. if($actionType == 'commented')
  95. {
  96. $oldAction = new stdclass();
  97. $oldAction->id = $actionID;
  98. $newAction = clone $oldAction;
  99. $this->file->processFileDiffsForObject('comment', $oldAction, $newAction);
  100. if(!empty($newAction->files))
  101. {
  102. $action->files = $newAction->files;
  103. $this->dao->update(TABLE_ACTION)->set('files')->eq($newAction->files)->where('id')->eq($actionID)->exec();
  104. }
  105. $changes = common::createChanges($oldAction, $newAction);
  106. if($changes) $this->logHistory($actionID, $changes);
  107. }
  108. if($hasRecentTable)
  109. {
  110. $action->id = $actionID;
  111. $this->dao->insert(TABLE_ACTIONRECENT)->data($action)->autoCheck()->exec();
  112. }
  113. $this->file->updateObjectID($uid, $objectID, $objectType);
  114. if(empty($this->app->installing) && empty($this->app->upgrading)) $this->loadModel('message')->send(strtolower($objectType), $objectID, $actionType, $actionID, $actor, $extra);
  115. $this->saveIndex($objectType, $objectID, $actionType);
  116. $changeFunc = 'after' . ucfirst($objectType);
  117. if(method_exists($this, $changeFunc)) call_user_func_array(array($this, $changeFunc), array($action, $actionID));
  118. return $actionID;
  119. }
  120. /**
  121. * 访问任务或者bug时,更新action的read字段。
  122. * Update read field of action when view a task/bug.
  123. *
  124. * @param string $objectType
  125. * @param int $objectID
  126. * @access public
  127. * @return bool
  128. */
  129. public function read($objectType, $objectID)
  130. {
  131. $this->dao->update(TABLE_ACTION)
  132. ->set('`read`')->eq(1)
  133. ->where('objectType')->eq($objectType)
  134. ->andWhere('objectID')->eq($objectID)
  135. ->andWhere('`read`')->eq(0)
  136. ->exec();
  137. return !dao::isError();
  138. }
  139. /**
  140. * 获取对象的产品项目以及执行。
  141. * Get product, project, execution of the object.
  142. *
  143. * @param string $objectType
  144. * @param int $objectID
  145. * @param string $actionType
  146. * @param string $extra
  147. * @access public
  148. * @return array
  149. */
  150. public function getRelatedFields($objectType, $objectID, $actionType = '', $extra = '')
  151. {
  152. /* 处理项目、执行、产品、计划相关。 */
  153. /* Process project, execution, product, plan related. */
  154. if(in_array($objectType, array('project', 'execution', 'product', 'program', 'marketresearch')))
  155. {
  156. list($product, $project, $execution) = $this->actionTao->getNoFilterRequiredRelation($objectType, $objectID);
  157. return array('product' => ',' . implode(',', $product) . ',', 'project' => $project, 'execution' => $execution);
  158. }
  159. /* 过滤不在配置项中的类型。 */
  160. /* Filter object types not in configuration items。 */
  161. $product = array(0);
  162. $project = $execution = 0;
  163. if(strpos($this->config->action->needGetRelateField, ",{$objectType},") !== false)
  164. {
  165. list($product, $project, $execution) = $this->actionTao->getNeedRelatedFields($objectType, $objectID, $actionType, $extra);
  166. if($actionType == 'unlinkedfromproject' || $actionType == 'linked2project') $project = (int)$extra ;
  167. if(in_array($actionType, array('unlinkedfromexecution', 'linked2execution', 'linked2kanban'))) $execution = (int)$extra;
  168. if($execution && !$project) $project = $this->dao->select('project')->from(TABLE_EXECUTION)->where('id')->eq($execution)->fetch('project');
  169. }
  170. return array('product' => is_array($product) ? ',' . implode(',', $product) . ',': ',' . $product . ',', 'project' => $project, 'execution' => $execution);
  171. }
  172. /**
  173. * 根据对象类型和对象ID获取操作记录。
  174. * Get actions by objectType and objectID.
  175. *
  176. * @param string $objectType
  177. * @param array|int $objectID
  178. * @access public
  179. * @return array
  180. */
  181. public function getList($objectType, $objectID)
  182. {
  183. $this->loadModel('file');
  184. $this->loadModel('workflow');
  185. $modules = $objectType == 'module' ? $this->dao->select('id')->from(TABLE_MODULE)->where('root')->in($objectID)->fetchPairs('id') : array();
  186. $commiters = $this->loadModel('user')->getCommiters();
  187. $actions = $this->actionTao->getActionListByTypeAndID($objectType, $objectID, $modules);
  188. $histories = $this->getHistory(array_keys($actions));
  189. $files = $this->file->groupByObjectID('comment', array_keys($actions));
  190. $flowList = array();
  191. if($objectType == 'project') $actions = $this->processProjectActions($actions);
  192. foreach($actions as $actionID => $action)
  193. {
  194. $actionName = strtolower($action->action);
  195. if($this->config->edition != 'open' && !isset($flowList[$action->objectType])) $flowList[$action->objectType] = $this->workflow->getByModule($action->objectType);
  196. if(substr($actionName, 0, 7) == 'linked2') $this->actionTao->getLinkedExtra($action, substr($actionName, 7));
  197. if(substr($actionName, 0, 12) == 'unlinkedfrom') $this->actionTao->getLinkedExtra($action, substr($actionName, 12));
  198. if(in_array($actionName, array('svncommited', 'gitcommited')) && isset($commiters[$action->actor])) $action->actor = $commiters[$action->actor];
  199. if(!in_array($action->objectType, array('feedback', 'ticket')) && $actionName == 'tostory') $this->actionTao->processToStoryActionExtra($action);
  200. if($actionName == 'moved' && $action->objectType != 'module') $this->actionTao->processActionExtra(TABLE_PROJECT, $action, 'name', 'execution', 'task');
  201. if($actionName == 'frombug' && common::hasPriv('bug', 'view')) $action->extra = html::a(helper::createLink('bug', 'view', "bugID={$action->extra}"), $action->extra);
  202. if($actionName == 'importedcard') $this->actionTao->processActionExtra(TABLE_KANBAN, $action, 'name', 'kanban', 'view', true);
  203. if($actionName == 'createchildren') $this->actionTao->processCreateChildrenActionExtra($action);
  204. if($actionName == 'createrequirements') $this->actionTao->processCreateRequirementsActionExtra($action);
  205. if($actionName == 'deletechildrendemand') $this->actionTao->processActionExtra(TABLE_DEMAND, $action, 'title', 'demand', 'view');
  206. if($actionName == 'createchildrendemand') $this->actionTao->processLinkStoryAndBugActionExtra($action, 'demand', 'view');
  207. if($actionName == 'buildopened') $this->actionTao->processActionExtra(TABLE_BUILD, $action, 'name', 'build', 'view');
  208. if($actionName == 'fromlib' && $action->objectType == 'case') $this->actionTao->processActionExtra(TABLE_TESTSUITE, $action, 'name', 'caselib', 'browse', false, helper::hasFeature('caselib'));
  209. if($actionName == 'changedbycharter' && $action->objectType == 'story') $this->actionTao->processActionExtra(TABLE_CHARTER, $action, 'name', 'charter', 'view');
  210. if(($actionName == 'finished' && $objectType == 'todo') || ($actionName == 'closed' && in_array($action->objectType, array('story', 'demand'))) || ($actionName == 'resolved' && $action->objectType == 'bug')) $this->actionTao->processAppendLinkByExtra($action);
  211. if($actionName == 'distributed' && $objectType == 'story') $this->actionTao->processActionExtra(TABLE_DEMAND, $action, 'title', 'demand', 'view', false, $this->config->vision != 'or' ? false : true);
  212. if(in_array($actionName, array('retracted', 'restored'))) $this->actionTao->processActionExtra(TABLE_STORY, $action, 'title', 'story', 'storyView');
  213. if(in_array($actionName, array('totask', 'linkchildtask', 'unlinkchildrentask', 'linkparenttask', 'unlinkparenttask', 'deletechildrentask', 'converttotask')) && $action->objectType != 'feedback') $this->actionTao->processActionExtra(TABLE_TASK, $action, 'name', 'task', 'view');;
  214. if(in_array($actionName, array('linkchildstory', 'unlinkchildrenstory', 'linkparentstory', 'unlinkparentstory', 'deletechildrenstory', 'createchildrenstory'))) $this->actionTao->processActionExtra(TABLE_STORY, $action, 'title', 'story', 'storyView');
  215. if(in_array($actionName, array('testtaskopened', 'testtaskstarted', 'testtaskclosed'))) $this->actionTao->processActionExtra(TABLE_TESTTASK, $action, 'name', 'testtask', 'view');
  216. if(in_array($actionName, array('importfromstorylib', 'importfromrisklib', 'importfromissuelib', 'importfromopportunitylib')) && in_array($this->config->edition, array('max', 'ipd'))) $this->actionTao->processActionExtra(TABLE_ASSETLIB, $action, 'name', 'assetlib', $action->objectType);
  217. if(in_array($actionName, array('opened', 'managed', 'edited')) && in_array($objectType, array('execution', 'project'))) $this->processExecutionAndProjectActionExtra($action);
  218. if(in_array($actionName, array('linkstory', 'unlinkstory', 'linkur', 'unlinkur', 'linkrelatedstory', 'unlinkrelatedstory'))) $this->actionTao->processLinkStoryAndBugActionExtra($action, 'story', 'storyView');
  219. if(in_array($actionName, array('linkbug', 'unlinkbug'))) $this->actionTao->processLinkStoryAndBugActionExtra($action, 'bug', 'view');
  220. if($actionName == 'repocreated') $action->extra = str_replace("class='iframe'", 'data-app="devops"', $action->extra);
  221. if($actionName == 'createdsnapshot' && in_array($action->objectType, array('vm', 'zanode')) && $action->extra == 'defaultSnap') $action->actor = $this->lang->action->system;
  222. if($actionName == 'syncgrade') $this->actionTao->processStoryGradeActionExtra($action);
  223. if(in_array($actionName, array('createdsubtabledata', 'editedsubtabledata', 'deletedsubtabledata'))) $action->extra = !empty($flowList[$action->objectType]->name) ? $flowList[$action->objectType]->name . $action->objectID : '';
  224. $action->history = zget($histories, $actionID, array());
  225. foreach($action->history as $history)
  226. {
  227. if($history->field == 'subversion' || $history->field == 'git')
  228. {
  229. $history->diff = str_replace(array("class='iframe'", '+'), array("data-size='{\"width\": 800, \"height\": 500}' data-toggle='modal'", '%2B'), $history->diff);
  230. }
  231. if(in_array($history->field, array('adjustmemory', 'adjustcpu', 'adjustvol', 'adjustmemorychange', 'adjustcpuchange', 'adjustvolchange')))
  232. {
  233. if(empty($history->newValue) && empty($history->new)) $history->newValue = $this->lang->action->noLimit;
  234. if(empty($history->oldValue) && empty($history->old)) $history->oldValue = $this->lang->action->noLimit;
  235. }
  236. $history->new = !empty($history->newValue) ? $history->newValue : $history->new;
  237. $history->old = !empty($history->oldValue) ? $history->oldValue : $history->old;
  238. $history = $this->file->replaceImgURL($history, 'old,new');
  239. }
  240. $action->comment = $this->file->setImgSize($action->comment, $this->config->action->commonImgSize);
  241. $action->files = !empty($files[$actionID]) ? $files[$actionID] : array();
  242. $actions[$actionID] = $action;
  243. }
  244. return $actions;
  245. }
  246. /**
  247. * 将类型、状态等键值转换为具体的值。
  248. * Process object type, status and etc.
  249. *
  250. * @param object $history
  251. * @access public
  252. * @return object
  253. */
  254. public function processHistory($history = null)
  255. {
  256. if(empty($history)) return $history;
  257. $users = $this->loadModel('user')->getPairs('noletter');
  258. $action = $this->dao->select('objectType,objectID')->from(TABLE_ACTION)->where('id')->eq($history->action)->fetch();
  259. $objectType = $action->objectType == 'story' ? $this->dao->select('type')->from(TABLE_STORY)->where('id')->eq($action->objectID)->fetch('type') : $action->objectType;
  260. $objectTypeList = array();
  261. $history->old = (string)$history->old;
  262. $history->new = (string)$history->new;
  263. if(!isset($objectTypeList[$objectType])) $this->app->loadLang($objectType);
  264. $objectTypeList[$objectType] = $objectType;
  265. if(isset($this->config->action->approvalFields[$history->field]))
  266. {
  267. $fieldListVar = $this->config->action->approvalFields[$history->field];
  268. $fieldList = isset($this->lang->action->{$fieldListVar}) ? $this->lang->action->{$fieldListVar} : array();
  269. if(isset($fieldList[$history->old])) $history->oldValue = $fieldList[$history->old];
  270. if(isset($fieldList[$history->new])) $history->newValue = $fieldList[$history->new];
  271. }
  272. elseif(isset($this->config->action->multipleObjectFields[$objectType][$history->field]))
  273. {
  274. $fieldListVar = $this->config->action->multipleObjectFields[$objectType][$history->field];
  275. $fieldList = isset($this->lang->{$objectType}->{$fieldListVar}) ? $this->lang->{$objectType}->{$fieldListVar} : array();
  276. if(!empty($history->old))
  277. {
  278. $history->oldValue = '';
  279. $oldValues = explode(',', $history->old);
  280. foreach($oldValues as $value) $history->oldValue .= zget($fieldList, $value) . ',';
  281. $history->oldValue = trim($history->oldValue, ',');
  282. }
  283. if(!empty($history->new))
  284. {
  285. $history->newValue = '';
  286. $newValues = explode(',', $history->new);
  287. foreach($newValues as $value) $history->newValue .= zget($fieldList, $value) . ',';
  288. $history->newValue = trim($history->newValue, ',');
  289. }
  290. }
  291. elseif(strpos(",{$this->config->action->userFields},", ",{$history->field},") !== false)
  292. {
  293. if(isset($users[$history->old])) $history->oldValue = $users[$history->old];
  294. if(isset($users[$history->new])) $history->newValue = $users[$history->new];
  295. }
  296. elseif(strpos(",{$this->config->action->multipleUserFields},", ",{$history->field},") !== false)
  297. {
  298. if(!empty($history->old))
  299. {
  300. $history->oldValue = '';
  301. $oldValues = explode(',', $history->old);
  302. foreach($oldValues as $value) $history->oldValue .= zget($users, $value) . ',';
  303. $history->oldValue = trim($history->oldValue, ',');
  304. }
  305. if(!empty($history->new))
  306. {
  307. $history->newValue = '';
  308. $newValues = explode(',', $history->new);
  309. foreach($newValues as $value) $history->newValue .= zget($users, $value) . ',';
  310. $history->newValue = trim($history->newValue, ',');
  311. }
  312. }
  313. else
  314. {
  315. $fieldListVar = isset($this->config->action->objectFields[$objectType][$history->field]) ? $this->config->action->objectFields[$objectType][$history->field] : $history->field . 'List';
  316. $fieldList = isset($this->lang->{$objectType}->{$fieldListVar}) ? $this->lang->{$objectType}->{$fieldListVar} : array();
  317. if(isset($fieldList[$history->old])) $history->oldValue = $fieldList[$history->old];
  318. if(isset($fieldList[$history->new])) $history->newValue = $fieldList[$history->new];
  319. }
  320. /* 如果是升级, 检查oldValue和newValue字段是否存在。以防止从老版本升级的时候,而这两个字段不存在,导致升级失败。 */
  321. if(!empty($this->app->upgrading) && (isset($history->oldValue) || isset($history->newValue)))
  322. {
  323. $existHistory = $this->dao->select('*')->from(TABLE_HISTORY)->limit(1)->fetch();
  324. if(!isset($existHistory->oldValue)) unset($history->oldValue, $history->newValue);
  325. }
  326. return $history;
  327. }
  328. /**
  329. * 将项目行动类型转换为通用行动类型。
  330. * Process Project Actions change actionStype.
  331. *
  332. * @param array $actions
  333. * @access public
  334. * @return array
  335. */
  336. public function processProjectActions($actions)
  337. {
  338. /* 定义行动映射表。 */
  339. /* Define the action map table. */
  340. $map = array();
  341. $map['testtask']['opened'] = 'testtaskopened';
  342. $map['testtask']['started'] = 'testtaskstarted';
  343. $map['testtask']['closed'] = 'testtaskclosed';
  344. $map['build']['opened'] = 'buildopened';
  345. /* 处理action数据。 */
  346. /* Process actions data. */
  347. foreach($actions as $key => $action)
  348. {
  349. if($action->objectType != 'project' && !isset($map[$action->objectType][$action->action])) unset($actions[$key]);
  350. if(isset($map[$action->objectType][$action->action])) $action->action = $map[$action->objectType][$action->action];
  351. }
  352. return $actions;
  353. }
  354. /**
  355. * 获取一条操作记录。
  356. * Get an action record.
  357. *
  358. * @param int $actionID
  359. * @access public
  360. * @return object|bool
  361. */
  362. public function getById($actionID)
  363. {
  364. $action = $this->actionTao->fetchBaseInfo($actionID);
  365. if(!$action) return false;
  366. /* 当action值为repocreated的时候拼接域名。 */
  367. /* Splice domain name for connection when the action is equal to 'repocreated'.*/
  368. if($action->action == 'repocreated') $action->extra = str_replace("href='", "href='" . common::getSysURL(), $action->extra);
  369. return $action;
  370. }
  371. /**
  372. * 获取用户第一条操作。
  373. * Get user first action.
  374. *
  375. * @param string $account
  376. * @access public
  377. * @return object
  378. */
  379. public function getAccountFirstAction($account)
  380. {
  381. return $this->dao->select('*')->from(TABLE_ACTION)->where('actor')->eq($account)->orderBy('id')->limit(1)->fetch();
  382. }
  383. /**
  384. * 获取已删除的对象。
  385. * Get deleted objects.
  386. *
  387. * @param string $objectType
  388. * @param string $type all|hidden
  389. * @param string $orderBy
  390. * @param object $pager
  391. * @access public
  392. * @return array
  393. */
  394. public function getTrashes($objectType, $type, $orderBy, $pager = null)
  395. {
  396. $noMultipleExecutions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('multiple')->eq('0')->andWhere('type')->in('sprint,kanban')->fetchPairs();
  397. $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
  398. $trashes = $this->dao->select('*')->from(TABLE_ACTION)
  399. ->where('action')->eq('deleted')
  400. ->beginIF($objectType != 'all')->andWhere('objectType')->eq($objectType)->fi()
  401. ->beginIF($objectType == 'execution')->andWhere('objectID')->notIn($noMultipleExecutions)->fi()
  402. ->beginIF($objectType == 'all')
  403. ->andWhere('objectType', true)->ne('execution')
  404. ->orWhere('(objectType')->eq('execution')->andWhere('objectID')->notIn($noMultipleExecutions)
  405. ->markRight(2)
  406. ->fi()
  407. ->andWhere('objectType')->notIn($this->config->action->hiddenTrashObjects)
  408. ->andWhere('extra')->eq($extra)
  409. ->andWhere('vision')->eq($this->config->vision)
  410. ->orderBy($orderBy)
  411. ->page($pager)
  412. ->fetchAll('', false);
  413. if(empty($trashes)) return array();
  414. /* 按对象类型对已删除的对象进行分组,并获取名称字段。 */
  415. /* Group trashes by objectType, and get there name field. */
  416. $auditplanIdList = array();
  417. foreach($trashes as $object)
  418. {
  419. $object->objectType = str_replace('`', '', $object->objectType);
  420. if($object->objectType == 'auditplan') $auditplanIdList[$object->objectID] = $object->objectID;
  421. $typeTrashes[$object->objectType][] = $object->objectID;
  422. }
  423. $auditplanList = $this->dao->select('id,objectID,objectType')->from(TABLE_AUDITPLAN)->where('id')->in($auditplanIdList)->fetchAll('id');
  424. foreach($auditplanList as $auditplan) $typeTrashes[$auditplan->objectType][] = $auditplan->objectID;
  425. foreach($typeTrashes as $objectType => $objectIdList)
  426. {
  427. if(!isset($this->config->objectTables[$objectType])) continue;
  428. if($objectType == 'auditplan') continue;
  429. if(!isset($this->config->action->objectNameFields[$objectType])) continue;
  430. $table = $this->config->objectTables[$objectType];
  431. $field = $this->config->action->objectNameFields[$objectType];
  432. $objectIdList = array_unique($objectIdList);
  433. if($objectType == 'pipeline')
  434. {
  435. $objectNames['jenkins'] = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->andWhere('type')->eq('jenkins')->fetchPairs();
  436. $objectNames['gitlab'] = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->andWhere('type')->eq('gitlab')->fetchPairs();
  437. }
  438. elseif($objectType == 'pivot')
  439. {
  440. $objectNames[$objectType] = $this->dao->select("t1.id, t2.{$field} AS name")->from($table)->alias('t1')
  441. ->leftJoin(TABLE_PIVOTSPEC)->alias('t2')->on('t1.id = t2.pivot and t1.version = t2.version')
  442. ->where('t1.id')->in($objectIdList)
  443. ->fetchPairs();
  444. }
  445. elseif($objectType == 'stakeholder')
  446. {
  447. $objectNames[$objectType] = $this->dao->select("t1.id, t2.realname AS name")->from($table)->alias('t1')
  448. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.user = t2.account')
  449. ->where('t1.id')->in($objectIdList)
  450. ->fetchPairs();
  451. }
  452. else
  453. {
  454. $objectNames[$objectType] = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->filterTpl('skip')->fetchPairs();
  455. }
  456. }
  457. /* 将对象名称字段添加到回收站数据中。 */
  458. /* Add name field to the trashes. */
  459. foreach($trashes as $key => $trash)
  460. {
  461. if($trash->objectType == 'pipeline' && isset($objectNames['gitlab'][$trash->objectID])) $trash->objectType = 'gitlab';
  462. if($trash->objectType == 'pipeline' && isset($objectNames['jenkins'][$trash->objectID])) $trash->objectType = 'jenkins';
  463. if($trash->objectType == 'auditplan')
  464. {
  465. $realObjectID = isset($auditplanList[$trash->objectID]) ? $auditplanList[$trash->objectID]->objectID : 0;
  466. $realObjectType = isset($auditplanList[$trash->objectID]) ? $auditplanList[$trash->objectID]->objectType : '';
  467. $trash->objectName = isset($objectNames[$realObjectType][$realObjectID]) ? $objectNames[$realObjectType][$realObjectID] : '';
  468. }
  469. else
  470. {
  471. $trash->objectName = isset($objectNames[$trash->objectType][$trash->objectID]) ? $objectNames[$trash->objectType][$trash->objectID] : '';
  472. }
  473. }
  474. return $trashes;
  475. }
  476. /**
  477. * 通过查询获取回收站内的对象。
  478. * Get deleted objects by search.
  479. *
  480. * @param string $objectType
  481. * @param string $type all|hidden
  482. * @param string|int $queryID
  483. * @param string $orderBy
  484. * @param object $pager
  485. * @access public
  486. * @return array
  487. */
  488. public function getTrashesBySearch($objectType, $type, $queryID, $orderBy, $pager = null)
  489. {
  490. if($objectType == 'all') return array();
  491. if($queryID && $queryID != 'myQueryID')
  492. {
  493. $query = $this->loadModel('search')->getQuery($queryID);
  494. if($query)
  495. {
  496. $this->session->set('trashQuery', $query->sql);
  497. $this->session->set('trashForm', $query->form);
  498. }
  499. else
  500. {
  501. $this->session->set('trashQuery', ' 1 = 1');
  502. }
  503. }
  504. else
  505. {
  506. if($this->session->trashQuery === false) $this->session->set('trashQuery', ' 1 = 1');
  507. }
  508. $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
  509. $table = $this->config->objectTables[$objectType];
  510. $nameField = isset($this->config->action->objectNameFields[$objectType]) ? 't2.' . '`' . $this->config->action->objectNameFields[$objectType] . '`' : '';
  511. $trashQuery = $this->session->trashQuery;
  512. $trashQuery = str_replace(array('`objectID`', '`actor`', '`date`'), array('t1.`objectID`', 't1.`actor`', 't1.`date`'), $trashQuery);
  513. if($nameField) $trashQuery = preg_replace("/`objectName`/", $nameField, $trashQuery);
  514. $queryFields = $objectType != 'pipeline' ? "t1.*, {$nameField} AS objectName" : 't1.*, t1.objectType AS type, t2.name AS objectName, t2.type AS objectType';
  515. $trashes = $this->dao->select($queryFields)->from(TABLE_ACTION)->alias('t1')
  516. ->leftJoin($table)->alias('t2')->on('t1.objectID=t2.id')
  517. ->where('t1.action')->eq('deleted')
  518. ->andWhere($trashQuery)
  519. ->andWhere('t1.extra')->eq($extra)
  520. ->andWhere('t1.vision')->eq($this->config->vision)
  521. ->andWhere('objectType')->notIn($this->config->action->hiddenTrashObjects)
  522. ->beginIF($objectType != 'pipeline' && $objectType != 'all')->andWhere('t1.objectType')->eq($objectType)->fi()
  523. ->beginIF($objectType == 'pipeline')
  524. ->andWhere('(t2.type')->eq('gitlab')
  525. ->orWhere('t2.type')->eq('jenkins')
  526. ->markRight(1)
  527. ->fi()
  528. ->orderBy($orderBy)
  529. ->page($pager)
  530. ->fetchAll('objectID');
  531. return $trashes;
  532. }
  533. /**
  534. * 获取回收站的对象类型列表。
  535. * Get object type list of trashes.
  536. *
  537. * @param string $type
  538. * @access public
  539. * @return array
  540. */
  541. public function getTrashObjectTypes($type)
  542. {
  543. $extra = $type == 'hidden' ? self::BE_HIDDEN : self::CAN_UNDELETED;
  544. $noMultipleExecutions = $this->dao->select('id')->from(TABLE_EXECUTION)->where('multiple')->eq('0')->andWhere('type')->in('sprint,kanban')->fetchPairs();
  545. return $this->dao->select('objectType')->from(TABLE_ACTION)
  546. ->where('action')->eq('deleted')
  547. ->andWhere('extra')->eq($extra)
  548. ->andWhere('objectType', true)->ne('execution')
  549. ->orWhere('(objectType')->eq('execution')->andWhere('objectID')->notIn($noMultipleExecutions)
  550. ->markRight(2)
  551. ->andWhere('vision')->eq($this->config->vision)
  552. ->andWhere('objectType')->notIn($this->config->action->hiddenTrashObjects)
  553. ->fetchAll('objectType');
  554. }
  555. /**
  556. * 获取一个操作的历史记录。
  557. * Get histories of an action.
  558. *
  559. * @param array|int $actionID
  560. * @access public
  561. * @return array
  562. */
  563. public function getHistory($actionID)
  564. {
  565. return $this->dao->select('*')->from(TABLE_HISTORY)->where('action')->in($actionID)->fetchGroup('action');
  566. }
  567. /**
  568. * 记录操作的历史记录。
  569. * Log histories for an action.
  570. *
  571. * @param int $actionID
  572. * @param array $changes
  573. * @access public
  574. * @return bool
  575. */
  576. public function logHistory($actionID, $changes)
  577. {
  578. if(empty($actionID) || empty($changes)) return false;
  579. foreach($changes as $change)
  580. {
  581. $change = is_array($change) ? json_decode(json_encode($change)) : $change;
  582. $change->action = $actionID;
  583. $change = $this->processHistory($change);
  584. $this->dao->insert(TABLE_HISTORY)->data($change)->exec();
  585. if(dao::isError()) return false;
  586. }
  587. return true;
  588. }
  589. /**
  590. * 打印一个对象的所有操作记录。
  591. * Print actions of an object.
  592. *
  593. * @param object $action
  594. * @param string $desc
  595. * @access public
  596. * @return void
  597. */
  598. public function renderAction($action, $desc = '')
  599. {
  600. if(!isset($action->objectType) || !isset($action->action)) return false;
  601. $objectType = $action->objectType;
  602. $actionType = strtolower($action->action);
  603. /**
  604. *
  605. * 设置操作的描述。
  606. *
  607. * 1. 如果模块中定义了操作的描述,使用模块中定义的。
  608. * 2. 如果模块中没有定义,使用公共的操作描述。
  609. * 3. 如果公共的操作描述中没有定义,使用默认的操作描述。
  610. *
  611. * Set the desc string of this action.
  612. *
  613. * 1. If the module of this action has defined desc of this actionType, use it.
  614. * 2. If no defined in the module language, search the common action define.
  615. * 3. If not found in the lang->action->desc, use the $lang->action->desc->common or $lang->action->desc->extra as the default.
  616. */
  617. $this->app->loadLang($objectType == 'module' ? 'tree' : $objectType);
  618. if(empty($desc))
  619. {
  620. if(($action->objectType == 'story' or $action->objectType == 'demand') && $action->action == 'reviewed' && strpos($action->extra, ',') !== false)
  621. {
  622. $desc = $this->lang->{$objectType}->action->rejectreviewed;
  623. }
  624. elseif($action->objectType == 'productplan' && in_array($action->action, array('startedbychild','finishedbychild','closedbychild','activatedbychild', 'createchild')))
  625. {
  626. $desc = $this->lang->{$objectType}->action->changebychild;
  627. }
  628. elseif($action->objectType == 'module' && in_array($action->action, array('created', 'moved', 'deleted')))
  629. {
  630. $desc = $this->lang->{$objectType}->action->{$action->action};
  631. }
  632. elseif(strpos('createmr,editmr,removemr', $action->action) !== false && strpos($action->extra, '::') !== false)
  633. {
  634. $mrAction = str_replace('mr', '', $action->action) . 'Action';
  635. list($mrDate, $mrActor, $mrLink) = explode('::', $action->extra);
  636. if(!$mrActor) $mrActor = $action->actor;
  637. if(is_numeric($mrLink)) $mrLink = helper::createLink('mr', 'view', "mrID={$mrLink}");
  638. if(isInModal()) $mrLink .= ($this->config->requestType == 'GET' ? '&onlybody=yes' : '?onlybody=yes');
  639. $this->app->loadLang('mr');
  640. $desc = sprintf($this->lang->mr->{$mrAction}, $mrDate, $mrActor, $mrLink);
  641. }
  642. elseif(in_array($this->config->edition, array('max', 'ipd')) && strpos($this->config->action->assetType, ",{$action->objectType},") !== false && $action->action == 'approved')
  643. {
  644. $desc = empty($this->lang->action->approve->{$action->extra}) ? '' : $this->lang->action->approve->{$action->extra};
  645. }
  646. elseif(isset($this->lang->{$objectType}) && isset($this->lang->{$objectType}->action->{$actionType}))
  647. {
  648. $desc = $this->lang->{$objectType}->action->{$actionType};
  649. }
  650. elseif(in_array($action->extra, array('autobyparent', 'autobychild')) && isset($this->lang->{$objectType}) && isset($this->lang->{$objectType}->action->{$action->extra . $actionType}))
  651. {
  652. $desc = $this->lang->{$objectType}->action->{$action->extra . $actionType};
  653. }
  654. elseif($action->objectType == 'instance' && isset($this->lang->action->desc->{$actionType}))
  655. {
  656. $desc = $this->lang->action->desc->{$actionType};
  657. $extra = json_decode($action->extra);
  658. if(in_array($actionType, array('adjustmemory', 'adjustcpu', 'adjustvol')))
  659. {
  660. $action->newValue = $action->comment;
  661. $action->comment = '';
  662. if(!empty($action->history))
  663. {
  664. $itemHistory = current($action->history);
  665. $action->newValue = $itemHistory->new;
  666. $action->oldValue = $itemHistory->old;
  667. $desc = $this->lang->action->desc->{$actionType . 'change'};
  668. }
  669. }
  670. if(!empty($extra))
  671. {
  672. $action->extra = '';
  673. if(!empty($extra->data))
  674. {
  675. $action->oldName = zget($extra->data, 'oldName', '');
  676. $action->newName = zget($extra->data, 'newName', '');
  677. $action->oldVersion = zget($extra->data, 'oldVersion', '');
  678. $action->newVersion = zget($extra->data, 'newVersion', '');
  679. $action->oldAppName = zget($extra->data, 'oldAppName', '');
  680. $action->newAppName = zget($extra->data, 'newAppName', '');
  681. }
  682. if(!empty($extra->result->code) && $extra->result->code != 200 && !empty($extra->result->message)) $action->comment = $extra->result->message;
  683. if(is_string($extra->result) && $extra->result != 'fail' && isset($extra->message))
  684. {
  685. $action->comment = "\n" . $extra->message;
  686. }
  687. }
  688. }
  689. elseif($action->action == 'run' && $action->objectType == 'case' && strpos($action->extra, ',') !== false)
  690. {
  691. list($testtaskID, $result) = explode(',', $action->extra);
  692. if($result)
  693. {
  694. $link = '';
  695. $testtask = $this->dao->select('name')->from(TABLE_TESTTASK)->where('id')->eq($testtaskID)->fetch();
  696. if($testtask) $link = common::hasPriv('testtask', 'cases') ? html::a(helper::createLink('testtask', 'cases', "testtaskID=$testtaskID"), ' ' . $testtask->name) : ' ' . $testtask->name;
  697. if(!isset($this->lang->testcase->resultList)) $this->app->loadLang('testcase');
  698. $desc = sprintf($this->lang->action->desc->runresult, "<strong>{$link}</strong>", $result, zget($this->lang->testcase->resultList, $result));
  699. }
  700. else
  701. {
  702. $desc = $this->lang->action->desc->run;
  703. }
  704. }
  705. elseif(isset($this->lang->action->desc->{$actionType}))
  706. {
  707. $desc = $this->lang->action->desc->{$actionType};
  708. }
  709. else
  710. {
  711. $desc = $action->extra ? $this->lang->action->desc->extra : $this->lang->action->desc->common;
  712. }
  713. }
  714. $action->date = substr($action->date, 0, 19);
  715. if($this->app->getViewType() == 'mhtml') $action->date = date('m-d H:i', strtotime($action->date));
  716. /* 遍历actions, 替换变量。 */
  717. /* Cycle actions, replace vars. */
  718. foreach($action as $key => $value)
  719. {
  720. if($key == 'history') continue;
  721. /* 如果desc是数组,替换变量。 */
  722. /* Desc can be an array or string. */
  723. if(is_array($desc))
  724. {
  725. if($key == 'extra') continue;
  726. if($action->objectType == 'story' && $action->action == 'reviewed' && strpos($action->extra, '|') !== false && $key == 'actor')
  727. {
  728. $desc['main'] = str_replace('$actor', $this->lang->action->superReviewer . ' ' . $value, $desc['main']);
  729. }
  730. elseif(!is_array($value))
  731. {
  732. $desc['main'] = str_replace('$' . $key, (string)$value, $desc['main']);
  733. }
  734. }
  735. else
  736. {
  737. if(in_array($actionType, array('restoredsnapshot', 'createdsnapshot')) && in_array($action->objectType, array('vm', 'zanode')) && $value == 'defaultSnap') $value = $this->lang->{$objectType}->snapshot->defaultSnapName;
  738. if(!is_array($value) && !is_object($value)) $desc = str_replace('$' . $key, (string)$value, $desc);
  739. }
  740. }
  741. /* 如果desc是数组,处理extra。 */
  742. /* If the desc is an array, process extra. Please bug/lang. */
  743. if(!is_array($desc)) return $desc;
  744. $extra = strtolower($action->extra);
  745. /* Fix bug #741. */
  746. if(isset($desc['extra']))
  747. {
  748. if($objectType == 'story')
  749. {
  750. $story = $this->fetchByID($action->objectID, $objectType);
  751. if($story->type != 'story') $this->app->loadLang($story->type);
  752. $desc['extra'] = $this->lang->{$story->type}->{$desc['extra']};
  753. }
  754. else
  755. {
  756. $desc['extra'] = $this->lang->{$objectType}->{$desc['extra']};
  757. }
  758. }
  759. $actionDesc = '';
  760. if(isset($desc['extra'][$extra]))
  761. {
  762. $actionDesc = str_replace('$extra', $desc['extra'][$extra], $desc['main']);
  763. }
  764. else
  765. {
  766. $actionDesc = str_replace('$extra', $action->extra, $desc['main']);
  767. }
  768. if(($action->objectType == 'story' or $action->objectType == 'demand') && $action->action == 'reviewed')
  769. {
  770. if(strpos($action->extra, ',') !== false)
  771. {
  772. list($extra, $reason) = explode(',', $extra);
  773. $desc['reason'] = $this->lang->{$objectType}->{$desc['reason']};
  774. $actionDesc = str_replace(array('$extra', '$reason'), array($desc['extra'][$extra], $desc['reason'][$reason]), $desc['main']);
  775. }
  776. if(strpos($action->extra, '|') !== false)
  777. {
  778. list($extra, $isSuperReviewer) = explode('|', $extra);
  779. $actionDesc = str_replace('$extra', $desc['extra'][$extra], $desc['main']);
  780. }
  781. }
  782. if($action->objectType == 'story' && $action->action == 'synctwins')
  783. {
  784. if(!empty($extra) && strpos($extra, '|') !== false)
  785. {
  786. list($operate, $storyID) = explode('|', $extra);
  787. $desc['operate'] = $this->lang->{$objectType}->{$desc['operate']};
  788. $link = common::hasPriv('story', 'view') ? html::a(helper::createLink('story', 'view', "storyID=$storyID"), "#$storyID ") : "#$storyID";
  789. $actionDesc = str_replace(array('$extra', '$operate'), array($link, $desc['operate'][$operate]), $desc['main']);
  790. }
  791. }
  792. $isCloseStory = $action->objectType == 'story' && $action->action == 'closed';
  793. if($isCloseStory && !empty($extra) && strpos($extra, '|') !== false)
  794. {
  795. list($extra) = explode('|', $extra);
  796. if(!empty($desc['extra'][$extra])) $actionDesc = str_replace('$extra', $desc['extra'][$extra], $desc['main']);
  797. }
  798. if(($action->objectType == 'story' || $action->objectType == 'demand') && $action->action == 'fromboard')
  799. {
  800. $actionExtra = html::a(helper::createLink('board', 'view', "canvasID={$action->extra}"), $action->extra);
  801. $actionDesc = str_replace('$extra', $actionExtra, $desc['main']);
  802. }
  803. if($action->objectType == 'module' && strpos(',created,moved,', $action->action) !== false)
  804. {
  805. $moduleNames = $this->loadModel('tree')->getOptionMenu($action->objectID, 'story', 0, 'all', '');
  806. $modules = explode(',', $action->extra);
  807. $moduleNames = array_intersect_key($moduleNames, array_combine($modules, $modules));
  808. $moduleNames = implode(', ', $moduleNames);
  809. $actionDesc = str_replace('$extra', $moduleNames, $desc['main']);
  810. }
  811. elseif($action->objectType == 'module' && $action->action == 'deleted')
  812. {
  813. $module = $this->dao->select('*')->from(TABLE_MODULE)->where('id')->eq($action->objectID)->fetch();
  814. $moduleNames = $this->loadModel('tree')->getOptionMenu($module->root, 'story', 0, 'all', '');
  815. $actionDesc = str_replace('$extra', (string)zget($moduleNames, $action->objectID), $desc['main']);
  816. }
  817. if($action->objectType == 'board' && in_array($action->action, array('importstory', 'importdemand', 'importrequirement', 'importepic', 'convertdemand', 'convertepic', 'convertrequirement', 'convertstory')))
  818. {
  819. if($action->action == 'importstory' || $action->action == 'convertstory')
  820. {
  821. $story = $this->loadModel('story')->fetchById((int)$action->extra);
  822. $link = helper::createLink('story', 'view', "storyID={$action->extra}");
  823. }
  824. if($action->action == 'convertrequirement' || $action->action == 'importrequirement')
  825. {
  826. $story = $this->loadModel('story')->fetchById((int)$action->extra);
  827. $link = helper::createLink('requirement', 'view', "storyID={$action->extra}");
  828. }
  829. if($action->action == 'importdemand' || $action->action == 'convertdemand')
  830. {
  831. $story = $this->loadModel('demand')->fetchByID((int)$action->extra);
  832. $link = helper::createLink('demand', 'view', "demandID={$action->extra}");
  833. }
  834. if($action->action == 'importepic' || $action->action == 'convertepic')
  835. {
  836. $story = $this->loadModel('story')->fetchByID((int)$action->extra);
  837. $link = helper::createLink('epic', 'view', "epicID={$action->extra}");
  838. }
  839. $link .= $this->config->requestType == 'GET' ? '&onlybody=yes' : '?onlybody=yes';
  840. $replace = $story ? html::a('', "#$action->extra {$story->title}", '', "class='story-link' data-href='$link'") : '';
  841. $actionDesc = str_replace('$extra', $replace, $desc['main']);
  842. }
  843. return $actionDesc;
  844. }
  845. /**
  846. * 格式化操作备注。
  847. * Format action comment.
  848. *
  849. * @param string $comment
  850. * @access public
  851. * @return string
  852. */
  853. public function formatActionComment($comment)
  854. {
  855. if(strpos($comment, '<pre class="prettyprint lang-html">') !== false)
  856. {
  857. $before = explode('<pre class="prettyprint lang-html">', $comment);
  858. $after = explode('</pre>', $before[1]);
  859. $htmlCode = $after[0];
  860. return $before[0] . htmlspecialchars($htmlCode) . $after[1];
  861. }
  862. return strip_tags($comment) === $comment
  863. ? nl2br($comment)
  864. : $comment;
  865. }
  866. /**
  867. * 构建操作记录列表,便于前端组件进行渲染。
  868. * Build action list for render by frontend component.
  869. *
  870. * @param array $actions
  871. * @param array $users
  872. * @param bool $commentEditable
  873. * @access public
  874. * @return array
  875. */
  876. public function buildActionList($actions, $users = null, $commentEditable = true)
  877. {
  878. if(empty($users)) $users = $this->loadModel('user')->getPairs('noletter');
  879. $list = array();
  880. $endAction = end($actions);
  881. $account = $this->app->user->account;
  882. foreach($actions as $action)
  883. {
  884. $item = new stdClass();
  885. if(strlen(trim(($action->comment))) !== 0)
  886. {
  887. $item->comment = $this->formatActionComment($action->comment);
  888. $currentUserCanEdit = $action->id == $endAction->id && $action->actor == $account && $action->action == 'commented';
  889. $item->commentEditable = $commentEditable && common::hasPriv('action', 'editComment') && $currentUserCanEdit;
  890. }
  891. if($action->action === 'assigned' || $action->action === 'toaudit')
  892. {
  893. $action->extra = zget($users, $action->extra);
  894. if(strpos($action->extra, ':') !== false) $action->extra = substr($action->extra, strpos($action->extra, ':') + 1);
  895. }
  896. if(!isset($action->rawActor)) $action->rawActor = $action->actor;
  897. $action->actor = zget($users, $action->actor);
  898. if(strpos($action->actor, ':') !== false) $action->actor = substr($action->actor, strpos($action->actor, ':') + 1);
  899. if(!empty($action->history)) $item->historyChanges = $this->renderChanges($action->objectType, $action->objectID, $action->history);
  900. $item->id = $action->id;
  901. $item->action = $action->action;
  902. $item->hasRendered = true;
  903. $item->content = $this->renderAction($action);
  904. if(!empty($action->files)) $item->files = array_values($action->files);
  905. if(strlen(trim(($action->comment))) !== 0)
  906. {
  907. $isCurrentUserCreated = $action->rawActor == $account;
  908. $item->comment = $this->formatActionComment($action->comment);
  909. $item->commentEditable = $commentEditable && $endAction->id == $action->id && $isCurrentUserCreated && common::hasPriv('action', 'editComment');
  910. }
  911. if($action->objectType == 'instance' && in_array($action->action, array('adjustmemory', 'adjustcpu', 'adjustvol'))) unset($item->comment);
  912. $list[] = $item;
  913. }
  914. return $list;
  915. }
  916. /**
  917. * 打印一个对象的所有操作记录。
  918. * Print actions of an object.
  919. *
  920. * @param object $action
  921. * @param string $desc
  922. * @access public
  923. * @return void
  924. */
  925. public function printAction($action, $desc = '')
  926. {
  927. $content = $this->renderAction($action, $desc);
  928. if(is_string($content)) echo $content;
  929. return;
  930. }
  931. /**
  932. * 获取动态。
  933. * Get actions as dynamic.
  934. *
  935. * @param string $account
  936. * @param string $period
  937. * @param string $orderBy
  938. * @param int $limit
  939. * @param string|int $productID all|int(like 123)|notzero all => include zero, notzero, greater than 0
  940. * @param string|int $projectID same as productID
  941. * @param string|int $executionID same as productID
  942. * @param string $date
  943. * @param string $direction
  944. * @access public
  945. * @return array
  946. */
  947. public function getDynamic($account = 'all', $period = 'all', $orderBy = 'date_desc', $limit = 50, $productID = 'all', $projectID = 'all', $executionID = 'all', $date = '', $direction = 'next')
  948. {
  949. /* 计算时间段的开始和结束时间。 */
  950. /* Computer the begin and end date of a period. */
  951. $beginAndEnd = $this->computeBeginAndEnd($period, $date, $direction);
  952. $actionCondition = $this->getActionCondition();
  953. if(!$actionCondition && !$this->app->user->admin && isset($this->app->user->rights['acls']['actions']))
  954. {
  955. $this->session->set('actionQueryCondition', null, $this->app->tab);
  956. return array();
  957. }
  958. /* 构建权限搜索条件。 */
  959. /* Build has priv search condition. */
  960. $condition = "`objectType` != 'effort'";
  961. $noMultipleExecutions = $this->dao->select('id')->from(TABLE_PROJECT)->where('multiple')->eq(0)->andWhere('type')->in('sprint,kanban')->fetchPairs();
  962. if($noMultipleExecutions) $condition = "({$condition}) AND (`objectType` != 'execution' OR (`objectType` = 'execution' AND `objectID`" . (count($noMultipleExecutions) == 1 ? ' != ' . reset($noMultipleExecutions) : ' NOT ' . helper::dbIN($noMultipleExecutions)) . "))";
  963. $actions = $this->actionTao->getActionListByCondition("({$condition})", $beginAndEnd['begin'], $beginAndEnd['end'], $account, $productID, $projectID, $executionID, $actionCondition, $orderBy, $limit);
  964. if(!$actions) return array();
  965. $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'action');
  966. return $this->transformActions($actions);
  967. }
  968. /**
  969. * 通过产品获取动态。
  970. * Get actions as dynamic by product.
  971. *
  972. * @param int $productID
  973. * @param string $account
  974. * @param string $period
  975. * @param string $orderBy
  976. * @param int $limit
  977. * @param string $date
  978. * @param string $direction
  979. * @access public
  980. * @return array
  981. */
  982. public function getDynamicByProduct($productID, $account = 'all', $period = 'all', $orderBy = 'date_desc', $limit = 50, $date = '', $direction = 'next')
  983. {
  984. return $this->getDynamic($account, $period, $orderBy, $limit, (int)$productID, 'all', 'all', $date, $direction);
  985. }
  986. /**
  987. * 通过项目获取动态。
  988. * Get actions as dynamic by project.
  989. *
  990. * @param int $projectID
  991. * @param string $account
  992. * @param string $period
  993. * @param string $orderBy
  994. * @param int $limit
  995. * @param string $date
  996. * @param string $direction
  997. * @access public
  998. * @return array
  999. */
  1000. public function getDynamicByProject($projectID, $account = 'all', $period = 'all', $orderBy = 'date_desc', $limit = 50, $date = '', $direction = 'next')
  1001. {
  1002. return $this->getDynamic($account, $period, $orderBy, $limit, 'all', (int)$projectID, 'all', $date, $direction);
  1003. }
  1004. /**
  1005. * 通过执行获取动态。
  1006. * Get actions as dynamic by execution.
  1007. *
  1008. * @param int $executionID
  1009. * @param string $account
  1010. * @param string $period
  1011. * @param string $orderBy
  1012. * @param int $limit
  1013. * @param string $date
  1014. * @param string $direction
  1015. * @access public
  1016. * @return array
  1017. */
  1018. public function getDynamicByExecution($executionID, $account = 'all', $period = 'all', $orderBy = 'date_desc', $limit = 50, $date = '', $direction = 'next')
  1019. {
  1020. return $this->getDynamic($account, $period, $orderBy, $limit, 'all', 'all', (int)$executionID, $date, $direction);
  1021. }
  1022. /**
  1023. * 通过账户获取动态。
  1024. * Get actions as dynamic by account.
  1025. *
  1026. * @param string $account
  1027. * @param string $period
  1028. * @param string $orderBy
  1029. * @param int $limit
  1030. * @param string $date
  1031. * @param string $direction
  1032. * @access public
  1033. * @return array
  1034. */
  1035. public function getDynamicByAccount($account = '', $period = 'all', $orderBy = 'date_desc', $limit = 50, $date = '', $direction = 'next')
  1036. {
  1037. if(empty($account)) $account = $this->app->user->account;
  1038. return $this->getDynamic($account, $period, $orderBy, $limit, 'all', 'all', 'all', $date, $direction);
  1039. }
  1040. /**
  1041. * 通过视野获取用户可访问的动态类型。
  1042. * Get the action types that the user can access through the vision.
  1043. *
  1044. * @param string $module
  1045. * @access public
  1046. * @return string
  1047. */
  1048. public function getActionCondition($module = '')
  1049. {
  1050. if($this->app->user->admin) return '';
  1051. $actionCondition = array();
  1052. if(isset($this->app->user->rights['acls']['actions']))
  1053. {
  1054. if(empty($this->app->user->rights['acls']['actions'])) return '1 != 1';
  1055. if($module && !isset($this->app->user->rights['acls']['actions'][$module])) return '1 != 1';
  1056. foreach($this->app->user->rights['acls']['actions'] as $moduleName => $actions)
  1057. {
  1058. if($module && $module != $moduleName) continue;
  1059. if(isset($this->lang->mainNav->{$moduleName}) && !empty($this->app->user->rights['acls']['views']) && !isset($this->app->user->rights['acls']['views'][$moduleName])) continue;
  1060. $actionCondition[] = "(`objectType` = '{$moduleName}' AND `action` " . helper::dbIN(array_keys($actions)) . ")";
  1061. }
  1062. }
  1063. return implode(' OR ', $actionCondition);
  1064. }
  1065. /**
  1066. * 搜索获取动态。
  1067. * Get dynamic by search.
  1068. *
  1069. * @param int $queryID
  1070. * @param string $orderBy
  1071. * @param int $limit
  1072. * @param string $date
  1073. * @param string $direction
  1074. * @access public
  1075. * @return array
  1076. */
  1077. public function getDynamicBySearch($queryID, $orderBy = 'date_desc', $limit = 50, $date = '', $direction = 'next')
  1078. {
  1079. $query = $queryID ? $this->loadModel('search')->getQuery($queryID) : '';
  1080. /* 获取sql和表单内容。 */
  1081. /* Get sql and form content. */
  1082. if($query)
  1083. {
  1084. $this->session->set('actionQuery', $query->sql);
  1085. $this->session->set('actionForm', $query->form);
  1086. }
  1087. if($this->session->actionQuery == false) $this->session->set('actionQuery', ' 1 = 1');
  1088. $allProducts = "`product` = 'all'";
  1089. $allProjects = "`project` = 'all'";
  1090. $allExecutions = "`execution` = 'all'";
  1091. $actionQuery = $this->session->actionQuery;
  1092. $productID = 0;
  1093. if(preg_match("/`product` = '(\d*)'/", $actionQuery, $out)) $productID = $out[1];
  1094. /* 如果查询条件中包含所有产品的查询条件,不限制产品。 */
  1095. /* If the query condition include all products, no limit product. */
  1096. if(strpos($actionQuery, $allProducts) !== false) $actionQuery = str_replace($allProducts, '1 = 1', $actionQuery);
  1097. /* 如果查询条件中包含所有项目的查询条件,不限制项目。 */
  1098. /* If the query condition include all projects, no limit project. */
  1099. if(strpos($actionQuery, $allProjects) !== false) $actionQuery = str_replace($allProjects, '1 = 1', $actionQuery);
  1100. /* 如果查询条件中包含所有执行的查询条件,不限制执行。 */
  1101. /* If the query condition include all executions, no limit execution. */
  1102. if(strpos($actionQuery, $allExecutions) !== false) $actionQuery = str_replace($allExecutions, '1 = 1', $actionQuery);
  1103. if($productID) $actionQuery = str_replace(" `product` = '{$productID}'", " t2.`product` = '{$productID}'", $actionQuery);
  1104. if($date) $actionQuery = "({$actionQuery}) AND " . ('date' . ($direction == 'next' ? '<' : '>') . "'{$date}'");
  1105. /* 如果当前版本为lite,则过滤掉产品相关的动态。 */
  1106. /* If this vision is lite, delete product actions. */
  1107. if($this->config->vision == 'lite') $actionQuery .= " AND objectType != 'product'";
  1108. $actionQuery .= " AND vision = '{$this->config->vision}'";
  1109. $actions = $this->getBySQL($actionQuery, $orderBy, $limit);
  1110. $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'action');
  1111. return $actions ? $this->transformActions($actions) : array();
  1112. }
  1113. /**
  1114. * 通过sql获取actions。
  1115. * Get actions by SQL.
  1116. *
  1117. * @param string $sql
  1118. * @param string $orderBy
  1119. * @param int $limit
  1120. * @access public
  1121. * @return array
  1122. */
  1123. public function getBySQL($sql, $orderBy, $limit = 50)
  1124. {
  1125. $actionCondition = $this->getActionCondition();
  1126. $actionCondition = str_replace(' `action`', ' action.`action`', $actionCondition);
  1127. $hasProduct = preg_match('/t2\.([`"]?)product/', $sql);
  1128. /* 构建权限搜索条件。 */
  1129. /* Build has priv search condition. */
  1130. $condition = "`objectType` != 'effort'";
  1131. $noMultipleExecutions = $this->dao->select('id')->from(TABLE_PROJECT)->where('multiple')->eq(0)->andWhere('type')->in('sprint,kanban')->fetchPairs();
  1132. if($noMultipleExecutions) $condition = "({$condition}) AND (`objectType` != 'execution' OR (`objectType` = 'execution' AND `objectID`" . (count($noMultipleExecutions) == 1 ? ' != ' . reset($noMultipleExecutions) : ' NOT ' . helper::dbIN($noMultipleExecutions)) . "))";
  1133. return $this->dao->select('action.*')->from(TABLE_ACTION)->alias('action')
  1134. ->beginIF($hasProduct)->leftJoin(TABLE_ACTIONPRODUCT)->alias('t2')->on('action.id=t2.action')->fi()
  1135. ->where('objectType')->notIN($this->config->action->ignoreObjectType4Dynamic)
  1136. ->andWhere('action.action')->notIN($this->config->action->ignoreActions4Dynamic)
  1137. ->andWhere("({$sql})")
  1138. ->andWhere("({$condition})")
  1139. ->beginIF(!empty($actionCondition))->andWhere("($actionCondition)")->fi()
  1140. ->orderBy($orderBy)
  1141. ->limit($limit)
  1142. ->fetchAll('id', false);
  1143. }
  1144. /**
  1145. * 转换动态用于显示。
  1146. * Transform the actions for display.
  1147. *
  1148. * @param array $actions
  1149. * @access public
  1150. * @return array
  1151. */
  1152. public function transformActions($actions)
  1153. {
  1154. /* 获取评论用户以及当前登陆用户的本部门用户。 */
  1155. /* Get the commenters and the users of the current user's department. */
  1156. $commiters = $this->loadModel('user')->getCommiters();
  1157. $deptUsers = isset($this->app->user->dept) ? $this->loadModel('dept')->getDeptUserPairs((int)$this->app->user->dept, 'id') : '';
  1158. /* 通过action获取对象名称,所属项目以及需求。 */
  1159. /* Get object names, object projects and requirements by actions. */
  1160. list($objectNames, $relatedProjects, $requirements, $epics) = $this->getRelatedDataByActions($actions);
  1161. $projectGroups = array();
  1162. foreach($relatedProjects as $objectType => $idList) $projectGroups = array_merge($projectGroups, $idList);
  1163. /* If idList include ',*,' Format ',*,' to '*'. */
  1164. $projectIdList = array();
  1165. foreach($projectGroups as $idList)
  1166. {
  1167. $idList = explode(',', (string)$idList);
  1168. foreach($idList as $id) $projectIdList[] = $id;
  1169. }
  1170. if($projectIdList) $projectIdList = array_unique($projectIdList);
  1171. if($projectIdList) $projects = $this->dao->select('id,name,isTpl')->from(TABLE_PROJECT)->where('project')->in($projectIdList)->andWhere('deleted')->eq('0')->fetchAll('id');
  1172. $docIdList = array();
  1173. $apiIdList = array();
  1174. $docLibIdList = array();
  1175. foreach($actions as $action)
  1176. {
  1177. if($action->objectType == 'doc' || $action->objectType == 'doctemplate') $docIdList[$action->objectID] = $action->objectID;
  1178. if($action->objectType == 'doclib') $docLibIdList[$action->objectID] = $action->objectID;
  1179. if($action->objectType == 'api') $apiIdList[$action->objectID] = $action->objectID;
  1180. }
  1181. /* 获取需要验证的元素列表。 */
  1182. /* Get the list of elements that need to be verified. */
  1183. $shadowProducts = $this->dao->select('id')->from(TABLE_PRODUCT)->where('shadow')->eq(1)->fetchPairs();
  1184. $projectMultiples = $this->dao->select('id,type,multiple')->from(TABLE_PROJECT)->where('id')->in($projectIdList)->fetchAll('id');
  1185. $docList = array();
  1186. $apiList = array();
  1187. $docLibList = array();
  1188. $this->loadModel('doc');
  1189. if($docIdList)
  1190. {
  1191. $docList = $this->dao->select("`id`,`addedBy`,`type`,`lib`,`acl`,`users`,`readUsers`,`groups`,`readGroups`,`status`,`path`,`deleted`")->from(TABLE_DOC)->where('id')->in($docIdList)->fetchAll('id');
  1192. $docList = $this->doc->batchCheckPrivDoc($docList);
  1193. }
  1194. if($docLibIdList)
  1195. {
  1196. $docLibList = $this->dao->select('*')->from(TABLE_DOCLIB)->where('id')->in($docLibIdList)->fetchAll('id');
  1197. foreach($docLibList as $docLib)
  1198. {
  1199. if(!$this->doc->checkPrivLib($docLib)) unset($docLibList[$docLib->id]);
  1200. }
  1201. }
  1202. if($apiIdList)
  1203. {
  1204. $apiList = $this->dao->select('*')->from(TABLE_DOCLIB)->where('id')->in($docLibIdList)->fetchAll('id');
  1205. foreach($apiList as $api)
  1206. {
  1207. if(!$this->doc->checkPrivLib($api)) unset($apiList[$api->id]);
  1208. }
  1209. }
  1210. foreach($actions as $i => $action)
  1211. {
  1212. /* 如果doc,api,doclib,product类型对应的对象不存在,则从actions中删除。*/
  1213. /* If the object corresponding to the doc, api, doclib, and product types does not exist, it will be deleted from actions. */
  1214. if(!$this->actionTao->checkIsActionLegal($action, $shadowProducts, $docList, $apiList, $docLibList))
  1215. {
  1216. unset($actions[$i]);
  1217. continue;
  1218. }
  1219. $actionType = strtolower($action->action);
  1220. $objectType = strtolower($action->objectType);
  1221. $projectID = isset($relatedProjects[$action->objectType][$action->objectID]) ? $relatedProjects[$action->objectType][$action->objectID] : 0;
  1222. $this->loadModel($action->objectType);
  1223. $action->originalDate = $action->date;
  1224. $action->date = date(DT_MONTHTIME2, strtotime($action->date));
  1225. $action->actionLabel = isset($this->lang->{$objectType}->{$actionType}) ? $this->lang->{$objectType}->{$actionType} : $action->action;
  1226. $action->actionLabel = isset($this->lang->action->label->{$actionType}) ? $this->lang->action->label->{$actionType} : $action->actionLabel;
  1227. $action->objectLabel = $this->getObjectLabel($objectType, $action->objectID, $actionType, $requirements, $epics);
  1228. $action->major = isset($this->config->action->majorList[$action->objectType]) && in_array($action->action, $this->config->action->majorList[$action->objectType]) ? 1 : 0;
  1229. if($actionType == 'svncommited' || $actionType == 'gitcommited') $action->actor = zget($commiters, $action->actor);
  1230. /* 设置对象的名称和链接。 */
  1231. /* Set object name and set object link. */
  1232. $this->actionTao->addObjectNameForAction($action, $objectNames, $objectType);
  1233. $this->setObjectLink($action, $deptUsers, $shadowProducts, zget($projectMultiples, $projectID, ''), $projectIdList);
  1234. if($action->objectType == 'project' && !empty($projects[$action->objectID]->isTpl))
  1235. {
  1236. $action->objectLabel = $this->lang->project->template;
  1237. $action->objectLink = helper::createLink('project', 'execution', "status=undone&projectID={$action->objectID}");
  1238. }
  1239. elseif($action->objectType == 'project' && $action->action == 'managedeliverable')
  1240. {
  1241. $action->objectLink = helper::createLink('project', 'deliverable', "projectID={$action->objectID}"); // 交付物链接
  1242. }
  1243. elseif($action->objectType == 'execution' && $action->action == 'managedeliverable')
  1244. {
  1245. $action->objectLink = helper::createLink('execution', 'view', "executionID={$action->objectID}"); // 交付物链接
  1246. }
  1247. }
  1248. return $actions;
  1249. }
  1250. /**
  1251. * 通过actions获取关联的数据。
  1252. * Get related data by actions.
  1253. *
  1254. * @param array $actions
  1255. * @access public
  1256. * @return array
  1257. */
  1258. public function getRelatedDataByActions($actions)
  1259. {
  1260. /* Init object type array. */
  1261. $objectTypes = array();
  1262. foreach($actions as $object) $objectTypes[$object->objectType][$object->objectID] = $object->objectID;
  1263. if(isset($objectTypes['todo'])) $this->app->loadLang('todo');
  1264. if(isset($objectTypes['branch'])) $this->app->loadLang('branch');
  1265. $users = isset($objectTypes['gapanalysis']) || isset($objectTypes['stakeholder']) ? $this->loadModel('user')->getPairs('noletter') : array();
  1266. $objectNames = $relatedProjects = $requirements = $epics = array();
  1267. foreach($objectTypes as $objectType => $objectIdList)
  1268. {
  1269. if(!isset($this->config->objectTables[$objectType]) && strpos(',makeup,pivot,', ",{$objectType},") === false) continue; // If no defination for this type, omit it.
  1270. if(isset($this->config->objectTables[$objectType])) $table = $this->config->objectTables[$objectType];
  1271. if($objectType == 'makeup') $table = TABLE_OVERTIME;
  1272. if($objectType == 'pivot') $table = TABLE_PIVOTSPEC;
  1273. if($objectType == 'auditplan') continue;
  1274. $field = zget($this->config->action->objectNameFields, $objectType, '');
  1275. if(empty($field)) continue;
  1276. /* Get object name, related projects, requirements. */
  1277. list($objectName, $relatedProject, $requirements, $epics) = $this->getObjectRelatedData($table, $objectType, $objectIdList, $field, $users, $requirements, $epics);
  1278. if($objectType == 'branch' && in_array(BRANCH_MAIN, $objectIdList)) $objectName[BRANCH_MAIN] = $this->lang->branch->main;
  1279. $objectNames[$objectType] = $objectName;
  1280. $relatedProjects[$objectType] = $relatedProject;
  1281. }
  1282. $objectNames['user'][0] = 'guest'; // Add guest account.
  1283. return array($objectNames, $relatedProjects, $requirements, $epics);
  1284. }
  1285. /**
  1286. * 获取对象的标签。
  1287. * Get object label.
  1288. *
  1289. * @param string $objectType
  1290. * @param int $objectID
  1291. * @param string $actionType
  1292. * @param array $requirements
  1293. * @param array $epics
  1294. * @access public
  1295. * @return string
  1296. */
  1297. public function getObjectLabel($objectType, $objectID, $actionType, $requirements, $epics)
  1298. {
  1299. $actionObjectLabel = $objectType;
  1300. if(isset($this->lang->action->label->{$objectType}))
  1301. {
  1302. $objectLabel = $this->lang->action->label->{$objectType};
  1303. /* 用户故事替换为需求。 */
  1304. /* Replace story to requirement. */
  1305. if(isset($requirements[$objectID]) && is_string($objectLabel)) $objectLabel = str_replace($this->lang->SRCommon, $this->lang->URCommon, $objectLabel);
  1306. if(isset($epics[$objectID]) && is_string($objectLabel)) $objectLabel = str_replace($this->lang->SRCommon, $this->lang->ERCommon, $objectLabel);
  1307. if(!is_array($objectLabel)) $actionObjectLabel = $objectLabel;
  1308. if(is_array($objectLabel) && isset($objectLabel[$actionType])) $actionObjectLabel = $objectLabel[$actionType];
  1309. if($objectType == 'module')
  1310. {
  1311. $moduleType = $this->dao->select('type')->from(TABLE_MODULE)->where('id')->eq($objectID)->fetch('type');
  1312. if($moduleType == 'doc')
  1313. {
  1314. $this->app->loadLang('doc');
  1315. $actionObjectLabel = $this->lang->doc->menuTitle;
  1316. }
  1317. }
  1318. if($objectType == 'system' && strpos(strtolower($actionType), 'backup') !== false) $actionObjectLabel = '';
  1319. }
  1320. if(in_array($this->config->edition, array('max', 'ipd')) && $objectType == 'assetlib')
  1321. {
  1322. $libType = $this->dao->select('type')->from(TABLE_ASSETLIB)->where('id')->eq($objectID)->fetch('type');
  1323. if(strpos('story,issue,risk,opportunity,practice,component', $libType) !== false) $actionObjectLabel = $this->lang->action->label->{$libType . 'assetlib'};
  1324. }
  1325. return $actionObjectLabel;
  1326. }
  1327. /**
  1328. * 检查Action是否设置链接。
  1329. * Check if action has link.
  1330. *
  1331. * @param object $action
  1332. * @param array $projectIdList
  1333. * @access public
  1334. * @return bool
  1335. */
  1336. public function checkForSetLink($action, $projectIdList = array())
  1337. {
  1338. static $parents = array();
  1339. if(empty($parents) && !empty($projectIdList)) $parents = $this->dao->select('parent')->from(TABLE_PROJECT)->where('project')->in($projectIdList)->andWhere('deleted')->eq('0')->fetchPairs('parent', 'parent');
  1340. if($action->objectType == 'execution' && isset($parents[$action->objectID])) return false;
  1341. if($this->app->user->admin) return true;
  1342. if($action->execution && strpos(",{$this->app->user->view->sprints},", ",{$action->execution},") === false) return false;
  1343. if($action->project && strpos(",{$this->app->user->view->projects},", ",{$action->project},") === false) return false;
  1344. $hasPriv = false;
  1345. $products = array();
  1346. if($action->product) $products = array_unique(array_filter(explode(',', $action->product)));
  1347. if($products)
  1348. {
  1349. foreach($products as $productID)
  1350. {
  1351. if(strpos(",{$this->app->user->view->products},", ",{$productID},") !== false) $hasPriv = true;
  1352. }
  1353. if(!$hasPriv) return false;
  1354. }
  1355. return true;
  1356. }
  1357. /**
  1358. * 设置action的objectLink属性。
  1359. * Set the objectLink attribute of action.
  1360. *
  1361. * @param object $action
  1362. * @param array $deptUsers
  1363. * @param array $shadowProducts
  1364. * @param object|string $project
  1365. * @param array $projectIdList
  1366. * @access public
  1367. * @return object|bool
  1368. */
  1369. public function setObjectLink($action, $deptUsers, $shadowProducts, $project = "", $projectIdList = array())
  1370. {
  1371. $action->objectLink = $moduleName = $methodName = $params = '';
  1372. $action->objectLabel = zget($this->lang->action->objectTypes, $action->objectLabel);
  1373. $canSetLink = $this->checkForSetLink($action, $projectIdList);
  1374. $canParseLink = strpos($action->objectLabel, '|') !== false;
  1375. if(!$canParseLink && !$canSetLink) return $action;
  1376. if($canParseLink)
  1377. {
  1378. list($objectLabel, $moduleName, $methodName, $vars) = explode('|', $action->objectLabel);
  1379. $action->objectLabel = $objectLabel;
  1380. $action->product = trim((string)$action->product, ',');
  1381. if($action->objectType == 'module') return $action;
  1382. if(!$canSetLink) return $action;
  1383. if(in_array($action->objectType, array('program', 'project', 'product', 'execution')))
  1384. {
  1385. $objectTable = zget($this->config->objectTables, $action->objectType);
  1386. $objectDeleted = $this->dao->select('deleted')->from($objectTable)->where('id')->eq($action->objectID)->fetch('deleted');
  1387. if($objectDeleted) return $action;
  1388. }
  1389. if(in_array($this->config->edition, array('max', 'ipd')) && strpos($this->config->action->assetType, ",{$action->objectType},") !== false && empty($action->project) && empty($action->product) && empty($action->execution))
  1390. {
  1391. $this->actionTao->processMaxDocObjectLink($action, $moduleName, $methodName, $vars);
  1392. }
  1393. else
  1394. {
  1395. if($action->objectType !== 'doclib') $params = $this->actionTao->getObjectLinkParams($action, $vars);
  1396. if($action->objectType == 'doclib')
  1397. {
  1398. list($moduleName, $methodName, $params) = $this->actionTao->getDoclibTypeParams($action);
  1399. }
  1400. elseif($action->objectType == 'story')
  1401. {
  1402. $story = $this->loadModel('story')->fetchByID($action->objectID);
  1403. if(!empty($story))
  1404. {
  1405. $moduleName = $story->type;
  1406. $methodName = 'view';
  1407. if(isset($shadowProducts[$story->product]))
  1408. {
  1409. $moduleName = 'projectstory';
  1410. $methodName = 'view';
  1411. }
  1412. }
  1413. if(!empty($action->project) && !$project)
  1414. {
  1415. $project = $this->loadModel('project')->fetchById($action->project);
  1416. if(empty($project->multiple))
  1417. {
  1418. $moduleName = 'execution';
  1419. $methodName = 'storyView';
  1420. }
  1421. }
  1422. }
  1423. }
  1424. }
  1425. if($action->objectType == 'team') list($moduleName, $methodName, $params) = $this->getObjectTypeTeamParams($action);
  1426. if($action->objectType == 'story' && $this->config->vision == 'lite') list($moduleName, $methodName, $params) = array('projectstory', 'view', "storyID={$action->objectID}");
  1427. if($action->objectType == 'review') list($moduleName, $methodName, $params) = array('review', 'view', "reviewID={$action->objectID}");
  1428. if($action->objectType == 'build' && $this->app->tab == 'project') $moduleName = 'projectbuild';
  1429. if(empty($action->hasLink) && $this->actionTao->checkActionClickable($action, $deptUsers, $moduleName, $methodName)) $action->objectLink = helper::createLink($moduleName, $methodName, $params);
  1430. if(!empty($action->objectLink) && !empty($project) && empty($project->multiple)) $action->objectLink .= '#app=project'; // Set app for no multiple project.
  1431. if(!empty($action->objectLink) && $action->objectType == 'meeting') $action->objectLink .= '#app=' . $this->app->tab; // Set app for meeting by open tab.
  1432. if($this->config->vision == 'lite' && $action->objectType == 'module') $action->objectLink .= '#app=project';
  1433. if($action->objectType == 'nc' && !empty($action->execution)) $action->objectLink .= '#app=execution';
  1434. return $action;
  1435. }
  1436. /**
  1437. * 根据给定的一段时间的参数计算日期的开始和结束。
  1438. * Compute the begin date and end date of a period.
  1439. *
  1440. * @param string $period all|today|yesterday|twodaysago|latest2days|thisweek|lastweek|thismonth|lastmonth
  1441. * @param string $date
  1442. * @param string $direction pre|next
  1443. * @access public
  1444. * @return array
  1445. */
  1446. public function computeBeginAndEnd($period, $date, $direction)
  1447. {
  1448. $period = strtolower($period);
  1449. $beginDate = '';
  1450. $year = date('Y');
  1451. /* 查询所有动态时最多查询最后两年的数据。 */
  1452. /* When query all dynamic then query the data of the last two years at most. */
  1453. if($this->app->getMethodName() == 'dynamic') $year = $year - 1;
  1454. $beginDate = $year . '-01-01';
  1455. $beginAndEnd = array('begin' => $beginDate, 'end' => FUTURE_DATE);
  1456. $this->app->loadClass('date');
  1457. $today = date('Y-m-d');
  1458. $tomorrow = date::tomorrow();
  1459. $yesterday = date::yesterday();
  1460. $twoDaysAgo = date::twoDaysAgo();
  1461. if($period == 'today') $beginAndEnd = array('begin' => $today, 'end' => $tomorrow);
  1462. if($period == 'yesterday') $beginAndEnd = array('begin' => $yesterday, 'end' => $today);
  1463. if($period == 'twodaysago') $beginAndEnd = array('begin' => $twoDaysAgo, 'end' => $yesterday);
  1464. if($period == 'latest3days') $beginAndEnd = array('begin' => $twoDaysAgo, 'end' => $tomorrow);
  1465. /* 如果时间段为周,则给结束日期增加结束时间。 */
  1466. /* If the period is by week, add the end time to the end date. */
  1467. if($period == 'thisweek' || $period == 'lastweek')
  1468. {
  1469. $func = "get$period";
  1470. extract(date::$func());
  1471. $beginAndEnd = array('begin' => $begin, 'end' => $end);
  1472. }
  1473. if($period == 'thismonth') $beginAndEnd = date::getThisMonth();
  1474. if($period == 'lastmonth') $beginAndEnd = date::getLastMonth();
  1475. /* 确切的日期。 */
  1476. /* The exact date. */
  1477. if($date)
  1478. {
  1479. if($direction == 'pre') $beginAndEnd['begin'] = $date;
  1480. if($direction == 'next') $beginAndEnd['end'] = $date;
  1481. }
  1482. return $beginAndEnd;
  1483. }
  1484. /**
  1485. * 渲染每一个action的历史记录。
  1486. * Render histories of every action.
  1487. *
  1488. * @param string $objectType
  1489. * @param int $objectID
  1490. * @param array $histories
  1491. * @param bool $canChangeTag
  1492. * @access public
  1493. * @return string
  1494. */
  1495. public function renderChanges($objectType, $objectID, $histories, $canChangeTag = true)
  1496. {
  1497. if(empty($histories)) return '';
  1498. $maxLength = 0; // The max length of fields names.
  1499. $historiesWithDiff = array(); // To save histories without diff info.
  1500. $historiesWithoutDiff = array(); // To save histories with diff info.
  1501. /* 加载工作流新增的语言包。*/
  1502. if($this->config->edition != 'open')
  1503. {
  1504. $flow = $this->loadModel('workflow')->getByModule($objectType);
  1505. if(!empty($flow)) $this->loadModel('common')->loadCustomLang($objectType, 'view');
  1506. }
  1507. /* 区别是否有diff信息,以便于将有diff信息的字段放在最后。 */
  1508. /* Diff histories by hasing diff info or not. Thus we can to make sure the field with diff show at last. */
  1509. if($objectType == 'doctemplate') $this->app->loadLang('doc');
  1510. foreach($histories as $history)
  1511. {
  1512. $fieldName = $history->field;
  1513. $history->fieldLabel = isset($this->lang->{$objectType}) && isset($this->lang->{$objectType}->{$fieldName}) ? $this->lang->{$objectType}->{$fieldName} : $fieldName;
  1514. if($objectType == 'module') $history->fieldLabel = $this->lang->tree->{$fieldName};
  1515. if($objectType == 'doctemplate') $history->fieldLabel = zget($this->lang->docTemplate, $fieldName, zget($this->lang->doc, $fieldName, $fieldName));
  1516. if($fieldName == 'fileName') $history->fieldLabel = $this->lang->file->{$fieldName};
  1517. if(($length = strlen($history->fieldLabel)) > $maxLength) $maxLength = $length;
  1518. $history->diff ? $historiesWithDiff[] = $history : $historiesWithoutDiff[] = $history;
  1519. }
  1520. $histories = array_merge($historiesWithoutDiff, $historiesWithDiff);
  1521. /* 处理历史记录中的差别。 */
  1522. /* Process the diff of histories. */
  1523. $content = '';
  1524. foreach($histories as $history)
  1525. {
  1526. $history->fieldLabel = str_pad($history->fieldLabel, $maxLength, $this->lang->action->label->space);
  1527. if(strpos(',addDiff,removeDiff,', ",{$history->field},") !== false)
  1528. {
  1529. $fileLabel = isset($this->lang->file->common) ? $this->lang->file->common . ' ' : '';
  1530. $content .= sprintf($this->lang->action->desc->{$history->field}, strpos(',project,execution,', ",{$objectType},") !== false ? '' : $fileLabel, $history->diff) . '<br/>';
  1531. }
  1532. elseif($history->diff != '')
  1533. {
  1534. $history->diff = str_replace(array('<ins>', '</ins>', '<del>', '</del>'), array('[ins]', '[/ins]', '[del]', '[/del]'), $history->diff);
  1535. $history->diff = $history->field != 'subversion' && $history->field != 'git' ? htmlSpecialString($history->diff) : $history->diff; // Keep the diff link.
  1536. $history->diff = str_replace(array('[ins]', '[/ins]', '[del]', '[/del]'), array('<ins>', '</ins>', '<del>', '</del>'), $history->diff);
  1537. $history->diff = nl2br($history->diff);
  1538. $history->noTagDiff = $canChangeTag ? preg_replace('/&lt;\/?([a-z][a-z0-9]*)[^\/]*\/?&gt;/Ui', '', $history->diff) : '';
  1539. $content .= sprintf($this->lang->action->desc->diff2, $history->fieldLabel, $history->noTagDiff, $history->diff);
  1540. }
  1541. else
  1542. {
  1543. $content .= sprintf($this->lang->action->desc->diff1, $history->fieldLabel, $history->old, $history->new);
  1544. }
  1545. }
  1546. return $content;
  1547. }
  1548. /**
  1549. * 打印每一个action的历史记录。
  1550. * Print histories of every action.
  1551. *
  1552. * @param string $objectType
  1553. * @param int $objectID
  1554. * @param array $histories
  1555. * @param bool $canChangeTag
  1556. * @access public
  1557. * @return void
  1558. */
  1559. public function printChanges($objectType, $objectID, $histories, $canChangeTag = true)
  1560. {
  1561. $content = $this->renderChanges($objectType, $objectID, $histories, $canChangeTag);
  1562. if(is_string($content)) echo $content;
  1563. }
  1564. /**
  1565. * 通过对象类型删除action。
  1566. * Delete action by objectType.
  1567. *
  1568. * @param string $objectType
  1569. * @access public
  1570. * @return bool
  1571. */
  1572. public function deleteByType($objectType)
  1573. {
  1574. $this->dao->delete()->from(TABLE_ACTION)->where('objectType')->eq($objectType)->exec();
  1575. return !dao::isError();
  1576. }
  1577. /**
  1578. * 恢复一条记录。
  1579. * Undelete a record.
  1580. *
  1581. * @param int $actionID
  1582. * @access public
  1583. * @return string|bool
  1584. */
  1585. public function undelete($actionID)
  1586. {
  1587. if($actionID <= 0) return false;
  1588. $action = $this->getById($actionID);
  1589. if(!$action || $action->action != 'deleted') return false;
  1590. list($table, $orderby, $field, $queryKey) = $this->actionTao->getUndeleteParamsByObjectType($action->objectType);
  1591. if(empty($queryKey)) $queryKey = 'id';
  1592. $object = $this->actionTao->getObjectBaseInfo($table, array($queryKey => $action->objectID), $field, $orderby);
  1593. if(empty($object)) return false;
  1594. $result = $this->checkActionCanUndelete($action, $object);
  1595. if($result !== true) return $result;
  1596. /* 恢复被删除的元素。 */
  1597. /* Resotre deleted object. */
  1598. if($action->objectType == 'doc') $table = TABLE_DOC;
  1599. $this->dao->update($table)->set('deleted')->eq(0)->where('id')->eq($action->objectID)->exec();
  1600. if($action->objectType == 'module')
  1601. {
  1602. $module = $this->loadModel('tree')->getById($action->objectID);
  1603. if($module->type == 'doc' && $module->parent > 0)
  1604. {
  1605. $parentActionID = $this->dao->select('id')->from(TABLE_ACTION)->where('objectType')->eq('module')->andWhere('objectID')->eq($module->parent)->andWhere('action')->eq('deleted')->orderBy('id_desc')->fetch('id');
  1606. if($parentActionID) $this->undelete($parentActionID);
  1607. }
  1608. }
  1609. if($action->objectType == 'projectchange')
  1610. {
  1611. $reviewID = $this->dao->select('id')->from(TABLE_REVIEW)->where('type')->eq('projectchange')->andWhere('object')->eq($action->objectID)->andWhere('deleted')->eq('1')->fetch('id');
  1612. if($reviewID)
  1613. {
  1614. $reviewActionID = $this->dao->select('id')->from(TABLE_ACTION)->where('action')->eq('deleted')->andWhere('objectType')->eq('review')->andWhere('objectID')->eq($reviewID)->andWhere('extra')->eq(self::CAN_UNDELETED)->fetch('id');
  1615. if($reviewActionID) $this->undelete($reviewActionID);
  1616. }
  1617. }
  1618. if($action->objectType == 'review')
  1619. {
  1620. $review = $this->dao->select('*')->from(TABLE_REVIEW)->where('id')->eq($action->objectID)->fetch();
  1621. if($review->type == 'projectchange')
  1622. {
  1623. $projectchangeActionID = $this->dao->select('id')->from(TABLE_ACTION)->where('action')->eq('deleted')->andWhere('objectType')->eq('projectchange')->andWhere('objectID')->eq($review->object)->andWhere('extra')->eq(self::CAN_UNDELETED)->fetch('id');
  1624. if($projectchangeActionID) $this->undelete($projectchangeActionID);
  1625. }
  1626. }
  1627. if($action->objectType == 'deliverable')
  1628. {
  1629. $this->dao->update(TABLE_DELIVERABLE)->set('status')->eq('disabled')->where('id')->eq($action->objectID)->exec();
  1630. }
  1631. $this->recoverRelatedData($action, $object);
  1632. /* 还原已删除的需求时重算OR需求和业用研需的阶段。 */
  1633. /* The stage of recalculating OR requirements and industrial research needs when restoring deleted requirements. */
  1634. if(in_array($action->objectType, array('story', 'epic', 'requirement')))
  1635. {
  1636. $this->loadModel('story')->setStage($action->objectID);
  1637. $this->story->updateParentStatus($action->objectID);
  1638. }
  1639. if($action->objectType == 'demand' && !empty($object->parent)) $this->loadModel('demand')->updateParentDemandStage($object->parent);
  1640. if($action->objectType == 'release') $this->loadModel('system')->setSystemRelease((int)$object->system, $action->objectID);
  1641. if(in_array($action->objectType, array('release', 'build')) && !empty($object->system))
  1642. {
  1643. $systemActionID = $this->dao->select('id')->from(TABLE_ACTION)->where('objectType')->eq('system')->andWhere('objectID')->eq($object->system)->andWhere('action')->eq('deleted')->orderBy('id_desc')->fetch('id');
  1644. if($systemActionID) $this->undelete($systemActionID);
  1645. }
  1646. /* 在action表中更新action记录。 */
  1647. /* Update action record in action table. */
  1648. $this->dao->update(TABLE_ACTION)->set('extra')->eq(actionModel::BE_UNDELETED)->where('id')->eq($actionID)->exec();
  1649. $this->create($action->objectType, $action->objectID, 'undeleted');
  1650. return true;
  1651. }
  1652. /**
  1653. * 隐藏一个对象。
  1654. * Hide an object.
  1655. *
  1656. * @param int $actionID
  1657. * @access public
  1658. * @return bool
  1659. */
  1660. public function hideOne($actionID)
  1661. {
  1662. $action = $this->getById($actionID);
  1663. if(!$action || $action->action != 'deleted') return false;
  1664. $this->dao->update(TABLE_ACTION)->set('extra')->eq(self::BE_HIDDEN)->where('id')->eq($actionID)->exec();
  1665. $this->create($action->objectType, $action->objectID, 'hidden');
  1666. return !dao::isError();
  1667. }
  1668. /**
  1669. * 隐藏所有被删除的对象。
  1670. * Hide all deleted objects.
  1671. *
  1672. * @access public
  1673. * @return bool
  1674. */
  1675. public function hideAll()
  1676. {
  1677. $this->dao->update(TABLE_ACTION)
  1678. ->set('extra')->eq(self::BE_HIDDEN)
  1679. ->where('action')->eq('deleted')
  1680. ->andWhere('extra')->eq(self::CAN_UNDELETED)
  1681. ->exec();
  1682. return !dao::isError();
  1683. }
  1684. /**
  1685. * 更新一个action的评论。
  1686. * Update comment of a action.
  1687. *
  1688. * @param int $actionID
  1689. * @param object $newComment
  1690. * @access public
  1691. * @return bool
  1692. */
  1693. public function updateComment($actionID, $newComment)
  1694. {
  1695. $action = $this->getById($actionID);
  1696. if(!$action) return false;
  1697. $action->files = $this->loadModel('file')->getByObject('comment', $actionID);
  1698. /* 只保留允许的标签。 */
  1699. /* Keep only allowed tags. */
  1700. $newComment->comment = fixer::stripDataTags($newComment->lastComment);
  1701. /* 处理评论内的图片。*/
  1702. /* Handle images in comment. */
  1703. $newComment = $this->loadModel('file')->processImgURL($newComment, 'comment', $newComment->uid);
  1704. $this->dao->update(TABLE_ACTION)
  1705. ->set('date')->eq(helper::now())
  1706. ->set('comment')->eq($newComment->comment)
  1707. ->where('id')->eq($actionID)
  1708. ->exec();
  1709. $this->file->updateObjectID($newComment->uid, $action->objectID, $action->objectType);
  1710. $this->file->processFileDiffsForObject('comment', $action, $newComment);
  1711. if(!empty($newComment->files))
  1712. {
  1713. $action->files = $newComment->files;
  1714. $this->dao->update(TABLE_ACTION)->set('files')->eq($newComment->files)->where('id')->eq($actionID)->exec();
  1715. }
  1716. $changes = common::createChanges($action, $newComment);
  1717. if($changes) $this->logHistory($actionID, $changes);
  1718. return true;
  1719. }
  1720. /**
  1721. * 根据actions构建日期组。
  1722. * Build date group by actions
  1723. *
  1724. * @param array $actions
  1725. * @param string $direction
  1726. * @param string $orderBy date_desc|date_asc
  1727. * @access public
  1728. * @return array
  1729. */
  1730. public function buildDateGroup($actions, $direction = 'next', $orderBy = 'date_desc')
  1731. {
  1732. $dateGroup = array();
  1733. $firstTime = 0;
  1734. $timeStamp = 0;
  1735. foreach($actions as $action)
  1736. {
  1737. $timeStamp = strtotime(isset($action->originalDate) ? $action->originalDate : $action->date);
  1738. $date = date(DT_DATE3, $timeStamp);
  1739. $action->time = date(DT_TIME2, $timeStamp);
  1740. $dateGroup[$date][] = $action;
  1741. if(empty($firstTime)) $firstTime = $timeStamp;
  1742. }
  1743. /* 将日期的顺序修改正确。 */
  1744. /* Modify date to the corrret order. */
  1745. if($dateGroup && $firstTime < $timeStamp)
  1746. {
  1747. $dateGroup = array_reverse($dateGroup);
  1748. foreach($dateGroup as $key => $dateItem) $dateGroup[$key] = array_reverse($dateItem);
  1749. }
  1750. return $dateGroup;
  1751. }
  1752. /**
  1753. * 检查是否有上一条或者下一条。
  1754. * Check Has pre or next.
  1755. *
  1756. * @param string $date
  1757. * @param string $direction
  1758. * @param string $period
  1759. * @access public
  1760. * @return bool
  1761. */
  1762. public function hasPreOrNext($date, $direction = 'next', $period = 'all')
  1763. {
  1764. if(empty($date)) return false;
  1765. $condition = $this->session->actionQueryCondition;
  1766. if(empty($condition)) return false;
  1767. $beginAndEnd = $this->computeBeginAndEnd($period, '', $direction);
  1768. $hasProduct = preg_match('/t2\.([`"]?)product/', $condition);
  1769. $condition = preg_replace("/AND +`?date`?\s*(<|>|<=|>=)\s*'\d{4}\-\d{2}\-\d{2}'/", '', $condition);
  1770. $actions = $this->dao->select('action.id')->from(TABLE_ACTION)->alias('action')
  1771. ->beginIF($hasProduct)->leftJoin(TABLE_ACTIONPRODUCT)->alias('t2')->on('action.id=t2.action')->fi()
  1772. ->where($condition)
  1773. ->andWhere('`date`' . ($direction == 'next' ? '<' : '>') . "'{$date}'")
  1774. ->beginIF($direction == 'next' && $beginAndEnd['begin'] != EPOCH_DATE)->andWhere('date')->ge($beginAndEnd['begin'])->fi()
  1775. ->beginIF($direction != 'next' && $beginAndEnd['end'] != FUTURE_DATE)->andWhere('date')->le($beginAndEnd['end'])->fi()
  1776. ->limit(1)
  1777. ->fetchAll();
  1778. return count($actions) > 0;
  1779. }
  1780. /**
  1781. * 保存全局搜索对象索引信息。
  1782. * Save global search object index information.
  1783. *
  1784. * @param string $objectType
  1785. * @param int $objectID
  1786. * @param string $actionType
  1787. * @access public
  1788. * @return bool
  1789. */
  1790. public function saveIndex($objectType, $objectID, $actionType)
  1791. {
  1792. $this->loadModel('search');
  1793. if($this->config->edition != 'open' && $this->app->isServing()) $this->loadModel('workflow')->appendSearchConfig();
  1794. $actionType = strtolower($actionType);
  1795. if(!isset($this->config->search->fields->{$objectType})) return false;
  1796. $isCommentedAction = $actionType == 'commented';
  1797. if(strpos($this->config->search->buildAction, ",{$actionType},") === false && !$isCommentedAction && empty($_POST['comment'])) return false;
  1798. if($isCommentedAction && empty($_POST['actioncomment'])) return false;
  1799. if($actionType == 'deleted' || $actionType == 'erased') return $this->search->deleteIndex($objectType, $objectID);
  1800. $field = $this->config->search->fields->{$objectType};
  1801. $query = $this->search->buildIndexQuery($objectType, false);
  1802. $data = $query->andWhere('t1.' . $field->id)->eq($objectID)->fetch();
  1803. if(empty($data)) return false;
  1804. $data->comment = '';
  1805. if($objectType == 'effort' && $data->objectType == 'task') return false;
  1806. if($objectType == 'case')
  1807. {
  1808. $caseStep = $this->dao->select('`desc`,`expect`')->from(TABLE_CASESTEP)->where('`case`')->eq($objectID)->andWhere('version')->eq($data->version)->fetchAll();
  1809. $data->desc = '';
  1810. $data->expect = '';
  1811. foreach($caseStep as $step)
  1812. {
  1813. $data->desc .= $step->desc . "\n";
  1814. $data->expect .= $step->expect . "\n";
  1815. }
  1816. }
  1817. $actions = $this->dao->select('*')->from(TABLE_ACTION)
  1818. ->where('objectType')->eq($objectType)
  1819. ->andWhere('objectID')->eq($objectID)
  1820. ->orderBy('id asc')
  1821. ->fetchAll();
  1822. foreach($actions as $action)
  1823. {
  1824. if($action->action == 'opened') $data->{$field->addedDate} = $action->date;
  1825. $data->{$field->editedDate} = $action->date;
  1826. if(!empty($action->comment)) $data->comment .= $action->comment . "\n";
  1827. }
  1828. $this->search->saveIndex($objectType, $data);
  1829. return !dao::isError();
  1830. }
  1831. /**
  1832. * 打印API(极狐)对象上的操作。
  1833. * Print actions of an object for API(JIHU).
  1834. *
  1835. * @param object $action
  1836. * @access public
  1837. * @return false|void
  1838. */
  1839. public function printActionForGitLab($action)
  1840. {
  1841. if(!isset($action->objectType) || !isset($action->action)) return false;
  1842. $actionType = strtolower($action->action);
  1843. if(isset($this->lang->action->apiTitle->{$actionType}) && isset($action->extra))
  1844. {
  1845. /* 如果extra列是一个用户名,则组装链接。 */
  1846. /* If extra column is a username, then assemble link to that. */
  1847. if($action->action == "assigned")
  1848. {
  1849. $user = $this->loadModel('user')->getById($action->extra);
  1850. if($user)
  1851. {
  1852. $url = helper::createLink('user', 'profile', "userID={$user->id}");
  1853. $action->extra = "<a href='{$url}' target='_blank'>{$action->extra}</a>";
  1854. }
  1855. }
  1856. echo sprintf($this->lang->action->apiTitle->{$actionType}, $action->extra);
  1857. }
  1858. elseif(isset($this->lang->action->apiTitle->{$actionType}) && !isset($action->extra))
  1859. {
  1860. echo $this->lang->action->apiTitle->{$actionType};
  1861. }
  1862. else
  1863. {
  1864. echo $actionType;
  1865. }
  1866. }
  1867. /**
  1868. * 处理操作记录用于API。
  1869. * Process action for API.
  1870. *
  1871. * @param array|object $actions
  1872. * @param array|object $users
  1873. * @param array|object $objectLang
  1874. * @access public
  1875. * @return array
  1876. */
  1877. public function processActionForAPI($actions, $users = array(), $objectLang = array())
  1878. {
  1879. if(is_object($actions)) $actions = (array)$actions;
  1880. if(is_object($users)) $users = (array)$users;
  1881. if(is_object($objectLang)) $objectLang = (array)$objectLang;
  1882. foreach($actions as $action)
  1883. {
  1884. $action->actor = zget($users, $action->actor);
  1885. if($action->action == 'assigned') $action->extra = zget($users, $action->extra);
  1886. if(strpos($action->actor, ':') !== false) $action->actor = substr($action->actor, strpos($action->actor, ':') + 1);
  1887. ob_start();
  1888. $this->printAction($action);
  1889. $action->desc = ob_get_contents();
  1890. ob_end_clean();
  1891. if($action->history)
  1892. {
  1893. foreach($action->history as $i => $history)
  1894. {
  1895. $history->fieldName = zget($objectLang, $history->field);
  1896. $action->history[$i] = $history;
  1897. }
  1898. }
  1899. }
  1900. return array_values($actions);
  1901. }
  1902. /**
  1903. * 处理动态用于API。
  1904. * Process dynamic for API.
  1905. *
  1906. * @param array $dynamics
  1907. * @access public
  1908. * @return array
  1909. */
  1910. public function processDynamicForAPI($dynamics)
  1911. {
  1912. /* 获取用户列表。 */
  1913. /* Get user list. */
  1914. $users = $this->loadModel('user')->getList();
  1915. $simplifyUsers = array();
  1916. foreach($users as $user)
  1917. {
  1918. $simplifyUser = new stdclass();
  1919. $simplifyUser->id = $user->id;
  1920. $simplifyUser->account = $user->account;
  1921. $simplifyUser->realname = $user->realname;
  1922. $simplifyUser->avatar = $user->avatar;
  1923. $simplifyUsers[$user->account] = $simplifyUser;
  1924. }
  1925. $actions = array();
  1926. foreach($dynamics as $dynamic)
  1927. {
  1928. if($dynamic->objectType == 'user') continue; //过滤掉用户动态。
  1929. $simplifyUser = zget($simplifyUsers, $dynamic->actor, '');
  1930. $actor = $simplifyUser;
  1931. if(empty($simplifyUser))
  1932. {
  1933. $actor = new stdclass();
  1934. $actor->id = 0;
  1935. $actor->account = $dynamic->actor;
  1936. $actor->realname = $dynamic->actor;
  1937. $actor->avatar = '';
  1938. }
  1939. $dynamic->actor = $actor;
  1940. $actions[] = $dynamic;
  1941. }
  1942. return $actions;
  1943. }
  1944. /**
  1945. * 构建搜索表单数据。
  1946. * Build search form data.
  1947. *
  1948. * @param int $queryID
  1949. * @param string $actionURL
  1950. * @access public
  1951. * @return void
  1952. */
  1953. public function buildTrashSearchForm($queryID, $actionURL)
  1954. {
  1955. $this->config->trash->search['actionURL'] = $actionURL;
  1956. $this->config->trash->search['queryID'] = $queryID;
  1957. $this->loadModel('search')->setSearchParams($this->config->trash->search);
  1958. }
  1959. /**
  1960. * 恢复阶段。
  1961. * Restore stages.
  1962. *
  1963. * @param array $stageList
  1964. * @access public
  1965. * @return bool
  1966. */
  1967. public function restoreStages($stageList)
  1968. {
  1969. $deletedActions = $this->dao->select('*')->from(TABLE_ACTION)
  1970. ->where('objectID')->in(array_keys($stageList))
  1971. ->andWhere('objectType')->eq('execution')
  1972. ->andWhere('action')->eq('deleted')
  1973. ->orderBy('id_desc')
  1974. ->fetchGroup('objectID');
  1975. foreach($stageList as $stageID)
  1976. {
  1977. $deletedAction = $deletedActions[$stageID][0];
  1978. $this->dao->update(TABLE_EXECUTION)->set('deleted')->eq('0')->where('id')->eq($stageID)->exec();
  1979. $this->dao->update(TABLE_ACTION)->set('extra')->eq(actionModel::BE_UNDELETED)->where('id')->eq($deletedAction->id)->exec();
  1980. $this->create($deletedAction->objectType, $deletedAction->objectID, 'undeleted');
  1981. if(dao::isError()) return false;
  1982. }
  1983. return true;
  1984. }
  1985. /**
  1986. * 获取属性相同的对象。
  1987. * Get repeat object.
  1988. *
  1989. * @param object $action
  1990. * @param string $table
  1991. * @access public
  1992. * @return array
  1993. */
  1994. public function getRepeatObject($action, $table)
  1995. {
  1996. $object = $this->dao->select('*')->from($table)->where('id')->eq($action->objectID)->fetch();
  1997. if($action->objectType == 'product')
  1998. {
  1999. $programID = isset($object->program) ? $object->program : 0;
  2000. $repeatObject = $this->dao->select('*')->from(TABLE_PRODUCT)
  2001. ->where('id')->ne($action->objectID)
  2002. ->andWhere("(name = '{$object->name}' AND program = {$programID})", true)
  2003. ->beginIF($object->code)->orWhere("code = '{$object->code}'")->fi()
  2004. ->markRight(1)
  2005. ->andWhere('deleted')->eq('0')
  2006. ->fetch();
  2007. }
  2008. elseif(in_array($action->objectType, array('program', 'project', 'execution')))
  2009. {
  2010. $sprintProject = isset($object->project) ? $object->project : 0;
  2011. $repeatObject = $this->dao->select('*')->from(TABLE_PROJECT)
  2012. ->where('id')->ne($action->objectID)
  2013. ->beginIF($action->objectType == 'program' || $action->objectType == 'project')->andWhere("(name = '{$object->name}' AND parent = {$object->parent})", true)->fi()
  2014. ->beginIF($action->objectType == 'execution')->andWhere("(name = '{$object->name}' AND project = {$sprintProject})", true)->fi()
  2015. ->beginIF($action->objectType == 'project' && $object->code)->orWhere("(code = '{$object->code}' AND model = '{$object->model}')")->fi()
  2016. ->beginIF($action->objectType == 'execution' && $object->code)->orWhere("code = '{$object->code}'")->fi()
  2017. ->markRight(1)
  2018. ->beginIF($action->objectType == 'program')->andWhere('type')->eq('program')->fi()
  2019. ->beginIF($action->objectType == 'project')->andWhere('type')->eq('project')->fi()
  2020. ->beginIF($action->objectType == 'execution')->andWhere('type')->in('sprint,stage,kanban')->fi()
  2021. ->andWhere('deleted')->eq('0')
  2022. ->fetch();
  2023. }
  2024. else
  2025. {
  2026. $objectNameFields = $this->config->action->objectNameFields[$action->objectType];
  2027. $repeatObject = $this->dao->select('*')->from($table)->where('id')->ne($action->objectID)->andWhere('deleted')->eq('0')->andWhere($objectNameFields)->eq($object->name)->fetch();
  2028. }
  2029. return array($repeatObject, $object);
  2030. }
  2031. /**
  2032. * 获取和需求属性相近的对象。
  2033. * Get like object.
  2034. *
  2035. * @param string $table
  2036. * @param string $columns
  2037. * @param string $param
  2038. * @param string $value
  2039. * @access public
  2040. * @return array
  2041. */
  2042. public function getLikeObject($table, $columns, $param, $value)
  2043. {
  2044. return $this->dao->select($columns)->from($table)->where($param)->like($value)->fetchPairs();
  2045. }
  2046. /**
  2047. * 通过id更新对象。
  2048. * Update object by id.
  2049. *
  2050. * @param string $table
  2051. * @param int $id
  2052. * @param array $params
  2053. * @access public
  2054. * @return bool
  2055. */
  2056. public function updateObjectByID($table, $id, $params)
  2057. {
  2058. $updateParams = array();
  2059. foreach($params as $key => $value) $updateParams[] = '`' . $key . '`' . '="' . $value . '"';
  2060. $this->dao->update($table)->set(implode(',', $updateParams))->where('id')->eq($id)->exec();
  2061. return !dao::isError();
  2062. }
  2063. /**
  2064. * 根据执行id获取attribute属性。
  2065. * Get attribute by execution id.
  2066. *
  2067. * @param int $executionID
  2068. * @access public
  2069. * @return string|bool
  2070. */
  2071. public function getAttributeByExecutionID($executionID)
  2072. {
  2073. return $this->dao->select('attribute')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('attribute');
  2074. }
  2075. /**
  2076. * 根据id获取已经删除的阶段。
  2077. * Get deleted stage by ids.
  2078. *
  2079. * @param array $list
  2080. * @access public
  2081. * @return array|bool
  2082. */
  2083. public function getDeletedStagedByList($list)
  2084. {
  2085. return $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->in($list)->andWhere('deleted')->eq(1)->andWhere('type')->eq('stage')->orderBy('id_asc')->fetchAll('id');
  2086. }
  2087. /**
  2088. * 更新阶段的attribute属性。
  2089. * Update stage attribute.
  2090. *
  2091. * @param string $attribute
  2092. * @param array $executions
  2093. * @access public
  2094. * @return bool
  2095. */
  2096. public function updateStageAttribute($attribute, $executions)
  2097. {
  2098. $this->dao->update(TABLE_EXECUTION)->set('attribute')->eq($attribute)->where('id')->in($executions)->exec();
  2099. return !dao::isError();
  2100. }
  2101. /**
  2102. * 获取动态对象关联的数据。
  2103. * Get action object related data.
  2104. *
  2105. * @param string $table
  2106. * @param string $objectType
  2107. * @param array $objectIdList
  2108. * @param string $field
  2109. * @param array $users
  2110. * @param array $requirements
  2111. * @param array $epics
  2112. * @access public
  2113. * @return array
  2114. */
  2115. public function getObjectRelatedData($table, $objectType, $objectIdList, $field, $users, $requirements, $epics)
  2116. {
  2117. $objectName = array();
  2118. $relatedProject = array();
  2119. if($table == TABLE_TODO)
  2120. {
  2121. $todos = $this->dao->select("id, {$field} AS name, account, private, type, objectID")->from($table)->where('id')->in($objectIdList)->orderBy('id_asc')->fetchAll();
  2122. foreach($todos as $todo)
  2123. {
  2124. /* Get related object name. */
  2125. if(in_array($todo->type, array('task', 'bug', 'story', 'testtask'))) $todo->name = $this->dao->findById($todo->objectID)->from($this->config->objectTables[$todo->type])->fetch($this->config->action->objectNameFields[$todo->type]);
  2126. $objectName[$todo->id] = $todo->private == 1 && $todo->account != $this->app->user->account ? $this->lang->todo->thisIsPrivate : $todo->name;
  2127. }
  2128. }
  2129. elseif(strpos(",{$this->config->action->needGetProjectType},", ",{$objectType},") !== false || $objectType == 'project' || $objectType == 'execution')
  2130. {
  2131. $objectInfo = $this->dao->select("id, project, {$field} AS name")->from($table)->where('id')->in($objectIdList)->filterTpl(false)->orderBy('id_asc')->fetchAll();
  2132. foreach($objectInfo as $object)
  2133. {
  2134. $objectName[$object->id] = $objectType == 'gapanalysis' ? zget($users, $object->name) : $object->name; // Get user realname if objectType is gapanalysis.
  2135. $relatedProject[$object->id] = $object->project;
  2136. }
  2137. }
  2138. elseif($objectType == 'story' || $objectType == 'team') // Get story or team related data.
  2139. {
  2140. if($objectType == 'team') $table = TABLE_PROJECT;
  2141. $objectField = $objectType == 'story' ? 'id,title,type' : 'id,team AS title,type';
  2142. $objectInfo = $this->dao->select($objectField)->from($table)->where('id')->in($objectIdList)->filterTpl(false)->orderBy('id_asc')->fetchAll();
  2143. foreach($objectInfo as $object)
  2144. {
  2145. $objectName[$object->id] = $object->title;
  2146. if($object->type == 'requirement') $requirements[$object->id] = $object->id;
  2147. if($object->type == 'epic') $epics[$object->id] = $object->id;
  2148. if($object->type == 'project') $relatedProject[$object->id] = $object->id;
  2149. }
  2150. }
  2151. elseif($objectType == 'stakeholder') // Get stakeholder realname.
  2152. {
  2153. $objectName = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->orderBy('id_asc')->fetchPairs();
  2154. foreach($objectName as $id => $name) $objectName[$id] = zget($users, $name);
  2155. }
  2156. elseif($objectType == 'pivot')
  2157. {
  2158. $objectName = $this->dao->select("pivot, {$field} AS name")->from($table)->where('pivot')->in($objectIdList)->orderBy('pivot_asc')->fetchPairs();
  2159. }
  2160. else
  2161. {
  2162. $objectName = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->filterTpl(false)->orderBy('id_asc')->fetchPairs();
  2163. }
  2164. return array($objectName, $relatedProject, $requirements, $epics);
  2165. }
  2166. /**
  2167. * 获取对象类型为team的link元素。
  2168. * Get link element of objecttype team.
  2169. *
  2170. * @param object $action
  2171. * @access protected
  2172. * @return array
  2173. */
  2174. protected function getObjectTypeTeamParams($action)
  2175. {
  2176. if($action->project) return array('project', 'team', 'projectID=' . $action->project);
  2177. if($action->execution) return array('execution', 'team', 'executionID=' . $action->execution);
  2178. return array('', '', '');
  2179. }
  2180. /**
  2181. * 检查action是否可以被还原。
  2182. * Check action can be undeleted.
  2183. *
  2184. * @param object $action
  2185. * @param object $object
  2186. * @access protected
  2187. * @return string|bool
  2188. */
  2189. protected function checkActionCanUndelete($action, $object)
  2190. {
  2191. if($action->objectType == 'execution')
  2192. {
  2193. if($object->deleted && empty($object->project)) return $this->lang->action->undeletedTips;
  2194. $projectCount = $this->dao->select('COUNT(1) AS count')->from(TABLE_PROJECT)->where('id')->eq($object->project)->andWhere('deleted')->eq('0')->fetch('count');
  2195. if((int)$projectCount == 0) return $this->lang->action->executionNoProject;
  2196. }
  2197. elseif($action->objectType == 'repo' && in_array($object->SCM, array('Gitlab', 'Gitea', 'Gogs')))
  2198. {
  2199. $server = $this->dao->select('*')->from(TABLE_PIPELINE)->where('id')->eq($object->serviceHost)->andWhere('deleted')->eq('0')->fetch();
  2200. if(empty($server)) return $this->lang->action->repoNoServer;
  2201. }
  2202. elseif($action->objectType == 'module')
  2203. {
  2204. $repeatName = $this->loadModel('tree')->checkUnique($object);
  2205. if($repeatName) return sprintf($this->lang->tree->repeatName, $repeatName);
  2206. if($object->parent > 0 && $object->type != 'doc')
  2207. {
  2208. $parent = $this->dao->select('*')->from(TABLE_MODULE)->where('id')->eq($object->parent)->fetch();
  2209. if($parent && $parent->deleted == '1') return $this->lang->action->refusemodule;
  2210. }
  2211. }
  2212. elseif($action->objectType == 'case' && $object->scene)
  2213. {
  2214. $scene = $this->dao->select('*')->from(TABLE_SCENE)->where('id')->eq($object->scene)->fetch();
  2215. if($scene->deleted) return $this->lang->action->refusecase;
  2216. }
  2217. elseif($action->objectType == 'scene' && $object->parent)
  2218. {
  2219. $scenerow = $this->dao->select('*')->from(TABLE_SCENE)->where('id')->eq($object->parent)->fetch();
  2220. if($scenerow->deleted) return $this->lang->action->refusescene;
  2221. }
  2222. elseif($action->objectType == 'reviewissue' && !empty($object->review))
  2223. {
  2224. $review = $this->dao->select('*')->from(TABLE_REVIEW)->where('id')->eq($object->review)->fetch();
  2225. if($review->deleted)
  2226. {
  2227. $this->app->loadLang('reviewissue');
  2228. return $this->lang->reviewissue->undeleteAction;
  2229. }
  2230. }
  2231. elseif($action->objectType == 'kanban' && $object->space)
  2232. {
  2233. $kanbanSpace = $this->dao->select('*')->from(TABLE_KANBANSPACE)->where('id')->eq($object->space)->fetch();
  2234. if($kanbanSpace->deleted) return $this->lang->action->refusekanban;
  2235. }
  2236. return true;
  2237. }
  2238. /**
  2239. * 恢复被删除对象的关联数据。
  2240. * Recover related data of deleted object.
  2241. *
  2242. * @param object $action
  2243. * @param object $object
  2244. * @access protected
  2245. * @return void
  2246. */
  2247. protected function recoverRelatedData($action, $object)
  2248. {
  2249. if($action->objectType == 'release' && $object->shadow) $this->dao->update(TABLE_BUILD)->set('deleted')->eq(0)->where('id')->eq($object->shadow)->exec();
  2250. if(in_array($action->objectType, array('program', 'project', 'execution', 'product')))
  2251. {
  2252. $objectType = $action->objectType == 'execution' ? 'sprint' : $action->objectType;
  2253. if($object->acl != 'open') $this->loadModel('user')->updateUserView(array($object->id), $objectType);
  2254. /* 恢复隐藏产品。 */
  2255. /* Resotre hidden products. */
  2256. if($action->objectType == 'project' && !$object->hasProduct)
  2257. {
  2258. $productID = $this->loadModel('product')->getProductIDByProject($object->id);;
  2259. $this->dao->update(TABLE_PRODUCT)->set('name')->eq($object->name)->set('deleted')->eq(0)->where('id')->eq($productID)->exec();
  2260. }
  2261. /* 恢复隐藏执行。 */
  2262. /* Resotre hidden execution. */
  2263. if($action->objectType == 'project' && empty($object->multiple)) $this->dao->update(TABLE_EXECUTION)->set('deleted')->eq('0')->where('project')->eq($object->id)->andWhere('multiple')->eq('0')->exec();
  2264. }
  2265. if($action->objectType == 'doc' && $object->files) $this->dao->update(TABLE_FILE)->set('deleted')->eq('0')->where('id')->in($object->files)->exec();
  2266. /* 当还原项目或者执行的时候恢复用户的产品权限。 */
  2267. /* Revert userView products when undelete project or execution. */
  2268. if($action->objectType == 'project' || $action->objectType == 'execution')
  2269. {
  2270. $products = $this->loadModel('product')->getProducts($object->id, 'all', '', false);
  2271. if(!empty($products)) $this->loadModel('user')->updateUserView(array_keys($products), 'product');
  2272. if($action->objectType == 'execution')
  2273. {
  2274. $execution = $this->dao->select('id, type, project, grade, parent, status, deleted')->from(TABLE_EXECUTION)->where('id')->eq($action->objectID)->fetch();
  2275. $this->loadModel('common')->syncExecutionByChild($execution);
  2276. }
  2277. }
  2278. /* 还原产品或者项目的时候恢复文档库。 */
  2279. /* Revert doclib when undelete product or project. */
  2280. if($action->objectType == 'execution' || $action->objectType == 'product') $this->dao->update(TABLE_DOCLIB)->set('deleted')->eq(0)->where($action->objectType)->eq($action->objectID)->exec();
  2281. /* 还原子任务的时候更新任务状态。 */
  2282. /* Update task status when undelete child task. */
  2283. if($action->objectType == 'task')
  2284. {
  2285. $task = $this->loadModel('task')->fetchByID((int)$action->objectID);
  2286. if($task->parent > 0)
  2287. {
  2288. $parentConsumed = $this->dao->select('consumed')->from(TABLE_TASK)->where('id')->eq($task->parent)->fetch('consumed');
  2289. if($parentConsumed)
  2290. {
  2291. $this->dao->update(TABLE_TASK)->set('parent')->eq('0')->set('path')->eq(",{$task->id},")->where('id')->eq($task->id)->exec();
  2292. }
  2293. else
  2294. {
  2295. $this->loadModel('task')->updateParent($task, false);
  2296. }
  2297. }
  2298. }
  2299. }
  2300. /**
  2301. * 执行和项目相关操作记录的extra信息。
  2302. * Build execution and project action extra info.
  2303. *
  2304. * @param object $action
  2305. * @access protected
  2306. * @return void
  2307. */
  2308. protected function processExecutionAndProjectActionExtra($action)
  2309. {
  2310. $this->app->loadLang('execution');
  2311. $linkedProducts = $this->dao->select('id,name')->from(TABLE_PRODUCT)->where('id')->in($action->extra)->fetchPairs('id', 'name');
  2312. $action->extra = '';
  2313. if($linkedProducts && $this->config->vision == 'rnd')
  2314. {
  2315. foreach($linkedProducts as $productID => $productName) $linkedProducts[$productID] = html::a(helper::createLink('product', 'browse', "productID={$productID}"), "#{$productID} {$productName}");
  2316. $action->extra = sprintf($this->lang->execution->action->extra, '<strong>' . join(', ', $linkedProducts) . '</strong>');
  2317. }
  2318. }
  2319. /**
  2320. * 获取动态的数量。
  2321. * Get dynamic count.
  2322. *
  2323. * @access public
  2324. * @return int
  2325. */
  2326. public function getDynamicCount($period = 'all')
  2327. {
  2328. $condition = $this->session->actionQueryCondition;
  2329. if(empty($condition)) return 0;
  2330. $table = $this->actionTao->getActionTable($period);
  2331. $hasProduct = preg_match('/t2\.([`"]?)product/', $condition);
  2332. $useIndex = $hasProduct && strpos('mysql,oceanbase', $this->config->db->driver) !== false ? ' USE INDEX (`vision_date`)' : ''; // 关联产品时指定索引以提高查询性能。Specify index to improve query performance when associated with product.
  2333. $beginAndEnd = $this->computeBeginAndEnd($period, '', 'next');
  2334. $condition = preg_replace("/AND +`?date`? +(<|>|<=|>=) +'\d{4}\-\d{2}\-\d{2}'/", '', $condition);
  2335. $actions = $this->dao->select('action.id')->from($table)->alias('action' . $useIndex)
  2336. ->beginIF($hasProduct)->leftJoin(TABLE_ACTIONPRODUCT)->alias('t2')->on('action.id=t2.action')->fi()
  2337. ->where($condition)
  2338. ->beginIF($beginAndEnd['begin'] != EPOCH_DATE)->andWhere('date')->ge($beginAndEnd['begin'])->fi()
  2339. ->beginIF($beginAndEnd['end'] != FUTURE_DATE)->andWhere('date')->le($beginAndEnd['end'])->fi()
  2340. ->limit(self::MAXCOUNT)
  2341. ->fetchAll('id');
  2342. return count($actions);
  2343. }
  2344. /**
  2345. * 清除一个月前的动态记录。
  2346. * Clear dynamic records older than one month.
  2347. *
  2348. * @access public
  2349. * @return bool
  2350. */
  2351. public function cleanActions()
  2352. {
  2353. $cleanDate = zget($this->app->config->global, 'cleanActionsDate', '');
  2354. $today = helper::today();
  2355. if($cleanDate == $today) return true;
  2356. $this->loadModel('setting')->setItem('system.common.global.cleanActionsDate', $today);
  2357. $lastMonth = date('Y-m-d', strtotime('-1 month'));
  2358. $this->dao->delete()->from(TABLE_ACTIONRECENT)->where('date')->lt($lastMonth)->exec();
  2359. return !dao::isError();
  2360. }
  2361. /**
  2362. * 获取最早的动态记录。
  2363. * Get the first action.
  2364. *
  2365. * @access public
  2366. * @return object|bool
  2367. */
  2368. public function getFirstAction()
  2369. {
  2370. return $this->dao->select('*')->from(TABLE_ACTION)->orderBy('id')->limit(1)->fetch();
  2371. }
  2372. /**
  2373. * 检查该日期是否还有的更多日志。
  2374. * Check has more actions for this date.
  2375. *
  2376. * @param object $lastAction
  2377. * @access public
  2378. * @return bool
  2379. */
  2380. public function hasMoreAction($lastAction)
  2381. {
  2382. $condition = $this->session->actionQueryCondition;
  2383. if(empty($condition)) return false;
  2384. $hasProduct = preg_match('/t2\.([`"]?)product/', $condition);
  2385. $lastDate = substr($lastAction->originalDate, 0, 10);
  2386. $condition = preg_replace("/AND +`?date`? +(<|>|<=|>=) +'\d{4}\-\d{2}\-\d{2}'/", '', $condition);
  2387. $actions = $this->dao->select('action.id')->from(TABLE_ACTION)->alias('action')
  2388. ->beginIF($hasProduct)->leftJoin(TABLE_ACTIONPRODUCT)->alias('t2')->on('action.id=t2.action')->fi()
  2389. ->where($condition)
  2390. ->andWhere("`date`")->ge($lastDate)
  2391. ->andWhere("`date`")->lt($lastDate . ' 23:59:59')
  2392. ->andWhere("`date` < '{$lastAction->originalDate}'")
  2393. ->orderBy('date_desc')
  2394. ->limit(1)
  2395. ->fetchAll();
  2396. return count($actions) > 0;
  2397. }
  2398. /**
  2399. * 获取该日期的更多日志。
  2400. * Get more actions for this date.
  2401. *
  2402. * @param int $lastActionID
  2403. * @param int $limit
  2404. * @access public
  2405. * @return array
  2406. */
  2407. public function getMoreActions($lastActionID, $limit = 100)
  2408. {
  2409. $condition = $this->session->actionQueryCondition;
  2410. if(empty($condition)) return array();
  2411. $hasProduct = preg_match('/t2\.([`"]?)product/', $condition);
  2412. $lastAction = $this->dao->select('*')->from(TABLE_ACTION)->where('id')->eq($lastActionID)->fetch();
  2413. $lastDate = substr($lastAction->date, 0, 10);
  2414. $actions = $this->dao->select('action.*')->from(TABLE_ACTION)->alias('action')
  2415. ->beginIF($hasProduct)->leftJoin(TABLE_ACTIONPRODUCT)->alias('t2')->on('action.id=t2.action')->fi()
  2416. ->where($condition)
  2417. ->andWhere("`date`")->ge($lastDate)
  2418. ->andWhere("`date`")->lt($lastDate . ' 23:59:59')
  2419. ->andWhere("`date` < '{$lastAction->date}'")
  2420. ->orderBy('date_desc')
  2421. ->limit($limit)
  2422. ->fetchAll('id', false);
  2423. $actions = $this->transformActions($actions);
  2424. foreach($actions as $action)
  2425. {
  2426. $timeStamp = strtotime(isset($action->originalDate) ? $action->originalDate : $action->date);
  2427. $action->time = date(DT_TIME2, $timeStamp);
  2428. }
  2429. return array_values($actions);
  2430. }
  2431. }