model.php 237 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555
  1. <?php
  2. /**
  3. * The model file of execution 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 execution
  9. * @version $Id: model.php 5118 2013-07-12 07:41:41Z chencongzhi520@gmail.com $
  10. * @link https://www.zentao.net
  11. */
  12. class executionModel extends model
  13. {
  14. /* The members every linking. */
  15. const LINK_MEMBERS_ONE_TIME = 20;
  16. /**
  17. * 检查执行的权限
  18. * Check the privilege.
  19. *
  20. * @param int $executionID
  21. * @access public
  22. * @return bool
  23. */
  24. public function checkPriv($executionID)
  25. {
  26. if(commonModel::isTutorialMode()) return true;
  27. return !empty($executionID) && ($this->app->user->admin || (strpos(",{$this->app->user->view->sprints},", ",{$executionID},") !== false));
  28. }
  29. /**
  30. * 提示没有查看执行的权限并跳转到执行列表。
  31. * Tip no permission to view the execution and jump to the execution list.
  32. *
  33. * @access public
  34. * @return bool|void
  35. */
  36. public function accessDenied()
  37. {
  38. if(commonModel::isTutorialMode()) return true;
  39. return $this->app->control->sendError($this->lang->execution->accessDenied, helper::createLink('execution', 'all'));
  40. }
  41. /**
  42. * 获取系统关闭的功能。
  43. * Get the system close function.
  44. *
  45. * @param object $execution
  46. * @access public
  47. * @return array
  48. */
  49. public function getExecutionFeatures($execution)
  50. {
  51. $features = array('story' => true, 'task' => true, 'qa' => true, 'devops' => true, 'burn' => true, 'build' => true, 'other' => true, 'plan' => true);
  52. /* Unset story, bug, build and testtask if type is ops. */
  53. if($execution->lifetime == 'ops')
  54. {
  55. $features['story'] = false;
  56. $features['qa'] = false;
  57. $features['build'] = false;
  58. $features['burn'] = false;
  59. }
  60. elseif(!empty($execution->attribute))
  61. {
  62. $features['other'] = false;
  63. /* Product-related features are disabled during the request, design, and review stage. */
  64. if(in_array($execution->attribute, array('request', 'design', 'review')))
  65. {
  66. $features['qa'] = false;
  67. $features['devops'] = false;
  68. $features['build'] = false;
  69. if(in_array($execution->attribute, array('request', 'review'))) $features['plan'] = false;
  70. if($execution->attribute == 'review') $features['story'] = false;
  71. }
  72. if(empty($execution->hasProduct) && in_array($execution->attribute, array('plan', 'develop', 'qualify', 'launch'))) $features['plan'] = false;
  73. }
  74. /* The plan function is disabled for no-product project. */
  75. if(isset($execution->projectInfo) && !empty($execution->projectInfo->model) && in_array($execution->projectInfo->model, array('waterfall', 'kanban', 'waterfallplus')) && empty($execution->projectInfo->hasProduct))
  76. {
  77. $features['plan'] = false;
  78. }
  79. return $features;
  80. }
  81. /**
  82. * 设置执行导航。
  83. * Set menu.
  84. *
  85. * @param int $executionID
  86. * @access public
  87. * @return void
  88. */
  89. public function setMenu($executionID)
  90. {
  91. $execution = commonModel::isTutorialMode() ? $this->loadModel('tutorial')->getExecution() : $this->fetchByID((int)$executionID);
  92. if(!$execution) return;
  93. if($execution->type == 'kanban') $this->executionTao->setKanbanMenu();
  94. /* Check execution permission. */
  95. $executions = $this->fetchPairs($execution->project, 'all');
  96. if(!$executionID && $this->session->execution) $executionID = $this->session->execution;
  97. if(!$executionID) $executionID = key($executions);
  98. $canAccess = !empty($executions) && isset($executions[$executionID]) && $this->checkPriv($executionID);
  99. if($execution->multiple && !$execution->isTpl && !$canAccess) return $this->accessDenied();
  100. if(empty($executionID)) return;
  101. /* Replaces the iterated language with the stage. */
  102. if($execution->type == 'stage')
  103. {
  104. global $lang;
  105. $this->app->loadLang('project');
  106. $lang->executionCommon = $lang->project->stage;
  107. include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
  108. }
  109. /* Set secondary navigation based on the conditions. */
  110. $this->removeMenu($execution);
  111. if($this->cookie->executionMode == 'noclosed' && $execution && ($execution->status == 'done' || $execution->status == 'closed'))
  112. {
  113. helper::setcookie('executionMode', 'all');
  114. $this->cookie->executionMode = 'all';
  115. }
  116. $this->session->set('execution', $executionID, $this->app->tab);
  117. $this->lang->switcherMenu = $this->getSwitcher($executionID, (string)$this->app->rawModule, (string)$this->app->rawMethod);
  118. common::setMenuVars('execution', $executionID);
  119. if($execution->type != 'kanban' && ($this->app->getModuleName() == 'repo' || $this->app->getModuleName() == 'mr')) $this->loadModel('repo')->setHideMenu($executionID);
  120. /* Set stroy navigation for no-product project. */
  121. $this->loadModel('project')->setNoMultipleMenu($executionID);
  122. if(isset($this->lang->execution->menu->story['dropMenu']) && $this->app->getMethodName() == 'storykanban')
  123. {
  124. $this->lang->execution->menu->story['link'] = str_replace(array($this->lang->common->story, 'story'), array($this->lang->SRCommon, 'storykanban'), $this->lang->execution->menu->story['link']);
  125. $this->lang->execution->menu->story['dropMenu']->story = str_replace('execution|story', 'execution|storykanban', $this->lang->execution->menu->story['dropMenu']->story);
  126. }
  127. /* 模板执行过滤部分导航菜单。 */
  128. if(!empty($execution->isTpl))
  129. {
  130. dao::$filterTpl = 'never';
  131. $this->lang->execution->common = $this->lang->execution->template;
  132. if(empty($execution->multiple)) $this->lang->project->common = $this->lang->execution->template;
  133. unset($this->lang->execution->menu->burn);
  134. unset($this->lang->execution->menu->kanban);
  135. unset($this->lang->execution->menu->story);
  136. unset($this->lang->execution->menu->qa);
  137. unset($this->lang->execution->menu->devops);
  138. unset($this->lang->execution->menu->build);
  139. unset($this->lang->execution->menu->release);
  140. unset($this->lang->execution->menu->effort);
  141. unset($this->lang->execution->menu->more);
  142. if(!empty($this->lang->execution->menu->view['subMenu']->gantt))
  143. {
  144. $this->lang->execution->menu->gantt = $this->lang->execution->menu->view['subMenu']->gantt;
  145. $taskOrder = 0;
  146. foreach($this->lang->execution->menuOrder as $order => $menu) if($menu == 'task') $taskOrder = $order;
  147. $this->lang->execution->menuOrder[$taskOrder + 1] = 'gantt';
  148. if(!empty($this->lang->project->menuOrder))
  149. {
  150. $taskOrder = 0;
  151. foreach($this->lang->project->menuOrder as $order => $menu) if($menu == 'task') $taskOrder = $order;
  152. $this->lang->project->menuOrder[$taskOrder + 1] = 'gantt';
  153. }
  154. }
  155. if(!empty($this->lang->execution->menu->other['dropMenu']->pssp))
  156. {
  157. $this->lang->execution->menu->pssp = $this->lang->execution->menu->other['dropMenu']->pssp;
  158. if(isset($this->lang->execution->menu->other['dropMenu']->auditplan))
  159. {
  160. $this->lang->execution->menu->auditplan = $this->lang->execution->menu->other['dropMenu']->auditplan;
  161. }
  162. $docOrder = 0;
  163. foreach($this->lang->execution->menuOrder as $order => $menu) if($menu == 'doc') $docOrder = $order;
  164. $this->lang->execution->menuOrder[$docOrder + 1] = 'pssp';
  165. $this->lang->execution->menuOrder[$docOrder + 2] = 'auditplan';
  166. if(!empty($this->lang->project->menuOrder))
  167. {
  168. $docOrder = 0;
  169. foreach($this->lang->project->menuOrder as $order => $menu) if($menu == 'doc') $docOrder = $order;
  170. $this->lang->project->menuOrder[$docOrder + 1] = 'pssp';
  171. $this->lang->project->menuOrder[$docOrder + 2] = 'auditplan';
  172. }
  173. }
  174. unset($this->lang->execution->menu->view);
  175. unset($this->lang->execution->menu->other);
  176. if(isset($this->lang->execution->menu->settings['subMenu']->products)) unset($this->lang->execution->menu->settings['subMenu']->products); // 模板下隐藏产品
  177. if(isset($this->lang->execution->menu->settings['subMenu']->whitelist)) unset($this->lang->execution->menu->settings['subMenu']->whitelist); // 模板下隐藏白名单
  178. }
  179. }
  180. /**
  181. * 根据条件设置执行二级导航。
  182. * Set secondary navigation based on the conditions.
  183. *
  184. * @param object $execution
  185. * @access public
  186. * @return bool
  187. */
  188. public function removeMenu($execution)
  189. {
  190. $project = $this->loadModel('project')->fetchByID($execution->project);
  191. if($execution->type == 'stage' || (!empty($project) && $project->model == 'waterfallplus')) unset($this->lang->execution->menu->settings['subMenu']->products);
  192. if(empty($execution->hasProduct)) unset($this->lang->execution->menu->settings['subMenu']->products);
  193. if(isset($execution->acl) && $execution->acl != 'private') unset($this->lang->execution->menu->settings['subMenu']->whitelist);
  194. $features = $this->getExecutionFeatures($execution);
  195. if(!$features['story']) unset($this->lang->execution->menu->story);
  196. if(!$features['story']) unset($this->lang->execution->menu->view['subMenu']->groupTask);
  197. if(!$features['story']) unset($this->lang->execution->menu->view['subMenu']->tree);
  198. if(!$features['qa']) unset($this->lang->execution->menu->qa);
  199. if(!$features['devops']) unset($this->lang->execution->menu->devops);
  200. if(!$features['build']) unset($this->lang->execution->menu->build);
  201. if(!$features['burn']) unset($this->lang->execution->menu->burn);
  202. if(!$features['other']) unset($this->lang->execution->menu->other);
  203. if(!$features['story'] && $this->config->edition == 'open') unset($this->lang->execution->menu->view);
  204. if($this->config->inCompose)
  205. {
  206. $repoServers = $this->loadModel('pipeline')->getPairs($this->config->pipeline->checkRepoServers);
  207. if(empty($repoServers)) unset($this->lang->execution->menu->devops);
  208. }
  209. return true;
  210. }
  211. /**
  212. * 检查用户是否可以访问当前执行。
  213. * Check whether access to the current execution is allowed or not.
  214. *
  215. * @param int $executionID
  216. * @param array $executions
  217. * @access public
  218. * @return int
  219. */
  220. public function checkAccess($executionID, $executions)
  221. {
  222. if(commonModel::isTutorialMode()) return $executionID;
  223. /* When the cookie and session do not exist, get it from the config. */
  224. if(!$executionID)
  225. {
  226. if($this->cookie->lastExecution) $executionID = (int)$this->cookie->lastExecution;
  227. if(!$executionID && $this->session->execution) $executionID = (int)$this->session->execution;
  228. if(!$executionID && isset($this->config->execution->lastExecution)) $executionID = (int)$this->config->execution->lastExecution;
  229. }
  230. /* 项目模板不校验访问权限。 */
  231. $isTpl = $this->dao->select('isTpl')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->andWhere('id')->in($this->app->user->view->sprints)->fetch('isTpl');
  232. /* If the execution doesn't exist in the list, use the first execution in the list. */
  233. if(empty($isTpl) && !isset($executions[$executionID]))
  234. {
  235. /* Check execution. */
  236. if($executionID)
  237. {
  238. $execution = $this->dao->findByID($executionID)->from(TABLE_EXECUTION)->fetch();
  239. if(empty($execution)) return $this->app->control->sendError($this->lang->notFound, helper::createLink('execution', 'all'));
  240. if(!$this->app->user->admin && strpos(",{$this->app->user->view->sprints},", ",{$executionID},") === false) $this->accessDenied();
  241. }
  242. $executionID = key($executions);
  243. }
  244. /* Save session. */
  245. $this->executionTao->saveSession((int)$executionID);
  246. /* Return execution id. */
  247. return (int)$executionID;
  248. }
  249. /**
  250. * 给执行所属的项目ID设置session。
  251. * Set project into session.
  252. *
  253. * @param int $executionID
  254. * @access public
  255. * @return void
  256. */
  257. public function setProjectSession($executionID)
  258. {
  259. $execution = $this->fetchByID($executionID);
  260. if(!empty($execution)) $this->session->set('project', $execution->project, $this->app->tab);
  261. }
  262. /**
  263. * Create a execution.
  264. *
  265. * @param object $execution
  266. * @param array $postMembers
  267. * @access public
  268. * @return int|false
  269. */
  270. public function create($execution, $postMembers)
  271. {
  272. $skipFlow = isset($execution->multiple) && $execution->multiple == 0;
  273. $this->dao->insert(TABLE_EXECUTION)->data($execution, 'products,plans,branch')
  274. ->autoCheck('begin,end')
  275. ->batchCheck($this->config->execution->create->requiredFields, 'notempty')
  276. ->checkIF(!empty($execution->name), 'name', 'unique', "`type` in ('sprint','stage', 'kanban') and `project` = " . (int)$execution->project . " and `deleted` = '0'")
  277. ->checkIF(!empty($execution->code), 'code', 'unique', "`type` in ('sprint','stage', 'kanban') and `project` = " . (int)$execution->project . " and `deleted` = '0'")
  278. ->checkIF($execution->begin != '', 'begin', 'date')
  279. ->checkIF($execution->end != '', 'end', 'date')
  280. ->checkIF($execution->end != '', 'end', 'ge', $execution->begin)
  281. ->checkFlow($skipFlow) // 影子迭代跳过检查 skip check flow for shadow iteration
  282. ->exec();
  283. /* Add the creator to the team. */
  284. if(dao::isError()) return false;
  285. $executionID = $this->dao->lastInsertId();
  286. $project = $this->loadModel('project')->fetchByID($execution->project);
  287. if(empty($project) || $project->model != 'kanban')
  288. {
  289. $execution->id = $executionID;
  290. if(!isset($execution->attribute)) $execution->attribute = '';
  291. $this->loadModel('kanban')->createExecutionLane($execution);
  292. }
  293. /* Api create infinites stages. */
  294. if(isset($execution->parent) && ($execution->parent != $execution->project) && ($execution->type == 'stage' || $project->model == 'ipd'))
  295. {
  296. $parent = $this->fetchByID((int)$execution->parent);
  297. $grade = $parent->grade + 1;
  298. $path = rtrim($parent->path, ',') . ",{$executionID}";
  299. $attribute = $parent->attribute;
  300. $this->dao->update(TABLE_EXECUTION)->set('attribute')->eq($attribute)->set('path')->eq($path)->set('grade')->eq($grade)->where('id')->eq($executionID)->exec();
  301. }
  302. /* Save order. */
  303. $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec();
  304. $this->loadModel('file')->updateObjectID($this->post->uid, $executionID, 'execution');
  305. /* Update the path. */
  306. $this->setTreePath($executionID);
  307. $this->executionTao->addExecutionMembers($executionID, $postMembers);
  308. $this->executionTao->createMainLib($execution->project, $executionID, $execution->type);
  309. $this->loadModel('personnel')->updateWhitelist(explode(',', $execution->whitelist), 'sprint', $executionID);
  310. if($execution->acl != 'open') $this->updateUserView($executionID);
  311. $this->updateProducts($executionID, $execution);
  312. $this->loadModel('programplan')->computeProgress($executionID, 'create');
  313. $this->loadModel('score')->create('program', 'createguide', $executionID);
  314. return $executionID;
  315. }
  316. /**
  317. * 更新一个迭代。
  318. * Update a execution.
  319. *
  320. * @param int $executionID
  321. * @param object $postData
  322. * @access public
  323. * @return array|false
  324. */
  325. public function update($executionID, $postData)
  326. {
  327. $oldExecution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch();
  328. /* Judge workdays is legitimate. */
  329. $this->app->loadLang('project');
  330. $workdays = helper::diffDate($postData->end, $postData->begin) + 1;
  331. if(isset($postData->days) and $postData->days > $workdays)
  332. {
  333. dao::$errors['days'] = sprintf($this->lang->project->workdaysExceed, $workdays);
  334. return false;
  335. }
  336. if(!empty($postData->products))
  337. {
  338. $multipleProducts = $this->loadModel('product')->getMultiBranchPairs();
  339. if(isset($postData->branch) and !empty($postData->branch)) $postData->branch = is_array($postData->branch) ? $postData->branch : json_decode($postData->branch, true);
  340. foreach($postData->products as $index => $productID)
  341. {
  342. if(!isset($postData->branch[$index])) continue;
  343. $branches = is_array($postData->branch[$index]) ? implode(',', $postData->branch[$index]) : $postData->branch[$index];
  344. if(isset($multipleProducts[$productID]) && $branches == '')
  345. {
  346. dao::$errors["branch[$index][]"] = $this->lang->project->error->emptyBranch;
  347. return false;
  348. }
  349. }
  350. }
  351. if(!empty($postData->heightType) && $postData->heightType == 'custom' && !$this->loadModel('kanban')->checkDisplayCards($postData->displayCards)) return false;
  352. if(in_array($postData->status, array('closed', 'suspended'))) $this->computeBurn($executionID);
  353. /* Check the begin date and end date. */
  354. $parentExecution = !empty($postData->parent) ? $postData : $oldExecution;
  355. if($oldExecution->multiple &&(empty($postData->project) || $postData->project == $oldExecution->project)) $this->checkBeginAndEndDate($oldExecution->project, $postData->begin, $postData->end, $parentExecution->parent);
  356. if(dao::isError()) return false;
  357. /* Child stage inherits parent stage permissions. */
  358. if(!isset($postData->acl)) $postData->acl = $oldExecution->acl;
  359. $execution = $this->loadModel('file')->processImgURL($postData, $this->config->execution->editor->edit['id'], (string)$this->post->uid);
  360. /* Check the workload format and total, such as check Workload Ratio if it enabled. */
  361. if(!empty($execution->percent) && isset($this->config->setPercent) && $this->config->setPercent == 1) $this->checkWorkload('update', (float)$execution->percent, $oldExecution);
  362. if(dao::isError()) return false;
  363. /* Set planDuration and realDuration. */
  364. if(in_array($this->config->edition, array('max', 'ipd')))
  365. {
  366. $execution->planDuration = $this->loadModel('programplan')->getDuration($execution->begin, $execution->end);
  367. if(!empty($execution->realBegan) and !empty($execution->realEnd)) $execution->realDuration = $this->programplan->getDuration($execution->realBegan, $execution->realEnd);
  368. }
  369. /* Redefines the language entries for the fields in the project table. */
  370. $requiredFields = $this->config->execution->edit->requiredFields;
  371. foreach(explode(',', $this->config->execution->edit->requiredFields) as $field)
  372. {
  373. if(empty($field)) continue;
  374. if(isset($this->lang->execution->$field)) $this->lang->project->$field = $this->lang->execution->$field;
  375. if($oldExecution->type == 'stage' and $field == 'name') $this->lang->project->name = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->project->name);
  376. /* 删除瀑布类型阶段无用必填项。 Delete useless required fields for waterfall stage. */
  377. if((in_array($field, array('QD', 'RD')) && in_array($execution->attribute, array('request', 'design', 'review'))) || ($field == 'PO' && $execution->attribute == 'review')) $requiredFields = trim(str_replace(",{$field},", ',', ",{$requiredFields},"), ',');
  378. }
  379. /* Update data. */
  380. $this->lang->error->unique = $this->lang->error->repeat;
  381. $executionProject = isset($execution->project) ? $execution->project : $oldExecution->project;
  382. $this->dao->update(TABLE_EXECUTION)->data($execution, 'products, branch, uid, plans, syncStories, contactListMenu, teamMembers, heightType, delta')
  383. ->autoCheck('begin,end')
  384. ->batchCheck($requiredFields, 'notempty')
  385. ->checkIF($execution->begin != '', 'begin', 'date')
  386. ->checkIF($execution->end != '', 'end', 'date')
  387. ->checkIF($execution->end != '', 'end', 'ge', $execution->begin)
  388. ->checkIF(!empty($execution->name), 'name', 'unique', "id != $executionID and type in ('sprint','stage', 'kanban') and `project` = '$executionProject' and `deleted` = '0'")
  389. ->checkIF(!empty($execution->code), 'code', 'unique', "id != $executionID and type in ('sprint','stage', 'kanban') and `project` = '$executionProject' and `deleted` = '0'")
  390. ->checkFlow()
  391. ->where('id')->eq($executionID)
  392. ->exec();
  393. if(dao::isError()) return false;
  394. if(isset($postData->parent)) $this->loadModel('programplan')->setTreePath($executionID);
  395. /* Update the team. */
  396. $this->executionTao->updateTeam($executionID, $oldExecution, $execution);
  397. /* Update whitelist. */
  398. $whitelist = array();
  399. if(!empty($execution->whitelist)) $whitelist = is_string($execution->whitelist) ? explode(',', $execution->whitelist) : $execution->whitelist;
  400. $this->loadModel('personnel')->updateWhitelist($whitelist, 'sprint', $executionID);
  401. /* Set the product for project same to execution. */
  402. if(isset($execution->project))
  403. {
  404. $executionProductList = $this->loadModel('product')->getProducts($executionID);
  405. $projectProductList = $this->product->getProducts((int)$execution->project);
  406. $executionProductIdList = array_keys($executionProductList);
  407. $projectProductIdList = array_keys($projectProductList);
  408. $diffProductIdList = array_diff($executionProductIdList, $projectProductIdList);
  409. if(!empty($diffProductIdList))
  410. {
  411. foreach($diffProductIdList as $newProductID)
  412. {
  413. $data = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)
  414. ->where('project')->eq($executionID)
  415. ->andWhere('product')->eq($newProductID)
  416. ->fetch();
  417. $data->project = $execution->project;
  418. $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data, 'id')->exec();
  419. }
  420. }
  421. }
  422. if(!dao::isError())
  423. {
  424. if(isset($execution->project) and $execution->project != $oldExecution->project)
  425. {
  426. $execution->parent = $execution->project;
  427. $execution->path = ",{$execution->project},{$executionID},";
  428. $this->changeProject((int)$execution->project, $oldExecution->project, $executionID, $postData->syncStories ?? 'no');
  429. }
  430. $this->file->updateObjectID($this->post->uid, $executionID, 'execution');
  431. return common::createChanges($oldExecution, $execution);
  432. }
  433. }
  434. /**
  435. * 批量编辑执行。
  436. * Batch update executions.
  437. *
  438. * @param object $postData
  439. * @access public
  440. * @return array|false
  441. */
  442. public function batchUpdate($postData)
  443. {
  444. $this->loadModel('user');
  445. $this->loadModel('project');
  446. $this->app->loadLang('programplan');
  447. $allChanges = array();
  448. $oldExecutions = $this->getByIdList($postData->id);
  449. $executions = $this->buildBatchUpdateExecutions($postData, $oldExecutions);
  450. if(dao::isError()) return false;
  451. /* Update burn before close execution. */
  452. $closedIdList = array();
  453. foreach($executions as $executionID => $execution)
  454. {
  455. if(isset($execution->status) and in_array($execution->status, array('done', 'closed', 'suspended'))) $closedIdList[$executionID] = $executionID;
  456. }
  457. $this->computeBurn($closedIdList);
  458. foreach($executions as $executionID => $execution)
  459. {
  460. $oldExecution = $oldExecutions[$executionID];
  461. $team = $this->user->getTeamMemberPairs($executionID, 'execution');
  462. $projectID = isset($execution->project) ? (int)$execution->project : (int)$oldExecution->project;
  463. if(isset($execution->project))
  464. {
  465. $executionProductList = $this->loadModel('product')->getProducts($executionID);
  466. $projectProductList = $this->product->getProducts((int)$execution->project);
  467. $executionProductIdList = array_keys($executionProductList);
  468. $projectProductIdList = array_keys($projectProductList);
  469. $diffProductIdList = array_diff($executionProductIdList, $projectProductIdList);
  470. if(!empty($diffProductIdList))
  471. {
  472. foreach($diffProductIdList as $newProductID)
  473. {
  474. $projectProduct = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)
  475. ->where('project')->eq($executionID)
  476. ->andWhere('product')->eq($newProductID)
  477. ->fetch();
  478. $projectProduct->project = $execution->project;
  479. $this->dao->insert(TABLE_PROJECTPRODUCT)->data($projectProduct, 'id')->exec();
  480. }
  481. }
  482. }
  483. $this->dao->update(TABLE_EXECUTION)->data($execution)
  484. ->autoCheck('begin,end')
  485. ->batchCheck($this->config->execution->edit->requiredFields, 'notempty')
  486. ->checkIF($execution->begin != '', 'begin', 'date')
  487. ->checkIF($execution->end != '', 'end', 'date')
  488. ->checkIF($execution->end != '', 'end', 'ge', $execution->begin)
  489. ->checkIF(!empty($execution->name), 'name', 'unique', "id != $executionID and type in ('sprint','stage','kanban') and `project` = $projectID and `deleted` = '0'")
  490. ->checkIF(!empty($execution->code), 'code', 'unique', "id != $executionID and type in ('sprint','stage','kanban') and `project` = $projectID and `deleted` = '0'")
  491. ->checkFlow()
  492. ->where('id')->eq($executionID)
  493. ->exec();
  494. if(dao::isError()) return false;
  495. if(!empty($execution->project) and $oldExecution->project != $execution->project)
  496. {
  497. $execution->parent = $execution->project;
  498. $execution->path = ",{$execution->project},{$executionID},";
  499. $this->changeProject((int)$execution->project, $oldExecution->project, $executionID, isset($postData->syncStories[$executionID]) ? $postData->syncStories[$executionID] : 'no');
  500. }
  501. if(!empty($execution->attribute) && $oldExecution->attribute != $execution->attribute && $execution->attribute != 'mix')
  502. {
  503. $this->dao->update(TABLE_EXECUTION)->set('attribute')->eq($execution->attribute)->where('parent')->eq($executionID)->exec();
  504. }
  505. $changedAccounts = array();
  506. foreach($this->config->execution->ownerFields as $ownerField)
  507. {
  508. $owner = zget($execution, $ownerField, '');
  509. if(empty($owner) or isset($team[$owner])) continue;
  510. $member = new stdclass();
  511. $member->root = (int)$executionID;
  512. $member->account = $owner;
  513. $member->join = helper::today();
  514. $member->role = $this->lang->execution->$ownerField;
  515. $member->days = zget($execution, 'days', 0);
  516. $member->type = 'execution';
  517. $member->hours = $this->config->execution->defaultWorkhours;
  518. $this->dao->replace(TABLE_TEAM)->data($member)->exec();
  519. $changedAccounts[] = $owner;
  520. }
  521. if(!empty($changedAccounts)) $this->updateUserView($executionID, 'sprint', $changedAccounts);
  522. $allChanges[$executionID] = common::createChanges($oldExecution, $execution);
  523. }
  524. $this->fixOrder();
  525. return $allChanges;
  526. }
  527. /**
  528. * 批量更改执行的状态。
  529. * Batch change status.
  530. *
  531. * @param array $executionIdList
  532. * @param string $status
  533. * @access public
  534. * @return array 返回不符合条件被过滤了的执行,来提示执行下任务或子阶段已经开始,无法修改,已过滤。参见 story#41875。
  535. */
  536. public function batchChangeStatus($executionIdList, $status)
  537. {
  538. /* Sort the IDs, the child stage comes first, and the parent stage follows. */
  539. $executionList = $this->dao->select('id,name,status,grade,type,attribute,project')->from(TABLE_EXECUTION)->where('id')->in($executionIdList)->filterTpl(false)->orderBy('grade_desc')->fetchAll('id');
  540. $this->loadModel('programplan');
  541. $message = array('byChild' => '', 'byDeliverable' => '');
  542. foreach($executionList as $executionID => $execution)
  543. {
  544. $needCheckDeliverable = $status == 'closed' && $execution->status == 'doing' && $execution->grade == 1;
  545. if(in_array($this->config->edition, array('max', 'ipd')) && $needCheckDeliverable && !$this->canCloseByDeliverable($execution))
  546. {
  547. $message['byDeliverable'] .= '#' . $execution->id . ' ' . $execution->name . "\n";
  548. continue;
  549. }
  550. /* The state of the parent stage or the sibling stage may be affected by the child stage before the change, so it cannot be checked in advance. */
  551. $selfAndChildrenList = $this->programplan->getSelfAndChildrenList($executionID);
  552. $selfAndChildren = $selfAndChildrenList[$executionID];
  553. $execution = $selfAndChildren[$executionID];
  554. if($status == 'wait' and $execution->status != 'wait')
  555. {
  556. $message['byChild'] .= $this->changeStatus2Wait($executionID, $selfAndChildren);
  557. }
  558. if($status == 'doing' and $execution->status != 'doing')
  559. {
  560. $this->changeStatus2Doing($executionID, $selfAndChildren);
  561. }
  562. if(($status == 'suspended' and $execution->status != 'suspended') or ($status == 'closed' and $execution->status != 'closed'))
  563. {
  564. $message['byChild'] .= $this->changeStatus2Inactive($executionID, $status, $selfAndChildren);
  565. }
  566. }
  567. $message['byChild'] = trim($message['byChild'], ',');
  568. return $message;
  569. }
  570. /**
  571. * 设置状态为未开始。
  572. * Change status to wait.
  573. *
  574. * @param int $executionID
  575. * @param array $selfAndChildren
  576. * @access public
  577. * @return string
  578. */
  579. public function changeStatus2Wait($executionID, $selfAndChildren)
  580. {
  581. /* There are already tasks consuming work in this phase or its sub-phases already have start times. */
  582. $hasStartedChildren = $this->dao->select('id')->from(TABLE_EXECUTION)
  583. ->where('deleted')->eq('0')
  584. ->andWhere('realBegan')->notNULL()
  585. ->andWhere('id')->in(array_keys($selfAndChildren))
  586. ->andWhere('id')->ne($executionID)
  587. ->fetchPairs();
  588. $hasConsumedTasks = $this->dao->select('count(consumed) AS count')->from(TABLE_TASK)
  589. ->where('deleted')->eq('0')
  590. ->andWhere('execution')->in(array_keys($selfAndChildren))
  591. ->andWhere('consumed')->gt(0)
  592. ->fetch('count');
  593. if($hasStartedChildren or $hasConsumedTasks) return "'{$selfAndChildren[$executionID]->name}',";
  594. $newExecution = $this->buildExecutionByStatus('wait');
  595. $this->dao->update(TABLE_EXECUTION)->data($newExecution)->where('id')->eq($executionID)->exec();
  596. if(!dao::isError())
  597. {
  598. $changes = common::createChanges($selfAndChildren[$executionID], $newExecution);
  599. $actionID = $this->loadModel('action')->create('execution', $executionID, 'Edited');
  600. $this->action->logHistory($actionID, $changes);
  601. /* This stage has a parent stage. */
  602. $isTopStage = $this->loadModel('programplan')->isTopStage($executionID);
  603. if(!$isTopStage) $this->programplan->computeProgress($executionID);
  604. }
  605. return '';
  606. }
  607. /**
  608. * 设置状态为进行中。
  609. * Change status to doing.
  610. *
  611. * @param int $executionID
  612. * @param array $selfAndChildren
  613. * @access public
  614. * @return string
  615. */
  616. public function changeStatus2Doing($executionID, $selfAndChildren)
  617. {
  618. $this->loadModel('programplan');
  619. $this->loadModel('action');
  620. $newExecution = $this->buildExecutionByStatus('doing');
  621. $this->dao->update(TABLE_EXECUTION)->data($newExecution)->where('id')->eq($executionID)->exec();
  622. if(!dao::isError())
  623. {
  624. $changes = common::createChanges($selfAndChildren[$executionID], $newExecution);
  625. $actionID = $this->action->create('execution', $executionID, 'Started');
  626. $this->action->logHistory($actionID, $changes);
  627. /* This stage has a parent stage. */
  628. $isTopStage = $this->programplan->isTopStage($executionID);
  629. if(!$isTopStage) $this->programplan->computeProgress($executionID);
  630. }
  631. return '';
  632. }
  633. /**
  634. * 设置状态为暂停或者关闭。
  635. * Change status to suspended or closed.
  636. *
  637. * @param int $executionID
  638. * @param string $status
  639. * @param array $selfAndChildren
  640. * @access public
  641. * @return string
  642. */
  643. public function changeStatus2Inactive($executionID, $status, $selfAndChildren)
  644. {
  645. $checkedStatus = $status == 'suspended' ? 'wait,doing' : 'wait,doing,suspended';
  646. /* If status is suspended, the rules is there are sub-stages under this stage, and not all sub-stages are suspended or closed. */
  647. /* If status is closed, the rules is there are sub-stages under this stage, and not all sub-stages are closed. */
  648. $checkLeafStage = $this->loadModel('programplan')->checkLeafStage($executionID);
  649. if(!$checkLeafStage)
  650. {
  651. foreach($selfAndChildren as $childID => $child)
  652. {
  653. if($childID == $executionID) continue;
  654. if(strpos($checkedStatus, $child->status) !== false) return "'{$selfAndChildren[$executionID]->name}',";
  655. }
  656. }
  657. $newExecution = $this->buildExecutionByStatus($status);
  658. $this->dao->update(TABLE_EXECUTION)->data($newExecution)->where('id')->eq($executionID)->exec();
  659. if(!dao::isError())
  660. {
  661. $changes = common::createChanges($selfAndChildren[$executionID], $newExecution);
  662. $actionID = $this->loadModel('action')->create('execution', $executionID, strtoupper($status));
  663. $this->action->logHistory($actionID, $changes);
  664. /* Suspended: When all child stages at the same level are suspended or closed, the status of the parent stage becomes "suspended". */
  665. /* Closed: When all child stages at the same level are closed, the status of the parent stage becomes "closed". */
  666. $isTopStage = $this->programplan->isTopStage($executionID);
  667. if(!$isTopStage) $this->programplan->computeProgress($executionID);
  668. }
  669. return '';
  670. }
  671. /**
  672. * 开始一个执行。
  673. * Start the execution.
  674. *
  675. * @param int $executionID
  676. * @param object $postData
  677. * @access public
  678. * @return array|false
  679. */
  680. public function start($executionID, $postData)
  681. {
  682. $oldExecution = $this->fetchById($executionID);
  683. $execution = $postData;
  684. if(!empty($postData->uid)) $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->start['id'], $postData->uid);
  685. $this->dao->update(TABLE_EXECUTION)->data($execution, 'comment')
  686. ->autoCheck()
  687. ->check($this->config->execution->start->requiredFields, 'notempty')
  688. ->checkIF(!empty($execution->realBegan) && $execution->realBegan != '', 'realBegan', 'le', helper::today())
  689. ->checkFlow()
  690. ->where('id')->eq($executionID)
  691. ->exec();
  692. /* When it has multiple errors, only the first one is prompted */
  693. if(dao::isError() and count(dao::$errors['realBegan']) > 1) dao::$errors['realBegan'] = dao::$errors['realBegan'][0];
  694. if(dao::isError()) return false;
  695. /* Record the end date as firstEnd when the project is started. */
  696. $this->loadModel('project')->recordFirstEnd($executionID);
  697. $changes = common::createChanges($oldExecution, $execution);
  698. if(!empty($postData->comment) || !empty($changes))
  699. {
  700. $this->loadModel('action');
  701. $actionID = $this->action->create('execution', $executionID, 'Started', $this->post->comment);
  702. $this->action->logHistory($actionID, $changes);
  703. }
  704. return $changes;
  705. }
  706. /**
  707. * 延期一个迭代。
  708. * Delay the execution.
  709. *
  710. * @param int $executionID
  711. * @param object $postData
  712. * @access public
  713. * @return array|false
  714. */
  715. public function putoff($executionID, $postData)
  716. {
  717. $oldExecution = $this->fetchById($executionID);
  718. $this->checkBeginAndEndDate($oldExecution->project, $postData->begin, $postData->end);
  719. if(dao::isError()) return false;
  720. $execution = $this->loadModel('file')->processImgURL($postData, $this->config->execution->editor->putoff['id'], $postData->uid);
  721. $this->dao->update(TABLE_EXECUTION)->data($execution, 'comment,delta')
  722. ->autoCheck()
  723. ->checkFlow()
  724. ->batchCheck($this->config->execution->putoff->requiredFields, 'notempty')
  725. ->where('id')->eq($executionID)
  726. ->exec();
  727. if(dao::isError()) return false;
  728. $changes = common::createChanges($oldExecution, $execution);
  729. if($postData->comment != '' || !empty($changes))
  730. {
  731. $this->loadModel('action');
  732. $actionID = $this->action->create('execution', $executionID, 'Delayed', $this->post->comment);
  733. $this->action->logHistory($actionID, $changes);
  734. }
  735. return $changes;
  736. }
  737. /**
  738. * 挂起一个执行。
  739. * Suspend a execution.
  740. *
  741. * @param int $executionID
  742. * @param object $postData
  743. * @access public
  744. * @return array|false
  745. */
  746. public function suspend($executionID, $postData)
  747. {
  748. $oldExecution = $this->fetchById($executionID);
  749. $execution = $this->loadModel('file')->processImgURL($postData, $this->config->execution->editor->suspend['id'], (string)$this->post->uid);
  750. $this->dao->update(TABLE_EXECUTION)->data($execution, 'comment')
  751. ->autoCheck()
  752. ->checkFlow()
  753. ->where('id')->eq($executionID)
  754. ->exec();
  755. if(dao::isError()) return false;
  756. $changes = common::createChanges($oldExecution, $execution);
  757. if(!empty($changes) || $this->post->comment != '')
  758. {
  759. $actionID = $this->loadModel('action')->create('execution', $executionID, 'Suspended', $this->post->comment);
  760. $this->action->logHistory($actionID, $changes);
  761. }
  762. return $changes;
  763. }
  764. /**
  765. * 激活一个执行。
  766. * Activate a execution.
  767. *
  768. * @param int $executionID
  769. * @param object $postData
  770. * @access public
  771. * @return array|false
  772. */
  773. public function activate($executionID, $postData)
  774. {
  775. $oldExecution = $this->fetchById($executionID);
  776. if(empty($oldExecution->totalConsumed) and helper::isZeroDate($oldExecution->realBegan)) $postData->status = 'wait';
  777. /* Check the date which user input. */
  778. $begin = $postData->begin;
  779. $end = $postData->end;
  780. if($begin > $end) dao::$errors['end'] = sprintf($this->lang->execution->errorLesserPlan, $end, $begin); /* The begin date should larger than end. */
  781. if(dao::isError()) return false;
  782. /* Check the begin and end date if the execution has a parent, such as a child Stage, Sprint or Kanban. */
  783. if($oldExecution->parent != 0)
  784. {
  785. $parent = $this->dao->select('begin,end')->from(TABLE_PROJECT)->where('id')->eq($oldExecution->parent)->fetch();
  786. if(!$parent) return false;
  787. $parentBegin = $parent->begin;
  788. $parentEnd = $parent->end;
  789. if($begin < $parentBegin)
  790. {
  791. dao::$errors['begin'] = sprintf($this->lang->execution->errorLesserParent, $parentBegin); /* The begin date of child execution should larger than parent. */
  792. }
  793. if($end > $parentEnd)
  794. {
  795. dao::$errors['end'] = sprintf($this->lang->execution->errorGreaterParent, $parentEnd); /* The end date of child execution should lesser than parent. */
  796. }
  797. }
  798. if(dao::isError()) return false;
  799. /* Do update for this execution. */
  800. $execution = $this->loadModel('file')->processImgURL($postData, $this->config->execution->editor->activate['id'], $postData->uid);
  801. $this->dao->update(TABLE_EXECUTION)->data($execution, 'comment,readjustTask')
  802. ->autoCheck()
  803. ->checkFlow()
  804. ->where('id')->eq((int)$executionID)
  805. ->exec();
  806. /* 顺延任务的起止时间。Adjust the begin and end date for tasks in this execution. */
  807. if(!empty($postData->readjustTask))
  808. {
  809. $beginTimeStamp = strtotime($execution->begin);
  810. $tasks = $this->dao->select('id,estStarted,deadline,status')->from(TABLE_TASK)
  811. ->where('execution')->eq($executionID)
  812. ->andWhere('deadline')->notNULL()
  813. ->andWhere('status')->in('wait,doing')
  814. ->fetchAll();
  815. $this->loadModel('action');
  816. foreach($tasks as $task)
  817. {
  818. if(helper::isZeroDate($task->deadline)) continue;
  819. if($task->status == 'wait' and !helper::isZeroDate($task->estStarted))
  820. {
  821. $taskDays = helper::diffDate($task->deadline, $task->estStarted);
  822. $taskOffset = helper::diffDate($task->estStarted, $oldExecution->begin);
  823. $estStartedTimeStamp = $beginTimeStamp + $taskOffset * 24 * 3600;
  824. $estStarted = date('Y-m-d', $estStartedTimeStamp);
  825. $deadline = date('Y-m-d', $estStartedTimeStamp + $taskDays * 24 * 3600);
  826. if($estStarted > $execution->end) $estStarted = $execution->end;
  827. if($deadline > $execution->end) $deadline = $execution->end;
  828. $this->dao->update(TABLE_TASK)->set('estStarted')->eq($estStarted)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec();
  829. }
  830. else
  831. {
  832. $taskOffset = helper::diffDate($task->deadline, $oldExecution->begin);
  833. $deadline = date('Y-m-d', $beginTimeStamp + $taskOffset * 24 * 3600);
  834. if($deadline > $execution->end) $deadline = $execution->end;
  835. $this->dao->update(TABLE_TASK)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec();
  836. }
  837. $this->action->create('task', $task->id, 'Edited', $this->lang->execution->readjustTask );
  838. }
  839. }
  840. $changes = common::createChanges($oldExecution, $execution);
  841. if($this->post->comment != '' or !empty($changes))
  842. {
  843. $this->loadModel('action');
  844. $actionID = $this->action->create('execution', $executionID, 'Activated', $this->post->comment);
  845. $this->action->logHistory($actionID, $changes);
  846. }
  847. return $changes;
  848. }
  849. /**
  850. * 关闭迭代。
  851. * Close execution.
  852. *
  853. * @param int $executionID
  854. * @param object $postData
  855. * @access public
  856. * @return int|false
  857. */
  858. public function close($executionID, $postData)
  859. {
  860. $oldExecution = $this->fetchById($executionID); /* Save previous execution to variable for later compare. */
  861. $this->lang->error->ge = $this->lang->execution->ge;
  862. $execution = $this->loadModel('file')->processImgURL($postData, $this->config->execution->editor->close['id'], (string)$this->post->uid);
  863. $this->dao->update(TABLE_EXECUTION)->data($execution, 'comment')
  864. ->autoCheck()
  865. ->check($this->config->execution->close->requiredFields,'notempty')
  866. ->checkIF($execution->realEnd != '', 'realEnd', 'le', helper::today())
  867. ->checkIF($execution->realEnd != '', 'realEnd', 'ge', $oldExecution->realBegan)
  868. ->checkFlow()
  869. ->where('id')->eq($executionID)
  870. ->exec();
  871. /* When it has multiple errors, only the first one is prompted */
  872. if(dao::isError() && !empty(dao::$errors['realEnd']) && count(dao::$errors['realEnd']) > 1) dao::$errors['realEnd'] = dao::$errors['realEnd'][0];
  873. if(dao::isError()) return false;
  874. $changes = common::createChanges($oldExecution, $execution);
  875. if($this->post->comment != '' || !empty($changes))
  876. {
  877. $this->loadModel('action');
  878. $actionID = $this->action->create('execution', $executionID, 'Closed', $this->post->comment);
  879. $this->action->logHistory($actionID, $changes);
  880. }
  881. $this->loadModel('score')->create('execution', 'close', $oldExecution);
  882. return $actionID;
  883. }
  884. /**
  885. * 设置看板配置。
  886. * Set Kanban.
  887. *
  888. * @param int $executionID
  889. * @param object $execution
  890. * @access public
  891. * @return void
  892. */
  893. public function setKanban($executionID, $execution)
  894. {
  895. $this->loadModel('kanban');
  896. $this->lang->project->colWidth = $this->lang->kanban->colWidth;
  897. $this->lang->project->minColWidth = $this->lang->kanban->minColWidth;
  898. $this->lang->project->maxColWidth = $this->lang->kanban->maxColWidth;
  899. $this->dao->update(TABLE_EXECUTION)->data($execution)
  900. ->autoCheck()
  901. ->batchCheck($this->config->kanban->edit->requiredFields, 'notempty')
  902. ->checkIF(!$execution->fluidBoard, 'colWidth', 'ge', $this->config->minColWidth)
  903. ->batchCheckIF($execution->fluidBoard, 'minColWidth', 'ge', $this->config->minColWidth)
  904. ->checkIF($execution->minColWidth >= $this->config->minColWidth && $execution->fluidBoard, 'maxColWidth', 'gt', $execution->minColWidth)
  905. ->where('id')->eq((int)$executionID)
  906. ->exec();
  907. }
  908. /**
  909. * 检查阶段的工作量占比。
  910. * Check the workload format and total.
  911. *
  912. * @param string $type create|update
  913. * @param float $percent
  914. * @param object $oldExecution
  915. * @access public
  916. * @return bool
  917. */
  918. public function checkWorkload($type = '', $percent = 0, $oldExecution = null)
  919. {
  920. /* Check whether the workload is positive. */
  921. if(!preg_match("/^[0-9]+(.[0-9]{1,3})?$/", (string)$percent))
  922. {
  923. dao::$errors['percent'] = $this->lang->programplan->error->percentNumber;
  924. return false;
  925. }
  926. $oldExecutionGrade = !empty($oldExecution->grade) ? $oldExecution->grade : 0;
  927. $oldExecutionType = !empty($oldExecution->type) ? $oldExecution->type : '';
  928. $oldExecutionID = !empty($oldExecution->id) ? $oldExecution->id : 0;
  929. $oldExecutionParent = !empty($oldExecution->parent) ? $oldExecution->parent : 0;
  930. $oldExecutionPercent = !empty($oldExecution->percent) ? $oldExecution->percent : 0;
  931. $oldExecutionProject = !empty($oldExecution->project) ? $oldExecution->project : 0;
  932. /* The total workload of the first stage should not exceed 100%. */
  933. if($type == 'create' || ($oldExecutionGrade == 1 && isset($this->lang->execution->typeList[$oldExecutionType])))
  934. {
  935. $branchID = !empty($_POST['branch'][0]) ? current($this->post->branch[0]) : 0;
  936. $oldPercentTotal = $this->dao->select('SUM(t2.percent) as total')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  937. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id')
  938. ->where('t1.product')->eq($this->post->products[0])
  939. ->beginIF($branchID)->andWhere('t1.branch')->eq($branchID)->fi()
  940. ->andWhere('t2.type')->eq('stage')
  941. ->andWhere('t2.grade')->eq(1)
  942. ->andWhere('t2.deleted')->eq(0)
  943. ->beginIF($type == 'create')->andWhere('t2.parent')->eq($oldExecutionID)->fi()
  944. ->beginIF(!empty($oldExecution) && isset($this->lang->execution->typeList[$oldExecutionType]))->andWhere('t2.parent')->eq($oldExecutionParent)->fi()
  945. ->fetch('total');
  946. if(!$oldPercentTotal) $oldPercentTotal = 0;
  947. if($type == 'create') $percentTotal = $percent + $oldPercentTotal;
  948. if(!empty($oldExecution) && isset($this->lang->execution->typeList[$oldExecutionType])) $percentTotal = $oldPercentTotal - $oldExecutionPercent + $percent;
  949. if($percentTotal > 100)
  950. {
  951. $printPercent = $type == 'create' ? $oldPercentTotal : $percentTotal;
  952. dao::$errors['percent'] = sprintf($this->lang->execution->workloadTotal, '%', $printPercent . '%');
  953. return false;
  954. }
  955. }
  956. if($type == 'update' && $oldExecutionGrade > 1)
  957. {
  958. $childrenTotalPercent = $this->dao->select('SUM(percent) as total')->from(TABLE_EXECUTION)->where('parent')->eq($oldExecutionParent)->andWhere('project')->eq($oldExecutionProject)->andWhere('deleted')->eq(0)->fetch('total');
  959. $childrenTotalPercent = $childrenTotalPercent - $oldExecutionPercent + $percent;
  960. if($childrenTotalPercent > 100)
  961. {
  962. dao::$errors['percent'] = sprintf($this->lang->execution->workloadTotal, '%', $childrenTotalPercent . '%');
  963. return false;
  964. }
  965. }
  966. return true;
  967. }
  968. /**
  969. * 检查执行开始、结束日期是否正确。
  970. * Check begin and end date.
  971. *
  972. * @param int $projectID
  973. * @param string $begin
  974. * @param string $end
  975. * @param int $parentID
  976. * @access public
  977. * @return void
  978. */
  979. public function checkBeginAndEndDate($projectID, $begin, $end, $parentID = 0)
  980. {
  981. $project = $this->loadModel('project')->fetchByID($projectID);
  982. if(empty($project)) return;
  983. if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd')) && $parentID != $projectID)
  984. {
  985. $this->app->loadLang('programplan');
  986. $parent = $this->fetchByID($parentID);
  987. if($parent && $begin < $parent->begin) dao::$errors['begin'] = sprintf($this->lang->programplan->error->letterParent, $parent->begin);
  988. if($parent && $end > $parent->end) dao::$errors['end'] = sprintf($this->lang->programplan->error->greaterParent, $parent->end);
  989. }
  990. if(dao::isError()) return;
  991. if($begin < $project->begin) dao::$errors['begin'] = sprintf($this->lang->execution->errorCommonBegin, $project->begin);
  992. if(!helper::isZeroDate($project->end) && $end > $project->end) dao::$errors['end'] = sprintf($this->lang->execution->errorCommonEnd, $project->end);
  993. }
  994. /**
  995. * 获取执行id:name的键值对。
  996. * Get execution pairs.
  997. *
  998. * @param int $projectID
  999. * @param string $type all|sprint|stage|kanban
  1000. * @param string $mode all|noclosed|stagefilter|withdelete|multiple|leaf|order_asc|empty|noprefix|withobject|hideMultiple
  1001. * @param bool $ignoreVision
  1002. * @access public
  1003. * @return array
  1004. */
  1005. public function getPairs($projectID = 0, $type = 'all', $mode = '', $ignoreVision = false)
  1006. {
  1007. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionPairs();
  1008. $mode .= $this->cookie->executionMode;
  1009. $orderBy = $this->config->execution->orderBy;
  1010. $projectModel = '';
  1011. if($projectID)
  1012. {
  1013. $projectInfo = $this->dao->select('model,isTpl')->from(TABLE_EXECUTION)->where('id')->eq($projectID)->andWhere('deleted')->eq(0)->fetch();
  1014. $projectModel = isset($projectInfo->model) ? $projectInfo->model : '';
  1015. $orderBy = in_array($projectModel, array('waterfall', 'waterfallplus')) ? 'sortStatus_asc,begin_asc,id_asc' : 'id_desc';
  1016. if(!empty($projectInfo->isTpl)) $this->dao->filterTpl('never');
  1017. /* Waterfall execution, when all phases are closed, in reverse order of date. */
  1018. if(in_array($projectModel, array('waterfall', 'waterfallplus')))
  1019. {
  1020. $statistic = $this->dao->select("count(id) as executions, sum(IF(INSTR('closed', status) < 1, 0, 1)) as closedExecutions")->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('deleted')->eq('0')->fetch();
  1021. if($statistic->executions == $statistic->closedExecutions) $orderBy = 'sortStatus_asc,begin_desc,id_asc';
  1022. }
  1023. }
  1024. /* Order by status's content whether or not done. */
  1025. $executions = $this->dao->select("*, IF(INSTR('done,closed', status) < 2, 0, 1) AS isDone, INSTR('doing,wait,suspended,closed', status) AS sortStatus")->from(TABLE_EXECUTION)
  1026. ->where('1=1')
  1027. ->beginIF(!$ignoreVision)->andWhere('vision')->eq($this->config->vision)->fi()
  1028. ->beginIF((!$this->session->multiple && $this->app->tab == 'execution') || strpos($mode, 'multiple') !== false)->andWhere('multiple')->eq('1')->fi()
  1029. ->beginIF($type != 'all')->andWhere('type')->in($type)->fi()
  1030. ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint,kanban')->fi()
  1031. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  1032. ->beginIF(strpos($mode, 'withdelete') === false)->andWhere('deleted')->eq(0)->fi()
  1033. ->beginIF(!$this->app->user->admin && strpos($mode, 'all') === false)->andWhere('id')->in($this->app->user->view->sprints)->fi()
  1034. ->orderBy($orderBy)
  1035. ->fetchAll('id');
  1036. if(strpos($mode, 'order_asc') !== false) $executions = $this->resetExecutionSorts($executions);
  1037. /* If mode == leaf, only show leaf executions. */
  1038. $allExecutions = $this->dao->select('id,name,parent,grade')->from(TABLE_EXECUTION)
  1039. ->where('type')->notin(array('program', 'project'))
  1040. ->andWhere('deleted')->eq('0')
  1041. ->beginIf($projectID)->andWhere('project')->eq($projectID)->fi()
  1042. ->fetchAll('id');
  1043. $parents = array();
  1044. foreach($allExecutions as $exec) $parents[$exec->parent] = true;
  1045. $projectPairs = strpos($mode, 'withobject') !== false ? $this->dao->select('id,name')->from(TABLE_PROJECT)->fetchPairs('id') : array();
  1046. return $this->executionTao->buildExecutionPairs($mode, $allExecutions, $executions, $parents, $projectPairs, $projectModel);
  1047. }
  1048. /**
  1049. * 根据执行ID列表获取执行列表信息。
  1050. * Get the execution list information by the execution ID list.
  1051. *
  1052. * @param array $executionIdList
  1053. * @param string $mode all
  1054. * @param string $orderBy
  1055. * @access public
  1056. * @return array
  1057. */
  1058. public function getByIdList($executionIdList = array(), $mode = '', $orderBy = 'id_asc')
  1059. {
  1060. return $this->dao->select('*,whitelist')->from(TABLE_EXECUTION)
  1061. ->where('id')->in($executionIdList)
  1062. ->beginIF($mode != 'all')->andWhere('deleted')->eq(0)->fi()
  1063. ->orderBy($orderBy)
  1064. ->fetchAll('id');
  1065. }
  1066. /**
  1067. * 获取执行列表信息。
  1068. * Get execution list information.
  1069. *
  1070. * @param int $projectID
  1071. * @param string $type all|sprint|stage|kanban
  1072. * @param string $status all|undone|wait|running
  1073. * @param int $limit
  1074. * @param int $productID
  1075. * @param int $branch
  1076. * @param object|null $pager
  1077. * @param bool $withChildren
  1078. * @access public
  1079. * @return array
  1080. */
  1081. public function getList($projectID = 0, $type = 'all',$status = 'all', $limit = 0, $productID = 0, $branch = 0, $pager = null, $withChildren = true)
  1082. {
  1083. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionStats($type);
  1084. if($status == 'involved') return $this->getInvolvedExecutionList($projectID, $status, $limit, $productID, $branch);
  1085. if($productID != 0)
  1086. {
  1087. return $this->dao->select('t2.*, t2.`desc`')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  1088. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project= t2.id')
  1089. ->where('t1.product')->eq($productID)
  1090. ->andWhere('t2.deleted')->eq(0)
  1091. ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi()
  1092. ->beginIF($type == 'all')->andWhere('t2.type')->in('sprint,stage,kanban')->fi()
  1093. ->beginIF($type != 'all')->andWhere('t2.type')->eq($type)->fi()
  1094. ->beginIF($status == 'undone')->andWhere('t2.status')->notIN('done,closed')->fi()
  1095. ->beginIF($status == 'delayed')->andWhere('t2.end')->gt('1970-1-1')->andWhere('t2.end')->lt(date(DT_DATE1))->andWhere('t2.status')->notin('done,closed,suspended')->fi()
  1096. ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi()
  1097. ->beginIF($status != 'all' && $status != 'undone' && $status != 'delayed')->andWhere('t2.status')->in($status)->fi()
  1098. ->beginIF(!$this->app->user->admin and isset($this->app->user->view))->andWhere('t2.id')->in($this->app->user->view->sprints)->fi()
  1099. ->beginIF(!$withChildren)->andWhere('grade')->eq(1)->fi()
  1100. ->orderBy('order_desc')
  1101. ->page($pager)
  1102. ->beginIF($limit)->limit($limit)->fi()
  1103. ->fetchAll('id');
  1104. }
  1105. else
  1106. {
  1107. return $this->dao->select("*, `desc`, IF(INSTR(' done,closed', status) < 2, 0, 1) AS isDone")->from(TABLE_EXECUTION)
  1108. ->where('deleted')->eq(0)
  1109. ->andWhere('vision')->eq($this->config->vision)
  1110. ->beginIF($type == 'all')->andWhere('type')->in('sprint,stage,kanban')->fi()
  1111. ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
  1112. ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
  1113. ->beginIF($status == 'delayed')->andWhere('end')->gt('1970-1-1')->andWhere('end')->lt(date(DT_DATE1))->andWhere('status')->notin('done,closed,suspended')->fi()
  1114. ->beginIF($status != 'all' && $status != 'undone' && $status != 'delayed')->andWhere('status')->in($status)->fi()
  1115. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  1116. ->beginIF(!$this->app->user->admin and isset($this->app->user->view))->andWhere('id')->in($this->app->user->view->sprints)->fi()
  1117. ->beginIF(!$withChildren)->andWhere('grade')->eq(1)->fi()
  1118. ->orderBy('order_desc')
  1119. ->page($pager)
  1120. ->beginIF($limit)->limit($limit)->fi()
  1121. ->fetchAll('id');
  1122. }
  1123. }
  1124. /**
  1125. * 获取泳道的最后更新日期。
  1126. * Get the last updated date of the lane.
  1127. *
  1128. * @param int $executionID
  1129. * @access public
  1130. * @return string|null
  1131. */
  1132. public function getLaneMaxEditedTime($executionID)
  1133. {
  1134. return $this->dao->select("max(lastEditedTime) as lastEditedTime")->from(TABLE_KANBANLANE)->where('execution')->eq($executionID)->fetch('lastEditedTime');
  1135. }
  1136. /**
  1137. * 获取我参与的执行列表信息。
  1138. * Get involved execution list information.
  1139. *
  1140. * @param int $projectID
  1141. * @param string $status involved
  1142. * @param int $limit
  1143. * @param int $productID
  1144. * @param int $branch
  1145. * @access public
  1146. * @return object[]
  1147. */
  1148. public function getInvolvedExecutionList($projectID = 0, $status = 'involved', $limit = 0, $productID = 0, $branch = 0)
  1149. {
  1150. if($productID != 0)
  1151. {
  1152. return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  1153. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project= t2.id')
  1154. ->leftJoin(TABLE_TEAM)->alias('t3')->on('t3.root=t2.id')
  1155. ->where('t1.product')->eq($productID)
  1156. ->andWhere('t2.deleted')->eq(0)
  1157. ->andWhere('t2.type')->in('sprint,stage,kanban')
  1158. ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi()
  1159. ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi()
  1160. ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi()
  1161. ->andWhere('t2.openedBy', true)->eq($this->app->user->account)
  1162. ->orWhere('t3.account')->eq($this->app->user->account)
  1163. ->markRight(1)
  1164. ->orderBy('order_desc')
  1165. ->beginIF($limit)->limit($limit)->fi()
  1166. ->fetchAll('id');
  1167. }
  1168. else
  1169. {
  1170. return $this->dao->select("t1.*, IF(INSTR(' done,closed', t1.status) < 2, 0, 1) AS isDone")->from(TABLE_EXECUTION)->alias('t1')
  1171. ->leftJoin(TABLE_TEAM)->alias('t2')->on('t2.root=t1.id')
  1172. ->where('t1.deleted')->eq(0)
  1173. ->andWhere('t1.type')->in('sprint,stage,kanban')
  1174. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
  1175. ->beginIF($projectID)->andWhere('t1.project')->eq($projectID)->fi()
  1176. ->andWhere('t1.openedBy', true)->eq($this->app->user->account)
  1177. ->orWhere('t2.account')->eq($this->app->user->account)
  1178. ->markRight(1)
  1179. ->orderBy('t1.order_desc')
  1180. ->beginIF($limit)->limit($limit)->fi()
  1181. ->fetchAll('id');
  1182. }
  1183. }
  1184. /**
  1185. * 获取给定项目下所有执行的Id列表。
  1186. * Get execution id list by project.
  1187. *
  1188. * @param int $projectID
  1189. * @param string $status all|undone|wait|doing|suspended|closed
  1190. * @access public
  1191. * @return array
  1192. */
  1193. public function getIdList($projectID, $status = 'all')
  1194. {
  1195. return $this->dao->select('id')->from(TABLE_EXECUTION)
  1196. ->where('type')->in('sprint,stage,kanban')
  1197. ->andWhere('deleted')->eq('0')
  1198. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  1199. ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
  1200. ->beginIF(!in_array($status, array('all', 'undone')))->andWhere('status')->in($status)->fi()
  1201. ->fetchPairs('id', 'id');
  1202. }
  1203. /**
  1204. * Get execution count.
  1205. *
  1206. * @param int $projectID
  1207. * @param string $browseType all|undone|wait|doing|suspended|closed|involved|review
  1208. * @access public
  1209. * @return int
  1210. */
  1211. public function getExecutionCounts($projectID = 0, $browseType = 'all')
  1212. {
  1213. $executions = $this->dao->select('t1.*,t2.`name` as projectName, t2.`model` as projectModel')->from(TABLE_EXECUTION)->alias('t1')
  1214. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  1215. ->where('t1.type')->in('sprint,stage,kanban')
  1216. ->andWhere('t1.deleted')->eq('0')
  1217. ->andWhere('t1.vision')->eq($this->config->vision)
  1218. ->andWhere('t1.multiple')->eq('1')
  1219. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
  1220. ->beginIF($projectID)->andWhere('t1.project')->eq($projectID)->fi()
  1221. ->beginIF(!in_array($browseType, array('all', 'undone', 'involved', 'review', 'bySearch')))->andWhere('t1.status')->eq($browseType)->fi()
  1222. ->beginIF($browseType == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi()
  1223. ->beginIF($browseType == 'review')
  1224. ->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
  1225. ->andWhere('t1.reviewStatus')->eq('doing')
  1226. ->fi()
  1227. ->fetchAll('id');
  1228. return count($executions);
  1229. }
  1230. /**
  1231. * 获取执行数据。
  1232. * Get execution stat data.
  1233. *
  1234. * @param int $projectID
  1235. * @param string $browseType all|undone|wait|doing|suspended|closed|involved|bySearch|review
  1236. * @param int $productID
  1237. * @param int $branch
  1238. * @param bool $withTasks
  1239. * @param string|int $param skipParent|hasParentName
  1240. * @param string $orderBy
  1241. * @param object|null $pager
  1242. * @access public
  1243. * @return array
  1244. */
  1245. public function getStatData($projectID = 0, $browseType = 'undone', $productID = 0, $branch = 0, $withTasks = false, $param = '', $orderBy = 'id_asc', $pager = null)
  1246. {
  1247. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionStats($browseType);
  1248. $executions = $this->fetchExecutionList($projectID, $browseType, $productID, (int) $param, $orderBy, $pager);
  1249. $executions = $this->batchProcessExecution($executions, $projectID, $productID, $withTasks, $param);
  1250. return array_values($executions);
  1251. }
  1252. /**
  1253. * 获取搜索执行的查询语句。
  1254. * Get execution query SQL.
  1255. *
  1256. * @param int $queryID
  1257. * @access public
  1258. * @return string
  1259. */
  1260. public function getExecutionQuery($queryID)
  1261. {
  1262. /* Get query SQL. */
  1263. if($queryID)
  1264. {
  1265. $query = $this->loadModel('search')->getQuery($queryID);
  1266. if($query)
  1267. {
  1268. $this->session->set('executionQuery', $query->sql);
  1269. $this->session->set('executionForm', $query->form);
  1270. }
  1271. }
  1272. if($this->session->executionQuery === false) $this->session->set('executionQuery', ' 1 = 1');
  1273. $executionQuery = $this->session->executionQuery;
  1274. /* If all projects are searched change the query SQL to 1=1. */
  1275. $allProject = "`project` = 'all'";
  1276. if(strpos($executionQuery, $allProject) !== false) $executionQuery = str_replace($allProject, '1', $executionQuery);
  1277. /* Replace field. */
  1278. $executionQuery = preg_replace('/(`\w*`)/', 't1.$1',$executionQuery);
  1279. return $executionQuery;
  1280. }
  1281. /**
  1282. * 获取执行id=>name的键值对。
  1283. * Get an array of execution id:name.
  1284. *
  1285. * @param int $projectID
  1286. * @param string $type
  1287. * @param bool $filterMulti
  1288. * @param bool $queryAll
  1289. * @access public
  1290. * @return array
  1291. */
  1292. public function fetchPairs($projectID = 0, $type = 'all', $filterMulti = true, $queryAll = false)
  1293. {
  1294. return $this->dao->select('id,name')->from(TABLE_EXECUTION)
  1295. ->where('deleted')->eq(0)
  1296. ->andWhere('vision')->eq($this->config->vision)
  1297. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  1298. ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint,kanban')->fi()
  1299. ->beginIF($type != 'all')->andWhere('type')->in($type)->fi()
  1300. ->beginIF($filterMulti)->andWhere('multiple')->eq('1')->fi()
  1301. ->beginIF(!$queryAll && !$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi()
  1302. ->fetchPairs();
  1303. }
  1304. /**
  1305. * 批量查询关联传入项目的执行,并按照项目分组。
  1306. * Fetch executions of linked project by project id list.
  1307. *
  1308. * @param array $projectIdList
  1309. * @access public
  1310. * @return array
  1311. */
  1312. public function fetchExecutionsByProjectIdList($projectIdList = array())
  1313. {
  1314. return $this->dao->select('t1.*,t2.`name` projectName, t2.`model` as projectModel')->from(TABLE_EXECUTION)->alias('t1')
  1315. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  1316. ->where('t1.type')->in('sprint,stage,kanban')
  1317. ->andWhere('t1.deleted')->eq('0')
  1318. ->andWhere('t1.vision')->eq($this->config->vision)
  1319. ->andWhere('t1.multiple')->eq('1')
  1320. ->andWhere('t1.project')->in($projectIdList)
  1321. ->fetchGroup('project', 'id');
  1322. }
  1323. /**
  1324. * 获取执行列表信息。
  1325. * Get execution list information.
  1326. *
  1327. * @param int $projectID
  1328. * @param string $browseType all|undone|wait|doing|suspended|closed|involved|bySearch|review
  1329. * @param int $productID
  1330. * @param string $orderBy
  1331. * @param int $param
  1332. * @param object|null $pager
  1333. * @access public
  1334. * @return array
  1335. */
  1336. public function fetchExecutionList($projectID = 0, $browseType = 'undone', $productID = 0, $param = 0, $orderBy = 'id_asc', $pager = null)
  1337. {
  1338. if(strpos($orderBy, 'nameCol') !== false) $orderBy = str_replace('nameCol', 'name', $orderBy);
  1339. /* Construct the query SQL at search executions. */
  1340. $executionQuery = $browseType == 'bySearch' ? $this->getExecutionQuery($param) : '';
  1341. $projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('model');
  1342. return $this->dao->select('t1.*,t2.`name` as projectName, t2.`model` as projectModel')->from(TABLE_EXECUTION)->alias('t1')
  1343. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
  1344. ->beginIF($productID)->leftJoin(TABLE_PROJECTPRODUCT)->alias('t3')->on('t1.id=t3.project')->fi()
  1345. ->where('t1.type')->in('sprint,stage,kanban')
  1346. ->andWhere('t1.deleted')->eq('0')
  1347. ->andWhere('t1.vision')->eq($this->config->vision)
  1348. ->andWhere('t1.multiple')->eq('1')
  1349. ->beginIF($projectModel == 'ipd')->andWhere('t1.enabled')->eq('on')->fi()
  1350. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
  1351. ->beginIF(!empty($executionQuery))->andWhere($executionQuery)->fi()
  1352. ->beginIF($productID)->andWhere('t3.product')->eq($productID)->fi()
  1353. ->beginIF($projectID)->andWhere('t1.project')->eq($projectID)->fi()
  1354. ->beginIF(!in_array($browseType, array('all', 'undone', 'involved', 'review', 'bySearch', 'delayed')))->andWhere('t1.status')->eq($browseType)->fi()
  1355. ->beginIF($browseType == 'delayed')->andWhere('t1.end')->gt('1970-1-1')->andWhere('t1.end')->lt(date(DT_DATE1))->andWhere('t1.status')->notin('done,closed,suspended')->fi()
  1356. ->beginIF($browseType == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi()
  1357. ->beginIF($browseType == 'review')
  1358. ->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
  1359. ->andWhere('t1.reviewStatus')->eq('doing')
  1360. ->fi()
  1361. ->orderBy($orderBy)
  1362. ->page($pager, 't1.id')
  1363. ->fetchAll('id', false);
  1364. }
  1365. /**
  1366. * 批量处理执行数据。
  1367. * Batch process execution data.
  1368. *
  1369. * @param array $executions
  1370. * @param int $projectID
  1371. * @param int $productID
  1372. * @param bool $withTasks
  1373. * @param string|int $param skipParent|hasParentName
  1374. * @param array $executionTasks
  1375. * @access public
  1376. * @return array
  1377. */
  1378. public function batchProcessExecution($executions, $projectID = 0, $productID = 0, $withTasks = false, $param = '', $executionTasks = array())
  1379. {
  1380. if(empty($executions)) return $executions;
  1381. $project = $this->loadModel('project')->fetchByID($projectID);
  1382. $productList = $this->executionTao->getProductList($projectID); // Get product name of the linked execution.
  1383. if($withTasks && empty($executionTasks)) $executionTasks = $this->getTaskGroupByExecution(array_keys($executions), false);
  1384. $parentList = array();
  1385. $today = helper::today();
  1386. $burns = $this->getBurnData($executions);
  1387. $parentExecutions = $this->dao->select('parent,parent')->from(TABLE_EXECUTION)->where('parent')->ne(0)->andWhere('deleted')->eq(0)->fetchPairs();
  1388. $statusGroup = $this->dao->select('parent,status')->from(TABLE_EXECUTION)->where('parent')->in(array_keys($executions))->andWhere('deleted')->eq(0)->fetchGroup('parent', 'status');
  1389. /* Get workingDays. */
  1390. $earliestEnd = $today;
  1391. foreach($executions as $execution)
  1392. {
  1393. if(!empty($execution->end) && !helper::isZeroDate($execution->end) && $execution->end < $earliestEnd) $earliestEnd = $execution->end;
  1394. }
  1395. $workingDays = $this->loadModel('holiday')->getActualWorkingDays($earliestEnd, $today);
  1396. foreach($executions as $execution)
  1397. {
  1398. $execution->productName = isset($productList[$execution->id]) ? trim($productList[$execution->id]->productName, ',') : '';
  1399. $execution->product = $productID;
  1400. $execution->productID = $productID;
  1401. $execution->delay = 0;
  1402. $execution->totalEstimate = $execution->estimate;
  1403. $execution->totalConsumed = $execution->consumed;
  1404. $execution->totalLeft = $execution->left;
  1405. if($execution->end) $execution->end = date(DT_DATE1, strtotime($execution->end));
  1406. if(!isset($execution->projectName)) $execution->projectName = $project->name;
  1407. if(!isset($execution->projectModel)) $execution->projectModel = $project->model;
  1408. if(isset($parentExecutions[$execution->id])) $executions[$execution->id]->isParent = 1;
  1409. if(empty($productID) && !empty($productList[$execution->id])) $execution->product = trim($productList[$execution->id]->product, ',');
  1410. /** 如果子执行都关闭了,则父执行可以手动关闭。 */
  1411. if(isset($statusGroup[$execution->id]))
  1412. {
  1413. $childStatus = array_keys($statusGroup[$execution->id]);
  1414. if(count($childStatus) == 1 && $childStatus[0] == 'closed') $execution->parentCanClose = true;
  1415. }
  1416. /* Judge whether the execution is delayed. */
  1417. if($execution->status != 'done' && $execution->status != 'closed' && $execution->status != 'suspended' && !empty($workingDays))
  1418. {
  1419. $betweenDays = $this->holiday->getDaysBetween($execution->end, $today);
  1420. if($betweenDays)
  1421. {
  1422. $delayDays = array_intersect($betweenDays, $workingDays);
  1423. $delay = count($delayDays) - 1;
  1424. if($delay > 0) $execution->delay = $delay;
  1425. }
  1426. }
  1427. /* Process the burns. */
  1428. $execution->burns = array();
  1429. $burnData = isset($burns[$execution->id]) ? $burns[$execution->id] : array();
  1430. foreach($burnData as $data) $execution->burns[] = (float)$data->value;
  1431. if(isset($executionTasks) && isset($executionTasks[$execution->id])) $execution->tasks = $executionTasks[$execution->id];
  1432. /* In the case of the waterfall model, calculate the sub-stage. */
  1433. if($param === 'skipParent')
  1434. {
  1435. if($execution->parent) $parentList[$execution->parent] = $execution->parent;
  1436. if($execution->projectName) $execution->name = $execution->projectName . ' / ' . $execution->name;
  1437. }
  1438. elseif(strpos((string)$param, 'hasParentName') !== false)
  1439. {
  1440. $parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($execution->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs();
  1441. $executions[$execution->id]->title = implode('/', $parentExecutions);
  1442. }
  1443. }
  1444. foreach($parentList as $parentID) unset($executions[$parentID]);
  1445. return $executions;
  1446. }
  1447. /**
  1448. * 根据项目ID获取项目下的执行信息。
  1449. * Get executions data by project.
  1450. *
  1451. * @param int $projectID
  1452. * @param string $status
  1453. * @param int $limit
  1454. * @param bool $pairs
  1455. * @param bool $devel
  1456. * @param int $appendedID
  1457. * @access public
  1458. * @return object[]|array
  1459. */
  1460. public function getByProject($projectID, $status = 'all', $limit = 0, $pairs = false, $devel = false, $appendedID = 0)
  1461. {
  1462. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionPairs();
  1463. $project = $this->loadModel('project')->fetchByID($projectID);
  1464. $executions = $this->dao->select('*')->from(TABLE_EXECUTION)
  1465. ->where('type')->in('stage,sprint,kanban')
  1466. ->andWhere('deleted')->eq('0')
  1467. ->andWhere('vision')->eq($this->config->vision)
  1468. ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
  1469. ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi()
  1470. ->beginIF(!in_array($status, array('all', 'undone', 'noclosed')))->andWhere('status')->in($status)->fi()
  1471. ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
  1472. ->beginIF($status == 'noclosed')->andWhere('status')->ne('closed')->fi()
  1473. ->beginIF($devel === true)->andWhere('attribute')->in('dev,qa,release')->fi()
  1474. ->beginIF($appendedID)->orWhere('id')->eq($appendedID)->fi()
  1475. ->orderBy('order_asc')
  1476. ->beginIF($limit)->limit($limit)->fi()
  1477. ->fetchAll('id');
  1478. /* Add product name and parent stage name to stage name. */
  1479. $executions = $this->executionTao->batchProcessName($executions, $project);
  1480. if(!$pairs) return $executions;
  1481. $projectPairs = array();
  1482. if(empty($projectID))
  1483. {
  1484. $projectPairs = $this->dao->select('t1.id,t1.name')->from(TABLE_PROJECT)->alias('t1')
  1485. ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.id=t2.project')
  1486. ->where('t2.id')->in(array_keys($executions))
  1487. ->fetchPairs('id', 'name');
  1488. }
  1489. else
  1490. {
  1491. $projectPairs = $this->dao->select('id,name')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetchPairs('id', 'name');
  1492. }
  1493. $this->app->loadLang('project');
  1494. $executionPairs = array();
  1495. foreach($executions as $execution)
  1496. {
  1497. $executionPairs[$execution->id] = '';
  1498. $executionPairs[$execution->id] .= isset($projectPairs[$execution->project]) ? ($projectPairs[$execution->project] . '/') : '';
  1499. $executionPairs[$execution->id] .= $execution->name;
  1500. if(empty($execution->multiple)) $executionPairs[$execution->id] = $projectPairs[$execution->project] . "({$this->lang->project->disableExecution})";
  1501. }
  1502. return $executionPairs;
  1503. }
  1504. /**
  1505. * 批量处理执行的名称。
  1506. * The name of the batch process execution.
  1507. *
  1508. * @param array $executions
  1509. * @param object|bool $project
  1510. * @access protected
  1511. * @return array
  1512. */
  1513. protected function batchProcessName($executions, $project)
  1514. {
  1515. if(!$project) return $executions;
  1516. if(isset($project->model) && in_array($project->model, array('waterfall', 'waterfallplus', 'ipd')))
  1517. {
  1518. $executionProducts = array();
  1519. if($project->hasProduct && ($project->stageBy == 'product'))
  1520. {
  1521. $executionProducts = $this->dao->select('t1.project, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  1522. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id')
  1523. ->where('project')->in(array_keys($executions))
  1524. ->andWhere('t2.deleted')->eq(0)
  1525. ->fetchPairs();
  1526. }
  1527. $allExecutions = $this->dao->select('id,name,parent,grade')->from(TABLE_EXECUTION)
  1528. ->where('type')->in('stage,sprint,kanban')
  1529. ->andWhere('deleted')->eq('0')
  1530. ->andWhere('project')->eq($project->id)
  1531. ->fetchAll('id');
  1532. $parents = array();
  1533. foreach($allExecutions as $id => $execution) $parents[$execution->parent] = $execution->parent;
  1534. $executions = $this->resetExecutionSorts($executions);
  1535. foreach($executions as $id => $execution)
  1536. {
  1537. if(isset($parents[$execution->id]))
  1538. {
  1539. unset($executions[$id]);
  1540. continue;
  1541. }
  1542. $executionName = '';
  1543. $paths = array_slice(explode(',', trim($execution->path, ',')), 1);
  1544. foreach($paths as $path)
  1545. {
  1546. if(isset($allExecutions[$path])) $executionName .= '/' . $allExecutions[$path]->name;
  1547. }
  1548. if($executionName) $execution->name = ltrim($executionName, '/');
  1549. if(isset($executionProducts[$id])) $execution->name = $executionProducts[$id] . '/' . $execution->name;
  1550. }
  1551. }
  1552. return $executions;
  1553. }
  1554. /**
  1555. * 获取影子执行的ID。
  1556. * Get no multiple execution id.
  1557. *
  1558. * @param int $projectID
  1559. * @access public
  1560. * @return int
  1561. */
  1562. public function getNoMultipleID($projectID)
  1563. {
  1564. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getExecution()->id;
  1565. return (int)$this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('multiple')->eq(0)->andWhere('deleted')->eq(0)->fetch('id');
  1566. }
  1567. /**
  1568. * 获取执行下分支列表。
  1569. * Get branches of execution.
  1570. *
  1571. * @param int $executionID
  1572. * @access public
  1573. * @return array
  1574. */
  1575. public function getBranches($executionID)
  1576. {
  1577. $productBranchPairs = $this->dao->select('product, branch')->from(TABLE_PROJECTPRODUCT)
  1578. ->where('project')->eq($executionID)
  1579. ->fetchPairs();
  1580. $branches = $this->loadModel('branch')->getByProducts(array_keys($productBranchPairs));
  1581. foreach($productBranchPairs as $product => $branch)
  1582. {
  1583. if($branch == 0 && isset($branches[$product])) $productBranchPairs[$product] = implode(',', array_keys($branches[$product]));
  1584. }
  1585. return $productBranchPairs;
  1586. }
  1587. /**
  1588. * 获取执行树状图数据。
  1589. * Get executions tree data.
  1590. * @param int $executionID
  1591. * @access public
  1592. * @return array
  1593. */
  1594. public function getTree($executionID)
  1595. {
  1596. $firstTree = array(
  1597. 'id' => 0,
  1598. 'name' => '/',
  1599. 'type' => 'task',
  1600. 'actions' => false,
  1601. 'root' => $executionID,
  1602. );
  1603. $fullTrees = $this->loadModel('tree')->getTaskStructure($executionID, 0);
  1604. array_unshift($fullTrees, $firstTree);
  1605. foreach($fullTrees as $i => &$tree)
  1606. {
  1607. $tree = (object) $tree;
  1608. if($tree->type == 'product')
  1609. {
  1610. $firstTree['type'] = 'story';
  1611. $firstTree['root'] = $tree->root;
  1612. array_unshift($tree->children, $firstTree);
  1613. }
  1614. $tree = $this->fillTasksInTree($tree, $executionID);
  1615. if(empty($tree->children)) unset($fullTrees[$i]);
  1616. }
  1617. return array_values($fullTrees);
  1618. }
  1619. /**
  1620. * 获取完整路径的执行名称。
  1621. * Get full path name of execution.
  1622. * @param array $executions
  1623. * @access public
  1624. * @return array
  1625. */
  1626. public function getFullNameList($executions)
  1627. {
  1628. $allExecutions = $this->dao->select('id,name,parent')->from(TABLE_EXECUTION)
  1629. ->where('type')->notin(array('program', 'project'))
  1630. ->andWhere('deleted')->eq('0')
  1631. ->fetchAll('id');
  1632. $nameList = array();
  1633. foreach($executions as $executionID => $execution)
  1634. {
  1635. if($execution->grade <= 1)
  1636. {
  1637. $nameList[$executionID] = $execution->name;
  1638. continue;
  1639. }
  1640. /* Set execution name. */
  1641. $paths = array_slice(explode(',', trim($execution->path, ',')), 1);
  1642. $executionName = array();
  1643. foreach($paths as $path)
  1644. {
  1645. if(isset($allExecutions[$path])) $executionName[] = $allExecutions[$path]->name;
  1646. }
  1647. $nameList[$executionID] = implode('/', $executionName);
  1648. }
  1649. return $nameList;
  1650. }
  1651. /**
  1652. * 获取执行关联的产品下的执行列表。
  1653. * Get related executions.
  1654. *
  1655. * @param int $executionID
  1656. * @access public
  1657. * @return array
  1658. */
  1659. public function getRelatedExecutions($executionID)
  1660. {
  1661. $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$executionID)->fetchAll('product');
  1662. if(!$products) return array();
  1663. return $this->dao->select('t1.id, t1.name')->from(TABLE_EXECUTION)->alias('t1')
  1664. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')
  1665. ->on('t1.id = t2.project')
  1666. ->where('t2.product')->in(array_keys($products))
  1667. ->andWhere('t1.id')->ne((int)$executionID)
  1668. ->andWhere('t1.deleted')->eq(0)
  1669. ->orderBy('t1.id')
  1670. ->fetchPairs();
  1671. }
  1672. /**
  1673. * 获取子阶段列表。
  1674. * Get child executions.
  1675. *
  1676. * @param int $executionID
  1677. * @param string $orderBy
  1678. * @param string $type all|child
  1679. * @access public
  1680. * @return array
  1681. */
  1682. public function getChildExecutions($executionID, $orderBy = 'id_desc', $type = 'child')
  1683. {
  1684. $executionID = (int)$executionID;
  1685. if(empty($executionID)) return array();
  1686. $path = '';
  1687. if($type != 'child')
  1688. {
  1689. $path = $this->dao->select('path')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('path');
  1690. if(empty($path)) return array();
  1691. }
  1692. return $this->dao->select('*')->from(TABLE_EXECUTION)
  1693. ->where('deleted')->eq(0)
  1694. ->beginIF($type == 'child')->andWhere('parent')->eq($executionID)->fi()
  1695. ->beginIF($type != 'child')->andWhere('path')->like("{$path}%")->andWhere('id')->ne($executionID)->fi()
  1696. ->orderBy($orderBy)
  1697. ->fetchAll('id', false);
  1698. }
  1699. /**
  1700. * 获取受限执行id并保存到session中。
  1701. * Get limited execution id and save it to session.
  1702. *
  1703. * @access public
  1704. * @return bool|string
  1705. */
  1706. public function getLimitedExecution()
  1707. {
  1708. /* If is admin, return true. */
  1709. if($this->app->user->admin) return true;
  1710. /* Get all teams of all limited projects and group by projects. */
  1711. $subExecutions = array();
  1712. $projects = $this->dao->select('root, limited')->from(TABLE_TEAM)
  1713. ->where('type')->eq('project')
  1714. ->andWhere('account')->eq($this->app->user->account)
  1715. ->andWhere('limited')->eq('yes')
  1716. ->orderBy('root asc')
  1717. ->fetchPairs('root', 'root');
  1718. if($projects) $subExecutions = $this->dao->select('id, id')->from(TABLE_EXECUTION)->where('project')->in($projects)->fetchPairs();
  1719. /* Get no limited executions in limited projects. */
  1720. $notLimitedExecutions = $this->dao->select('root, limited')->from(TABLE_TEAM)
  1721. ->where('type')->eq('execution')
  1722. ->andWhere('account')->eq($this->app->user->account)
  1723. ->andWhere('limited')->eq('no')
  1724. ->andWhere('root')->in($subExecutions)
  1725. ->orderBy('root asc')
  1726. ->fetchPairs('root', 'root');
  1727. /* 获取当前用户再受限项目下的非受限执行以外的执行。 */
  1728. $subExecutions = array_diff($subExecutions, $notLimitedExecutions);
  1729. /* Get all teams of all executions and group by executions, save it as static. */
  1730. $executions = $this->dao->select('root, limited')->from(TABLE_TEAM)
  1731. ->where('type')->eq('execution')
  1732. ->andWhere('account')->eq($this->app->user->account)
  1733. ->andWhere('limited')->eq('yes')
  1734. ->orderBy('root asc')
  1735. ->fetchPairs('root', 'root');
  1736. $executions = array_merge($executions, $subExecutions);
  1737. $this->session->set('limitedExecutions', implode(',', $executions));
  1738. return $this->session->limitedExecutions;
  1739. }
  1740. /**
  1741. * 根据产品/执行等信息获取任务列表
  1742. * Get tasks by product/execution etc.
  1743. *
  1744. * @param int $productID
  1745. * @param int|array $executionID
  1746. * @param array $executions
  1747. * @param string $browseType
  1748. * @param int $queryID
  1749. * @param int $moduleID
  1750. * @param string $sort
  1751. * @param object $pager
  1752. * @access public
  1753. * @return array
  1754. */
  1755. public function getTasks($productID, $executionID, $executions, $browseType, $queryID, $moduleID, $sort, $pager = null)
  1756. {
  1757. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getTasks();
  1758. /* Set modules and $browseType. */
  1759. $modules = array();
  1760. if($moduleID) $modules = $this->loadModel('tree')->getAllChildID($moduleID);
  1761. if(in_array($browseType, array('bymodule', 'byproduct')) && $this->session->taskBrowseType && $this->session->taskBrowseType != 'bysearch') $browseType = $this->session->taskBrowseType;
  1762. /* Get tasks. */
  1763. if($browseType != "bysearch")
  1764. {
  1765. $queryStatus = $browseType == 'byexecution' ? 'all' : $browseType;
  1766. if($queryStatus == 'unclosed')
  1767. {
  1768. $queryStatus = $this->lang->task->statusList;
  1769. unset($queryStatus['closed']);
  1770. $queryStatus = array_keys($queryStatus);
  1771. }
  1772. return $this->loadModel('task')->getExecutionTasks($executionID, $productID, $queryStatus, $modules, $sort, $pager);
  1773. }
  1774. else
  1775. {
  1776. $query = $this->loadModel('search')->getQuery($queryID);
  1777. if($query)
  1778. {
  1779. $this->session->set('taskQuery', $query->sql);
  1780. $this->session->set('taskForm', $query->form);
  1781. }
  1782. elseif(!$this->session->taskQuery)
  1783. {
  1784. $this->session->set('taskQuery', ' 1 = 1');
  1785. }
  1786. if(strpos($this->session->taskQuery, "deleted =") === false) $this->session->set('taskQuery', $this->session->taskQuery . " AND deleted = '0'");
  1787. $taskQuery = $this->session->taskQuery;
  1788. /* Limit current execution when no execution. */
  1789. if(strpos($taskQuery, "`execution` =") === false && strpos($taskQuery, "`project` =") === false && $executionID) $taskQuery .= " AND `execution` = $executionID";
  1790. if(strpos($taskQuery, "`execution` = 'all'") !== false)
  1791. {
  1792. $executions = $this->getPairs(0, 'all', "nocode,noprefix,multiple");
  1793. $executionQuery = "`execution` " . helper::dbIN(array_keys($executions));
  1794. $taskQuery = str_replace("`execution` = 'all'", $executionQuery, $taskQuery); // Search all execution.
  1795. }
  1796. if(strpos($taskQuery, "`execution`") === false) $taskQuery .= " AND `execution` " . helper::dbIN(array_keys($executions));
  1797. /* Process all project query. */
  1798. if(strpos($taskQuery, "`project` = 'all'") !== false)
  1799. {
  1800. $projects = $this->loadModel('project')->getPairsByProgram();
  1801. $projectQuery = "`project` " . helper::dbIN(array_keys($projects));
  1802. $taskQuery = str_replace("`project` = 'all'", $projectQuery, $taskQuery);
  1803. }
  1804. $this->session->set('taskQueryCondition', $taskQuery, $this->app->tab);
  1805. $this->session->set('taskOnlyCondition', true, $this->app->tab);
  1806. return $this->getSearchTasks($taskQuery, $sort, $pager);
  1807. }
  1808. }
  1809. /**
  1810. * 根据执行ID列表获取父任务分组信息。
  1811. * Get the task data group by execution id list.
  1812. *
  1813. * @param array $executionIdList
  1814. * @param bool $filterStatus
  1815. * @access public
  1816. * @return array
  1817. */
  1818. public function getTaskGroupByExecution($executionIdList = array(), $filterStatus = true)
  1819. {
  1820. if(empty($executionIdList)) return array();
  1821. $fields = "t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.product, t2.branch, t2.version AS latestStoryVersion, t2.status AS storyStatus";
  1822. $executionTasks = $this->dao->select($fields)
  1823. ->from(TABLE_TASK)->alias('t1')
  1824. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  1825. ->where('t1.deleted')->eq(0)
  1826. ->beginIF($filterStatus)->andWhere('t1.status')->notin('closed,cancel')->fi()
  1827. ->andWhere('t1.execution')->in($executionIdList)
  1828. ->orderBy('t1.order_asc, t1.id_asc')
  1829. ->fetchGroup('execution', 'id');
  1830. $begin = $end = helper::today();
  1831. $taskIdList = array();
  1832. foreach($executionTasks as $tasks)
  1833. {
  1834. $taskIdList = array_merge($taskIdList, array_keys($tasks));
  1835. foreach($tasks as $task)
  1836. {
  1837. if(!empty($task->deadline) && !helper::isZeroDate($task->deadline) && $task->deadline < $begin) $begin = $task->deadline;
  1838. }
  1839. }
  1840. $taskIdList = array_unique($taskIdList);
  1841. $teamGroups = $this->dao->select('id,task,account,status')->from(TABLE_TASKTEAM)->where('task')->in($taskIdList)->fetchGroup('task', 'id');
  1842. $storyVersionPairs = $this->loadModel('task')->getTeamStoryVersion($taskIdList);
  1843. $workingDays = $this->loadModel('holiday')->getActualWorkingDays($begin, $end);
  1844. foreach($executionTasks as $tasks)
  1845. {
  1846. foreach($tasks as $task)
  1847. {
  1848. if(isset($teamGroups[$task->id])) $task->team = $teamGroups[$task->id];
  1849. /* Delayed or not?. */
  1850. $isNotCancel = !in_array($task->status, array('cancel', 'closed')) || ($task->status == 'closed' && !helper::isZeroDate($task->finishedDate) && $task->closedReason != 'cancel');
  1851. $isComputeDelay = !helper::isZeroDate($task->deadline) && $isNotCancel;
  1852. if($isComputeDelay) $task = $this->task->computeDelay($task, $task->deadline, $workingDays);
  1853. /* Story changed or not. */
  1854. $task->storyVersion = zget($storyVersionPairs, $task->id, $task->storyVersion);
  1855. $task->needConfirm = false;
  1856. if(!empty($task->storyStatus) && $task->storyStatus == 'active' && !in_array($task->status, array('cancel', 'closed')) && $task->latestStoryVersion > $task->storyVersion)
  1857. {
  1858. $task->needConfirm = true;
  1859. $task->status = 'changed';
  1860. }
  1861. }
  1862. }
  1863. return $executionTasks;
  1864. }
  1865. /**
  1866. * 获取执行的信息。
  1867. * Get the execution information by ID.
  1868. *
  1869. * @param int $executionID
  1870. * @param bool $setImgSize
  1871. * @access public
  1872. * @return object|false
  1873. */
  1874. public function getByID($executionID, $setImgSize = false)
  1875. {
  1876. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecution();
  1877. $execution = $this->fetchByID($executionID);
  1878. if(!$execution) return false;
  1879. /* Judge whether the execution is delayed. */
  1880. if($execution->status != 'done' and $execution->status != 'closed' and $execution->status != 'suspended')
  1881. {
  1882. $delayDays = $this->loadModel('holiday')->getActualWorkingDays($execution->end, helper::today());
  1883. if(!empty($delayDays))
  1884. {
  1885. $delay = count($delayDays) - 1;
  1886. if($delay > 0) $execution->delay = $delay;
  1887. }
  1888. }
  1889. $totalHours = $this->dao->select('sum(t1.days * t1.hours) AS totalHours')->from(TABLE_TEAM)->alias('t1')
  1890. ->leftJoin(TABLE_USER)->alias('t2')
  1891. ->on('t1.account=t2.account')
  1892. ->where('t1.root')->eq($execution->id)
  1893. ->andWhere('t1.type')->eq('execution')
  1894. ->andWhere('t2.deleted')->eq(0)
  1895. ->fetch('totalHours');
  1896. /* Set the hours information for the task. */
  1897. $execution->totalHours = $totalHours;
  1898. $execution->days = $execution->days ? $execution->days : 0;
  1899. $execution->totalEstimate = round((float)$execution->estimate, 1);
  1900. $execution->totalConsumed = round((float)$execution->consumed, 1);
  1901. $execution->totalLeft = round((float)$execution->left, 1);
  1902. $execution = $this->loadModel('file')->replaceImgURL($execution, 'desc');
  1903. if($setImgSize) $execution->desc = $this->file->setImgSize($execution->desc);
  1904. $child = $this->dao->select('id')->from(TABLE_EXECUTION)->where('parent')->eq($executionID)->andWhere('deleted')->eq(0)->fetch('id');
  1905. $execution->isParent = !empty($child) ? 1 : 0;
  1906. return $execution;
  1907. }
  1908. /**
  1909. * Get execution by build id.
  1910. *
  1911. * @param int $buildID
  1912. * @access public
  1913. * @return object
  1914. */
  1915. public function getByBuild($buildID)
  1916. {
  1917. $build = $this->loadModel('build')->getById($buildID);
  1918. if(!$build) return 0;
  1919. return $this->getById((int)$build->execution);
  1920. }
  1921. /**
  1922. * 获取执行的产品相关负责人。
  1923. * Get the default managers for a execution from it's related products.
  1924. *
  1925. * @param int $executionID
  1926. * @access public
  1927. * @return object
  1928. */
  1929. public function getDefaultManagers($executionID)
  1930. {
  1931. $managers = $this->dao->select('PO,QD,RD')->from(TABLE_PRODUCT)->alias('t1')
  1932. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.product')
  1933. ->where('t2.project')->eq($executionID)
  1934. ->fetch();
  1935. if($managers) return $managers;
  1936. $managers = new stdclass();
  1937. $managers->PO = '';
  1938. $managers->QD = '';
  1939. $managers->RD = '';
  1940. return $managers;
  1941. }
  1942. /**
  1943. * 根据产品ID列表查询分支信息。
  1944. * Get branch information by the product ID list.
  1945. *
  1946. * @param array $productIdList
  1947. * @param int $projectID
  1948. * @param string $param
  1949. * @param array $appendBranch
  1950. * @access public
  1951. * @return array
  1952. */
  1953. public function getBranchByProduct($productIdList, $projectID = 0, $param = 'noclosed', $appendBranch = array())
  1954. {
  1955. $branchGroup = $this->loadModel('branch')->getByProducts($productIdList, $param, $appendBranch);
  1956. if($projectID)
  1957. {
  1958. $projectProducts = $this->loadModel('project')->getBranchesByProject($projectID);
  1959. foreach($branchGroup as $productID => $branchPairs)
  1960. {
  1961. foreach($branchPairs as $branchID => $branchName)
  1962. {
  1963. if(strpos($param, 'withMain') !== false and $branchID == BRANCH_MAIN) continue;
  1964. if(!isset($projectProducts[$productID][$branchID])) unset($branchGroup[$productID][$branchID]);
  1965. }
  1966. }
  1967. }
  1968. return $branchGroup;
  1969. }
  1970. /**
  1971. * 获取排序后的执行列表信息。
  1972. * Get ordered executions.
  1973. *
  1974. * @param int $executionID
  1975. * @param string $status
  1976. * @param int $num
  1977. * @param string $param
  1978. * @access public
  1979. * @return array
  1980. */
  1981. public function getOrderedExecutions($executionID, $status, $num = 0, $param = '')
  1982. {
  1983. $executionList = $this->getList($executionID, 'all', $status);
  1984. if(empty($executionList)) return $executionList;
  1985. $executions = array();
  1986. $param = strtolower($param);
  1987. if($param == 'skipparent')
  1988. {
  1989. $parentExecutions = array();
  1990. foreach($executionList as $execution) $parentExecutions[$execution->parent] = $execution->parent;
  1991. }
  1992. foreach($executionList as $execution)
  1993. {
  1994. if(empty($execution->multiple)) continue;
  1995. if(!$this->app->user->admin && !$this->checkPriv($execution->id)) continue;
  1996. if($param == 'skipparent' && isset($parentExecutions[$execution->id])) continue;
  1997. if($execution->status != 'done' && $execution->status != 'closed' && $execution->PM == $this->app->user->account)
  1998. {
  1999. $executions[$execution->id] = $execution;
  2000. }
  2001. elseif($execution->status != 'done' && $execution->status != 'closed' && $execution->PM != $this->app->user->account)
  2002. {
  2003. $executions[$execution->id] = $execution;
  2004. }
  2005. elseif($execution->status == 'done' || $execution->status == 'closed')
  2006. {
  2007. $executions[$execution->id] = $execution;
  2008. }
  2009. }
  2010. if(empty($num)) return $executions;
  2011. return array_slice($executions, 0, $num, true);
  2012. }
  2013. /**
  2014. * 构造需求的搜索表单。
  2015. * Build story search form.
  2016. *
  2017. * @param array $products
  2018. * @param array $branchGroups
  2019. * @param array $modules
  2020. * @param int $queryID
  2021. * @param string $actionURL
  2022. * @param string $type executionStory
  2023. * @param object $execution
  2024. * @access public
  2025. * @return void
  2026. */
  2027. public function buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, $type = 'executionStory', $execution = null)
  2028. {
  2029. $this->loadModel('productplan');
  2030. $this->app->loadLang('branch');
  2031. $branchPairs = array(BRANCH_MAIN => $this->lang->branch->main);
  2032. $productType = 'normal';
  2033. $productPairs = array(0 => '');
  2034. $branches = empty($execution) ? array() : $this->loadModel('project')->getBranchesByProject($execution->id);
  2035. $planGroup = array();
  2036. $planPairs = array();
  2037. /* Get the relevant data for the search. */
  2038. foreach($products as $productID => $product)
  2039. {
  2040. $productPairs[$product->id] = $product->name;
  2041. $planGroup = $this->productplan->getBranchPlanPairs($productID, array(BRANCH_MAIN) + $product->branches, '', true);
  2042. foreach($planGroup as $plans) $planPairs += $plans;
  2043. if($product->type == 'normal') continue;
  2044. $productType = $product->type;
  2045. if(!isset($branches[$product->id])) continue;
  2046. foreach($branches[$product->id] as $branchID => $branch)
  2047. {
  2048. if(!isset($branchGroups[$product->id][$branchID])) continue;
  2049. if($branchID != BRANCH_MAIN) $branchPairs[$branchID] = ((count($products) > 1) ? $product->name . '/' : '') . $branchGroups[$product->id][$branchID];
  2050. }
  2051. }
  2052. /* Build search form. */
  2053. if($type == 'executionStory') $this->config->product->search['module'] = 'executionStory';
  2054. $this->config->product->search['actionURL'] = $actionURL;
  2055. $this->config->product->search['queryID'] = $queryID;
  2056. $this->config->product->search['fields']['title'] = $this->lang->story->name;
  2057. $this->config->product->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject);
  2058. $this->config->product->search['params']['plan']['values'] = $planPairs;
  2059. $this->config->product->search['params']['module']['values'] = $modules;
  2060. $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList);
  2061. $this->config->product->search['params']['stage']['values'] = array('' => '') + $this->lang->story->stageList;
  2062. if($productType == 'normal')
  2063. {
  2064. unset($this->config->product->search['fields']['branch']);
  2065. unset($this->config->product->search['params']['branch']);
  2066. }
  2067. else
  2068. {
  2069. $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productType]);
  2070. $this->config->product->search['params']['branch']['values'] = $branchPairs;
  2071. }
  2072. $project = $execution;
  2073. if(in_array($execution->type, array('sprint', 'stage', 'kanban'))) $project = $this->loadModel('project')->getByID($execution->project);
  2074. if(empty($project->hasProduct))
  2075. {
  2076. unset($this->config->product->search['fields']['product']);
  2077. unset($this->config->product->search['params']['product']);
  2078. if($project->model != 'kanban')
  2079. {
  2080. unset($this->config->product->search['fields']['plan']);
  2081. unset($this->config->product->search['params']['plan']);
  2082. }
  2083. }
  2084. $gradePairs = array();
  2085. $gradeList = $this->loadModel('story')->getGradeList('');
  2086. $storyTypes = isset($project->storyType) ? $project->storyType : 'story';
  2087. if(!($execution->type == 'stage' && in_array($execution->attribute, array('mix', 'request', 'design')))) $storyTypes = 'story';
  2088. foreach($gradeList as $grade)
  2089. {
  2090. if(strpos($storyTypes, $grade->type) === false) continue;
  2091. $key = (string)$grade->type . (string)$grade->grade;
  2092. $gradePairs[$key] = $grade->name;
  2093. }
  2094. asort($gradePairs);
  2095. $this->config->product->search['params']['grade']['values'] = $gradePairs;
  2096. $this->config->product->search['onMenuBar'] = 'yes';
  2097. $this->loadModel('search')->setSearchParams($this->config->product->search);
  2098. }
  2099. /**
  2100. * 获取要导入的执行列表。
  2101. * Get executions to import.
  2102. *
  2103. * @param array $executionIds
  2104. * @param string $type sprint|stage|kanban
  2105. * @param string $model
  2106. * @access public
  2107. * @return array
  2108. */
  2109. public function getToImport($executionIds, $type, $model = '')
  2110. {
  2111. return $this->dao->select("t1.id, concat_ws(' / ', t2.name, t1.name) as name")->from(TABLE_EXECUTION)->alias('t1')
  2112. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t2.id=t1.project')
  2113. ->where('t1.id')->in($executionIds)
  2114. ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
  2115. ->beginIF(empty($model) || strpos(',waterfallplus,agileplus,', ",$model,") === false)->andWhere('t1.type')->eq($type)->fi()
  2116. ->beginIF(!empty($model) && $model == 'agileplus')->andWhere('t1.type')->in(array('sprint', 'kanban'))->fi()
  2117. ->andWhere('t1.deleted')->eq(0)
  2118. ->orderBy('t1.id desc')
  2119. ->fetchPairs('id', 'name');
  2120. }
  2121. /**
  2122. * 更新执行关联的产品信息。
  2123. * Update products of a execution.
  2124. *
  2125. * @param int $executionID
  2126. * @param object|array $postData
  2127. * @access public
  2128. * @return bool
  2129. */
  2130. public function updateProducts($executionID, $postData)
  2131. {
  2132. $this->loadModel('user');
  2133. $otherProducts = zget($postData, 'otherProducts', array());
  2134. $products = !empty($otherProducts) ? array_filter(array_merge(zget($postData, 'products', array()), $otherProducts)) : zget($postData, 'products', array());
  2135. $branches = zget($postData, 'branch', array(0));
  2136. $plans = zget($postData, 'plans', array());
  2137. $oldProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->fetchGroup('product', 'branch');
  2138. if(empty($_POST['otherProducts'])) $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->exec();
  2139. $members = array_keys($this->getTeamMembers($executionID));
  2140. if(empty($products))
  2141. {
  2142. $this->user->updateUserView(array_keys($oldProducts), 'product', $members);
  2143. return true;
  2144. }
  2145. $existedProducts = array();
  2146. foreach($products as $i => $productID)
  2147. {
  2148. if(!isset($existedProducts[$productID])) $existedProducts[$productID] = array();
  2149. $oldPlan = '';
  2150. $branch = isset($branches[$i]) ? (array)$branches[$i] : array(0);
  2151. foreach($branch as $branchID)
  2152. {
  2153. if(isset($existedProducts[$productID][$branchID])) continue;
  2154. if(isset($oldProducts[$productID][$branchID]))
  2155. {
  2156. $oldProduct = $oldProducts[$productID][$branchID];
  2157. if($this->app->rawModule == 'project' || $this->app->rawMethod != 'edit') $oldPlan = $oldProduct->plan;
  2158. }
  2159. $data = new stdclass();
  2160. $data->project = $executionID;
  2161. $data->product = (int)$productID;
  2162. $data->branch = (int)$branchID;
  2163. $data->plan = isset($plans[$productID]) ? implode(',', $plans[$productID]) : $oldPlan;
  2164. $data->plan = trim($data->plan, ',');
  2165. $data->plan = empty($data->plan) ? 0 : ",$data->plan,";
  2166. $this->dao->replace(TABLE_PROJECTPRODUCT)->data($data)->exec();
  2167. $existedProducts[$productID][$branchID] = true;
  2168. }
  2169. }
  2170. $oldProductKeys = array_keys($oldProducts);
  2171. $needUpdate = array_merge(array_diff($oldProductKeys, $products), array_diff($products, $oldProductKeys));
  2172. if($needUpdate) $this->user->updateUserView($needUpdate, 'product', $members);
  2173. return true;
  2174. }
  2175. /**
  2176. * 获取可被导入的任务列表。
  2177. * Get tasks can be imported.
  2178. *
  2179. * @param int $toExecution
  2180. * @param array $branches
  2181. * @param string $orderBy
  2182. * @access public
  2183. * @return array
  2184. */
  2185. public function getTasks2Imported($toExecution, $branches, $orderBy = 'id_desc')
  2186. {
  2187. $execution = $this->fetchById($toExecution);
  2188. $project = $this->loadModel('project')->fetchById($execution->project);
  2189. $brotherProjects = $this->project->getBrotherProjects($project);
  2190. $executions = $this->dao->select('id')->from(TABLE_EXECUTION)
  2191. ->where('project')->in($brotherProjects)
  2192. ->andWhere('multiple')->eq('1')
  2193. ->andWhere('status')->ne('closed')
  2194. ->andWhere('deleted')->eq(0)
  2195. ->fetchPairs('id');
  2196. $branches = str_replace(',', "','", $branches);
  2197. return $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName')->from(TABLE_TASK)->alias('t1')
  2198. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  2199. ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account')
  2200. ->where('t1.status')->in('wait,doing,pause,cancel')
  2201. ->andWhere('t1.deleted')->eq(0)
  2202. ->andWhere('t1.execution')->in(array_keys($executions))
  2203. ->andWhere("(t1.story = 0 OR (t2.branch IN ('0','" . implode("','", $branches) . "') AND t2.product " . helper::dbIN(array_keys($branches)) . "))")
  2204. ->orderBy($orderBy)
  2205. ->fetchGroup('execution', 'id');
  2206. }
  2207. /**
  2208. * 转入任务到指定的执行。
  2209. * Import tasks.
  2210. *
  2211. * @param int $executionID
  2212. * @param array $taskIdList
  2213. * @access public
  2214. * @return array
  2215. */
  2216. public function importTask($executionID, $taskIdList)
  2217. {
  2218. $this->loadModel('action');
  2219. $dateExceed = array();
  2220. $taskStories = array();
  2221. $parents = array();
  2222. $execution = $this->fetchByID($executionID);
  2223. $tasks = $this->loadModel('task')->getByIdList($taskIdList);
  2224. $assignedToes = array();
  2225. foreach($tasks as $task)
  2226. {
  2227. /* Save the assignedToes and stories, should linked to execution. */
  2228. $assignedToes[$task->assignedTo] = $task->execution;
  2229. $taskStories[$task->story] = $task->story;
  2230. if($task->isParent) $parents[$task->id] = $task->id;
  2231. $data = new stdclass();
  2232. $data->project = $execution->project;
  2233. $data->execution = $executionID;
  2234. $data->canceledBy = '';
  2235. $data->canceledDate = null;
  2236. if(!empty($this->config->limitTaskDate))
  2237. {
  2238. if($task->estStarted < $execution->begin || $task->estStarted > $execution->end || $task->deadline > $execution->end || $task->deadline < $execution->begin) $dateExceed[] = "#{$task->id}";
  2239. if($task->estStarted < $execution->begin || $task->estStarted > $execution->end) $data->estStarted = $execution->begin;
  2240. if($task->deadline > $execution->end || $task->deadline < $execution->begin) $data->deadline = $execution->end;
  2241. }
  2242. /* Update tasks and save logs. */
  2243. if($task->isParent) $this->dao->update(TABLE_TASK)->data($data)->where('parent')->eq($task->id)->exec();
  2244. $data->status = $task->consumed > 0 ? 'doing' : 'wait';
  2245. if($data->status == 'wait') $data->realStarted = null;
  2246. $this->dao->update(TABLE_TASK)->data($data)->where('id')->eq($task->id)->exec();
  2247. $this->action->create('task', $task->id, 'moved', '', $task->execution);
  2248. }
  2249. /* Other data process after task batch import. */
  2250. $this->afterImportTask($execution, $parents, $assignedToes, $taskStories);
  2251. return $dateExceed;
  2252. }
  2253. /**
  2254. * 批量导入任务后的其他数据处理。
  2255. * Other data process after task batch import.
  2256. *
  2257. * @param object $execution
  2258. * @param array $parents
  2259. * @param array $assignedToes
  2260. * @param array $taskStories
  2261. * @access public
  2262. * @return void
  2263. */
  2264. public function afterImportTask($execution, $parents, $assignedToes, $taskStories)
  2265. {
  2266. /* Get stories of children task. */
  2267. if(!empty($parents))
  2268. {
  2269. $children = $this->dao->select('*')->from(TABLE_TASK)->where('parent')->in($parents)->fetchAll('id');
  2270. foreach($children as $child) $taskStories[$child->story] = $child->story;
  2271. }
  2272. /* Add members to execution team. */
  2273. $teamMembers = $this->loadModel('user')->getTeamMemberPairs($execution->id, 'execution');
  2274. $today = helper::today();
  2275. foreach($assignedToes as $account => $preExecutionID)
  2276. {
  2277. if(!empty($account) && !isset($teamMembers[$account]))
  2278. {
  2279. $role = $this->dao->select('*')->from(TABLE_TEAM)
  2280. ->where('root')->eq($preExecutionID)
  2281. ->andWhere('type')->eq('execution')
  2282. ->andWhere('account')->eq($account)
  2283. ->fetch();
  2284. if(empty($role)) continue;
  2285. $role->root = $execution->id;
  2286. $role->join = $today;
  2287. $this->dao->replace(TABLE_TEAM)->data($role)->exec();
  2288. }
  2289. }
  2290. /* Link stories. */
  2291. $executionStories = $this->loadModel('story')->getExecutionStoryPairs($execution->id);
  2292. $lastOrder = (int)$this->dao->select('`order`')->from(TABLE_PROJECTSTORY)->where('project')->eq($execution->id)->orderBy('order_desc')->limit(1)->fetch('order');
  2293. $stories = $this->dao->select("id as story, product, version")->from(TABLE_STORY)->where('id')->in(array_keys($taskStories))->fetchAll('story');
  2294. $this->loadModel('action');
  2295. foreach($taskStories as $storyID)
  2296. {
  2297. if(!isset($executionStories[$storyID]) && isset($stories[$storyID]))
  2298. {
  2299. $lastOrder ++;
  2300. $story = $stories[$storyID];
  2301. $story->project = $execution->id;
  2302. $story->order = $lastOrder;
  2303. $this->dao->insert(TABLE_PROJECTSTORY)->data($story)->exec();
  2304. if($execution->multiple || $execution->type == 'project') $this->action->create('story', $storyID, 'linked2execution', '', $execution->id);
  2305. }
  2306. }
  2307. }
  2308. /**
  2309. * 统计执行的需求数、任务数、Bug数。
  2310. * Statistics the number of stories, tasks, and bugs for the execution.
  2311. *
  2312. * @param int $executionID
  2313. * @access public
  2314. * @return object
  2315. */
  2316. public function statRelatedData($executionID)
  2317. {
  2318. $storyCount = $this->dao->select('count(t2.story) as storyCount')->from(TABLE_STORY)->alias('t1')
  2319. ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id = t2.story')
  2320. ->where('t2.project')->eq($executionID)
  2321. ->andWhere('t1.deleted')->eq(0)
  2322. ->fetch('storyCount');
  2323. $taskCount = $this->dao->select('count(id) as taskCount')->from(TABLE_TASK)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->fetch('taskCount');
  2324. $bugCount = $this->dao->select('count(id) as bugCount')->from(TABLE_BUG)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->fetch('bugCount');
  2325. $statData = new stdclass();
  2326. $statData->storyCount = $storyCount;
  2327. $statData->taskCount = $taskCount;
  2328. $statData->bugCount = $bugCount;
  2329. return $statData;
  2330. }
  2331. /**
  2332. * 导入Bug。
  2333. * Import task from Bug.
  2334. *
  2335. * @param array $tasks
  2336. * @access public
  2337. * @return array|bool
  2338. */
  2339. public function importBug($tasks)
  2340. {
  2341. $this->loadModel('story');
  2342. $this->loadModel('action');
  2343. foreach($tasks as $key => $task)
  2344. {
  2345. $bug = $task->bug;
  2346. unset($task->bug);
  2347. if(isset($task->estimate)) $task->estimate = round((float)$task->estimate, 2);
  2348. if(isset($task->left)) $task->left = round((float)$task->left, 2);
  2349. if(!$bug->confirmed) $this->dao->update(TABLE_BUG)->set('confirmed')->eq(1)->where('id')->eq($bug->id)->exec();
  2350. $task->version = 1;
  2351. $this->dao->insert(TABLE_TASK)->data($task)->exec();
  2352. if(dao::isError()) return false;
  2353. $taskID = $this->dao->lastInsertID();
  2354. $this->dao->update(TABLE_TASK)->set('path')->eq(",$taskID,")->where('id')->eq($taskID)->exec();
  2355. /* Update story's stage and create a action. */
  2356. if($task->story !== false) $this->story->setStage($task->story);
  2357. $actionID = $this->action->create('task', $taskID, 'Opened', '');
  2358. $this->dao->update(TABLE_BUG)->set('toTask')->eq($taskID)->where('id')->eq($key)->exec();
  2359. $this->action->create('bug', $key, 'Totask', '', $taskID);
  2360. $mails[$key] = new stdClass();
  2361. $mails[$key]->taskID = $taskID;
  2362. $mails[$key]->actionID = $actionID;
  2363. $this->afterImportBug($task, $bug);
  2364. if(dao::isError()) return false;
  2365. if($this->config->edition != 'open')
  2366. {
  2367. $relation = new stdClass();
  2368. $relation->AType = 'bug';
  2369. $relation->AID = $bug->id;
  2370. $relation->relation = 'transferredto';
  2371. $relation->BType = 'task';
  2372. $relation->BID = $taskID;
  2373. $relation->product = 0;
  2374. $this->dao->replace(TABLE_RELATION)->data($relation)->exec();
  2375. }
  2376. }
  2377. return $mails;
  2378. }
  2379. /**
  2380. * 批量导入Bug后的其他数据处理。
  2381. * other data process after import bugs.
  2382. *
  2383. * @param object $task
  2384. * @param object $bug
  2385. * @access public
  2386. * @return bool
  2387. */
  2388. public function afterImportBug($task, $bug)
  2389. {
  2390. $this->loadModel('action');
  2391. /* activate bug if bug postponed. */
  2392. $now = helper::now();
  2393. if($bug->status == 'resolved' && $bug->resolution == 'postponed')
  2394. {
  2395. $newBug = new stdclass();
  2396. $newBug->lastEditedBy = $this->app->user->account;
  2397. $newBug->lastEditedDate = $now;
  2398. $newBug->assignedDate = $now;
  2399. $newBug->status = 'active';
  2400. $newBug->resolvedDate = null;
  2401. $newBug->resolution = '';
  2402. $newBug->resolvedBy = '';
  2403. $newBug->resolvedBuild = '';
  2404. $newBug->closedBy = '';
  2405. $newBug->closedDate = null;
  2406. $newBug->duplicateBug = '0';
  2407. $this->dao->update(TABLE_BUG)->data($newBug)->autoCheck()->where('id')->eq($bug->id)->exec();
  2408. $this->dao->update(TABLE_BUG)->set('activatedCount = activatedCount + 1')->where('id')->eq($bug->id)->exec();
  2409. $actionID = $this->action->create('bug', $bug->id, 'Activated');
  2410. $changes = common::createChanges($bug, $newBug);
  2411. $this->action->logHistory($actionID, $changes);
  2412. }
  2413. if(isset($task->assignedTo) && $task->assignedTo && $task->assignedTo != $bug->assignedTo)
  2414. {
  2415. $newBug = new stdClass();
  2416. $newBug->lastEditedBy = $this->app->user->account;
  2417. $newBug->lastEditedDate = $now;
  2418. $newBug->assignedTo = $task->assignedTo;
  2419. $newBug->assignedDate = $now;
  2420. $this->dao->update(TABLE_BUG)->data($newBug)->where('id')->eq($bug->id)->exec();
  2421. if(dao::isError()) return false;
  2422. $actionID = $this->action->create('bug', $bug->id, 'Assigned', '', $newBug->assignedTo);
  2423. $changes = common::createChanges($bug, $newBug);
  2424. $this->action->logHistory($actionID, $changes);
  2425. }
  2426. return !dao::isError();
  2427. }
  2428. /**
  2429. * 修改执行的所属项目。
  2430. * Change execution project.
  2431. *
  2432. * @param int $newProjectID
  2433. * @param int $oldProjectID
  2434. * @param int $executionID
  2435. * @param string $syncStories yes|no
  2436. * @access public
  2437. * @return void
  2438. */
  2439. public function changeProject($newProjectID, $oldProjectID, $executionID, $syncStories = 'no')
  2440. {
  2441. if($newProjectID == $oldProjectID) return;
  2442. $this->dao->update(TABLE_EXECUTION)->set('parent')->eq($newProjectID)->set('path')->eq(",$newProjectID,$executionID,")->where('id')->eq($executionID)->exec();
  2443. /* Update the project to which the relevant data belongs. */
  2444. $this->dao->update(TABLE_BUILD)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2445. $this->dao->update(TABLE_BUG)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2446. $this->dao->update(TABLE_CASE)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2447. $this->dao->update(TABLE_DOC)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2448. $this->dao->update(TABLE_DOCLIB)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2449. $this->dao->update(TABLE_TASK)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2450. $this->dao->update(TABLE_TESTREPORT)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2451. $this->dao->update(TABLE_TESTTASK)->set('project')->eq($newProjectID)->where('project')->eq($oldProjectID)->andWhere('execution')->eq($executionID)->exec();
  2452. /* Update the hours and efforts. */
  2453. $this->updateProjectHours($newProjectID, $oldProjectID, $executionID);
  2454. /* Update the team members and whitelist of the project. */
  2455. $addedAccounts = $this->updateProjectUsers($executionID, $newProjectID);
  2456. if($addedAccounts) $this->loadModel('user')->updateUserView(array($newProjectID), 'project', $addedAccounts);
  2457. /* Sync stories to new project. */
  2458. if($syncStories == 'yes')
  2459. {
  2460. $this->loadModel('action');
  2461. $projectLinkedStories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($newProjectID)->fetchPairs('story', 'story');
  2462. $executionLinkedStories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->fetchAll();
  2463. foreach($executionLinkedStories as $linkedStory)
  2464. {
  2465. if(isset($projectLinkedStories[$linkedStory->story])) continue;
  2466. $linkedStory->project = $newProjectID;
  2467. $this->dao->insert(TABLE_PROJECTSTORY)->data($linkedStory, 'id')->exec();
  2468. $this->action->create('story', $linkedStory->story, 'linked2project', '', $newProjectID);
  2469. }
  2470. }
  2471. }
  2472. /**
  2473. * 更新项目的团队成员和白名单。
  2474. * Update the team members and whitelist of the project.
  2475. *
  2476. * @param int $executionID
  2477. * @param int $newProjectID
  2478. * @access public
  2479. * @return array
  2480. */
  2481. public function updateProjectUsers($executionID, $newProjectID)
  2482. {
  2483. $executionTeam = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution');
  2484. $projectTeam = $this->user->getTeamMemberPairs($newProjectID, 'project');
  2485. $addedAccounts = array();
  2486. $today = helper::today();
  2487. foreach($executionTeam as $account => $realname)
  2488. {
  2489. if(isset($projectTeam[$account])) continue;
  2490. $member = new stdclass();
  2491. $member->root = $newProjectID;
  2492. $member->type = 'project';
  2493. $member->account = $account;
  2494. $member->join = $today;
  2495. $member->days = 0;
  2496. $member->hours = $this->config->execution->defaultWorkhours;
  2497. $this->dao->replace(TABLE_TEAM)->data($member)->exec();
  2498. $addedAccounts[$account] = $account;
  2499. }
  2500. $executionWhitelist = $this->loadModel('personnel')->getWhitelistAccount($executionID, 'sprint');
  2501. $projectWhitelist = $this->personnel->getWhitelistAccount($newProjectID, 'project');
  2502. foreach($executionWhitelist as $account)
  2503. {
  2504. if(isset($projectWhitelist[$account])) continue;
  2505. $whitelist = new stdclass();
  2506. $whitelist->account = $account;
  2507. $whitelist->objectType = 'project';
  2508. $whitelist->objectID = $newProjectID;
  2509. $whitelist->type = 'whitelist';
  2510. $whitelist->source = 'sync';
  2511. $this->dao->replace(TABLE_ACL)->data($whitelist)->exec();
  2512. $addedAccounts[$account] = $account;
  2513. }
  2514. return $addedAccounts;
  2515. }
  2516. /**
  2517. * 关联需求到项目或执行。
  2518. * Link story for project or execution.
  2519. *
  2520. * @param int $executionID projectID|executionID
  2521. * @param array $stories
  2522. * @param string $extra
  2523. * @param array $lanes
  2524. * @param string $storyType
  2525. * @access public
  2526. * @return bool
  2527. */
  2528. public function linkStory($executionID, $stories = array(), $extra = '', $lanes = array(), $storyType = 'story')
  2529. {
  2530. if(empty($executionID) || empty($stories)) return false;
  2531. $extra = str_replace(array(',', ' '), array('&', ''), $extra);
  2532. parse_str($extra, $output);
  2533. $this->loadModel('action');
  2534. $this->loadModel('kanban');
  2535. $this->loadModel('story');
  2536. $versions = $this->story->getVersions($stories);
  2537. $linkedStories = $this->dao->select('story,`order`')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->orderBy('order_desc')->fetchPairs('story', 'order');
  2538. $lastOrder = (int)reset($linkedStories);
  2539. $storyList = $this->story->getByList(array_values($stories));
  2540. $execution = $this->getByID($executionID);
  2541. $notAllowedStatus = $this->app->rawMethod == 'batchcreate' ? 'closed' : 'draft,reviewing,closed';
  2542. $laneID = isset($output['laneID']) ? $output['laneID'] : 0;
  2543. $project = $execution->type == 'project' ? $execution : $this->loadModel('project')->getByID($execution->project);
  2544. foreach($stories as $storyID)
  2545. {
  2546. if(isset($linkedStories[$storyID])) continue;
  2547. if(!isset($storyList[$storyID])) continue;
  2548. if(strpos($notAllowedStatus, (string)$storyList[$storyID]->status) !== false) continue;
  2549. $storyID = (int)$storyID;
  2550. $story = zget($storyList, $storyID, '');
  2551. if(empty($story)) continue;
  2552. if(strpos($project->storyType, "$story->type") === false && $this->config->vision == 'rnd') continue;
  2553. if($execution->multiple && $story->type != 'story' && (!($execution->type == 'stage' && in_array($execution->attribute, array('mix', 'request', 'design'))) && $execution->type != 'project') && $this->config->vision == 'rnd') continue;
  2554. if(!empty($lanes[$storyID])) $laneID = $lanes[$storyID];
  2555. $columnID = $this->kanban->getColumnIDByLaneID((int)$laneID, 'backlog');
  2556. if(empty($columnID)) $columnID = isset($output['columnID']) ? $output['columnID'] : 0;
  2557. if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, (int)$laneID, (int)$columnID, $storyType, (string)$storyID);
  2558. $data = new stdclass();
  2559. $data->project = $executionID;
  2560. $data->product = (int)$story->product;
  2561. $data->branch = $story->branch;
  2562. $data->story = $storyID;
  2563. $data->version = $versions[$storyID];
  2564. $data->order = ++ $lastOrder;
  2565. $this->dao->replace(TABLE_PROJECTSTORY)->data($data)->exec();
  2566. $this->story->setStage($storyID);
  2567. $this->linkCases($executionID, $data->product, $storyID);
  2568. $action = $execution->type == 'project' ? 'linked2project' : 'linked2execution';
  2569. if($action == 'linked2execution' and $execution->type == 'kanban') $action = 'linked2kanban';
  2570. if($execution->multiple or $execution->type == 'project') $this->action->create('story', $storyID, $action, '', $executionID);
  2571. }
  2572. if(!isset($output['laneID']) or !isset($output['columnID'])) $this->kanban->updateLane($executionID);
  2573. return true;
  2574. }
  2575. /**
  2576. * 执行批量关联用例。
  2577. * Batch link cases.
  2578. *
  2579. * @param int $executionID
  2580. * @param int $productID
  2581. * @param int $storyID
  2582. * @access public
  2583. * @return void
  2584. */
  2585. public function linkCases($executionID, $productID, $storyID)
  2586. {
  2587. $this->loadModel('action');
  2588. $linkedCases = $this->dao->select('*')->from(TABLE_PROJECTCASE)->where('project')->eq($executionID)->orderBy('order_desc')->fetchPairs('case', 'order');
  2589. $lastCaseOrder = empty($linkedCases) ? 0 : (int)reset($linkedCases);
  2590. $cases = $this->dao->select('id, version')->from(TABLE_CASE)->where('story')->eq($storyID)->fetchPairs();
  2591. $execution = $this->getByID($executionID);
  2592. foreach($cases as $caseID => $version)
  2593. {
  2594. if(isset($linkedCases[$caseID])) continue;
  2595. $object = new stdclass();
  2596. $object->project = $executionID;
  2597. $object->product = $productID;
  2598. $object->case = $caseID;
  2599. $object->version = $version;
  2600. $object->order = ++ $lastCaseOrder;
  2601. $this->dao->insert(TABLE_PROJECTCASE)->data($object)->exec();
  2602. $action = $execution->type == 'project' ? 'linked2project' : 'linked2execution';
  2603. if($execution->multiple || $execution->type == 'project') $this->action->create('case', $caseID, $action, '', $executionID);
  2604. }
  2605. }
  2606. /**
  2607. * 批量关联需求。
  2608. * Link all stories by execution.
  2609. *
  2610. * @param int $executionID
  2611. * @access public
  2612. * @return bool
  2613. */
  2614. public function linkStories($executionID)
  2615. {
  2616. $stories = array();
  2617. $plans = $this->dao->select('product, plan')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->fetchPairs('product', 'plan');
  2618. $execution = $this->fetchByID($executionID);
  2619. $project = $this->fetchByID($execution->project);
  2620. $this->session->set('project', $project->id);
  2621. $this->loadModel('story');
  2622. $executionProducts = $this->loadModel('project')->getBranchesByProject($executionID);
  2623. foreach($plans as $productID => $planIdList)
  2624. {
  2625. if(empty($planIdList)) continue;
  2626. $planIdList = array_filter(explode(',', $planIdList));
  2627. $executionBranches = zget($executionProducts, $productID, array());
  2628. foreach($planIdList as $planID)
  2629. {
  2630. $planStories = $this->story->getPlanStories((int)$planID);
  2631. if(empty($planStories)) continue;
  2632. foreach($planStories as $id => $story)
  2633. {
  2634. if($story->status != 'active' || (!empty($story->branch) && !empty($executionBranches) && !isset($executionBranches[$story->branch]))) unset($planStories[$id]);
  2635. if(strpos($project->storyType, $story->type) === false) unset($planStories[$id]);
  2636. if(!in_array($execution->attribute, array('mix', 'request', 'design')) && $story->type != 'story' && $execution->multiple) unset($planStories[$id]);
  2637. }
  2638. $stories = array_merge($stories, array_keys($planStories));
  2639. }
  2640. }
  2641. $this->linkStory($project->id, $stories);
  2642. $this->linkStory($executionID, $stories);
  2643. return true;
  2644. }
  2645. /**
  2646. * 移除需求。
  2647. * Unlink a story.
  2648. *
  2649. * @param int $executionID
  2650. * @param int $storyID
  2651. * @param int $laneID
  2652. * @param int $columnID
  2653. * @access public
  2654. * @return array|bool
  2655. */
  2656. public function unlinkStory($executionID, $storyID, $laneID = 0, $columnID = 0)
  2657. {
  2658. $storyFrozen = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch('frozen');
  2659. if(!empty($storyFrozen))
  2660. {
  2661. $this->app->loadLang('story');
  2662. dao::$errors[] = sprintf($this->lang->story->frozenTip, $this->lang->story->unlink);
  2663. return false;
  2664. }
  2665. $execution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch();
  2666. if($execution->type == 'project')
  2667. {
  2668. $executions = $this->dao->select('*')->from(TABLE_EXECUTION)->where('parent')->eq($executionID)->fetchAll('id');
  2669. $executionStories = $this->dao->select('project,story')->from(TABLE_PROJECTSTORY)->where('story')->eq($storyID)->andWhere('project')->in(array_keys($executions))->fetchAll();
  2670. if(!empty($executionStories)) return dao::$errors[] = $this->lang->execution->notAllowedUnlinkStory;
  2671. }
  2672. $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->andWhere('story')->eq($storyID)->exec();
  2673. /* Resolve TABLE_KANBANCELL's field cards. */
  2674. if($execution->type == 'kanban')
  2675. {
  2676. $cell = $this->dao->select('*')->from(TABLE_KANBANCELL)
  2677. ->where('kanban')->eq($executionID)
  2678. ->andWhere('`column`')->eq($columnID)
  2679. ->andWhere('lane')->eq($laneID)
  2680. ->fetch();
  2681. if($cell)
  2682. {
  2683. /* Resolve signal ','. */
  2684. $cell->cards = str_replace(",$storyID,", ',', $cell->cards);
  2685. if(strlen($cell->cards) == 1) $cell->cards = '';
  2686. $this->dao->update(TABLE_KANBANCELL)->data($cell)
  2687. ->where('kanban')->eq($executionID)
  2688. ->andWhere('`column`')->eq($columnID)
  2689. ->andWhere('lane')->eq($laneID)
  2690. ->exec();
  2691. }
  2692. }
  2693. /* 因为有需求被移除了,所以需要将剩余的需求重新排序。*/
  2694. $order = 1;
  2695. $relations = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->orderBy('order')->fetchAll();
  2696. foreach($relations as $relation)
  2697. {
  2698. if($relation->order != $order) $this->dao->update(TABLE_PROJECTSTORY)->set('`order`')->eq($order)->where('project')->eq($executionID)->andWhere('story')->eq($relation->story)->exec();
  2699. $order++;
  2700. }
  2701. return $this->afterUnlinkStory($execution, $storyID);
  2702. }
  2703. /**
  2704. * 取消关联需求后的其他数据处理。
  2705. * Other data process after unlink story.
  2706. *
  2707. * @param object $execution
  2708. * @param int $storyID
  2709. * @access public
  2710. * @return bool
  2711. */
  2712. public function afterUnlinkStory($execution, $storyID)
  2713. {
  2714. $this->loadModel('story')->setStage($storyID);
  2715. $this->unlinkCases($execution->id, $storyID);
  2716. $actionType = $execution->type == 'project' ? 'unlinkedFromProject' : 'unlinkedFromExecution';
  2717. if($execution->multiple || $execution->type == 'project') $this->loadModel('action')->create('story', $storyID, $actionType, '', $execution->id);
  2718. /* 从迭代中移除该需求,并记录日志。*/
  2719. if(empty($execution->multiple) && $execution->type != 'project')
  2720. {
  2721. $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('project')->eq($execution->project)->andWhere('story')->eq($storyID)->exec();
  2722. $this->loadModel('action')->create('story', $storyID, 'unlinkedFromProject', '', $execution->project);
  2723. }
  2724. /* 取消该需求关联的所有任务。*/
  2725. $tasks = $this->dao->select('*')->from(TABLE_TASK)->where('story')->eq($storyID)->andWhere('execution')->eq($execution->id)->andWhere('status')->in('wait,doing')->fetchAll();
  2726. $now = helper::now();
  2727. foreach($tasks as $task)
  2728. {
  2729. if(empty($task)) continue;
  2730. $cancelTask = new stdclass();
  2731. $cancelTask->id = $task->id;
  2732. $cancelTask->status = 'cancel';
  2733. $cancelTask->assignedTo = $task->openedBy;
  2734. $cancelTask->assignedDate = $now;
  2735. $cancelTask->canceledBy = $task->lastEditedBy = $this->app->user->account;
  2736. $cancelTask->canceledDate = $task->lastEditedDate = $now;
  2737. $cancelTask->finishedBy = '';
  2738. $cancelTask->finishedDate = null;
  2739. $cancelTask->parent = $task->parent;
  2740. $this->loadModel('task')->cancel($task, $cancelTask);
  2741. }
  2742. return !dao::isError();
  2743. }
  2744. /**
  2745. * 解除用例跟执行的关联关系。
  2746. * Unlink cases.
  2747. *
  2748. * @param int $executionID
  2749. * @param int $storyID
  2750. * @access public
  2751. * @return void
  2752. */
  2753. public function unlinkCases($executionID, $storyID)
  2754. {
  2755. $this->loadModel('action');
  2756. $execution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch();
  2757. $cases = $this->dao->select('id')->from(TABLE_CASE)->where('story')->eq($storyID)->fetchAll('id');
  2758. foreach($cases as $caseID => $case)
  2759. {
  2760. $this->dao->delete()->from(TABLE_PROJECTCASE)->where('project')->eq($executionID)->andWhere('`case`')->eq($caseID)->exec();
  2761. $action = $execution->type == 'project' ? 'unlinkedfromproject' : 'unlinkedfromexecution';
  2762. if($execution->multiple || $execution->type == 'project') $this->action->create('case', $caseID, $action, '', $executionID);
  2763. /* Sync unlink case in no multiple execution. */
  2764. if(empty($execution->multiple) && $execution->type != 'project')
  2765. {
  2766. $this->dao->delete()->from(TABLE_PROJECTCASE)->where('project')->eq($execution->project)->andWhere('`case`')->eq($caseID)->exec();
  2767. $this->action->create('case', $caseID, 'unlinkedfromproject', '', $execution->project);
  2768. }
  2769. }
  2770. $order = 1;
  2771. $cases = $this->dao->select('*')->from(TABLE_PROJECTCASE)->where('project')->eq($executionID)->orderBy('order')->fetchAll();
  2772. foreach($cases as $case)
  2773. {
  2774. if($case->order != $order) $this->dao->update(TABLE_PROJECTCASE)->set('`order`')->eq($order)->where('project')->eq($executionID)->andWhere('`case`')->eq($case->case)->exec();
  2775. $order ++;
  2776. }
  2777. }
  2778. /**
  2779. * 获取执行团队成员列表。
  2780. * Get team members.
  2781. *
  2782. * @param int $executionID
  2783. * @access public
  2784. * @return array
  2785. */
  2786. public function getTeamMembers($executionID)
  2787. {
  2788. if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getTeamMembers();
  2789. return $this->dao->select("t1.*, t1.hours * t1.days AS totalHours, t2.id as userID, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1')
  2790. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  2791. ->where('t1.root')->eq((int)$executionID)
  2792. ->andWhere('t1.type')->eq('execution')
  2793. ->andWhere('t2.deleted')->eq('0')
  2794. ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi()
  2795. ->fetchAll('account');
  2796. }
  2797. /**
  2798. * 获取给定执行的团队成员信息。
  2799. * Get team members information for the execution id list.
  2800. *
  2801. * @param array $executionIdList
  2802. * @access public
  2803. * @return array
  2804. */
  2805. public function getMembersByIdList($executionIdList)
  2806. {
  2807. return $this->dao->select("t1.root, t1.account, t2.realname")->from(TABLE_TEAM)->alias('t1')
  2808. ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
  2809. ->where('t1.root')->in($executionIdList)
  2810. ->andWhere('t1.type')->eq('execution')
  2811. ->andWhere('t2.deleted')->eq('0')
  2812. ->fetchGroup('root', 'account');
  2813. }
  2814. /**
  2815. * 获取可导入的执行成员。
  2816. * Get members of a execution who can be imported.
  2817. *
  2818. * @param int $executionID
  2819. * @param array $currentMembers
  2820. * @access public
  2821. * @return array
  2822. */
  2823. public function getMembers2Import($executionID, $currentMembers)
  2824. {
  2825. if($executionID == 0) return array();
  2826. return $this->dao->select('account, role, hours')
  2827. ->from(TABLE_TEAM)
  2828. ->where('root')->eq($executionID)
  2829. ->andWhere('type')->in('project,execution')
  2830. ->andWhere('account')->notIN($currentMembers)
  2831. ->fetchAll('account');
  2832. }
  2833. /**
  2834. * 获取可以复制团队的项目、执行列表。
  2835. * Get projects and executions that copy the team.
  2836. *
  2837. * @param int $projectID
  2838. * @access public
  2839. * @return array
  2840. */
  2841. public function getCanCopyObjects($projectID = 0)
  2842. {
  2843. if(empty($projectID)) return array();
  2844. $objectPairs = $this->dao->select('id,name')->from(TABLE_PROJECT)
  2845. ->where('deleted')->eq(0)
  2846. ->andWhere('project', true)->eq($projectID)
  2847. ->andWhere('type')->ne('project')
  2848. ->markRight(1)
  2849. ->orWhere('id')->eq($projectID)
  2850. ->orderBy('type_asc,openedDate_desc')
  2851. ->limit('11')
  2852. ->fetchPairs();
  2853. $countPairs = $this->dao->select('root, COUNT(1) AS count')->from(TABLE_TEAM)
  2854. ->where('( type')->eq('project')
  2855. ->andWhere('root')->eq($projectID)
  2856. ->markRight(1)
  2857. ->orWhere('( type')->eq('execution')
  2858. ->andWhere('root')->in(array_keys($objectPairs))
  2859. ->markRight(1)
  2860. ->groupBy('root')
  2861. ->fetchPairs('root');
  2862. foreach($objectPairs as $objectID => $objectName)
  2863. {
  2864. $memberCount = zget($countPairs, $objectID, 0);
  2865. $countTip = $memberCount > 1 ? str_replace('member', 'members', $this->lang->execution->countTip) : $this->lang->execution->countTip;
  2866. $objectPairs[$objectID] = $objectName . sprintf($countTip, $memberCount);
  2867. }
  2868. return $objectPairs;
  2869. }
  2870. /**
  2871. * 维护执行团队成员。
  2872. * Manage team members.
  2873. *
  2874. * @param object $execution
  2875. * @param array $members
  2876. * @access public
  2877. * @return void
  2878. */
  2879. public function manageMembers($execution, $members)
  2880. {
  2881. $oldJoin = $this->dao->select('`account`, `join`')->from(TABLE_TEAM)->where('root')->eq($execution->id)->andWhere('type')->eq('execution')->fetchPairs();
  2882. $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($execution->id)->andWhere('type')->eq('execution')->exec();
  2883. $accountList = array();
  2884. $executionMember = array();
  2885. foreach($members as $member)
  2886. {
  2887. if(in_array($member->account, $accountList)) continue;
  2888. $member->root = isset($member->root) ? $member->root : $execution->id;
  2889. $member->type = isset($member->type) ? $member->type : 'execution';
  2890. $member->join = isset($oldJoin[$member->account]) ? $oldJoin[$member->account] : helper::today();
  2891. $executionMember[$member->account] = $member;
  2892. $accountList[] = $member->account;
  2893. $this->dao->insert(TABLE_TEAM)->data($member)->exec();
  2894. }
  2895. /* Only changed account update userview. */
  2896. $oldAccountList = array_keys($oldJoin);
  2897. $addedAccountList = array_diff($accountList, $oldAccountList);
  2898. $removedAccountList = array_diff($oldAccountList, $accountList);
  2899. $changedAccountList = array_merge($addedAccountList, $removedAccountList);
  2900. $changedAccountList = array_unique($changedAccountList);
  2901. /* Log history. */
  2902. $actionID = $this->loadModel('action')->create('execution', $execution->id, 'managedTeam');
  2903. if(empty($addedAccountList) && empty($removedAccountList)) return;
  2904. $users = $this->loadModel('user')->getPairs('noletter');
  2905. $addedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $addedAccountList);
  2906. $removedAccountList = array_map(function($account) use ($users) { return zget($users, $account); }, $removedAccountList);
  2907. if(!empty($addedAccountList)) $changes[] = array('field' => 'addDiff', 'old' => '', 'new' => '', 'diff' => implode(',', $addedAccountList));
  2908. if(!empty($removedAccountList)) $changes[] = array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => implode(',', $removedAccountList));
  2909. if(!empty($changes)) $this->action->logHistory($actionID, $changes);
  2910. /* Add the execution team members to the project. */
  2911. if($execution->project) $this->addProjectMembers($execution->project, $executionMember);
  2912. /* Add the execution team members to parent executions. */
  2913. foreach(explode(',', $execution->path) as $parentID)
  2914. {
  2915. if(empty($parentID) || $parentID == $execution->project) continue;
  2916. $this->executionTao->addExecutionMembers((int)$parentID, array_keys($executionMember));
  2917. }
  2918. if($execution->acl != 'open') $this->updateUserView($execution->id, 'sprint', $changedAccountList);
  2919. }
  2920. /**
  2921. * 添加项目团队成员。
  2922. * Add the execution team members to the project.
  2923. *
  2924. * @param int $projectID
  2925. * @param array $members
  2926. * @access public
  2927. * @return void
  2928. */
  2929. public function addProjectMembers($projectID = 0, $members = array())
  2930. {
  2931. if(empty($members)) return;
  2932. $projectType = 'project';
  2933. $oldJoin = $this->dao->select('`account`, `join`')->from(TABLE_TEAM)->where('root')->eq($projectID)->andWhere('type')->eq($projectType)->fetchPairs();
  2934. $accountList = array();
  2935. foreach($members as $member)
  2936. {
  2937. if(isset($oldJoin[$member->account])) continue;
  2938. $accountList[] = $member->account;
  2939. $member->root = $projectID;
  2940. $member->type = $projectType;
  2941. $this->dao->insert(TABLE_TEAM)->data($member)->exec();
  2942. }
  2943. /* Only changed account update userview. */
  2944. $oldAccountList = array_keys($oldJoin);
  2945. $changedAccountList = array_diff($accountList, $oldAccountList);
  2946. $changedAccountList = array_merge($changedAccountList, array_diff($oldAccountList, $accountList));
  2947. $changedAccountList = array_unique($changedAccountList);
  2948. if($changedAccountList)
  2949. {
  2950. $this->loadModel('user')->updateUserView(array($projectID), $projectType, $changedAccountList);
  2951. $linkedProducts = $this->dao->select("t2.id")->from(TABLE_PROJECTPRODUCT)->alias('t1')
  2952. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
  2953. ->where('t2.deleted')->eq(0)
  2954. ->andWhere('t1.project')->eq($projectID)
  2955. ->andWhere('t2.vision')->eq($this->config->vision)
  2956. ->fetchPairs();
  2957. if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', $changedAccountList);
  2958. }
  2959. if(empty($accountList) || ($this->app->rawModule == 'project' && $this->app->rawMethod == 'create')) return;
  2960. /* Log history. */
  2961. $users = $this->loadModel('user')->getPairs('noletter');
  2962. $accountList = array_map(function($account) use ($users) { return zget($users, $account); }, $accountList);
  2963. $actionID = $this->loadModel('action')->create('project', $projectID, 'syncExecutionTeam');
  2964. $changes = array();
  2965. if(!empty($accountList)) $changes[] = array('field' => 'addDiff', 'old' => '', 'new' => '', 'diff' => implode(',', $accountList));
  2966. if(!empty($changes)) $this->action->logHistory($actionID, $changes);
  2967. }
  2968. /**
  2969. * 移除执行团队成员。
  2970. * Remove the user from the execution team members.
  2971. *
  2972. * @param int $executionID
  2973. * @param string $account
  2974. * @access public
  2975. * @return void
  2976. */
  2977. public function unlinkMember($executionID, $account)
  2978. {
  2979. /* Remove the user from the execution team members. */
  2980. $execution = $this->getByID($executionID);
  2981. $type = strpos(',stage,sprint,kanban,', ",$execution->type,") !== false ? 'execution' : $execution->type;
  2982. $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($executionID)->andWhere('type')->eq($type)->andWhere('account')->eq($account)->exec();
  2983. /* Update the user's execution permission. */
  2984. $this->updateUserView($executionID, 'sprint', array($account));
  2985. $linkedProducts = $this->loadModel('product')->getProductPairsByProject($execution->id);
  2986. if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', array($account));
  2987. }
  2988. /**
  2989. * 计算燃尽图数据。
  2990. * Compute burn of a execution.
  2991. *
  2992. * @param int|string|array $executionID
  2993. * @access public
  2994. * @return array
  2995. */
  2996. public function computeBurn($executionID = '')
  2997. {
  2998. if(is_int($executionID)) $executionID = (string)$executionID;
  2999. $executions = $this->dao->select('id, code')->from(TABLE_EXECUTION)
  3000. ->where('type')->in('sprint,stage')
  3001. ->andWhere('lifetime')->ne('ops')
  3002. ->andWhere('status')->notin('done,closed,suspended')
  3003. ->beginIF($executionID)->andWhere('id')->in($executionID)->fi()
  3004. ->fetchPairs();
  3005. if(!$executions) return array();
  3006. /* Get burn related data. */
  3007. list($burns, $closedLefts, $finishedEstimates, $storyPoints) = $this->executionTao->fetchBurnData(array_keys($executions));
  3008. /* Update today's data of burn. */
  3009. foreach($burns as $executionID => $burn)
  3010. {
  3011. if(isset($closedLefts[$executionID]))
  3012. {
  3013. $closedLeft = $closedLefts[$executionID];
  3014. $burn->left -= (int)$closedLeft->left;
  3015. }
  3016. if(isset($finishedEstimates[$executionID]))
  3017. {
  3018. $finishedEstimate = $finishedEstimates[$executionID];
  3019. $burn->estimate -= (int)$finishedEstimate->estimate;
  3020. }
  3021. $burn->product = 0;
  3022. $burn->task = 0;
  3023. if(isset($storyPoints[$executionID])) $burn->storyPoint = $storyPoints[$executionID]->storyPoint;
  3024. $this->dao->replace(TABLE_BURN)->data($burn)->exec();
  3025. $burn->executionName = $executions[$burn->execution];
  3026. }
  3027. return $burns;
  3028. }
  3029. /**
  3030. * 计算累计流图的数据。
  3031. * Compute cfd of a execution.
  3032. *
  3033. * @param int $executionID
  3034. * @access public
  3035. * @return array
  3036. */
  3037. public function computeCFD($executionID = 0)
  3038. {
  3039. $executions = $this->dao->select('id, code')->from(TABLE_EXECUTION)
  3040. ->where('type')->eq('kanban')
  3041. ->andWhere('status')->notin('done,closed,suspended')
  3042. ->beginIF($executionID)->andWhere('id')->in($executionID)->fi()
  3043. ->fetchPairs();
  3044. if(!$executions) return array();
  3045. /* Update today's data of cfd. */
  3046. $cells = $this->dao->select("t1.id, t1.kanban as execution, t1.`column`, t1.type, t1.cards, t1.lane, t2.name, t2.parent")
  3047. ->from(TABLE_KANBANCELL)->alias('t1')
  3048. ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id')
  3049. ->where('t1.kanban')->in(array_keys($executions))
  3050. ->andWhere('t2.deleted')->eq('0')
  3051. ->andWhere('t1.type')->in('story,bug,task')
  3052. ->orderBy('t2.id asc')
  3053. ->fetchAll('id');
  3054. /* Group by execution/type/name/lane/column. */
  3055. $columnGroup = array();
  3056. $parentNames = array();
  3057. foreach($cells as $column)
  3058. {
  3059. if($column->parent == '-1')
  3060. {
  3061. $parentNames[$column->column] = $column->name;
  3062. continue;
  3063. }
  3064. $column->name = isset($parentNames[$column->parent]) ? $parentNames[$column->parent] . "($column->name)" : $column->name;
  3065. $columnGroup[$column->execution][$column->type][$column->name][$column->lane][$column->column] = $column;
  3066. }
  3067. foreach($columnGroup as $executionID => $executionGroup)
  3068. {
  3069. foreach($executionGroup as $type => $columns)
  3070. {
  3071. foreach($columns as $colName => $laneGroup)
  3072. {
  3073. $this->executionTao->updateTodayCFDData($executionID, $type, $colName, $laneGroup);
  3074. }
  3075. }
  3076. }
  3077. }
  3078. /**
  3079. * 查看指定的执行日期是否有数据,且没有更新最新日期的数据。
  3080. * Check whether there is data on the specified date of execution, and there is no data with the latest date added.
  3081. *
  3082. * @param int $executionID
  3083. * @param string $date
  3084. * @access public
  3085. * @return void
  3086. */
  3087. public function updateCFDData($executionID, $date)
  3088. {
  3089. $today = helper::today();
  3090. if($date >= $today) return;
  3091. $checkData = $this->dao->select("date, `count` AS value, `name`")->from(TABLE_CFD)
  3092. ->where('execution')->eq((int)$executionID)
  3093. ->andWhere('date')->eq($date)
  3094. ->orderBy('date DESC, id asc')->fetchGroup('name', 'date');
  3095. if(!$checkData)
  3096. {
  3097. $closetoDate = $this->dao->select("max(date) as date")->from(TABLE_CFD)->where('execution')->eq((int)$executionID)->andWhere('date')->lt($date)->fetch('date');
  3098. if($closetoDate)
  3099. {
  3100. $copyData = $this->dao->select("*")->from(TABLE_CFD)
  3101. ->where('execution')->eq((int)$executionID)
  3102. ->andWhere('date')->eq($closetoDate)
  3103. ->fetchAll();
  3104. foreach($copyData as $data)
  3105. {
  3106. unset($data->id);
  3107. $data->date = $date;
  3108. $this->dao->replace(TABLE_CFD)->data($data)->exec();
  3109. }
  3110. }
  3111. }
  3112. }
  3113. /**
  3114. * 修改燃尽图首天工时。
  3115. * Fix burn for first day.
  3116. *
  3117. * @param object $burn
  3118. * @access public
  3119. * @return bool
  3120. */
  3121. public function fixFirst($burn)
  3122. {
  3123. $this->dao->replace(TABLE_BURN)->data($burn)->exec();
  3124. return !dao::isError();
  3125. }
  3126. /**
  3127. * 获取累计流图的开始、结束日期。
  3128. * Get begin and end for CFD.
  3129. *
  3130. * @param object $execution
  3131. * @access public
  3132. * @return array
  3133. */
  3134. public function getBeginEnd4CFD($execution)
  3135. {
  3136. $end = (!helper::isZeroDate($execution->closedDate) && date('Y-m-d', strtotime($execution->closedDate)) < helper::today()) ? date('Y-m-d', strtotime($execution->closedDate)) : helper::today();
  3137. $begin = (!helper::isZeroDate($execution->openedDate) && date('Y-m-d', strtotime($execution->openedDate)) > date('Y-m-d', strtotime('-13 days', strtotime($end)))) ? date('Y-m-d', strtotime($execution->openedDate)) : date('Y-m-d', strtotime('-13 days', strtotime($end)));
  3138. return array($begin, $end);
  3139. }
  3140. /**
  3141. * 获取燃尽图时间点数据。
  3142. * Get burn chart flot data.
  3143. *
  3144. * @param int $executionID
  3145. * @param string $burnBy left|estimate|storyPoint
  3146. * @param bool $showDelay
  3147. * @param array $dateList
  3148. * @access public
  3149. * @return array|null
  3150. */
  3151. public function getBurnDataFlot($executionID = 0, $burnBy = '', $showDelay = false, $dateList = array())
  3152. {
  3153. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionBurnData($dateList);
  3154. /* Get execution and burn counts. */
  3155. $execution = $this->getByID($executionID);
  3156. /* If the burnCounts > $itemCounts, get the latest $itemCounts records. */
  3157. $sets = $this->dao->select("date AS name, `$burnBy` AS value, `$burnBy`")->from(TABLE_BURN)->where('execution')->eq((int)$executionID)->andWhere('task')->eq(0)->orderBy('date DESC')->fetchAll('name');
  3158. $burnData = array();
  3159. foreach($sets as $date => $set)
  3160. {
  3161. if($date < $execution->begin) continue;
  3162. if(!$showDelay && $date > $execution->end) $set->value = 'null';
  3163. if($showDelay && $date < $execution->end) $set->value = 'null';
  3164. $burnData[$date] = $set;
  3165. }
  3166. foreach($dateList as $date)
  3167. {
  3168. if(isset($burnData[$date])) continue;
  3169. if(($showDelay && $date < $execution->end) || (!$showDelay && $date > $execution->end))
  3170. {
  3171. $set = new stdClass();
  3172. $set->name = $date;
  3173. $set->value = 'null';
  3174. $set->$burnBy = 0;
  3175. $burnData[$date] = $set;
  3176. }
  3177. }
  3178. krsort($burnData);
  3179. $burnData = array_reverse($burnData);
  3180. return $burnData;
  3181. }
  3182. /**
  3183. * 获取执行的燃尽图数据。
  3184. * Get execution burn data.
  3185. *
  3186. * @param array $executions
  3187. * @access public
  3188. * @return array
  3189. */
  3190. public function getBurnData($executions)
  3191. {
  3192. if(empty($executions)) return array();
  3193. /* Get burndown charts datas. */
  3194. $burnList = $this->dao->select('execution, date AS name, `left` AS value')
  3195. ->from(TABLE_BURN)
  3196. ->where('execution')->in(array_keys($executions))
  3197. ->andWhere('task')->eq(0)
  3198. ->orderBy('date desc')
  3199. ->fetchGroup('execution', 'name');
  3200. foreach($burnList as $executionID => $executionBurnList)
  3201. {
  3202. /* If executionBurnList > $itemCounts, split it, else call processBurnData() to pad burnList. */
  3203. $begin = helper::isZeroDate($executions[$executionID]->begin) ? $executions[$executionID]->openedDate : $executions[$executionID]->begin;
  3204. $end = $executions[$executionID]->end;
  3205. /* Unset burn information that is greater than the execution end date. */
  3206. foreach($executionBurnList as $date => $burnInfo)
  3207. {
  3208. if($date > $end) unset($executionBurnList[$date]);
  3209. }
  3210. $executionBurnList = $this->processBurnData($executionBurnList, $this->config->execution->defaultBurnPeriod, $begin, $end);
  3211. /* Shorter names. */
  3212. foreach($executionBurnList as $executionBurn)
  3213. {
  3214. $executionBurn->name = substr($executionBurn->name, 5);
  3215. unset($executionBurn->execution);
  3216. }
  3217. ksort($executionBurnList);
  3218. $burnList[$executionID] = $executionBurnList;
  3219. }
  3220. return $burnList;
  3221. }
  3222. /**
  3223. * 根据传入的条件筛选日期列表。
  3224. * Process burndown datas when the sets is smaller than the itemCounts.
  3225. *
  3226. * @param array $dateList
  3227. * @param int $itemCounts
  3228. * @param string $begin
  3229. * @param string $end
  3230. * @param string $mode noempty
  3231. * @access public
  3232. * @return array
  3233. */
  3234. public function processBurnData($dateList, $itemCounts, $begin, $end, $mode = 'noempty')
  3235. {
  3236. /* Get the date interval if the $end is not empty, otherwise get the $end. */
  3237. if(!helper::isZeroDate($end))
  3238. {
  3239. $period = helper::diffDate($end, $begin) + 1;
  3240. $counts = $period > $itemCounts ? $itemCounts : $period;
  3241. }
  3242. else
  3243. {
  3244. $counts = $period = $itemCounts;
  3245. $end = date(DT_DATE1, strtotime("+$counts days", strtotime($begin)));
  3246. }
  3247. $current = $begin;
  3248. $today = helper::today();
  3249. $endTime = strtotime($end);
  3250. $preValue = 0;
  3251. $todayTag = 0;
  3252. /* Removes date that are not in the current date range. */
  3253. foreach($dateList as $date => $value)
  3254. {
  3255. if($begin > $date) unset($dateList[$date]);
  3256. }
  3257. /* Update date that are not in the date list and are in the date range. */
  3258. for($i = 0; $i < $period; $i++)
  3259. {
  3260. $currentTime = strtotime($current);
  3261. if($currentTime > $endTime) break;
  3262. if($currentTime > time() && !$todayTag) $todayTag = $i + 1;
  3263. if(isset($dateList[$current])) $preValue = $dateList[$current]->value;
  3264. if(!isset($dateList[$current]) && $mode == 'noempty')
  3265. {
  3266. $dateList[$current] = new stdclass();
  3267. $dateList[$current]->name = $current;
  3268. $dateList[$current]->value = helper::diffDate($current, $today) < 0 ? $preValue : 'null';
  3269. }
  3270. $nextDay = date(DT_DATE1, $currentTime + 24 * 3600);
  3271. $current = $nextDay;
  3272. }
  3273. ksort($dateList);
  3274. if(count($dateList) <= $counts) return $dateList;
  3275. if($endTime <= time()) return array_slice($dateList, -$counts, $counts);
  3276. if($todayTag <= $counts) return array_slice($dateList, 0, $counts);
  3277. if($todayTag > $counts) return array_slice($dateList, $todayTag - $counts, $counts);
  3278. }
  3279. /**
  3280. * 构造累计流图数据。
  3281. * Build CFD data.
  3282. *
  3283. * @param int $executionID
  3284. * @param array $dateList
  3285. * @param string $type story|task|bug
  3286. * @access public
  3287. * @return array
  3288. */
  3289. public function buildCFDData($executionID, $dateList, $type)
  3290. {
  3291. $nameGroup = $this->getCFDData($executionID, $dateList, $type);
  3292. if(empty($nameGroup)) return array();
  3293. $chartData['labels'] = $this->loadModel('report')->convertFormat($dateList, DT_DATE5);
  3294. $chartData['line'] = array();
  3295. foreach($nameGroup as $name => $value)
  3296. {
  3297. $chartData['line'][$name] = $this->report->createSingleJSON($value, $dateList);
  3298. }
  3299. return $chartData;
  3300. }
  3301. /**
  3302. * 获取累计流图的数据。
  3303. * Get CFD data to display.
  3304. *
  3305. * @param int $executionID
  3306. * @param array $dateList
  3307. * @param string $type story|task|bug
  3308. * @access public
  3309. * @return array
  3310. */
  3311. public function getCFDData($executionID = 0, $dateList = array(), $type = 'story')
  3312. {
  3313. $execution = $this->getByID($executionID);
  3314. $nameGroup = $this->dao->select("date, `count` AS value, `name`")->from(TABLE_CFD)
  3315. ->where('execution')->eq($executionID)
  3316. ->andWhere('type')->eq($type)
  3317. ->andWhere('date')->in($dateList)
  3318. ->orderBy('date DESC, id asc')
  3319. ->fetchGroup('name', 'date');
  3320. $data = array();
  3321. foreach($nameGroup as $name => $dateList)
  3322. {
  3323. foreach($dateList as $date => $value)
  3324. {
  3325. if($date < $execution->begin) continue;
  3326. $data[$name][$date] = $value;
  3327. }
  3328. }
  3329. return $data;
  3330. }
  3331. /**
  3332. * 通过搜索条件获取任务列表信息。
  3333. * Get tasks by search.
  3334. *
  3335. * @param string $condition
  3336. * @param string $orderBy
  3337. * @param object $pager
  3338. * @param string $queryKey
  3339. * @access public
  3340. * @return array
  3341. */
  3342. public function getSearchTasks($condition, $orderBy, $pager = null, $queryKey = 'task')
  3343. {
  3344. /* 按指派人搜索的时候,可以搜索到参与的多人任务。 */
  3345. if(strpos($condition, '`assignedTo`') !== false)
  3346. {
  3347. preg_match_all("/`assignedTo`\s+(([^']*) ('([^']*)'))/", $condition, $matches);
  3348. $condition = preg_replace('/`(\w+)`/', 't1.`$1`', $condition);
  3349. foreach($matches[0] as $matchIndex => $match)
  3350. {
  3351. $subQuery = $this->dao->select('1')->from(TABLE_TASKTEAM)
  3352. ->where('task = t1.id')
  3353. ->andWhere('account' . $matches[1][$matchIndex])
  3354. ->get();
  3355. $condition = str_replace(
  3356. "t1.{$match}",
  3357. "(t1.{$match} OR EXISTS (" . $subQuery . "))",
  3358. $condition
  3359. );
  3360. }
  3361. $this->session->set("{$queryKey}QueryCondition", $condition, $this->app->tab);
  3362. }
  3363. $orderBy = array_map(function($value)
  3364. {
  3365. return strpos($value, '.') === false ? 't1.' . $value : $value;
  3366. }, explode(',', $orderBy));
  3367. $orderBy = str_replace('t1.storyTitle', 't2.title', implode(',', $orderBy));
  3368. $orderBy = str_replace(array('t1.pri_', 't1.`pri'), array('priOrder_', '`priOrder_'), $orderBy);
  3369. if(strpos($condition, 't1.') === false)
  3370. {
  3371. $condition = preg_replace('/`(\w+)`/', 't1.`$1`', $condition);
  3372. }
  3373. $condition = str_replace("AND deleted = '0'", '', $condition);
  3374. $tasks = $this->dao->select('DISTINCT t1.*,
  3375. t2.id AS storyID,
  3376. t2.title AS storyTitle,
  3377. t2.product,
  3378. t2.branch,
  3379. t2.version AS latestStoryVersion,
  3380. t2.status AS storyStatus,
  3381. t3.realname AS assignedToRealName,
  3382. IF(t1.`pri` = 0, 999, t1.`pri`) as priOrder')
  3383. ->from(TABLE_TASK)->alias('t1')
  3384. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  3385. ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account')
  3386. ->where('t1.deleted')->eq(0)
  3387. ->andWhere($condition)
  3388. ->orderBy($orderBy)
  3389. ->page($pager, 't1.id')
  3390. ->fetchAll('id');
  3391. $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task', false);
  3392. return $this->processTasks($tasks);
  3393. }
  3394. /**
  3395. * 批量处理任务,团队、父子层级、泳道等信息。
  3396. * Batch process tasks, teams, parent-child, lanes, etc.
  3397. *
  3398. * @param array $tasks
  3399. * @access public
  3400. * @return array
  3401. */
  3402. public function processTasks($tasks)
  3403. {
  3404. if(empty($tasks)) return array();
  3405. $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task');
  3406. if(!empty($taskTeam))
  3407. {
  3408. foreach($taskTeam as $taskID => $team) $tasks[$taskID]->team = $team;
  3409. }
  3410. if($this->config->vision == 'lite') $tasks = $this->loadModel('task')->appendLane($tasks);
  3411. return $this->loadModel('task')->processTasks($tasks);
  3412. }
  3413. /**
  3414. * 获取任务列表的统计信息。
  3415. * Get the summary of execution.
  3416. *
  3417. * @param array $tasks
  3418. * @access public
  3419. * @return string
  3420. */
  3421. public function summary($tasks)
  3422. {
  3423. $taskSum = 0;
  3424. $totalEstimate = $totalConsumed = $totalLeft = 0.0;
  3425. $summations = array();
  3426. $this->app->loadLang('task');
  3427. /* 当前只需要显示wait 和 doing 状态,但是从代码分析将来可能需要统计其他状态的,所以取全部状态。 */
  3428. foreach($this->lang->task->statusList as $statusCode => $statusName) $summations[$statusCode] = 0;
  3429. foreach($tasks as $task)
  3430. {
  3431. if($task->isParent == '0')
  3432. {
  3433. $totalEstimate += $task->estimate;
  3434. $totalConsumed += $task->consumed;
  3435. if($task->status != 'cancel' and $task->status != 'closed') $totalLeft += (float)$task->left;
  3436. }
  3437. if(isset($summations[$task->status])) $summations[$task->status] ++;
  3438. if(isset($task->children))
  3439. {
  3440. foreach($task->children as $child)
  3441. {
  3442. if(isset($summations[$child->status])) $summations[$child->status] ++;
  3443. $taskSum ++;
  3444. }
  3445. }
  3446. $taskSum ++;
  3447. }
  3448. return sprintf($this->lang->execution->taskSummary, $taskSum, $summations['wait'], $summations['doing'], round($totalEstimate, 1), round($totalConsumed, 1), round($totalLeft, 1));
  3449. }
  3450. /**
  3451. * 判断操作按钮是否可以点击。
  3452. * Judge an action is clickable or not.
  3453. *
  3454. * @param object $execution
  3455. * @param string $action
  3456. * @access public
  3457. * @return bool
  3458. */
  3459. public static function isClickable($execution, $action)
  3460. {
  3461. if(!empty($execution->frozen) && in_array($action, array('edit', 'createChildStage', 'delete', 'putoff'))) return false;
  3462. if($action == 'createChildStage') return commonModel::hasPriv('programplan', 'create') && $execution->type == 'stage';
  3463. if($action == 'createTask') return commonModel::hasPriv('task', 'create') && commonModel::hasPriv('execution', 'create') && empty($execution->isParent);
  3464. if($action == 'edit') return isset($execution->projectModel) && in_array($execution->projectModel, array('waterfall', 'waterfallplus', 'ipd')) ? commonModel::hasPriv('programplan', 'edit') : commonModel::hasPriv('execution', 'edit');
  3465. if(!commonModel::hasPriv('execution', $action)) return false;
  3466. $action = strtolower($action);
  3467. if($action == 'start') return $execution->status == 'wait';
  3468. if($action == 'close') return $execution->status != 'closed' && (empty($execution->isParent) || !empty($execution->parentCanClose));
  3469. if($action == 'suspend') return $execution->status == 'wait' || $execution->status == 'doing';
  3470. if($action == 'putoff') return $execution->status == 'wait' || $execution->status == 'doing';
  3471. if($action == 'activate') return $execution->status == 'suspended' || $execution->status == 'closed';
  3472. if($action == 'delete') return empty($execution->isParent);
  3473. return true;
  3474. }
  3475. /**
  3476. * 获取日期列表数据。
  3477. * Get date list data.
  3478. *
  3479. * @param string $begin
  3480. * @param string $end
  3481. * @param string $type noweekend|withweekend
  3482. * @param int|string $interval
  3483. * @param string $format
  3484. * @param string $executionDeadline
  3485. * @access public
  3486. * @return array
  3487. */
  3488. public function getDateList($begin, $end, $type, $interval = 0, $format = 'm/d/Y', $executionDeadline = '')
  3489. {
  3490. $this->app->loadClass('date', true);
  3491. $dateList = date::getDateList($begin, $end, $format, $type, $this->config->execution->weekend);
  3492. if(!$interval) $interval = floor(count($dateList) / $this->config->execution->maxBurnDay);
  3493. /* Remove date by interval. */
  3494. if($interval)
  3495. {
  3496. $spaces = (int)$interval;
  3497. $counter = $spaces;
  3498. foreach($dateList as $i => $date)
  3499. {
  3500. $counter ++;
  3501. if($date == $executionDeadline) continue;
  3502. if($counter <= $spaces)
  3503. {
  3504. unset($dateList[$i]);
  3505. continue;
  3506. }
  3507. $counter = 0;
  3508. }
  3509. }
  3510. return array(array_values($dateList), $interval);
  3511. }
  3512. /**
  3513. * 获取当前执行下任务的总预计工时。
  3514. * Get the total estimate for the current execution's tasks.
  3515. *
  3516. * @param int $executionID
  3517. * @access public
  3518. * @return float
  3519. */
  3520. public function getTotalEstimate($executionID)
  3521. {
  3522. $estimate = $this->dao->select('SUM(estimate) as estimate')->from(TABLE_TASK)->where('execution')->eq($executionID)->andWhere('deleted')->eq('0')->fetch('estimate');
  3523. return round((float)$estimate);
  3524. }
  3525. /**
  3526. * 修复执行的排序顺序。
  3527. * Fix the sort order of execution.
  3528. *
  3529. * @access public
  3530. * @return void
  3531. */
  3532. public function fixOrder()
  3533. {
  3534. $executions = $this->dao->select('id,`order`')->from(TABLE_EXECUTION)->orderBy('order')->fetchPairs('id', 'order');
  3535. $i = 0;
  3536. foreach($executions as $id => $order)
  3537. {
  3538. $i++;
  3539. $newOrder = $i * 5;
  3540. if($order == $newOrder) continue;
  3541. $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($newOrder)->where('id')->eq($id)->exec();
  3542. }
  3543. }
  3544. /**
  3545. * 构造Bug的搜索表单。
  3546. * Build bug search form.
  3547. *
  3548. * @param array $products
  3549. * @param int $queryID
  3550. * @param string $actionURL
  3551. * @param string $type
  3552. * @access public
  3553. * @return void
  3554. */
  3555. public function buildBugSearchForm($products, $queryID, $actionURL, $type = 'execution')
  3556. {
  3557. $modules = array();
  3558. $builds = array('' => '', 'trunk' => $this->lang->trunk);
  3559. foreach($products as $product)
  3560. {
  3561. $productModules = $this->loadModel('tree')->getOptionMenu($product->id, 'bug');
  3562. foreach($productModules as $moduleID => $moduleName) $modules[$moduleID] = ((count($products) >= 2 and $moduleID) ? $product->name : '') . $moduleName;
  3563. $productBuilds = $this->loadModel('build')->getBuildPairs(array($product->id), 'all', 'noempty|notrunk|withbranch');
  3564. foreach($productBuilds as $buildID => $buildName) $builds[$buildID] = ((count($products) >= 2 and $buildID) ? $product->name . '/' : '') . $buildName;
  3565. }
  3566. $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products));
  3567. $branchPairs = array();
  3568. $productType = 'normal';
  3569. $productPairs = array(0 => '');
  3570. foreach($products as $product)
  3571. {
  3572. $productPairs[$product->id] = $product->name;
  3573. if($product->type != 'normal')
  3574. {
  3575. $productType = $product->type;
  3576. if(isset($product->branches))
  3577. {
  3578. foreach($product->branches as $branch)
  3579. {
  3580. if(isset($branchGroups[$product->id][$branch])) $branchPairs[$branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$branch];
  3581. }
  3582. }
  3583. else
  3584. {
  3585. $productBranches = isset($branchGroups[$product->id]) ? $branchGroups[$product->id] : array(0);
  3586. if(count($products) > 1)
  3587. {
  3588. foreach($productBranches as $branchID => $branchName) $productBranches[$branchID] = $product->name . '/' . $branchName;
  3589. }
  3590. $branchPairs += $productBranches;
  3591. }
  3592. }
  3593. }
  3594. $projects = $this->loadModel('project')->getPairsByProgram();
  3595. $this->config->bug->search['module'] = $type == 'execution' ? 'executionBug' : 'projectBug';
  3596. $this->config->bug->search['actionURL'] = $actionURL;
  3597. $this->config->bug->search['queryID'] = $queryID;
  3598. $this->config->bug->search['params']['project']['values'] = $projects + array('all' => $this->lang->project->allProjects);
  3599. $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array_keys($products));
  3600. $this->config->bug->search['params']['module']['values'] = $modules;
  3601. $this->config->bug->search['params']['openedBuild']['values'] = $builds;
  3602. $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values'];
  3603. if(isset($this->config->bug->search['params']['product'])) $this->config->bug->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject);
  3604. if($type == 'execution' || !$this->session->multiple)
  3605. {
  3606. unset($this->config->bug->search['fields']['execution']);
  3607. unset($this->config->bug->search['params']['execution']);
  3608. }
  3609. if($productType == 'normal')
  3610. {
  3611. unset($this->config->bug->search['fields']['branch']);
  3612. unset($this->config->bug->search['params']['branch']);
  3613. }
  3614. else
  3615. {
  3616. $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productType]);
  3617. $this->config->bug->search['params']['branch']['values'] = $branchPairs;
  3618. }
  3619. $this->config->bug->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->bug->statusList);
  3620. $this->loadModel('search')->setSearchParams($this->config->bug->search);
  3621. }
  3622. /**
  3623. * 构造用例列表的搜索表单。
  3624. * Build testcase search form.
  3625. *
  3626. * @param array $products
  3627. * @param int $queryID
  3628. * @param string $actionURL
  3629. * @param int $executionID
  3630. * @access public
  3631. * @return void
  3632. */
  3633. public function buildCaseSearchForm($products, $queryID, $actionURL, $executionID)
  3634. {
  3635. $modules = array();
  3636. foreach($products as $product)
  3637. {
  3638. $productModules = $this->loadModel('tree')->getOptionMenu($product->id, 'case');
  3639. foreach($productModules as $moduleID => $moduleName) $modules[$moduleID] = ((count($products) >= 2 and $moduleID) ? $product->name : '') . $moduleName;
  3640. }
  3641. $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products));
  3642. $branchPairs = array();
  3643. $productType = 'normal';
  3644. $productPairs = array(0 => '');
  3645. foreach($products as $product)
  3646. {
  3647. $productPairs[$product->id] = $product->name;
  3648. if($product->type != 'normal')
  3649. {
  3650. $productType = $product->type;
  3651. if(isset($product->branches))
  3652. {
  3653. foreach($product->branches as $branch)
  3654. {
  3655. if(isset($branchGroups[$product->id][$branch])) $branchPairs[$branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$branch];
  3656. }
  3657. }
  3658. else
  3659. {
  3660. $productBranches = isset($branchGroups[$product->id]) ? $branchGroups[$product->id] : array(0);
  3661. if(count($products) > 1)
  3662. {
  3663. foreach($productBranches as $branchID => $branchName) $productBranches[$branchID] = $product->name . '/' . $branchName;
  3664. }
  3665. $branchPairs += $productBranches;
  3666. }
  3667. }
  3668. }
  3669. unset($this->config->testcase->search['fields']['execution']);
  3670. unset($this->config->testcase->search['params']['execution']);
  3671. $this->config->testcase->search['module'] = 'executionCase';
  3672. $this->config->testcase->search['actionURL'] = $actionURL;
  3673. $this->config->testcase->search['queryID'] = $queryID;
  3674. $this->config->testcase->search['params']['module']['values'] = $modules;
  3675. if(isset($this->config->testcase->search['params']['product'])) $this->config->testcase->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject);
  3676. if($productType == 'normal')
  3677. {
  3678. unset($this->config->testcase->search['fields']['branch']);
  3679. unset($this->config->testcase->search['params']['branch']);
  3680. }
  3681. else
  3682. {
  3683. $this->config->testcase->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productType]);
  3684. $this->config->testcase->search['params']['branch']['values'] = $branchPairs;
  3685. }
  3686. $this->config->testcase->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->testcase->statusList);
  3687. $this->config->testcase->search['params']['story']['values'] = $this->loadModel('story')->getExecutionStoryPairs($executionID);
  3688. $this->loadModel('search')->setSearchParams($this->config->testcase->search);
  3689. }
  3690. /**
  3691. * 构建搜索任务的表单。
  3692. * Build task search form.
  3693. *
  3694. * @param int $executionID
  3695. * @param int $productID
  3696. * @param array $executions
  3697. * @param int $queryID
  3698. * @param string $actionURL
  3699. * @param bool $cacheSearchFunc 是否缓存构造搜索参数的方法。默认缓存可以提高性能,构造搜索表单时再加载真实值。
  3700. * @access public
  3701. * @return void
  3702. * @param string $module
  3703. */
  3704. public function buildTaskSearchForm($executionID, $executions, $queryID, $actionURL, $module = 'task', $cacheSearchFunc = true)
  3705. {
  3706. $searchConfig = $this->config->execution->search;
  3707. if($cacheSearchFunc)
  3708. {
  3709. $this->cacheSearchFunc($module, __METHOD__, func_get_args());
  3710. return $searchConfig;
  3711. }
  3712. $searchConfig['module'] = $module;
  3713. $searchConfig['actionURL'] = $actionURL;
  3714. $searchConfig['queryID'] = $queryID;
  3715. $showAll = empty($executionID) && empty($executions);
  3716. if($showAll)
  3717. {
  3718. $executions = $this->getPairs(0, 'all', "nocode,noprefix,multiple");
  3719. $executionID = empty($executions) ? 0 : current(array_keys($executions));
  3720. }
  3721. $execution = $this->getByID($executionID);
  3722. $searchConfig['params']['story']['values'] = $this->loadModel('story')->getExecutionStoryPairs($executionID, 0, 'all', '', 'full', 'unclosed', 'story', false);
  3723. if($module == 'task')
  3724. {
  3725. $searchConfig['onMenuBar'] = 'yes';
  3726. if(!$execution->multiple) unset($searchConfig['fields']['execution']);
  3727. }
  3728. elseif($module == 'projectTask')
  3729. {
  3730. unset($searchConfig['fields']['project']);
  3731. unset($searchConfig['fields']['module']);
  3732. }
  3733. if(isset($execution->type) && $execution->type == 'project')
  3734. {
  3735. unset($searchConfig['fields']['project']);
  3736. if(isset($searchConfig['fields']['execution'])) $searchConfig['params']['execution']['values'] = array('' => '') + $executions;
  3737. }
  3738. else
  3739. {
  3740. if(isset($searchConfig['fields']['execution'])) $searchConfig['params']['execution']['values'] = $showAll ? $executions : array(''=>'', $executionID => zget($executions, $executionID, ''), 'all' => $this->lang->execution->allExecutions);
  3741. }
  3742. if(isset($searchConfig['fields']['project']))
  3743. {
  3744. $projects = $this->loadModel('project')->getPairsByProgram();
  3745. $searchConfig['params']['project']['values'] = $projects + array('all' => $this->lang->project->allProjects);
  3746. }
  3747. if(isset($searchConfig['fields']['module']))
  3748. {
  3749. $showAllModule = $this->config->execution->task->allModule ?? '';
  3750. $searchConfig['params']['module']['values'] = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, $showAllModule ? 'allModule' : '');
  3751. }
  3752. $this->loadModel('search')->setSearchParams($searchConfig);
  3753. return $searchConfig;
  3754. }
  3755. /**
  3756. * 获取看板的任务卡片数据。
  3757. * Get the Kanban task card data.
  3758. *
  3759. * @param int $executionID
  3760. * @param string $orderBy
  3761. * @param object|null $pager
  3762. * @param array $excludeTasks
  3763. * @access public
  3764. * @return array
  3765. */
  3766. public function getKanbanTasks($executionID, $orderBy = 'status_asc, id_desc', $excludeTasks = array(), $pager = null)
  3767. {
  3768. $excludeTasks = array_filter($excludeTasks);
  3769. $tasks = $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName')
  3770. ->from(TABLE_TASK)->alias('t1')
  3771. ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
  3772. ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account')
  3773. ->where('t1.execution')->eq($executionID)
  3774. ->andWhere('t1.deleted')->eq(0)
  3775. ->beginIF(!$this->cookie->showParent)->andWhere('t1.isParent')->ne('1')->fi()
  3776. ->beginIF($excludeTasks)->andWhere('t1.id')->notIN($excludeTasks)->fi()
  3777. ->orderBy($orderBy)
  3778. ->page($pager)
  3779. ->fetchAll('id');
  3780. if($tasks) return $this->loadModel('task')->processTasks($tasks);
  3781. return array();
  3782. }
  3783. /**
  3784. * 获取需求看板视图的分组数据。
  3785. * Get group data for stories in Kanban view.
  3786. *
  3787. * @param array $stories
  3788. * @param array $tasks
  3789. * @param array $bugs
  3790. * @access public
  3791. * @return array
  3792. * @param string $type
  3793. */
  3794. public function getKanbanGroupData($stories, $tasks = array(), $bugs = array(), $type = 'story')
  3795. {
  3796. $kanbanGroup = array();
  3797. if($type == 'story') $kanbanGroup = $stories;
  3798. foreach($tasks as $task)
  3799. {
  3800. $groupKey = $type == 'story' ? $task->storyID : $task->$type;
  3801. $status = $task->status;
  3802. if(!empty($groupKey) and (($type == 'story' and isset($stories[$groupKey])) or $type != 'story'))
  3803. {
  3804. if($type == 'assignedTo' and $groupKey == 'closed')
  3805. {
  3806. $closedTasks[$groupKey][] = $task;
  3807. }
  3808. else
  3809. {
  3810. if(!isset($kanbanGroup[$groupKey])) $kanbanGroup[$groupKey] = new stdclass();
  3811. $kanbanGroup[$groupKey]->tasks[$status][] = $task;
  3812. }
  3813. }
  3814. else
  3815. {
  3816. $noKeyTasks[$status][] = $task;
  3817. }
  3818. }
  3819. foreach($bugs as $bug)
  3820. {
  3821. $groupKey = $type == 'finishedBy' ? $bug->resolvedBy : $bug->$type;
  3822. $status = $bug->status == 'active' ? 'wait' : $bug->status;
  3823. if($status == 'resolved') $status = $bug->resolution == 'postponed' ? 'cancel' : 'done';
  3824. if(!empty($groupKey) and (($type == 'story' and isset($stories[$groupKey])) or $type != 'story'))
  3825. {
  3826. if($type == 'assignedTo' and $groupKey == 'closed')
  3827. {
  3828. $closedBugs[$groupKey][] = $bug;
  3829. }
  3830. else
  3831. {
  3832. if(!isset($kanbanGroup[$groupKey])) $kanbanGroup[$groupKey] = new stdclass();
  3833. $kanbanGroup[$groupKey]->bugs[$status][] = $bug;
  3834. }
  3835. }
  3836. else
  3837. {
  3838. $noKeyBugs[$status][] = $bug;
  3839. }
  3840. }
  3841. $kanbanGroup['closed'] = new stdclass();
  3842. if(isset($closedTasks)) $kanbanGroup['closed']->tasks = $closedTasks;
  3843. if(isset($closedBugs)) $kanbanGroup['closed']->bugs = $closedBugs;
  3844. $kanbanGroup['nokey'] = new stdclass();
  3845. if(isset($noKeyTasks)) $kanbanGroup['nokey']->tasks = $noKeyTasks;
  3846. if(isset($noKeyBugs)) $kanbanGroup['nokey']->bugs = $noKeyBugs;
  3847. return $kanbanGroup;
  3848. }
  3849. /**
  3850. * 保存看板数据。
  3851. * Save Kanban Data.
  3852. *
  3853. * @param int $executionID
  3854. * @param array $kanbanDataList
  3855. * @access public
  3856. * @return void
  3857. */
  3858. public function saveKanbanData($executionID, $kanbanDataList)
  3859. {
  3860. $data = array();
  3861. foreach($kanbanDataList as $type => $kanbanData) $data[$type] = array_keys($kanbanData);
  3862. $this->loadModel('setting')->setItem("null.execution.kanban.execution$executionID", json_encode($data));
  3863. }
  3864. /**
  3865. * 获取上一个看板的数据。
  3866. * Get the data from the previous Kanban.
  3867. *
  3868. * @param int $executionID
  3869. * @access public
  3870. * @return array|null
  3871. */
  3872. public function getPrevKanban($executionID)
  3873. {
  3874. $prevKanbans = $this->loadModel('setting')->getItem("owner=null&module=execution&section=kanban&key=execution$executionID");
  3875. return json_decode($prevKanbans, true);
  3876. }
  3877. /**
  3878. * 获取看板的设置。
  3879. * Get kanban setting.
  3880. *
  3881. * @access public
  3882. * @return object
  3883. */
  3884. public function getKanbanSetting()
  3885. {
  3886. $allCols = '1';
  3887. $showOption = '0';
  3888. if(isset($this->config->execution->kanbanSetting->allCols)) $allCols = $this->config->execution->kanbanSetting->allCols;
  3889. $colorList = $this->config->execution->kanbanSetting->colorList;
  3890. if(!is_array($colorList)) $colorList = json_decode($colorList, true);
  3891. $kanbanSetting = new stdclass();
  3892. $kanbanSetting->allCols = $allCols;
  3893. $kanbanSetting->showOption = $showOption;
  3894. $kanbanSetting->colorList = $colorList;
  3895. return $kanbanSetting;
  3896. }
  3897. /**
  3898. * 获取看板列的列表。
  3899. * Get the list of kanban columns.
  3900. *
  3901. * @param object $kanbanSetting
  3902. * @access public
  3903. * @return array
  3904. */
  3905. public function getKanbanColumns($kanbanSetting)
  3906. {
  3907. $kanbanColumns = array('wait', 'doing', 'pause', 'done');
  3908. if(!empty($kanbanSetting->allCols)) array_push($kanbanColumns, 'cancel', 'closed');
  3909. return $kanbanColumns;
  3910. }
  3911. /**
  3912. * 获取状态和方法的映射关系,此关系决定了看板内容能否从一个泳道拖动到另一个泳道,以及拖动后执行什么方法。
  3913. * Get the mapping between state and method. This relationship determines whether kanban content can be dragged from one lane
  3914. * to another, and what method is executed after dragging.
  3915. *
  3916. * 映射关系的基本格式为 map[$mode][$fromStatus][$toStatus] = $methodName。
  3917. * The basic format of the mapping relationship is map[$mode][$fromStatus][$toStatus] = $methodName.
  3918. *
  3919. * @param string $mode 看板内容类型,可选值 task|bug The content mode of kanban, should be task or bug.
  3920. * @param string $fromStatus 拖动内容的来源泳道 The origin lane the content dragged from.
  3921. * @param string $toStatus 拖动内容的目标泳道 The destination lane the content dragged to.
  3922. * @param string $methodName 拖动到目标泳道后执行的方法名 The method to execute after dragged the content.
  3923. *
  3924. * 例如 map['task']['doing']['done'] = 'close' 表示:任务(task)看板从进行中(doing)泳道拖动到已完成(done)泳道时,执行关闭(close)方法。
  3925. * For example, map['task']['doing']['done'] = 'close' means: when the task kanban is dragged from the doing lane to the done lane,
  3926. * execute the close method.
  3927. *
  3928. * @param object $kanbanSetting This param is used in the biz version, don't remove it.
  3929. * @access public
  3930. * @return string
  3931. */
  3932. public function getKanbanStatusMap($kanbanSetting)
  3933. {
  3934. $statusMap = array();
  3935. if(common::hasPriv('task', 'start')) $statusMap['task']['wait']['doing'] = 'start';
  3936. if(common::hasPriv('task', 'pause')) $statusMap['task']['doing']['pause'] = 'pause';
  3937. if(common::hasPriv('task', 'finish'))
  3938. {
  3939. $statusMap['task']['wait']['done'] = 'finish';
  3940. $statusMap['task']['doing']['done'] = 'finish';
  3941. $statusMap['task']['pause']['done'] = 'finish';
  3942. }
  3943. if(common::hasPriv('task', 'cancel'))
  3944. {
  3945. $statusMap['task']['wait']['cancel'] = 'cancel';
  3946. $statusMap['task']['pause']['cancel'] = 'cancel';
  3947. }
  3948. if(common::hasPriv('task', 'activate'))
  3949. {
  3950. $statusMap['task']['pause']['doing'] = 'activate';
  3951. $statusMap['task']['done']['doing'] = 'activate';
  3952. $statusMap['task']['cancel']['doing'] = 'activate';
  3953. $statusMap['task']['closed']['doing'] = 'activate';
  3954. }
  3955. if(common::hasPriv('task', 'close'))
  3956. {
  3957. $statusMap['task']['done']['closed'] = 'close';
  3958. $statusMap['task']['cancel']['closed'] = 'close';
  3959. }
  3960. if(common::hasPriv('bug', 'resolve'))
  3961. {
  3962. $statusMap['bug']['wait']['done'] = 'resolve';
  3963. $statusMap['bug']['wait']['cancel'] = 'resolve';
  3964. }
  3965. if(common::hasPriv('bug', 'close'))
  3966. {
  3967. $statusMap['bug']['done']['closed'] = 'close';
  3968. $statusMap['bug']['cancel']['closed'] = 'close';
  3969. }
  3970. if(common::hasPriv('bug', 'activate'))
  3971. {
  3972. $statusMap['bug']['done']['wait'] = 'activate';
  3973. $statusMap['bug']['cancel']['wait'] = 'activate';
  3974. $statusMap['bug']['closed']['wait'] = 'activate';
  3975. }
  3976. return $statusMap;
  3977. }
  3978. /**
  3979. * 获取看板状态列表。
  3980. * Get status list of kanban.
  3981. *
  3982. * @param object $kanbanSetting This param is used in the biz version, don't remove it.
  3983. * @access public
  3984. * @return array
  3985. */
  3986. public function getKanbanStatusList($kanbanSetting)
  3987. {
  3988. return $this->lang->task->statusList;
  3989. }
  3990. /**
  3991. * 获取看板颜色列表。
  3992. * Get color list of kanban.
  3993. *
  3994. * @param object $kanbanSetting
  3995. * @access public
  3996. * @return array
  3997. */
  3998. public function getKanbanColorList($kanbanSetting)
  3999. {
  4000. return $kanbanSetting->colorList;
  4001. }
  4002. /**
  4003. * 构建燃尽图数据。
  4004. * Build burn data.
  4005. *
  4006. * @param int $executionID
  4007. * @param array $dateList
  4008. * @param string $burnBy left|estimate|storyPoint
  4009. * @param string $executionEnd
  4010. * @access public
  4011. * @return array
  4012. */
  4013. public function buildBurnData($executionID, $dateList, $burnBy = 'left', $executionEnd = '')
  4014. {
  4015. $this->loadModel('report');
  4016. $burnBy = $burnBy ? $burnBy : 'left';
  4017. $sets = $this->getBurnDataFlot($executionID, $burnBy, false, $dateList);
  4018. $firstBurn = empty($sets) ? 0 : reset($sets);
  4019. $firstTime = !empty($firstBurn->$burnBy) ? $firstBurn->$burnBy : 0;
  4020. if(!$firstTime && !empty($firstBurn->value)) $firstTime = $firstBurn->value;
  4021. if($firstTime == 'null') $firstTime = 0;
  4022. /* If the $executionEnd is passed, the guide should end of execution. */
  4023. $days = $executionEnd ? array_search($executionEnd, $dateList) : count($dateList) - 1;
  4024. $rate = $days ? $firstTime / $days : '';
  4025. $baseline = array();
  4026. foreach($dateList as $i => $date)
  4027. {
  4028. $value = ($i > $days ? 0 : round(($days - $i) * (float)$rate, 3));
  4029. $baseline[] = $value;
  4030. }
  4031. $chartData['labels'] = $this->report->convertFormat($dateList, DT_DATE5);
  4032. $chartData['burnLine'] = $this->report->createSingleJSON($sets, $dateList);
  4033. $chartData['baseLine'] = $baseline;
  4034. $execution = $this->getByID($executionID);
  4035. /*
  4036. * 1. Execution status is not closed and suspended, end date less than today;
  4037. * 2. Execution status is closed, end date less than closed date;
  4038. * 3. Execution status is suspended, end date less than suspended date;
  4039. * Processing burn down chart Information.
  4040. */
  4041. $endDate = helper::today();
  4042. if($execution->status == 'closed') $endDate = empty($execution->closedDate) ? '' : substr($execution->closedDate, 0, 10);
  4043. if($execution->status == 'suspended') $endDate = $execution->suspendedDate;
  4044. if($endDate > $execution->end)
  4045. {
  4046. $delaySets = $this->getBurnDataFlot($executionID, $burnBy, true, $dateList);
  4047. $chartData['delayLine'] = $this->report->createSingleJSON($delaySets, $dateList);
  4048. }
  4049. return $chartData;
  4050. }
  4051. /**
  4052. * 在树状图中填充任务。
  4053. * Fill tasks in tree.
  4054. * @param object $tree
  4055. * @param int $executionID
  4056. * @access public
  4057. * @return object
  4058. * @param object $node
  4059. */
  4060. public function fillTasksInTree($node, $executionID)
  4061. {
  4062. static $taskGroups = array();
  4063. if(empty($taskGroups) && !$this->cookie->showStory) $taskGroups = $this->executionTao->getTaskGroups($executionID);
  4064. if(!empty($node->children))
  4065. {
  4066. foreach($node->children as $i => &$child)
  4067. {
  4068. $child = $this->fillTasksInTree((object)$child, $executionID);
  4069. /* Remove no children node. */
  4070. if($child->type != 'story' && $child->type != 'task' && empty($child->children)) unset($node->children[$i]);
  4071. }
  4072. }
  4073. if(!isset($node->id)) $node->id = 0;
  4074. if($node->type == 'story')
  4075. {
  4076. static $storyGroups;
  4077. if(empty($storyGroups))
  4078. {
  4079. if($this->config->vision == 'lite') $execution = $this->getById($executionID);
  4080. $stories = $this->loadModel('story')->getListByProject(isset($execution->project) ? $execution->project : $executionID);
  4081. $storyGroups = array();
  4082. foreach($stories as $story) $storyGroups[$story->product][$story->module][$story->id] = $story;
  4083. }
  4084. $node = $this->executionTao->processStoryNode($node, $storyGroups, $taskGroups, $executionID);
  4085. }
  4086. elseif($node->type == 'task')
  4087. {
  4088. $node = $this->executionTao->processTaskNode($node, $taskGroups);
  4089. }
  4090. elseif($node->type == 'product')
  4091. {
  4092. $node->title = $node->name;
  4093. if(isset($node->children[0]) && empty($node->children[0]->children)) array_shift($node->children);
  4094. }
  4095. $node->actions = false;
  4096. if(!empty($node->children)) $node->children = array_values($node->children);
  4097. return $node;
  4098. }
  4099. /**
  4100. * 通过产品的ID列表获取计划数据。
  4101. * Get plan data from the ID list of the product.
  4102. *
  4103. * @param array $productID
  4104. * @param string $param withMainPlan|skipParent|unexpired|noclosed|sortedByDate
  4105. * @param int $executionID
  4106. * @return array
  4107. * @param mixed[] $productIdList
  4108. */
  4109. public function getPlans($productIdList, $param = '', $executionID = 0)
  4110. {
  4111. $param = strtolower($param);
  4112. $branchIdList = strpos($param, 'withmainplan') !== false ? array(BRANCH_MAIN => BRANCH_MAIN) : array();
  4113. $branchGroups = $this->getBranchByProduct($productIdList, $executionID, 'noclosed');
  4114. foreach($branchGroups as $branches)
  4115. {
  4116. foreach($branches as $branchID => $branchName) $branchIdList[] = $branchID;
  4117. }
  4118. $branchQuery = '(';
  4119. if(!empty($branchIdList))
  4120. {
  4121. $branchCount = count($branchIdList);
  4122. foreach($branchIdList as $index => $branchID)
  4123. {
  4124. $branchQuery .= "FIND_IN_SET('$branchID', branch)";
  4125. if($index < $branchCount - 1) $branchQuery .= ' OR ';
  4126. }
  4127. }
  4128. else
  4129. {
  4130. $branchQuery .= "FIND_IN_SET('0', branch)";
  4131. }
  4132. $branchQuery .= " OR branch = '')";
  4133. $plans = $this->dao->select('t1.id,t1.title,t1.product,t1.parent,t1.begin,t1.end,t1.branch,t2.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1')
  4134. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t2.id=t1.product')
  4135. ->where('t1.product')->in($productIdList)
  4136. ->andWhere('t1.deleted')->eq(0)
  4137. ->andWhere($branchQuery)
  4138. ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('t1.end')->ge(helper::today())->fi()
  4139. ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi()
  4140. ->orderBy('t1.begin desc, t1.id desc')
  4141. ->fetchAll('id');
  4142. return $this->processProductPlans($plans, $param);
  4143. }
  4144. /**
  4145. * 处理产品计划的数据。
  4146. * Process product planning data.
  4147. *
  4148. * @param array $plans
  4149. * @param string $param withmainplan|skipparent
  4150. * @access public
  4151. * @return array
  4152. */
  4153. public function processProductPlans($plans, $param = '')
  4154. {
  4155. if(strpos($param, 'sortedbydate') !== false)
  4156. {
  4157. $pendPlans = array();
  4158. $normalPlans = array();
  4159. foreach($plans as $plan)
  4160. {
  4161. if($plan->begin == '2030-01-01' && $plan->end == '2030-01-01')
  4162. {
  4163. $pendPlans[$plan->id] = $plan;
  4164. }
  4165. else
  4166. {
  4167. $normalPlans[$plan->id] = $plan;
  4168. }
  4169. }
  4170. $plans = arrayUnion($normalPlans, $pendPlans);
  4171. }
  4172. $plans = $this->loadModel('productplan')->reorder4Children($plans);
  4173. $plans = $this->productplan->relationBranch($plans);
  4174. $productPlans = array();
  4175. foreach($plans as $plan)
  4176. {
  4177. if($plan->parent == '-1' && strpos($param, 'skipparent') !== false) continue;
  4178. if($plan->parent > 0 && isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title;
  4179. $productPlans[$plan->product][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]";
  4180. if($plan->begin == '2030-01-01' && $plan->end == '2030-01-01') $productPlans[$plan->product][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future;
  4181. if($plan->productType != 'normal') $productPlans[$plan->product][$plan->id] = $productPlans[$plan->product][$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main);
  4182. }
  4183. return $productPlans;
  4184. }
  4185. /**
  4186. * 构造树状图的数据。
  4187. * Build tree data.
  4188. *
  4189. * @param array $trees
  4190. * @param bool $hasProduct
  4191. * @param array $gradeGroup
  4192. * @access pubic
  4193. * @return array
  4194. */
  4195. public function buildTree($trees, $hasProduct = true, $gradeGroup = array())
  4196. {
  4197. $treeData = array();
  4198. $canViewTask = common::hasPriv('execution', 'treeTask');
  4199. $canViewStory = common::hasPriv('execution', 'treeStory');
  4200. foreach($trees as $index => $tree)
  4201. {
  4202. $tree = (object)$tree;
  4203. $treeData[$index] = array('className' => 'py-2 cursor-pointer ' . $tree->type);
  4204. $assigedToHtml = !empty($tree->assignedTo) ? ' <span class="user align-bottom"><div class="avatar rounded-full size-xs ml-1 align-' . (strlen(zget($tree, 'avatar')) == 1 ? 'middle primary' : 'sub') . '">' . zget($tree, 'avatar') . '</div> ' . zget($tree, 'avatarAccount') . '</span>' : '';
  4205. switch($tree->type)
  4206. {
  4207. case 'task':
  4208. $label = $this->lang->task->common;
  4209. if($tree->parent > 0 && !$tree->isParent) $label = $this->lang->task->children;
  4210. if($tree->isParent) $label = $this->lang->task->parent;
  4211. $treeData[$index]['url'] = $canViewTask ? helper::createLink('execution', 'treeTask', "taskID={$tree->id}") : '';
  4212. $treeData[$index]['content'] = array(
  4213. 'html' => "<div class='tree-link'><span class='label gray-pale rounded-full align-sub'>{$label}</span><span class='ml-4 align-sub'>{$tree->id}</span><span class='title ml-4 " . ($canViewTask ? 'text-primary' : '') . " align-sub' title='{$tree->title}'>" . $tree->title . '</span>'. $assigedToHtml . '</div>',
  4214. );
  4215. break;
  4216. case 'product':
  4217. $treeData[$index]['content'] = array('html' => "<span class='label rounded-full p-2 gray-outline' title='{$tree->name}'>{$tree->name}</span>");
  4218. break;
  4219. case 'story':
  4220. $this->app->loadLang('story');
  4221. $gradePairs = zget($gradeGroup, $tree->type, array());
  4222. $grade = zget($gradePairs, $tree->grade, $tree->grade);
  4223. $gradeName = ($grade && isset($grade->name)) ? "<span class='label gray-pale rounded-full'>{$grade->name}</span>" : '';
  4224. $treeData[$index]['url'] = $canViewStory ? helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}") : '';
  4225. $treeData[$index]['content'] = array(
  4226. 'html' => "<div class='tree-link'>{$gradeName}<span class='ml-4'>{$tree->storyId}</span><span class='title " . ($canViewStory ? 'text-primary' : '') . " ml-4' title='{$tree->title}'>{$tree->title}</span>" . $assigedToHtml . '</div>',
  4227. );
  4228. break;
  4229. case 'requirement':
  4230. $this->app->loadLang('requirement');
  4231. $gradePairs = zget($gradeGroup, $tree->type, array());
  4232. $grade = zget($gradePairs, $tree->grade, $tree->grade);
  4233. $gradeName = ($grade && isset($grade->name)) ? "<span class='label gray-pale rounded-full'>{$grade->name}</span>" : '';
  4234. $treeData[$index]['url'] = helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}");
  4235. $treeData[$index]['content'] = array(
  4236. 'html' => "<div class='tree-link'>{$gradeName}<span class='ml-4'>{$tree->storyId}</span><span class='title text-primary ml-4' title='{$tree->title}'>{$tree->title}</span>" . $assigedToHtml . '</div>',
  4237. );
  4238. break;
  4239. case 'epic':
  4240. $this->app->loadLang('epic');
  4241. $gradePairs = zget($gradeGroup, $tree->type, array());
  4242. $grade = zget($gradePairs, $tree->grade, $tree->grade);
  4243. $gradeName = ($grade && isset($grade->name)) ? "<span class='label gray-pale rounded-full'>{$grade->name}</span>" : '';
  4244. $treeData[$index]['url'] = helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}");
  4245. $treeData[$index]['content'] = array(
  4246. 'html' => "<div class='tree-link'>{$gradeName}<span class='ml-4'>{$tree->storyId}</span><span class='title text-primary ml-4' title='{$tree->title}'>{$tree->title}</span>" . $assigedToHtml . '</div>',
  4247. );
  4248. break;
  4249. case 'branch':
  4250. $this->app->loadLang('branch');
  4251. $treeData[$index]['content'] = array(
  4252. 'html' => "<span class='label gray-pale rounded-full'>{$this->lang->branch->common}</span><span class='title ml-4' title='{$tree->name}'>{$tree->name}</span>"
  4253. );
  4254. break;
  4255. default:
  4256. $firstClass = $tree->id == 0 ? 'label rounded-full p-2 gray-outline' : '';
  4257. $treeData[$index]['content'] = array('html' => "<span class='{$firstClass} title' title='{$tree->name}'>" . $tree->name . '</span>');
  4258. break;
  4259. }
  4260. if(isset($tree->children))
  4261. {
  4262. if($tree->type == 'task')
  4263. {
  4264. $label = $this->lang->task->common;
  4265. if($tree->parent > 0 && !$tree->isParent) $label = $this->lang->task->children;
  4266. if($tree->isParent) $label = $this->lang->task->parent;
  4267. $treeData[$index]['url'] = $canViewTask ? helper::createLink('execution', 'treeTask', "taskID={$tree->id}") : '';
  4268. $treeData[$index]['content'] = array(
  4269. 'html' => "<div class='tree-link'><span class='label gray-pale rounded-full align-sub'>{$label}</span><span class='ml-4 align-sub'>{$tree->id}</span><span class='title ml-4 " . ($canViewTask ? 'text-primary' : '') . " align-sub' title='{$tree->title}'>" . $tree->title . '</span>'. $assigedToHtml . '</div>',
  4270. );
  4271. }
  4272. $treeData[$index]['items'] = $this->buildTree($tree->children, $hasProduct, $gradeGroup);
  4273. }
  4274. }
  4275. return $treeData;
  4276. }
  4277. /**
  4278. * 更新用户可查看的执行和产品。
  4279. * Update the execution and product that users can view.
  4280. *
  4281. * @param int $executionID
  4282. * @param string $objectType
  4283. * @param array $users
  4284. * @access public
  4285. * @return void
  4286. */
  4287. public function updateUserView($executionID, $objectType = 'sprint', $users = array())
  4288. {
  4289. $this->loadModel('user')->updateUserView(array($executionID), $objectType, $users);
  4290. $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false);
  4291. if(!empty($products)) $this->user->updateUserView(array_keys($products), 'product', $users);
  4292. }
  4293. /**
  4294. * 获取阶段关联的产品。
  4295. * Get the products associated with the stage.
  4296. *
  4297. * @param array $stageIdList
  4298. * @access public
  4299. * @return array
  4300. */
  4301. public function getStageLinkProductPairs($stageIdList = array())
  4302. {
  4303. return $this->dao->select('t1.project, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  4304. ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id')
  4305. ->where('t1.project')->in($stageIdList)
  4306. ->fetchPairs('project', 'name');
  4307. }
  4308. /**
  4309. * 设置阶段的层级和父子关系。
  4310. * Set the level and parent-child relationship of the stage.
  4311. *
  4312. * @param int $executionID
  4313. * @access public
  4314. * @return bool
  4315. */
  4316. public function setTreePath($executionID)
  4317. {
  4318. $execution = $this->fetchByID($executionID);
  4319. if(!$execution) return false;
  4320. $parent = $this->fetchByID($execution->parent);
  4321. if(!$parent) return false;
  4322. $path = array();
  4323. if($parent->type == 'project')
  4324. {
  4325. $path['path'] = ",{$parent->id},{$execution->id},";
  4326. $path['grade'] = 1;
  4327. }
  4328. elseif($parent->type == 'stage')
  4329. {
  4330. $path['path'] = $parent->path . "{$execution->id},";
  4331. $path['grade'] = $parent->grade + 1;
  4332. }
  4333. $this->dao->update(TABLE_PROJECT)->data($path)->where('id')->eq($execution->id)->exec();
  4334. return !dao::isError();
  4335. }
  4336. /**
  4337. * 生成dtable的行数据。
  4338. * Generate row for dtable.
  4339. *
  4340. * @param array $executions
  4341. * @param array $users
  4342. * @param array $avatarList
  4343. * @access public
  4344. * @return array
  4345. */
  4346. public function generateRow($executions, $users, $avatarList)
  4347. {
  4348. $rows = array();
  4349. $this->app->loadConfig('project');
  4350. $this->getLimitedExecution();
  4351. $executionList = array();
  4352. $groupIdList = array();
  4353. foreach($executions as $execution)
  4354. {
  4355. $executionList[$execution->id] = $execution;
  4356. $groupIdList[$execution->workflowGroup] = $execution->workflowGroup;
  4357. }
  4358. $flowActionConditions = array();
  4359. if($this->config->edition != 'open')
  4360. {
  4361. $this->loadModel('flow');
  4362. $this->loadModel('workflowaction');
  4363. foreach($groupIdList as $groupID)
  4364. {
  4365. $flowActions = $this->workflowaction->getList('execution', 'status_desc,order_asc', $groupID);
  4366. foreach($flowActions as $flowAction)
  4367. {
  4368. if(!empty($flowAction->conditions)) $flowActionConditions[$groupID][$flowAction->action] = $flowAction->conditions;
  4369. }
  4370. }
  4371. }
  4372. $canCreateChildStage = commonModel::hasPriv('programplan', 'create');
  4373. $canCreateTask = commonModel::hasPriv('task', 'create');
  4374. $canEditStage = commonModel::hasPriv('programplan', 'edit');
  4375. foreach($executionList as $execution)
  4376. {
  4377. $execution->rawID = $execution->id;
  4378. $execution->isExecution = 1;
  4379. $execution->id = 'pid' . (string)$execution->id;
  4380. $execution->projectID = $execution->project;
  4381. $execution->project = $execution->projectName;
  4382. $execution->rawParent = $execution->parent;
  4383. $execution->parent = (isset($executionList[$execution->parent]) && $execution->parent && $execution->grade > 1) ? 'pid' . (string)$execution->parent : '';
  4384. $execution->hasChild = !empty($execution->isParent);
  4385. $execution->isParent = !empty($execution->isParent) or !empty($execution->tasks);
  4386. $execution->actions = array();
  4387. if(isset($this->config->project->execution->dtable->actionsRule[$execution->projectModel]))
  4388. {
  4389. $canModify = common::canModify('execution', $execution);
  4390. $isStage = in_array($execution->projectModel, array('waterfall', 'waterfallplus', 'ipd'));
  4391. foreach($this->config->project->execution->dtable->actionsRule[$execution->projectModel] as $actionKey)
  4392. {
  4393. $action = array();
  4394. $actions = explode('|', $actionKey);
  4395. foreach($actions as $actionName)
  4396. {
  4397. if(!$canModify && !in_array($actionName, array('edit', 'delete', 'activate'))) continue;
  4398. if($actionName == 'createChildStage' && !$canCreateChildStage) continue;
  4399. if($actionName == 'createTask' && !$canCreateTask) continue;
  4400. if($actionName == 'edit' && $isStage && !$canEditStage) continue;
  4401. if($actionName == 'edit' && !$isStage && !commonModel::hasPriv('execution', 'edit')) continue;
  4402. if(!in_array($actionName, array('createTask', 'createChildStage', 'edit')) && !commonModel::hasPriv('execution', $actionName)) continue;
  4403. $action = array('name' => $actionName, 'disabled' => $this->isClickable($execution, $actionName) ? false : true);
  4404. if($actionName == 'createChildStage' && $action['disabled'] && $execution->type != 'stage') $action['hint'] = $this->lang->programplan->error->notStage;
  4405. if(!$action['disabled']) break;
  4406. if($actionName == 'close' && $execution->status != 'closed') break;
  4407. if(!empty($execution->frozen) && in_array($actionName, array('edit', 'createChildStage', 'delete', 'putoff'))) $action['hint'] = sprintf($this->lang->execution->stageFrozenTip, $this->lang->execution->$actionName);
  4408. }
  4409. if(!empty($action))
  4410. {
  4411. if(!empty($flowActionConditions[$execution->workflowGroup][$action['name']]) && !$action['disabled']) $action['disabled'] = !$this->flow->checkConditions($flowActionConditions[$execution->workflowGroup][$action['name']], $execution);
  4412. $execution->actions[] = $action;
  4413. }
  4414. }
  4415. }
  4416. /* For user's avatar. */
  4417. if($execution->PM)
  4418. {
  4419. $realname = zget($users, $execution->PM);
  4420. if(empty($realname)) continue;
  4421. $execution->PMAccount = $execution->PM;
  4422. $execution->PM = $realname;
  4423. $execution->PMAvatar = zget($avatarList, $execution->PMAccount, '');
  4424. }
  4425. $rows[$execution->id] = $execution;
  4426. /* Append tasks and child stages. */
  4427. if(!empty($execution->tasks)) $rows = $this->appendTasks($execution->tasks, $rows, $users, $avatarList, $canModify);
  4428. }
  4429. if(in_array($this->config->edition, array('max', 'ipd'))) $rows = $this->loadModel('project')->countDeliverable($rows, 'execution');
  4430. return $rows;
  4431. }
  4432. /**
  4433. * 追加任务列表到执行列表。
  4434. * Append tasks to execution list.
  4435. *
  4436. * @param array $tasks
  4437. * @param array $rows
  4438. * @param array $users
  4439. * @param array $avatarList
  4440. * @param bool $canModify
  4441. * @access public
  4442. * @return array
  4443. */
  4444. public function appendTasks($tasks, $rows, $users = array(), $avatarList = array(), $canModify = true)
  4445. {
  4446. $this->loadModel('task');
  4447. $this->app->loadConfig('project');
  4448. $flowActionConditions = array();
  4449. if($this->config->edition != 'open')
  4450. {
  4451. $this->loadModel('flow');
  4452. $flowActions = $this->loadModel('workflowaction')->getList('task');
  4453. foreach($flowActions as $flowAction)
  4454. {
  4455. if(!empty($flowAction->conditions)) $flowActionConditions[$flowAction->action] = $flowAction->conditions;
  4456. }
  4457. }
  4458. foreach($tasks as $task)
  4459. {
  4460. if(!$canModify) continue;
  4461. if($task->status == 'changed')
  4462. {
  4463. if(!commonModel::hasPriv('task', 'confirmStoryChange')) continue;
  4464. if(!common::hasDBPriv($task, 'task', 'confirmStoryChange')) continue;
  4465. $clickable = $this->task->isClickable($task, 'confirmStoryChange');
  4466. $task->actions[] = array('name' => 'confirmStoryChange', 'disabled' => !$clickable);
  4467. $task = $this->task->processConfirmStoryChange($task);
  4468. }
  4469. else
  4470. {
  4471. foreach($this->config->project->execution->dtable->actionsRule['task'] as $action)
  4472. {
  4473. $rawAction = str_replace('Task', '', $action);
  4474. if(!commonModel::hasPriv('task', $rawAction)) continue;
  4475. if(!common::hasDBPriv($task, 'task', $rawAction)) continue;
  4476. $clickable = $this->task->isClickable($task, $rawAction);
  4477. if($clickable && !empty($flowActionConditions[$rawAction])) $clickable = $this->flow->checkConditions($flowActionConditions[$rawAction], $task);
  4478. $action = array('name' => $action);
  4479. if(!$clickable) $action['disabled'] = true;
  4480. $task->actions[] = $action;
  4481. }
  4482. }
  4483. $prefixLabel = "<span class='pri-{$task->pri} mr-1'>" . zget($this->lang->task->priList, $task->pri) . '</span> ';
  4484. if($task->isParent > 0)
  4485. {
  4486. $prefixLabel .= "<span class='label gray-pale rounded-xl mx-1'>{$this->lang->task->parentAB}</span>";
  4487. }
  4488. elseif($task->parent > 0)
  4489. {
  4490. $prefixLabel .= "<span class='label gray-pale rounded-xl mx-1'>{$this->lang->task->childrenAB}</span>";
  4491. }
  4492. $task->prefixLabel = $prefixLabel;
  4493. $task->rawName = $task->name;
  4494. $task->name = $prefixLabel . html::a(helper::createLink('task', 'view', "id={$task->id}"), $task->name);
  4495. $task->rawID = $task->id;
  4496. $task->id = 'tid' . (string)$task->id;
  4497. $task->totalEstimate = $task->estimate;
  4498. $task->totalConsumed = $task->consumed;
  4499. $task->totalLeft = $task->left;
  4500. $task->parent = $task->parent > 0 && isset($tasks[$task->parent]) ? "tid{$task->parent}" : "pid{$task->execution}";
  4501. $task->progress = ($task->consumed + $task->left) == 0 ? 0 : round($task->consumed / ($task->consumed + $task->left), 2) * 100;
  4502. $task->begin = $task->estStarted;
  4503. $task->end = $task->deadline;
  4504. $task->realBegan = $task->realStarted;
  4505. $task->realEnd = $task->finishedDate;
  4506. $task->PM = $task->assignedTo;
  4507. if($task->PM)
  4508. {
  4509. $realname = zget($users, $task->PM);
  4510. if(empty($realname)) continue;
  4511. $task->PMAccount = $task->PM;
  4512. $task->PM = $realname;
  4513. $task->PMAvatar = zget($avatarList, $task->PMAccount, '');
  4514. }
  4515. $task->needConfirm = false;
  4516. if(!empty($task->storyStatus) && $task->storyStatus == 'active' && !in_array($task->status, array('closed', 'cancel')) && $task->latestStoryVersion > $task->storyVersion)
  4517. {
  4518. $task->needConfirm = true;
  4519. $task->rawStatus = $task->status;
  4520. $task->status = 'changed';
  4521. }
  4522. $rows[] = $task;
  4523. }
  4524. return $rows;
  4525. }
  4526. /*
  4527. * 构建执行列表的搜索表单。
  4528. * Build search form for execution list.
  4529. *
  4530. * @param int $queryID
  4531. * @param string $actionURL
  4532. * @return void
  4533. * */
  4534. /**
  4535. * @param int $queryID
  4536. * @param string $actionURL
  4537. */
  4538. public function buildSearchForm($queryID, $actionURL)
  4539. {
  4540. $this->config->execution->all->search['queryID'] = $queryID;
  4541. $this->config->execution->all->search['actionURL'] = $actionURL;
  4542. $projectPairs = array(0 => '');
  4543. $projectPairs += $this->loadModel('project')->getPairsByProgram(0, 'all', false, 'order_asc', '', '', 'multiple');
  4544. $this->config->execution->all->search['params']['project']['values'] = $projectPairs + array('all' => $this->lang->execution->allProject);
  4545. $this->loadModel('search')->setSearchParams($this->config->execution->all->search);
  4546. }
  4547. /*
  4548. * 不启用迭代的项目,创建默认迭代。
  4549. * Create default sprint for project which is not using sprint.
  4550. *
  4551. * @param int $projectID
  4552. * @return int
  4553. * */
  4554. /**
  4555. * @param int $projectID
  4556. */
  4557. public function createDefaultSprint($projectID)
  4558. {
  4559. $project = $this->fetchByID($projectID);
  4560. $executionData = new stdclass();
  4561. $executionData->project = $projectID;
  4562. $executionData->name = $project->name;
  4563. $executionData->grade = 1;
  4564. $executionData->storyType = $project->storyType;
  4565. $executionData->begin = $project->begin;
  4566. $executionData->end = $project->end;
  4567. $executionData->status = 'wait';
  4568. $executionData->type = $project->model == 'kanban' ? 'kanban' : 'sprint';
  4569. $executionData->days = $project->days;
  4570. $executionData->team = $project->team;
  4571. $executionData->desc = $project->desc;
  4572. $executionData->acl = 'open';
  4573. $executionData->PO = $project->PO;
  4574. $executionData->QD = $project->QD;
  4575. $executionData->PM = $project->PM;
  4576. $executionData->RD = $project->RD;
  4577. $executionData->multiple = '0';
  4578. $executionData->whitelist = '';
  4579. $executionData->plans = array();
  4580. $executionData->hasProduct = $project->hasProduct;
  4581. $executionData->openedBy = $this->app->user->account;
  4582. $executionData->openedDate = helper::now();
  4583. $executionData->parent = $projectID;
  4584. $executionData->isTpl = $project->isTpl;
  4585. if($project->code) $executionData->code = $project->code;
  4586. $projectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchAll();
  4587. foreach($projectProducts as $projectProduct)
  4588. {
  4589. if($projectProduct->product) $executionData->products[] = $projectProduct->product;
  4590. if($projectProduct->branch) $executionData->branch[] = $projectProduct->branch;
  4591. if($projectProduct->plan)
  4592. {
  4593. $plans = explode(',', trim($projectProduct->plan, ','));
  4594. $executionData->plans[$projectProduct->product] = isset($executionData->plans[$projectProduct->product]) ? array_merge($executionData->plans[$projectProduct->product], $plans) : $plans;
  4595. }
  4596. }
  4597. $this->config->execution->create->requiredFields = '';
  4598. $executionID = $this->create($executionData, array($this->app->user->account));
  4599. if($project->model == 'kanban')
  4600. {
  4601. $execution = $this->fetchById($executionID);
  4602. $this->loadModel('kanban')->createRDKanban($execution);
  4603. }
  4604. $this->linkStories($executionID);
  4605. return $executionID;
  4606. }
  4607. /**
  4608. * 同步无迭代项目下的影子迭代。
  4609. * Sync no multiple project to sprint.
  4610. *
  4611. * @param int $projectID
  4612. * @access public
  4613. * @return int
  4614. */
  4615. public function syncNoMultipleSprint($projectID)
  4616. {
  4617. $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch();
  4618. if(empty($project)) return 0;
  4619. $postData = new stdclass();
  4620. $postData->project = $projectID;
  4621. $postData->name = $project->name;
  4622. $postData->storyType = $project->storyType;
  4623. $postData->begin = $project->begin;
  4624. $postData->end = $project->end;
  4625. $postData->realBegan = $project->realBegan ? $project->realBegan : null;
  4626. $postData->realEnd = $project->realEnd ? $project->realEnd : null;
  4627. $postData->days = $project->days;
  4628. $postData->team = $project->team;
  4629. $postData->PO = $project->PO;
  4630. $postData->QD = $project->QD;
  4631. $postData->PM = $project->PM;
  4632. $postData->RD = $project->RD;
  4633. $postData->status = $project->status;
  4634. $postData->acl = 'open';
  4635. $postData->products = '';
  4636. $postData->code = empty($project->code) ? $project->name : $project->code;
  4637. $postData->uid = '';
  4638. /* Handle extend fields. */
  4639. $extendFields = $this->loadModel('project')->getFlowExtendFields($projectID);
  4640. foreach($extendFields as $field) $_POST[$field->field] = $project->{$field->field};
  4641. if(isset($this->config->setCode) and $this->config->setCode == 1) $postData->code = $project->code;
  4642. $updateProductsData = new stdclass();
  4643. $updateProductsData->products = array();
  4644. $updateProductsData->branch = array();
  4645. $updateProductsData->plans = array();
  4646. $projectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchAll();
  4647. foreach($projectProducts as $projectProduct)
  4648. {
  4649. $updateProductsData->products[] = $projectProduct->product;
  4650. $updateProductsData->branch[] = $projectProduct->branch;
  4651. if($projectProduct->plan) $updateProductsData->plans[$projectProduct->product] = explode(',', trim($projectProduct->plan, ','));
  4652. }
  4653. $teamMembers = $this->dao->select('*')->from(TABLE_TEAM)->where('type')->eq('project')->andWhere('root')->eq($projectID)->fetchPairs('account', 'account');
  4654. $postData->teamMembers = array_values($teamMembers);
  4655. /* Update execution and linked product. */
  4656. $executionID = (int)$this->dao->select('*')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('type')->in('sprint,kanban')->andWhere('multiple')->eq(0)->fetch('id');
  4657. if($executionID)
  4658. {
  4659. $this->config->execution->edit->requiredFields = ''; // 从项目同步过来的字段,不需要验证必填。
  4660. $this->update($executionID, $postData);
  4661. $this->updateProducts($executionID, (array)$updateProductsData);
  4662. }
  4663. return $executionID;
  4664. }
  4665. /**
  4666. * 根据状态创建和设置迭代用于更新迭代的状态。
  4667. * Build execution object by status.
  4668. *
  4669. * @param string $status
  4670. * @access public
  4671. * @return object
  4672. */
  4673. public function buildExecutionByStatus($status)
  4674. {
  4675. $execution = new stdclass();
  4676. $execution->status = $status;
  4677. $execution->lastEditedBy = $this->app->user->account;
  4678. $execution->lastEditedDate = helper::now();
  4679. if($status == 'wait')
  4680. {
  4681. $execution->closedBy = '';
  4682. $execution->canceledBy = '';
  4683. $execution->closedDate = null;
  4684. $execution->canceledDate = null;
  4685. $execution->realBegan = null;
  4686. $execution->realEnd = null;
  4687. $execution->suspendedDate = null;
  4688. }
  4689. elseif($status == 'doing')
  4690. {
  4691. $execution->realBegan = helper::today();
  4692. $execution->closedBy = '';
  4693. $execution->canceledBy = '';
  4694. }
  4695. elseif($status == 'suspended')
  4696. {
  4697. $execution->suspendedDate = helper::now();
  4698. $execution->closedBy = '';
  4699. $execution->closedDate = null;
  4700. $execution->realEnd = null;
  4701. }
  4702. elseif($status == 'closed')
  4703. {
  4704. $execution->closedBy = $this->app->user->account;
  4705. $execution->realEnd = helper::today();
  4706. $execution->closedDate = helper::now();
  4707. }
  4708. return $execution;
  4709. }
  4710. /**
  4711. * 给执行列表重新排序。
  4712. * Reset execution orders.
  4713. *
  4714. * @param array $executions
  4715. * @param array $parentExecutions
  4716. * @param array $childExecutions
  4717. * @access public
  4718. * @return array
  4719. */
  4720. public function resetExecutionSorts($executions, $parentExecutions = array(), $childExecutions = array())
  4721. {
  4722. if(empty($executions)) return array();
  4723. if(empty($parentExecutions) && empty($childExecutions))
  4724. {
  4725. foreach($executions as $execution)
  4726. {
  4727. if($execution->grade == 1) $parentExecutions[$execution->id] = $execution;
  4728. if($execution->grade > 1 && $execution->parent) $childExecutions[$execution->parent][$execution->id] = $execution;
  4729. }
  4730. }
  4731. if(empty($parentExecutions)) return $executions;
  4732. $sortedExecutions = array();
  4733. foreach($parentExecutions as $executionID => $execution)
  4734. {
  4735. if(!isset($sortedExecutions[$executionID]) and isset($executions[$executionID])) $sortedExecutions[$executionID] = $executions[$executionID];
  4736. if(!empty($childExecutions[$executionID])) $sortedExecutions += $this->resetExecutionSorts($executions, $childExecutions[$executionID], $childExecutions);
  4737. }
  4738. return $sortedExecutions;
  4739. }
  4740. /**
  4741. * 删除一个执行。
  4742. * Delete an execution.
  4743. *
  4744. * @param string $table
  4745. * @param int $executionID
  4746. * @access public
  4747. * @return void
  4748. */
  4749. public function delete($table, $executionID)
  4750. {
  4751. $this->dao->update($table)->set('deleted')->eq(1)->where('id')->eq($executionID)->exec();
  4752. $this->loadModel('action')->create('execution', $executionID, 'deleted', '' , 1);
  4753. }
  4754. /*
  4755. * 通过父级id获取同级的所有执行类型。
  4756. * Get all execution types of the same level through the parent id.
  4757. *
  4758. * @param int $parentID
  4759. * @access public
  4760. * @return void
  4761. */
  4762. public function getSiblingsTypeByParentID($parentID)
  4763. {
  4764. return $this->dao->select('DISTINCT type')->from(TABLE_EXECUTION)->where('deleted')->eq(0)->andWhere('parent')->eq($parentID)->fetchPairs('type');
  4765. }
  4766. /**
  4767. * 通过 ID 列表获取执行键对。
  4768. * Get execution pairs by id list.
  4769. *
  4770. * @param array $executionIdList
  4771. * @param string $type
  4772. * @param string $orderBy
  4773. * @access public
  4774. * @return array
  4775. */
  4776. public function getPairsByList($executionIdList, $type = '', $orderBy = 'id_asc')
  4777. {
  4778. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionPairs();
  4779. return $this->dao->select('id,name')->from(TABLE_EXECUTION)
  4780. ->where('id')->in($executionIdList)
  4781. ->beginIF(!empty($type))->andWhere('type')->in($type)->fi()
  4782. ->orderBy($orderBy)
  4783. ->fetchPairs();
  4784. }
  4785. /**
  4786. * 通过执行ID列表获取执行的子级ID列表组。
  4787. * Get the children id list of the execution group by the parent id list.
  4788. *
  4789. * @param array $executionIdList
  4790. * @access public
  4791. * @return array
  4792. * @param mixed[] $parentIdList
  4793. */
  4794. public function getChildIdGroup($parentIdList)
  4795. {
  4796. return $this->dao->select('id,parent')->from(TABLE_EXECUTION)->where('parent')->in($parentIdList)->andWhere('type')->in('stage,kanban,sprint')->fetchGroup('parent', 'id');
  4797. }
  4798. /*
  4799. * 获取旧页面1.5级下拉。
  4800. * Get execution switcher.
  4801. *
  4802. * @param int $executionID
  4803. * @param string $currentModule
  4804. * @param string $currentMethod
  4805. * @access public
  4806. * @return string
  4807. */
  4808. /**
  4809. * @param int $executionID
  4810. * @param string $currentModule
  4811. * @param string $currentMethod
  4812. */
  4813. public function getSwitcher($executionID, $currentModule, $currentMethod)
  4814. {
  4815. if($currentModule == 'execution' and in_array($currentMethod, array('index', 'all', 'batchedit', 'create'))) return '';
  4816. $currentExecutionName = $this->lang->execution->common;
  4817. if($executionID)
  4818. {
  4819. $currentExecution = $this->getById($executionID);
  4820. $currentExecutionName = $currentExecution->name;
  4821. }
  4822. if($this->app->viewType == 'mhtml' and $executionID)
  4823. {
  4824. $output = html::a(helper::createLink('execution', 'index'), $this->lang->executionCommon) . $this->lang->hyphen;
  4825. $output .= "<a id='currentItem' href=\"javascript:showSearchMenu('execution', '$executionID', '$currentModule', '$currentMethod', '')\">{$currentExecutionName} <span class='icon-caret-down'></span></a><div id='currentItemDropMenu' class='hidden affix enter-from-bottom layer'></div>";
  4826. return $output;
  4827. }
  4828. $dropMenuLink = helper::createLink('execution', 'ajaxGetDropMenu', "executionID=$executionID&module=$currentModule&method=$currentMethod&extra=");
  4829. $output = "<div class='btn-group header-btn' id='swapper'><button data-toggle='dropdown' type='button' class='btn' id='currentItem' title='{$currentExecutionName}'><span class='text'>{$currentExecutionName}</span> <span class='caret' style='margin-bottom: -1px'></span></button><div id='dropMenu' class='dropdown-menu search-list' data-ride='dropmenu' data-url='$dropMenuLink'>";
  4830. $output .= '<div class="input-control search-box has-icon-left has-icon-right search-example"><input type="search" class="form-control search-input" /><label class="input-control-icon-left search-icon"><i class="icon icon-search"></i></label><a class="input-control-icon-right search-clear-btn"><i class="icon icon-close icon-sm"></i></a></div>';
  4831. $output .= "</div></div>";
  4832. return $output;
  4833. }
  4834. /**
  4835. * 获取执行的收件人和抄送人列表。
  4836. * Get to list and cc list of the execution.
  4837. *
  4838. * @param object $execution
  4839. * @access public
  4840. * @return array
  4841. */
  4842. public function getToAndCcList($execution)
  4843. {
  4844. $teamMembers = $this->loadModel('user')->getTeamMemberPairs($execution->id, 'execution');
  4845. $whitelist = !empty($execution->whitelist) ? explode(',', $execution->whitelist) : array();
  4846. $whitelist = array_filter($whitelist);
  4847. $toList = $ccList = '';
  4848. $toList = array_merge(array_keys($teamMembers), $whitelist);
  4849. return array(implode(',', $toList), $ccList);
  4850. }
  4851. /**
  4852. * 检查迭代是否可以关闭。
  4853. * Check if the execution can be closed.
  4854. *
  4855. * @param object $execution
  4856. * @access public
  4857. * @return bool
  4858. */
  4859. public function canCloseByDeliverable($execution)
  4860. {
  4861. $stageType = $execution->type == 'stage' ? $execution->attribute : $execution->type;
  4862. $project = $this->loadModel('project')->fetchByID((int)$execution->project);
  4863. $deliverables = $this->dao->select('t1.id')->from(TABLE_DELIVERABLE)->alias('t1')
  4864. ->leftJoin(TABLE_DELIVERABLESTAGE)->alias('t2')->on('t1.id = t2.deliverable')
  4865. ->where('t1.deleted')->eq('0')
  4866. ->andWhere('t1.workflowGroup')->eq((int)$project->workflowGroup)
  4867. ->andWhere('t1.status')->eq('enabled')
  4868. ->andWhere('t2.required')->eq('1')
  4869. ->andWhere('t2.stage')->eq($stageType)
  4870. ->fetchPairs();
  4871. if(empty($deliverables)) return true;
  4872. $countExecutionDeliverables = $this->dao->select('count(*) as count')->from(TABLE_PROJECTDELIVERABLE)->where('project')->eq($execution->project)->andWhere('deliverable')->in($deliverables)->fetch('count');
  4873. return $countExecutionDeliverables >= count($deliverables);
  4874. }
  4875. /**
  4876. * 更新项目的工时。
  4877. * Update project hours.
  4878. *
  4879. * @param int $newProjectID
  4880. * @param int $oldProjectID
  4881. * @param int $executionID
  4882. * @access public
  4883. * @return bool
  4884. */
  4885. public function updateProjectHours($newProjectID, $oldProjectID, $executionID)
  4886. {
  4887. if(empty($newProjectID) || empty($oldProjectID) || empty($executionID)) return true;
  4888. $this->dao->update(TABLE_EFFORT)->set('project')->eq($newProjectID)->where('execution')->eq($executionID)->exec();
  4889. $executionHours = $this->dao->select('estimate,`left`,consumed')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch();
  4890. $oldProjectHours = $this->dao->select('estimate,`left`,consumed')->from(TABLE_PROJECT)->where('id')->eq($oldProjectID)->fetch();
  4891. $newProjectHours = $this->dao->select('estimate,`left`,consumed')->from(TABLE_PROJECT)->where('id')->eq($newProjectID)->fetch();
  4892. if(!empty($executionHours->estimate))
  4893. {
  4894. $oldProjectHours->estimate -= $executionHours->estimate;
  4895. $newProjectHours->estimate += $executionHours->estimate;
  4896. }
  4897. if(!empty($executionHours->left))
  4898. {
  4899. $oldProjectHours->left -= $executionHours->left;
  4900. $newProjectHours->left += $executionHours->left;
  4901. }
  4902. if(!empty($executionHours->consumed))
  4903. {
  4904. $oldProjectHours->consumed -= $executionHours->consumed;
  4905. $newProjectHours->consumed += $executionHours->consumed;
  4906. }
  4907. $oldProjectHours->progress = ($oldProjectHours->consumed + $oldProjectHours->left) > 0 ? floor($oldProjectHours->consumed / ($oldProjectHours->consumed + $oldProjectHours->left) * 1000) / 1000 * 100 : 0;
  4908. $newProjectHours->progress = ($newProjectHours->consumed + $newProjectHours->left) > 0 ? floor($newProjectHours->consumed / ($newProjectHours->consumed + $newProjectHours->left) * 1000) / 1000 * 100 : 0;
  4909. $this->dao->update(TABLE_PROJECT)->data($oldProjectHours)->where('id')->eq($oldProjectID)->exec();
  4910. $this->dao->update(TABLE_PROJECT)->data($newProjectHours)->where('id')->eq($newProjectID)->exec();
  4911. return !dao::isError();
  4912. }
  4913. }