| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235 |
- <?php
- /**
- * The control file of story module of ZenTaoPMS.
- *
- * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
- * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
- * @author Chunsheng Wang <chunsheng@cnezsoft.com>
- * @package story
- * @version $Id: control.php 5145 2013-07-15 06:47:26Z chencongzhi520@gmail.com $
- * @link https://www.zentao.net
- */
- class story extends control
- {
- /**
- * The construct function, load product, tree, user auto.
- *
- * @param string $module
- * @param string $method
- * @access public
- * @return void
- */
- public function __construct($module = '', $method = '')
- {
- parent::__construct($module, $method);
- $this->loadModel('product');
- $this->loadModel('project');
- $this->loadModel('execution');
- $this->loadModel('tree');
- $this->loadModel('user');
- $this->loadModel('action');
- $this->loadModel('epic');
- $this->loadModel('requirement');
- if($this->app->rawModule == 'projectstory') $this->app->tab = 'project';
- }
- /**
- * Create a story.
- *
- * @param int $productID
- * @param string $branch
- * @param int $moduleID
- * @param int $storyID
- * @param int $objectID projectID|executionID
- * @param int $bugID
- * @param int $planID
- * @param int $todoID
- * @param string $extra for example feedbackID=0
- * @param string $storyType requirement|story
- * @access public
- * @return void
- */
- public function create($productID = 0, $branch = '', $moduleID = 0, $storyID = 0, $objectID = 0, $bugID = 0, $planID = 0, $todoID = 0, $extra = '', $storyType = 'story')
- {
- /* Set menu. */
- $this->story->replaceURLang($storyType);
- $copyStoryID = $storyID;
- list($productID, $objectID) = $this->storyZen->setMenuForCreate($productID, $objectID, $extra);
- if($productID == 0 && $objectID == 0) return $this->locate($this->createLink('product', 'create'));
- if($productID == 0 && $objectID != 0) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$objectID"));
- if(!empty($_POST))
- {
- if(isset($_POST['module'])) $moduleID = $this->post->module;
- if(isset($_POST['modules'])) $moduleID = reset($_POST['modules']);
- helper::setcookie('lastStoryModule', $moduleID, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false);
- /* API will post projectID or executionID. */
- if($this->post->project) $objectID = (int)$this->post->project;
- if($this->post->execution) $objectID = (int)$this->post->execution;
- /* API only has productID. */
- if(!isset($_POST['product'])) $_POST['product'] = $productID;
- /* Get story data from post. */
- $storyData = $this->storyZen->buildStoryForCreate($objectID, $bugID, $storyType);
- if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- /* Insert story data. */
- $createFunction = (!empty($storyData->branches) && $storyData->type == 'story') ? 'createTwins' : 'create'; // 只有软件需求才有孪生需求
- $storyID = $this->story->{$createFunction}($storyData, $objectID, $bugID, $extra, $todoID);
- if(empty($storyID) || dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- if(!empty($_POST['fileList']))
- {
- $fileList = $this->post->fileList;
- if($fileList) $fileList = json_decode($fileList, true);
- $this->loadModel('file')->saveDefaultFiles($fileList, 'story', $storyID, 1);
- }
- $productID = $this->post->product ? $this->post->product : $productID;
- $message = $this->executeHooks($storyID);
- if(empty($message)) $message = $this->post->status == 'draft' ? $this->lang->story->saveDraftSuccess : $this->lang->saveSuccess;
- if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $message, 'id' => $storyID));
- /* Get response when create in modal. */
- $response = $this->storyZen->getResponseInModal($message);
- if($response) return $this->send($response);
- $response = array('result' => 'success', 'message' => $message);
- if($this->post->newStory)
- {
- $response['message'] = $message . $this->lang->story->newStory;
- $response['load'] = $this->createLink('story', 'create', "productID=$productID&branch=$branch&moduleID=$moduleID&story=$copyStoryID&objectID=$objectID&bugID=$bugID&planID=$planID&todoID=$todoID&extra=$extra&storyType=$storyType");
- return $this->send($response);
- }
- $response['load'] = $this->storyZen->getAfterCreateLocation((int)$productID, $branch, $objectID, $storyID, $storyType, $extra);
- return $this->send($response);
- }
- /* Init vars. */
- $initStory = $this->storyZen->initStoryForCreate($planID, $copyStoryID, $bugID, $todoID, $extra);
- /* Get form fields. */
- $this->storyZen->setViewVarsForKanban($objectID, $this->story->parseExtra($extra), $storyType);
- $fields = $this->storyZen->getFormFieldsForCreate($productID, $branch, $objectID, $initStory, $storyType);
- $fields = $this->storyZen->setModuleField($fields, $moduleID);
- $fields = $this->storyZen->removeFormFieldsForCreate($fields, $storyType);
- $this->view->title = $this->view->product->name . $this->lang->hyphen . $this->lang->story->create;
- $this->view->fields = $fields;
- $this->view->blockID = $this->storyZen->getAssignMeBlockID();
- $this->view->type = $storyType;
- $this->view->story = $initStory;
- $this->view->forceReview = $this->story->checkForceReview($storyType);
- $extras = str_replace(array(',', ' ', '*'), array('&', '', '-'), $extra);
- parse_str($extras, $params);
- if(!isset($params['needNotReview'])) $extra .= ',needNotReview={needNotReview}';
- $this->view->needNotReview = $params['needNotReview'] ?? !$this->view->forceReview;
- $this->view->loadUrl = $this->createLink($storyType, 'create', "productID={product}&branch={branch}&moduleID=$moduleID&story=$storyID&objectID=$objectID&bugID=$bugID&planID=$planID&todoID=$todoID&extra=$extra&storyType=$storyType");
- $this->display();
- }
- /**
- * Create a batch stories.
- *
- * @param int $productID
- * @param string $branch
- * @param int $moduleID
- * @param int $storyID
- * @param int $executionID projectID|executionID
- * @param int $plan
- * @param string $storyType requirement|story
- * @param string $extra for example feedbackID=0
- * @access public
- * @return void
- */
- public function batchCreate($productID = 0, $branch = '', $moduleID = 0, $storyID = 0, $executionID = 0, $plan = 0, $storyType = 'story', $extra = '')
- {
- $this->story->replaceURLang($storyType);
- if(!empty($_POST))
- {
- $stories = $this->storyZen->buildStoriesForBatchCreate($productID, $storyType);
- if(empty($stories)) return $this->sendError($this->lang->story->errorEmptyStory, true);
- if(dao::isError()) return $this->sendError(dao::getError());
- $storyIdList = $this->story->batchCreate($stories);
- if(dao::isError()) return $this->sendError(dao::getError(), true);
- /* Project or execution linked stories. */
- if($executionID)
- {
- $lanes = array();
- $products = array();
- foreach($storyIdList as $i => $newStoryID)
- {
- if(isset($stories[$i]) && !empty($stories[$i]->lane)) $lanes[$newStoryID] = $stories[$i]->lane;
- $products[$newStoryID] = $productID;
- }
- if($this->session->project && $executionID != $this->session->project) $this->execution->linkStory($this->session->project, $storyIdList);
- $this->execution->linkStory($executionID, $storyIdList, $extra, $lanes, $storyType);
- }
- if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $stories));
- if(isInModal()) return $this->send($this->storyZen->getResponseInModal($this->lang->saveSuccess));
- $locateLink = $this->storyZen->getAfterBatchCreateLocation($productID, $branch, $executionID, $storyID, $storyType);
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $locateLink));
- }
- $this->storyZen->setMenuForBatchCreate($productID, $branch, $executionID, $extra, $storyType);
- if($productID == 0 && $executionID != 0) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$executionID"));
- $product = $this->product->getByID($productID);
- if($product) $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
- /* The 'batchCreateFields' of global variable $config will be changed and used by the following business logic. */
- $customFields = $this->storyZen->getCustomFields($this->config, $storyType, $this->view->hiddenPlan, $product);
- $showFields = $this->storyZen->getShowFields($this->config->{$storyType}->custom->batchCreateFields, $storyType, $product);
- $fields = $this->storyZen->getFormFieldsForBatchCreate($productID, $branch, $executionID, $storyType);
- $fields = $this->storyZen->removeFormFieldsForBatchCreate($fields, $this->view->hiddenPlan, isset($this->view->execution) ? $this->view->execution->type : '', $executionID);
- if($storyID)
- {
- $story = $this->story->getByID($storyID);
- if($story)
- {
- if(isset($fields['parent'])) $fields['parent']['default'] = $storyID;
- $gradeOptions = $this->story->getGradeOptions($story, $storyType);
- if(empty($gradeOptions)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorNoGradeSplit, 'locate' => $this->session->storyList)));
- $fields['grade']['options'] = $gradeOptions;
- $fields['grade']['default'] = key($gradeOptions);
- }
- $this->view->story = $story;
- $this->view->storyTitle = isset($story->title) ? $story->title : '';
- /* Check can subdivide or not. */
- if(!$this->story->checkCanSubdivide($story, !empty($product->shadow))) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorNotSubdivide)));
- /* Check can split requirement/story or not. */
- if($storyType != $story->type && !$this->story->checkCanSplit($story)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorCannotSplit)));
- }
- $this->view->title = $product->name . $this->lang->hyphen . ($storyID ? $this->lang->story->subdivide : $this->lang->story->batchCreate);
- $this->view->customFields = $customFields;
- $this->view->showFields = $showFields;
- $this->view->product = $product;
- $this->view->branch = $branch;
- $this->view->productID = $productID;
- $this->view->storyID = $storyID;
- $this->view->moduleID = $moduleID;
- $this->view->executionID = $executionID;
- $this->view->type = $storyType;
- $this->view->fields = $fields;
- $this->view->planID = $plan;
- $this->view->maxGradeGroup = $this->story->getMaxGradeGroup('all');
- $this->view->stories = $this->storyZen->getDataFromUploadImages($productID, $moduleID, $plan);
- $this->view->storyTitle = isset($story->title) ? $story->title : '';
- $this->view->forceReview = $this->story->checkForceReview($storyType);
- $this->display();
- }
- /**
- * The common action when edit or change a story.
- *
- * @param int $storyID
- * @param int $projectID
- * @access public
- * @return void
- */
- public function commonAction($storyID, $projectID = 0)
- {
- /* Get data. */
- $story = $this->story->getByID($storyID);
- if(!$story) return $this->send(array('result' => 'fail', 'message' => $this->lang->story->noStory));
- $this->story->replaceURLang($story->type);
- /* Set menu. */
- if($this->app->tab == 'project')
- {
- if(empty($projectID)) $projectID = $this->session->project;
- $this->loadModel('project')->setMenu($projectID);
- $this->view->projectID = $projectID;
- }
- elseif($this->app->tab == 'execution')
- {
- if(empty($projectID)) $projectID = $this->session->execution;
- $this->loadModel('execution')->setMenu($projectID);
- $this->view->executionID = $projectID;
- }
- elseif($this->app->tab == 'qa')
- {
- $this->loadModel('qa')->setMenu($story->product);
- }
- else
- {
- $this->product->setMenu($story->product, $story->branch);
- }
- $product = $this->product->getByID($story->product);
- if($product->shadow)
- {
- $products = $this->product->getPairs('', 0, '', 'all');
- }
- else
- {
- $products = $this->product->getPairs();
- }
- /* Assign. */
- $this->view->product = $product;
- $this->view->productID = $this->view->product->id;
- $this->view->products = $products;
- $this->view->story = $story;
- $this->view->moduleOptionMenu = $this->tree->getOptionMenu($story->product, 'story', 0, (string)$story->branch);
- $this->view->plans = $this->loadModel('productplan')->getPairs($story->product, 0, '', true);
- $this->view->actions = $this->action->getList('story', $storyID);
- }
- /**
- * Edit a story.
- *
- * @param int $storyID
- * @param string $kanbanGroup
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function edit($storyID, $kanbanGroup = 'default', $storyType = 'story')
- {
- $this->loadModel('file');
- $this->app->loadLang('bug');
- $this->commonAction($storyID);
- $this->story->getAllChildId($storyID);
- if(!empty($_POST))
- {
- $storyData = $this->storyZen->buildStoryForEdit($storyID);
- if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->storyZen->processDataForEdit($storyID, $storyData);
- $this->story->update($storyID, $storyData, $this->post->comment);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $message = $this->executeHooks($storyID);
- if(empty($message)) $message = $this->lang->saveSuccess;
- if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
- $response = $this->storyZen->getResponseInModal($message);
- if($response) return $this->send($response);
- $response = array('result' => 'success', 'message' => $message);
- $response['load'] = $this->storyZen->getAfterEditLocation($storyID, $storyType);
- return $this->send($response);
- }
- $story = $this->story->getByID($storyID);
- if($story->type == 'requirement') $this->lang->story->notice->reviewerNotEmpty = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->notice->reviewerNotEmpty);
- $fields = $this->storyZen->getFormFieldsForEdit($storyID);
- $fields = $this->storyZen->hiddenFormFieldsForEdit($fields, $storyType);
- $this->view->title = $this->lang->story->edit . "STORY" . $this->lang->hyphen . $this->view->story->title;
- $this->view->story = $story;
- $this->view->showGrade = !empty($this->config->showStoryGrade);
- $this->view->twins = empty($story->twins) ? array() : $this->story->getByList($story->twins);
- $this->view->fields = $fields;
- $this->view->branches = $this->view->product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($story->product);
- $this->view->lastReviewer = $this->story->getLastReviewer($story->id);
- $this->display();
- }
- /**
- * Batch edit story.
- *
- * @param int $productID
- * @param int $executionID
- * @param string $branch
- * @param string $storyType
- * @param string $from
- * @access public
- * @return void
- */
- public function batchEdit($productID = 0, $executionID = 0, $branch = '', $storyType = 'story', $from = '')
- {
- $this->story->replaceURLang($storyType);
- $this->storyZen->setMenuForBatchEdit($productID, $executionID, $storyType, $from);
- if($this->config->vision != 'or') unset($this->config->story->form->batchEdit['roadmap']);
- /* Load model. */
- $this->loadModel('productplan');
- if($this->post->title)
- {
- $stories = $this->storyZen->buildStoriesForBatchEdit();
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->story->batchUpdate($stories);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->session->storyList));
- }
- $stories = $this->storyZen->getStoriesByChecked();
- if(!$stories) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->batchEditError, 'locate' => $this->session->storyList)));
- /* Set Custom*/
- foreach(explode(',', $this->config->story->list->customBatchEditFields) as $field) $customFields[$field] = $this->lang->story->$field;
- $product = $this->product->getByID($productID);
- if($product && $product->type == 'normal') unset($customFields['branch']);
- $this->view->customFields = $customFields;
- $this->view->showFields = $this->config->{$storyType}->custom->batchEditFields;
- $this->storyZen->setFormOptionsForBatchEdit($productID, $executionID, $stories);
- foreach($stories as $story) $story->branch = 'branch' . $story->branch;
- $this->view->title = $this->lang->story->batchEdit;
- $this->view->productID = $productID;
- $this->view->branch = $branch;
- $this->view->storyType = $storyType;
- $this->view->stories = $stories;
- $this->view->executionID = $executionID;
- $this->view->from = $from;
- $this->display();
- }
- /**
- * Change a story.
- *
- * @param int $storyID
- * @param string $from
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function change($storyID, $from = '', $storyType = 'story')
- {
- if(!empty($_POST))
- {
- $storyData = $this->storyZen->buildStoryForChange($storyID);
- if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $location = $this->storyZen->getAfterChangeLocation($storyID, $storyType);
- $dataChanged = $this->storyZen->checkDataChanged($storyID, $storyData);
- if(!$dataChanged) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $location));
- $changes = $this->story->change($storyID, $storyData);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- if($this->post->comment != '' or !empty($changes))
- {
- $action = !empty($changes) ? 'Changed' : 'Commented';
- $actionID = $this->action->create('story', $storyID, $action, $this->post->comment);
- $this->action->logHistory($actionID, $changes);
- /* Record submit review action. */
- $story = $this->story->fetchByID($storyID);
- if($story->status == 'reviewing') $this->action->create('story', $storyID, 'submitReview');
- }
- $message = $this->executeHooks($storyID);
- if(empty($message)) $message = $this->lang->saveSuccess;
- if(defined('RUN_MODE') and RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
- $response = $this->storyZen->getResponseInModal($message);
- if($response) return $this->send($response);
- return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location));
- }
- $this->commonAction($storyID);
- $story = $this->view->story;
- $this->story->getAffectedScope($story);
- $gradeGroup = array();
- $gradeList = $this->story->getGradeList('');
- foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
- /* Assign. */
- $this->view->title = $this->lang->story->change . "STORY" . $this->lang->hyphen . $story->title;
- $this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $story->assignedTo);
- $this->view->fields = $this->storyZen->getFormFieldsForChange($storyID);
- $this->view->gradeGroup = $gradeGroup;
- $this->view->lastReviewer = $this->story->getLastReviewer($story->id);
- $this->display();
- }
- /**
- * 激活需求。
- * Activate a story.
- *
- * @param int $storyID
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function activate($storyID, $storyType = 'story')
- {
- if(!empty($_POST))
- {
- $postData = $this->storyZen->buildStoryForActivate($storyID);
- $this->story->activate($storyID, $postData);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->executeHooks($storyID);
- if(isInModal())
- {
- $execution = $this->execution->getByID((int)$this->session->execution);
- if($this->app->tab == 'execution' and $execution->type == 'kanban')
- {
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => "refreshKanban()", 'closeModal' => true));
- }
- else
- {
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
- }
- }
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
- }
- $this->commonAction($storyID);
- /* Assign. */
- $this->view->title = $this->lang->story->activate . "STORY" . $this->lang->hyphen . $this->view->story->title;
- $this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $this->view->story->closedBy);
- $this->display();
- }
- /**
- * View a story.
- *
- * @param int $storyID
- * @param int $version
- * @param int $param executionID|projectID
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function view($storyID, $version = 0, $param = 0, $storyType = 'story')
- {
- $this->config->morphUpdate = false;
- $uri = $this->app->getURI(true);
- $tab = $this->app->tab;
- $story = $this->story->getById($storyID, $version, true);
- $product = $this->product->getByID((int)$story->product);
- $isAPI = defined('RUN_MODE') && RUN_MODE == 'api';
- if(!isInModal() && $tab == 'product' && !empty($product->shadow) && !$isAPI) return $this->send(array('result' => 'success', 'open' => array('url' => $uri, 'app' => 'project')));
- if(!$story || (isset($story->type) && $story->type != $storyType))
- {
- $locateModule = $this->config->vision == 'lite' ? 'project' : 'product';
- $locateMethod = ($this->config->edition == 'ipd' && $this->config->vision == 'or') ? 'browse' : 'index';
- return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink($locateModule, $locateMethod))));
- }
- if(!$this->app->user->admin and strpos(",{$this->app->user->view->products},", ",$story->product,") === false) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->product->accessDenied, 'locate' => $this->createLink('my', 'index'))));
- $this->session->set('productList', $uri . "#app={$tab}", 'product');
- if(!empty($story->fromBug)) $this->session->set('bugList', $uri, 'qa');
- $version = empty($version) ? $story->version : $version;
- $story = $this->story->mergeReviewer($story, true);
- $this->app->loadLang('bug');
- $this->story->replaceURLang($story->type);
- $this->storyZen->getLinkedObjects($story);
- $this->storyZen->setHiddenFieldsForView($product);
- if($product->type != 'normal') $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
- /* Set menu. */
- if($this->app->tab == 'project')
- {
- if($product->shadow)
- {
- $project = $this->loadModel('project')->getByShadowProduct($product->id);
- $projectID = $project->id;
- }
- else
- {
- $projectID = $param ? $param : $this->session->project;
- $project = $this->loadModel('project')->fetchByID((int)$projectID);
- }
- $this->view->projectID = $projectID;
- $this->view->project = $project;
- if(!$project->multiple)
- {
- $executionID = $param ? $param : $this->session->execution;
- $this->project->setMenu((int)$project->id);
- $this->view->executionID = $executionID;
- $this->view->execution = $this->loadModel('execution')->fetchByID($executionID);
- }
- else
- {
- $this->project->setMenu((int)$projectID);
- }
- }
- elseif($this->app->tab == 'execution')
- {
- $executionID = $param ? $param : $this->session->execution;
- $this->loadModel('execution')->setMenu($executionID);
- $this->view->executionID = $executionID;
- $this->view->execution = $this->execution->fetchByID($executionID);
- }
- elseif($this->app->tab == 'qa')
- {
- $this->loadModel('qa')->setMenu($story->product);
- }
- else
- {
- $this->product->setMenu($story->product, $story->branch);
- }
- $gradeGroup = array();
- $gradeList = $this->story->getGradeList('');
- foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
- $this->view->title = "STORY #$story->id $story->title - $product->name";
- $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id);
- $this->view->users = $this->user->getPairs('noletter');
- $this->view->executions = $this->execution->getPairs(0, 'all', 'nocode');
- $this->view->version = $version;
- $this->view->preAndNext = $this->loadModel('common')->getPreAndNextObject($story->type, $storyID);
- $this->view->builds = $this->loadModel('build')->getStoryBuilds($storyID);
- $this->view->releases = $this->loadModel('release')->getStoryReleases($storyID);
- $this->view->story = $story;
- $this->view->product = $product;
- $this->view->maxGradeGroup = $this->story->getMaxGradeGroup();
- $this->view->gradePairs = $this->story->getGradePairs($story->type, 'all');
- $this->view->gradeGroup = $gradeGroup;
- $this->view->roadmaps = $this->config->edition == 'ipd' ? array(0 => '') + $this->loadModel('roadmap')->getPairs() : array();
- $this->view->demand = $this->config->edition == 'ipd' ? $this->loadModel('demand')->getByID($story->demand) : new stdclass();
- $this->view->showGrade = !empty($this->config->showStoryGrade);
- $this->view->actions = $this->action->getList('story', $storyID);
- $this->view->branch = $story->branch;
- $this->view->project = isset($projectID) ? $this->loadModel('project')->getById($projectID) : null;
- $this->display();
- }
- /**
- * 需求详情,延后鉴权。
- * View a story, delay auth.
- *
- * @param int $storyID
- * @access public
- * @return void
- */
- public function storyView($storyID)
- {
- $story = $this->story->fetchByID($storyID);
- if(common::hasPriv($story->type, 'view'))
- {
- echo $this->fetch($story->type, 'view', "storyID=$storyID");
- }
- else
- {
- $vars = "module={$story->type}&method=view";
- if(isset($this->server->http_referer))
- {
- $referer = helper::safe64Encode($this->server->http_referer);
- $vars .= "&referer=$referer";
- }
- $denyLink = helper::createLink('user', 'deny', $vars);
- return $this->send(array('result' => 'success', 'load' => $denyLink));
- }
- }
- /**
- * Delete a story.
- *
- * @param int $storyID
- * @param string $confirm yes|no
- * @param string $from taskkanban
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function delete($storyID, $confirm = 'no', $from = '', $storyType = 'story')
- {
- $story = $this->story->fetchById($storyID);
- if($story->parent < 0) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert('{$this->lang->story->cannotDeleteParent}');"));
- if($confirm == 'no')
- {
- if($story->type != 'story')
- {
- $replacement = $story->type == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon;
- $this->lang->story->confirmDelete = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->confirmDelete);
- }
- $confirmURL = $this->createLink($storyType, 'delete', "story=$storyID&confirm=yes&from=$from&storyType=$storyType");
- return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$this->lang->story->confirmDelete}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});"));
- }
- else
- {
- $this->dao->update(TABLE_STORY)->set('deleted')->eq(1)->where('id')->eq($storyID)->exec();
- $this->loadModel('action')->create($story->type, $storyID, 'deleted', '', actionModel::CAN_UNDELETED);
- if($story->parent > 0)
- {
- $this->story->updateParentStatus($story->id);
- $this->action->create('story', $story->parent, 'deleteChildrenStory', '', $storyID);
- }
- $this->story->setStage($story->id);
- $this->executeHooks($storyID);
- if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success'));
- if($this->app->tab == 'execution' and $from == 'taskkanban') return $this->send(array('result' => 'success', 'closeModal' => true, 'callback' => "refreshKanban()"));
- $locateLink = $this->session->storyList ? $this->session->storyList : $this->createLink('product', 'browse', "productID={$story->product}");
- $locateLink = isInModal() ? true : $locateLink;
- return $this->send(array('result' => 'success', 'load' => $locateLink, 'closeModal' => true));
- }
- }
- /**
- * Review a story.
- *
- * @param int $storyID
- * @param string $from product|project
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function review($storyID, $from = 'product', $storyType = 'story')
- {
- if(!empty($_POST))
- {
- $storyData = $this->storyZen->buildStoryForReview($storyID);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->story->review($storyID, $storyData, (string)$this->post->comment);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $message = $this->executeHooks($storyID);
- if(empty($message)) $message = $this->lang->saveSuccess;
- if(isInModal())
- {
- if($this->app->tab == 'execution') $this->loadModel('kanban')->updateLane($this->session->execution, 'story', $storyID);
- return $this->send($this->storyZen->getResponseInModal($message));
- }
- if(defined('RUN_MODE') and RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
- $location = $this->storyZen->getAfterReviewLocation($storyID, $storyType, $from);
- return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location));
- }
- $this->commonAction($storyID);
- $story = $this->view->story;
- if(!empty($reviewers[$this->app->user->account]))
- {
- return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$this->lang->hasReviewed}'}).then((res) => {loadCurrentPage()});"));
- }
- $reviewers = $this->story->getReviewerPairs($storyID, $story->version);
- $this->story->getAffectedScope($story);
- $gradeGroup = array();
- $gradeList = $this->story->getGradeList('');
- foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
- $this->view->title = $this->lang->story->review . "STORY" . $this->lang->hyphen . $story->title;
- $this->view->fields = $this->storyZen->getFormFieldsForReview($storyID);
- $this->view->reviewers = $reviewers;
- $this->view->gradeGroup = $gradeGroup;
- $this->view->isLastOne = count(array_diff(array_keys($reviewers), explode(',', $story->reviewedBy))) <= 1;
- $this->display();
- }
- /**
- * Batch review stories.
- *
- * @param string $result
- * @param string $reason
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function batchReview($result, $reason = '', $storyType = 'story')
- {
- if(!$this->post->storyIdList) return $this->send(array('result' => 'success', 'load' => $this->session->storyList));
- $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
- $message = $this->story->batchReview($storyIdList, $result, $reason);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->loadModel('score')->create('ajax', 'batchOther');
- $response = array();
- $response['result'] = 'success';
- $response['load'] = false;
- if($message) $response['callback'] = "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$message}'}).then((res) => {loadCurrentPage()});";
- if(empty($message)) $response['load'] = true;
- return $this->send($response);
- }
- /**
- * Recall the story review or story change.
- *
- * @param int $storyID
- * @param string $from list
- * @param string $confirm no|yes
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function recall($storyID, $from = 'list', $confirm = 'no', $storyType = 'story')
- {
- $story = $this->story->fetchById($storyID);
- if($confirm == 'no')
- {
- $confirmTips = $story->status == 'changing' ? $this->lang->story->confirmRecallChange : $this->lang->story->confirmRecallReview;
- $confirmURL = $this->createLink($story->type, 'recall', "storyID=$storyID&from=$from&confirm=yes&storyType=$storyType");
- return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message:'{$confirmTips}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});"));
- }
- if($story->status == 'changing') $this->story->recallChange($storyID);
- if($story->status == 'reviewing') $this->story->recallReview($storyID);
- $action = $story->status == 'changing' ? 'recalledChange' : 'Recalled';
- $this->loadModel('action')->create('story', $storyID, $action);
- if($from == 'modal') return $this->send(array('result' => 'success', 'load' => 'modal'));
- $locateLink = $this->session->storyList ? $this->session->storyList : $this->createLink('product', 'browse', "productID={$story->product}");
- if($from == 'view')
- {
- $module = $story->type;
- $method = 'view';
- $params = "storyID=$storyID&version=0¶m=0&storyType=$storyType";
- if($this->app->tab == 'project')
- {
- $module = 'projectstory';
- $method = 'view';
- $params = "storyID=$storyID";
- }
- elseif($this->app->tab == 'execution')
- {
- $module = 'execution';
- $method = 'storyView';
- $params = "storyID=$storyID";
- }
- $locateLink = $this->createLink($module, $method, $params);
- if(strpos(',qa,doc,', ",{$this->app->tab},") !== false) $locateLink = true;
- }
- return $this->send(array('result' => 'success', 'load' => $locateLink, 'closeModal' => true));
- }
- /**
- * Submit review.
- *
- * @param int $storyID
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function submitReview($storyID, $storyType = 'story')
- {
- if($_POST)
- {
- $storyData = $this->storyZen->buildStoryForSubmitReview($storyID);
- if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $changes = $this->story->submitReview($storyID, $storyData);
- if(dao::isError()) return print(js::error(dao::getError()));
- if($changes)
- {
- $actionID = $this->loadModel('action')->create('story', $storyID, 'submitReview');
- $this->action->logHistory($actionID, $changes);
- }
- if(isInModal()) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => 'loadCurrentPage()'));
- $module = $this->app->tab == 'project' ? 'projectstory' : 'story';
- $params = $this->app->tab == 'project' ? "storyID=$storyID&project={$this->session->project}" : "storyID=$storyID&version=0¶m=0&storyType=$storyType";
- return $this->send(array('result' => 'success', 'load' => $this->createLink($module, 'view', $params), 'message' => $this->lang->saveSuccess));
- }
- /* Get story and product. */
- $story = $this->story->fetchById($storyID);
- $product = $this->product->getById($story->product);
- /* Get reviewers. */
- $reviewers = $product->reviewer;
- if(!$reviewers and $product->acl != 'open') $reviewers = $this->loadModel('user')->getProductViewListUsers($product);
- /* Get story reviewer. */
- $reviewerList = $this->story->getReviewerPairs($story->id, $story->version);
- $story->reviewer = array_keys($reviewerList);
- $this->view->story = $story;
- $this->view->actions = $this->action->getList('story', $storyID);
- $this->view->reviewers = $this->user->getPairs('noclosed|nodeleted', '', 0, $reviewers);
- $this->view->users = $this->user->getPairs('noclosed|noletter');
- $this->view->needReview = (($this->app->user->account == $product->PO or $this->config->{$storyType}->needReview == 0 or !$this->story->checkForceReview($storyType)) and empty($story->reviewer)) ? "checked='checked'" : "";
- $this->view->lastReviewer = $this->story->getLastReviewer($story->id);
- $this->display();
- }
- /**
- * 关闭需求。
- * Close the story.
- *
- * @param int $storyID
- * @param string $from taskkanban
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function close($storyID, $from = '', $storyType = 'story')
- {
- $story = $this->story->getById($storyID);
- $this->commonAction($storyID);
- if(!empty($_POST))
- {
- $postData = form::data($this->config->story->form->close, $storyID)
- ->stripTags($this->config->story->editor->close['id'], $this->config->allowedTags)
- ->removeIF($this->post->closedReason != 'duplicate', 'duplicateStory')
- ->get();
- if(strpos($this->config->{$story->type}->change->requiredFields, 'comment') !== false and !$this->post->comment) $this->send(array('result' => 'fail', 'message' => array('comment' => sprintf($this->lang->error->notempty, $this->lang->comment))));
- $postData = $this->loadModel('file')->processImgURL($postData, $this->config->story->editor->close['id'], $this->post->uid);
- $changes = $this->story->close($storyID, $postData);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->executeHooks($storyID);
- if(isInModal())
- {
- $execution = $this->execution->getByID((int)$this->session->execution);
- if(($this->app->tab == 'execution' && $execution->type == 'kanban') || $from == 'taskkanban')
- {
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => "refreshKanban()"));
- }
- else
- {
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => 'table'));
- }
- }
- if(defined('RUN_MODE') && RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID));
- $module = $this->app->tab == 'project' ? 'projectstory' : 'story';
- $params = $this->app->tab == 'project' ? "storyID=$storyID&project={$this->session->project}" : "storyID=$storyID&version=0¶m=0&storyType=$storyType";
- return $this->send(array('result' => 'success', 'load' => $this->createLink($module, 'view', $params), 'message' => $this->lang->saveSuccess, 'closeModal' => true));
- }
- /* Get story and product. */
- $product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fields('name, id, `type`')->fetch();
- $this->story->replaceURLang($story->type);
- /* Set the closed reason options and remove subdivided options. */
- $reasonList = $this->lang->{$storyType}->reasonList;
- if($story->status == 'draft') unset($reasonList['cancel']);
- unset($reasonList['subdivided']);
- $this->view->title = $this->lang->story->close . "STORY" . $this->lang->hyphen . $story->title;
- $this->view->product = $product;
- $this->view->story = $story;
- $this->view->actions = $this->action->getList('story', $storyID);
- $this->view->users = $this->loadModel('user')->getPairs();
- $this->view->reasonList = $reasonList;
- $this->display();
- }
- /**
- * 批量关闭需求。
- * Batch close the stories.
- *
- * @param int $productID
- * @param int $executionID
- * @param string $storyType story|requirement
- * @param string $from contribute|work
- * @access public
- * @return void
- */
- public function batchClose($productID = 0, $executionID = 0, $storyType = 'story', $from = '')
- {
- if(!$this->post->storyIdList) return $this->send(array('result' => 'success', 'load' => $this->session->storyList));
- $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
- $this->story->replaceURLang($storyType);
- if($this->post->comment)
- {
- $stories = $this->storyZen->buildStoriesForBatchClose(); /* Get the stories which need to be closed. */
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->story->batchClose($stories);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->loadModel('score')->create('ajax', 'batchOther');
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->session->storyList));
- }
- $this->storyZen->setMenuForBatchClose($productID, $executionID, $storyType, $from);
- /* Get the skipped and already closed stories, and the count of stories which have a twin. */
- $stories = $this->story->getByList($storyIdList);
- $closedStory = array();
- $ignoreTwins = array();
- $twinsCount = array();
- $storyCount = count($stories);
- foreach($stories as $story)
- {
- if(!empty($ignoreTwins) and isset($ignoreTwins[$story->id]))
- {
- unset($stories[$story->id]);
- continue;
- }
- if($story->status == 'closed')
- {
- $closedStory[] = $story->id;
- unset($stories[$story->id]);
- }
- if(!empty($story->twins))
- {
- $twinsCount[$story->id] = 0;
- foreach(explode(',', trim($story->twins, ',')) as $twinID)
- {
- $twinsCount[$story->id] ++;
- $ignoreTwins[$twinID] = $twinID;
- }
- }
- }
- if($storyCount == count($closedStory)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->notice->closed, 'locate' => $this->session->storyList)));
- $errorTips = '';
- if($closedStory) $errorTips .= sprintf($this->lang->story->closedStory, implode(',', $closedStory));
- $this->view->productID = $productID;
- $this->view->stories = $stories;
- $this->view->storyType = $storyType;
- $this->view->twinsCount = $twinsCount;
- $this->view->errorTips = $errorTips;
- $this->display();
- }
- /**
- * Batch change the module of story.
- *
- * @param int $moduleID
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function batchChangeModule($moduleID, $storyType = 'story')
- {
- if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
- $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
- $allChanges = $this->story->batchChangeModule($storyIdList, $moduleID);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- foreach($allChanges as $storyID => $changes)
- {
- $actionID = $this->action->create('story', $storyID, 'Edited');
- $this->action->logHistory($actionID, $changes);
- }
- $this->loadModel('score')->create('ajax', 'batchOther');
- return $this->send(array('result' => 'success', 'load' => true));
- }
- /**
- * Batch stories convert to tasks.
- *
- * @param int $executionID
- * @param int $projectID
- * @access public
- * @return void
- */
- public function batchToTask($executionID = 0, $projectID = 0)
- {
- if($this->app->tab == 'execution' and $executionID) $this->loadModel('execution')->setMenu($executionID);
- if($this->app->tab == 'project' and $executionID) $this->loadModel('execution')->setMenu($executionID);
- if(!empty($_POST['name']))
- {
- $response['result'] = 'success';
- $response['message'] = $this->lang->story->successToTask;
- $tasks = $this->storyZen->buildDataForBatchToTask($executionID, $projectID);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $taskIdList = $this->story->batchToTask($executionID, $tasks);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $taskIdList));
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->createLink('execution', 'task', "executionID=$executionID")));
- }
- if(!$this->post->storyIdList) return $this->locate($this->session->storyList);
- $stories = $this->story->getByList($_POST['storyIdList']);
- $activeStories = array();
- $storyPairs = array();
- $hasParent = false;
- $hasERUR = false;
- foreach($stories as $story)
- {
- if(strpos(',draft,reviewing,changing,closed,', ",{$story->status},") !== false) continue;
- if($story->type != 'story')
- {
- $hasERUR = true;
- continue;
- }
- if($story->isParent == '1')
- {
- $hasParent = true;
- continue;
- }
- $activeStories[$story->id] = $story;
- $storyPairs[$story->id] = $story->title;
- }
- if(empty($activeStories)) return $this->sendError($this->lang->story->noStoryToTask, $this->session->storyList . "#app={$this->app->tab}");
- $execution = $this->execution->fetchByID($executionID);
- if($execution->multiple) $manageLink = common::hasPriv('execution', 'manageMembers') ? $this->createLink('execution', 'manageMembers', "execution={$execution->id}") : '';
- if(!$execution->multiple) $manageLink = common::hasPriv('project', 'manageMembers') ? $this->createLink('project', 'manageMembers', "projectID={$execution->project}") : '';
- $this->view->title = $this->lang->story->batchToTask;
- $this->view->executionID = $executionID;
- $this->view->projectID = $projectID;
- $this->view->manageLink = $manageLink;
- $this->view->syncFields = empty($_POST['fields']) ? array() : $_POST['fields'];
- $this->view->hourPointValue = empty($_POST['hourPointValue']) ? 0 : $_POST['hourPointValue'];
- $this->view->taskType = empty($_POST['type']) ? '' : $_POST['type'];
- $this->view->stories = $activeStories;
- $this->view->storyPairs = $storyPairs;
- $this->view->hasParent = $hasParent;
- $this->view->hasERUR = $hasERUR;
- $this->view->modules = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, 'allModule');
- $this->view->members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted');
- $this->view->storyTasks = $this->loadModel('task')->getStoryTaskCounts(array_keys($stories), $executionID);
- $this->display();
- }
- /**
- * Batch change the plan of story.
- *
- * @param int $planID
- * @param int $oldPlanID
- * @access public
- * @return void
- */
- public function batchChangePlan($planID, $oldPlanID = 0)
- {
- if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
- $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
- $allChanges = $this->story->batchChangePlan($storyIdList, $planID, $oldPlanID);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- foreach($allChanges as $storyID => $changes)
- {
- $actionID = $this->action->create('story', $storyID, 'Edited');
- $this->action->logHistory($actionID, $changes);
- }
- $this->loadModel('score')->create('ajax', 'batchOther');
- return $this->send(array('result' => 'success', 'load' => true));
- }
- /**
- * Batch change branch.
- *
- * @param int $branchID
- * @param string $confirm yes|no
- * @param string $storyIdList
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function batchChangeBranch($branchID, $confirm = '', $storyIdList = '', $storyType = 'story')
- {
- if(empty($storyIdList) and empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
- if(!empty($_POST['storyIdList'])) $storyIdList = $this->post->storyIdList;
- if(is_string($storyIdList)) $storyIdList = array_filter(explode(',', $storyIdList));
- $storyIdList = $this->storyZen->convertChildID($storyIdList);
- $plans = $this->loadModel('productplan')->getPlansByStories($storyIdList);
- if(empty($confirm))
- {
- $stories = $this->story->getByList($storyIdList);
- $normalStotyIdList = '';
- $conflictStoryIdList = '';
- $conflictStoryArray = array();
- /* Determine whether there is a conflict between the branch of the story and the linked plan. */
- foreach($storyIdList as $storyID)
- {
- if($stories[$storyID]->branch == $branchID) continue;
- if($branchID == BRANCH_MAIN) continue;
- if(!isset($plans[$storyID])) continue;
- foreach($plans[$storyID] as $plan)
- {
- if($plan->branch == $branchID) continue;
- $conflictStoryIdList .= "[{$storyID}]";
- $conflictStoryArray[] = $storyID;
- break;
- }
- }
- /* Prompt the user whether to continue to modify the conflicting stories branch. */
- if($conflictStoryIdList)
- {
- $normalStotyIdList = array_diff($storyIdList, $conflictStoryArray);
- $normalStotyIdList = implode(',', $normalStotyIdList);
- $storyIdList = implode(',', $storyIdList);
- $confirmURL = $this->createLink('story', 'batchChangeBranch', "branchID=$branchID&confirm=yes&storyIdList=$storyIdList&storyType=$storyType");
- $cancelURL = $this->createLink('story', 'batchChangeBranch', "branchID=$branchID&confirm=no&storyIdList=$normalStotyIdList&storyType=$storyType");
- return $this->send(array('result' => 'success', 'load' => array('confirm' => sprintf($this->lang->story->confirmChangeBranch, $conflictStoryIdList), 'confirmed' => $confirmURL, 'canceled' => $cancelURL)));
- }
- }
- $allChanges = $this->story->batchChangeBranch($storyIdList, $branchID, $confirm, $plans);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- foreach($allChanges as $storyID => $changes)
- {
- $actionID = $this->action->create('story', $storyID, 'Edited');
- $this->action->logHistory($actionID, $changes);
- }
- if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther');
- return $this->send(array('result' => 'success', 'load' => true));
- }
- /**
- * Batch change the grade of story.
- *
- * @param int $grade
- * @param string $storyType story|requirement|epic
- * @access public
- * @return void
- */
- public function batchChangeGrade($grade, $storyType = 'story')
- {
- if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
- $storyIdList = array_unique($this->post->storyIdList);
- $message = $this->story->batchChangeGrade($storyIdList, $grade, $storyType);
- $response = array();
- $response['result'] = 'success';
- $response['load'] = false;
- if($message) $response['callback'] = "zui.Modal.alert('{$message}').then((res) => {loadCurrentPage()});";
- if(empty($message)) $response['load'] = true;
- return $this->send($response);
- }
- /**
- * Batch change the stage of story.
- *
- * @param string $stage
- * @access public
- * @return void
- */
- public function batchChangeStage($stage)
- {
- if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
- $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
- $message = $this->story->batchChangeStage($storyIdList, $stage);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $this->loadModel('score')->create('ajax', 'batchOther');
- $response = array();
- $response['result'] = 'success';
- $response['load'] = false;
- if($message) $response['callback'] = "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$message}'}).then((res) => {loadCurrentPage()});";
- if(empty($message)) $response['load'] = true;
- return $this->send($response);
- }
- /**
- * 批量修改需求的父需求。
- * Batch change the parent of story.
- *
- * @param int $productID
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function batchChangeParent($productID = 0, $storyType = 'story')
- {
- if($_POST)
- {
- $storyIdList = $this->cookie->checkedItem;
- $result = $this->story->batchChangeParent($storyIdList, (int)$this->post->parent, $storyType);
- if($result) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert('$result');", 'closeModal' => true, 'load' => true));
- return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true));
- }
- $this->view->parents = $this->story->getParentStoryPairs($productID, '', $storyType);
- $this->display();
- }
- /**
- * 需求的指派给页面。
- * Assign the story to a user.
- *
- * @param int $storyID
- * @access public
- * @return void
- */
- public function assignTo($storyID)
- {
- if(!empty($_POST))
- {
- $story = form::data($this->config->story->form->assignTo, $storyID)->get();
- $this->story->assign($storyID, $story);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $message = $this->executeHooks($storyID);
- $response = $this->storyZen->getResponseInModal($message);
- if($response) return $this->send($response);
- return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'closeModal' => true));
- }
- /* Get story and product to render the view. */
- $story = $this->story->getById($storyID);
- $product = $this->product->getByID($story->product);
- $users = $this->config->vision == 'lite' ? $this->user->getTeamMemberPairs($this->session->project) : $this->user->getPairs('nodeleted|noclosed|pofirst|noletter');
- $this->view->title = zget($product, 'name', $story->title) . $this->lang->hyphen . $this->lang->story->assign;
- $this->view->story = $story;
- $this->view->actions = $this->action->getList('story', $storyID);
- $this->view->users = $users;
- $this->display();
- }
- /**
- * Batch assign to.
- *
- * @param string $storyType story|requirement
- * @access public
- * @return void
- * @param string $assignedTo
- */
- public function batchAssignTo($storyType = 'story', $assignedTo = '')
- {
- if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true));
- if(empty($assignedTo)) $assignedTo = $this->post->assignedTo;
- $storyIdList = $this->storyZen->convertChildID($this->post->storyIdList);
- $oldStories = $this->story->getByList($storyIdList);
- $allChanges = $this->story->batchAssignTo($storyIdList, $assignedTo);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- $ignoreStories = array();
- $assignedTwins = array();
- foreach($allChanges as $storyID => $changes)
- {
- $oldStory = $oldStories[$storyID];
- if($oldStory->status == 'closed') $ignoreStories[] = "#{$storyID}";
- /* Sync twins. */
- if(empty($oldStory->twins)) continue;
- $twins = array_unique(array_filter(explode(',', $oldStory->twins)));
- foreach($twins as $i => $twinID)
- {
- if(in_array($twinID, $storyIdList) || isset($assignedTwins[$twinID])) unset($twins[$i]);
- }
- $this->story->syncTwins($storyID, implode(',', $twins), $changes, 'Assigned');
- foreach($twins as $assignedID) $assignedTwins[$assignedID] = $assignedID;
- }
- $this->loadModel('score')->create('ajax', 'batchOther');
- $response = array('result' => 'success', 'load' => true);
- if($ignoreStories) $response['ignoreMessage'] = sprintf($this->lang->story->ignoreClosedStory, implode(',', $ignoreStories));
- return $this->send($response);
- }
- /**
- * 查看需求的相关任务。
- * Tasks of a story.
- *
- * @param int $storyID
- * @param int $executionID
- * @access public
- * @return void
- */
- public function tasks($storyID, $executionID = 0)
- {
- $tasks = $this->loadModel('task')->getListByStory($storyID, $executionID);
- $this->view->tasks = $tasks;
- $this->view->users = $this->user->getPairs('noletter');
- $this->view->summary = $this->execution->summary($tasks);
- $this->view->story = $this->story->getById($storyID);
- $this->display();
- }
- /**
- * 查看需求的相关缺陷。
- * Bugs of a story.
- *
- * @param int $storyID
- * @param int $executionID
- * @access public
- * @return void
- */
- public function bugs($storyID, $executionID = 0)
- {
- $this->view->bugs = $this->loadModel('bug')->getStoryBugs($storyID, $executionID);
- $this->view->users = $this->user->getPairs('noletter');
- $this->view->story = $this->story->getById($storyID);
- $this->display();
- }
- /**
- * 查看需求的相关用例。
- * Cases of a story.
- *
- * @param int $storyID
- * @access public
- * @return void
- */
- public function cases($storyID)
- {
- $this->view->cases = $this->loadModel('testcase')->getStoryCases($storyID);
- $this->view->users = $this->user->getPairs('noletter');
- $this->view->resultList = $this->lang->testcase->resultList;
- $this->view->story = $this->story->getById($storyID);
- $this->display();
- }
- /**
- * 展示用户需求关联的软件需求,或软件需求关联的用户需求。
- * Show the URs/SRs of the SR/UR.
- *
- * @param int $storyID
- * @param string $storyType story|requirement
- * @access public
- * @return void
- */
- public function relation($storyID, $storyType = 'story')
- {
- $story = $this->story->getById($storyID);
- $storyType = $storyType != $story->type ? $story->type : $storyType;
- $selectFields = array('id', 'pri', 'title', 'plan', 'openedBy', 'assignedTo', 'estimate', 'status');
- $this->view->relation = $this->story->getStoryRelation($storyID, $storyType, $selectFields);
- $this->view->users = $this->user->getPairs('noletter');
- $this->view->storyType = $storyType;
- $this->view->story = $story;
- $this->display();
- }
- /**
- * 需求详情页关联需求。
- * Link story in view.
- *
- * @param int $storyID
- * @param string $type link|remove
- * @param int $linkedStoryID
- * @param string $browseType ''|bySearch
- * @param int $queryID 0|
- * @param int $recTotal
- * @param int $recPerPage
- * @param int $pageID
- * @param string $orderBy
- * @access public
- * @return void
- */
- public function linkStory($storyID, $type = 'link', $linkedStoryID = 0, $browseType = '', $queryID = 0, $recTotal = 0, $recPerPage = 20, $pageID = 1, $orderBy = 'id_desc')
- {
- $this->commonAction($storyID);
- $story = $this->story->getById($storyID);
- if($type == 'remove')
- {
- $this->story->unlinkStory($storyID, $linkedStoryID);
- return $this->send(array('result' => 'success', 'load' => true));
- }
- if($_POST)
- {
- $this->story->linkStories($storyID, $this->post->stories);
- if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
- return $this->send(array('result' => 'success', 'callback' => "const modal = zui.Modal.query('.modal-dialog[data-auto-load=\"story:$storyID\"]'); if(typeof(modal) !== 'undefined') modal.render(); else loadCurrentPage();", 'closeModal' => true));
- }
- /* Get story, product, products, and queryID. */
- $products = $this->product->getPairs('', 0, '', 'all');
- /* Build search form. */
- $actionURL = $this->createLink('story', 'linkStory', "storyID=$storyID&type=$type&linkedStoryID=$linkedStoryID&browseType=bySearch&queryID=myQueryID&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID");
- $this->product->buildSearchForm($story->product, $products, $queryID, $actionURL, 'all', (string)$story->branch);
- /* Load pager. */
- $this->app->loadClass('pager', true);
- $pager = new pager($recTotal, $recPerPage, $pageID);
- /* Get stories to link. */
- $stories2Link = $this->story->getStories2Link($storyID, $browseType, $queryID, $pager, $orderBy);
- /* Assign. */
- $this->view->title = $this->lang->story->linkStory . "STORY" . $this->lang->hyphen .$this->lang->story->linkStory;
- $this->view->type = $type;
- $this->view->pager = $pager;
- $this->view->stories2Link = $stories2Link;
- $this->view->maxGradeGroup = $this->story->getMaxGradeGroup();
- $this->view->users = $this->loadModel('user')->getPairs('noletter');
- $this->view->storyID = $storyID;
- $this->view->linkedStoryID = $linkedStoryID;
- $this->view->browseType = $browseType;
- $this->view->queryID = $queryID;
- $this->view->orderBy = $orderBy;
- $this->display();
- }
- /**
- * 需求的父需求变更时,子需求确认变更。
- * Confirm the change of the parent story.
- *
- * @param int $storyID
- * @access public
- * @return void
- */
- public function processStoryChange($storyID)
- {
- $story = $this->story->fetchByID($storyID);
- if($story->parent <= 0 && $this->config->edition == 'ipd') return $this->fetch('demand', 'processDemandChange', "objectID=$storyID&type=story");
- $parent = $this->story->fetchByID($story->parent);
- $this->dao->update(TABLE_STORY)->set('parentVersion')->eq($parent->version)->where('id')->eq($storyID)->exec();
- return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true));
- }
- /**
- * 获取执行的需求列表信息用于html下拉列表。
- * AJAX: get stories of a execution in html select.
- *
- * @param int $executionID
- * @param int $productID
- * @param int|string $branch 0|all|integer
- * @param int $moduleID
- * @param int $storyID
- * @param string $pageType batch
- * @param string $type full|short
- * @param string $status all|unclosed
- * @access public
- * @return void
- */
- public function ajaxGetExecutionStories($executionID, $productID = 0, $branch = 0, $moduleID = 0, $storyID = 0, $pageType = '', $type = 'full', $status = 'all')
- {
- if($moduleID)
- {
- $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
- $moduleID = $this->tree->getAllChildID($moduleID);
- }
- $stories = $this->story->getExecutionStoryPairs($executionID, $productID, $branch, $moduleID, $type, $status, 'story', false);
- if($this->app->getViewType() === 'json')
- {
- return print(json_encode($stories));
- }
- elseif($this->app->getViewType() == 'mhtml')
- {
- return print(html::select('story', empty($stories) ? array() : $stories, $storyID, 'onchange=setStoryRelated()'));
- }
- elseif($pageType == 'batch')
- {
- $storyList = array();
- foreach($stories as $id => $name) $storyList[] = array('value' => $id, 'text' => $name);
- return $this->send($storyList);
- }
- else
- {
- $items = $this->story->addGradeLabel($stories);
- return print(json_encode($items));
- }
- }
- /**
- * 获取产品的需求列表信息用于html下拉列表。
- * AJAX: get the stories of a product in html select.
- *
- * @param int $productID
- * @param int $branch
- * @param int $moduleID
- * @param int $storyID
- * @param string $onlyOption
- * @param string $status ''|all|noclosed|changing|active|draft|closed|reviewing
- * @param int $limit
- * @param string $type full|all
- * @param int $hasParent 0|1
- * @param int $objectID projectID|executionID
- * @param int $isHTML 0|1
- * @access public
- * @return void
- */
- public function ajaxGetProductStories($productID, $branch = 0, $moduleID = 0, $storyID = 0, $onlyOption = 'false', $status = '', $limit = 0, $type = 'full', $hasParent = 1, $objectID = 0, $isHTML = 1)
- {
- $hasParent = $hasParent >= 1 ? true : false;
- $modules = array();
- if($moduleID)
- {
- $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
- $modules = $this->tree->getAllChildID($moduleID);
- }
- $storyStatus = $this->story->getStatusList($status);
- if($storyStatus == 'active') $storyStatus = 'active,reviewing';
- if($objectID)
- {
- $stories = $this->story->getExecutionStoryPairs($objectID, $productID, $branch, $modules, $type, 'all', 'story', $hasParent);
- }
- else
- {
- $stories = $this->story->getProductStoryPairs($productID, $branch, $modules, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent);
- }
- if(!in_array($this->app->tab, array('execution', 'project')) and empty($stories)) $stories = $this->story->getProductStoryPairs($productID, $branch, 0, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent);
- if($storyID && !isset($stories[$storyID]))
- {
- $story = $this->story->fetchByID($storyID);
- if(!$moduleID || in_array($story->module, $modules)) $stories = arrayUnion($stories, array($storyID => $storyID . ':' . $story->title));
- }
- if($isHTML == 0)
- {
- $storyList = array();
- foreach($stories as $storyID => $storyName) $storyList[] = array('value' => $storyID, 'text' => $storyName);
- $storyList = $this->story->addGradeLabel($stories);
- return $this->send($storyList);
- }
- $items = $this->story->addGradeLabel($stories);
- return print(json_encode($items));
- }
- /**
- * AJAX: search stories of a product as json
- *
- * @param string $key #storyID|storyID|keyword
- * @param int $productID
- * @param int $branch
- * @param int $moduleID
- * @param int $storyID
- * @param string $status noclosed
- * @param int $limit 50|0
- * @access public
- * @return void
- */
- public function ajaxSearchProductStories($key, $productID, $branch = 0, $moduleID = 0, $storyID = 0, $status = 'noclosed', $limit = 50)
- {
- if($moduleID)
- {
- $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
- $moduleID = $this->tree->getAllChildID($moduleID);
- }
- $storyStatus = array();
- if($status == 'noclosed')
- {
- $storyStatus = $this->lang->story->statusList;
- unset($storyStatus['closed']);
- $storyStatus = array_keys($storyStatus);
- }
- $stories = $this->story->getProductStoryPairs($productID, $branch, $moduleID, $storyStatus);
- $result = array();
- $resultCount = 0;
- foreach ($stories as $id => $story)
- {
- if($limit > 0 && $resultCount > $limit) break;
- if(('#' . $id) === $key || stripos($story, $key) !== false)
- {
- $result[$id] = $story;
- $resultCount++;
- }
- }
- if($resultCount < 1) $result['info'] = $this->lang->noResultsMatch;
- echo json_encode($result);
- }
- /**
- * 获取关闭需求页面重复需求的下拉列表。
- * AJAX: get the duplicated stories of a story.
- *
- * @param int $storyID
- * @param int $productID
- * @access public
- * @return void
- */
- public function ajaxGetDuplicatedStories($storyID, $productID)
- {
- $product = $this->dao->findById($productID)->from(TABLE_PRODUCT)->fields('name, id, `type`')->fetch();
- $story = $this->story->fetchByID($storyID);
- $storyBranch = $story->branch > 0 ? $story->branch : '0';
- $branch = $product->type == 'branch' ? $storyBranch : 'all';
- $productStories = $this->story->getProductStoryPairs($story->product, $branch, 0, 'all', 'id_desc', 0, '', $story->type);
- $children = $this->story->getAllChildId($storyID);
- $productStories = array_diff_key($productStories, array_flip($children));
- if(isset($productStories[$storyID])) unset($productStories[$storyID]);
- $items = array();
- foreach($productStories as $storyID => $storyName) $items[] = array('text' => $storyName, 'value' => $storyID);
- return print(json_encode($items));
- }
- /**
- * 获取用例可关联的需求下拉列表。
- * AJAX: get the stories of a case.
- *
- * @param int $productID
- * @param int $moduleID
- * @param int $branch
- * @param int $storyID
- * @access public
- * @return void
- */
- public function ajaxGetCaseStories($productID = 0, $moduleID = 0, $branch = '', $storyID = 0)
- {
- if($storyID)
- {
- $story = $this->story->fetchByID($storyID);
- if(empty($moduleID)) $moduleID = $story->module;
- }
- $modules = array();
- if($moduleID)
- {
- $moduleID = $this->loadModel('tree')->getStoryModule($moduleID);
- $modules = $this->tree->getAllChildID($moduleID);
- }
- $stories = $this->story->getProductStoryPairs($productID, $branch, $modules, 'active,reviewing', 'id_desc', 0, '', 'story', false);
- if($this->app->tab != 'qa' && $this->app->tab != 'product' && $this->app->tab != 'my')
- {
- $projectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution;
- if($projectID) $stories = $this->story->getExecutionStoryPairs($projectID, $productID, $branch, $modules, 'full', 'all', 'story', false);
- }
- if(!in_array($this->app->tab, array('execution', 'project')) and empty($stories)) $stories = $this->story->getProductStoryPairs($productID, $branch, 0, 'active,reviewing', 'id_desc', 0, '', 'story', false);
- if($storyID && !isset($stories[$storyID])) $stories = arrayUnion($stories, array($storyID => $storyID . ':' . $story->title));
- $stories = $this->story->addGradeLabel($stories);
- return print(json_encode($stories));
- }
- /**
- * 获取需求详情和操作日志。
- * AJAX: get the actions and detail of the story for web app.
- *
- * @param int $storyID
- * @access public
- * @return void
- */
- public function ajaxGetDetail($storyID)
- {
- $this->view->actions = $this->action->getList('story', $storyID);
- $this->view->story = $this->story->getByID($storyID);
- $this->display();
- }
- /**
- * 获取需求层级下拉列表及默认值。
- * AJAX: get the grade of a story.
- *
- * @param int $storyID
- * @param string $type
- * @param int $grade
- * @access public
- * @return void
- */
- public function ajaxGetGrade($storyID, $type = 'story', $grade = 0)
- {
- $story = $this->story->fetchByID($storyID);
- $gradeOptions = $this->story->getGradeOptions($story, $type, (array)$grade);
- $items = array();
- foreach($gradeOptions as $grade => $name) $items[] = array('text' => $name, 'value' => $grade);
- return $this->send(array('items' => array_values($items), 'default' => key($gradeOptions)));
- }
- /**
- * 检查需求的等级是否超出系统设置。
- * AJAX: check the grade of a story.
- *
- * @param int $storyID
- * @param int $newGrade
- * @access public
- * @return bool
- */
- public function ajaxCheckGrade($storyID, $newGrade)
- {
- $oldStory = $this->story->fetchByID($storyID);
- $story = clone $oldStory;
- $story->grade = $newGrade;
- if($story->grade > $oldStory->grade)
- {
- if(!$this->story->checkGrade($story, $oldStory)) return print(json_encode(array('result' => false, 'message' => dao::getError())));
- }
- return print(json_encode(array('result' => true)));
- }
- /**
- * 获取需求的信息。
- * AJAX: get module of a story.
- *
- * @param int $storyID
- * @param string $pageType batch
- * @access public
- * @return mixed
- */
- public function ajaxGetInfo($storyID, $pageType = '')
- {
- $story = $this->story->getByID($storyID);
- if(empty($story)) return;
- $storyInfo['moduleID'] = $story->module;
- $storyInfo['estimate'] = $story->estimate;
- $storyInfo['pri'] = $story->pri;
- $storyInfo['spec'] = html_entity_decode($story->spec, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
- $storyInfo['status'] = $story->status;
- if($pageType == 'batch') return $this->send(array('storyInfo' => $storyInfo));
- return print(json_encode($storyInfo));
- }
- /**
- * 获取某产品下的父需求列表。
- * AJAX: get the parent story.
- *
- * @param int $productID
- * @param int $storyID
- * @access public
- * @return string
- */
- public function ajaxGetParentStory($productID, $storyID = 0)
- {
- if($storyID)
- {
- $story = $this->story->fetchByID($storyID);
- $stories = $this->story->getParentStoryPairs($story->product, $story->parent, $story->type, $storyID);
- }
- else
- {
- $stories = $this->story->getParentStoryPairs($productID);
- }
- $items = array();
- foreach($stories as $storyID => $storyTitle)
- {
- if(empty($storyID)) continue;
- $items[] = array('text' => $storyTitle, 'value' => $storyID);
- }
- return $this->send($items);
- }
- /**
- * 查看需求的报告。
- * The report page.
- *
- * @param int $productID
- * @param int $branchID
- * @param string $storyType
- * @param string $browseType
- * @param int $moduleID
- * @param string $chartType
- * @param int $projectID
- * @access public
- * @return void
- */
- public function report($productID, $branchID, $storyType = 'story', $browseType = 'unclosed', $moduleID = 0, $chartType = 'pie', $projectID = 0)
- {
- $this->loadModel('report');
- $this->view->charts = array();
- $this->view->datas = array();
- if(!empty($_POST))
- {
- foreach($this->post->charts as $chart)
- {
- $chartFunc = 'getDataOf' . $chart;
- $chartData = $this->story->$chartFunc($storyType);
- $chartOption = $this->lang->story->report->$chart;
- if(!empty($chartType)) $chartOption->type = $chartType;
- $this->story->mergeChartOption($chart);
- $this->view->charts[$chart] = $chartOption;
- $this->view->datas[$chart] = $this->report->computePercent($chartData);
- }
- }
- $this->story->replaceURLang($storyType);
- $product = $this->product->getByID($productID);
- if(strpos('project,execution', $this->app->tab) !== false)
- {
- $project = $this->dao->findByID($projectID)->from(TABLE_PROJECT)->fetch();
- if($project->type == 'project')
- {
- $this->loadModel('project')->setMenu($projectID);
- if($project and $project->model == 'waterfall') unset($this->lang->story->report->charts['storiesPerPlan']);
- $this->view->projectID = $projectID;
- }
- else
- {
- $this->loadModel('execution')->setMenu($projectID);
- $this->view->executionID = $projectID;
- }
- if(!$project->hasProduct)
- {
- unset($this->lang->story->report->charts['storiesPerProduct']);
- if(!$project->multiple) unset($this->lang->story->report->charts['storiesPerPlan']);
- }
- }
- else
- {
- $this->product->setMenu($productID, $branchID);
- }
- $this->view->title = $product->name . $this->lang->hyphen . $this->lang->story->reportChart;
- $this->view->productID = $productID;
- $this->view->branchID = $branchID;
- $this->view->browseType = $browseType;
- $this->view->storyType = $storyType;
- $this->view->moduleID = $moduleID;
- $this->view->chartType = $chartType;
- $this->view->projectID = $projectID;
- $this->view->checkedCharts = $this->post->charts ? implode(',', $this->post->charts) : '';
- $this->display();
- }
- /**
- * 导出需求数据。
- * Get the data of the stories to export.
- *
- * @param int $productID
- * @param string $orderBy
- * @param int $executionID
- * @param string $browseType
- * @param string $storyType requirement|story
- * @access public
- * @return void
- */
- public function export($productID, $orderBy, $executionID = 0, $browseType = '', $storyType = 'story')
- {
- $this->story->replaceURLang($storyType);
- /* format the fields of every story in order to export data. */
- if($_POST)
- {
- $this->loadModel('transfer');
- $postData = form::data($this->config->transfer->form->export)->get();
- $this->session->set("{$storyType}TransferParams", array('productID' => $productID, 'executionID' => $executionID));
- /* 给评审过的人员添加下拉选择,方便再次导入时转换成待评审人员。*/
- /* Add a drop-down selection to the reviewer to facilitate the conversion to the reviewer during import. */
- $this->config->story->dtable->fieldList['reviewedBy']['control'] = 'multiple';
- $this->config->story->dtable->fieldList['reviewedBy']['dataSource'] = array('module' => 'story', 'method' => 'getProductReviewers', 'params' => array('productID' => (int)$productID));
- if($executionID) $this->lang->story->title = $this->lang->story->name;
- /* Create field lists. */
- if(!$productID or $browseType == 'bysearch')
- {
- $this->config->story->dtable->fieldList['branch']['dataSource'] = array('module' => 'branch', 'method' => 'getAllPairs');
- $this->config->story->dtable->fieldList['module']['dataSource']['method'] = 'getAllModulePairs';
- $this->config->story->dtable->fieldList['module']['dataSource']['params'] = 'story';
- $this->config->story->dtable->fieldList['project']['dataSource'] = array('module' => 'project', 'method' => 'getPairsByIdList', 'params' => $executionID);
- $this->config->story->dtable->fieldList['execution']['dataSource'] = array('module' => 'execution', 'method' => 'getPairs', 'params' => $executionID);
- $products = $this->loadModel('product')->getPairs('all', 0, '', 'all');
- $productIdList = array_keys($products);
- $this->config->story->dtable->fieldList['plan']['dataSource'] = array('module' => 'productplan', 'method' => 'getPairs', 'params' => array($productIdList));
- }
- $this->fetch('transfer', 'export', "model=$storyType");
- }
- $project = null;
- $hasBranch = false;
- if($executionID)
- {
- $execution = $this->loadModel('execution')->getByID($executionID);
- $fileName = $execution->name . $this->lang->dash . $this->lang->common->story;
- $project = $execution;
- if($execution->type == 'execution') $project = $this->project->getById($execution->project);
- $this->lang->story->title = $this->lang->story->name;
- $products = $this->loadModel('product')->getProducts($executionID);
- foreach($products as $product)
- {
- if($product->type != 'normal') $hasBranch = true;
- }
- }
- else
- {
- $productName = $this->lang->product->all;
- if($productID)
- {
- $product = $this->product->getById($productID);
- $productName = $product->name;
- if($product->shadow) $project = $this->project->getByShadowProduct($productID);
- if($product->type != 'normal') $hasBranch = true;
- }
- if(isset($this->lang->product->featureBar['browse'][$browseType]))
- {
- $browseType = $this->lang->product->featureBar['browse'][$browseType];
- }
- else
- {
- $browseType = isset($this->lang->product->moreSelects[$browseType]) ? $this->lang->product->moreSelects[$browseType] : '';
- }
- $fileName = $productName . $this->lang->dash . $browseType . $this->lang->common->story;
- }
- /* Unset branch field. */
- if(!$hasBranch) $this->config->story->exportFields = str_replace(', branch', '', $this->config->story->exportFields);
- /* If or vision, unset plan field. */
- if($this->config->vision == 'or') $this->config->story->exportFields = str_replace(', plan,', ',', $this->config->story->exportFields);
- /* Unset product field when in single project. */
- if(isset($project->hasProduct) && !$project->hasProduct)
- {
- $filterFields = array(', product,', ', branch,');
- if($project->model != 'scrum') $filterFields[] = ', plan,';
- $this->config->story->exportFields = str_replace($filterFields, ',', $this->config->story->exportFields);
- }
- /* Append workflow field. */
- if($this->config->edition != 'open')
- {
- $exportFlowFields = $this->loadModel('workflowfield')->getExportFields($storyType);
- foreach($exportFlowFields as $field => $name)
- {
- $this->config->story->exportFields .= ",{$field}";
- $this->lang->story->{$field} = $name;
- }
- }
- $this->view->fileName = $fileName;
- $this->view->allExportFields = $this->config->story->exportFields;
- $this->view->customExport = true;
- $this->view->storyType = $storyType;
- $this->display();
- }
- /**
- * 通过AJAX方式获取用户需要处理的需求。
- * AJAX: get stories of a user in html select.
- *
- * @param int $userID
- * @param string $id the id of the select control.
- * @param int $appendID
- * @param string $storyType
- * @access public
- * @return string
- */
- public function ajaxGetUserStories($userID = 0, $id = '', $appendID = 0, $storyType = 'story')
- {
- if(empty($userID)) $userID = $this->app->user->id;
- $user = $this->loadModel('user')->getById($userID, 'id');
- $stories = $this->story->getUserStoryPairs($user->account, 10, $storyType, '', $appendID);
- $items = array();
- foreach($stories as $storyID => $storyTitle) $items[] = array('text' => $storyTitle, 'value' => $storyID);
- $fieldName = $id ? "stories[$id]" : $storyType;
- return print(json_encode(array('name' => $fieldName, 'items' => $items)));
- }
- /**
- * 用AJAX方式获取需求的指派给。
- * AJAX: get story assignee.
- *
- * @param string $type create|review|change
- * @param int $storyID
- * @param array|string $assignees
- * @access public
- * @return mixed
- */
- public function ajaxGetAssignedTo($type = '', $storyID = 0, $assignees = '')
- {
- $users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
- if($type == 'create')
- {
- $selectUser = is_array($assignees) ? current($assignees) : '';
- return print(html::select('assignedTo', $users, $selectUser, "class='from-control picker-select'"));
- }
- if($type == 'review')
- {
- $moduleName = $this->app->rawModule;
- $story = $this->story->getByID($storyID);
- $reviewers = $this->story->getReviewerPairs($storyID, $story->version);
- $isChanged = $story->changedBy ? true : false;
- $superReviewers = trim(zget($this->config->{$moduleName}, 'superReviewers', ''), ',');
- $isSuperReviewer = strpos(",{$superReviewers},", ",{$this->app->user->account},") !== false;
- if(count($reviewers) == 1)
- {
- $selectUser = $isChanged ? $story->changedBy : $story->openedBy;
- }
- else
- {
- unset($reviewers[$this->app->user->account]);
- foreach($reviewers as $account => $result)
- {
- if(empty($result))
- {
- $selectUser = $account;
- break;
- }
- else
- {
- $selectUser = $isChanged ? $story->changedBy : $story->openedBy;
- }
- }
- }
- if($isSuperReviewer !== false) $selectUser = $isChanged ? $story->changedBy : $story->openedBy;
- return print(html::select('assignedTo', $users, $selectUser, "class='from-control picker-select'"));
- }
- if($type == 'change')
- {
- $selectUser = is_array($assignees) ? current($assignees) : '';
- return print(html::select('assignedTo', $users, $selectUser, "class='from-control picker-select'"));
- }
- return false;
- }
- /**
- * 移除需求的孪生需求。
- * AJAX: Deleted story twin.
- *
- * @access public
- * @return void
- */
- public function ajaxRelieveTwins()
- {
- $twinID = !empty($_POST['twinID']) ? $_POST['twinID'] : 0;
- $story = $this->story->getByID((int)$twinID);
- $twins = explode(',', trim($story->twins, ','));
- if(empty($story->twins)) return $this->send(array('result' => 'fail'));
- $this->story->relieveTwins($story->product, (int)$twinID);
- if(!dao::isError()) $this->loadModel('action')->create('story', (int)$twinID, 'relieved');
- return $this->send(array('result' => 'success', 'twinsCount' => count($twins)-1));
- }
- /**
- * 获取需求ID和需求概要信息键值对的html信息。批量关闭时如果原因是重复了,需要选择重复的需求。
- * AJAX: Get the html with story pairs to choose the duplicated story.
- *
- * @param int $storyID
- * @access public
- * @return void
- */
- public function ajaxGetDuplicatedStory($storyID)
- {
- $story = $this->story->getByID($storyID);
- $stories = $this->story->getProductStoryPairs($story->product, $story->branch, 0, 'all', 'id_desc', 0, '', $story->type);
- $items = array();
- foreach($stories as $id => $storyTitle)
- {
- if(empty($id)) continue;
- if($id == $storyID) continue;
- $items[] = array('text' => $storyTitle, 'value' => $id, 'keys' => $storyTitle);
- }
- return print(json_encode($items));
- }
- /**
- * 创建代码分支。
- * Create repo branch.
- *
- * @param int $storyID
- * @param int $repoID
- * @access public
- * @return void
- */
- public function createBranch($storyID, $repoID = 0)
- {
- return print($this->fetch('repo', 'createBranch', array('objectID' => $storyID, 'repoID' => $repoID)));
- }
- /**
- * 取消代码分支的关联。
- * Unlink code branch.
- *
- * @access public
- * @return void
- */
- public function unlinkBranch()
- {
- return print($this->fetch('repo', 'unlinkBranch'));
- }
- }
|