diff --git a/module/story/control.php b/module/story/control.php index e38f70c83b..48efd0b2e3 100755 --- a/module/story/control.php +++ b/module/story/control.php @@ -61,8 +61,8 @@ class story extends control helper::setcookie('lastStoryModule', (int)$this->post->module, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false); /* Get story data from post. */ - $storyData = $this->productZen->buildStoryForCreate($objectID, $bugID); - $response = $this->productZen->checkRepeatStory($storyData, $objectID); + $storyData = $this->storyZen->buildStoryForCreate($objectID, $bugID); + $response = $this->storyZen->checkRepeatStory($storyData, $objectID); if($response) return $this->send($response); /* Insert story data. */ @@ -968,78 +968,30 @@ class story extends control * @access public * @return void */ - public function review($storyID, $from = 'product', $storyType = 'story') + public function review(int $storyID, string $from = 'product', string $storyType = 'story') { if(!empty($_POST)) { - $this->story->review($storyID); - if(dao::isError()) return print(js::error(dao::getError())); + $storyData = $this->storyZen->buildStoryForReview($storyID); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); - $this->executeHooks($storyID); + $this->story->review($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(isonlybody()) { - $execution = $this->execution->getByID($this->session->execution); - if($this->app->tab == 'execution') - { - $this->loadModel('kanban')->updateLane($this->session->execution, 'story', $storyID); + if($this->app->tab == 'execution') $this->loadModel('kanban')->updateLane($this->session->execution, 'story', $storyID); - $executionLaneType = $this->session->executionLaneType ? $this->session->executionLaneType : 'all'; - $executionGroupBy = $this->session->executionGroupBy ? $this->session->executionGroupBy : 'default'; - if($execution->type == 'kanban') - { - $rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : ''; - $kanbanData = $this->loadModel('kanban')->getRDKanban($this->session->execution, $executionLaneType, 'id_desc', 0, $executionGroupBy, $rdSearchValue); - $kanbanData = json_encode($kanbanData); - return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban($kanbanData)")); - } - if($from == 'taskkanban') - { - $taskSearchValue = $this->session->taskSearchValue ? $this->session->taskSearchValue : ''; - $kanbanData = $this->loadModel('kanban')->getExecutionKanban($this->session->execution, $executionLaneType, $executionGroupBy, $taskSearchValue); - $kanbanType = $executionLaneType == 'all' ? 'story' : key($kanbanData); - $kanbanData = $kanbanData[$kanbanType]; - $kanbanData = json_encode($kanbanData); - return print(js::closeModal('parent.parent', '', "parent.parent.updateKanban(\"story\", $kanbanData)")); - } - } - else - { - return print(js::closeModal('parent.parent', 'this', "function(){parent.parent.location.reload();}")); - } + $response = $this->storyZen->responseAfterCreateInModal($message); + if($response) return $this->send($response); } - if(defined('RUN_MODE') and RUN_MODE == 'api') return $this->send(array('status' => 'success', 'data' => $storyID)); - if($from == 'project') - { - $module = 'projectstory'; - $method = 'view'; - $params = "storyID=$storyID"; - } - elseif($from == 'execution') - { - $execution = $this->execution->getByID($this->session->execution); - if($execution->multiple) - { - $module = 'execution'; - $method = 'storyView'; - $params = "storyID=$storyID"; - } - else - { - $module = 'story'; - $method = 'view'; - $params = "storyID=$storyID&version=0¶m={$this->session->execution}&storyType=$storyType"; - } - } - else - { - $module = 'story'; - $method = 'view'; - $params = "storyID=$storyID&version=0¶m=0&storyType=$storyType"; - } - return print(js::locate($this->createLink($module, $method, $params), 'parent')); + $location = $this->storyZen->getAfterReviewLocation($storyID, $storyType, $from); + return $this->send(array('result' => 'success', 'message' => $message, 'load' => $location)); } $this->commonAction($storyID); diff --git a/module/story/model.php b/module/story/model.php index 7a5f90ba8d..baa0030a4d 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -40,19 +40,15 @@ class storyModel extends model if($setImgSize) $story->spec = $this->file->setImgSize($story->spec); if($setImgSize) $story->verify = $this->file->setImgSize($story->verify); + $twinsIdList = $storyID . ($story->twins ? ",{$story->twins}" : ''); $story->executions = $this->dao->select('t1.project, t2.name, t2.status, t2.type, t2.multiple')->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project = t2.id') ->where('t2.type')->in('sprint,stage,kanban') - ->beginIF($story->twins)->andWhere('t1.story')->in($story->twins . ',' . $story->id) - ->beginIF(!$story->twins)->andWhere('t1.story')->eq($story->id) + ->andWhere('t1.story')->in($twinsIdList) ->orderBy('t1.`order` DESC') ->fetchAll('project'); - $story->tasks = $this->dao->select('id, name, assignedTo, execution, project, status, consumed, `left`,type')->from(TABLE_TASK)->where('deleted')->eq(0) - ->beginIF($story->twins)->andWhere('story')->in($story->twins . ',' . $story->id) - ->beginIF(!$story->twins)->andWhere('story')->eq($story->id) - ->orderBy('id DESC') - ->fetchGroup('execution'); + $story->tasks = $this->dao->select('id,name,assignedTo,execution,project,status,consumed,`left`,type')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('story')->in($twinsIdList)->orderBy('id DESC')->fetchGroup('execution'); if($story->toBug) $story->toBugTitle = $this->dao->findById($story->toBug)->from(TABLE_BUG)->fetch('title'); if($story->parent > 0) $story->parentName = $this->dao->findById($story->parent)->from(TABLE_STORY)->fetch('title'); @@ -1450,99 +1446,45 @@ class storyModel extends model * Review a story. * * @param int $storyID + * @param object $story + * @param string $comment * @access public * @return bool */ - public function review($storyID) + public function review(int $storyID, object $story, string $comment = ''): bool { - if(strpos($this->config->story->review->requiredFields, 'comment') !== false and !$this->post->comment) - { - dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->comment); - return false; - } - - if($this->post->result == false) - { - dao::$errors[] = $this->lang->story->mustChooseResult; - return false; - } - $oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); $now = helper::now(); - $date = helper::today(); - $story = fixer::input('post') - ->setDefault('lastEditedBy', $this->app->user->account) - ->setDefault('lastEditedDate', $now) - ->setDefault('status', $oldStory->status) - ->setDefault('reviewedDate', $date) - ->stripTags($this->config->story->editor->review['id'], $this->config->allowedTags) - ->setIF(!$this->post->assignedTo, 'assignedTo', '') - ->setIF(!empty($_POST['assignedTo']), 'assignedDate', $now) - ->removeIF($this->post->result != 'reject', 'closedReason, duplicateStory, childStories') - ->removeIF($this->post->result == 'reject' and $this->post->closedReason != 'duplicate', 'duplicateStory') - ->removeIF($this->post->result == 'reject' and $this->post->closedReason != 'subdivided', 'childStories') - ->add('reviewedBy', $oldStory->reviewedBy . ',' . $this->app->user->account) - ->add('id', $storyID) - ->remove('result,comment') - ->get(); - - $story->reviewedBy = implode(',', array_unique(explode(',', $story->reviewedBy))); - $story = $this->loadModel('file')->processImgURL($story, $this->config->story->editor->review['id'], $this->post->uid); - - /* Fix bug #671. */ - $this->lang->story->closedReason = $this->lang->story->rejectedReason; $this->dao->update(TABLE_STORYREVIEW) - ->set('result')->eq($this->post->result) + ->set('result')->eq($story->result) ->set('reviewDate')->eq($now) - ->where('story')->eq($storyID) + ->where('story')->in($storyID . ($oldStory->twins ? ",{$oldStory->twins}" : '')) ->andWhere('version')->eq($oldStory->version) ->andWhere('reviewer')->eq($this->app->user->account) ->exec(); - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->update(TABLE_STORYREVIEW) - ->set('result')->eq($this->post->result) - ->set('reviewDate')->eq($now) - ->where('story')->eq($twinID) - ->andWhere('version')->eq($oldStory->version) - ->andWhere('reviewer')->eq($this->app->user->account) - ->exec(); - } - } - $story = $this->updateStoryByReview($storyID, $oldStory, $story); - $skipFields = 'finalResult'; - $isSuperReviewer = strpos(',' . trim(zget($this->config->story, 'superReviewers', ''), ',') . ',', ',' . $this->app->user->account . ','); - if($isSuperReviewer === false) + $skipFields = 'finalResult,result'; + $isSuperReviewer = $this->storyTao->isSuperReviewer(); + if($isSuperReviewer) { $reviewers = $this->getReviewerPairs($storyID, $oldStory->version); if(count($reviewers) > 1) $skipFields .= ',closedReason'; } - $this->dao->update(TABLE_STORY)->data($story, $skipFields) - ->autoCheck() - ->batchCheck($this->config->story->review->requiredFields, 'notempty') - ->checkIF($this->post->result == 'reject', 'closedReason', 'notempty') - ->checkIF($this->post->result == 'reject' and $this->post->closedReason == 'duplicate', 'duplicateStory', 'notempty') - ->checkFlow() - ->where('id')->eq($storyID) - ->exec(); + $this->dao->update(TABLE_STORY)->data($story, $skipFields)->autoCheck()->checkFlow()->where('id')->eq($storyID)->exec(); if(dao::isError()) return false; - if($this->post->result != 'reject') $this->setStage($storyID); + if($story->result != 'reject') $this->setStage($storyID); - if(isset($story->closedReason) and $isSuperReviewer === false) unset($story->closedReason); $changes = common::createChanges($oldStory, $story); if($changes) { - $actionID = $this->recordReviewAction($story, $this->post->result, $this->post->closedReason); - $this->action->logHistory($actionID, $changes); + $story->id = $storyID; + $actionID = $this->recordReviewAction($story, $comment); + if($actionID) $this->action->logHistory($actionID, $changes); } if(!empty($oldStory->twins)) $this->syncTwins($oldStory->id, $oldStory->twins, $changes, 'Reviewed'); @@ -1573,7 +1515,7 @@ class storyModel extends model { if(!$storyID) continue; - $isSuperReviewer = strpos(',' . trim(zget($this->config->story, 'superReviewers', ''), ',') . ',', ',' . $this->app->user->account . ','); + $isSuperReviewer = $this->storyTao->isSuperReviewer(); $oldStory = $oldStories[$storyID]; if($oldStory->status != 'reviewing') continue; @@ -1582,7 +1524,7 @@ class storyModel extends model if($reviewerInfo->version != $oldStory->version) unset($reviewerList[$storyID][$reviewer]); } - if(!in_array($this->app->user->account, array_keys($reviewerList[$storyID])) and $isSuperReviewer === false) continue; + if(!in_array($this->app->user->account, array_keys($reviewerList[$storyID])) and !$isSuperReviewer) continue; if(isset($hasResult[$storyID]) and $hasResult[$storyID]->version == $oldStories[$storyID]->version) continue; if($oldStory->version > 1 and $result == 'reject') continue; @@ -1593,29 +1535,12 @@ class storyModel extends model $story->reviewedBy = $oldStory->reviewedBy . ',' . $this->app->user->account; $story->status = $oldStory->status; - $this->dao->update(TABLE_STORYREVIEW)->set('result')->eq($result)->set('reviewDate')->eq($now)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->andWhere('reviewer')->eq($this->app->user->account)->exec(); - - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->update(TABLE_STORYREVIEW) - ->set('result')->eq($result) - ->set('reviewDate')->eq($now) - ->where('story')->eq($twinID) - ->andWhere('version')->eq($oldStory->version) - ->andWhere('reviewer')->eq($this->app->user->account) - ->exec(); - } - } + $twinsIdList = $storyID . ($oldStory->twins ? ",{$oldStory->twins}" : ''); + $this->dao->update(TABLE_STORYREVIEW)->set('result')->eq($result)->set('reviewDate')->eq($now)->where('story')->in($twinsIdList)->andWhere('version')->eq($oldStory->version)->andWhere('reviewer')->eq($this->app->user->account)->exec(); /* Update the story status by review rules. */ $reviewedBy = explode(',', trim($story->reviewedBy, ',')); - if($isSuperReviewer !== false) - { - $story = $this->superReview($storyID, $oldStory, $story, $result, $reason); - } + if($isSuperReviewer) $story = $this->superReview($storyID, $oldStory, $story, $result, $reason); if(!array_diff(array_keys($reviewerList[$storyID]), $reviewedBy)) { $reviewerPairs = array(); @@ -1666,16 +1591,8 @@ class storyModel extends model $story->status = $isChanged ? 'changing' : 'draft'; $this->dao->update(TABLE_STORY)->set('status')->eq($story->status)->where('id')->eq($storyID)->exec(); - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->exec(); - - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($twinID)->andWhere('version')->eq($oldStory->version)->exec(); - } - } + $twinsIdList = $storyID . ($oldStory->twins ? ",{$oldStory->twins}" : ''); + $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->in($twinsIdList)->andWhere('version')->eq($oldStory->version)->exec(); $changes = common::createChanges($oldStory, $story); if(!empty($oldStory->twins)) $this->syncTwins($storyID, $oldStory->twins, $changes, 'recalled'); @@ -1700,18 +1617,9 @@ class storyModel extends model $this->dao->update(TABLE_STORY)->set('title')->eq($story->title)->set('version')->eq($story->version)->set('status')->eq($story->status)->where('id')->eq($storyID)->exec(); /* Delete versions that is after this version. */ - $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($storyID)->andWHere('version')->eq($oldStory->version)->exec(); - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->exec(); - - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($twinID)->andWHere('version')->eq($oldStory->version)->exec(); - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($twinID)->andWhere('version')->eq($oldStory->version)->exec(); - } - } + $twinsIdList = $storyID . ($oldStory->twins ? ",{$oldStory->twins}" : ''); + $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->in($twinsIdList)->andWHere('version')->eq($oldStory->version)->exec(); + $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->in($twinsIdList)->andWhere('version')->eq($oldStory->version)->exec(); $changes = common::createChanges($oldStory, $story); if(!empty($oldStory->twins)) $this->syncTwins($storyID, $oldStory->twins, $changes, 'recalledChange'); @@ -1745,16 +1653,8 @@ class storyModel extends model ->join('reviewer', ',') ->get(); - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->exec(); - - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($twinID)->andWhere('version')->eq($oldStory->version)->exec(); - } - } + $twinsIdList = $storyID . ($oldStory->twins ? ",{$oldStory->twins}" : ''); + $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->in($twinsIdList)->andWhere('version')->eq($oldStory->version)->exec(); if(isset($_POST['reviewer'])) { @@ -2499,16 +2399,8 @@ class storyModel extends model if($story->status == 'active') { - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->exec(); - - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($twinID)->exec(); - } - } + $twinsIdList = $storyID . ($oldStory->twins ? ",{$oldStory->twins}" : ''); + $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->in($twinsIdList)->exec(); } $this->setStage($storyID); @@ -3809,11 +3701,11 @@ class storyModel extends model $story->reviewer = isset($story->reviewer) ? $story->reviewer : array(); $story->notReview = isset($story->notReview) ? $story->notReview : array(); - $isSuperReviewer = strpos(',' . trim(zget($config->story, 'superReviewers', ''), ',') . ',', ',' . $app->user->account . ','); + $isSuperReviewer = $this->storyTao->isSuperReviewer(); - if($action == 'change') return (($isSuperReviewer !== false or count($story->reviewer) == 0 or count($story->notReview) == 0) and $story->status == 'active'); + if($action == 'change') return (($isSuperReviewer or count($story->reviewer) == 0 or count($story->notReview) == 0) and $story->status == 'active'); if($action == 'submitReview') return strpos('draft,changing', $story->status) !== false; - if($action == 'review') return (($isSuperReviewer !== false or in_array($app->user->account, $story->notReview)) and $story->status == 'reviewing'); + if($action == 'review') return (($isSuperReviewer or in_array($app->user->account, $story->notReview)) and $story->status == 'reviewing'); if($action == 'recall') return strpos('reviewing,changing', $story->status) !== false; if($action == 'close') return $story->status != 'closed'; if($action == 'activate') return $story->status == 'closed'; @@ -5090,7 +4982,7 @@ class storyModel extends model * @access public * @return array */ - public function getReviewerPairs($storyID, $version) + public function getReviewerPairs(int $storyID, int $version): array { return $this->dao->select('reviewer,result')->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($version)->fetchPairs('reviewer', 'result'); } @@ -5102,7 +4994,7 @@ class storyModel extends model * @access public * @return string */ - public function getReviewResult($reviewerList) + public function getReviewResult(array $reviewerList): string { $results = ''; $passCount = 0; @@ -5130,9 +5022,9 @@ class storyModel extends model if($revertCount >= floor(count($reviewerList) / 2) + 1) return 'revert'; if($rejectCount >= floor(count($reviewerList) / 2) + 1) return 'reject'; - if(strpos($results, 'clarify') !== false) return 'clarify'; - if(strpos($results, 'revert') !== false) return 'revert'; - if(strpos($results, 'reject') !== false) return 'reject'; + if(str_contains($results, 'clarify')) return 'clarify'; + if(str_contains($results, 'revert')) return 'revert'; + if(str_contains($results, 'reject')) return 'reject'; } return $finalResult; @@ -5148,7 +5040,7 @@ class storyModel extends model * @access public * @return array */ - public function setStatusByReviewResult($story, $oldStory, $result, $reason = 'cancel') + public function setStatusByReviewResult(object $story, object $oldStory, string $result, string $reason = 'cancel'): object { if($result == 'pass') $story->status = 'active'; @@ -5166,18 +5058,9 @@ class storyModel extends model $story->title = $this->dao->select('title')->from(TABLE_STORYSPEC)->where('story')->eq($story->id)->andWHere('version')->eq($oldStory->version - 1)->fetch('title'); /* Delete versions that is after this version. */ - $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($story->id)->andWHere('version')->in($oldStory->version)->exec(); - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($story->id)->andWhere('version')->in($oldStory->version)->exec(); - - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($twinID)->andWHere('version')->in($oldStory->version)->exec(); - $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($twinID)->andWhere('version')->in($oldStory->version)->exec(); - } - } + $twinsIdList = $storyID . ($oldStory->twins ? ",{$oldStory->twins}" : ''); + $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->in($twinsIdList)->andWHere('version')->in($oldStory->version)->exec(); + $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->in($twinsIdList)->andWhere('version')->in($oldStory->version)->exec(); } if($result == 'reject') @@ -5207,23 +5090,25 @@ class storyModel extends model * @access public * @return int|string */ - public function recordReviewAction($story, $result = '', $reason = '') + public function recordReviewAction(object $story, string $comment = ''): int { - $isSuperReviewer = strpos(',' . trim(zget($this->config->story, 'superReviewers', ''), ',') . ',', ',' . $this->app->user->account . ','); + $isSuperReviewer = $this->storyTao->isSuperReviewer(); + $result = zget($story, 'result', ''); + $reason = zget($story, 'closedReason', ''); - $comment = isset($_POST['comment']) ? $this->post->comment : ''; - - if($isSuperReviewer !== false and $this->app->rawMethod != 'edit') return $this->loadModel('action')->create('story', $story->id, 'Reviewed', $comment, ucfirst($result) . '|superReviewer'); + $this->loadModel('action'); + if($isSuperReviewer and $this->app->rawMethod != 'edit') return $this->action->create('story', $story->id, 'Reviewed', $comment, ucfirst($result) . '|superReviewer'); $reasonParam = $result == 'reject' ? ',' . $reason : ''; - $actionID = !empty($result) ? $this->loadModel('action')->create('story', $story->id, 'Reviewed', $comment, ucfirst($result) . $reasonParam) : ''; + $actionID = 0; + if(!empty($result)) $actionID = $this->action->create('story', $story->id, 'Reviewed', $comment, ucfirst($result) . $reasonParam); if(isset($story->finalResult)) { - if($story->finalResult == 'reject') $this->action->create('story', $story->id, 'ReviewRejected'); - if($story->finalResult == 'pass') $this->action->create('story', $story->id, 'ReviewPassed'); - if($story->finalResult == 'clarify') $this->action->create('story', $story->id, 'ReviewClarified'); - if($story->finalResult == 'revert') $this->action->create('story', $story->id, 'ReviewReverted'); + if($story->finalResult == 'reject') return $this->action->create('story', $story->id, 'ReviewRejected'); + if($story->finalResult == 'pass') return $this->action->create('story', $story->id, 'ReviewPassed'); + if($story->finalResult == 'clarify') return $this->action->create('story', $story->id, 'ReviewClarified'); + if($story->finalResult == 'revert') return $this->action->create('story', $story->id, 'ReviewReverted'); } return $actionID; @@ -5238,12 +5123,12 @@ class storyModel extends model * @access public * @return object */ - public function updateStoryByReview($storyID, $oldStory, $story) + public function updateStoryByReview(int $storyID, object $oldStory, object $story): object { - $isSuperReviewer = strpos(',' . trim(zget($this->config->story, 'superReviewers', ''), ',') . ',', ',' . $this->app->user->account . ','); - if($isSuperReviewer !== false) return $this->superReview($storyID, $oldStory, $story); + $isSuperReviewer = $this->storyTao->isSuperReviewer(); + if($isSuperReviewer) return $this->superReview($storyID, $oldStory, $story); - $reviewerList = $this->getReviewerPairs($storyID, $oldStory->version); + $reviewerList = $this->getReviewerPairs($storyID, (int)$oldStory->version); $reviewedBy = explode(',', trim($story->reviewedBy, ',')); if(!array_diff(array_keys($reviewerList), $reviewedBy)) { @@ -5265,33 +5150,20 @@ class storyModel extends model * @access public * @return object */ - public function superReview($storyID, $oldStory, $story, $result = '', $reason = '') + public function superReview(int $storyID, object $oldStory, object $story, string $result = '', string $reason = ''): object { - $result = isset($_POST['result']) ? $this->post->result : $result; + $result = isset($story->result) ? $story->result : $result; if(empty($result)) return $story; - $reason = isset($_POST['closedReason']) ? $_POST['closedReason'] : $reason; + $reason = isset($story->closedReason) ? $story->closedReason : $reason; $story = $this->setStatusByReviewResult($story, $oldStory, $result, $reason); $this->dao->delete()->from(TABLE_STORYREVIEW) - ->where('story')->eq($storyID) + ->where('story')->in($storyID . ($oldStory->twins ? ",{$oldStory->twins}" : '')) ->andWhere('version')->eq($oldStory->version) ->andWhere('result')->eq('') ->exec(); - /* Sync twins. */ - if(!empty($oldStory->twins)) - { - foreach(explode(',', trim($oldStory->twins, ',')) as $twinID) - { - $this->dao->delete()->from(TABLE_STORYREVIEW) - ->where('story')->eq($twinID) - ->andWhere('version')->eq($oldStory->version) - ->andWhere('result')->eq('') - ->exec(); - } - } - return $story; } @@ -5624,9 +5496,9 @@ class storyModel extends model * @access public * @return void */ - public function syncTwins($storyID, $twins, $changes, $operate) + public function syncTwins(int $storyID, string $twins, array $changes, string $operate): void { - if(empty($twins) or empty($changes)) return; + if(empty($twins) || empty($changes)) return; /* Get the fields and values to be synchronized. */ $syncFieldList = array(); @@ -5635,25 +5507,25 @@ class storyModel extends model $fieldName = $changeInfo['field']; $fieldValue = $changeInfo['new']; - if(strpos('product,branch,module,plan,stage,stagedBy,spec,verify,files,reviewers', $fieldName) !== false) continue; + if(str_contains(',product,branch,module,plan,stage,stagedBy,spec,verify,files,reviewers,', ",{$fieldName},")) continue; $syncFieldList[$fieldName] = $fieldValue; } - if(empty($syncFieldList)) return; /* Synchronize and record dynamics. */ $this->loadModel('action'); - $twins = explode(',', trim($twins, ',')); - foreach($twins as $twinID) + foreach(explode(',', $twins) as $twinID) { - $this->dao->update(TABLE_STORY)->data($syncFieldList)->where('id')->eq((int)$twinID)->exec(); - if(!dao::isError()) - { - $this->setStage($twinID); + $twinID = (int)$twinID; + if(empty($twinID)) continue; - $actionID = $this->action->create('story', $twinID, 'synctwins', '', "$operate|$storyID"); - $this->action->logHistory($actionID, $changes); - } + $this->dao->update(TABLE_STORY)->data($syncFieldList)->where('id')->eq($twinID)->exec(); + if(dao::isError()) continue; + + $this->setStage($twinID); + + $actionID = $this->action->create('story', $twinID, 'synctwins', '', "$operate|$storyID"); + $this->action->logHistory($actionID, $changes); } } diff --git a/module/story/tao.php b/module/story/tao.php index b646509016..bc027fe2d1 100644 --- a/module/story/tao.php +++ b/module/story/tao.php @@ -1652,4 +1652,16 @@ class storyTao extends storyModel return $actions; } + + /** + * 检查当前账号是否是超级评审人。 + * Check account is super reviewer or not. + * + * @access protected + * @return bool + */ + protected function isSuperReviewer(): bool + { + return str_contains(',' . zget($this->config->story, 'superReviewers', '') . ',', ",{$this->app->user->account},"); + } } diff --git a/module/story/zen.php b/module/story/zen.php index eb50c50c22..fd0b4bbe2c 100644 --- a/module/story/zen.php +++ b/module/story/zen.php @@ -671,9 +671,9 @@ class storyZen extends story * @param int $executionID * @param int $bugID * @access protected - * @return object + * @return object|false */ - protected function buildStoryForCreate(int $executionID, int $bugID): object + protected function buildStoryForCreate(int $executionID, int $bugID): object|false { $fields = $this->config->story->form->create; $editorFields = array_keys(array_filter(array_map(function($config){return $config['control'] == 'editor';}, $fields))); @@ -700,6 +700,55 @@ class storyZen extends story return $this->loadModel('file')->processImgURL($storyData, $editorFields, $this->post->uid); } + /** + * 构建评审需求数据。 + * Build story for review + * + * @param int $storyID + * @access protected + * @return object|false + */ + protected function buildStoryForReview(int $storyID): object|false + { + $now = helper::now(); + $fields = $this->config->story->form->review; + foreach(explode(',', trim($this->config->story->create->requiredFields, ',')) as $field) + { + if($field == 'comment' && !$this->post->comment) + { + dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->comment); + return false; + } + if(isset($fields[$field])) $fields[$field]['required'] = true; + } + if($this->post->result == false) + { + dao::$errors[] = $this->lang->story->mustChooseResult; + return false; + } + + $editorFields = array_keys(array_filter(array_map(function($config){return $config['control'] == 'editor';}, $fields))); + $result = $this->post->result; + $closedReason = $this->post->closedReason; + $oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); + $storyData = form::data($fields) + ->setDefault('lastEditedBy', $this->app->user->account) + ->setDefault('lastEditedDate', $now) + ->removeIF($result != 'reject', 'closedReason,duplicateStory,childStories') + ->removeIF($result == 'reject' && $closedReason != 'duplicate', 'duplicateStory') + ->removeIF($result == 'reject' && $closedReason != 'subdivided', 'childStories') + ->get(); + + if($oldStory->assignedTo != $storyData->assignedTo) $storyData->assignedDate = $now; + $storyData->reviewedBy = implode(',', array_unique(explode(',', $oldStory->reviewedBy . ',' . $this->app->user->account))); + + if($result == 'reject' && empty($closedReason)) dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->story->rejectedReason); + if($result == 'reject' && $closedReason == 'duplicate' && empty($storyData->duplicateStory)) dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory); + if(dao::isError()) return false; + + return $this->loadModel('file')->processImgURL($storyData, $editorFields, $this->post->uid); + } + /** * 构建批量创建需求数据。 * Build stories for batch create. @@ -778,7 +827,7 @@ class storyZen extends story protected function responseAfterCreateInModal(string $message, int $executionID = 0): array|false { if(!isonlybody()) return false; - if($this->app->tab != 'execution') return array('result' => 'success', 'message' => $message, 'reload' => true, 'closedModal' => true); + if($this->app->tab != 'execution') return array('result' => 'success', 'message' => $message, 'load' => true, 'closedModal' => true); $executionID = $executionID ? $executionID : $this->session->execution; $execution = $this->execution->getByID($executionID); @@ -862,6 +911,35 @@ class storyZen extends story return $this->createLink('product', 'browse', "productID=$productID&branch=$branch&browseType=unclosed&queryID=0&storyType=$storyType"); } + /** + * 获取评审需求后的跳转地址。 + * Get after review location. + * + * @param int $storyID + * @param string $storyType + * @param string $from + * @access protected + * @return string + */ + protected function getAfterReviewLocation(int $storyID, string $storyType = 'story', string $from = ''): string + { + if($from == 'project') return helper::createLink('projectstory', 'view', "storyID={$storyID}"); + if($from != 'execution') return helper::createLink('story', 'view', "storyID={$storyID}&version=0¶m=0&storyType={$storyType}"); + + $execution = $this->execution->getByID($this->session->execution); + + $module = 'story'; + $method = 'view'; + $params = "storyID=$storyID&version=0¶m={$this->session->execution}&storyType=$storyType"; + if($execution->multiple) + { + $module = 'execution'; + $method = 'storyView'; + $params = "storyID=$storyID"; + } + return helper::createLink($module, $method, $params); + } + /** * 根据上传图片,批量创建需求时,获取初始化需求数据。 * Get data from upload images.