* @package execution * @version $Id: model.php 5118 2013-07-12 07:41:41Z chencongzhi520@gmail.com $ * @link http://www.zentao.net */ class executionModel extends model { /* The members every linking. */ const LINK_MEMBERS_ONE_TIME = 20; /** * Check the privilege. * * @param int $executionID * @access public * @return bool */ public function checkPriv($executionID) { if(empty($executionID)) return false; /* If is admin, return true. */ if($this->app->user->admin) return true; return (strpos(",{$this->app->user->view->sprints},", ",{$executionID},") !== false); } /** * Show accessDenied response. * * @access private * @return void */ public function accessDenied() { if(defined('TUTORIAL')) return true; echo(js::alert($this->lang->execution->accessDenied)); if(!$this->server->http_referer) return print(js::locate(helper::createLink('execution', 'all'))); $loginLink = $this->config->requestType == 'GET' ? "?{$this->config->moduleVar}=user&{$this->config->methodVar}=login" : "user{$this->config->requestFix}login"; if(strpos($this->server->http_referer, $loginLink) !== false) return print(js::locate(helper::createLink('execution', 'all'))); return print(js::locate(helper::createLink('execution', 'all'))); } /** * Get features of execution. * * @param object $execution * @access public * @return array */ public function getExecutionFeatures($execution) { $features = array('story' => true, 'task' => true, 'qa' => true, 'devops' => true, 'burn' => true, 'build' => true, 'other' => true, 'plan' => true); /* Unset story, bug, build and testtask if type is ops. */ if($execution->lifetime == 'ops') { $features['story'] = false; $features['qa'] = false; $features['build'] = false; $features['burn'] = false; } elseif(!empty($execution->attribute)) { $features['other'] = false; if(in_array($execution->attribute, array('request', 'design', 'review'))) { $features['qa'] = false; $features['devops'] = false; $features['build'] = false; if(in_array($execution->attribute, array('request', 'review'))) { $features['story'] = false; $features['plan'] = false; } } } if(isset($execution->projectInfo) and in_array($execution->projectInfo->model, array('waterfall', 'kanban', 'waterfallplus')) and empty($execution->projectInfo->hasProduct)) { $features['plan'] = false; } return $features; } /** * Set menu. * * @param int $executionID * @param int $buildID * @param string $extra * @access public * @return void */ public function setMenu($executionID, $buildID = 0, $extra = '') { $execution = $this->getByID($executionID); if(!$execution) return; if($execution and $execution->type == 'kanban') { global $lang; $lang->executionCommon = $lang->execution->kanban; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; $this->lang->execution->menu = new stdclass(); $this->lang->execution->menu->kanban = array('link' => "{$this->lang->kanban->common}|execution|kanban|executionID=%s", 'subModule' => 'task'); $this->lang->execution->menu->CFD = array('link' => "{$this->lang->execution->CFD}|execution|cfd|executionID=%s"); $this->lang->execution->menu->build = array('link' => "{$this->lang->build->common}|execution|build|executionID=%s"); $this->lang->execution->menu->settings = array('link' => "{$this->lang->settings}|execution|view|executionID=%s", 'subModule' => 'personnel', 'alias' => 'edit,manageproducts,team,whitelist,addwhitelist,managemembers', 'class' => 'dropdown dropdown-hover'); $this->lang->execution->dividerMenu = ''; $this->lang->execution->menu->settings['subMenu'] = new stdclass(); $this->lang->execution->menu->settings['subMenu']->view = array('link' => "{$this->lang->overview}|execution|view|executionID=%s", 'subModule' => 'view', 'alias' => 'edit,start,suspend,putoff,close'); $this->lang->execution->menu->settings['subMenu']->products = array('link' => "{$this->lang->productCommon}|execution|manageproducts|executionID=%s"); $this->lang->execution->menu->settings['subMenu']->team = array('link' => "{$this->lang->team->common}|execution|team|executionID=%s", 'alias' => 'managemembers'); $this->lang->execution->menu->settings['subMenu']->whitelist = array('link' => "{$this->lang->whitelist}|execution|whitelist|executionID=%s", 'subModule' => 'personnel', 'alias' => 'addwhitelist'); } $project = $this->loadModel('project')->getByID($execution->project); if($execution->type == 'stage' or (!empty($project) and $project->model == 'waterfallplus')) unset($this->lang->execution->menu->settings['subMenu']->products); if(!$this->app->user->admin and strpos(",{$this->app->user->view->sprints},", ",$executionID,") === false and !defined('TUTORIAL') and $executionID != 0) return print(js::error($this->lang->execution->accessDenied) . js::locate('back')); $executions = $this->getPairs(0, 'all', 'nocode'); if(!$executionID and $this->session->execution) $executionID = $this->session->execution; if(!$executionID or !in_array($executionID, array_keys($executions))) $executionID = key($executions); $this->session->set('execution', $executionID, $this->app->tab); if($execution and $execution->type == 'stage') { global $lang; $this->app->loadLang('project'); $lang->executionCommon = $lang->project->stage; include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php'; } if(isset($execution->acl) and $execution->acl != 'private') unset($this->lang->execution->menu->settings['subMenu']->whitelist); /* Redjust menus. */ $features = $this->getExecutionFeatures($execution); if(!$features['story']) unset($this->lang->execution->menu->story); if(!$features['story']) unset($this->lang->execution->menu->view['subMenu']->groupTask); if(!$features['story']) unset($this->lang->execution->menu->view['subMenu']->tree); if(!$features['qa']) unset($this->lang->execution->menu->qa); if(!$features['devops']) unset($this->lang->execution->menu->devops); if(!$features['build']) unset($this->lang->execution->menu->build); if(!$features['burn']) unset($this->lang->execution->menu->burn); if(!$features['other']) unset($this->lang->execution->menu->other); if(!$features['story'] and $this->config->edition == 'open') unset($this->lang->execution->menu->view); if($executions and (!isset($executions[$executionID]) or !$this->checkPriv($executionID))) $this->accessDenied(); $moduleName = $this->app->getModuleName(); $methodName = $this->app->getMethodName(); if($this->cookie->executionMode == 'noclosed' and $execution and ($execution->status == 'done' or $execution->status == 'closed')) { setcookie('executionMode', 'all'); $this->cookie->executionMode = 'all'; } if(empty($execution->hasProduct)) unset($this->lang->execution->menu->settings['subMenu']->products); $this->lang->switcherMenu = $this->getSwitcher($executionID, $this->app->rawModule, $this->app->rawMethod); common::setMenuVars('execution', $executionID); $this->loadModel('project')->setNoMultipleMenu($executionID); if(isset($this->lang->execution->menu->storyGroup)) unset($this->lang->execution->menu->storyGroup); if(isset($this->lang->execution->menu->story['dropMenu']) and $methodName == 'storykanban') { unset($this->lang->execution->menu->story['dropMenu']); $this->lang->execution->menu->story['link'] = str_replace(array($this->lang->common->story, 'story'), array($this->lang->SRCommon, 'storykanban'), $this->lang->execution->menu->story['link']); } } /** * Create the select code of executions. * * @param array $executions * @param int $executionID * @param int $buildID * @param string $currentModule * @param string $currentMethod * @param string $extra * @access public * @return string */ public function select($executions, $executionID, $buildID, $currentModule, $currentMethod, $extra = '') { if(!$executions or !$executionID) return false; $isMobile = $this->app->viewType == 'mhtml'; setCookie("lastExecution", $executionID, $this->config->cookieLife, $this->config->webRoot, '', false, true); $currentExecution = $this->getById($executionID); if(isset($currentExecution->type) and $currentExecution->type == 'program') return; if($currentExecution->project) $project = $this->loadModel('project')->getByID($currentExecution->project); if(isset($project) and $project->model == 'waterfall') { $productID = $this->loadModel('product')->getProductIDByProject($project->id); $productName = $this->dao->findByID($productID)->from(TABLE_PRODUCT)->fetch('name'); $currentExecution->name = $productName . '/' . $currentExecution->name; } $dropMenuLink = helper::createLink('execution', 'ajaxGetDropMenu', "executionID=$executionID&module=$currentModule&method=$currentMethod&extra=$extra"); $currentExecutionName = ''; if(isset($currentExecution->name)) $currentExecutionName = $currentExecution->name; $output = "
"; if($isMobile) $output = "{$currentExecution->name} "; return $output; } /** * Get execution tree menu. * * @access public * @return void */ public function tree() { $products = $this->loadModel('product')->getPairs('nocode', 0); $productGroup = $this->getProductGroupList(); $executionTree = ""; return $executionTree; } /** * Save the execution id user last visited to session. * * @param int $executionID * @param array $executions * @access public * @return int */ public function saveState($executionID, $executions) { if(defined('TUTORIAL')) return $executionID; /* When the cookie and session do not exist, get it from the database. */ if(empty($executionID) and isset($this->config->execution->lastExecution) and isset($executions[$this->config->execution->lastExecution])) { $this->session->set('execution', $this->config->execution->lastExecution, $this->app->tab); $this->setProjectSession($this->session->execution); return $this->session->execution; } if($executionID == 0 and $this->cookie->lastExecution) { /* Execution link is execution-task. */ $executionID = (int)$this->cookie->lastExecution; $executionID = in_array($executionID, array_keys($executions)) ? $executionID : key($executions); } if($executionID == 0 and $this->session->execution) $executionID = $this->session->execution; if($executionID == 0) $executionID = key($executions); $this->session->set('execution', (int)$executionID, $this->app->tab); if(!isset($executions[$executionID])) { $this->session->set('execution', key($executions), $this->app->tab); if($executionID) { $execution = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->andWhere('type')->in('sprint,stage,kanban')->fetch(); if(empty($execution)) return js::error($this->lang->notFound); if(strpos(",{$this->app->user->view->sprints},", ",{$executionID},") === false) $this->accessDenied(); } } $this->setProjectSession($this->session->execution); return $this->session->execution; } /** * Set project into session. * * @param int $executionID * @access public * @return void */ public function setProjectSession($executionID) { $execution = $this->getByID($executionID); if(!empty($execution)) $this->session->set('project', $execution->project, $this->app->tab); } /** * Create a execution. * * @param int $copyExecutionID * * @access public * @return bool|int */ public function create($copyExecutionID = '') { $this->lang->execution->team = $this->lang->execution->teamname; if(empty($_POST['project'])) { dao::$errors['message'][] = $this->lang->execution->projectNotEmpty; return false; } $this->checkBeginAndEndDate($_POST['project'], $_POST['begin'], $_POST['end']); if(dao::isError()) return false; if($_POST['products']) { $this->app->loadLang('project'); $multipleProducts = $this->loadModel('product')->getMultiBranchPairs(); foreach($_POST['products'] as $index => $productID) { if(isset($multipleProducts[$productID]) and !isset($_POST['branch'][$index])) { dao::$errors[] = $this->lang->project->emptyBranch; return false; } } } /* Determine whether to add a sprint or a stage according to the model of the execution. */ $project = $this->loadModel('project')->getByID($_POST['project']); $type = 'sprint'; if($project) $type = zget($this->config->execution->modelList, $project->model, 'sprint'); if($project->model == 'waterfall' or $project->model == 'waterfallplus') { $_POST['products'] = array_filter($_POST['products']); if(empty($_POST['products'])) dao::$errors['products0'] = $this->lang->project->errorNoProducts; if(isset($this->config->setPercent) and $this->config->setPercent == 1) $this->checkWorkload('create', $_POST['percent'], $project); if(dao::isError()) return false; } $this->config->execution->create->requiredFields .= ',project'; /* Judge workdays is legitimate. */ $workdays = helper::diffDate($_POST['end'], $_POST['begin']) + 1; if(isset($_POST['days']) and $_POST['days'] > $workdays) { dao::$errors['days'] = sprintf($this->lang->project->workdaysExceed, $workdays); return false; } /* Get the data from the post. */ $sprint = fixer::input('post') ->setDefault('status', 'wait') ->setDefault('openedBy', $this->app->user->account) ->setDefault('openedDate', helper::now()) ->setDefault('openedVersion', $this->config->version) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setDefault('days', '0') ->setDefault('team', $this->post->name) ->setDefault('parent', $this->post->project) ->setIF($this->post->parent, 'parent', $this->post->parent) ->setIF($this->post->heightType == 'auto', 'displayCards', 0) ->setIF(!isset($_POST['whitelist']), 'whitelist', '') ->setIF($this->post->acl == 'open', 'whitelist', '') ->join('whitelist', ',') ->setDefault('type', $type) ->stripTags($this->config->execution->editor->create['id'], $this->config->allowedTags) ->remove('products, workDays, delta, branch, uid, plans, teams, teamMembers, contactListMenu, heightType') ->get(); if(!empty($sprint->parent) and ($sprint->project == $sprint->parent)) { $project = $this->loadModel('project')->getByID($sprint->parent); $sprint->hasProduct = $project->hasProduct; } if(isset($_POST['heightType']) and $this->post->heightType == 'custom') { if(!$this->loadModel('kanban')->checkDisplayCards($sprint->displayCards)) return; } /* Check the workload format and total. */ if(!empty($sprint->percent) and isset($this->config->setPercent) and $this->config->setPercent == 1) $this->checkWorkload('create', $sprint->percent, $sprint->project); /* Set planDuration and realDuration. */ if($this->config->edition == 'max') { $sprint->planDuration = $this->loadModel('programplan')->getDuration($sprint->begin, $sprint->end); if(!empty($sprint->realBegan) and !empty($sprint->realEnd)) $sprint->realDuration = $this->loadModel('programplan')->getDuration($sprint->realBegan, $sprint->realEnd); } $sprint = $this->loadModel('file')->processImgURL($sprint, $this->config->execution->editor->create['id'], $this->post->uid); /* Redefines the language entries for the fields in the project table. */ foreach(explode(',', $this->config->execution->create->requiredFields) as $field) { if(isset($this->lang->execution->$field)) $this->lang->project->$field = $this->lang->execution->$field; } /* Replace required language. */ if($this->app->tab == 'project') { $this->lang->project->name = $this->lang->execution->name; $this->lang->project->code = $this->lang->execution->code; } else { $this->lang->project->name = $this->lang->execution->execName; $this->lang->project->code = $this->lang->execution->execCode; } $this->lang->error->unique = $this->lang->error->repeat; $sprintProject = isset($sprint->project) ? $sprint->project : '0'; $this->dao->insert(TABLE_EXECUTION)->data($sprint) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->execution->create->requiredFields, 'notempty') ->checkIF(!empty($sprint->name), 'name', 'unique', "`type` in ('sprint','stage', 'kanban') and `project` = $sprintProject and `deleted` = '0'") ->checkIF(!empty($sprint->code), 'code', 'unique', "`type` in ('sprint','stage', 'kanban') and `deleted` = '0'") ->checkIF($sprint->begin != '', 'begin', 'date') ->checkIF($sprint->end != '', 'end', 'date') ->checkIF($sprint->end != '', 'end', 'ge', $sprint->begin) ->checkFlow() ->exec(); /* Add the creater to the team. */ if(!dao::isError()) { $executionID = $this->dao->lastInsertId(); $today = helper::today(); $teamMembers = array(); if((isset($project) and $project->model != 'kanban') or empty($project)) $this->loadModel('kanban')->createExecutionLane($executionID); /* Api create infinitus stages. */ if(isset($sprint->parent) and ($sprint->parent != $sprint->project) and $sprint->type == 'stage') { $parent = $this->getByID($sprint->parent); $grade = $parent->grade + 1; $path = rtrim($parent->path, ',') . ",{$executionID}"; $this->dao->update(TABLE_EXECUTION)->set('path')->eq($path)->set('grade')->eq($grade)->where('id')->eq($executionID)->exec(); } /* Save order. */ $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec(); $this->file->updateObjectID($this->post->uid, $executionID, 'execution'); /* Update the path. */ $this->setTreePath($executionID); $this->updateProducts($executionID); /* Set team of execution. */ $members = isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array(); array_push($members, $sprint->PO, $sprint->QD, $sprint->PM, $sprint->RD, $sprint->openedBy); $members = array_unique($members); $roles = $this->loadModel('user')->getUserRoles(array_values($members)); foreach($members as $account) { if(empty($account)) continue; $member = new stdClass(); $member->root = $executionID; $member->type = 'execution'; $member->account = $account; $member->role = zget($roles, $account, ''); $member->join = $today; $member->days = $sprint->days; $member->hours = $this->config->execution->defaultWorkhours; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); $teamMembers[$account] = $member; } $this->addProjectMembers($sprint->project, $teamMembers); /* Create doc lib. */ $this->app->loadLang('doc'); $lib = new stdclass(); $lib->execution = $executionID; $lib->name = $type == 'stage' ? str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->doclib->main['execution']) : $this->lang->doclib->main['execution']; $lib->type = 'execution'; $lib->main = '1'; $lib->acl = 'default'; $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); $whitelist = explode(',', $sprint->whitelist); $this->loadModel('personnel')->updateWhitelist($whitelist, 'sprint', $executionID); if($sprint->acl != 'open') $this->updateUserView($executionID); if(!dao::isError()) $this->loadModel('score')->create('program', 'createguide', $executionID); return $executionID; } } /** * Update a execution. * * @param int $executionID * @access public * @return array|bool */ public function update($executionID) { /* Convert executionID format and get oldExecution. */ $executionID = (int)$executionID; $oldExecution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch(); /* Judgment of required items. */ if($oldExecution->type != 'stage' and $this->post->code == '' and isset($this->config->setCode) and $this->config->setCode == 1) { dao::$errors['code'] = sprintf($this->lang->error->notempty, $this->lang->execution->code); return false; } /* Judge workdays is legitimate. */ $this->app->loadLang('project'); $workdays = helper::diffDate($_POST['end'], $_POST['begin']) + 1; if(isset($_POST['days']) and $_POST['days'] > $workdays) { dao::$errors['days'] = sprintf($this->lang->project->workdaysExceed, $workdays); return false; } if($_POST['products']) { $this->app->loadLang('project'); $multipleProducts = $this->loadModel('product')->getMultiBranchPairs(); if(is_string($_POST['branch']) !== false) $_POST['branch'] = json_decode($_POST['branch'], true); foreach($_POST['products'] as $index => $productID) { if(isset($multipleProducts[$productID]) and !isset($_POST['branch'][$index])) { dao::$errors[] = $this->lang->project->emptyBranch; return false; } } } /* Get the data from the post. */ $execution = fixer::input('post') ->add('id', $executionID) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) ->setIF($this->post->heightType == 'auto', 'displayCards', 0) ->setIF(helper::isZeroDate($this->post->begin), 'begin', '') ->setIF(helper::isZeroDate($this->post->end), 'end', '') ->setIF(!isset($_POST['whitelist']), 'whitelist', '') ->setIF($this->post->status == 'closed' and $oldExecution->status != 'closed', 'closedDate', helper::now()) ->setIF($this->post->status == 'suspended' and $oldExecution->status != 'suspended', 'suspendedDate', helper::today()) ->setIF($oldExecution->type == 'stage', 'project', $oldExecution->project) ->setDefault('days', '0') ->setDefault('team', $this->post->name) ->join('whitelist', ',') ->stripTags($this->config->execution->editor->edit['id'], $this->config->allowedTags) ->remove('products, branch, uid, plans, syncStories, contactListMenu, teamMembers, heightType') ->get(); if(isset($_POST['heightType']) and $this->post->heightType == 'custom') { if(!$this->loadModel('kanban')->checkDisplayCards($execution->displayCards)) return; } if(in_array($execution->status, array('closed', 'suspended'))) $this->computeBurn($executionID); if(empty($execution->project) or $execution->project == $oldExecution->project) $this->checkBeginAndEndDate($oldExecution->project, $execution->begin, $execution->end, !empty($execution->parent) ? $execution : $oldExecution); if(dao::isError()) return false; /* Child stage inherits parent stage permissions. */ if(!isset($execution->acl)) $execution->acl = $oldExecution->acl; $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->edit['id'], $this->post->uid); /* Check the workload format and total. */ if(!empty($execution->percent) and isset($this->config->setPercent) and $this->config->setPercent == 1) $this->checkWorkload('update', $execution->percent, $oldExecution); /* Set planDuration and realDuration. */ if($this->config->edition == 'max') { $execution->planDuration = $this->loadModel('programplan')->getDuration($execution->begin, $execution->end); if(!empty($execution->realBegan) and !empty($execution->realEnd)) $execution->realDuration = $this->programplan->getDuration($execution->realBegan, $execution->realEnd); } /* Redefines the language entries for the fields in the project table. */ foreach(explode(',', $this->config->execution->edit->requiredFields) as $field) { if(isset($this->lang->execution->$field)) $this->lang->project->$field = $this->lang->execution->$field; if($oldExecution->type == 'stage' and $field == 'name') $this->lang->project->name = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->project->name); } $relatedExecutionsID = $this->getRelatedExecutions($executionID); $relatedExecutionsID = !empty($relatedExecutionsID) ? implode(',', array_keys($relatedExecutionsID)) : '0'; /* Update data. */ $this->lang->error->unique = $this->lang->error->repeat; $executionProject = isset($execution->project) ? $execution->project : $oldExecution->project; $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->execution->edit->requiredFields, 'notempty') ->checkIF($execution->begin != '', 'begin', 'date') ->checkIF($execution->end != '', 'end', 'date') ->checkIF($execution->end != '', 'end', 'ge', $execution->begin) ->checkIF(!empty($execution->name), 'name', 'unique', "id in ($relatedExecutionsID) and type in ('sprint','stage', 'kanban') and `project` = '$executionProject' and `deleted` = '0'") ->checkIF(!empty($execution->code), 'code', 'unique', "id != $executionID and type in ('sprint','stage', 'kanban') and `deleted` = '0'") ->checkFlow() ->where('id')->eq($executionID) ->limit(1) ->exec(); if(dao::isError()) return false; if(isset($_POST['parent'])) $this->loadModel('programplan')->setTreePath($executionID); /* Get team and language item. */ $this->loadModel('user'); $team = $this->user->getTeamMemberPairs($executionID, 'execution'); $members = isset($_POST['teamMembers']) ? $_POST['teamMembers'] : array(); array_push($members, $execution->PO, $execution->QD, $execution->PM, $execution->RD); $members = array_unique($members); $roles = $this->user->getUserRoles(array_values($members)); $changedAccounts = array(); $teamMembers = array(); foreach($members as $account) { if(empty($account) or isset($team[$account])) continue; $member = new stdclass(); $member->root = (int)$executionID; $member->account = $account; $member->join = helper::today(); $member->role = zget($roles, $account, ''); $member->days = zget($execution, 'days', 0); $member->type = 'execution'; $member->hours = $this->config->execution->defaultWorkhours; $this->dao->replace(TABLE_TEAM)->data($member)->exec(); $changedAccounts[$account] = $account; $teamMembers[$account] = $member; } $this->dao->delete()->from(TABLE_TEAM) ->where('root')->eq((int)$executionID) ->andWhere('type')->eq('execution') ->andWhere('account')->in(array_keys($team)) ->andWhere('account')->notin(array_values($members)) ->andWhere('account')->ne($oldExecution->openedBy) ->exec(); if(isset($execution->project) and $execution->project) $this->addProjectMembers($execution->project, $teamMembers); $whitelist = explode(',', $execution->whitelist); $this->loadModel('personnel')->updateWhitelist($whitelist, 'sprint', $executionID); /* Fix bug#3074, Update views for team members. */ if($execution->acl != 'open') $this->updateUserView($executionID, 'sprint', $changedAccounts); if(isset($execution->project)) { $executionProductList = $this->loadModel('product')->getProducts($executionID); $projectProductList = $this->product->getProducts($execution->project); $executionProductIdList = array_keys($executionProductList); $projectProductIdList = array_keys($projectProductList); $diffProductIdList = array_diff($executionProductIdList, $projectProductIdList); if(!empty($diffProductIdList)) { foreach($diffProductIdList as $key => $newProductID) { $data = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($executionID) ->andWhere('product')->eq($newProductID) ->fetch(); $data->project = $execution->project; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); } } } if(!dao::isError()) { if(isset($execution->project) and $execution->project != $oldExecution->project) { $execution->parent = $execution->project; $execution->path = ",{$execution->project},{$executionID},"; $this->changeProject($execution->project, $oldExecution->project, $executionID, $this->post->syncStories); } $this->file->updateObjectID($this->post->uid, $executionID, 'execution'); return common::createChanges($oldExecution, $execution); } } /** * Batch update. * * @access public * @return void */ public function batchUpdate() { $this->loadModel('user'); $this->loadModel('project'); $this->app->loadLang('programplan'); $executions = array(); $allChanges = array(); $data = fixer::input('post')->get(); $oldExecutions = $this->getByIdList($this->post->executionIDList); $nameList = array(); $codeList = array(); $projectModel = 'scrum'; $parents = array(); foreach($oldExecutions as $oldExecution) $parents[$oldExecution->id] = $oldExecution->parent; /* Replace required language. */ if($this->app->tab == 'project') { $projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($this->session->project)->fetch('model'); if($projectModel == 'scrum') { $this->lang->project->name = $this->lang->execution->name; $this->lang->project->code = $this->lang->execution->code; } else { $this->lang->project->name = str_replace($this->lang->project->common, $this->lang->project->stage, $this->lang->project->name); $this->lang->project->code = str_replace($this->lang->project->common, $this->lang->project->stage, $this->lang->project->code); } } $this->lang->error->unique = $this->lang->error->repeat; $extendFields = $this->getFlowExtendFields(); foreach($data->executionIDList as $executionID) { $executionName = $data->names[$executionID]; if(isset($data->codes)) $executionCode = $data->codes[$executionID]; $executionID = (int)$executionID; $executions[$executionID] = new stdClass(); $executions[$executionID]->id = $executionID; $executions[$executionID]->name = $executionName; $executions[$executionID]->PM = $data->PMs[$executionID]; $executions[$executionID]->PO = $data->POs[$executionID]; $executions[$executionID]->QD = $data->QDs[$executionID]; $executions[$executionID]->RD = $data->RDs[$executionID]; $executions[$executionID]->begin = $data->begins[$executionID]; $executions[$executionID]->end = $data->ends[$executionID]; $executions[$executionID]->team = $data->teams[$executionID]; $executions[$executionID]->desc = htmlspecialchars_decode($data->descs[$executionID]); $executions[$executionID]->days = $data->dayses[$executionID]; $executions[$executionID]->lastEditedBy = $this->app->user->account; $executions[$executionID]->lastEditedDate = helper::now(); if(isset($data->codes)) $executions[$executionID]->code = $executionCode; if(isset($data->projects)) $executions[$executionID]->project = zget($data->projects, $executionID, 0); if(isset($data->attributes[$executionID])) $executions[$executionID]->attribute = zget($data->attributes, $executionID, ''); if(isset($data->lifetimes[$executionID])) $executions[$executionID]->lifetime = $data->lifetimes[$executionID]; $oldExecution = $oldExecutions[$executionID]; $projectID = isset($executions[$executionID]->project) ? $executions[$executionID]->project : $oldExecution->project; $project = $this->project->getByID($projectID); /* Check unique code for edited executions. */ if(isset($data->codes) and empty($executionCode)) { dao::$errors["codes$executionID"][] = sprintf($this->lang->error->notempty, $this->lang->execution->execCode); } elseif(isset($data->codes) and $executionCode) { if(isset($codeList[$executionCode])) { dao::$errors["codes$executionID"][] = sprintf($this->lang->error->unique, $this->lang->execution->execCode, $executionCode); } $codeList[$executionCode] = $executionCode; } /* Name check. */ $parentID = $parents[$executionID]; if(isset($nameList[$executionName])) { foreach($nameList[$executionName] as $repeatID) { if($parentID == $parents[$repeatID]) { $type = $oldExecution->type == 'stage' ? 'stage' : 'agileplus'; dao::$errors["names$executionID"][] = sprintf($this->lang->execution->errorNameRepeat, strtolower(zget($this->lang->programplan->typeList, $type))); break; } } } $nameList[$executionName][] = $executionID; /* Attribute check. */ if(isset($data->attributes)) { if(isset($project->model) and ($project->model == 'waterfall' or $project->model == 'waterfallplus')) { $this->app->loadLang('stage'); $attribute = $executions[$executionID]->attribute; if(isset($attributeList[$parentID])) { $parentAttr = $attributeList[$parentID]; } else { $parentAttr = $this->dao->select('attribute')->from(TABLE_PROJECT)->where('id')->eq($parentID)->fetch('attribute'); } if($parentAttr and $parentAttr != $attribute and $parentAttr != 'mix') { $parentAttr = zget($this->lang->stage->typeList, $parentAttr); dao::$errors["attributes$executionID"][] = sprintf($this->lang->execution->errorAttrMatch, $parentAttr); } $attributeList[$executionID] = $attribute; } } /* Judge workdays is legitimate. */ $workdays = helper::diffDate($data->ends[$executionID], $data->begins[$executionID]) + 1; if(isset($data->dayses[$executionID]) and $data->dayses[$executionID] > $workdays) { $this->app->loadLang('project'); dao::$errors["dayses{$executionID}"][] = sprintf($this->lang->project->workdaysExceed, $workdays); } /* Parent stage begin and end check. */ if(isset($executions[$parentID])) { $begin = $executions[$executionID]->begin; $end = $executions[$executionID]->end; $parentBegin = $executions[$parentID]->begin; $parentEnd = $executions[$parentID]->end; if($begin < $parentBegin) { dao::$errors["begins$executionID"][] = sprintf($this->lang->execution->errorLetterParent, $parentBegin); } if($end > $parentEnd) { dao::$errors["ends$executionID"][] = sprintf($this->lang->execution->errorGreaterParent, $parentEnd); } } foreach($extendFields as $extendField) { $executions[$executionID]->{$extendField->field} = $this->post->{$extendField->field}[$executionID]; if(is_array($executions[$executionID]->{$extendField->field})) $executions[$executionID]->{$extendField->field} = join(',', $executions[$executionID]->{$extendField->field}); $executions[$executionID]->{$extendField->field} = htmlSpecialString($executions[$executionID]->{$extendField->field}); } if(empty($executions[$executionID]->begin)) dao::$errors["begins{$executionID}"][] = sprintf($this->lang->error->notempty, $this->lang->execution->begin); if(empty($executions[$executionID]->end)) dao::$errors["ends{$executionID}"][] = sprintf($this->lang->error->notempty, $this->lang->execution->end); /* Project begin and end check. */ if(!empty($executions[$executionID]->begin) and !empty($executions[$executionID]->end)) { if($executions[$executionID]->begin > $executions[$executionID]->end) { dao::$errors["ends{$executionID}"][] = sprintf($this->lang->execution->errorLetterPlan, $executions[$executionID]->end, $executions[$executionID]->begin); } if($project and $executions[$executionID]->begin < $project->begin) { dao::$errors["begins{$executionID}"][] = sprintf($this->lang->execution->errorLetterProject, $project->begin); } if($project and $executions[$executionID]->end > $project->end) { dao::$errors["ends{$executionID}"][] = sprintf($this->lang->execution->errorGreaterProject, $project->end); } } } if(dao::isError()) return false; /* Update burn before close execution. */ $closedIdList = array(); foreach($executions as $executionID => $execution) { if(isset($execution->status) and in_array($execution->status, array('done', 'closed', 'suspended'))) $closedIdList[$executionID] = $executionID; } $this->computeBurn($closedIdList); foreach($executions as $executionID => $execution) { $oldExecution = $oldExecutions[$executionID]; $team = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution'); $projectID = isset($execution->project) ? $execution->project : $oldExecution->project; if(isset($execution->project)) { $executionProductList = $this->loadModel('product')->getProducts($executionID); $projectProductList = $this->product->getProducts($execution->project); $executionProductIdList = array_keys($executionProductList); $projectProductIdList = array_keys($projectProductList); $diffProductIdList = array_diff($executionProductIdList, $projectProductIdList); if(!empty($diffProductIdList)) { foreach($diffProductIdList as $key => $newProductID) { $data = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($executionID) ->andWhere('product')->eq($newProductID) ->fetch(); $data->project = $execution->project; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); } } } $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->execution->edit->requiredFields, 'notempty') ->checkIF($execution->begin != '', 'begin', 'date') ->checkIF($execution->end != '', 'end', 'date') ->checkIF($execution->end != '', 'end', 'ge', $execution->begin) ->checkIF(!empty($execution->name), 'name', 'unique', "id != $executionID and type in ('sprint','stage','kanban') and `project` = $projectID and `deleted` = '0'") ->checkIF(!empty($execution->code), 'code', 'unique', "id != $executionID and type in ('sprint','stage','kanban') and `deleted` = '0'") ->checkFlow() ->where('id')->eq($executionID) ->limit(1) ->exec(); if(dao::isError()) return false; if(!empty($execution->project) and $oldExecution->project != $execution->project) { $execution->parent = $execution->project; $execution->path = ",{$execution->project},{$executionID},"; $this->changeProject($execution->project, $oldExecution->project, $executionID, isset($_POST['syncStories'][$executionID]) ? $_POST['syncStories'][$executionID] : 'no'); } $changedAccounts = array(); foreach($this->config->execution->ownerFields as $ownerField) { $owner = zget($execution, $ownerField, ''); if(empty($owner) or isset($team[$owner])) continue; $member = new stdclass(); $member->root = (int)$executionID; $member->account = $owner; $member->join = helper::today(); $member->role = $this->lang->execution->$ownerField; $member->days = zget($execution, 'days', 0); $member->type = 'execution'; $member->hours = $this->config->execution->defaultWorkhours; $this->dao->replace(TABLE_TEAM)->data($member)->exec(); $changedAccounts[] = $owner; } if(!empty($changedAccounts)) $this->updateUserView($executionID, 'sprint', $changedAccounts); $allChanges[$executionID] = common::createChanges($oldExecution, $execution); } $this->fixOrder(); return $allChanges; } /** * Batch change status. * * @param array $executionIdList * @param string $status * @access public * @return void */ public function batchChangeStatus($executionIdList, $status) { $this->loadModel('programplan'); /* Sort the IDs, the child stage comes first, and the parent stage follows. */ $executionIdList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('id')->in($executionIdList)->orderBy('grade_desc')->fetchPairs(); $pointOutStages = ''; foreach($executionIdList as $executionID) { /* The state of the parent stage or the sibling stage may be affected by the child stage before the change, so it cannot be checked in advance. */ $selfAndChildrenList = $this->programplan->getSelfAndChildrenList($executionID); $siblingStages = $this->programplan->getSiblings($executionID); $selfAndChildren = $selfAndChildrenList[$executionID]; $execution = $selfAndChildren[$executionID]; $executionType = $execution->type; $siblingList = array(); if($executionType == 'stage') $siblingList = $siblingStages[$executionID]; if($status == 'wait' and $execution->status != 'wait') { $pointOutStages .= $this->changeStatus2Wait($executionID, $selfAndChildren, $siblingList); } if($status == 'doing' and $execution->status != 'doing') { $this->changeStatus2Doing($executionID, $selfAndChildren); } if(($status == 'suspended' and $execution->status != 'suspended') or ($status == 'closed' and $execution->status != 'closed')) { $pointOutStages .= $this->changeStatus2Inactived($executionID, $status, $selfAndChildren, $siblingList); } } return trim($pointOutStages, ','); } /** * Change status to wait. * * @param int $executionID * @param array $selfAndChildren * @param array $siblingStages * @access public * @return string */ public function changeStatus2Wait($executionID, $selfAndChildren, $siblingStages) { $this->loadModel('programplan'); $this->loadModel('action'); $parentID = $selfAndChildren[$executionID]->parent; /* There are already tasks consuming work in this phase or its sub-phases already have start times. */ $hasStartedChildren = $this->dao->select('id')->from(TABLE_EXECUTION)->where('deleted')->eq(0)->andWhere('realBegan')->ne('0000-00-00')->andWhere('id')->in(array_keys($selfAndChildren))->andWhere('id')->ne($executionID)->fetchPairs(); $hasConsumedTasks = $this->dao->select('count(consumed) as count')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('execution')->in(array_keys($selfAndChildren))->andWhere('consumed')->gt(0)->fetch('count'); if($hasStartedChildren or $hasConsumedTasks) return "'{$selfAndChildren[$executionID]->name}',"; $newExecution = $this->buildExecutionByStatus('wait'); $this->dao->update(TABLE_EXECUTION)->data($newExecution)->where('id')->eq($executionID)->exec(); if(!dao::isError()) { /* Action. */ $changes = common::createChanges($selfAndChildren[$executionID], $newExecution); $actionID = $this->action->create('execution', $executionID, 'Edited'); $this->action->logHistory($actionID, $changes); /* This stage has a parent stage. */ $checkTopStage = $this->programplan->checkTopStage($executionID); if(!$checkTopStage) $this->programplan->computeProgress($executionID); } } /** * Change status to doing. * * @param int $executionID * @param array $selfAndChildren * @access public * @return string */ public function changeStatus2Doing($executionID, $selfAndChildren) { $this->loadModel('programplan'); $this->loadModel('action'); $type = $selfAndChildren[$executionID]->type; $parentID = $selfAndChildren[$executionID]->parent; $newExecution = $this->buildExecutionByStatus('doing'); $this->dao->update(TABLE_EXECUTION)->data($newExecution)->where('id')->eq($executionID)->exec(); if(!dao::isError()) { $changes = common::createChanges($selfAndChildren[$executionID], $newExecution); $actionID = $this->action->create('execution', $executionID, 'Started'); $this->action->logHistory($actionID, $changes); /* This stage has a parent stage. */ $checkTopStage = $this->programplan->checkTopStage($executionID); if(!$checkTopStage) $this->programplan->computeProgress($executionID); } } /** * Change status to suspended or closed. * * @param int $executionID * @param strint $status * @param array $selfAndChildren * @param array $siblingStages * @access public * @return string */ public function changeStatus2Inactived($executionID, $status, $selfAndChildren, $siblingStages) { $this->loadModel('programplan'); $this->loadModel('action'); $type = $selfAndChildren[$executionID]->type; $parentID = $selfAndChildren[$executionID]->parent; $checkedStatus = $status == 'suspended' ? 'wait,doing' : 'wait,doing,suspended'; /* If status is suspended, the rules is there are sub-stages under this stage, and not all sub-stages are suspended or closed. */ /* If status is closed, the rules is there are sub-stages under this stage, and not all sub-stages are closed. */ $checkLeafStage = $this->programplan->checkLeafStage($executionID); if(!$checkLeafStage) { foreach($selfAndChildren as $childID => $child) { if($childID == $executionID) continue; if(strpos($checkedStatus, $child->status) !== false) return "'{$selfAndChildren[$executionID]->name}',"; } } $newExecution = $this->buildExecutionByStatus($status); $this->dao->update(TABLE_EXECUTION)->data($newExecution)->where('id')->eq($executionID)->exec(); if(!dao::isError()) { $changes = common::createChanges($selfAndChildren[$executionID], $newExecution); $actionID = $this->action->create('execution', $executionID, strtoupper($status)); $this->action->logHistory($actionID, $changes); /* Suspended: When all child stages at the same level are suspended or closed, the status of the parent stage becomes "suspended". */ /* Closed: When all child stages at the same level are closed, the status of the parent stage becomes "closed". */ $checkTopStage = $this->programplan->checkTopStage($executionID); if(!$checkTopStage) $this->programplan->computeProgress($executionID); } } /** * Start execution. * * @param int $executionID * @access public * @return array */ public function start($executionID) { $oldExecution = $this->getById($executionID); $now = helper::now(); $execution = fixer::input('post') ->add('id', $executionID) ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->stripTags($this->config->execution->editor->start['id'], $this->config->allowedTags) ->remove('comment') ->get(); $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->start['id'], $this->post->uid); $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->check($this->config->execution->start->requiredFields, 'notempty') ->checkIF($execution->realBegan != '', 'realBegan', 'le', helper::today()) ->checkFlow() ->where('id')->eq((int)$executionID) ->exec(); /* When it has multiple errors, only the first one is prompted */ if(dao::isError() and count(dao::$errors['realBegan']) > 1) dao::$errors['realBegan'] = dao::$errors['realBegan'][0]; if(!dao::isError()) return common::createChanges($oldExecution, $execution); } /** * Put execution off. * * @param int $executionID * @access public * @return void */ public function putoff($executionID) { $oldExecution = $this->getById($executionID); $now = helper::now(); $execution = fixer::input('post') ->add('id', $executionID) ->stripTags($this->config->execution->editor->putoff['id'], $this->config->allowedTags) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->remove('comment') ->get(); $this->checkBeginAndEndDate($oldExecution->project, $execution->begin, $execution->end); if(dao::isError()) return false; $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->putoff['id'], $this->post->uid); $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->checkFlow() ->where('id')->eq((int)$executionID) ->exec(); if(!dao::isError()) return common::createChanges($oldExecution, $execution); } /** * Suspend execution. * * @param int $executionID * @access public * @return void */ public function suspend($executionID) { $oldExecution = $this->getById($executionID); $now = helper::now(); $execution = fixer::input('post') ->add('id', $executionID) ->setDefault('status', 'suspended') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->setDefault('suspendedDate', helper::today()) ->stripTags($this->config->execution->editor->suspend['id'], $this->config->allowedTags) ->remove('comment')->get(); $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->suspend['id'], $this->post->uid); $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->checkFlow() ->where('id')->eq((int)$executionID) ->exec(); if(!dao::isError()) return common::createChanges($oldExecution, $execution); } /** * Activate execution. * * @param int $executionID * @access public * @return void */ public function activate($executionID) { $oldExecution = $this->getById($executionID); $now = helper::now(); $execution = fixer::input('post') ->add('id', $executionID) ->setDefault('realEnd', '') ->setDefault('status', 'doing') ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->setDefault('closedBy', '') ->setDefault('closedDate', '') ->stripTags($this->config->execution->editor->activate['id'], $this->config->allowedTags) ->remove('comment,readjustTime,readjustTask') ->get(); if(empty($oldExecution->totalConsumed) and helper::isZeroDate($oldExecution->realBegan)) $execution->status = 'wait'; if(!$this->post->readjustTime) { unset($execution->begin); unset($execution->end); } if($this->post->readjustTime) { $begin = $execution->begin; $end = $execution->end; if($begin > $end) dao::$errors["message"][] = sprintf($this->lang->execution->errorLetterPlan, $end, $begin); if($oldExecution->grade > 1) { $parent = $this->dao->select('begin,end')->from(TABLE_PROJECT)->where('id')->eq($oldExecution->parent)->fetch(); $parentBegin = $parent->begin; $parentEnd = $parent->end; if($begin < $parentBegin) { dao::$errors["message"][] = sprintf($this->lang->execution->errorLetterParent, $parentBegin); } if($end > $parentEnd) { dao::$errors["message"][] = sprintf($this->lang->execution->errorGreaterParent, $parentEnd); } } } if(dao::isError()) return false; $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->activate['id'], $this->post->uid); $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->checkFlow() ->where('id')->eq((int)$executionID) ->exec(); /* Readjust task. */ if($this->post->readjustTime and $this->post->readjustTask) { $beginTimeStamp = strtotime($execution->begin); $tasks = $this->dao->select('id,estStarted,deadline,status')->from(TABLE_TASK) ->where('deadline')->ne('0000-00-00') ->andWhere('status')->in('wait,doing') ->andWhere('execution')->eq($executionID) ->fetchAll(); foreach($tasks as $task) { if($task->status == 'wait' and !helper::isZeroDate($task->estStarted)) { $taskDays = helper::diffDate($task->deadline, $task->estStarted); $taskOffset = helper::diffDate($task->estStarted, $oldExecution->begin); $estStartedTimeStamp = $beginTimeStamp + $taskOffset * 24 * 3600; $estStarted = date('Y-m-d', $estStartedTimeStamp); $deadline = date('Y-m-d', $estStartedTimeStamp + $taskDays * 24 * 3600); if($estStarted > $execution->end) $estStarted = $execution->end; if($deadline > $execution->end) $deadline = $execution->end; $this->dao->update(TABLE_TASK)->set('estStarted')->eq($estStarted)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec(); } else { $taskOffset = helper::diffDate($task->deadline, $oldExecution->begin); $deadline = date('Y-m-d', $beginTimeStamp + $taskOffset * 24 * 3600); if($deadline > $execution->end) $deadline = $execution->end; $this->dao->update(TABLE_TASK)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec(); } } } $changes = common::createChanges($oldExecution, $execution); if($this->post->comment != '' or !empty($changes)) { $this->loadModel('action'); $actionID = $this->action->create('execution', $executionID, 'Activated', $this->post->comment); $this->action->logHistory($actionID, $changes); } return $changes; } /** * Close execution. * * @param int $executionID * @access public * @return array */ public function close($executionID) { $oldExecution = $this->getById($executionID); $now = helper::now(); $execution = fixer::input('post') ->add('id', $executionID) ->setDefault('status', 'closed') ->setDefault('closedBy', $this->app->user->account) ->setDefault('closedDate', $now) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->stripTags($this->config->execution->editor->close['id'], $this->config->allowedTags) ->remove('comment') ->get(); $this->lang->error->ge = $this->lang->execution->ge; $execution = $this->loadModel('file')->processImgURL($execution, $this->config->execution->editor->close['id'], $this->post->uid); $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->check($this->config->execution->close->requiredFields,'notempty') ->checkIF($execution->realEnd != '', 'realEnd', 'le', helper::today()) ->checkIF($execution->realEnd != '', 'realEnd', 'ge', $oldExecution->realBegan) ->checkFlow() ->where('id')->eq((int)$executionID) ->exec(); /* When it has multiple errors, only the first one is prompted */ if(dao::isError() and count(dao::$errors['realEnd']) > 1) dao::$errors['realEnd'] = dao::$errors['realEnd'][0]; if(!dao::isError()) { $changes = common::createChanges($oldExecution, $execution); if($this->post->comment != '' or !empty($changes)) { $this->loadModel('action'); $actionID = $this->action->create('execution', $executionID, 'Closed', $this->post->comment); $this->action->logHistory($actionID, $changes); } $this->loadModel('score')->create('execution', 'close', $oldExecution); return $changes; } } /** * Set Kanban. * * @param int $executionID * @access public * @return void */ public function setKanban($executionID) { $execution = fixer::input('post') ->setIF($this->post->heightType == 'auto', 'displayCards', 0) ->remove('heightType') ->get(); if(isset($_POST['heightType']) and $this->post->heightType == 'custom' and !$this->loadModel('kanban')->checkDisplayCards($execution->displayCards)) return; $this->app->loadLang('kanban'); $this->lang->project->colWidth = $this->lang->kanban->colWidth; $this->lang->project->minColWidth = $this->lang->kanban->minColWidth; $this->lang->project->maxColWidth = $this->lang->kanban->maxColWidth; $this->dao->update(TABLE_EXECUTION)->data($execution) ->autoCheck() ->batchCheck($this->config->kanban->edit->requiredFields, 'notempty') ->checkIF(!$execution->fluidBoard, 'colWidth', 'ge', $this->config->minColWidth) ->batchCheckIF($execution->fluidBoard, 'minColWidth', 'ge', $this->config->minColWidth) ->checkIF($execution->minColWidth >= $this->config->minColWidth and $execution->fluidBoard, 'maxColWidth', 'gt', $execution->minColWidth) ->where('id')->eq((int)$executionID) ->exec(); } /** * Check the workload format and total. * * @param string $type create|update * @param int $percent * @param object|int $oldExecution * @access public * @return bool */ public function checkWorkload($type = '', $percent = 0, $oldExecution = '') { if(!preg_match("/^[0-9]+(.[0-9]{1,3})?$/", $percent)) { dao::$errors['percent'] = $this->lang->programplan->error->percentNumber; return false; } /* The total workload of the first stage should not exceed 100%. */ if($type == 'create' or (!empty($oldExecution) and $oldExecution->grade == 1 and isset($this->lang->execution->typeList[$oldExecution->type]))) { $oldPercentTotal = $this->dao->select('SUM(t2.percent) as total')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id') ->where('t1.product')->eq($this->post->products[0]) ->beginIF(!empty($_POST['branch'][0]))->andWhere('t1.branch')->eq(current($this->post->branch[0]))->fi() ->andWhere('t2.type')->eq('stage') ->andWhere('t2.grade')->eq(1) ->andWhere('t2.deleted')->eq(0) ->beginIF($type == 'create')->andWhere('t2.parent')->eq($oldExecution->id)->fi() ->beginIF(!empty($oldExecution) and isset($this->lang->execution->typeList[$oldExecution->type]))->andWhere('t2.parent')->eq($oldExecution->parent)->fi() ->fetch('total'); if(!$oldPercentTotal) $oldPercentTotal = 0; if($type == 'create') $percentTotal = $percent + $oldPercentTotal; if(!empty($oldExecution) and isset($this->lang->execution->typeList[$oldExecution->type])) $percentTotal = $oldPercentTotal - $oldExecution->percent + $percent; if($percentTotal > 100) { $printPercent = $type == 'create' ? $oldPercentTotal : $percentTotal; dao::$errors['percent'] = sprintf($this->lang->execution->workloadTotal, '%', $printPercent . '%'); return false; } } if($type == 'update' and $oldExecution->grade > 1) { $childrenTotalPercent = $this->dao->select('SUM(percent) as total')->from(TABLE_EXECUTION)->where('parent')->eq($oldExecution->parent)->andWhere('project')->eq($oldExecution->project)->andWhere('deleted')->eq(0)->fetch('total'); $childrenTotalPercent = $childrenTotalPercent - $oldExecution->percent + $this->post->percent; if($childrenTotalPercent > 100) { dao::$errors['percent'] = sprintf($this->lang->execution->workloadTotal, '%', $childrenTotalPercent . '%'); return false; } } } /** * Check begin and end date. * * @param int $projectID * @param string $begin * @param string $end * @param object $execution * @access public * @return void */ public function checkBeginAndEndDate($projectID, $begin, $end, $execution = null) { $project = $this->loadModel('project')->getByID($projectID); if(empty($project)) return; if($begin < $project->begin) dao::$errors['begin'] = sprintf($this->lang->execution->errorCommonBegin, $project->begin); if($end > $project->end) dao::$errors['end'] = sprintf($this->lang->execution->errorCommonEnd, $project->end); if(($project->model == 'waterfall' or $project->model == 'waterfallplus') and isset($execution) and $execution->parent != $projectID) { $this->app->loadLang('programplan'); $parent = $this->getByID($execution->parent); if($begin < $parent->begin) dao::$errors['begin'] = sprintf($this->lang->programplan->error->letterParent, $parent->begin); if($end > $parent->end) dao::$errors['end'] = sprintf($this->lang->programplan->error->greaterParent, $parent->end); } } /* * Get execution switcher. * * @param int $executionID * @param string $currentModule * @param string $currentMethod * @access public * @return string */ public function getSwitcher($executionID, $currentModule, $currentMethod) { if($currentModule == 'execution' and in_array($currentMethod, array('index', 'all', 'batchedit', 'create'))) return; $projectNameSpan = ''; $projectNameTitle = ''; $currentExecutionName = $this->lang->execution->common; if($executionID) { $currentExecution = $this->getById($executionID); $currentExecutionName = $currentExecution->name; $project = $this->loadModel('project')->getByID($currentExecution->project); if($project) { $projectNameTitle = $project->name . ' / '; $projectNameSpan = "{$project->name} / "; } } if($this->app->viewType == 'mhtml' and $executionID) { $output = html::a(helper::createLink('execution', 'index'), $this->lang->executionCommon) . $this->lang->colon; $output .= "{$currentExecutionName} "; return $output; } $dropMenuLink = helper::createLink('execution', 'ajaxGetDropMenu', "executionID=$executionID&module=$currentModule&method=$currentMethod&extra="); $output = "
"; return $output; } /** * Get project pairs. * * @param int $projectID * @param string $type all|sprint|stage|kanban * @param string $mode all|noclosed|stagefilter|withdelete|multiple|leaf|order_asc or empty * @access public * @return array */ public function getPairs($projectID = 0, $type = 'all', $mode = '') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getExecutionPairs(); $mode .= $this->cookie->executionMode; $orderBy = $this->config->execution->orderBy; if($projectID) { $executionModel = $this->dao->select('model')->from(TABLE_EXECUTION)->where('id')->eq($projectID)->andWhere('deleted')->eq(0)->fetch('model'); $orderBy = in_array($executionModel, array('waterfall', 'waterfallplus')) ? 'sortStatus_asc,begin_asc,id_asc' : 'id_desc'; /* Waterfall execution, when all phases are closed, in reverse order of date. */ if(in_array($executionModel, array('waterfall', 'waterfallplus'))) { $summary = $this->dao->select('count(id) as executions, sum(IF(INSTR("closed", status) < 1, 0, 1)) as closedExecutions')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('deleted')->eq('0')->fetch(); if($summary->executions == $summary->closedExecutions) $orderBy = 'sortStatus_asc,begin_desc,id_asc'; } } /* Order by status's content whether or not done */ $executions = $this->dao->select('*, IF(INSTR("done,closed", status) < 2, 0, 1) AS isDone, INSTR("doing,wait,suspended,closed", status) AS sortStatus')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) ->andWhere('vision')->eq($this->config->vision) ->beginIF(!$this->session->multiple and $this->app->tab == 'execution')->andWhere('multiple')->eq('1')->fi() ->beginIF(strpos($mode, 'multiple') !== false)->andWhere('multiple')->eq('1')->fi() ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint,kanban')->fi() ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi() ->beginIF(strpos($mode, 'withdelete') === false)->andWhere('deleted')->eq(0)->fi() ->beginIF(!$this->app->user->admin and strpos($mode, 'all') === false)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->orderBy($orderBy) ->fetchAll('id'); /* If mode == leaf, only show leaf executions. */ $allExecutions = $this->dao->select('id,name,parent,grade')->from(TABLE_EXECUTION) ->where('type')->notin(array('program', 'project')) ->andWhere('deleted')->eq('0') ->beginIf($projectID)->andWhere('project')->eq($projectID)->fi() ->fetchAll('id'); $parents = array(); foreach($allExecutions as $exec) $parents[$exec->parent] = true; if(strpos($mode, 'order_asc') !== false) $executions = $this->resetExecutionSorts($executions); $pairs = array(); $noMultiples = array(); foreach($executions as $execution) { if(strpos($mode, 'leaf') !== false and isset($parents[$execution->id])) continue; // Only show leaf. if(strpos($mode, 'noclosed') !== false and ($execution->status == 'done' or $execution->status == 'closed')) continue; if(strpos($mode, 'stagefilter') !== false and isset($executionModel) and in_array($executionModel, array('waterfall', 'waterfallplus')) and in_array($execution->attribute, array('request', 'design', 'review'))) continue; // Some stages of waterfall and waterfallplus not need. if(empty($execution->multiple)) $noMultiples[$execution->id] = $execution->project; /* Set execution name. */ $paths = array_slice(explode(',', trim($execution->path, ',')), 1); $executionName = ''; foreach($paths as $path) { if(isset($allExecutions[$path])) $executionName .= '/' . $allExecutions[$path]->name; } $pairs[$execution->id] = $executionName; } if($noMultiples) { if(strpos($mode, 'hideMultiple') !== false) { foreach($noMultiples as $executionID => $projectID) $pairs[$executionID] = ''; } else { $this->app->loadLang('project'); $noMultipleProjects = $this->dao->select('id, name')->from(TABLE_PROJECT)->where('id')->in($noMultiples)->fetchPairs('id', 'name'); foreach($noMultiples as $executionID => $projectID) { if(isset($noMultipleProjects[$projectID])) $pairs[$executionID] = $noMultipleProjects[$projectID] . "({$this->lang->project->disableExecution})"; } } } if(strpos($mode, 'empty') !== false) $pairs[0] = ''; /* If the pairs is empty, to make sure there's an execution in the pairs. */ if(empty($pairs) and isset($executions[0])) { $firstExecution = $executions[0]; $pairs[$firstExecution->id] = $firstExecution->name; } return $pairs; } /** * Get execution by idList. * * @param array $executionIdList * @param string $mode all * @access public * @return array */ public function getByIdList($executionIdList = array(), $mode = '') { return $this->dao->select('*')->from(TABLE_EXECUTION) ->where('id')->in($executionIdList) ->beginIF($mode != 'all')->andWhere('deleted')->eq(0)->fi() ->fetchAll('id'); } /** * Get project lists. * * @param int $projectID * @param string $type all|sprint|stage|kanban * @param string $status all|undone|wait|running * @param int $limit * @param int $productID * @param int $branch * @param object $pager * @param bool $withChildren * @access public * @return array */ public function getList($projectID = 0, $type = 'all', $status = 'all', $limit = 0, $productID = 0, $branch = 0, $pager = null, $withChildren = true) { if($status == 'involved') return $this->getInvolvedExecutionList($projectID, $status, $limit, $productID, $branch); if($productID != 0) { return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project= t2.id') ->where('t1.product')->eq($productID) ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi() ->beginIF($type == 'all')->andWhere('t2.type')->in('sprint,stage,kanban')->fi() ->beginIF($type != 'all')->andWhere('t2.type')->eq($type)->fi() ->andWhere('t2.deleted')->eq(0) ->beginIF($status == 'undone')->andWhere('t2.status')->notIN('done,closed')->fi() ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($status != 'all' and $status != 'undone')->andWhere('status')->in($status)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi() ->beginIF(!$withChildren)->andWhere('grade')->eq(1)->fi() ->orderBy('order_desc') ->page($pager) ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } else { return $this->dao->select('*, IF(INSTR(" done,closed", status) < 2, 0, 1) AS isDone')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) ->andWhere('vision')->eq($this->config->vision) ->beginIF($type == 'all')->andWhere('type')->in('sprint,stage,kanban')->fi() ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi() ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi() ->beginIF($status != 'all' and $status != 'undone')->andWhere('status')->in($status)->fi() ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->beginIF(!$withChildren)->andWhere('grade')->eq(1)->fi() ->orderBy('order_desc') ->page($pager) ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } } /** * Get involved execution list. * * @param int $projectID * @param string $status involved * @param int $limit * @param int $productID * @param int $branch * @access public * @return array */ public function getInvolvedExecutionList($projectID = 0, $status = 'involved', $limit = 0, $productID = 0, $branch = 0) { if($productID != 0) { return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project= t2.id') ->leftJoin(TABLE_TEAM)->alias('t3')->on('t3.root=t2.id') ->where('t1.product')->eq($productID) ->andWhere('t2.deleted')->eq(0) ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi() ->andWhere('t2.openedBy', true)->eq($this->app->user->account) ->orWhere('t3.account')->eq($this->app->user->account) ->markRight(1) ->andWhere('t2.type')->in('sprint,stage,kanban') ->beginIF($projectID)->andWhere('t2.project')->eq($projectID)->fi() ->orderBy('order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } else { return $this->dao->select('t1.*, IF(INSTR(" done,closed", t1.status) < 2, 0, 1) AS isDone')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_TEAM)->alias('t2')->on('t2.root=t1.id') ->where('t1.deleted')->eq(0) ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi() ->andWhere('t1.openedBy', true)->eq($this->app->user->account) ->orWhere('t2.account')->eq($this->app->user->account) ->markRight(1) ->andWhere('t1.type')->in('sprint,stage,kanban') ->beginIF($projectID)->andWhere('t1.project')->eq($projectID)->fi() ->orderBy('t1.order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } } /** * Get execution id list by project. * * @param int $projectID * @param int $status all|undone|wait|doing|suspended|closed * @access public * @return array */ public function getIdList($projectID, $status = 'all') { return $this->dao->select('id')->from(TABLE_EXECUTION) ->where('type')->in('sprint,stage,kanban') ->andWhere('deleted')->eq('0') ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi() ->beginIF($status != 'all' and $status != 'undone')->andWhere('status')->in($status)->fi() ->fetchPairs('id', 'id'); } /** * Get execution stat data. * @param int $projectID * @param string $browseType all|undone|wait|doing|suspended|closed|involved|bySearch|review * @param int $productID * @param int $branch * @param bool $withTasks * @param string|int $param skipParent * @param string $orderBy * @param object $pager * @access public * @return array */ public function getStatData($projectID = 0, $browseType = 'undone', $productID = 0, $branch = 0, $withTasks = false, $param = '', $orderBy = 'id_asc', $pager = null) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getExecutionStats($browseType); /* Construct the query SQL at search executions. */ $executionQuery = ''; if($browseType == 'bySearch') { $queryID = (int)$param; if($queryID) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('executionQuery', $query->sql); $this->session->set('executionForm', $query->form); } } if($this->session->executionQuery == false) $this->session->set('executionQuery', ' 1 = 1'); $executionQuery = $this->session->executionQuery; $allProject = "`project` = 'all'"; if(strpos($executionQuery, $allProject) !== false) $executionQuery = str_replace($allProject, '1', $executionQuery); $executionQuery = preg_replace('/(`\w*`)/', 't1.$1',$executionQuery); } /* Get involved executions. */ $myExecutionIDList = array(); if($browseType == 'involved') { $myExecutionIDList = $this->dao->select('root')->from(TABLE_TEAM) ->where('account')->eq($this->app->user->account) ->andWhere('type')->eq('execution') ->fetchPairs(); } $executions = $this->dao->select('t1.*,t2.name projectName, t2.model as projectModel')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->beginIF($productID)->leftJoin(TABLE_PROJECTPRODUCT)->alias('t3')->on('t1.id=t3.project')->fi() ->where('t1.type')->in('sprint,stage,kanban') ->andWhere('t1.deleted')->eq('0') ->andWhere('t1.vision')->eq($this->config->vision) ->andWhere('t1.multiple')->eq('1') ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi() ->beginIF(!empty($executionQuery))->andWhere($executionQuery)->fi() ->beginIF($productID)->andWhere('t3.product')->eq($productID)->fi() ->beginIF($projectID)->andWhere('t1.project')->eq($projectID)->fi() ->beginIF(!in_array($browseType, array('all', 'undone', 'involved', 'review', 'bySearch')))->andWhere('t1.status')->eq($browseType)->fi() ->beginIF($browseType == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi() ->beginIF($browseType == 'review') ->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)") ->andWhere('t1.reviewStatus')->eq('doing') ->fi() ->orderBy($orderBy) ->page($pager, 't1.id') ->fetchAll('id'); if(empty($productID) and !empty($executions)) $projectProductIdList = $this->dao->select('project, GROUP_CONCAT(product) as product')->from(TABLE_PROJECTPRODUCT)->where('project')->in(array_keys($executions))->groupBy('project')->fetchPairs(); $hours = $this->loadModel('project')->computerProgress($executions); $burns = $this->getBurnData($executions); /* Get the number of execution teams. */ $teams = $this->dao->select('t1.root,count(t1.id) as teams')->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') ->where('t1.root')->in(array_keys($executions)) ->andWhere('t1.type')->ne('project') ->andWhere('t2.deleted')->eq(0) ->groupBy('t1.root') ->fetchAll('root'); $productNameList = $this->dao->select('t1.id,GROUP_CONCAT(t3.`name`) as productName')->from(TABLE_EXECUTION)->alias('t1') ->leftjoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id=t2.project') ->leftjoin(TABLE_PRODUCT)->alias('t3')->on('t2.product=t3.id') ->where('t1.project')->eq($projectID) ->andWhere('t1.type')->in('kanban,sprint,stage') ->groupBy('t1.id') ->fetchPairs(); if($withTasks) $executionTasks = $this->getTaskGroupByExecution(array_keys($executions)); /* Process executions. */ $this->app->loadConfig('task'); $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); $today = helper::today(); $childList = array(); $parentList = array(); foreach($executions as $key => $execution) { $execution->productName = isset($productNameList[$execution->id]) ? $productNameList[$execution->id] : ''; /* Process the end time. */ $execution->end = date(DT_DATE1, strtotime($execution->end)); /* Judge whether the execution is delayed. */ if($execution->status != 'done' and $execution->status != 'closed' and $execution->status != 'suspended') { $delay = helper::diffDate($today, $execution->end); if($delay > 0) $execution->delay = $delay; } /* Process the burns. */ $execution->burns = array(); $burnData = isset($burns[$execution->id]) ? $burns[$execution->id] : array(); foreach($burnData as $data) $execution->burns[] = $data->value; /* Process the hours. */ $execution->hours = isset($hours[$execution->id]) ? $hours[$execution->id] : (object)$emptyHour; $execution->teamCount = isset($teams[$execution->id]) ? $teams[$execution->id]->teams : 0; if(isset($executionTasks) and isset($executionTasks[$execution->id])) { $tasks = array_chunk($executionTasks[$execution->id], $this->config->task->defaultLoadCount, true); $execution->tasks = $tasks[0]; } /* In the case of the waterfall model, calculate the sub-stage. */ if($param === 'skipParent') { if($execution->parent) $parentList[$execution->parent] = $execution->parent; if($execution->projectName) $execution->name = $execution->projectName . ' / ' . $execution->name; } elseif(strpos($param, 'hasParentName') !== false) { $parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($execution->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs(); $executions[$execution->id]->title = implode('/', $parentExecutions); if(strpos($param, 'skipParent') !== false) { $children = $this->getChildExecutions($execution->id); if(count($children) > 0) $parentList[$execution->id] = $execution->id; } } elseif(isset($executions[$execution->parent])) { $executions[$execution->parent]->children[$key] = $execution; $childList[$key] = $key; } /* Bind execution product */ if(!empty($projectProductIdList) and !empty($projectProductIdList[$execution->id])) { $execution->product = $projectProductIdList[$execution->id]; } } if(strpos($param, 'withchild') === false) { foreach($childList as $childID) unset($executions[$childID]); } foreach($parentList as $parentID) unset($executions[$parentID]); return array_values($executions); } /** * Get executions by project. * * @param int $projectID * @param string $status * @param int $limit * @param string $pairs * @param int $appenedID * @access public * @return array */ public function getByProject($projectID, $status = 'all', $limit = 0, $pairs = false, $devel = false, $appendedID = 0) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getExecutionPairs(); $project = $this->loadModel('project')->getByID($projectID); $executions = $this->dao->select('*')->from(TABLE_EXECUTION) ->where('type')->in('stage,sprint,kanban') ->andWhere('deleted')->eq('0') ->andWhere('vision')->eq($this->config->vision) ->beginIF($projectID)->andWhere('project')->eq((int)$projectID)->fi() ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi() ->beginIF($status != 'all' and $status != 'undone' and $status != 'noclosed')->andWhere('status')->in($status)->fi() ->beginIF($status == 'noclosed')->andWhere('status')->ne('closed')->fi() ->beginIF($devel === true)->andWhere('attribute')->in('dev,qa,release')->fi() ->beginIF($appendedID)->orWhere('id')->eq($appendedID)->fi() ->orderBy('order_asc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); /* Add product name and parent stage name to stage name. */ if(isset($project->model) and in_array($project->model, array('waterfall', 'waterfallplus'))) { $executionProducts = array(); if($project->hasProduct and $project->division) { $executionList = array(); $executionProducts = $this->dao->select('t1.project, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') ->where('project')->in(array_keys($executions)) ->andWhere('t2.deleted')->eq(0) ->fetchPairs(); } $allExecutions = $this->dao->select('id,name,parent,grade')->from(TABLE_EXECUTION) ->where('type')->in('stage,sprint,kanban') ->andWhere('deleted')->eq('0') ->beginIf($projectID)->andWhere('project')->eq($projectID)->fi() ->fetchAll('id'); $parents = array(); foreach($allExecutions as $id => $execution) $parents[$execution->parent] = $execution->parent; $executions = $this->resetExecutionSorts($executions); foreach($executions as $id => $execution) { if(isset($parents[$execution->id])) { unset($executions[$id]); continue; } $executionName = ''; $paths = array_slice(explode(',', trim($execution->path, ',')), 1); foreach($paths as $path) { if(isset($allExecutions[$path])) $executionName .= '/' . $allExecutions[$path]->name; } if($executionName) $execution->name = ltrim($executionName, '/'); if(isset($executionProducts[$id])) $execution->name = $executionProducts[$id] . '/' . $execution->name; } } $projects = array(); if(empty($projectID)) { $projects = $this->dao->select('t1.id,t1.name')->from(TABLE_PROJECT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.id=t2.project') ->where('t2.id')->in(array_keys($executions)) ->fetchPairs('id', 'name'); } else { $projects = $this->dao->select('id,name')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetchPairs('id', 'name'); } if($pairs) { $this->app->loadLang('project'); $executionPairs = array(); foreach($executions as $execution) { $executionPairs[$execution->id] = ''; $executionPairs[$execution->id] .= isset($projects[$execution->project]) ? ($projects[$execution->project] . '/') : ''; $executionPairs[$execution->id] .= $execution->name; if(empty($execution->multiple)) $executionPairs[$execution->id] = $projects[$execution->project] . "({$this->lang->project->disableExecution})"; } $executions = $executionPairs; } return $executions; } /** * Get no multiple execution id. * * @param int $projectID * @access public * @return int */ public function getNoMultipleID($projectID) { return $this->dao->select('id')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('multiple')->eq(0)->andWhere('deleted')->eq(0)->fetch('id'); } /** * Create the link from module,method,extra * * @param string $module * @param string $method * @param mix $extra * @access public * @return void */ public function getLink($module, $method, $extra) { $link = ''; if($module == 'task' and ($method == 'view' || $method == 'edit' || $method == 'batchedit')) { $module = 'execution'; $method = 'task'; } if($module == 'testcase' and ($method == 'view' or $method == 'edit' or $method == 'batchedit')) { $module = 'execution'; $method = 'testcase'; } if($module == 'testtask') { $module = 'execution'; $method = 'testtask'; } if($module == 'build' and ($method == 'edit' or $method == 'view')) { $module = 'execution'; $method = 'build'; } if($module == 'story') { $module = 'execution'; $method = 'story'; } if($module == 'product' and $method == 'showerrornone') { $module = 'execution'; $method = 'task'; } if($module == 'execution' and $method == 'create') return; if($extra != '') { $link = helper::createLink($module, $method, "executionID=%s&type=$extra"); } elseif($module == 'execution' and ($method == 'index' or $method == 'all')) { $link = helper::createLink($module, 'task', "executionID=%s"); } elseif($module == 'bug' and $method == 'create' and $this->app->tab == 'execution') { $link = helper::createLink($module, $method, "productID=0&branch=0&extra=executionID=%s"); } elseif(in_array($module, array('bug', 'case', 'testtask', 'testreport')) and strpos(',view,edit,', ",$method,") !== false) { $link = helper::createLink('execution', $module, "executionID=%s"); } elseif($module == 'repo') { $link = helper::createLink('repo', 'browse', "repoID=0&branchID=&executionID=%s"); } elseif($module == 'doc') { $link = helper::createLink('doc', $method, "type=execution&objectID=%s&from=execution"); } elseif(in_array($module, array('issue', 'risk', 'opportunity', 'pssp', 'auditplan', 'nc', 'meeting'))) { $link = helper::createLink($module, 'browse', "executionID=%s&from=execution"); } elseif($module == 'testreport' and $method == 'create') { $link = helper::createLink('execution', 'testtask', "executionID=%s"); } else { $link = helper::createLink($module, $method, "executionID=%s"); } return $link; } /** * Get branches of execution. * * @param int $executionID * @access public * @return array */ public function getBranches($executionID) { $productBranchPairs = $this->dao->select('product, branch')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($executionID) ->fetchPairs(); $branches = $this->loadModel('branch')->getByProducts(array_keys($productBranchPairs)); foreach($productBranchPairs as $product => $branch) { if($branch == 0 and isset($branches[$product])) $productBranchPairs[$product] = join(',', array_keys($branches[$product])); } return $productBranchPairs; } /** * Get executions tree data * @param int $executionID * @access public * @return array */ public function getTree($executionID) { $fullTrees = $this->loadModel('tree')->getTaskStructure($executionID, 0); array_unshift($fullTrees, array('id' => 0, 'name' => '/', 'type' => 'task', 'actions' => false, 'root' => $executionID)); foreach($fullTrees as $i => $tree) { $tree = (object)$tree; if($tree->type == 'product') array_unshift($tree->children, array('id' => 0, 'name' => '/', 'type' => 'story', 'actions' => false, 'root' => $tree->root)); $fullTree = $this->fillTasksInTree($tree, $executionID); if(empty($fullTree->children)) { unset($fullTrees[$i]); } else { $fullTrees[$i] = $fullTree; } } if(isset($fullTrees[0]) and empty($fullTrees[0]->children)) array_shift($fullTrees); return array_values($fullTrees); } /** * Get full name by path. * @param string $executionPath * @access public * @return string */ public function getFullNameByPath($executionPath) { $paths = explode(',', trim($executionPath, ',')); $executions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in($paths)->andWhere('type')->notin('program,project')->fetchPairs(); $executionName = array(); foreach($paths as $path) { if(isset($executions[$path])) $executionName[] = $executions[$path]; } return implode('/', $executionName); } /** * Get full name of executons. * @param array $executions * @access public * @return array */ public function getFullNameList($executions) { $allExecutions = $this->dao->select('id,name,parent')->from(TABLE_EXECUTION) ->where('type')->notin(array('program', 'project')) ->andWhere('deleted')->eq('0') ->fetchAll('id'); $nameList = array(); foreach($executions as $executionID => $execution) { if($execution->grade <= 1) { $nameList[$executionID] = $execution->name; continue; } /* Set execution name. */ $paths = array_slice(explode(',', trim($execution->path, ',')), 1); $executionName = array(); foreach($paths as $path) { if(isset($allExecutions[$path])) $executionName[] = $allExecutions[$path]->name; } $nameList[$executionID] = implode('/', $executionName); } return $nameList; } /** * Get related executions * * @param int $executionID * @access public * @return array */ public function getRelatedExecutions($executionID) { $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$executionID)->fetchAll('product'); // $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->fetchAll('product'); if(!$products) return array(); $products = array_keys($products); return $this->dao->select('t1.id, t1.name')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') ->on('t1.id = t2.project') ->where('t2.product')->in($products) ->andWhere('t1.id')->ne((int)$executionID) ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.id') ->fetchPairs(); } /** * Get child executions. * * @param int $executionID * @param string $orderBy * @access public * @return array */ public function getChildExecutions($executionID, $orderBy = 'id_desc') { return $this->dao->select('*')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) ->andWhere('parent')->eq((int)$executionID) ->orderBy($orderBy) ->fetchAll('id'); } /** * Check the privilege. * * @access public * @return bool */ public function getLimitedExecution() { /* If is admin, return true. */ if($this->app->user->admin) return true; /* Get all teams of all executions and group by executions, save it as static. */ $executions = $this->dao->select('root, limited')->from(TABLE_TEAM) ->where('type')->eq('execution') ->andWhere('account')->eq($this->app->user->account) ->andWhere('limited')->eq('yes') ->orderBy('root asc') ->fetchPairs('root', 'root'); $_SESSION['limitedExecutions'] = join(',', $executions); } /** * Get executions lists grouped by product. * * @access public * @return array */ public function getProductGroupList() { $list = $this->dao->select('t1.id,t1.name,t1.status,t2.product')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project') ->where('t1.deleted')->eq(0) ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi() ->fetchGroup('product'); $noProducts = array(); foreach($list as $id => $product) { foreach($product as $ID => $execution) { if(!$execution->product) { if($this->checkPriv($execution->id)) $noProducts[] = $execution; unset($list[$id][$ID]); } } } unset($list['']); $list[''] = $noProducts; return $list; } /** * Get tasks. * * @param int $productID * @param int $executionID * @param array $executions * @param string $browseType * @param int $queryID * @param int $moduleID * @param string $sort * @param object $pager * @access public * @return array */ public function getTasks($productID, $executionID, $executions, $browseType, $queryID, $moduleID, $sort, $pager) { $this->loadModel('task'); /* Set modules and $browseType. */ $modules = array(); if($moduleID) $modules = $this->loadModel('tree')->getAllChildID($moduleID); if($browseType == 'bymodule' or $browseType == 'byproduct') { if(($this->session->taskBrowseType) and ($this->session->taskBrowseType != 'bysearch')) $browseType = $this->session->taskBrowseType; } /* Get tasks. */ $tasks = array(); if($browseType != "bysearch") { $queryStatus = $browseType == 'byexecution' ? 'all' : $browseType; if($queryStatus == 'unclosed') { $queryStatus = $this->lang->task->statusList; unset($queryStatus['closed']); $queryStatus = array_keys($queryStatus); } $tasks = $this->task->getExecutionTasks($executionID, $productID, $queryStatus, $modules, $sort, $pager); } else { if($queryID) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('taskQuery', $query->sql); $this->session->set('taskForm', $query->form); } else { $this->session->set('taskQuery', ' 1 = 1'); } } else { if($this->session->taskQuery == false) $this->session->set('taskQuery', ' 1 = 1'); } if(strpos($this->session->taskQuery, "deleted =") === false) $this->session->set('taskQuery', $this->session->taskQuery . " AND deleted = '0'"); $taskQuery = $this->session->taskQuery; /* Limit current execution when no execution. */ if(strpos($taskQuery, "`execution` =") === false) $taskQuery = $taskQuery . " AND `execution` = $executionID"; $executionQuery = "`execution` " . helper::dbIN(array_keys($executions)); $taskQuery = str_replace("`execution` = 'all'", $executionQuery, $taskQuery); // Search all execution. $this->session->set('taskQueryCondition', $taskQuery, $this->app->tab); $this->session->set('taskOnlyCondition', true, $this->app->tab); $tasks = $this->getSearchTasks($taskQuery, $pager, $sort); } return $tasks; } /** * Get the task data group by execution id list. * * @param array $executionIdList * @access public * @return array */ public function getTaskGroupByExecution($executionIdList = array()) { if(empty($executionIdList)) return array(); $executionTasks = $this->dao->select('*')->from(TABLE_TASK) ->where('deleted')->eq(0) ->andWhere('status')->notin('closed,cancel') ->andWhere('execution')->in($executionIdList) ->orderBy('id_asc') ->fetchGroup('execution', 'id'); $taskIdList = array(); foreach($executionTasks as $executionID => $tasks) $taskIdList = array_merge($taskIdList, array_keys($tasks)); $taskIdList = array_unique($taskIdList); $teamGroups = $this->dao->select('id,task,account,status')->from(TABLE_TASKTEAM)->where('task')->in($taskIdList)->fetchGroup('task', 'id'); foreach($executionTasks as $executionID => $tasks) { foreach($tasks as $task) { if(isset($teamGroups[$task->id])) $task->team = $teamGroups[$task->id]; if($task->parent > 0 and isset($executionTasks[$executionID][$task->parent])) { $executionTasks[$executionID][$task->parent]->children[$task->id] = $task; unset($executionTasks[$executionID][$task->id]); } } } return $executionTasks; } /** * Get the execution by ID. * * @param int $executionID * @param bool $setImgSize * @access public * @return object */ public function getByID($executionID, $setImgSize = false) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getExecution(); $execution = $this->dao->findById((int)$executionID)->from(TABLE_EXECUTION)->fetch(); if(!$execution) return false; /* Judge whether the execution is delayed. */ if($execution->status != 'done' and $execution->status != 'closed' and $execution->status != 'suspended') { $delay = helper::diffDate(helper::today(), $execution->end); if($delay > 0) $execution->delay = $delay; } $total = $this->dao->select(' ROUND(SUM(estimate), 2) AS totalEstimate, ROUND(SUM(consumed), 2) AS totalConsumed, ROUND(SUM(`left`), 2) AS totalLeft') ->from(TABLE_TASK) ->where('execution')->eq((int)$executionID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->lt(1) ->fetch(); $closedTotalLeft = $this->dao->select('ROUND(SUM(`left`), 2) AS totalLeft')->from(TABLE_TASK) ->where('execution')->eq((int)$executionID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->lt(1) ->andWhere('status')->in('closed,cancel') ->fetch('totalLeft'); $execution->days = $execution->days ? $execution->days : ''; $execution->totalHours = $this->dao->select('sum(days * hours) AS totalHours')->from(TABLE_TEAM)->where('root')->eq($execution->id)->andWhere('type')->eq('execution')->fetch('totalHours'); $execution->totalEstimate = round((float)$total->totalEstimate, 1); $execution->totalConsumed = round((float)$total->totalConsumed, 1); $execution->totalLeft = round((float)($total->totalLeft - $closedTotalLeft), 1); $execution = $this->loadModel('file')->replaceImgURL($execution, 'desc'); if($setImgSize) $execution->desc = $this->file->setImgSize($execution->desc); return $execution; } /** * Get the default managers for a execution from it's related products. * * @param int $executionID * @access public * @return object */ public function getDefaultManagers($executionID) { $managers = $this->dao->select('PO,QD,RD')->from(TABLE_PRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.product') ->where('t2.project')->eq($executionID) ->fetch(); if($managers) return $managers; $managers = new stdclass(); $managers->PO = ''; $managers->QD = ''; $managers->RD = ''; return $managers; } /** * Get branch pairs by product id list. * * @param array $products * @param int $projectID * @param string $param * @param string|array $appendBranch * @access public * @return array */ public function getBranchByProduct($products, $projectID = 0, $param = 'noclosed', $appendBranch = '') { $branchGroups = $this->loadModel('branch')->getByProducts($products, $param, $appendBranch); if($projectID) { $projectProducts = $this->loadModel('project')->getBranchesByProject($projectID); foreach($branchGroups as $productID => $branchPairs) { foreach($branchPairs as $branchID => $branchName) { if(strpos($param, 'withMain') !== false and $branchID == BRANCH_MAIN) continue; if(!isset($projectProducts[$productID][$branchID])) unset($branchGroups[$productID][$branchID]); } } } return $branchGroups; } /** * Get ordered executions. * * @param int $executionID * @param string $status * @param int $num * @param string $param * @access public * @return array */ public function getOrderedExecutions($executionID, $status, $num = 0, $param = '') { $executionList = $this->getList($executionID, 'all', $status); if(empty($executionList)) return $executionList; $executions = $mineExecutions = $otherExecutions = $closedExecutions = array(); $param = strtolower($param); if($param == 'skipparent') { $parentExecutions = array(); foreach($executionList as $execution) $parentExecutions[$execution->parent] = $execution->parent; } foreach($executionList as $execution) { if(empty($execution->multiple)) continue; if(!$this->app->user->admin and !$this->checkPriv($execution->id)) continue; if($param == 'skipparent' and isset($parentExecutions[$execution->id])) continue; if($execution->status != 'done' and $execution->status != 'closed' and $execution->PM == $this->app->user->account) { $mineExecutions[$execution->id] = $execution; } elseif($execution->status != 'done' and $execution->status != 'closed' and !($execution->PM == $this->app->user->account)) { $otherExecutions[$execution->id] = $execution; } elseif($execution->status == 'done' or $execution->status == 'closed') { $closedExecutions[$execution->id] = $execution; } } $executions = $mineExecutions + $otherExecutions + $closedExecutions; if(empty($num)) return $executions; return array_slice($executions, 0, $num, true); } /** * Build story search form. * * @param array $products * @param array $branchGroups * @param array $modules * @param int $queryID * @param string $actionURL * @param string $type * @param object $execution * @access public * @return void */ public function buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, $type = 'executionStory', $execution = null) { $this->app->loadLang('branch'); $branchPairs = array(BRANCH_MAIN => $this->lang->branch->main); $productType = 'normal'; $productNum = count($products); $productPairs = array(0 => ''); $branches = empty($execution) ? array() : $this->loadModel('project')->getBranchesByProject($execution->id); foreach($products as $product) { $productPairs[$product->id] = $product->name; if($product->type != 'normal') { $productType = $product->type; if(isset($branches[$product->id])) { foreach($branches[$product->id] as $branchID => $branch) { if(!isset($branchGroups[$product->id][$branchID])) continue; if($branchID != BRANCH_MAIN) $branchPairs[$branchID] = ((count($products) > 1) ? $product->name . '/' : '') . $branchGroups[$product->id][$branchID]; } } } } /* Build search form. */ if($type == 'executionStory') { $this->config->product->search['module'] = 'executionStory'; } $this->config->product->search['fields']['title'] = str_replace($this->lang->SRCommon, $this->lang->SRCommon, $this->lang->story->title); $this->config->product->search['actionURL'] = $actionURL; $this->config->product->search['queryID'] = $queryID; $this->config->product->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject); $this->loadModel('productplan'); $plans = array(); $planPairs = array('' => ''); foreach($products as $productID => $product) { $plans = $this->productplan->getBranchPlanPairs($productID, array(BRANCH_MAIN) + $product->branches, 'unexpired', true); foreach($plans as $plan) $planPairs += $plan; } $this->config->product->search['params']['plan']['values'] = $planPairs; $this->config->product->search['params']['module']['values'] = array('' => '') + $modules; if($productType == 'normal') { unset($this->config->product->search['fields']['branch']); unset($this->config->product->search['params']['branch']); } else { $this->config->product->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productType]); $this->config->product->search['params']['branch']['values'] = array('' => '') + $branchPairs; } $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->story->statusList); $project = $execution; if(strpos('sprint,stage,kanban', $execution->type) !== false) $project = $this->loadModel('project')->getByID($execution->project); if(isset($project->hasProduct) && empty($project->hasProduct)) { unset($this->config->product->search['fields']['product']); if($project->model != 'kanban') unset($this->config->product->search['fields']['plan']); } $this->loadModel('search')->setSearchParams($this->config->product->search); } /** * Get executions to import * * @param array $executionIds * @param string $type sprint|stage|kanban * @param string $model * @access public * @return array */ public function getToImport($executionIds, $type, $model = '') { return $this->dao->select('t1.id,concat_ws(" / ", t2.name, t1.name) as name')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t2.id=t1.project') ->where('t1.id')->in($executionIds) ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi() ->beginIF(empty($model) or strpos(',waterfallplus,agileplus,', ",$model,") === false)->andWhere('t1.type')->eq($type)->fi() ->beginIF(!empty($model) and $model == 'agileplus')->andWhere('t1.type')->in(array('sprint', 'kanban'))->fi() ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.id desc') ->fetchPairs('id', 'name'); } /** * Update products of a execution. * * @param int $executionID * @param array $products * @access public * @return void */ public function updateProducts($executionID, $products = '') { $this->loadModel('user'); $products = isset($_POST['products']) ? $_POST['products'] : $products; $oldProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$executionID)->fetchGroup('product', 'branch'); $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$executionID)->exec(); $members = array_keys($this->getTeamMembers($executionID)); if(empty($products)) { $this->user->updateUserView(array_keys($oldProducts), 'product', $members); return true; } $branches = isset($_POST['branch']) ? $_POST['branch'] : array(); $plans = isset($_POST['plans']) ? $_POST['plans'] : array();; $existedProducts = array(); foreach($products as $i => $productID) { if(empty($productID)) continue; if(!isset($existedProducts[$productID])) $existedProducts[$productID] = array(); $oldPlan = 0; $branch = isset($branches[$i]) ? $branches[$i] : 0; if(!is_array($branch)) $branch = array($branch); foreach($branch as $branchID) { if(isset($existedProducts[$productID][$branchID])) continue; if(isset($oldProducts[$productID][$branchID])) { $oldProduct = $oldProducts[$productID][$branchID]; if($this->app->rawMethod != 'edit') $oldPlan = $oldProduct->plan; } $data = new stdclass(); $data->project = $executionID; $data->product = $productID; $data->branch = $branchID; $data->plan = isset($plans[$productID]) ? implode(',', $plans[$productID]) : $oldPlan; $data->plan = trim($data->plan, ','); $data->plan = empty($data->plan) ? 0 : ",$data->plan,"; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); $existedProducts[$productID][$branchID] = true; } } $oldProductKeys = array_keys($oldProducts); $needUpdate = array_merge(array_diff($oldProductKeys, $products), array_diff($products, $oldProductKeys)); if($needUpdate) $this->user->updateUserView($needUpdate, 'product', $members); } /** * Get tasks can be imported. * * @param int $toExecution * @param array $branches * @access public * @return array */ public function getTasks2Imported($toExecution, $branches) { $execution = $this->getById($toExecution); $project = $this->loadModel('project')->getById($execution->project); $brotherProjects = $this->project->getBrotherProjects($project); $executions = $this->dao->select('id')->from(TABLE_EXECUTION) ->where('project')->in($brotherProjects) ->andWhere('multiple')->eq('1') ->andWhere('status')->ne('closed') ->andWhere('deleted')->eq(0) ->fetchPairs('id'); $branches = str_replace(',', "','", $branches); $tasks = $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account') ->where('t1.status')->in('wait,doing,pause,cancel') ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->lt(1) ->andWhere('t1.execution')->in(array_keys($executions)) ->andWhere("(t1.story = 0 OR (t2.branch in ('0','" . join("','", $branches) . "') and t2.product " . helper::dbIN(array_keys($branches)) . "))") ->fetchGroup('execution', 'id'); return $tasks; } /** * Import tasks. * * @param int $executionID * @access public * @return void */ public function importTask($executionID) { $this->loadModel('task'); $dateExceed = ''; $taskStories = array(); $parents = array(); $execution = $this->getByID($executionID); $tasks = $this->dao->select('id,execution,assignedTo,story,consumed,status,parent,estStarted,deadline')->from(TABLE_TASK)->where('id')->in($this->post->tasks)->fetchAll('id'); foreach($tasks as $task) { /* Save the assignedToes and stories, should linked to execution. */ $assignedToes[$task->assignedTo] = $task->execution; $taskStories[$task->story] = $task->story; if($task->parent < 0) $parents[$task->id] = $task->id; $data = new stdclass(); $data->project = $execution->project; $data->execution = $executionID; $data->status = $task->consumed > 0 ? 'doing' : 'wait'; if($task->status == 'cancel') { $data->canceledBy = ''; $data->canceledDate = null; } if(!empty($this->config->limitTaskDate)) { if($task->estStarted < $execution->begin or $task->estStarted > $execution->end or $task->deadline > $execution->end or $task->deadline < $execution->begin) $dateExceed .= "#{$task->id},"; if($task->estStarted < $execution->begin or $task->estStarted > $execution->end) $data->estStarted = $execution->begin; if($task->deadline > $execution->end or $task->deadline < $execution->begin) $data->deadline = $execution->end; } /* Update tasks. */ $this->dao->update(TABLE_TASK)->data($data)->where('id')->eq($task->id)->exec(); unset($data->status); $this->dao->update(TABLE_TASK)->data($data)->where('parent')->eq($task->id)->exec(); $this->loadModel('action')->create('task', $task->id, 'moved', '', $task->execution); } if(!empty($dateExceed)) { $dateExceed = trim($dateExceed, ','); echo js::alert(sprintf($this->lang->task->error->dateExceed, $dateExceed)); } /* Get stories of children task. */ if(!empty($parents)) { $childrens = $this->dao->select('*')->from(TABLE_TASK)->where('parent')->in($parents)->fetchAll('id'); foreach($childrens as $children) $taskStories[$children->story] = $children->story; } /* Remove empty story. */ unset($taskStories[0]); /* Add members to execution team. */ $teamMembers = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution'); foreach($assignedToes as $account => $preExecutionID) { if(!isset($teamMembers[$account])) { $role = $this->dao->select('*')->from(TABLE_TEAM) ->where('root')->eq($preExecutionID) ->andWhere('type')->eq('execution') ->andWhere('account')->eq($account) ->fetch(); $role->root = $executionID; $role->join = helper::today(); $this->dao->replace(TABLE_TEAM)->data($role)->exec(); } } /* Link stories. */ $executionStories = $this->loadModel('story')->getExecutionStoryPairs($executionID); $lastOrder = (int)$this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->orderBy('order_desc')->limit(1)->fetch('order'); $stories = $this->dao->select("id as story, product, version")->from(TABLE_STORY)->where('id')->in(array_keys($taskStories))->fetchAll('story'); foreach($taskStories as $storyID) { if(!isset($executionStories[$storyID])) { $story = zget($stories, $storyID, ''); if(empty($story)) continue; $story->project = $executionID; $story->order = ++$lastOrder; $this->dao->insert(TABLE_PROJECTSTORY)->data($story)->exec(); if($execution->multiple or $execution->type == 'project') $this->action->create('story', $storyID, 'linked2execution', '', $executionID); } } } /** * Stat story, task, bug data for execution. * * @param int $executionID * @access public * @return void */ public function statRelatedData($executionID) { $storyCount = $this->dao->select('count(t2.story) as storyCount')->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id = t2.story') ->where('t2.project')->eq($executionID) ->andWhere('t1.deleted')->eq(0) ->fetch('storyCount'); $taskCount = $this->dao->select('count(id) as taskCount')->from(TABLE_TASK)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->fetch('taskCount'); $bugCount = $this->dao->select('count(id) as bugCount')->from(TABLE_BUG)->where('execution')->eq($executionID)->andWhere('deleted')->eq(0)->fetch('bugCount'); $statData = new stdclass(); $statData->storyCount = $storyCount; $statData->taskCount = $taskCount; $statData->bugCount = $bugCount; return $statData; } /** * Import task from Bug. * * @param int $executionID * @access public * @return void */ public function importBug($executionID) { $this->loadModel('bug'); $this->loadModel('task'); $this->loadModel('story'); $now = helper::now(); $showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : ''; $modules = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, 0, $showAllModule ? 'allModule' : ''); $execution = $this->getByID($executionID); $requiredFields = str_replace(',story,', ',', ',' . $this->config->task->create->requiredFields . ','); $requiredFields = trim($requiredFields, ','); $bugToTasks = fixer::input('post')->get(); $bugs = $this->bug->getByList(array_keys($bugToTasks->import)); foreach($bugToTasks->import as $key => $value) { $bug = zget($bugs, $key, ''); if(empty($bug)) continue; $task = new stdClass(); $task->bug = $bug; $task->project = $execution->project; $task->execution = $executionID; $task->story = $bug->story; $task->storyVersion = $bug->storyVersion; $task->module = isset($modules[$bug->module]) ? $bug->module : 0; $task->fromBug = $key; $task->name = $bug->title; $task->type = 'devel'; $task->pri = $bugToTasks->pri[$key]; $task->estStarted = $bugToTasks->estStarted[$key]; $task->deadline = $bugToTasks->deadline[$key]; $task->estimate = $bugToTasks->estimate[$key]; $task->consumed = 0; $task->assignedTo = ''; $task->status = 'wait'; $task->openedDate = $now; $task->openedBy = $this->app->user->account; if($task->estimate !== '') $task->left = $task->estimate; if(strpos($requiredFields, 'estStarted') !== false and helper::isZeroDate($task->estStarted)) $task->estStarted = ''; if(strpos($requiredFields, 'deadline') !== false and helper::isZeroDate($task->deadline)) $task->deadline = ''; if(!empty($bugToTasks->assignedTo[$key])) { $task->assignedTo = $bugToTasks->assignedTo[$key]; $task->assignedDate = $now; } /* Check task required fields. */ foreach(explode(',', $requiredFields) as $field) { if(empty($field)) continue; if(!isset($task->$field)) continue; if(!empty($task->$field)) continue; if($field == 'estimate' and strlen(trim($task->estimate)) != 0) continue; dao::$errors['message'][] = sprintf($this->lang->error->notempty, $this->lang->task->$field); return false; } if(!preg_match("/^[0-9]+(.[0-9]{1,3})?$/", $task->estimate) and !empty($task->estimate)) { dao::$errors['message'][] = $this->lang->task->error->estimateNumber; return false; } if(!empty($this->config->limitTaskDate)) { $this->task->checkEstStartedAndDeadline($executionID, $task->estStarted, $task->deadline); if(dao::isError()) return false; } $tasks[$key] = $task; } foreach($tasks as $key => $task) { $bug = $task->bug; unset($task->bug); if(!$bug->confirmed) $this->dao->update(TABLE_BUG)->set('confirmed')->eq(1)->where('id')->eq($bug->id)->exec(); $this->dao->insert(TABLE_TASK)->data($task)->checkIF($task->estimate != '', 'estimate', 'float')->exec(); if(dao::isError()) { echo js::error(dao::getError()); return print(js::reload('parent')); } $taskID = $this->dao->lastInsertID(); if($task->story != false) $this->story->setStage($task->story); $actionID = $this->loadModel('action')->create('task', $taskID, 'Opened', ''); $mails[$key] = new stdClass(); $mails[$key]->taskID = $taskID; $mails[$key]->actionID = $actionID; $this->action->create('bug', $key, 'Totask', '', $taskID); $this->dao->update(TABLE_BUG)->set('toTask')->eq($taskID)->where('id')->eq($key)->exec(); /* activate bug if bug postponed. */ if($bug->status == 'resolved' && $bug->resolution == 'postponed') { $newBug = new stdclass(); $newBug->lastEditedBy = $this->app->user->account; $newBug->lastEditedDate = $now; $newBug->assignedDate = $now; $newBug->status = 'active'; $newBug->resolvedDate = '0000-00-00'; $newBug->resolution = ''; $newBug->resolvedBy = ''; $newBug->resolvedBuild = ''; $newBug->closedBy = ''; $newBug->closedDate = '0000-00-00'; $newBug->duplicateBug = '0'; $this->dao->update(TABLE_BUG)->data($newBug)->autoCheck()->where('id')->eq($key)->exec(); $this->dao->update(TABLE_BUG)->set('activatedCount = activatedCount + 1')->where('id')->eq($key)->exec(); $actionID = $this->action->create('bug', $key, 'Activated'); $changes = common::createChanges($bug, $newBug); $this->action->logHistory($actionID, $changes); } if(isset($task->assignedTo) and $task->assignedTo and $task->assignedTo != $bug->assignedTo) { $newBug = new stdClass(); $newBug->lastEditedBy = $this->app->user->account; $newBug->lastEditedDate = $now; $newBug->assignedTo = $task->assignedTo; $newBug->assignedDate = $now; $this->dao->update(TABLE_BUG)->data($newBug)->where('id')->eq($key)->exec(); if(dao::isError()) return print(js::error(dao::getError())); $changes = common::createChanges($bug, $newBug); $actionID = $this->action->create('bug', $key, 'Assigned', '', $newBug->assignedTo); $this->action->logHistory($actionID, $changes); } } return $mails; } /** * Update childs. * * @param int $executionID * @access public * @return void */ public function updateChilds($executionID) { $sql = "UPDATE " . TABLE_PROJECT . " SET parent = 0 WHERE parent = '$executionID'"; $this->dbh->exec($sql); if(!isset($_POST['childs'])) return; $childs = array_unique($_POST['childs']); foreach($childs as $childExecutionID) { $sql = "UPDATE " . TABLE_PROJECT . " SET parent = '$executionID' WHERE id = '$childExecutionID'"; $this->dbh->query($sql); } } /** * Change execution project. * * @param int $newProject * @param int $oldProject * @param int $executionID * @param string $syncStories yes|no * @access public * @return void */ public function changeProject($newProject, $oldProject, $executionID, $syncStories = 'no') { if($newProject == $oldProject) return; $this->dao->update(TABLE_EXECUTION)->set('parent')->eq($newProject)->set('path')->eq(",$newProject,$executionID,")->where('id')->eq($executionID)->exec(); $this->dao->update(TABLE_BUILD)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $this->dao->update(TABLE_BUG)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $this->dao->update(TABLE_CASE)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $this->dao->update(TABLE_DOC)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $this->dao->update(TABLE_DOCLIB)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $this->dao->update(TABLE_TASK)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $this->dao->update(TABLE_TESTREPORT)->set('project')->eq($newProject)->where('project')->eq($oldProject)->andWhere('execution')->eq($executionID)->exec(); $executionTeam = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution'); $projectTeam = $this->user->getTeamMemberPairs($newProject, 'project'); $addedAccounts = array(); foreach($executionTeam as $account => $realname) { if(isset($projectTeam[$account])) continue; $member = new stdclass(); $member->root = (int)$newProject; $member->type = 'project'; $member->account = $account; $member->join = helper::today(); $member->days = 0; $member->hours = $this->config->execution->defaultWorkhours; $this->dao->replace(TABLE_TEAM)->data($member)->exec(); $addedAccounts[$account] = $account; } $executionWhitelist = $this->loadModel('personnel')->getWhitelistAccount($executionID, 'sprint'); $projectWhitelist = $this->personnel->getWhitelistAccount($newProject, 'project'); foreach($executionWhitelist as $account) { if(isset($projectWhitelist[$account])) continue; $whitelist = new stdclass(); $whitelist->account = $account; $whitelist->objectType = 'project'; $whitelist->objectID = (int)$newProject; $whitelist->type = 'whitelist'; $whitelist->source = 'sync'; $this->dao->replace(TABLE_ACL)->data($whitelist)->exec(); $addedAccounts[$account] = $account; } /* Sync stories to new project. */ if($syncStories == 'yes') { $this->loadModel('action'); $projectLinkedStories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($newProject)->fetchPairs('story', 'story'); $executionLinkedStories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->fetchAll(); foreach($executionLinkedStories as $linkedStory) { if(isset($projectLinkedStories[$linkedStory->story])) continue; $linkedStory->project = $newProject; $this->dao->insert(TABLE_PROJECTSTORY)->data($linkedStory)->exec(); $this->action->create('story', $linkedStory->story, 'linked2project', '', $newProject); } } if($addedAccounts) $this->loadModel('user')->updateUserView($newProject, 'project', $addedAccounts); } /** * Link story. * * @param int $executionID * @param array $stories * @param array $products * @param string $extra * @param array $lanes * * @access public * @return bool */ public function linkStory($executionID, $stories = array(), $products = array(), $extra = '', $lanes = array()) { if(empty($executionID)) return false; if(empty($stories)) $stories = $this->post->stories; if(empty($stories)) return false; if(empty($products)) $products = $this->post->products; $this->loadModel('action'); $this->loadModel('kanban'); $versions = $this->loadModel('story')->getVersions($stories); $linkedStories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->orderBy('order_desc')->fetchPairs('story', 'order'); $lastOrder = reset($linkedStories); $storyList = $this->dao->select('id, status, branch')->from(TABLE_STORY)->where('id')->in(array_values($stories))->fetchAll('id'); $execution = $this->getById($executionID); $extra = str_replace(array(',', ' '), array('&', ''), $extra); parse_str($extra, $output); foreach($stories as $key => $storyID) { $notAllowedStatus = $this->app->rawMethod == 'batchcreate' ? 'closed' : 'draft,reviewing,closed'; if(strpos($notAllowedStatus, $storyList[$storyID]->status) !== false) continue; if(isset($linkedStories[$storyID])) continue; $laneID = isset($output['laneID']) ? $output['laneID'] : 0; if(!empty($lanes[$storyID])) $laneID = $lanes[$storyID]; $columnID = $this->kanban->getColumnIDByLaneID($laneID, 'backlog'); if(empty($columnID)) $columnID = isset($output['columnID']) ? $output['columnID'] : 0; if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, $laneID, $columnID, 'story', $storyID); $data = new stdclass(); $data->project = $executionID; $data->product = (int)$products[$storyID]; $data->branch = $storyList[$storyID]->branch; $data->story = $storyID; $data->version = $versions[$storyID]; $data->order = ++$lastOrder; $this->dao->replace(TABLE_PROJECTSTORY)->data($data)->exec(); $this->story->setStage($storyID); $this->linkCases($executionID, (int)$products[$storyID], $storyID); $action = $execution->type == 'project' ? 'linked2project' : 'linked2execution'; if($action == 'linked2execution' and $execution->type == 'kanban') $action = 'linked2kanban'; if($execution->multiple or $execution->type == 'project') $this->action->create('story', $storyID, $action, '', $executionID); } if(!isset($output['laneID']) or !isset($output['columnID'])) $this->kanban->updateLane($executionID, 'story'); } /** * Link cases. * * @param int $executionID * @param int $productID * @param int $storyID * @access public * @return void */ public function linkCases($executionID, $productID, $storyID) { $this->loadModel('action'); $linkedCases = $this->dao->select('*')->from(TABLE_PROJECTCASE)->where('project')->eq($executionID)->orderBy('order_desc')->fetchPairs('case', 'order'); $lastCaseOrder = empty($linkedCases) ? 0 : reset($linkedCases); $cases = $this->dao->select('id, version')->from(TABLE_CASE)->where('story')->eq($storyID)->fetchPairs(); $execution = $this->getById($executionID); foreach($cases as $caseID => $version) { if(isset($linkedCases[$caseID])) continue; $object = new stdclass(); $object->project = $executionID; $object->product = $productID; $object->case = $caseID; $object->version = $version; $object->order = ++$lastCaseOrder; $this->dao->insert(TABLE_PROJECTCASE)->data($object)->exec(); $action = $execution->type == 'project' ? 'linked2project' : 'linked2execution'; if($execution->multiple or $execution->type == 'project') $this->action->create('case', $caseID, $action, '', $executionID); } } /** * Link all stories by execution. * * @param int $executionID * @access public * @return void */ public function linkStories($executionID) { $plans = $this->dao->select('product, plan')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($executionID) ->fetchPairs('product', 'plan'); $planStories = array(); $planProducts = array(); $this->loadModel('story'); if(!empty($plans)) { $executionProducts = $this->loadModel('project')->getBranchesByProject($executionID); foreach($plans as $productID => $planIdList) { if(empty($planIdList)) continue; $planIdList = explode(',', $planIdList); $executionBranches = zget($executionProducts, $productID, array()); foreach($planIdList as $planID) { $planStory = $this->story->getPlanStories($planID); if(!empty($planStory)) { foreach($planStory as $id => $story) { if($story->status != 'active' or (!empty($story->branch) and !empty($executionBranches) and !isset($executionBranches[$story->branch]))) { unset($planStory[$id]); continue; } $planProducts[$story->id] = $story->product; } $planStories = array_merge($planStories, array_keys($planStory)); } } } } $projectID = $this->dao->select('project')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('project'); $this->session->set('project', $projectID); $this->linkStory($projectID, $planStories, $planProducts); $this->linkStory($executionID, $planStories, $planProducts); } /** * Unlink story. * * @param int $executionID * @param int $storyID * @param int $laneID * @param int $columnID * @access public * @return void */ public function unlinkStory($executionID, $storyID, $laneID = 0, $columnID = 0) { $execution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch(); if($execution->type == 'project') { $executions = $this->dao->select('*')->from(TABLE_EXECUTION)->where('parent')->eq($executionID)->fetchAll('id'); $executionStories = $this->dao->select('project,story')->from(TABLE_PROJECTSTORY)->where('story')->eq($storyID)->andWhere('project')->in(array_keys($executions))->fetchAll(); if(!empty($executionStories)) return print(js::alert($this->lang->execution->notAllowedUnlinkStory)); } $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->andWhere('story')->eq($storyID)->limit(1)->exec(); /* Resolve TABLE_KANBANCELL's field cards. */ if($execution->type == 'kanban') { $cell = $this->dao->select('*')->from(TABLE_KANBANCELL) ->where('kanban')->eq($executionID) ->andWhere('`column`')->eq($columnID) ->andWhere('lane')->eq($laneID) ->fetch(); /* Resolve signal ','. */ $cell->cards = str_replace(",$storyID,", ',', $cell->cards); if(strlen($cell->cards) == 1) $cell->cards = ''; $this->dao->update(TABLE_KANBANCELL)->data($cell) ->where('kanban')->eq($executionID) ->andWhere('`column`')->eq($columnID) ->andWhere('lane')->eq($laneID) ->exec(); } $order = 1; $stories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->orderBy('order')->fetchAll(); foreach($stories as $executionstory) { if($executionstory->order != $order) $this->dao->update(TABLE_PROJECTSTORY)->set('`order`')->eq($order)->where('project')->eq($executionID)->andWhere('story')->eq($executionstory->story)->exec(); $order++; } $this->loadModel('story')->setStage($storyID); $this->unlinkCases($executionID, $storyID); $objectType = $execution->type == 'project' ? 'unlinkedfromproject' : 'unlinkedfromexecution'; if($execution->multiple or $execution->type == 'project') $this->loadModel('action')->create('story', $storyID, $objectType, '', $executionID); /* Sync unlink story in no multiple execution. */ if(empty($execution->multiple) and $execution->type != 'project') { $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('project')->eq($execution->project)->andWhere('story')->eq($storyID)->limit(1)->exec(); $this->loadModel('action')->create('story', $storyID, 'unlinkedfromproject', '', $execution->project); } $tasks = $this->dao->select('id')->from(TABLE_TASK)->where('story')->eq($storyID)->andWhere('execution')->eq($executionID)->andWhere('status')->in('wait,doing')->fetchPairs('id'); foreach($tasks as $taskID) { if(empty($taskID)) continue; $changes = $this->loadModel('task')->cancel($taskID); $actionID = $this->action->create('task', $taskID, 'Canceled'); $this->action->logHistory($actionID, $changes); } } /** * Unlink cases. * * @param int $executionID * @param int $storyID * @access public * @return void */ public function unlinkCases($executionID, $storyID) { $this->loadModel('action'); $execution = $this->dao->findById($executionID)->from(TABLE_EXECUTION)->fetch(); $cases = $this->dao->select('id')->from(TABLE_CASE)->where('story')->eq($storyID)->fetchAll('id'); foreach($cases as $caseID => $case) { $this->dao->delete()->from(TABLE_PROJECTCASE)->where('project')->eq($executionID)->andWhere('`case`')->eq($caseID)->limit(1)->exec(); $action = $execution->type == 'project' ? 'unlinkedfromproject' : 'unlinkedfromexecution'; if($execution->multiple or $execution->type == 'project') $this->action->create('case', $caseID, $action, '', $executionID); /* Sync unlink case in no multiple execution. */ if(empty($execution->multiple) and $execution->type != 'project') { $this->dao->delete()->from(TABLE_PROJECTCASE)->where('project')->eq($execution->project)->andWhere('`case`')->eq($caseID)->limit(1)->exec(); $this->loadModel('action')->create('case', $caseID, 'unlinkedfromproject', '', $execution->project); } } $order = 1; $cases = $this->dao->select('*')->from(TABLE_PROJECTCASE)->where('project')->eq($executionID)->orderBy('order')->fetchAll(); foreach($cases as $case) { if($case->order != $order) $this->dao->update(TABLE_PROJECTCASE)->set('`order`')->eq($order)->where('project')->eq($executionID)->andWhere('`case`')->eq($case->case)->exec(); $order++; } } /** * Get team members. * * @param int $executionID * @access public * @return array */ public function getTeamMembers($executionID) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getTeamMembers(); return $this->dao->select("t1.*, t1.hours * t1.days AS totalHours, t2.id as userID, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') ->where('t1.root')->eq((int)$executionID) ->andWhere('t1.type')->eq('execution') ->andWhere('t2.deleted')->eq('0') ->beginIF($this->config->vision)->andWhere("CONCAT(',', t2.visions, ',')")->like("%,{$this->config->vision},%")->fi() ->fetchAll('account'); } /** * Get members by execution id list. * * @param array $executionIdList * @access public * @return void */ public function getMembersByIdList($executionIdList) { return $this->dao->select("t1.root, t1.account, t2.realname")->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') ->where('t1.root')->in($executionIdList) ->andWhere('t1.type')->eq('execution') ->andWhere('t2.deleted')->eq('0') ->fetchGroup('root'); } /** * Get teams which can be imported. * * @param string $account * @param int $currentExecution * @access public * @return array */ public function getTeams2Import($account, $currentExecution) { $execution = $this->dao->findById($currentExecution)->from(TABLE_EXECUTION)->fetch(); return $this->dao->select('t1.root, t2.name as executionName') ->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.root = t2.id') ->where('t1.account')->eq($account) ->andWhere('t1.root')->ne($currentExecution) ->andWhere('t1.type')->eq('execution') ->andWhere('t2.project')->eq($execution->project) ->andWhere('t2.deleted')->eq('0') ->groupBy('t1.root') ->orderBy('t1.root DESC') ->fetchPairs(); } /** * Get members of a execution who can be imported. * * @param int $execution * @param array $currentMembers * @access public * @return array */ public function getMembers2Import($execution, $currentMembers) { if($execution == 0) return array(); return $this->dao->select('account, role, hours') ->from(TABLE_TEAM) ->where('root')->eq($execution) ->andWhere('type')->in('project,execution') ->andWhere('account')->notIN($currentMembers) ->fetchAll('account'); } /** * Get projects and executions that copy the team. * * @param int $projectID * @access public * @return array */ public function getCanCopyObjects($projectID = 0) { if(empty($projectID)) return array(); $objectPairs = $this->dao->select('id,name')->from(TABLE_PROJECT) ->where('deleted')->eq(0) ->andWhere('project', true)->eq($projectID) ->andWhere('type')->ne('project') ->markRight(1) ->orWhere('id')->eq($projectID) ->orderBy('type_asc,openedDate_desc') ->limit('9') ->fetchPairs(); $countPairs = $this->dao->select('root, COUNT(*) as count')->from(TABLE_TEAM) ->where('( type')->eq('project') ->andWhere('root')->eq($projectID) ->markRight(1) ->orWhere('( type')->eq('execution') ->andWhere('root')->in(array_keys($objectPairs)) ->markRight(1) ->groupBy('root') ->fetchPairs('root'); foreach($objectPairs as $objectID => $objectName) { $memberCount = zget($countPairs, $objectID, 0); $countTip = $memberCount > 1 ? str_replace('member', 'members', $this->lang->execution->countTip) : $this->lang->execution->countTip; $objectPairs[$objectID] = $objectName . sprintf($countTip, $memberCount); } return $objectPairs; } /** * Manage team members. * * @param int $executionID * @access public * @return void */ public function manageMembers($executionID) { $execution = $this->getByID($executionID); $data = (array)fixer::input('post')->get(); extract($data); $executionID = (int)$executionID; $executionType = 'execution'; $accounts = array_unique($accounts); $limited = array_values($limited); $oldJoin = $this->dao->select('`account`, `join`')->from(TABLE_TEAM)->where('root')->eq($executionID)->andWhere('type')->eq($executionType)->fetchPairs(); foreach($accounts as $key => $account) { if(empty($account)) continue; if(!empty($execution->days) and (int)$days[$key] > $execution->days) { dao::$errors['message'][] = sprintf($this->lang->execution->daysGreaterProject, $execution->days); return false; } if((float)$hours[$key] > 24) { dao::$errors['message'][] = $this->lang->execution->errorHours; return false; } } $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($executionID)->andWhere('type')->eq($executionType)->exec(); $executionMember = array(); foreach($accounts as $key => $account) { if(empty($account)) continue; $member = new stdclass(); $member->role = $roles[$key]; $member->days = $days[$key]; $member->hours = $hours[$key]; $member->limited = $limited[$key]; $member->root = $executionID; $member->account = $account; $member->join = isset($oldJoin[$account]) ? $oldJoin[$account] : helper::today(); $member->type = $executionType; $executionMember[$account] = $member; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } /* Only changed account update userview. */ $oldAccounts = array_keys($oldJoin); $changedAccounts = array_diff($accounts, $oldAccounts); $changedAccounts = array_merge($changedAccounts, array_diff($oldAccounts, $accounts)); $changedAccounts = array_unique($changedAccounts); /* Add the execution team members to the project. */ if($execution->project) $this->addProjectMembers($execution->project, $executionMember); if($execution->acl != 'open') $this->updateUserView($executionID, 'sprint', $changedAccounts); } /** * Add the execution team members to the project. * * @param int $projectID * @param array $members * @access public * @return void */ public function addProjectMembers($projectID = 0, $members = array()) { $projectType = 'project'; $oldJoin = $this->dao->select('`account`, `join`')->from(TABLE_TEAM)->where('root')->eq($projectID)->andWhere('type')->eq($projectType)->fetchPairs(); $accounts = array(); foreach($members as $account => $member) { if(isset($oldJoin[$member->account])) continue; $accounts[] = $member->account; $member->root = $projectID; $member->type = $projectType; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } /* Only changed account update userview. */ $oldAccounts = array_keys($oldJoin); $changedAccounts = array_diff($accounts, $oldAccounts); $changedAccounts = array_merge($changedAccounts, array_diff($oldAccounts, $accounts)); $changedAccounts = array_unique($changedAccounts); if($changedAccounts) { $this->loadModel('user')->updateUserView($projectID, $projectType, $changedAccounts); $linkedProducts = $this->dao->select("t2.id")->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where('t2.deleted')->eq(0) ->andWhere('t1.project')->eq($projectID) ->andWhere('t2.vision')->eq($this->config->vision) ->fetchPairs(); if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', $changedAccounts); } } /** * Unlink a member. * * @param int $sprintID * @param string $account * @access public * @return void */ public function unlinkMember($sprintID, $account) { $sprint = $this->getByID($sprintID); $type = strpos(',stage,sprint,kanban,', ",$sprint->type,") !== false ? 'execution' : $sprint->type; $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq((int)$sprintID)->andWhere('type')->eq($type)->andWhere('account')->eq($account)->exec(); $this->updateUserView($sprintID, 'sprint', array($account)); /* Remove team members from the sprint or stage, and determine whether to remove team members from the execution. */ if(strpos(',stage,sprint,kanban,', ",$sprint->type,") !== false) { $teamMember = $this->dao->select('t1.id, t2.account')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_TEAM)->alias('t2')->on('t1.id = t2.root') ->where('t1.project')->eq($sprint->project) ->andWhere('t1.type')->eq($sprint->type) ->andWhere('t2.account')->eq($account) ->fetch(); if(empty($teamMember)) { $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq($sprint->project)->andWhere('type')->eq('project')->andWhere('account')->eq($account)->exec(); $this->loadModel('user')->updateUserView($sprint->project, 'project', array($account)); $linkedProducts = $this->loadModel('product')->getProductPairsByProject($sprint->project); if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', array($account)); } } } /** * Compute burn of a execution. * * @param int $executionID * @access public * @return array */ public function computeBurn($executionID = 0) { $today = helper::today(); $executions = $this->dao->select('id, code')->from(TABLE_EXECUTION) ->where('type')->in('sprint,stage') ->andWhere('lifetime')->ne('ops') ->andWhere('status')->notin('done,closed,suspended') ->beginIF($executionID)->andWhere('id')->in($executionID)->fi() ->fetchPairs(); if(!$executions) return array(); /* Update today's data of burn. */ $burns = $this->dao->select("execution, '$today' AS date, sum(estimate) AS `estimate`, sum(`left`) AS `left`, SUM(consumed) AS `consumed`") ->from(TABLE_TASK) ->where('execution')->in(array_keys($executions)) ->andWhere('deleted')->eq('0') ->andWhere('parent')->ge('0') ->andWhere('status')->ne('cancel') ->groupBy('execution') ->fetchAll('execution'); $closedLefts = $this->dao->select('execution, sum(`left`) AS `left`')->from(TABLE_TASK) ->where('execution')->in(array_keys($executions)) ->andWhere('deleted')->eq('0') ->andWhere('parent')->ge('0') ->andWhere('status')->eq('closed') ->groupBy('execution') ->fetchAll('execution'); $finishedEstimates = $this->dao->select("execution, sum(`estimate`) AS `estimate`")->from(TABLE_TASK) ->where('execution')->in(array_keys($executions)) ->andWhere('deleted')->eq('0') ->andWhere('parent')->ge('0') ->andWhere('status', true)->eq('done') ->orWhere('status')->eq('closed') ->markRight(1) ->groupBy('execution') ->fetchAll('execution'); $storyPoints = $this->dao->select('t1.project, sum(t2.estimate) AS `storyPoint`')->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t2.product = t3.id') ->where('t1.project')->in(array_keys($executions)) ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.status')->ne('closed') ->andWhere('t2.stage')->in('wait,planned,projected,developing') ->groupBy('project') ->fetchAll('project'); foreach($burns as $executionID => $burn) { if(isset($closedLefts[$executionID])) { $closedLeft = $closedLefts[$executionID]; $burn->left -= (int)$closedLeft->left; } if(isset($finishedEstimates[$executionID])) { $finishedEstimate = $finishedEstimates[$executionID]; $burn->estimate -= (int)$finishedEstimate->estimate; } if(isset($storyPoints[$executionID])) $burn->storyPoint = $storyPoints[$executionID]->storyPoint; $this->dao->replace(TABLE_BURN)->data($burn)->exec(); $burn->executionName = $executions[$burn->execution]; } return $burns; } /** * Compute cfd of a execution. * * @param int $executionID * @access public * @return array */ public function computeCFD($executionID = 0) { $today = helper::today(); $executions = $this->dao->select('id, code')->from(TABLE_EXECUTION) ->where('type')->eq('kanban') ->andWhere('status')->notin('done,closed,suspended') ->beginIF($executionID)->andWhere('id')->in($executionID)->fi() ->fetchPairs(); if(!$executions) return array(); /* Update today's data of cfd. */ $cells = $this->dao->select("t1.id, t1.kanban as execution, t1.`column`, t1.type, t1.cards, t1.lane, t2.name, t2.parent") ->from(TABLE_KANBANCELL)->alias('t1') ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id') ->where('t1.kanban')->in(array_keys($executions)) ->andWhere('t2.deleted')->eq('0') ->andWhere('t1.type')->in('story,bug,task') ->orderBy('t2.id asc') ->fetchAll('id'); /* Group by execution/type/name/lane/column. */ $columnGroup = array(); $parentNames = array(); foreach($cells as $id => $column) { if($column->parent == '-1') { $parentNames[$column->column] = $column->name; continue; } $column->name = isset($parentNames[$column->parent]) ? $parentNames[$column->parent] . "($column->name)" : $column->name; $columnGroup[$column->execution][$column->type][$column->name][$column->lane][$column->column] = $column; } foreach($columnGroup as $executionID => $executionGroup) { foreach($executionGroup as $type => $columns) { foreach($columns as $colName => $laneGroup) { $cfd = new stdclass(); $cfd->count = 0; $cfd->date = $today; $cfd->type = $type; foreach($laneGroup as $laneID => $columnGroup) { foreach($columnGroup as $colID => $columnCard) { $cards = trim($columnCard->cards, ','); $cfd->count += $cards ? count(explode(',', $cards)) : 0; } } $cfd->name = $colName; $cfd->execution = $executionID; $this->dao->replace(TABLE_CFD)->data($cfd)->exec(); } } } } /** * Check whether there is data on the specified date of execution, and there is no data with the latest date added. * * @param int $executionID * @param string $date * @access public * @return void */ public function checkCFDData($executionID, $date) { $today = helper::today(); if($date >= $today) return; $checkData = $this->dao->select("date, `count` AS value, `name`")->from(TABLE_CFD) ->where('execution')->eq((int)$executionID) ->andWhere('date')->eq($date) ->orderBy('date DESC, id asc')->fetchGroup('name', 'date'); if(!$checkData) { $closetoDate = $this->dao->select("max(date) as date")->from(TABLE_CFD)->where('execution')->eq((int)$executionID)->andWhere('date')->lt($date)->fetch('date'); if($closetoDate) { $copyData = $this->dao->select("*")->from(TABLE_CFD) ->where('execution')->eq((int)$executionID) ->andWhere('date')->eq($closetoDate) ->fetchAll(); foreach($copyData as $data) { unset($data->id); $data->date = $date; $this->dao->replace(TABLE_CFD)->data($data)->exec(); } } } } /** * Fix burn for first day. * * @param int $executionID * @access public * @return void */ public function fixFirst($executionID) { $execution = $this->getById($executionID); $burn = $this->dao->select('*')->from(TABLE_BURN)->where('execution')->eq($executionID)->andWhere('date')->eq($execution->begin)->andWhere('task')->eq(0)->fetch(); $withLeft = $this->post->withLeft ? $this->post->withLeft : 0; $data = fixer::input('post') ->add('execution', $executionID) ->add('date', $execution->begin) ->add('left', $withLeft ? $this->post->estimate : $burn->left) ->add('consumed', empty($burn) ? 0 : $burn->consumed) ->remove('withLeft') ->get(); if(!is_numeric($data->estimate)) return false; $this->dao->replace(TABLE_BURN)->data($data)->exec(); } /** * Get begin and end for CFD. * * @param object $execution * @access public * @return void */ public function getBeginEnd4CFD($execution) { $end = (!helper::isZeroDate($execution->closedDate) and date('Y-m-d', strtotime($execution->closedDate)) < helper::today()) ? date('Y-m-d', strtotime($execution->closedDate)) : helper::today(); $begin = (!helper::isZeroDate($execution->openedDate) and date('Y-m-d', strtotime($execution->openedDate)) > date('Y-m-d', strtotime('-13 days', strtotime($end)))) ? date('Y-m-d', strtotime($execution->openedDate)) : date('Y-m-d', strtotime('-13 days', strtotime($end))); return array($begin, $end); } /** * Get burn data for flot * * @param int $executionID * @param string $burnBy * @param bool $showDelay * @param array $dateList * @access public * @return array */ public function getBurnDataFlot($executionID = 0, $burnBy = '', $showDelay = false, $dateList = array()) { /* Get execution and burn counts. */ $execution = $this->getById($executionID); /* If the burnCounts > $itemCounts, get the latest $itemCounts records. */ $sets = $this->dao->select("date AS name, `$burnBy` AS value, `$burnBy`")->from(TABLE_BURN)->where('execution')->eq((int)$executionID)->andWhere('task')->eq(0)->orderBy('date DESC')->fetchAll('name'); $burnData = array(); foreach($sets as $date => $set) { if($date < $execution->begin) continue; if(!$showDelay and $date > $execution->end) $set->value = 'null'; if($showDelay and $date < $execution->end) $set->value = 'null'; $burnData[$date] = $set; } foreach($dateList as $date) { if(!isset($burnData[$date])) { if(($showDelay and $date < $execution->end) or (!$showDelay and $date > $execution->end)) { $set = new stdClass(); $set->name = $date; $set->value = 'null'; $set->$burnBy = 0; $burnData[$date] = $set; } } } krsort($burnData); $burnData = array_reverse($burnData); return $burnData; } /** * Get execution burn data. * * @param array $executions * @access public * @return array */ public function getBurnData($executions) { if(empty($executions)) return array(); /* Get burndown charts datas. */ $burns = $this->dao->select('execution, date AS name, `left` AS value') ->from(TABLE_BURN) ->where('execution')->in(array_keys($executions)) ->andWhere('task')->eq(0) ->orderBy('date desc') ->fetchGroup('execution', 'name'); foreach($burns as $executionID => $executionBurns) { /* If executionBurns > $itemCounts, split it, else call processBurnData() to pad burns. */ $begin = $executions[$executionID]->begin; $end = $executions[$executionID]->end; if(helper::isZeroDate($begin)) $begin = $executions[$executionID]->openedDate; $executionBurns = $this->processBurnData($executionBurns, $this->config->execution->defaultBurnPeriod, $begin, $end); /* Shorter names. */ foreach($executionBurns as $executionBurn) { $executionBurn->name = substr($executionBurn->name, 5); unset($executionBurn->execution); } ksort($executionBurns); $burns[$executionID] = $executionBurns; } return $burns; } /** * Process burndown datas when the sets is smaller than the itemCounts. * * @param array $sets * @param int $itemCounts * @param date $begin * @param date $end * @param string $mode * @access public * @return array */ public function processBurnData($sets, $itemCounts, $begin, $end, $mode = 'noempty') { if(!helper::isZeroDate($end)) { $period = helper::diffDate($end, $begin) + 1; $counts = $period > $itemCounts ? $itemCounts : $period; } else { $counts = $itemCounts; $period = $itemCounts; $end = date(DT_DATE1, strtotime("+$counts days", strtotime($begin))); } $current = $begin; $today = helper::today(); $endTime = strtotime($end); $preValue = 0; $todayTag = 0; foreach($sets as $date => $set) { if($begin > $date) unset($sets[$date]); } for($i = 0; $i < $period; $i++) { $currentTime = strtotime($current); if($currentTime > $endTime) break; if($currentTime > time() and !$todayTag) { $todayTag = $i + 1; } if(isset($sets[$current])) $preValue = $sets[$current]->value; if(!isset($sets[$current]) and $mode == 'noempty') { $sets[$current] = new stdclass(); $sets[$current]->name = $current; $sets[$current]->value = helper::diffDate($current, $today) < 0 ? $preValue : 'null'; } $nextDay = date(DT_DATE1, $currentTime + 24 * 3600); $current = $nextDay; } ksort($sets); if(count($sets) <= $counts) return $sets; if($endTime <= time()) return array_slice($sets, -$counts, $counts); if($todayTag <= $counts) return array_slice($sets, 0, $counts); if($todayTag > $counts) return array_slice($sets, $todayTag - $counts, $counts); } /** * Build CFD data. * * @param int $executionID * @param string $type * @param array $dateList * @access public * @return array */ public function buildCFDData($executionID, $dateList, $type) { $this->loadModel('report'); $setGroup = $this->getCFDData($executionID, $dateList, $type); if(empty($setGroup)) return array(); $chartData['labels'] = $this->report->convertFormat($dateList, DT_DATE5); $chartData['line'] = array(); foreach($setGroup as $name => $sets) { $chartData['line'][$name] = $this->report->createSingleJSON($sets, $dateList); } return $chartData; } /** * Get CFD data to display. * * @param int $executionID * @param string $type * @param array $dateList * @access public * @return array */ public function getCFDData($executionID = 0, $dateList = array(), $type = 'story') { $execution = $this->getById($executionID); $setGroup = $this->dao->select("date, `count` AS value, `name`")->from(TABLE_CFD) ->where('execution')->eq((int)$executionID) ->andWhere('type')->eq($type) ->andWhere('date')->in($dateList) ->orderBy('date DESC, id asc')->fetchGroup('name', 'date'); $data = array(); foreach($setGroup as $name => $sets) { foreach($sets as $date => $set) { if($date < $execution->begin) continue; $data[$name][$date] = $set; } } return $data; } /** * Get taskes by search. * * @param string $condition * @param object $pager * @param string $orderBy * @access public * @return array */ public function getSearchTasks($condition, $pager, $orderBy) { if(strpos($condition, '`assignedTo`') !== false) { preg_match("/`assignedTo`\s+(([^']*) ('([^']*)'))/", $condition, $matches); $condition = preg_replace('/`(\w+)`/', 't1.`$1`', $condition); $condition = str_replace("t1.$matches[0]", "(t1.$matches[0] or (t1.mode = 'multi' and t2.`account` $matches[1] and t1.status != 'closed' and t2.status != 'done') )", $condition); } $sql = $this->dao->select('t1.id')->from(TABLE_TASK)->alias('t1'); if(strpos($condition, '`assignedTo`') !== false) $sql = $sql->leftJoin(TABLE_TASKTEAM)->alias('t2')->on("t2.task = t1.id and t2.account $matches[1]"); $orderBy = array_map(function($value){return 't1.' . $value;}, explode(',', $orderBy)); $orderBy = implode(',', $orderBy); $taskIdList = $sql->where($condition) ->andWhere('deleted')->eq(0) ->orderBy($orderBy) ->page($pager, 't1.id') ->fetchAll('id'); $orderBy = str_replace('pri_', 'priOrder_', $orderBy); $tasks = $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.product, t2.branch, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName, IF(t1.`pri` = 0, 999, t1.`pri`) as priOrder') ->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account') ->where('t1.deleted')->eq(0) ->andWhere('t1.id')->in(array_keys($taskIdList)) ->orderBy($orderBy) ->fetchAll('id'); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task', true); if(empty($tasks)) return array(); $taskTeam = $this->dao->select('*')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task'); if(!empty($taskTeam)) { foreach($taskTeam as $taskID => $team) $tasks[$taskID]->team = $team; } $parents = array(); foreach($tasks as $task) { if($task->parent > 0) $parents[$task->parent] = $task->parent; } $parents = $this->dao->select('*')->from(TABLE_TASK)->where('id')->in($parents)->fetchAll('id'); if($this->config->vision == 'lite') $tasks = $this->loadModel('task')->appendLane($tasks); foreach($tasks as $task) { if($task->parent > 0) { if(isset($tasks[$task->parent])) { $tasks[$task->parent]->children[$task->id] = $task; unset($tasks[$task->id]); } else { $parent = $parents[$task->parent]; $task->parentName = $parent->name; } } } return $this->loadModel('task')->processTasks($tasks); } /** * Get bugs by search in execution. * * @param array $products * @param int $executionID * @param string $sql * @param object $pager * @param string $orderBy * @access public * @return mixed */ public function getSearchBugs($products, $executionID, $sql, $pager, $orderBy) { return $this->dao->select('*')->from(TABLE_BUG) ->where($sql) ->andWhere('status')->eq('active') ->andWhere('toTask')->eq(0) ->andWhere('tostory')->eq(0) ->beginIF(!empty($products))->andWhere('product')->in(array_keys($products))->fi() ->beginIF(empty($products))->andWhere('execution')->eq($executionID)->fi() ->andWhere('deleted')->eq(0) ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); } /** * Get the summary of execution. * * @param array $tasks * @access public * @return string */ public function summary($tasks) { $taskSum = $statusWait = $statusDone = $statusDoing = $statusClosed = $statusCancel = $statusPause = 0; $totalEstimate = $totalConsumed = $totalLeft = 0.0; foreach($tasks as $task) { if(!isset($tasks[$task->parent]) or $task->parent <= 0) { $totalEstimate += $task->estimate; $totalConsumed += $task->consumed; if($task->status != 'cancel' and $task->status != 'closed') $totalLeft += $task->left; } $statusVar = 'status' . ucfirst($task->status); $$statusVar ++; if(isset($task->children)) { foreach($task->children as $children) { $statusVar = 'status' . ucfirst($children->status); $$statusVar ++; $taskSum ++; } } $taskSum ++; } return sprintf($this->lang->execution->taskSummary, $taskSum, $statusWait, $statusDoing, round($totalEstimate, 1), round($totalConsumed, 1), round($totalLeft, 1)); } /** * Judge an action is clickable or not. * * @param object $execution * @param string $action * @access public * @return bool */ public static function isClickable($execution, $action) { $action = strtolower($action); $clickable = commonModel::hasPriv('execution', $action); if(!$clickable) return false; if($action == 'start') return $execution->status == 'wait'; if($action == 'close') return $execution->status != 'closed'; if($action == 'suspend') return $execution->status == 'wait' or $execution->status == 'doing'; if($action == 'putoff') return $execution->status == 'wait' or $execution->status == 'doing'; if($action == 'activate') return $execution->status == 'suspended' or $execution->status == 'closed'; return true; } /** * Get no weekend date * * @param string $begin * @param string $end * @param string $type * @param string|int $interval * @param string $format * @param string $executionDeadline * @access public * @return array */ public function getDateList($begin, $end, $type, $interval = '', $format = 'm/d/Y', $executionDeadline = '') { $this->app->loadClass('date', true); $dateList = date::getDateList($begin, $end, $format, $type, $this->config->execution->weekend); $days = count($dateList); if(!$interval) $interval = floor(count($dateList) / $this->config->execution->maxBurnDay); /* Remove date by interval. */ if($interval) { $spaces = (int)$interval; $counter = $spaces; foreach($dateList as $i => $date) { $counter ++; if($date == $executionDeadline) continue; if($counter <= $spaces) { unset($dateList[$i]); continue; } $counter = 0; } } return array(array_values($dateList), $interval); } /** * Get total estimate. * * @param int $executionID * @access public * @return float */ public function getTotalEstimate($executionID) { $estimate = $this->dao->select('SUM(estimate) as estimate')->from(TABLE_TASK)->where('execution')->eq($executionID)->andWhere('deleted')->eq('0')->fetch('estimate'); return round($estimate); } /** * Fix order. * * @access public * @return void */ public function fixOrder() { $executions = $this->dao->select('id,`order`')->from(TABLE_EXECUTION)->orderBy('order')->fetchPairs('id', 'order'); $i = 0; foreach($executions as $id => $order) { $i++; $newOrder = $i * 5; if($order == $newOrder) continue; $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($newOrder)->where('id')->eq($id)->exec(); } } /** * Build bug search form. * * @param array $products * @param int $queryID * @param string $actionURL * @access public * @return void */ public function buildBugSearchForm($products, $queryID, $actionURL, $type = 'execution') { $modules = array(); $builds = array('' => '', 'trunk' => $this->lang->trunk); foreach($products as $product) { $productModules = $this->loadModel('tree')->getOptionMenu($product->id, 'bug'); $productBuilds = $this->loadModel('build')->getBuildPairs($product->id, 'all', $params = 'noempty|notrunk|withbranch'); foreach($productModules as $moduleID => $moduleName) { $modules[$moduleID] = ((count($products) >= 2 and $moduleID) ? $product->name : '') . $moduleName; } foreach($productBuilds as $buildID => $buildName) { $builds[$buildID] = ((count($products) >= 2 and $buildID) ? $product->name . '/' : '') . $buildName; } } $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products)); $branchPairs = array(); $productType = 'normal'; $productNum = count($products); $productPairs = array(0 => ''); foreach($products as $product) { $productPairs[$product->id] = $product->name; if($product->type != 'normal') { $productType = $product->type; if(isset($product->branches)) { foreach($product->branches as $branch) { if(isset($branchGroups[$product->id][$branch])) $branchPairs[$branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$branch]; } } else { $productBranches = isset($branchGroups[$product->id]) ? $branchGroups[$product->id] : array(0); if(count($products) > 1) { foreach($productBranches as $branchID => $branchName) $productBranches[$branchID] = $product->name . '/' . $branchName; } $branchPairs += $productBranches; } } } $this->config->bug->search['module'] = $type == 'execution' ? 'executionBug' : 'projectBug'; $this->config->bug->search['actionURL'] = $actionURL; $this->config->bug->search['queryID'] = $queryID; unset($this->config->bug->search['fields']['execution']); $this->config->bug->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject); $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts($products); $this->config->bug->search['params']['module']['values'] = $modules; $this->config->bug->search['params']['openedBuild']['values'] = $builds; $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; if($productType == 'normal') { unset($this->config->bug->search['fields']['branch']); unset($this->config->bug->search['params']['branch']); } else { $this->config->bug->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productType]); $this->config->bug->search['params']['branch']['values'] = array('' => '') + $branchPairs; } $this->config->bug->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $this->lang->bug->statusList); $this->loadModel('search')->setSearchParams($this->config->bug->search); } /** * Build task search form. * * @param int $executionID * @param array $executions * @param int $queryID * @param string $actionURL * @access public * @return void */ public function buildTaskSearchForm($executionID, $executions, $queryID, $actionURL) { $this->config->execution->search['actionURL'] = $actionURL; $this->config->execution->search['queryID'] = $queryID; $this->config->execution->search['params']['execution']['values'] = array(''=>'', $executionID => $executions[$executionID], 'all' => $this->lang->execution->allExecutions); $showAllModule = isset($this->config->execution->task->allModule) ? $this->config->execution->task->allModule : ''; $this->config->execution->search['params']['module']['values'] = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, 0, $showAllModule ? 'allModule' : ''); $this->loadModel('search')->setSearchParams($this->config->execution->search); } /** * Get Kanban tasks * * @param int $executionID * @param string $orderBy * @param object $pager * @param array|string $excludeTasks * @access public * @return void */ public function getKanbanTasks($executionID, $orderBy = 'status_asc, id_desc', $pager = null, $excludeTasks = '') { $tasks = $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName') ->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account') ->where('t1.execution')->eq((int)$executionID) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->ge(0) ->beginIF($excludeTasks)->andWhere('t1.id')->notIN($excludeTasks)->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); if($tasks) return $this->loadModel('task')->processTasks($tasks); return array(); } /** * Get kanban group data. * * @param array $tasks * @param array $bugs * @access public * @return array */ public function getKanbanGroupData($stories, $tasks, $bugs, $type = 'story') { $kanbanGroup = array(); if($type == 'story') $kanbanGroup = $stories; foreach($tasks as $task) { $groupKey = $type == 'story' ? $task->storyID : $task->$type; $status = $task->status; if(!empty($groupKey) and (($type == 'story' and isset($stories[$groupKey])) or $type != 'story')) { if($type == 'assignedTo' and $groupKey == 'closed') { $closedTasks[$groupKey][] = $task; } else { if(!isset($kanbanGroup[$groupKey])) $kanbanGroup[$groupKey] = new stdclass(); $kanbanGroup[$groupKey]->tasks[$status][] = $task; } } else { $noKeyTasks[$status][] = $task; } } foreach($bugs as $bug) { $groupKey = $type == 'finishedBy' ? $bug->resolvedBy : $bug->$type; $status = $bug->status; $status = $status == 'active' ? 'wait' : ($status == 'resolved' ? ($bug->resolution == 'postponed' ? 'cancel' : 'done') : $status); if(!empty($groupKey) and (($type == 'story' and isset($stories[$groupKey])) or $type != 'story')) { if($type == 'assignedTo' and $groupKey == 'closed') { $closedBugs[$groupKey][] = $bug; } else { if(!isset($kanbanGroup[$groupKey])) $kanbanGroup[$groupKey] = new stdclass(); $kanbanGroup[$groupKey]->bugs[$status][] = $bug; } } else { $noKeyBugs[$status][] = $bug; } } $kanbanGroup['closed'] = new stdclass(); if(isset($closedTasks)) $kanbanGroup['closed']->tasks = $closedTasks; if(isset($closedBugs)) $kanbanGroup['closed']->bugs = $closedBugs; $kanbanGroup['nokey'] = new stdclass(); if(isset($noKeyTasks)) $kanbanGroup['nokey']->tasks = $noKeyTasks; if(isset($noKeyBugs)) $kanbanGroup['nokey']->bugs = $noKeyBugs; return $kanbanGroup; } /** * Save Kanban Data. * * @param int $executionID * @param array $kanbanDatas * @access public * @return void */ public function saveKanbanData($executionID, $kanbanDatas) { $data = array(); foreach($kanbanDatas as $type => $kanbanData) $data[$type] = array_keys($kanbanData); $this->loadModel('setting')->setItem("null.execution.kanban.execution$executionID", json_encode($data)); } /** * Get Prev Kanban. * * @param int $executionID * @access public * @return array */ public function getPrevKanban($executionID) { $prevKanbans = $this->loadModel('setting')->getItem("owner=null&module=execution§ion=kanban&key=execution$executionID"); return json_decode($prevKanbans, true); } /** * Get kanban setting. * * @access public * @return object */ public function getKanbanSetting() { $allCols = '1'; $showOption = '0'; if(isset($this->config->execution->kanbanSetting->allCols)) $allCols = $this->config->execution->kanbanSetting->allCols; $colorList = $this->config->execution->kanbanSetting->colorList; if(!is_array($colorList)) $colorList = json_decode($colorList, true); $kanbanSetting = new stdclass(); $kanbanSetting->allCols = $allCols; $kanbanSetting->showOption = $showOption; $kanbanSetting->colorList = $colorList; return $kanbanSetting; } /** * Get kanban columns. * * @param object $kanbanSetting * @access public * @return array */ public function getKanbanColumns($kanbanSetting) { if($kanbanSetting->allCols) return array('wait', 'doing', 'pause', 'done', 'cancel', 'closed'); return array('wait', 'doing', 'pause', 'done'); } /** * 获取状态和方法的映射关系,此关系决定了看板内容能否从一个泳道拖动到另一个泳道,以及拖动后执行什么方法。 * Get the mapping between state and method. This relationship determines whether kanban content can be dragged from one lane * to another, and what method is executed after dragging. * * 映射关系的基本格式为 map[$mode][$fromStatus][$toStatus] = $methodName。 * The basic format of the mapping relationship is map[$mode][$fromStatus][$toStatus] = $methodName. * * @param string $mode 看板内容类型,可选值 task|bug The content mode of kanban, should be task or bug. * @param string $fromStatus 拖动内容的来源泳道 The origin lane the content draged from. * @param string $toStatus 拖动内容的目标泳道 The destination lane the content draged to. * @param string $methodName 拖动到目标泳道后执行的方法名 The method to execute after draged the content. * * 例如 map['task']['doing']['done'] = 'close' 表示:任务(task)看板从进行中(doing)泳道拖动到已完成(done)泳道时,执行关闭(close)方法。 * For example, map['task']['doing']['done'] = 'close' means: when the task kanban is dragged from the doing lane to the done lane, * execute the close method. * * @param object $kanbanSetting This param is used in the biz version, don't remove it. * @access public * @return string */ public function getKanbanStatusMap($kanbanSetting) { $statusMap = array(); if(common::hasPriv('task', 'start')) $statusMap['task']['wait']['doing'] = 'start'; if(common::hasPriv('task', 'pause')) $statusMap['task']['doing']['pause'] = 'pause'; if(common::hasPriv('task', 'finish')) { $statusMap['task']['wait']['done'] = 'finish'; $statusMap['task']['doing']['done'] = 'finish'; $statusMap['task']['pause']['done'] = 'finish'; } if(common::hasPriv('task', 'cancel')) { $statusMap['task']['wait']['cancel'] = 'cancel'; $statusMap['task']['pause']['cancel'] = 'cancel'; } if(common::hasPriv('task', 'activate')) { $statusMap['task']['pause']['doing'] = 'activate'; $statusMap['task']['done']['doing'] = 'activate'; $statusMap['task']['cancel']['doing'] = 'activate'; $statusMap['task']['closed']['doing'] = 'activate'; } if(common::hasPriv('task', 'close')) { $statusMap['task']['done']['closed'] = 'close'; $statusMap['task']['cancel']['closed'] = 'close'; } if(common::hasPriv('bug', 'resolve')) { $statusMap['bug']['wait']['done'] = 'resolve'; $statusMap['bug']['wait']['cancel'] = 'resolve'; } if(common::hasPriv('bug', 'close')) { $statusMap['bug']['done']['closed'] = 'close'; $statusMap['bug']['cancel']['closed'] = 'close'; } if(common::hasPriv('bug', 'activate')) { $statusMap['bug']['done']['wait'] = 'activate'; $statusMap['bug']['cancel']['wait'] = 'activate'; $statusMap['bug']['closed']['wait'] = 'activate'; } return $statusMap; } /** * Get status list of kanban. * * @param object $kanbanSetting This param is used in the biz version, don't remove it. * @access public * @return string */ public function getKanbanStatusList($kanbanSetting) { return $this->lang->task->statusList; } /** * Get color list of kanban. * * @param object $kanbanSetting * @access public * @return array */ public function getKanbanColorList($kanbanSetting) { return $kanbanSetting->colorList; } /** * Build burn data. * * @param int $executionID * @param array $dateList * @param string $type * @param string $burnBy * @param string $executionEnd * @access public * @return array */ public function buildBurnData($executionID, $dateList, $type, $burnBy = 'left', $executionEnd = '') { $this->loadModel('report'); $burnBy = $burnBy ? $burnBy : 'left'; $sets = $this->getBurnDataFlot($executionID, $burnBy, false, $dateList); $limitJSON = '[]'; $baselineJSON = '[]'; $firstBurn = empty($sets) ? 0 : reset($sets); $firstTime = !empty($firstBurn->$burnBy) ? $firstBurn->$burnBy : (!empty($firstBurn->value) ? $firstBurn->value : 0); $firstTime = $firstTime == 'null' ? 0 : $firstTime; /* If the $executionEnd is passed, the guide should end of execution. */ $days = $executionEnd ? array_search($executionEnd, $dateList) : count($dateList) - 1; $rate = $days ? $firstTime / $days : ''; $baselineJSON = '['; foreach($dateList as $i => $date) { $value = ($i > $days ? 0 : round(($days - $i) * (float)$rate, 3)) . ','; $baselineJSON .= $value; } $baselineJSON = rtrim($baselineJSON, ',') . ']'; $chartData['labels'] = $this->report->convertFormat($dateList, DT_DATE5); $chartData['burnLine'] = $this->report->createSingleJSON($sets, $dateList); $chartData['baseLine'] = $baselineJSON; $execution = $this->getById($executionID); if((strpos('closed,suspended', $execution->status) === false and helper::today() > $execution->end) or ($execution->status == 'closed' and substr($execution->closedDate, 0, 10) > $execution->end) or ($execution->status == 'suspended' and $execution->suspendedDate > $execution->end)) { $delaySets = $this->getBurnDataFlot($executionID, $burnBy, true, $dateList); $chartData['delayLine'] = $this->report->createSingleJSON($delaySets, $dateList); } return $chartData; } /** * Fill tasks in tree. * @param object $tree * @param int $executionID * @access public * @return object */ public function fillTasksInTree($node, $executionID) { $node = (object)$node; static $storyGroups, $taskGroups; if(empty($storyGroups)) { if($this->config->vision == 'lite') { $execution = $this->getById($executionID); $stories = $this->dao->select('t2.*, t1.version as taskVersion')->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->where('t1.project')->eq((int)$execution->project) ->andWhere('t2.deleted')->eq(0) ->orderBy('t1.`order`_desc') ->fetchAll(); } else { $stories = $this->dao->select('t2.*, t1.version as taskVersion')->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->where('t1.project')->eq((int)$executionID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.type')->eq('story') ->orderBy('t1.`order`_desc') ->fetchAll(); } $storyGroups = array(); foreach($stories as $story) $storyGroups[$story->product][$story->module][$story->id] = $story; } if(empty($taskGroups)) { $tasks = $this->dao->select('*')->from(TABLE_TASK) ->where('execution')->eq((int)$executionID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->lt(1) ->orderBy('id_desc') ->fetchAll(); $childTasks = $this->dao->select('*')->from(TABLE_TASK) ->where('execution')->eq((int)$executionID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->ne(0) ->orderBy('id_desc') ->fetchGroup('parent'); $taskGroups = array(); foreach($tasks as $task) { $taskGroups[$task->module][$task->story][$task->id] = $task; if(!empty($childTasks[$task->id])) { $taskGroups[$task->module][$task->story][$task->id]->children = $childTasks[$task->id]; } } } if(!empty($node->children)) { foreach($node->children as $i => $child) { $subNode = $this->fillTasksInTree($child, $executionID); /* Remove no children node. */ if($subNode->type != 'story' and $subNode->type != 'task' and empty($subNode->children)) { unset($node->children[$i]); } else { $node->children[$i] = $subNode; } } } if(!isset($node->id))$node->id = 0; if($node->type == 'story') { static $users; if(empty($users)) $users = $this->loadModel('user')->getPairs('noletter'); $node->type = 'module'; $stories = isset($storyGroups[$node->root][$node->id]) ? $storyGroups[$node->root][$node->id] : array(); foreach($stories as $story) { $storyItem = new stdclass(); $storyItem->type = 'story'; $storyItem->id = 'story' . $story->id; $storyItem->title = $story->title; $storyItem->color = $story->color; $storyItem->pri = $story->pri; $storyItem->storyId = $story->id; $storyItem->openedBy = zget($users, $story->openedBy); $storyItem->assignedTo = zget($users, $story->assignedTo); $storyItem->url = helper::createLink('execution', 'storyView', "storyID=$story->id&execution=$executionID"); $storyItem->taskCreateUrl = helper::createLink('task', 'batchCreate', "executionID={$executionID}&story={$story->id}"); $storyTasks = isset($taskGroups[$node->id][$story->id]) ? $taskGroups[$node->id][$story->id] : array(); if(!empty($storyTasks)) { $taskItems = $this->formatTasksForTree($storyTasks, $story); $storyItem->tasksCount = count($taskItems); $storyItem->children = $taskItems; } $node->children[] = $storyItem; } /* Append for task of no story and node is not root. */ if($node->id and isset($taskGroups[$node->id][0])) { $taskItems = $this->formatTasksForTree($taskGroups[$node->id][0]); $node->tasksCount = count($taskItems); foreach($taskItems as $taskItem) $node->children[] = $taskItem; } } elseif($node->type == 'task') { $node->type = 'module'; $node->tasksCount = 0; if(isset($taskGroups[$node->id])) { foreach($taskGroups[$node->id] as $tasks) { $taskItems = $this->formatTasksForTree($tasks); $node->tasksCount += count($taskItems); foreach($taskItems as $taskItem) { $node->children[$taskItem->id] = $taskItem; if(!empty($tasks[$taskItem->id]->children)) { $task = $this->formatTasksForTree($tasks[$taskItem->id]->children); $node->children[$taskItem->id]->children=$task; $node->tasksCount += count($task); } } } $node->children = array_values($node->children); } } elseif($node->type == 'product') { $node->title = $node->name; if(isset($node->children[0]) and empty($node->children[0]->children)) array_shift($node->children); } $node->actions = false; if(!empty($node->children)) $node->children = array_values($node->children); return $node; } /** * Format tasks for tree. * * @param array $tasks * @param object $story * @access public * @return array */ public function formatTasksForTree($tasks, $story = '') { static $users; if(empty($users)) $users = $this->loadModel('user')->getPairs('noletter'); $taskItems = array(); foreach($tasks as $task) { $taskItem = new stdclass(); $taskItem->type = 'task'; $taskItem->id = $task->id; $taskItem->title = $task->name; $taskItem->color = $task->color; $taskItem->pri = (int)$task->pri; $taskItem->status = $task->status; $taskItem->parent = $task->parent; $taskItem->estimate = $task->estimate; $taskItem->consumed = $task->consumed; $taskItem->left = $task->left; $taskItem->openedBy = zget($users, $task->openedBy); $taskItem->assignedTo = zget($users, $task->assignedTo); $taskItem->url = helper::createLink('task', 'view', "task=$task->id"); $taskItem->storyChanged = $story and $story->status == 'active' and $story->version > $story->taskVersion; $buttons = ''; $buttons .= common::buildIconButton('task', 'assignTo', "executionID=$task->execution&taskID=$task->id", $task, 'list', '', '', 'iframe', true); $buttons .= common::buildIconButton('task', 'start', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); $buttons .= common::buildIconButton('task', 'recordEstimate', "taskID=$task->id", $task, 'list', 'time', '', 'iframe', true); if(isset($task->children)) $taskItem->children = $this->formatTasksForTree($task->children); if($taskItem->storyChanged) { $this->lang->task->confirmStoryChange = $this->lang->confirm; $buttons .= common::buildIconButton('task', 'confirmStoryChange', "taskid=$task->id", '', 'list', '', 'hiddenwin'); } $buttons .= common::buildIconButton('task', 'finish', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); $buttons .= common::buildIconButton('task', 'close', "taskID=$task->id", $task, 'list', '', '', 'iframe', true); $buttons .= common::buildIconButton('task', 'edit', "taskID=$task->id", '', 'list'); $taskItem->buttons = $buttons; $taskItem->actions = false; $taskItems[] = $taskItem; } return $taskItems; } /** * Get plans by $productID. * * @param int|array $productID * @param string $param withMainPlan|skipParent * @param int $executionID * @return mixed */ public function getPlans($products, $param = '', $executionID = 0) { $this->loadModel('productplan'); $date = date('Y-m-d'); $param = strtolower($param); $branchIdList = strpos($param, 'withmainplan') !== false ? array(BRANCH_MAIN => BRANCH_MAIN) : array(); $branchGroups = $this->getBranchByProduct(array_keys($products), $executionID, 'noclosed'); foreach($branchGroups as $branches) { foreach($branches as $branchID => $branchName) $branchIdList[] = $branchID; } $branchQuery = '('; if(!empty($branchIdList)) { $branchCount = count($branchIdList); foreach($branchIdList as $index => $branchID) { $branchQuery .= "FIND_IN_SET('$branchID', branch)"; if($index < $branchCount - 1) $branchQuery .= ' OR '; } } else { $branchQuery .= "FIND_IN_SET('0', branch)"; } $branchQuery .= ')'; $plans = $this->dao->select('t1.id,t1.title,t1.product,t1.parent,t1.begin,t1.end,t1.branch,t2.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t2.id=t1.product') ->where('t1.product')->in(array_keys($products)) ->andWhere('t1.deleted')->eq(0) ->andWhere($branchQuery) ->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->productplan->reorder4Children($plans); $plans = $this->productplan->relationBranch($plans); $productPlans = array(); foreach($plans as $plan) { if($plan->parent == '-1' and strpos($param, 'skipparent') !== false) continue; if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; $productPlans[$plan->product][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == '2030-01-01' and $plan->end == '2030-01-01') $productPlans[$plan->product][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; if($plan->productType != 'normal') $productPlans[$plan->product][$plan->id] = $productPlans[$plan->product][$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main); } return $productPlans; } /** * Print html for tree. * * @param object $trees * @param bool $hasProduct true|false * @access pubic * @return string */ public function printTree($trees, $hasProduct = true) { $html = ''; foreach($trees as $tree) { if(is_array($tree)) $tree = (object)$tree; switch($tree->type) { case 'module': $this->app->loadLang('tree'); $html .= "
  • "; $html .= "" . $tree->name . ''; break; case 'task': $link = helper::createLink('execution', 'treeTask', "taskID={$tree->id}"); $html .= '
  • '; $html .= '' . ($tree->parent > 0 ? $this->lang->task->children : $this->lang->task->common) . "" . $tree->title . ' ' . (empty($tree->assignedTo) ? $tree->openedBy : $tree->assignedTo) . '' . $tree->id . ''; break; case 'product': $this->app->loadLang('product'); $productName = $hasProduct ? $this->lang->productCommon : $this->lang->projectCommon; $html .= '
  • '; $html .= '' . $productName . "" . $tree->name . ''; break; case 'story': $this->app->loadLang('story'); $link = helper::createLink('execution', 'treeStory', "storyID={$tree->storyId}"); $html .= '
  • '; $html .= '' . $this->lang->story->common . "" . $tree->title . ' ' . (empty($tree->assignedTo) ? $tree->openedBy : $tree->assignedTo) . '' . $tree->storyId . ''; break; case 'branch': $this->app->loadLang('branch'); $html .= "
  • "; $html .= "{$this->lang->branch->common}{$tree->name}"; break; } if(isset($tree->children)) { $html .= ''; } $html .= '
  • '; } return $html; } /** * Print execution nested list. * * @param object $execution * @param bool $isChild * @param array $users * @param int $productID * @access public * @return void */ public function printNestedList($execution, $isChild, $users, $productID) { $this->loadModel('task'); $this->loadModel('execution'); $this->loadModel('programplan'); $today = helper::today(); if(!$isChild) { $trClass = 'is-top-level table-nest-child-hide'; $trAttrs = "data-id='$execution->id' data-order='$execution->order' data-nested='true' data-status={$execution->status}"; } else { if(strpos($execution->path, ",$execution->project,") !== false) { $path = explode(',', trim($execution->path, ',')); $path = array_slice($path, array_search($execution->project, $path) + 1); $path = implode(',', $path); } $trClass = 'table-nest-hide'; $trAttrs = "data-id={$execution->id} data-parent={$execution->parent} data-status={$execution->status}"; $trAttrs .= " data-nest-parent='$execution->parent' data-order='$execution->order' data-nest-path='$path'"; } $burns = join(',', $execution->burns); echo ""; echo ""; if(common::hasPriv('execution', 'batchEdit')) echo "id class='table-nest-icon icon table-nest-toggle'>"; $spanClass = $execution->type == 'stage' ? 'label-warning' : 'label-info'; echo "{$this->lang->execution->typeList[$execution->type]} "; if(empty($execution->children)) { echo html::a(helper::createLink('execution', 'view', "executionID=$execution->id"), $execution->name, '', "class='text-ellipsis' title='{$execution->name}'"); if(!helper::isZeroDate($execution->end)) { if($execution->status != 'closed') { echo strtotime($today) > strtotime($execution->end) ? '' . $this->lang->execution->delayed . '' : ''; } } } else { echo "" . $execution->name . ''; if(!helper::isZeroDate($execution->end)) { if($execution->status != 'closed') { echo strtotime($today) > strtotime($execution->end) ? '' . $this->lang->execution->delayed . '' : ''; } } } if(!empty($execution->division) and $execution->hasProduct) echo "{$execution->productName}"; echo "" . zget($this->lang->project->statusList, $execution->status) . ''; echo '' . zget($users, $execution->PM) . ''; echo helper::isZeroDate($execution->begin) ? '' : '' . $execution->begin . ''; echo helper::isZeroDate($execution->end) ? '' : '' . $execution->end . ''; echo "" . $execution->hours->totalEstimate . $this->lang->execution->workHourUnit . ''; echo "" . $execution->hours->totalConsumed . $this->lang->execution->workHourUnit . ''; echo "" . $execution->hours->totalLeft . $this->lang->execution->workHourUnit . ''; echo '' . html::ring($execution->hours->progress) . ''; echo ""; echo ''; common::printIcon('execution', 'start', "executionID={$execution->id}", $execution, 'list', '', '', 'iframe', true); $class = !empty($execution->children) ? 'disabled' : ''; common::printIcon('task', 'create', "executionID={$execution->id}", '', 'list', '', '', $class, false, "data-app='execution'"); $project = $this->loadModel('project')->getByID($execution->project); if($execution->type == 'stage' or ($this->app->tab == 'project' and !empty($project->model) and $project->model == 'waterfallplus')) { $isCreateTask = $this->loadModel('programplan')->isCreateTask($execution->id); $disabled = ($isCreateTask and $execution->type == 'stage') ? '' : ' disabled'; $title = !$isCreateTask ? $this->lang->programplan->error->createdTask : $this->lang->programplan->createSubPlan; $title = (!empty($disabled) and $execution->type != 'stage') ? $this->lang->programplan->error->notStage : $title; common::printIcon('programplan', 'create', "program={$execution->project}&productID=$productID&planID=$execution->id", $execution, 'list', 'split', '', $disabled, '', '', $title); } if($execution->type == 'stage') { common::printIcon('programplan', 'edit', "stageID=$execution->id&projectID=$execution->project", $execution, 'list', '', '', 'iframe', true); } else { common::printIcon('execution', 'edit', "executionID=$execution->id", $execution, 'list', '', '', 'iframe', true); } $disabled = !empty($execution->children) ? ' disabled' : ''; if($execution->status != 'closed' and common::hasPriv('execution', 'close', $execution)) { common::printIcon('execution', 'close', "stageID=$execution->id", $execution, 'list', 'off', 'hiddenwin' , $disabled . ' iframe', true, '', $this->lang->execution->close); } elseif($execution->status == 'closed' and common::hasPriv('execution', 'activate', $execution)) { common::printIcon('execution', 'activate', "stageID=$execution->id", $execution, 'list', 'magic', 'hiddenwin' , $disabled . ' iframe', true, '', $this->lang->execution->activate); } if(common::hasPriv('execution', 'delete', $execution)) { common::printIcon('execution', 'delete', "stageID=$execution->id&confirm=no", $execution, 'list', 'trash', 'hiddenwin' , $disabled, '', '', $this->lang->delete); } echo ''; echo ''; if(!empty($execution->children)) { foreach($execution->children as $child) { $child->division = $execution->division; $this->printNestedList($child, true, $users, $productID); } } if(!empty($execution->tasks)) { foreach($execution->tasks as $task) { $showmore = (count($execution->tasks) == 50) && ($task == end($execution->tasks)); echo $this->task->buildNestedList($execution, $task, false, $showmore, $users); } } } /** * Update user view of execution and it's product. * * @param int|array $executionID * @param string $objectType * @param array $users * @access public * @return void */ public function updateUserView($executionID, $objectType = 'sprint', $users = array()) { $this->loadModel('user')->updateUserView($executionID, $objectType, $users); $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false); if(!empty($products)) $this->user->updateUserView(array_keys($products), 'product', $users); } /** * Get the products associated with the stage. * * @param string $stageIdList * @access public * @return array */ public function getStageLinkProductPairs($stageIdList = array()) { $productpairs = $this->dao->select('t1.project, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') ->where('t1.project')->in($stageIdList) ->fetchPairs('project', 'name'); return $productpairs; } /** * Set stage tree path. * * @param int $executionID * @access public * @return bool */ public function setTreePath($executionID) { $execution = $this->dao->select('id, type, parent, path, grade')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch(); $parent = $this->dao->select('id, type, parent, path, grade')->from(TABLE_PROJECT)->where('id')->eq($execution->parent)->fetch(); if($parent->type == 'project') { $path['path'] = ",{$parent->id},{$execution->id},"; $path['grade'] = 1; } elseif($parent->type == 'stage') { $path['path'] = $parent->path . "{$execution->id},"; $path['grade'] = $parent->grade + 1; } $this->dao->update(TABLE_PROJECT)->set('path')->eq($path['path'])->set('grade')->eq($path['grade'])->where('id')->eq($execution->id)->exec(); } /** * Build execution action menu. * * @param object $execution * @param string $type * @access public * @return string */ public function buildOperateMenu($execution, $type = 'view') { if($execution->deleted) return ''; $menu = ''; $params = "executionID=$execution->id"; $menu .= "
    "; $menu .= $this->buildMenu('execution', 'start', $params, $execution, $type, '', '', 'iframe', true); $menu .= $this->buildMenu('execution', 'activate', $params, $execution, $type, '', '', 'iframe', true); $menu .= $this->buildMenu('execution', 'putoff', $params, $execution, $type, '', '', 'iframe', true); $menu .= $this->buildMenu('execution', 'suspend', $params, $execution, $type, '', '', 'iframe', true); $menu .= $this->buildMenu('execution', 'close', $params, $execution, $type, '', '', 'iframe', true); $menu .= "
    "; $menu .= $this->buildFlowMenu('execution', $execution, 'view', 'direct'); $menu .= "
    "; $menu .= $this->buildMenu('execution', 'edit', "execution=$execution->id", $execution); $menu .= $this->buildMenu('execution', 'delete', "execution=$execution->id", $execution, 'button', 'trash', 'hiddenwin'); return $menu; } /** * Print cell data. * * @param object $col * @param object $execution * @param array $users * @param string $mode * @param bool $isStage * @param int $productID * @param bool $child * @access public * @return void */ public function printCell($col, $execution, $users, $mode = 'datatable', $isStage = false, $productID = 0, $child = false) { $canBatchEdit = common::hasPriv('execution', 'batchEdit'); $id = $col->id; $onlyChildStage = ($execution->grade == 2 and $execution->project != $execution->parent); if(!$isStage and in_array($col->id, array('percent', 'attribute', 'actions'))) return; if($this->app->tab != 'execution' and $col->id == 'project') return; if($col->show) { $class = "c-{$id}"; if($id == 'id') $class .= ' cell-id'; if($id == 'name') $class .= ' text-left flex'; if($id == 'code') $class .= ' text-left'; if($id == 'project') $class .= ' text-left c-name'; if($id == 'status') $class .= ' text-center'; if($id == 'actions') $class .= ' text-center'; if($id == 'name' and !$child) $class .= ' sort-handler'; if($id == 'name' and !empty($execution->children)) $class .= ' parent'; if(in_array($id, array('estimate', 'consumed', 'left'))) $class .= ' hours'; $title = ''; if($id == 'name') { $title = " title='{$execution->name}'"; if(!empty($execution->children)) $class .= ' has-child'; if(isset($execution->delay)) $class .= ' delay'; } if($id == 'teamCount') { $title = " title='{$execution->teamCount}'"; $class .= ' text-right'; } if($id == 'project') $title = " title='{$execution->projectName}'"; if($id == 'code') $title = " title='{$execution->code}'"; if($id == 'status') { $executionStatus = $this->processStatus('execution', $execution); $title = " title='{$executionStatus}'"; } if(in_array($id, array('estimate', 'consumed', 'left'))) { $totalTitle = 'total' . ucfirst($id); $title .= $execution->hours->{$totalTitle} . $this->lang->execution->workHour; } echo ""; if($this->config->edition != 'open') $this->loadModel('flow')->printFlowCell('execution', $execution, $id); if($id == 'burn') { $burnValue = join(',', $execution->burns); echo ""; } switch($id) { case 'id': if($canBatchEdit) { echo "
    "; } echo sprintf('%03d', $execution->id); break; case 'name': $label = $execution->type == 'stage' ? 'label-warning' : 'label-info'; $executionLink = $execution->projectModel == 'kanban' ? html::a(helper::createLink('execution', 'kanban', 'executionID=' . $execution->id), $execution->name, '', "class='text-ellipsis'") : html::a(helper::createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'"); if(!$onlyChildStage) echo "{$this->lang->execution->typeList[$execution->type]}"; if($onlyChildStage) echo "{$this->lang->programplan->childrenAB} "; echo !empty($execution->children) ? "$execution->name" : $executionLink; if(isset($execution->delay)) echo "{$this->lang->execution->delayed} "; if(!empty($execution->children)) { echo ""; } break; case 'code': echo $execution->code; break; case 'project': echo "{$execution->projectName}"; break; case 'PM': echo zget($users, $execution->PM); break; case 'status': echo "$executionStatus"; break; case 'progress': echo html::ring($execution->hours->progress); break; case 'percent': echo $execution->percent . '%'; break; case 'attribute': echo zget($this->lang->stage->typeList, $execution->attribute, ''); break; case 'begin': echo helper::isZeroDate($execution->begin) ? '' : $execution->begin; break; case 'teamCount': echo $execution->teamCount; break; case 'end': echo helper::isZeroDate($execution->end) ? '' : $execution->end; break; case 'realBegan': echo helper::isZeroDate($execution->realBegan) ? '' : $execution->realBegan; break; case 'realEnd': echo helper::isZeroDate($execution->realEnd) ? '' : $execution->realEnd; break; case 'estimate': echo $execution->hours->totalEstimate . $this->lang->execution->workHourUnit; break; case 'consumed': echo $execution->hours->totalConsumed . $this->lang->execution->workHourUnit; break; case 'left': echo $execution->hours->totalLeft . $this->lang->execution->workHourUnit; break; case 'actions': common::printIcon('execution', 'start', "executionID={$execution->id}", $execution, 'list', '', '', 'iframe', true); $class = !empty($execution->children) ? 'disabled' : ''; common::printIcon('task', 'create', "executionID={$execution->id}", $execution, 'list', '', '', $class, false, "data-app='execution'"); if($execution->grade == 1 && $this->loadModel('programplan')->isCreateTask($execution->id)) { common::printIcon('programplan', 'create', "program={$execution->parent}&productID=$productID&planID=$execution->id", $execution, 'list', 'split', '', '', '', '', $this->lang->programplan->createSubPlan); } else { $disabled = ($execution->grade == 2) ? ' disabled' : ''; echo common::hasPriv('programplan', 'create') ? html::a('javascript:alert("' . $this->lang->programplan->error->createdTask . '");', '', '', 'class="btn ' . $disabled . '"') : ''; } common::printIcon('programplan', 'edit', "stageID=$execution->id&projectID=$execution->project", $execution, 'list', '', '', 'iframe', true); $disabled = !empty($execution->children) ? ' disabled' : ''; if($execution->status != 'closed' and common::hasPriv('execution', 'close', $execution)) { common::printIcon('execution', 'close', "stageID=$execution->id", $execution, 'list', 'off', 'hiddenwin' , $disabled . ' iframe', true, '', $this->lang->programplan->close); } elseif($execution->status == 'closed' and common::hasPriv('execution', 'activate', $execution)) { common::printIcon('execution', 'activate', "stageID=$execution->id", $execution, 'list', 'magic', 'hiddenwin' , $disabled . ' iframe', true, '', $this->lang->programplan->activate); } if(common::hasPriv('execution', 'delete', $execution)) { common::printIcon('execution', 'delete', "stageID=$execution->id&confirm=no", $execution, 'list', 'trash', 'hiddenwin' , $disabled, '', '', $this->lang->programplan->delete); } break; } echo ''; } } /** * Generate col for dtable. * * @param string $orderBy * @access public * @return void */ public function generateCol($orderBy = '') { $this->loadModel('datatable'); $setting = $this->datatable->getSetting('execution'); $fieldList = $this->config->execution->datatable->fieldList; foreach($fieldList as $field => $items) { $fieldKey = in_array($field, array('name', 'code', 'type', 'PM', 'status')) ? 'exec' . ucfirst($field) : $field; $title = $field == 'id' ? 'ID' : zget($this->lang->execution, $fieldKey, zget($this->lang, $field, $field)); $fieldList[$field]['title'] = $title; } if(empty($setting)) { $setting = $this->config->execution->datatable->defaultField; $order = 1; foreach($setting as $key => $value) { $id = $value; $set = new stdclass();; $set->order = $order++; $set->show = true; $set->name = $value; $set->title = $fieldList[$id]['title']; $sortType = ''; if(strpos($orderBy, $id) !== false) { $sort = str_replace("{$id}_", '', $orderBy); $sortType = $sort == 'asc' ? 'up' : 'down'; } if(isset($fieldList[$id]['checkbox'])) $set->checkbox = $fieldList[$id]['checkbox']; if(isset($fieldList[$id]['nestedToggle'])) $set->nestedToggle = $fieldList[$id]['nestedToggle']; if(isset($fieldList[$id]['fixed'])) $set->fixed = $fieldList[$id]['fixed']; if(isset($fieldList[$id]['width'])) $set->width = $fieldList[$id]['width']; if(isset($fieldList[$id]['type'])) $set->type = $fieldList[$id]['type']; if(isset($fieldList[$id]['sortType'])) $set->sortType = $fieldList[$id]['sortType']; if(isset($fieldList[$id]['flex'])) $set->flex = $fieldList[$id]['flex']; if(isset($fieldList[$id]['minWidth'])) $set->minWidth = $fieldList[$id]['minWidth']; if(isset($fieldList[$id]['maxWidth'])) $set->maxWidth = $fieldList[$id]['maxWidth']; if(isset($fieldList[$id]['pri'])) $set->pri = $fieldList[$id]['pri']; if($sortType) $set->sortType = $sortType; $setting[$key] = $set; } } else { foreach($setting as $key => $set) { if(empty($set->show)) { unset($setting[$key]); continue; } $sortType = ''; if(strpos($orderBy, $set->id) !== false) { $sort = str_replace("{$set->id}_", '', $orderBy); $sortType = $sort == 'asc' ? 'up' : 'down'; } $set->name = $set->id; $set->title = $fieldList[$set->id]['title']; if(isset($fieldList[$set->id]['checkbox'])) $set->checkbox = $fieldList[$set->id]['checkbox']; if(isset($fieldList[$set->id]['nestedToggle'])) $set->nestedToggle = $fieldList[$set->id]['nestedToggle']; if(isset($fieldList[$set->id]['fixed'])) $set->fixed = $fieldList[$set->id]['fixed']; if(isset($fieldList[$set->id]['type'])) $set->type = $fieldList[$set->id]['type']; if(isset($fieldList[$set->id]['sortType'])) $set->sortType = $fieldList[$set->id]['sortType']; if(isset($fieldList[$set->id]['flex'])) $set->flex = $fieldList[$set->id]['flex']; if(isset($fieldList[$set->id]['minWidth'])) $set->minWidth = $fieldList[$set->id]['minWidth']; if(isset($fieldList[$set->id]['maxWidth'])) $set->maxWidth = $fieldList[$set->id]['maxWidth']; if(isset($fieldList[$set->id]['pri'])) $set->pri = $fieldList[$set->id]['pri']; if($sortType) $set->sortType = $sortType; $set->width = str_replace('px', '', $set->width); unset($set->id); } } usort($setting, array('datatableModel', 'sortCols')); return $setting; } /** * Generate row for dtable. * * @param array $executions * @param array $users * @param int $productID * @access public * @return void */ public function generateRow($executions, $users, $productID) { $today = helper::today(); $rows = array(); foreach($executions as $id => $execution) { $label = $execution->type == 'stage' ? 'label-warning' : 'label-info'; $link = $execution->type == 'kanban' ? helper::createLink('execution', 'kanban', "id=$execution->id") : helper::createLink('execution', 'task', "id=$execution->id"); $execution->name = "{$this->lang->execution->typeList[$execution->type]} " . (empty($execution->children) ? html::a($link, $execution->name, '_self', 'class="text-primary"') : $execution->name) . (strtotime($today) > strtotime($execution->end) ? '' . $this->lang->execution->delayed . '' : '');; $execution->project = $execution->projectName; $execution->parent = ($execution->parent and $execution->grade > 1) ? $execution->parent : ''; $execution->asParent = !empty($execution->children); $execution->status = zget($this->lang->execution->statusList, $execution->status); $execution->PM = zget($users, $execution->PM); $execution->progress = $execution->hours->progress; $execution->estimate = $execution->hours->totalEstimate; $execution->consumed = $execution->hours->totalConsumed; $execution->left = $execution->hours->totalLeft; $children = isset($execution->children) ? $execution->children : array(); unset($execution->children); $rows[] = $execution; if(!empty($children)) { $rows = array_merge($rows, $this->generateRow($children, $users, $productID)); } } return $rows; } /* * Build search form * * @param int $queryID * @param string $actionURL * @return void * */ public function buildSearchForm($queryID, $actionURL) { $this->config->execution->all->search['queryID'] = $queryID; $this->config->execution->all->search['actionURL'] = $actionURL; $projectPairs = array(0 => ''); $projectPairs += $this->loadModel('project')->getPairsByProgram('', 'all', false, 'order_asc', '', '', 'multiple'); $this->config->execution->all->search['params']['project']['values'] = $projectPairs + array('all' => $this->lang->execution->allProject); $this->loadModel('search')->setSearchParams($this->config->execution->all->search); } /* * Create default sprint. * * @param int $projectID * @return int * */ public function createDefaultSprint($projectID) { $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); $post = $_POST; $_POST = array(); $extendFields = $this->getExtendFields(); foreach(array_keys($extendFields) as $field) { if(isset($post[$field])) $_POST[$field] = $post[$field]; } $_POST['project'] = $projectID; $_POST['name'] = $project->name; $_POST['begin'] = $project->begin; $_POST['end'] = $project->end; $_POST['status'] = 'wait'; $_POST['days'] = $project->days; $_POST['team'] = $project->team; $_POST['desc'] = $project->desc; $_POST['teamMembers'] = array($this->app->user->account); $_POST['acl'] = 'open'; $_POST['PO'] = $this->app->user->account; $_POST['QD'] = $this->app->user->account; $_POST['PM'] = $this->app->user->account; $_POST['RD'] = $this->app->user->account; $_POST['multiple'] = '0'; $_POST['hasProduct'] = $project->hasProduct; if($project->code) $_POST['code'] = $project->code; $projectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchAll(); foreach($projectProducts as $projectProduct) { $_POST['products'][] = $projectProduct->product; $_POST['branch'][] = $projectProduct->branch; if($projectProduct->plan) $_POST['plans'][$projectProduct->product][$projectProduct->branch] = explode(',', trim($projectProduct->plan, ',')); } $executionID = $this->create(); if($project->model == 'kanban') { $execution = $this->getById($executionID); $this->loadModel('kanban')->createRDKanban($execution); } $_POST = $post; return $executionID; } /** * Sync no multiple project to sprint. * * @param int $projectID * @access public * @return int */ public function syncNoMultipleSprint($projectID) { $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); if(empty($project)) return 0; $post = $_POST; $_POST = array(); $extendFields = $this->getExtendFields(); foreach(array_keys($extendFields) as $field) { if(isset($post[$field])) $_POST[$field] = $post[$field]; } $_POST['project'] = $projectID; $_POST['name'] = $project->name; $_POST['begin'] = $project->begin; $_POST['end'] = $project->end; $_POST['realBegan'] = $project->realBegan; $_POST['realEnd'] = $project->realEnd; $_POST['days'] = $project->days; $_POST['team'] = $project->team; $_POST['PO'] = $project->PO; $_POST['QD'] = $project->QD; $_POST['PM'] = $project->PM; $_POST['RD'] = $project->RD; $_POST['status'] = $project->status; $_POST['acl'] = 'open'; if(isset($this->config->setCode) and $this->config->setCode == 1) $_POST['code'] = $project->code; $projectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchAll(); foreach($projectProducts as $projectProduct) { $_POST['products'][] = $projectProduct->product; $_POST['branch'][] = $projectProduct->branch; if($projectProduct->plan) $_POST['plans'][$projectProduct->product][$projectProduct->branch] = explode(',', trim($projectProduct->plan, ',')); } $teamMembers = $this->dao->select('*')->from(TABLE_TEAM)->where('type')->eq('project')->andWhere('root')->eq($projectID)->fetchPairs('account', 'account'); $_POST['teamMembers'] = array_values($teamMembers); $executionID = $this->dao->select('*')->from(TABLE_EXECUTION)->where('project')->eq($projectID)->andWhere('type')->in('sprint,kanban')->andWhere('multiple')->eq(0)->fetch('id'); if($executionID) { $this->update($executionID); $this->updateProducts($executionID); } $_POST = $post; return $executionID; } /** * Expand executions, return id list. * * @param object $stats * @access public * @return array */ public function expandExecutionIdList($stats) { $executionIdList = array(); foreach($stats as $execution) { $executionIdList[$execution->id] = $execution->id; if(!empty($execution->children)) { foreach($execution->children as $child) { $childrenIdList = $this->expandExecutionChildrenIdList($child); foreach($childrenIdList as $childID) { $executionIdList[$childID] = $childID; } } } } return $executionIdList; } /** * Expand children of execution, return id list. * * @param object $execution * @access public * @return array */ public function expandExecutionChildrenIdList($execution) { $executionIdList = array(); $executionIdList[$execution->id] = $execution->id; if(!empty($execution->children)) { foreach($execution->children as $child) { $childrenIdList = $this->expandExecutionChildrenIdList($child); foreach($childrenIdList as $childID) { $executionIdList[$childID] = $childID; } } } return $executionIdList; } /** * Build execution object by status. * * @param string $status * @access public * @return object */ public function buildExecutionByStatus($status) { $execution = new stdclass(); $execution->status = $status; $execution->lastEditedBy = $this->app->user->account; $execution->lastEditedDate = helper::now(); if($status == 'wait') { $execution->realBegan = ''; $execution->realEnd = ''; $execution->closedBy = ''; $execution->closedDate = ''; $execution->canceledBy = ''; $execution->canceledDate = ''; $execution->suspendedDate = ''; } if($status == 'doing') { $execution->realBegan = helper::today(); $execution->realEnd = ''; $execution->closedBy = ''; $execution->closedDate = ''; $execution->canceledBy = ''; $execution->canceledDate = ''; $execution->suspendedDate = ''; } if($status == 'suspended') { $execution->suspendedDate = helper::now(); $execution->closedBy = ''; $execution->closedDate = ''; } if($status == 'closed') { $execution->closedBy = $this->app->user->account; $execution->closedDate = helper::now(); } return $execution; } /** * Reset execution orders. * * @param array $executions * @param array $parentExecutions * @access public * @return array */ public function resetExecutionSorts($executions, $parentExecutions = array()) { if(empty($executions)) return array(); if(empty($parentExecutions)) { $execution = current($executions); $parentExecutions = $this->dao->select('*')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) ->andWhere('type')->in('kanban,sprint,stage') ->andWhere('grade')->eq(1) ->andWhere('project')->eq($execution->project) ->orderBy('order_asc') ->fetchAll('id'); } $sortedExecutions = array(); foreach($parentExecutions as $executionID => $execution) { if(!isset($sortedExecutions[$executionID]) and isset($executions[$executionID])) $sortedExecutions[$executionID] = $executions[$executionID]; $children = $this->getChildExecutions($executionID, 'order_asc'); if(!empty($children)) $sortedExecutions += $this->resetExecutionSorts($executions, $children); } return $sortedExecutions; } /** * Get Extend Fields in workflow. * * @param string $module * @access public * @return array */ public function getExtendFields($module = 'project') { $extendFields = $this->dao->select('field') ->from(TABLE_WORKFLOWFIELD) ->where('module')->eq('project') ->andWhere('buildin')->eq(0) ->andWhere('role')->eq('custom') ->fetchPairs('field'); return $extendFields; } }