* @package productplan * @version $Id: model.php 4639 2013-04-11 02:06:35Z chencongzhi520@gmail.com $ * @link http://www.zentao.net */ ?> dao->findByID((int)$planID)->from(TABLE_PRODUCTPLAN)->fetch(); if(!$plan) return false; $plan = $this->loadModel('file')->replaceImgURL($plan, 'desc'); if($setImgSize) $plan->desc = $this->file->setImgSize($plan->desc); return $plan; } /** * Get plans by idList * * @param int $planIDList * @access public * @return array */ public function getByIDList($planIDList) { return $this->dao->select('*')->from(TABLE_PRODUCTPLAN)->where('id')->in($planIDList)->orderBy('begin desc')->fetchAll('id'); } /** * Get last plan. * * @param int $productID * @param int $branch * @param int $parent * @access public * @return object */ public function getLast($productID, $branch = '', $parent = 0) { $branchQuery = ''; if($branch !== '') { $branchQuery .= '('; $branchCount = count(explode(',', $branch)); foreach(explode(',', $branch) as $index => $branchID) { $branchQuery .= "CONCAT(',', branch, ',') LIKE '%,$branchID,%'"; if($index < $branchCount - 1) $branchQuery .= ' AND '; } $branchQuery .= ')'; } return $this->dao->select('*')->from(TABLE_PRODUCTPLAN) ->where('deleted')->eq(0) ->beginIF($parent <= 0)->andWhere('parent')->le((int)$parent)->fi() ->beginIF($parent > 0)->andWhere('parent')->eq((int)$parent)->fi() ->andWhere('product')->eq((int)$productID) ->andWhere('end')->ne($this->config->productplan->future) ->beginIF($branch !== '' and !empty($branchQuery))->andWhere($branchQuery)->fi() ->orderBy('end desc') ->limit(1) ->fetch(); } /** * Get list * * @param int $product * @param int $branch * @param string $browseType all|undone|wait|doing|done|closed * @param object $pager * @param string $orderBy * @param string $param skipparent * @param int $queryID * @access public * @return object */ public function getList($product = 0, $branch = 0, $browseType = 'undone', $pager = null, $orderBy = 'begin_desc', $param = '', $queryID = 0) { if(!empty($queryID)) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('productplanQuery', $query->sql); $this->session->set('productplanForm', $query->form); } else { $this->session->set('productplanQuery', ' 1 = 1'); } } else { if($this->session->productplanQuery == false) $this->session->set('productplanQuery', ' 1 = 1'); } $productplanQuery = $this->session->productplanQuery; $date = date('Y-m-d'); $products = (strpos($param, 'noproduct') !== false and empty($product)) ? $this->loadModel('product')->getList() : array(0); $plans = $this->dao->select('*')->from(TABLE_PRODUCTPLAN) ->where('deleted')->eq(0) ->beginIF(strpos($param, 'noproduct') === false or !empty($product))->andWhere('product')->eq($product)->fi() ->beginIF(strpos($param, 'noproduct') !== false and empty($product))->andWhere('product')->in(array_keys($products))->fi() ->beginIF(!empty($branch) and $branch != 'all')->andWhere('branch')->eq($branch)->fi() ->beginIF(strpos(',all,undone,bySearch,review,', ",$browseType,") === false)->andWhere('status')->eq($browseType)->fi() ->beginIF($browseType == 'undone')->andWhere('status')->in('wait,doing')->fi() ->beginIF($browseType == 'bySearch')->andWhere($productplanQuery)->fi() ->beginIF($browseType == 'review')->andWhere("FIND_IN_SET('{$this->app->user->account}', reviewers)")->fi() ->beginIF(strpos($param, 'skipparent') !== false)->andWhere('parent')->ne(-1)->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); if(!empty($plans)) { $plans = $this->reorder4Children($plans); $planIdList = array_keys($plans); $planProjects = array(); foreach($planIdList as $planID) { $planProjects[$planID] = $this->dao->select('t1.project,t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('1=1') ->beginIF(strpos($param, 'noproduct') === false or !empty($product))->andWhere('t1.product')->eq($product)->fi() ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.plan')->like(",$planID,") ->andWhere('t2.type')->in('sprint,stage,kanban') ->orderBy('project_desc') ->fetchAll('project'); } $storyCountInTable = $this->dao->select('plan,count(story) as count')->from(TABLE_PLANSTORY)->where('plan')->in($planIdList)->groupBy('plan')->fetchPairs('plan', 'count'); $product = $this->loadModel('product')->getById($product); if(!empty($product) and $product->type == 'normal') { $storyGroups = $this->dao->select('id,plan,estimate')->from(TABLE_STORY) ->where("plan")->in($planIdList) ->andWhere('deleted')->eq(0) ->fetchGroup('plan', 'id'); } $bugs = $this->dao->select('*')->from(TABLE_BUG)->where("plan")->in($planIdList)->andWhere('deleted')->eq(0)->fetchGroup('plan', 'id'); $parentStories = $parentBugs = $parentHour = array(); foreach($plans as $plan) { if(!empty($product) and $product->type == 'normal') { $stories = zget($storyGroups, $plan->id, array()); $storyPairs = array(); foreach($stories as $story) $storyPairs[$story->id] = $story->estimate; } else { $storyPairs = $this->dao->select('id,estimate')->from(TABLE_STORY) ->where("CONCAT(',', plan, ',')")->like("%,{$plan->id},%") ->andWhere('deleted')->eq(0) ->fetchPairs('id', 'estimate'); } $plan->stories = count($storyPairs); $plan->bugs = isset($bugs[$plan->id]) ? count($bugs[$plan->id]) : 0; $plan->hour = array_sum($storyPairs); $plan->projects = zget($planProjects, $plan->id, ''); $plan->expired = $plan->end < $date ? true : false; /* Sync linked stories. */ if(!isset($storyCountInTable[$plan->id]) or $storyCountInTable[$plan->id] != $plan->stories) { $this->dao->delete()->from(TABLE_PLANSTORY)->where('plan')->eq($plan->id)->exec(); $order = 1; foreach($storyPairs as $storyID => $estimate) { $planStory = new stdclass(); $planStory->plan = $plan->id; $planStory->story = $storyID; $planStory->order = $order ++; $this->dao->replace(TABLE_PLANSTORY)->data($planStory)->exec(); } } if(!isset($parentStories[$plan->parent])) $parentStories[$plan->parent] = 0; if(!isset($parentBugs[$plan->parent])) $parentBugs[$plan->parent] = 0; if(!isset($parentHour[$plan->parent])) $parentHour[$plan->parent] = 0; $parentStories[$plan->parent] += $plan->stories; $parentBugs[$plan->parent] += $plan->bugs; $parentHour[$plan->parent] += $plan->hour; } unset($parentStories[0]); unset($parentBugs[0]); unset($parentHour[0]); foreach($parentStories as $parentID => $count) { if(!isset($plans[$parentID])) continue; $plan = $plans[$parentID]; $plan->stories += $count; $plan->bugs += $parentBugs[$parentID]; $plan->hour += $parentHour[$parentID]; } } return $plans; } /** * Get top plan pairs. * * @param int $productID * @param int $exclude * @access public * @return array */ public function getTopPlanPairs($productID, $exclude = '') { $planPairs = $this->dao->select("id,title")->from(TABLE_PRODUCTPLAN) ->where('product')->eq($productID) ->andWhere('parent')->le(0) ->andWhere('deleted')->eq(0) ->beginIF($exclude)->andWhere('status')->notin($exclude) ->orderBy('id_desc') ->fetchPairs(); return $planPairs; } /** * Get plan pairs. * * @param array|int $product * @param int|string|array $branch * @param string $param unexpired|noclosed * @param bool $skipParent * @access public * @return array */ public function getPairs($product = 0, $branch = '', $param = '', $skipParent = false) { $this->app->loadLang('branch'); $date = date('Y-m-d'); $branchQuery = ''; if($branch !== '' and $branch != 'all') { $branchQuery .= '('; $branchCount = count(explode(',', $branch)); foreach(explode(',', $branch) as $index => $branchID) { $branchQuery .= "FIND_IN_SET('$branchID', t1.branch)"; if($index < $branchCount - 1) $branchQuery .= ' OR '; } $branchQuery .= ')'; } $plans = $this->dao->select('t1.id,t1.title,t1.parent,t1.begin,t1.end,t3.type as productType,t1.branch')->from(TABLE_PRODUCTPLAN)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t3.id=t1.product') ->where('t1.product')->in($product) ->andWhere('t1.deleted')->eq(0) ->beginIF(!empty($branchQuery))->andWhere($branchQuery)->fi() ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('t1.end')->ge($date)->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi() ->orderBy('t1.begin desc') ->fetchAll('id'); $plans = $this->reorder4Children($plans); $plans = $this->relationBranch($plans); $planPairs = array(); foreach($plans as $plan) { if($skipParent and $plan->parent == '-1') continue; if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; $planPairs[$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == $this->config->productplan->future and $plan->end == $this->config->productplan->future) $planPairs[$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; if($plan->productType != 'normal') $planPairs[$plan->id] = $planPairs[$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main); } return array('' => '') + $planPairs; } /** * Get plan pairs for story. * * @param array|int $product * @param int $branch * @param string $param skipParent|withMainPlan|unexpired|noclosed * @access public * @return array */ public function getPairsForStory($product = 0, $branch = '', $param = '') { $date = date('Y-m-d'); $param = strtolower($param); $branch = strpos($param, 'withmainplan') !== false ? "0,$branch" : $branch; $branchQuery = ''; if($branch !== '' and $branch != 'all') { $branchQuery .= '('; $branchCount = count(explode(',', $branch)); foreach(explode(',', $branch) as $index => $branchID) { $branchQuery .= "FIND_IN_SET('$branchID', branch)"; if($index < $branchCount - 1) $branchQuery .= ' OR '; } $branchQuery .= ')'; } $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->in($product) ->andWhere('deleted')->eq(0) ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('end')->ge($date)->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() ->beginIF($branch !== 'all' and $branch !== '' and !empty($branchQuery))->andWhere($branchQuery)->fi() ->orderBy('begin desc') ->fetchAll('id'); $plans = $this->reorder4Children($plans); $planPairs = array(); $parentTitle = array(); foreach($plans as $plan) { if($plan->parent == '-1') { $parentTitle[$plan->id] = $plan->title; if(strpos($param, 'skipparent') !== false) continue; } if($plan->parent > 0 and isset($parentTitle[$plan->parent])) $plan->title = $parentTitle[$plan->parent] . ' /' . $plan->title; $planPairs[$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == $this->config->productplan->future and $plan->end == $this->config->productplan->future) $planPairs[$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; } return array('' => '') + $planPairs; } /** * Get plans for products * * @param array $products * @access public * @return void */ public function getForProducts($products) { $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->in(array_keys($products)) ->andWhere('deleted')->eq(0) ->orderBy('begin desc') ->fetchAll('id'); $plans = $this->reorder4Children($plans); $planPairs = array(); $parentTitle = array(); foreach($plans as $plan) { if($plan->parent == '-1') $parentTitle[$plan->id] = $plan->title; if($plan->parent > 0 and isset($parentTitle[$plan->parent])) $plan->title = $parentTitle[$plan->parent] . ' /' . $plan->title; $planPairs[$plan->id] = $plan->title; } return array('' => '') + $planPairs; } /** * Get plan group by product id list. * * @param string|array $products * @param string $param skipParent|unexpired * @param string $field name * @param string $orderBy id_desc|begin_desc * @access public * @return array */ public function getGroupByProduct($products = '', $param = '', $field = 'name', $orderBy = 'id_desc') { $date = date('Y-m-d'); $param = strtolower($param); $plans = $this->dao->select('t1.*,t2.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t2.id=t1.product') ->where('t1.deleted')->eq(0) ->beginIF($products)->andWhere('t1.product')->in($products)->fi() ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('t1.end')->ge($date)->fi() ->orderBy('t1.' . $orderBy) ->fetchAll('id'); if(!empty($plans) and $field == 'name') $plans = $this->reorder4Children($plans); $plans = $this->relationBranch($plans); $parentTitle = array(); $planGroup = array(); foreach($plans as $plan) { foreach(explode(',', $plan->branch) as $branch) { if(!isset($planGroup[$plan->product][$branch])) $planGroup[$plan->product][$branch] = array('' => ''); if($plan->parent == '-1' and strpos($param, 'skipparent') !== false) continue 2; if($field == 'name') { if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; $planGroup[$plan->product][$branch][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == $this->config->productplan->future and $plan->end == $this->config->productplan->future) $planGroup[$plan->product][$branch][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; if($plan->productType != 'normal') $planGroup[$plan->product][$branch][$plan->id] = $planGroup[$plan->product][$branch][$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main); } else { $planGroup[$plan->product][$branch][$plan->id] = $plan; } } } return $planGroup; } /** * Get Children plan. * * @param int $planID * @access public * @return array */ public function getChildren($planID) { return $this->dao->select('*')->from(TABLE_PRODUCTPLAN)->where('parent')->eq((int)$planID)->andWhere('deleted')->eq('0')->fetchAll(); } /** * Get plan list by story id list. * * @param string|array $storyIdList * @access public * @return array */ public function getPlansByStories($storyIdList) { if(empty($storyIdList)) return array(); return $this->dao->select('t2.id as storyID, t3.*')->from(TABLE_PLANSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t2.id=t1.story') ->leftJoin(TABLE_PRODUCTPLAN)->alias('t3')->on('t3.id=t1.plan') ->where('t2.id')->in($storyIdList) ->fetchGroup('storyID', 'id'); } /** * Get branch plan pairs. * * @param int $productID * @param array $branches * @param string $param * @param bool $skipParent * @access public * @return array */ public function getBranchPlanPairs($productID, $branches = '', $param = '', $skipParent = false) { $branchQuery = ''; if($branches !== '' and $branches !== 'all') { $branchQuery .= '('; if(!is_array($branches)) $branches = explode(',', $branches); foreach($branches as $branchID) { $branchQuery .= "CONCAT(',', branch, ',') LIKE '%,$branchID,%'"; if($branchID != end($branches)) $branchQuery .= ' OR '; } $branchQuery .= ')'; } $date = date('Y-m-d'); $param = strtolower($param); $plans = $this->dao->select('parent,branch,id,title,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->eq($productID) ->andWhere('deleted')->eq(0) ->beginIF(!empty($branchQuery))->andWhere($branchQuery)->fi() ->beginIF($branches != '')->andWhere('branch')->in($branches)->fi() ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('end')->ge($date)->fi() ->orderBy('begin desc') ->fetchAll('id'); $planPairs = array(); foreach($plans as $planID => $plan) { foreach(explode(',', $plan->branch) as $branch) { if($skipParent and $plan->parent == '-1') continue 2; if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; $planPairs[$branch][$planID] = $plan->title . ' [' . $plan->begin . '~' . $plan->end . ']'; if($branch !== BRANCH_MAIN) $planPairs[BRANCH_MAIN][$planID] = $plan->title . ' [' . $plan->begin . '~' . $plan->end . ']'; } } return $planPairs; } /** * Create a plan. * * @access public * @return int */ public function create() { $plan = fixer::input('post')->stripTags($this->config->productplan->editor->create['id'], $this->config->allowedTags) ->setIF($this->post->future || empty($_POST['begin']), 'begin', $this->config->productplan->future) ->setIF($this->post->future || empty($_POST['end']), 'end', $this->config->productplan->future) ->setDefault('createdBy', $this->app->user->account) ->setDefault('createdDate', helper::now()) ->setDefault('branch', 0) ->join('branch', ',') ->remove('delta,uid,future') ->get(); $product = $this->loadModel('product')->getByID($plan->product); if($product->type != 'normal' and !isset($_POST['branch'])) { $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch); } if($plan->parent > 0) { $parentPlan = $this->getByID($plan->parent); if($parentPlan->begin != $this->config->productplan->future) { if($plan->begin < $parentPlan->begin) dao::$errors['begin'] = sprintf($this->lang->productplan->beginLetterParent, $parentPlan->begin); } if($parentPlan->end != $this->config->productplan->future) { if($plan->end !== $this->config->productplan->future and $plan->end > $parentPlan->end) dao::$errors['end'] = sprintf($this->lang->productplan->endGreaterParent, $parentPlan->end); } } if(!$this->post->future and strpos($this->config->productplan->create->requiredFields, 'begin') !== false and empty($_POST['begin'])) { dao::$errors['begin'] = sprintf($this->lang->error->notempty, $this->lang->productplan->begin); } if(!$this->post->future and strpos($this->config->productplan->create->requiredFields, 'end') !== false and empty($_POST['end'])) { dao::$errors['end'] = sprintf($this->lang->error->notempty, $this->lang->productplan->end); } if(dao::isError()) return false; $plan = $this->loadModel('file')->processImgURL($plan, $this->config->productplan->editor->create['id'], $this->post->uid); $this->dao->insert(TABLE_PRODUCTPLAN)->data($plan) ->autoCheck() ->batchCheck($this->config->productplan->create->requiredFields, 'notempty') ->checkIF(!$this->post->future && !empty($_POST['begin']) && !empty($_POST['end']), 'end', 'ge', $plan->begin) ->checkFlow() ->exec(); if(!dao::isError()) { $planID = $this->dao->lastInsertID(); $this->file->updateObjectID($this->post->uid, $planID, 'plan'); $this->loadModel('score')->create('productplan', 'create', $planID); if(!empty($plan->parent) and $parentPlan->parent == '0') { $plan->id = $planID; $this->transferStoriesAndBugs($plan); } return $planID; } } /** * Update a plan. * * @param int $planID * @access public * @return array */ public function update($planID) { $oldPlan = $this->getByID($planID); $plan = fixer::input('post')->stripTags($this->config->productplan->editor->edit['id'], $this->config->allowedTags) ->setIF($this->post->future or empty($_POST['begin']), 'begin', $this->config->productplan->future) ->setIF($this->post->future or empty($_POST['end']), 'end', $this->config->productplan->future) ->setDefault('branch', 0) ->join('branch', ',') ->add('id', $planID) ->remove('delta,uid,future') ->get(); $product = $this->loadModel('product')->getByID($oldPlan->product); if($product->type != 'normal') { if(!isset($_POST['branch'])) { $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch); } else { if($oldPlan->parent == -1) { /* Get branches of child plans. */ $childBranches = array(); $childPlans = $this->getChildren($oldPlan->id); $branchPairs = $this->loadModel('branch')->getPairs($oldPlan->product); foreach($childPlans as $children) { foreach(explode(',', $children->branch) as $childBranchID) $childBranches[$childBranchID] = $childBranchID; } /* Get branches of parent plan that cannot delete. */ $canDeleteBranch = true; $deleteBranches = ''; foreach(explode(',', $oldPlan->branch) as $oldBranchID) { if(strpos(",$plan->branch,", ",$oldBranchID,") === false and isset($childBranches[$oldBranchID])) { $canDeleteBranch = false; if(isset($branchPairs[$oldBranchID])) $deleteBranches .= "{$branchPairs[$oldBranchID]},"; } } $this->lang->productplan->deleteBranchTip = str_replace('@branch@', $this->lang->product->branchName[$product->type], $this->lang->productplan->deleteBranchTip); if(!$canDeleteBranch) dao::$errors[] = sprintf($this->lang->productplan->deleteBranchTip, trim($deleteBranches, ',')); } } } $parentPlan = $this->getByID($plan->parent); $futureTime = $this->config->productplan->future; if($plan->parent > 0) { if($parentPlan->begin !== $futureTime) { if($plan->begin < $parentPlan->begin) dao::$errors['begin'] = sprintf($this->lang->productplan->beginLetterParent, $parentPlan->begin); } if($parentPlan->end !== $futureTime) { if($plan->end !== $futureTime and $plan->end > $parentPlan->end) dao::$errors['end'] = sprintf($this->lang->productplan->endGreaterParent, $parentPlan->end); } } elseif($oldPlan->parent == -1 and ($plan->begin != $futureTime or $plan->end != $futureTime)) { $childPlans = $this->getChildren($planID); $minBegin = $plan->begin; $maxEnd = $plan->end; foreach($childPlans as $childID => $childPlan) { $childPlan = isset($plans[$childID]) ? $plans[$childID] : $childPlan; if($childPlan->begin < $minBegin) $minBegin = $childPlan->begin; if($childPlan->end > $maxEnd) $maxEnd = $childPlan->end; } if($minBegin < $plan->begin and $minBegin != $futureTime) dao::$errors['begin'] = sprintf($this->lang->productplan->beginGreaterChildTip, $oldPlan->title, $plan->begin, $minBegin); if($maxEnd > $plan->end and $maxEnd != $futureTime) dao::$errors['end'] = sprintf($this->lang->productplan->endLetterChildTip, $oldPlan->title, $plan->end, $maxEnd); } if(dao::isError()) return false; $plan = $this->loadModel('file')->processImgURL($plan, $this->config->productplan->editor->edit['id'], $this->post->uid); $this->dao->update(TABLE_PRODUCTPLAN)->data($plan) ->autoCheck() ->batchCheck($this->config->productplan->edit->requiredFields, 'notempty') ->checkIF(!$this->post->future && !empty($_POST['begin']) && !empty($_POST['end']), 'end', 'ge', $plan->begin) ->checkFlow() ->where('id')->eq((int)$planID) ->exec(); if(dao::isError()) return false; if($plan->parent > 0) $this->updateParentStatus($plan->parent); if($oldPlan->parent > 0) $this->updateParentStatus($oldPlan->parent); if(!dao::isError()) { $this->file->updateObjectID($this->post->uid, $planID, 'plan'); if(!empty($plan->parent) and $parentPlan->parent == '0') $this->transferStoriesAndBugs($plan); return common::createChanges($oldPlan, $plan); } } /** * Update a plan's status. * * @param int $planID * @param string $status doing|done|closed * @param string $action started|finished|closed|activated * @access public * @return bool */ public function updateStatus($planID, $status = '', $action = '') { $oldPlan = $this->getByID($planID); $plan = new stdclass(); $plan->status = $status; if($status == 'closed' and $this->post->closedReason) $plan->closedReason = $this->post->closedReason; if($status !== 'closed') $plan->closedReason = ''; $this->dao->update(TABLE_PRODUCTPLAN)->data($plan)->where('id')->eq($planID)->exec(); if(dao::isError()) return false; $changes = common::createChanges($oldPlan, $plan); $comment = $this->post->comment ? $this->post->comment : ''; $actionID = $this->loadModel('action')->create('productplan', $planID, $action, $comment); $this->action->logHistory($actionID, $changes); if($oldPlan->parent > 0) $this->updateParentStatus($oldPlan->parent); return !dao::isError(); } /** * Update a parent plan's status. * * @param int $parentID * @access public * @return bool */ public function updateParentStatus($parentID) { $parent = $this->getByID($parentID); $childStatus = $this->dao->select('status')->from(TABLE_PRODUCTPLAN)->where('parent')->eq($parentID)->andWhere('deleted')->eq(0)->fetchPairs(); /* If the subplan is empty, update the plan. */ if(empty($childStatus)) { $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq(0)->set('status')->eq('wait')->where('id')->eq($parentID)->exec(); return; } if(count($childStatus) == 1 and isset($childStatus['wait'])) { return; } elseif(count($childStatus) == 1 and isset($childStatus['closed'])) { if($parent->status != 'closed') { $parentStatus = 'closed'; $parentAction = 'closedbychild'; } } elseif(!isset($childStatus['wait']) and !isset($childStatus['doing'])) { if($parent->status != 'done') { $parentStatus = 'done'; $parentAction = 'finishedbychild'; } } else { if($parent->status != 'doing') { $parentStatus = 'doing'; $parentAction = $this->app->rawMethod == 'create' ? 'createchild' : 'activatedbychild'; } } if(isset($parentStatus)) { $this->dao->update(TABLE_PRODUCTPLAN)->set('status')->eq($parentStatus)->where('id')->eq($parentID)->exec(); $this->loadModel('action')->create('productplan', $parentID, $parentAction, '', $parentAction); } return !dao::isError(); } /** * Batch update plan. * * @param int $productID * @access public * @return array */ public function batchUpdate($productID) { $data = fixer::input('post')->get(); $oldPlans = $this->getByIDList($data->id); $this->app->loadClass('purifier', true); $config = HTMLPurifier_Config::createDefault(); $config->set('Cache.DefinitionImpl', null); $purifier = new HTMLPurifier($config); $plans = array(); $extendFields = $this->getFlowExtendFields(); $product = $this->loadModel('product')->getByID($productID); foreach($data->id as $planID) { if($product->type != 'normal' and $oldPlans[$planID]->parent != '-1' and !isset($data->branch[$planID])) { $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); return helper::end(js::error(sprintf($this->lang->error->notempty, $this->lang->product->branch))); } $isFuture = isset($data->future[$planID]) ? true : false; if(isset($data->status[$planID]) and $data->status[$planID] != 'wait') $isFuture = false; $plan = new stdclass(); $plan->branch = isset($data->branch[$planID]) ? join(',', $data->branch[$planID]) : $oldPlans[$planID]->branch; $plan->title = $data->title[$planID]; $plan->begin = isset($data->begin[$planID]) ? $data->begin[$planID] : ''; $plan->end = isset($data->end[$planID]) ? $data->end[$planID] : ''; $plan->status = isset($data->status[$planID]) ? $data->status[$planID] : $oldPlans[$planID]->status; $plan->parent = $oldPlans[$planID]->parent; $plan->id = $planID; if(empty($plan->title)) return helper::end(js::alert(sprintf($this->lang->productplan->errorNoTitle, $planID))); if($plan->begin > $plan->end and !empty($plan->end)) return helper::end(js::alert(sprintf($this->lang->productplan->beginGeEnd, $planID))); if($plan->begin == '') $plan->begin = $this->config->productplan->future; if($plan->end == '') $plan->end = $this->config->productplan->future; foreach($extendFields as $extendField) { $plan->{$extendField->field} = $this->post->{$extendField->field}[$planID]; if(is_array($plan->{$extendField->field})) $plan->{$extendField->field} = join(',', $plan->{$extendField->field}); $plan->{$extendField->field} = htmlSpecialString($plan->{$extendField->field}); } $plans[$planID] = $plan; } $changes = array(); $parents = array(); foreach($plans as $planID => $plan) { $parentID = $oldPlans[$planID]->parent; /* Determine whether the begin and end dates of the parent plan and the child plan are correct. */ if($parentID > 0) { $parent = isset($plans[$parentID]) ? $plans[$parentID] : $this->getByID($parentID); if($parent->begin != $this->config->productplan->future and $plan->begin != $this->config->productplan->future and $plan->begin < $parent->begin) { return helper::end(js::alert(sprintf($this->lang->productplan->beginLetterParentTip, $planID, $plan->begin, $parent->begin))); } elseif($parent->end != $this->config->productplan->future and $plan->end != $this->config->productplan->future and $plan->end > $parent->end) { return helper::end(js::alert(sprintf($this->lang->productplan->endGreaterParentTip, $planID, $plan->end, $parent->end))); } } elseif($parentID == -1 and $plan->begin != $this->config->productplan->future) { $childPlans = $this->dao->select('*')->from(TABLE_PRODUCTPLAN)->where('parent')->eq($planID)->andWhere('deleted')->eq(0)->fetchAll('id'); $minBegin = $plan->begin; $maxEnd = $plan->end; foreach($childPlans as $childID => $childPlan) { $childPlan = isset($plans[$childID]) ? $plans[$childID] : $childPlan; if($childPlan->begin < $minBegin and $minBegin != $this->config->productplan->future) $minBegin = $childPlan->begin; if($childPlan->end > $maxEnd and $maxEnd != $this->config->productplan->future) $maxEnd = $childPlan->end; } if($minBegin < $plan->begin and $minBegin != $this->config->productplan->future) return helper::end(js::alert(sprintf($this->lang->productplan->beginGreaterChildTip, $planID, $plan->begin, $minBegin))); if($maxEnd > $plan->end and $maxEnd != $this->config->productplan->future) return helper::end(js::alert(sprintf($this->lang->productplan->endLetterChildTip, $planID, $plan->end, $maxEnd))); } $change = common::createChanges($oldPlans[$planID], $plan); if($change) { if($parentID > 0 and !isset($parents[$parentID])) $parents[$parentID] = $parentID; $this->dao->update(TABLE_PRODUCTPLAN)->data($plan)->autoCheck()->checkFlow()->where('id')->eq($planID)->exec(); if(dao::isError()) return helper::end(js::error(dao::getError())); $changes[$planID] = $change; } } foreach($parents as $parent) $this->updateParentStatus($parent); return $changes; } /** * Batch change the status of productplan. * * @param array $planIDList * @param string $status * @access public * @return array */ public function batchChangeStatus($status) { $this->loadModel('action'); $allChanges = array(); $planIDList = $this->post->planIDList; if($status == 'closed') $closedReasons = $this->post->closedReasons; $oldPlans = $this->getByIDList($planIDList, $status); foreach($planIDList as $planID) { $oldPlan = $oldPlans[$planID]; if($status == $oldPlan->status) continue; $plan = new stdclass(); $plan->status = $status; if($status == 'closed') $plan->closedReason = $closedReasons[$planID]; if($status !== 'closed') $plan->closedReason = ''; $this->dao->update(TABLE_PRODUCTPLAN)->data($plan)->autoCheck()->where('id')->eq((int)$planID)->exec(); if($oldPlan->parent > 0) $this->updateParentStatus($oldPlan->parent); if(!dao::isError()) { $allChanges[$planID] = common::createChanges($oldPlan, $plan); } else { return print(js::error(dao::getError())); } } foreach($allChanges as $planID => $changes) { $comment = $this->post->comments[$planID] ? $this->post->comments[$planID] : ''; $actionID = $this->action->create('productplan', $planID, 'edited', $comment); $this->action->logHistory($actionID, $changes); } return $allChanges; } /** * Check date for plan. * * @param object $plan * @param string $begin * @param string $end * @access public * @return void */ public function checkDate4Plan($plan, $begin, $end) { if($plan->parent == -1) { $childPlans = $this->dao->select('*')->from(TABLE_PRODUCTPLAN)->where('parent')->eq($plan->id)->andWhere('deleted')->eq(0)->fetchAll(); $minBegin = $begin; $maxEnd = $end; foreach($childPlans as $childPlan) { if($childPlan->begin < $minBegin and $minBegin != $this->config->productplan->future) $minBegin = $childPlan->begin; if($childPlan->end > $maxEnd and $maxEnd != $this->config->productplan->future) $maxEnd = $childPlan->end; } if($minBegin < $begin and $begin != $this->config->productplan->future) dao::$errors['begin'] = sprintf($this->lang->beginGreaterChild, $minBegin); if($maxEnd > $end and $end != $this->config->productplan->future) dao::$errors['end'] = sprintf($this->lang->endLetterChild, $maxEnd); } elseif($plan->parent > 0) { $parentPlan = $this->getByID($plan->parent); if($begin < $parentPlan->begin and $parentPlan->begin != $this->config->productplan->future) dao::$errors['begin'] = sprintf($this->lang->productplan->beginLetterParent, $parentPlan->begin); if($end > $parentPlan->end and $parentPlan->end != $this->config->productplan->future) dao::$errors['end'] = sprintf($this->lang->productplan->endGreaterParent, $parentPlan->end); } } /** * Change parent field by planID. * * @param int $planID * @access public * @return void */ public function changeParentField($planID) { $plan = $this->getById($planID); if($plan->parent <= 0) return true; $childCount = count($this->getChildren($plan->parent)); $parent = $childCount == 0 ? '0' : '-1'; $parentPlan = $this->dao->select('*')->from(TABLE_PRODUCTPLAN)->where('id')->eq($plan->parent)->andWhere('deleted')->eq(0)->fetch(); if($parentPlan) { $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq($parent)->where('id')->eq((int)$plan->parent)->exec(); } else { $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('0')->where('id')->eq((int)$planID)->exec(); } } /** * Link stories. * * @param int $planID * @access public * @return void */ public function linkStory($planID) { $this->loadModel('story'); $this->loadModel('action'); $stories = $this->story->getByList($this->post->stories); $plan = $this->getByID($planID); foreach($this->post->stories as $storyID) { if(!isset($stories[$storyID])) continue; $story = $stories[$storyID]; if(strpos(",$story->plan,", ",{$planID},") !== false) continue; /* Update the plan linked with the story and the order of the story in the plan. */ $this->dao->update(TABLE_STORY)->set("plan")->eq($planID)->where('id')->eq((int)$storyID)->exec(); $this->story->updateStoryOrderOfPlan($storyID, $planID, $story->plan); $this->action->create('story', $storyID, 'linked2plan', '', $planID); $this->story->setStage($storyID); } $this->action->create('productplan', $planID, 'linkstory', '', implode(',', $this->post->stories)); } /** * Unlink story * * @param int $storyID * @access public * @return void */ public function unlinkStory($storyID, $planID) { $story = $this->dao->findByID($storyID)->from(TABLE_STORY)->fetch(); $plans = array_unique(explode(',', trim(str_replace(",$planID,", ',', ',' . trim($story->plan) . ','). ','))); $this->dao->update(TABLE_STORY)->set('plan')->eq(join(',', $plans))->where('id')->eq((int)$storyID)->exec(); /* Delete the story in the sort of the plan. */ $this->loadModel('story'); $this->story->updateStoryOrderOfPlan($storyID, '', $planID); $this->story->setStage($storyID); $this->loadModel('action')->create('story', $storyID, 'unlinkedfromplan', '', $planID); } /** * Link bugs. * * @param int $planID * @access public * @return void */ public function linkBug($planID) { $this->loadModel('story'); $this->loadModel('action'); $bugs = $this->loadModel('bug')->getByList($this->post->bugs); foreach($this->post->bugs as $bugID) { if(!isset($bugs[$bugID])) continue; $bug = $bugs[$bugID]; if($bug->plan == $planID) continue; $this->dao->update(TABLE_BUG)->set('plan')->eq($planID)->where('id')->eq((int)$bugID)->exec(); $this->action->create('bug', $bugID, 'linked2plan', '', $planID); } $this->action->create('productplan', $planID, 'linkbug', '', implode(',', $this->post->bugs)); } /** * Unlink bug. * * @param int $bugID * @access public * @return void */ public function unlinkBug($bugID) { $planID = $this->dao->findByID($bugID)->from(TABLE_BUG)->fetch('plan'); $this->dao->update(TABLE_BUG)->set('plan')->eq(0)->where('id')->eq((int)$bugID)->exec(); $this->loadModel('action')->create('bug', $bugID, 'unlinkedfromplan', '', $planID); } /** * Link project. * * @param int $projectID * @param array $newPlans * @access public * @return void */ public function linkProject($projectID, $newPlans) { $this->loadModel('execution'); foreach($newPlans as $planID) { $planStories = $planProducts = array(); $planStory = $this->loadModel('story')->getPlanStories($planID); if(!empty($planStory)) { $projectProducts = $this->loadModel('project')->getBranchesByProject($projectID); foreach($planStory as $id => $story) { $projectBranches = zget($projectProducts, $story->product, array()); if($story->status != 'active' or (!empty($story->branch) and !empty($projectBranches) and !isset($projectBranches[$story->branch]))) { unset($planStory[$id]); continue; } $planProducts[$story->id] = $story->product; } $planStories = array_keys($planStory); $this->execution->linkStory($projectID, $planStories, $planProducts); } } } /** * Reorder for children plans. * * @param array $plans * @access public * @return array */ public function reorder4Children($plans) { /* Get children and unset. */ $childrenPlans = array(); foreach($plans as $plan) { if($plan->parent > 0) { $childrenPlans[$plan->parent][$plan->id] = $plan; if(isset($plans[$plan->parent])) unset($plans[$plan->id]); } } if(!empty($childrenPlans)) { /* Append to parent plan. */ $reorderedPlans = array(); foreach($plans as $plan) { $reorderedPlans[$plan->id] = $plan; if(isset($childrenPlans[$plan->id])) { $plan->children = count($childrenPlans[$plan->id]); foreach($childrenPlans[$plan->id] as $childrenPlan) $reorderedPlans[$childrenPlan->id] = $childrenPlan; } } $plans = $reorderedPlans; } return $plans; } /** * Get relation branch for plans. * * @param array $plans * @access public * @return array */ public function relationBranch($plans) { if(empty($plans)) return $plans; $this->app->loadLang('branch'); $branchMap = $this->dao->select('id, name')->from(TABLE_BRANCH) ->where('status')->eq('active') ->andWhere('deleted')->eq('0') ->fetchPairs('id', 'name'); $branchMap[BRANCH_MAIN] = $this->lang->branch->main; foreach($plans as &$plan) { if($plan->branch) { $branchName = ''; foreach(explode(',', $plan->branch) as $planBranch) { $branchName .= isset($branchMap[$planBranch]) ? $branchMap[$planBranch] : ''; $branchName .= ','; } $plan->branchName = trim($branchName, ','); } else { $plan->branchName = $this->lang->branch->main; } } return $plans; } /** * Judge an action is clickable or not. * * @param object $plan * @param string $action * @param string $module * @access public * @return void */ public static function isClickable($plan, $action, $module = 'productplan') { $action = strtolower($action); $clickable = commonModel::hasPriv($module, $action); if(!$clickable) return false; switch($action) { case 'create' : if($plan->parent > 0 or strpos('done,closed', $plan->status) !== false) return false; break; case 'start' : if($plan->status != 'wait' or $plan->parent < 0) return false; break; case 'finish' : if($plan->status != 'doing' or $plan->parent < 0) return false; break; case 'close' : if($plan->status == 'closed' or $plan->parent < 0) return false; break; case 'activate' : if($plan->status == 'wait' or $plan->status == 'doing' or $plan->parent < 0) return false; break; case 'delete' : if($plan->parent < 0) return false; break; } return true; } /** * Build operate menu. * * @param object $plan * @param string $type * @access public * @return string */ public function buildOperateMenu($plan, $type = 'view') { $params = "planID=$plan->id"; $canStart = common::hasPriv('productplan', 'start'); $canFinish = common::hasPriv('productplan', 'finish'); $canClose = common::hasPriv('productplan', 'close'); $canCreateExec = common::hasPriv('execution', 'create'); $canLinkStory = common::hasPriv('productplan', 'linkStory', $plan); $canLinkBug = common::hasPriv('productplan', 'linkBug', $plan); $canEdit = common::hasPriv('productplan', 'edit'); $canCreateChild = common::hasPriv('productplan', 'create'); $canDelete = common::hasPriv('productplan', 'delete'); $menu = ''; $menu .= $this->buildMenu('productplan', 'start', $params, $plan, $type, 'play', 'hiddenwin', '', false, '', $this->lang->productplan->startAB); $menu .= $this->buildMenu('productplan', 'finish', $params, $plan, $type, 'checked', 'hiddenwin', '', false, '', $this->lang->productplan->finishAB); $menu .= $this->buildMenu('productplan', 'close', $params, $plan, $type, 'off', 'hiddenwin', 'iframe', true, '', $this->lang->productplan->closeAB); if($type == 'view') $menu .= $this->buildMenu('productplan', 'activate', $params, $plan, $type, 'magic', 'hiddenwin', '', false, '', $this->lang->productplan->activateAB); if($type == 'browse') { $canClickExecution = true; if($plan->parent < 0 || $plan->expired || in_array($plan->status, array('done', 'closed')) || !common::hasPriv('execution', 'create', $plan)) $canClickExecution = false; if($canClickExecution) { $product = $this->loadModel('product')->getById($plan->product); $branchList = $this->loadModel('branch')->getList($plan->product, 0, 'all'); $branchStatusList = array(); foreach($branchList as $productBranch) $branchStatusList[$productBranch->id] = $productBranch->status; if($product->type != 'normal') { $branchStatus = isset($branchStatusList[$plan->branch]) ? $branchStatusList[$plan->branch] : ''; if($branchStatus == 'closed') $canClickExecution = false; } } if($canClickExecution) { $menu .= html::a('#projects', '', '', "data-toggle='modal' data-id='$plan->id' onclick='getPlanID(this, $plan->branch)' class='btn' title='{$this->lang->productplan->createExecution}'"); } elseif($canCreateExec) { $menu .= ""; } if($type == 'browse' and ($canStart or $canFinish or $canClose or $canCreateExec) and ($canLinkStory or $canLinkBug or $canEdit or $canCreateChild or $canDelete)) { $menu .= "
"; } if($canLinkStory and $plan->parent >= 0) { $menu .= $this->buildMenu($this->app->rawModule, 'view', "{$params}&type=story&orderBy=id_desc&link=true", $plan, $type, 'link', '', '', '', '', $this->lang->productplan->linkStory); } elseif($canLinkStory) { $menu .= ""; } if($canLinkBug and $plan->parent >= 0) { $menu .= $this->buildMenu($this->app->rawModule, 'view', "{$params}&type=bug&orderBy=id_desc&link=true", $plan, $type, 'bug', '', '', '', '', $this->lang->productplan->linkBug); } elseif($canLinkBug) { $menu .= ""; } $menu .= $this->buildMenu($this->app->rawModule, 'edit', $params, $plan, $type); } $menu .= $this->buildMenu($this->app->rawModule, 'create', "product={$plan->product}&branch={$plan->branch}&parent={$plan->id}", $plan, $type, 'split', '', '', '', '', $this->lang->productplan->children); if($type == 'browse' and ($canLinkStory or $canLinkBug or $canEdit or $canCreateChild) and $canDelete) { $menu .= ""; } if($type == 'browse') $menu .= $this->buildMenu('productplan', 'delete', "{$params}&confirm=no", $plan, $type, 'trash', 'hiddenwin', '', '', $this->lang->productplan->delete); if($type == 'view') { $menu .= ""; $menu .= $this->buildFlowMenu('productplan', $plan, $type, 'direct'); $menu .= ""; $editClickable = $this->buildMenu($this->app->rawModule, 'edit', $params, $plan, $type, '', '', '', '', '', '', false); $deleteClickable = $this->buildMenu('productplan', 'delete', $params, $plan, $type, '', '', '', '', '', '', false); if($canEdit and $editClickable) $menu .= html::a(helper::createLink('productplan', 'edit', $params), " " . $this->lang->edit, '', "class='btn btn-link' title='{$this->lang->edit}'"); if($canDelete and $deleteClickable) $menu .= html::a(helper::createLink('productplan', 'delete', $params), " " . $this->lang->delete, '', "class='btn btn-link' title='{$this->lang->delete}' target='hiddenwin'"); } return $menu; } /** * Build search form. * * @param int $queryID * @param string $actionURL * @param object $product * @access public * @return void */ public function buildSearchForm($queryID, $actionURL, $product) { $this->config->productplan->search['actionURL'] = $actionURL; $this->config->productplan->search['queryID'] = $queryID; if($product->type != 'normal') $this->config->productplan->search['params']['branch']['values'] = array('' => '', '0' => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($product->id, 'noempty'); if($product->type == 'normal') unset($this->config->productplan->search['fields']['branch']); $this->loadModel('search')->setSearchParams($this->config->productplan->search); } /** * Transfer stories and bugs to new plan. * * @param object $plan * @access public * @return void */ public function transferStoriesAndBugs($plan) { $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('-1')->where('id')->eq($plan->parent)->andWhere('parent')->eq('0')->exec(); /* Transfer stories linked with the parent plan to the child plan. */ $stories = $this->dao->select('*')->from(TABLE_STORY)->where("CONCAT(',', plan, ',')")->like("%,{$plan->parent},%")->fetchAll('id'); $unlinkStories = array(); foreach($stories as $storyID => $story) { if(!empty($story->branch) and strpos(",$plan->branch,", ",$story->branch,") === false) { $unlinkStories[$storyID] = $storyID; $storyPlan = str_replace(",{$plan->parent},", ',', ",$story->plan,"); } else { $storyPlan = str_replace(",{$plan->parent},", ",$plan->id,", ",$story->plan,"); } $storyPlan = trim($storyPlan, ','); $this->dao->update(TABLE_STORY)->set('plan')->eq($storyPlan)->where('id')->eq($storyID)->exec(); } if(!empty($unlinkStories)) $this->dao->delete()->from(TABLE_PLANSTORY)->where('plan')->eq($plan->parent)->andWhere('story')->in($unlinkStories)->exec(); $this->dao->update(TABLE_PLANSTORY)->set('plan')->eq($plan->id)->where('plan')->eq($plan->parent)->exec(); /* Transfer bugs linked with the parent plan to the child plan. */ $bugs = $this->dao->select('*')->from(TABLE_BUG)->where('plan')->eq($plan->parent)->fetchAll('id'); $unlinkBugs = array(); foreach($bugs as $bugID => $bug) { if(!empty($bug->branch) and strpos(",$plan->branch,", ",$bug->branch,") === false) $unlinkBugs[$bugID] = $bugID; } if(!empty($unlinkBugs)) $this->dao->update(TABLE_BUG)->set('plan')->eq(0)->where('plan')->eq($plan->parent)->andWhere('id')->in($unlinkBugs)->exec(); $this->dao->update(TABLE_BUG)->set('plan')->eq($plan->id)->where('plan')->eq($plan->parent)->exec(); } /** * Get plan list by ids. * * @param array $planIds * @param boo $order * @access public * @return array */ public function getListByIds($planIds, $order = false) { $plans = $this->dao->select('*')->from(TABLE_PRODUCTPLAN) ->where('id')->in($planIds) ->orderBy('begin desc') ->fetchAll('id'); if($order) $plans = $this->relationBranch($plans); return $plans; } }