* @package project * @version $Id: model.php 5118 2013-07-12 07:41:41Z chencongzhi520@gmail.com $ * @link http://www.zentao.net */ ?> app->user->admin) return true; return (strpos(",{$this->app->user->view->sprints},", ",{$projectID},") !== false); } /** * Show accessDenied response. * * @access private * @return void */ public function accessDenied() { echo(js::alert($this->lang->project->accessDenied)); if(!$this->server->http_referer) die(js::locate(helper::createLink('project', 'index'))); $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) die(js::locate(helper::createLink('project', 'index'))); die(js::locate('back')); } /** * Set menu. * * @param array $projects * @param int $projectID * @param int $buildID * @param string $extra * @access public * @return void */ public function setMenu($projects, $projectID, $buildID = 0, $extra = '') { $program = $this->getByID($this->session->PRJ); if(empty($projects)) { if($program->model == 'waterfall') { if(($this->app->moduleName == 'programplan' && $this->app->methodName != 'create') || $this->app->moduleName == 'project') die(js::locate(helper::createLink('programplan', 'create', "progarmID=$program->id"))); } else { if($this->app->moduleName == 'project' && $this->app->methodName != 'create') die(js::locate(helper::createLink('project', 'create'))); } } if(!$projectID and $this->session->project) $projectID = $this->session->project; /* Get project and program */ $project = $this->getById($projectID); $isProgram = false; if(!empty($project)) { $isProgram = $project->model; $program = $isProgram ? $project : $this->getByID($project->parent); } if($isProgram) { $projectID = $this->session->project; if(!$projectID or !in_array($projectID, array_keys($projects))) $projectID = key($projects); } $this->session->set('project', $projectID); $products = $this->loadModel('product')->getPairs(); /* Unset story, bug, build and testtask if type is ops. */ if($project and $project->type == 'ops') { unset($this->lang->project->menu->story); unset($this->lang->project->menu->qa); unset($this->lang->project->subMenu->qa->bug); unset($this->lang->project->subMenu->qa->build); unset($this->lang->project->subMenu->qa->testtask); } /* Hide story and qa menu when project is story or design type. */ if($project and ($project->attribute == 'story' || $project->attribute == 'design')) { unset($this->lang->project->menu->story); unset($this->lang->project->menu->qa); } $moduleName = $this->app->getModuleName(); $methodName = $this->app->getMethodName(); if($this->cookie->projectMode == 'noclosed' and ($project->status == 'done' or $project->status == 'closed')) { setcookie('projectMode', 'all'); $this->cookie->projectMode = 'all'; } /* Set project module page nav. */ $label = $this->lang->project->index; $projectIndex = '
'; $projectIndex .= '
'; $projectIndex .= $this->select($projects, $projectID, null, $moduleName, $methodName, $extra); $this->lang->modulePageNav = $projectIndex; foreach($this->lang->project->menu as $key => $menu) { common::setMenuVars($this->lang->project->menu, $key, $projectID); if(isset($this->lang->project->subMenu->$key)) { $projectSubMenu = $this->lang->project->subMenu->$key; $subMenu = common::createSubMenu($this->lang->project->subMenu->$key, $projectID); if(!empty($subMenu)) { foreach($subMenu as $menuKey => $menu) { $itemMenu = zget($projectSubMenu, $menuKey, ''); $isActive['method'] = ($moduleName == strtolower($menu->link['module']) and $methodName == strtolower($menu->link['method'])); $isActive['alias'] = ($moduleName == strtolower($menu->link['module']) and (is_array($itemMenu) and isset($itemMenu['alias']) and strpos($itemMenu['alias'], $methodName) !== false)); $isActive['subModule'] = (is_array($itemMenu) and isset($itemMenu['subModule']) and strpos($itemMenu['subModule'], $moduleName) !== false); if($isActive['method'] or $isActive['alias'] or $isActive['subModule']) { $this->lang->project->menu->{$key}['link'] = $menu->text . "|" . join('|', $menu->link); break; } } $this->lang->project->menu->{$key}['subMenu'] = $subMenu; } } } return true; } /** * Create the select code of projects. * * @param array $projects * @param int $projectID * @param int $buildID * @param string $currentModule * @param string $currentMethod * @param string $extra * @access public * @return string */ public function select($projects, $projectID, $buildID, $currentModule, $currentMethod, $extra = '') { if(!$projects) return false; if(!$projectID) return; $isMobile = $this->app->viewType == 'mhtml'; setCookie("lastProject", $projectID, $this->config->cookieLife, $this->config->webRoot, '', false, true); $currentProject = $this->getById($projectID); if(isset($currentProject->type) and $currentProject->type == 'program') return; if(isset($currentProject->program)) $program = $this->getByID($currentProject->program); if(isset($program->product) and $program->product == 'multiple') { $productID = $this->loadModel('product')->getProductIDByProject($currentProject->id); $productName = $this->dao->findByID($productID)->from(TABLE_PRODUCT)->fetch('name'); if($currentProject->parent) { $parentInfo = $this->getById($currentProject->parent); $currentProject->name = $parentInfo->name . '/' . $currentProject->name; } $currentProject->name = $productName . '/' . $currentProject->name; } $dropMenuLink = helper::createLink('project', 'ajaxGetDropMenu', "objectID=$projectID&module=$currentModule&method=$currentMethod&extra=$extra"); $currentProjectName = ''; if(isset($currentProject->name)) $currentProjectName = $currentProject->name; $output = "
"; if($isMobile) $output = "{$currentProject->name} "; return $output; } /** * Get project tree menu. * * @access public * @return void */ public function tree() { $products = $this->loadModel('product')->getPairs('nocode', $this->session->PRJ); $productGroup = $this->getProductGroupList(); $projectTree = ""; return $projectTree; } /** * Save the project id user last visited to session. * * @param int $projectID * @param array $projects * @access public * @return int */ public function saveState($projectID, $projects) { if($projectID > 0) $this->session->set('project', (int)$projectID); if($projectID == 0 and $this->cookie->lastProject) $this->session->set('project', (int)$this->cookie->lastProject); if($projectID == 0 and $this->session->project == '') $this->session->set('project', key($projects)); if(!isset($projects[$this->session->project])) { $this->session->set('project', key($projects)); if($projectID && strpos(",{$this->app->user->view->sprints},", ",{$this->session->project},") === false) $this->accessDenied(); } return $this->session->project; } /** * Create a project. * * @param string $copyProjectID * * @access public * @return void */ public function create($copyProjectID = '') { $this->lang->project->team = $this->lang->project->teamname; /* Determine whether to add a sprint or a stage according to the model of the project. */ $project = $this->getByID($this->session->PRJ); $sprintType = zget($this->config->project->modelList, $project->model, ''); $sprint = fixer::input('post') ->setDefault('project', $this->session->PRJ) ->setDefault('status', 'wait') ->setDefault('parent', $this->session->PRJ) ->setDefault('openedBy', $this->app->user->account) ->setDefault('openedDate', helper::now()) ->setDefault('openedVersion', $this->config->version) ->setDefault('team', substr($this->post->name,0, 30)) ->add('type', $sprintType) ->setIF($this->post->acl != 'custom', 'whitelist', '') ->join('whitelist', ',') ->stripTags($this->config->project->editor->create['id'], $this->config->allowedTags) ->remove('products, workDays, delta, branch, uid, plans') ->get(); $sprint = $this->loadModel('file')->processImgURL($sprint, $this->config->project->editor->create['id'], $this->post->uid); $this->dao->insert(TABLE_PROJECT)->data($sprint) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->project->create->requiredFields, 'notempty') ->checkIF($sprint->begin != '', 'begin', 'date') ->checkIF($sprint->end != '', 'end', 'date') ->checkIF($sprint->end != '', 'end', 'gt', $sprint->begin) ->check('code', 'unique', "deleted='0'") ->exec(); /* Add the creater to the team. */ if(!dao::isError()) { $projectID = $this->dao->lastInsertId(); $today = helper::today(); $creatorExists = false; /* Save order. */ $this->dao->update(TABLE_PROJECT)->set('`order`')->eq($projectID * 5)->where('id')->eq($projectID)->exec(); $this->file->updateObjectID($this->post->uid, $projectID, 'project'); /* Update the path. */ $this->loadModel('programplan')->setTreePath($projectID); /* Copy team of project. */ if($copyProjectID != '') { $members = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->eq($copyProjectID)->andWhere('type')->eq('project')->fetchAll(); foreach($members as $member) { unset($member->id); $member->root = $projectID; $member->join = $today; $member->days = $sprint->days; $member->type = $sprintType; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); if($member->account == $this->app->user->account) $creatorExists = true; } } /* Add the creator to team. */ if($copyProjectID == '' or !$creatorExists) { $this->app->loadLang('user'); $member = new stdclass(); $member->root = $projectID; $member->account = $this->app->user->account; $member->role = $this->lang->user->roleList[$this->app->user->role]; $member->join = $today; $member->type = $sprintType; $member->days = $sprint->days; $member->hours = $this->config->project->defaultWorkhours; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); $this->addProjectMembers($this->session->PRJ, array($member)); } /* Create doc lib. */ $this->app->loadLang('doc'); $lib = new stdclass(); $lib->project = $projectID; $lib->name = $this->lang->doclib->main['project']; $lib->type = 'project'; $lib->main = '1'; $lib->acl = 'default'; $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); if($sprint->acl != 'open') $this->updateUserView($projectID); if(!dao::isError()) $this->loadModel('score')->create('program', 'createguide', $projectID); return $projectID; } } /** * Update a project. * * @param int $projectID * @access public * @return array */ public function update($projectID) { $projectID = (int)$projectID; $oldProject = $this->dao->findById($projectID)->from(TABLE_PROJECT)->fetch(); $team = $this->getTeamMemberPairs($projectID); $this->lang->project->team = $this->lang->project->teamname; $project = fixer::input('post') ->setIF($this->post->begin == '0000-00-00', 'begin', '') ->setIF($this->post->end == '0000-00-00', 'end', '') ->setIF($this->post->acl != 'custom', 'whitelist', '') ->setIF($this->post->acl == 'custom' and !isset($_POST['whitelist']), 'whitelist', '') ->setDefault('team', $this->post->name) ->join('whitelist', ',') ->stripTags($this->config->project->editor->edit['id'], $this->config->allowedTags) ->remove('products, branch, uid, plans') ->get(); $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->edit['id'], $this->post->uid); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->project->edit->requiredFields, 'notempty') ->checkIF($project->begin != '', 'begin', 'date') ->checkIF($project->end != '', 'end', 'date') ->checkIF($project->end != '', 'end', 'gt', $project->begin) ->check('code', 'unique', "id!=$projectID and deleted='0'") ->where('id')->eq($projectID) ->limit(1) ->exec(); $changedAccounts = array(); foreach($project as $fieldName => $value) { if($fieldName == 'PO' or $fieldName == 'PM' or $fieldName == 'QD' or $fieldName == 'RD' ) { if(!empty($value) and !isset($team[$value])) { $member = new stdclass(); $member->root = (int)$projectID; $member->account = $value; $member->join = helper::today(); $member->role = $this->lang->project->$fieldName; $member->days = zget($project, 'days', 0); $member->type = 'project'; $member->hours = $this->config->project->defaultWorkhours; $this->dao->replace(TABLE_TEAM)->data($member)->exec(); $changedAccounts[] = $value; } } } /* Fix bug#3074, Update views for team members. */ if($project->acl != 'open') $this->updateUserView($projectID, 'sprint', $changedAccounts); if(!dao::isError()) { $this->file->updateObjectID($this->post->uid, $projectID, 'project'); return common::createChanges($oldProject, $project); } } /** * Batch update. * * @access public * @return void */ public function batchUpdate() { $this->loadModel('user'); $projects = array(); $allChanges = array(); $data = fixer::input('post')->get(); $oldProjects = $this->getByIdList($this->post->projectIDList); foreach($data->projectIDList as $projectID) { $projectID = (int)$projectID; $projects[$projectID] = new stdClass(); $projects[$projectID]->name = $data->names[$projectID]; $projects[$projectID]->code = $data->codes[$projectID]; $projects[$projectID]->PM = $data->PMs[$projectID]; $projects[$projectID]->PO = $data->POs[$projectID]; $projects[$projectID]->QD = $data->QDs[$projectID]; $projects[$projectID]->RD = $data->RDs[$projectID]; $projects[$projectID]->lifetime = $data->lifetimes[$projectID]; $projects[$projectID]->status = $data->statuses[$projectID]; $projects[$projectID]->begin = $data->begins[$projectID]; $projects[$projectID]->end = $data->ends[$projectID]; $projects[$projectID]->team = $data->teams[$projectID]; $projects[$projectID]->desc = htmlspecialchars_decode($data->descs[$projectID]); $projects[$projectID]->days = $data->dayses[$projectID]; $projects[$projectID]->order = $data->orders[$projectID]; } foreach($projects as $projectID => $project) { $oldProject = $oldProjects[$projectID]; $team = $this->getTeamMemberPairs($projectID); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->project->edit->requiredFields, 'notempty') ->checkIF($project->begin != '', 'begin', 'date') ->checkIF($project->end != '', 'end', 'date') ->checkIF($project->end != '', 'end', 'gt', $project->begin) ->check('code', 'unique', "id!=$projectID and deleted='0'") ->where('id')->eq($projectID) ->limit(1) ->exec(); $changedAccounts = array(); foreach($project as $fieldName => $value) { if($fieldName == 'PO' or $fieldName == 'PM' or $fieldName == 'QD' or $fieldName == 'RD' ) { if(!empty($value) and !isset($team[$value])) { $member = new stdClass(); $member->root = (int)$projectID; $member->type = 'project'; $member->account = $value; $member->join = helper::today(); $member->role = $this->lang->project->$fieldName; $member->days = 0; $member->hours = $this->config->project->defaultWorkhours; $this->dao->replace(TABLE_TEAM)->data($member)->exec(); $changedAccounts[] = $value; } } } if(!empty($changedAccounts)) $this->updateUserView($projectID, 'sprint', $changedAccounts); if(dao::isError()) die(js::error('project#' . $projectID . dao::getError(true))); $allChanges[$projectID] = common::createChanges($oldProject, $project); } $this->fixOrder(); return $allChanges; } /** * Start project. * * @param int $projectID * @access public * @return void */ public function start($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->add('realBegan', $now) ->setDefault('status', 'doing') ->remove('comment')->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Put project off. * * @param int $projectID * @access public * @return void */ public function putoff($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post')->remove('comment')->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Suspend project. * * @param int $projectID * @access public * @return void */ public function suspend($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->setDefault('status', 'suspended') ->remove('comment')->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Activate project. * * @param int $projectID * @access public * @return void */ public function activate($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->setDefault('status', 'doing') ->remove('comment,readjustTime,readjustTask') ->get(); if(!$this->post->readjustTime) { unset($project->begin); unset($project->end); } $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); /* Readjust task. */ if($this->post->readjustTime and $this->post->readjustTask) { $beginTimeStamp = strtotime($project->begin); $tasks = $this->dao->select('id,estStarted,deadline,status')->from(TABLE_TASK) ->where('deadline')->ne('0000-00-00') ->andWhere('status')->in('wait,doing') ->andWhere('project')->eq($projectID) ->fetchAll(); foreach($tasks as $task) { if($task->status == 'wait' and $task->estStarted != '0000-00-00') { $taskDays = helper::diffDate($task->deadline, $task->estStarted); $taskOffset = helper::diffDate($task->estStarted, $oldProject->begin); $estStartedTimeStamp = $beginTimeStamp + $taskOffset * 24 * 3600; $estStarted = date('Y-m-d', $estStartedTimeStamp); $deadline = date('Y-m-d', $estStartedTimeStamp + $taskDays * 24 * 3600); if($estStarted > $project->end) $estStarted = $project->end; if($deadline > $project->end) $deadline = $project->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, $oldProject->begin); $deadline = date('Y-m-d', $beginTimeStamp + $taskOffset * 24 * 3600); if($deadline > $project->end) $deadline = $project->end; $this->dao->update(TABLE_TASK)->set('deadline')->eq($deadline)->where('id')->eq($task->id)->exec(); } } } if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Close project. * * @param int $projectID * @access public * @return array */ public function close($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->setDefault('status', 'closed') ->setDefault('closedBy', $this->app->user->account) ->setDefault('closedDate', $now) ->remove('comment') ->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); if(!dao::isError()) { $this->loadModel('score')->create('project', 'close', $oldProject); return common::createChanges($oldProject, $project); } } /** * Get project pairs. * * @param string $mode all|noclosed or empty * @param string $programID * @access public * @return array */ public function getPairs($mode = '', $programID = 0) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProjectPairs(); $orderBy = !empty($this->config->project->orderBy) ? $this->config->project->orderBy : 'isDone, status'; $mode .= $this->cookie->projectMode; /* Order by status's content whether or not done */ $projects = $this->dao->select('*, IF(INSTR(" done,closed", status) < 2, 0, 1) AS isDone')->from(TABLE_PROJECT) ->where('deleted')->eq(0) ->beginIF($programID)->andWhere('project')->eq($programID)->fi() ->beginIF(strpos($mode, 'withdelete') === false)->andWhere('deleted')->eq(0)->fi() ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->orderBy($orderBy) ->fetchAll(); $pairs = array(); foreach($projects as $project) { if(strpos($mode, 'noclosed') !== false and ($project->status == 'done' or $project->status == 'closed')) continue; $pairs[$project->id] = $project->name; } if(strpos($mode, 'empty') !== false) $pairs[0] = ''; /* If the pairs is empty, to make sure there's an project in the pairs. */ if(empty($pairs) and isset($projects[0])) { $firstProject = $projects[0]; $pairs[$firstProject->id] = $firstProject->name; } return $pairs; } /** * Get by idList. * * @param array $projectIDList * @access public * @return array */ public function getByIdList($projectIDList) { return $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->in($projectIDList)->fetchAll('id'); } /** * Get project lists. * * @param string $status all|undone|wait|running * @param int $limit * @param int $productID * @access public * @return array */ public function getList($status = 'all', $limit = 0, $productID = 0, $branch = 0, $programID = 0) { $programID = $programID ? $programID : $this->session->PRJ; if($status == 'involved') return $this->getInvolvedList($status, $limit, $productID, $branch); if($productID != 0) { return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t1.product')->eq($productID) ->andWhere('t2.type')->in('sprint,stage') ->andWhere('t2.deleted')->eq(0) ->beginIF($programID)->andWhere('t2.project')->eq($programID)->fi() ->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() ->orderBy('order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } else { return $this->dao->select('*, IF(INSTR(" done,closed", status) < 2, 0, 1) AS isDone')->from(TABLE_PROJECT) ->where('deleted')->eq(0) ->andWhere('type')->in('sprint,stage') ->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($programID)->andWhere('project')->eq($programID)->fi() ->orderBy('order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } } /** * Get project lists. * * @param string $status involved * @param int $limit * @param int $productID * @param int $branch * @access public * @return array */ public function getInvolvedList($status = 'involved', $limit = 0, $productID = 0, $branch = 0) { if($productID != 0) { return $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->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('t3.type')->eq('project') ->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_PROJECT)->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('t2.type')->eq('project') ->orderBy('t1.order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } } /** * Get projects by program. * * @param object $program * @access public * @return array */ public function getProjectsByProgram($program) { $projects = $this->dao->select('t1.*, t3.name as productName')->from(TABLE_PROJECT)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.id = t2.project') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t2.product = t3.id') ->where('t1.parent')->eq((int)$program->id) ->andWhere('t1.deleted')->eq('0') ->orderBy('t1.order desc') ->fetchAll('id'); if($program->model == 'waterfall') { foreach($projects as $projectID => $project) { if($project->parent and isset($projects[$project->id]) and isset($projects[$project->parent])) $projects[$project->id]->name = $projects[$project->parent]->name . '/' . $project->name; } if($program->product == 'multiple') { foreach($projects as $projectID => $project) $projects[$projectID]->name = $project->productName . '/' . $project->name; } foreach($projects as $projectID => $project) unset($projects[$project->parent]); } return $projects; } /** * Get projects 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_PROJECT)->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 => $project) { if(!$project->product) { if($this->checkPriv($project->id)) $noProducts[] = $project; unset($list[$id][$ID]); } } } unset($list['']); $list[''] = $noProducts; return $list; } /** * Get project stats. * * @param string $status * @param int $productID * @param int $itemCounts * @param string $orderBy * @param object $pager * @access public * @return void */ public function getProjectStats($status = 'undone', $productID = 0, $branch = 0, $itemCounts = 30, $orderBy = 'order_desc', $pager = null, $programID = 0) { /* Init vars. */ $projects = $this->getList($status, 0, $productID, $branch, $programID); $projects = $this->dao->select('*')->from(TABLE_PROJECT) ->where('id')->in(array_keys($projects)) ->andWhere('grade')->eq(1) ->andWhere('deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); if(empty($projects)) return array(); /* In case of a waterfall model, obtain sub-stage data. */ $PRJData = $this->getById($this->session->PRJ); if($PRJData->model == 'waterfall') { $childrens = $this->dao->select('*')->from(TABLE_PROJECT) ->where('parent')->in(array_keys($projects)) ->andWhere('grade')->eq(2) ->andWhere('deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); $projects = $projects + $childrens; } $projectKeys = array_keys($projects); $hours = array(); $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason') ->from(TABLE_TASK) ->where('project')->in($projectKeys) ->andWhere('parent')->lt(1) ->andWhere('deleted')->eq(0) ->fetchGroup('project', 'id'); /* Compute totalEstimate, totalConsumed, totalLeft. */ foreach($tasks as $projectID => $projectTasks) { $hour = (object)$emptyHour; foreach($projectTasks as $task) { if($task->status != 'cancel') { $hour->totalEstimate += $task->estimate; $hour->totalConsumed += $task->consumed; } if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += $task->left; } $hours[$projectID] = $hour; } /* Compute totalReal and progress. */ foreach($hours as $hour) { $hour->totalEstimate = round($hour->totalEstimate, 1) ; $hour->totalConsumed = round($hour->totalConsumed, 1); $hour->totalLeft = round($hour->totalLeft, 1); $hour->totalReal = $hour->totalConsumed + $hour->totalLeft; $hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 3) * 100 : 0; } /* Get burndown charts datas. */ $burns = $this->dao->select('project, date AS name, `left` AS value') ->from(TABLE_BURN) ->where('project')->in($projectKeys) ->andWhere('task')->eq(0) ->orderBy('date desc') ->fetchGroup('project', 'name'); foreach($burns as $projectID => $projectBurns) { /* If projectBurns > $itemCounts, split it, else call processBurnData() to pad burns. */ $begin = $projects[$projectID]->begin; $end = $projects[$projectID]->end; if($begin == '0000-00-00') $begin = $projects[$projectID]->openedDate; $projectBurns = $this->processBurnData($projectBurns, $itemCounts, $begin, $end); /* Shorter names. */ foreach($projectBurns as $projectBurn) { $projectBurn->name = substr($projectBurn->name, 5); unset($projectBurn->project); } ksort($projectBurns); $burns[$projectID] = $projectBurns; } /* Process projects. */ $parents = array(); $children = array(); foreach($projects as $key => $project) { /* Process the end time. */ $project->end = date(DT_DATE1, strtotime($project->end)); /* Judge whether the project is delayed. */ if($project->status != 'done' and $project->status != 'closed' and $project->status != 'suspended') { $delay = helper::diffDate(helper::today(), $project->end); if($delay > 0) $project->delay = $delay; } /* Process the burns. */ $project->burns = array(); $burnData = isset($burns[$project->id]) ? $burns[$project->id] : array(); foreach($burnData as $data) $project->burns[] = $data->value; /* Process the hours. */ $project->hours = isset($hours[$project->id]) ? $hours[$project->id] : (object)$emptyHour; $project->children = array(); $project->grade == 1 ? $parents[$project->id] = $project : $children[$project->parent][] = $project; } /* In the case of the waterfall model, calculate the sub-stage. */ if($PRJData->model == 'waterfall') { foreach($parents as $id => $project) $project->children = isset($children[$id]) ? $children[$id] : array(); } return $parents; } /** * Get tasks. * * @param int $productID * @param int $projectID * @param array $projects * @param string $browseType * @param int $queryID * @param int $moduleID * @param string $sort * @param object $pager * @access public * @return array */ public function getTasks($productID, $projectID, $projects, $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; } $this->session->set('taskWithChildren', in_array($browseType, array('unclosed', 'byproject', 'all', 'bysearch')) ? false : true); /* Get tasks. */ $tasks = array(); if($browseType != "bysearch") { $queryStatus = $browseType == 'byproject' ? 'all' : $browseType; if($queryStatus == 'unclosed') { $queryStatus = $this->lang->task->statusList; unset($queryStatus['closed']); $queryStatus = array_keys($queryStatus); } $tasks = $this->task->getProjectTasks($projectID, $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 project when no project. */ if(strpos($taskQuery, "`project` =") === false) $taskQuery = $taskQuery . " AND `project` = $projectID"; $projectQuery = "`project` " . helper::dbIN(array_keys($projects)); $taskQuery = str_replace("`project` = 'all'", $projectQuery, $taskQuery); // Search all project. $this->session->set('taskQueryCondition', $taskQuery); $this->session->set('taskOnlyCondition', true); $this->session->set('taskOrderBy', $sort); $tasks = $this->getSearchTasks($taskQuery, $pager, $sort); } return $tasks; } /** * Get project by id. * * @param int $projectID * @param bool $setImgSize * @access public * @return object */ public function getById($projectID, $setImgSize = false) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getProject(); $project = $this->dao->findById((int)$projectID)->from(TABLE_PROJECT)->fetch(); if(!$project) return false; /* Judge whether the project is delayed. */ if($project->status != 'done' and $project->status != 'closed' and $project->status != 'suspended') { $delay = helper::diffDate(helper::today(), $project->end); if($delay > 0) $project->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('project')->eq((int)$projectID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->lt(1) ->fetch(); $closedTotalLeft = $this->dao->select('ROUND(SUM(`left`), 2) AS totalLeft')->from(TABLE_TASK) ->where('project')->eq((int)$projectID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->lt(1) ->andWhere('status')->in('closed,cancel') ->fetch('totalLeft'); $project->days = $project->days ? $project->days : ''; $project->totalHours = $this->dao->select('sum(days * hours) AS totalHours')->from(TABLE_TEAM)->where('root')->eq($project->id)->andWhere('type')->eq('project')->fetch('totalHours'); $project->totalEstimate = round($total->totalEstimate, 1); $project->totalConsumed = round($total->totalConsumed, 1); $project->totalLeft = round($total->totalLeft - $closedTotalLeft, 1); $project = $this->loadModel('file')->replaceImgURL($project, 'desc'); if($setImgSize) $project->desc = $this->file->setImgSize($project->desc); return $project; } /** * Get the default managers for a project from it's related products. * * @param int $projectID * @access public * @return object */ public function getDefaultManagers($projectID) { $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($projectID) ->fetch(); if($managers) return $managers; $managers = new stdclass(); $managers->PO = ''; $managers->QD = ''; $managers->RD = ''; return $managers; } /** * Get products of a project. * * @param int $projectID * @access public * @return array */ public function getProducts($projectID, $withBranch = true) { if(defined('TUTORIAL')) { if(!$withBranch) return $this->loadModel('tutorial')->getProductPairs(); return $this->loadModel('tutorial')->getProjectProducts(); } $query = $this->dao->select('t2.id, t2.name, t2.type, t1.branch, t1.plan')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2') ->on('t1.product = t2.id') ->where('t1.project')->eq((int)$projectID) ->andWhere('t2.deleted')->eq(0); if(!$withBranch) return $query->fetchPairs('id', 'name'); return $query->fetchAll('id'); } /** * Get ordered projects. * * @param string $status * @param int $num * @param int $programID * @access public * @return array */ public function getOrderedProjects($status, $num = 0, $programID = 0) { $projectList = $this->getList($status, 0, 0, 0, $programID); if(empty($projectList)) return $projectList; $projects = $mineProjects = $otherProjects = $closedProjects = array(); foreach($projectList as $project) { if(!$this->app->user->admin and !$this->checkPriv($project->id)) continue; if($project->status != 'done' and $project->status != 'closed' and $project->PM == $this->app->user->account) { $mineProjects[$project->id] = $project; } elseif($project->status != 'done' and $project->status != 'closed' and !($project->PM == $this->app->user->account)) { $otherProjects[$project->id] = $project; } elseif($project->status == 'done' or $project->status == 'closed') { $closedProjects[$project->id] = $project; } } $projects = $mineProjects + $otherProjects + $closedProjects; if(empty($num)) return $projects; return array_slice($projects, 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 * @access public * @return void */ public function buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, $type = 'projectStory') { $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($product->branch) { if(!isset($branchGroups[$product->id][$product->branch])) continue; $branchPairs[$product->branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$product->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; } } } /* Build search form. */ if($type == 'projectStory') $this->config->product->search['module'] = 'projectStory'; $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->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts($products); $this->config->product->search['params']['module']['values'] = $modules; unset($this->lang->story->statusList['draft']); 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); $this->loadModel('search')->setSearchParams($this->config->product->search); } /** * Get projects to import * * @param array $projectIds * @access public * @return array */ public function getProjectsToImport($projectIds) { $projects = $this->dao->select('*')->from(TABLE_PROJECT) ->where('id')->in($projectIds) ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->andWhere('deleted')->eq(0) ->orderBy('id desc') ->fetchAll('id'); $pairs = array(); $now = date('Y-m-d'); foreach($projects as $id => $project) $pairs[$id] = ucfirst(substr($project->code, 0, 1)) . ':' . $project->name; return $pairs; } /** * Update products of a project. * * @param int $projectID * @access public * @return void */ public function updateProducts($projectID) { $this->loadModel('user'); $oldProjectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->fetchGroup('product', 'branch'); $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->exec(); $members = array_keys($this->getTeamMembers($projectID)); if(!isset($_POST['products'])) { $this->user->updateUserView(array_keys($oldProjectProducts), 'product', $members); return true; } $products = $_POST['products']; $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])) continue; $oldPlan = 0; $branch = isset($branches[$i]) ? $branches[$i] : 0; if(isset($oldProjectProducts[$productID][$branch])) { $oldProjectProduct = $oldProjectProducts[$productID][$branch]; $oldPlan = $oldProjectProduct->plan; } $data = new stdclass(); $data->project = $projectID; $data->product = $productID; $data->branch = $branch; $data->plan = isset($plans[$productID]) ? $plans[$productID] : $oldPlan; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); $existedProducts[$productID] = true; } $oldProductKeys = array_keys($oldProjectProducts); $needUpdate = array_merge(array_diff($oldProductKeys, $products), array_diff($products, $oldProductKeys)); if($needUpdate) $this->user->updateUserView($needUpdate, 'product', $members); } /** * Get related projects * * @param int $projectID * @access public * @return array */ public function getRelatedProjects($projectID) { $products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->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_PROJECT)->alias('t1') ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') ->on('t1.id = t2.project') ->where('t2.product')->in($products) ->andWhere('t1.id')->ne((int)$projectID) ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.id') ->fetchPairs(); } /** * Get tasks can be imported. * * @param int $toProject * @param array $branches * @access public * @return array */ public function getTasks2Imported($toProject, $branches) { $this->loadModel('task'); $products = $this->getProducts($toProject); $projects = $this->dao->select('product, project')->from(TABLE_PROJECTPRODUCT)->where('product')->in(array_keys($products))->fetchGroup('project'); $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.project')->in(array_keys($projects)) ->andWhere("(t1.story = 0 OR (t2.branch in ('0','" . join("','", $branches) . "') and t2.product " . helper::dbIN(array_keys($branches)) . "))") ->fetchGroup('project', 'id'); return $tasks; } /** * Import tasks. * * @param int $projectID * @access public * @return void */ public function importTask($projectID) { $this->loadModel('task'); $tasks = $this->dao->select('id, project, assignedTo, story, consumed,status')->from(TABLE_TASK)->where('id')->in($this->post->tasks)->fetchAll('id'); foreach($tasks as $task) { /* Save the assignedToes and stories, should linked to project. */ $assignedToes[$task->assignedTo] = $task->project; $stories[$task->story] = $task->story; $data = new stdclass(); $data->project = $projectID; $data->status = $task->consumed > 0 ? 'doing' : 'wait'; if($task->status == 'cancel') { $data->canceledBy = ''; $data->canceledDate = null; } /* 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->project); } /* Remove empty story. */ unset($stories[0]); /* Add members to project team. */ $teamMembers = $this->getTeamMemberPairs($projectID); foreach($assignedToes as $account => $preProjectID) { if(!isset($teamMembers[$account])) { $role = $this->dao->select('*')->from(TABLE_TEAM) ->where('root')->eq($preProjectID) ->andWhere('type')->eq('project') ->andWhere('account')->eq($account) ->fetch(); $role->root = $projectID; $role->join = helper::today(); $this->dao->replace(TABLE_TEAM)->data($role)->exec(); } } /* Link stories. */ $projectStories = $this->loadModel('story')->getProjectStoryPairs($projectID); $lastOrder = (int)$this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($projectID)->orderBy('order_desc')->limit(1)->fetch('order'); foreach($stories as $storyID) { if(!isset($projectStories[$storyID])) { $story = $this->dao->findById($storyID)->fields("$projectID as project, id as story, product, version")->from(TABLE_STORY)->fetch(); $story->order = ++$lastOrder; $this->dao->insert(TABLE_PROJECTSTORY)->data($story)->exec(); $this->action->create('story', $storyID, 'linked2project', '', $projectID); } } } /** * Stat story, task, bug data for project. * * @param int $projectID * @access public * @return void */ public function statRelatedData($projectID) { $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($projectID) ->andWhere('t1.deleted')->eq(0) ->fetch('storyCount'); $taskCount = $this->dao->select('count(id) as taskCount')->from(TABLE_TASK)->where('project')->eq($projectID)->andWhere('deleted')->eq(0)->fetch('taskCount'); $bugCount = $this->dao->select('count(id) as bugCount')->from(TABLE_BUG)->where('project')->eq($projectID)->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 $projectID * @access public * @return void */ public function importBug($projectID) { $this->loadModel('bug'); $this->loadModel('task'); $this->loadModel('story'); $now = helper::now(); $showAllModule = isset($this->config->project->task->allModule) ? $this->config->project->task->allModule : ''; $modules = $this->loadModel('tree')->getTaskOptionMenu($projectID, 0, 0, $showAllModule ? 'allModule' : ''); $project = $this->getByID($projectID); $requiredFields = ',' . $this->config->task->create->requiredFields . ','; if($project->type == 'ops') $requiredFields = str_replace(',story,', ',', $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 = $projectID; $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->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, 'deadline') !== false and $task->deadline == '0000-00-00') $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)) { dao::$errors['message'][] = $this->lang->task->error->estimateNumber; 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()); die(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()) die(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; } /** * Get child projects. * * @param int $projectID * @access public * @return void */ public function getChildProjects($projectID) { return $this->dao->select('id, name')->from(TABLE_PROJECT)->where('parent')->eq((int)$projectID)->fetchPairs(); } /** * Update childs. * * @param int $projectID * @access public * @return void */ public function updateChilds($projectID) { $sql = "UPDATE " . TABLE_PROJECT . " SET parent = 0 WHERE parent = '$projectID'"; $this->dbh->exec($sql); if(!isset($_POST['childs'])) return; $childs = array_unique($_POST['childs']); foreach($childs as $childProjectID) { $sql = "UPDATE " . TABLE_PROJECT . " SET parent = '$projectID' WHERE id = '$childProjectID'"; $this->dbh->query($sql); } } /** * Link story. * * @param int $projectID * @param array $stories * @param array $products * * @access public * @return mixed */ public function linkStory($projectID, $stories = array(), $products = array()) { if(empty($stories)) $stories = $this->post->stories; if(empty($stories)) return false; if(empty($products)) $products = $this->post->products; $this->loadModel('action'); $versions = $this->loadModel('story')->getVersions($stories); $linkedStories = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($projectID)->orderBy('order_desc')->fetchPairs('story', 'order'); $lastOrder = reset($linkedStories); $statusPairs = $this->dao->select('id, status')->from(TABLE_STORY)->where('id')->in(array_values($stories))->fetchPairs(); foreach($stories as $key => $storyID) { if($statusPairs[$storyID] == 'draft' || $statusPairs[$storyID] == 'closed') continue; if(isset($linkedStories[$storyID])) continue; $productID = (int)$products[$storyID]; $data = new stdclass(); $data->project = $projectID; $data->product = $productID; $data->story = $storyID; $data->version = $versions[$storyID]; $data->order = ++$lastOrder; $this->dao->insert(TABLE_PROJECTSTORY)->data($data)->exec(); $this->story->setStage($storyID); $this->action->create('story', $storyID, 'linked2project', '', $projectID); } } /** * Link all stories by project. * * @param $projectID */ public function linkStories($projectID) { $plans = $this->dao->select('plan,product')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($projectID) ->fetchPairs('plan', 'product'); $planStories = array(); $planProducts = array(); $count = 0; if(!empty($plans)) { foreach($plans as $planID => $productID) { if(empty($planID)) continue; $planStory = $this->loadModel('story')->getPlanStories($planID); if(!empty($planStory)) { foreach($planStory as $id => $story) { if($story->status == 'draft') { $count++; unset($planStory[$id]); continue; } $planProducts[$story->id] = $story->product; } $planStories = array_merge($planStories, array_keys($planStory)); } } } $this->linkStory($projectID, $planStories, $planProducts); if($count != 0) echo js::alert(sprintf($this->lang->project->haveDraft, $count)) . js::locate(helper::createLink('project', 'create', "productID=&projectID=$projectID")); } /** * Unlink story. * * @param int $projectID * @param int $storyID * @access public * @return void */ public function unlinkStory($projectID, $storyID) { $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('project')->eq($projectID)->andWhere('story')->eq($storyID)->limit(1)->exec(); $order = 1; $storys = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($projectID)->orderBy('order')->fetchAll(); foreach($storys as $projectstory) { if($projectstory->order != $order) $this->dao->update(TABLE_PROJECTSTORY)->set('`order`')->eq($order)->where('project')->eq($projectID)->andWhere('story')->eq($projectstory->story)->exec(); $order++; } $this->loadModel('story')->setStage($storyID); $this->loadModel('action')->create('story', $storyID, 'unlinkedfromproject', '', $projectID); $tasks = $this->dao->select('id')->from(TABLE_TASK)->where('story')->eq($storyID)->andWhere('project')->eq($projectID)->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); } } /** * Get team members. * * @param int $projectID * @access public * @return array */ public function getTeamMembers($projectID) { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getTeamMembers(); $project = $this->getByID($projectID); if(empty($project)) return array(); return $this->dao->select("t1.*, t1.hours * t1.days AS totalHours, 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)$projectID) ->andWhere('t1.type')->eq($project->type) ->andWhere('t2.deleted')->eq('0') ->fetchAll('account'); } /** * Get team members in pair. * * @param int $projectID * @param string $params * @param string $usersToAppended * @access public * @return array */ public function getTeamMemberPairs($projectID, $params = '', $usersToAppended = '') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getTeamMembersPairs(); $this->app->loadConfig('user'); $users = $this->dao->select('t1.account, t2.realname')->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account') ->where('t1.root')->eq((int)$projectID) ->andWhere('t1.type')->eq('project') ->beginIF($params == 'nodeleted' or empty($this->config->user->showDeleted)) ->andWhere('t2.deleted')->eq(0) ->fi() ->fetchPairs(); if($usersToAppended) $users += $this->dao->select('account, realname')->from(TABLE_USER)->where('account')->in($usersToAppended)->fetchPairs(); if(!$users) return array('' => ''); foreach($users as $account => $realName) { $firstLetter = ucfirst(substr($account, 0, 1)) . ':'; if(!empty($this->config->isINT)) $firstLetter = ''; $users[$account] = $firstLetter . ($realName ? $realName : $account); } return array('' => '') + $users; } /** * Get team slice. * * @param array $teams * @param string $begin * @param string $end * @access public * @return array */ public function getTeamSlice($teams, $begin, $end) { $members = array(); foreach($teams as $account => $team) { if($account == $end) break; if(!empty($begin) and $account != $begin and empty($members)) continue; $members[$account] = $team; } return $members; } /** * Get teams which can be imported. * * @param string $account * @param int $currentProject * @access public * @return array */ public function getTeams2Import($account, $currentProject) { return $this->dao->select('t1.root, t2.name as projectName') ->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.root = t2.id') ->where('t1.account')->eq($account) ->andWhere('t1.root')->ne($currentProject) ->andWhere('t1.type')->eq('project') ->andWhere('t2.deleted')->eq('0') ->groupBy('t1.root') ->orderBy('t1.root DESC') ->fetchPairs(); } /** * Get members of a project who can be imported. * * @param int $project * @param array $currentMembers * @access public * @return array */ public function getMembers2Import($project, $currentMembers) { if($project == 0) return array(); return $this->dao->select('account, role, hours') ->from(TABLE_TEAM) ->where('root')->eq($project) ->andWhere('type')->eq('project') ->andWhere('account')->notIN($currentMembers) ->fetchAll('account'); } /** * Manage team members. * * @param int $projectID * @access public * @return void */ public function manageMembers($projectID) { $project = $this->getByID($projectID); $data = (array)fixer::input('post')->get(); extract($data); $projectType = $project->type; $accounts = array_unique($accounts); $limited = array_values($limited); $oldJoin = $this->dao->select('`account`, `join`')->from(TABLE_TEAM)->where('root')->eq((int)$projectID)->andWhere('type')->eq($projectType)->fetchPairs(); $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq((int)$projectID)->andWhere('type')->eq($projectType)->exec(); $projectMember = 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 = (int)$projectID; $member->account = $account; $member->join = isset($oldJoin[$account]) ? $oldJoin[$account] : helper::today(); $member->type = $projectType; $projectMember[$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); if($projectType == 'project') { $childSprints = $this->dao->select('id')->from(TABLE_PROJECT)->where('project')->eq($project->id)->andWhere('type')->in('stage,sprint')->andWhere('deleted')->eq('0')->fetchPairs(); $linkedProducts = $this->loadModel('product')->getProductsByProject($project->id); if(!empty($childSprints)) $this->user->updateUserView($childSprints, 'sprint', $changedAccounts); if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', $changedAccounts); } /* Add iteration or phase team members to the project team. */ if($projectType == 'stage' || $projectType == 'sprint') { $this->addProjectMembers($project->project, $projectMember); if($project->acl != 'open') $this->updateUserView($projectID, 'sprint', $changedAccounts); } } /** * Add iteration or phase team members to the project team. * * @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); $this->loadModel('user')->updateUserView($projectID, $projectType, $changedAccounts); $linkedProducts = $this->loadModel('product')->getProductsByProject($projectID); 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); $this->dao->delete()->from(TABLE_TEAM)->where('root')->eq((int)$sprintID)->andWhere('type')->eq($sprint->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 project. */ if($sprint->type == 'stage' || $sprint->type == 'sprint') { $teamMember = $this->dao->select('t1.id, t2.account')->from(TABLE_PROJECT)->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')->getProductsByProject($sprint->project); if(!empty($linkedProducts)) $this->user->updateUserView(array_keys($linkedProducts), 'product', array($account)); } } } /** * Compute burn of a project. * * @access public * @return array */ public function computeBurn() { $today = helper::today(); $burns = array(); $projects = $this->dao->select('id, code')->from(TABLE_PROJECT) ->where("end")->ge($today) ->andWhere('type')->ne('ops') ->andWhere('status')->notin('done,closed,suspended') ->fetchPairs(); if(!$projects) return $burns; $burns = $this->dao->select("project, '$today' AS date, sum(estimate) AS `estimate`, sum(`left`) AS `left`, SUM(consumed) AS `consumed`") ->from(TABLE_TASK) ->where('project')->in(array_keys($projects)) ->andWhere('deleted')->eq('0') ->andWhere('parent')->ge('0') ->andWhere('status')->ne('cancel') ->groupBy('project') ->fetchAll('project'); $closedLefts = $this->dao->select("project, sum(`left`) AS `left`")->from(TABLE_TASK) ->where('project')->in(array_keys($projects)) ->andWhere('deleted')->eq('0') ->andWhere('parent')->ge('0') ->andWhere('status')->eq('closed') ->groupBy('project') ->fetchAll('project'); $finishedEstimates = $this->dao->select("project, sum(`estimate`) AS `estimate`")->from(TABLE_TASK) ->where('project')->in(array_keys($projects)) ->andWhere('deleted')->eq('0') ->andWhere('parent')->ge('0') ->andWhere('status')->eq('done') ->orWhere('status')->eq('closed') ->groupBy('project') ->fetchAll('project'); $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($projects)) ->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 $projectID => $burn) { if(isset($closedLefts[$projectID])) { $closedLeft = $closedLefts[$projectID]; $burn->left -= (int)$closedLeft->left; } if(isset($finishedEstimates[$projectID])) { $finishedEstimate = $finishedEstimates[$projectID]; $burn->estimate -= (int)$finishedEstimate->estimate; } if(isset($storyPoints[$projectID])) $burn->storyPoint = $storyPoints[$projectID]->storyPoint; $this->dao->replace(TABLE_BURN)->data($burn)->exec(); $burn->projectName = $projects[$burn->project]; } return $burns; } /** * Fix burn for first day. * * @param int $projectID * @access public * @return void */ public function fixFirst($projectID) { $project = $this->getById($projectID); $burn = $this->dao->select('*')->from(TABLE_BURN)->where('project')->eq($projectID)->andWhere('task')->eq(0)->andWhere('date')->eq($project->begin)->fetch(); $withLeft = $this->post->withLeft ? $this->post->withLeft : 0; $data = fixer::input('post') ->add('project', $projectID) ->add('date', $project->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 burn data for flot * * @param int $projectID * @param string $burnBy * @access public * @return array */ public function getBurnDataFlot($projectID = 0, $burnBy = '') { /* Get project and burn counts. */ $project = $this->getById($projectID); /* If the burnCounts > $itemCounts, get the latest $itemCounts records. */ $sets = $this->dao->select("date AS name, `$burnBy` AS value, `$burnBy`")->from(TABLE_BURN)->where('project')->eq((int)$projectID)->andWhere('task')->eq(0)->orderBy('date DESC')->fetchAll('name'); $count = 0; $burnData = array(); foreach($sets as $date => $set) { if($date < $project->begin) continue; if($date > $project->end) continue; $burnData[$date] = $set; $count++; } $burnData = array_reverse($burnData); return $burnData; } /** * 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($end != '0000-00-00') { $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; $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(isset($sets[$current])) $preValue = $sets[$current]->value; if($currentTime > time() and !$todayTag) { $todayTag = $i + 1; break; } if(!isset($sets[$current]) and $mode == 'noempty') { $sets[$current] = new stdclass(); $sets[$current]->name = $current; $sets[$current]->value = $preValue; } $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); } /** * Get taskes by search. * * @param string $condition * @param object $pager * @param string $orderBy * @access public * @return array */ public function getSearchTasks($condition, $pager, $orderBy) { $taskIdList = $this->dao->select('id') ->from(TABLE_TASK) ->where($condition) ->andWhere('deleted')->eq(0) ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); $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') ->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'); if(empty($tasks)) return array(); $taskTeam = $this->dao->select('*')->from(TABLE_TEAM)->where('root')->in(array_keys($tasks))->andWhere('type')->eq('task')->fetchGroup('root'); 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'); 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 project. * * @param array $products * @param int $projectID * @param string $sql * @param object $pager * @param string $orderBy * @access public * @return mixed */ public function getSearchBugs($products, $projectID, $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('project')->eq($projectID)->fi() ->andWhere('deleted')->eq(0) ->orderBy($orderBy) ->page($pager) ->fetchAll(); } /** * Get the summary of project. * * @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) { $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->project->taskSummary, $taskSum, $statusWait, $statusDoing, $totalEstimate, round($totalConsumed, 1), round($totalLeft, 1)); } /** * Judge an action is clickable or not. * * @param object $project * @param string $action * @access public * @return bool */ public static function isClickable($project, $action) { $action = strtolower($action); if($action == 'start') return $project->status == 'wait'; if($action == 'close') return $project->status != 'closed'; if($action == 'suspend') return $project->status == 'wait' or $project->status == 'doing'; if($action == 'putoff') return $project->status == 'wait' or $project->status == 'doing'; if($action == 'activate') return $project->status == 'suspended' or $project->status == 'closed'; return true; } /** * Create the link from module,method,extra * * @param string $module * @param string $method * @param mix $extra * @access public * @return void */ public function getProjectLink($module, $method, $extra) { $link = ''; if($module == 'task' and ($method == 'view' || $method == 'edit' || $method == 'batchedit')) { $module = 'project'; $method = 'task'; } if($module == 'build' and ($method == 'edit' || $method= 'view')) { $module = 'project'; $method = 'build'; } if($module == 'project' and $method == 'create') return; if($extra != '') { $link = helper::createLink($module, $method, "projectID=%s&type=$extra"); } elseif($module == 'project' && ($method == 'index' or $method == 'all')) { $link = helper::createLink($module, 'task', "projectID=%s"); } else { $link = helper::createLink($module, $method, "projectID=%s"); } if($module == 'doc') $link = helper::createLink('doc', 'objectLibs', "type=project&objectID=%s&from=project"); return $link; } /** * Get no weekend date * * @param string $begin * @param string $end * @param string $type * @param string|int $interval * @param string $format * @access public * @return array */ public function getDateList($begin, $end, $type, $interval = '', $format = 'm/d/Y') { $begin = strtotime($begin); $end = strtotime($end); $beginWeekDay = date('w', $begin); $days = ($end - $begin) / 3600 / 24; if($type == 'noweekend') { $allDays = $days; $weekDay = $beginWeekDay; for($i = 0; $i < $allDays; $i++, $weekDay++) { $weekDay = $weekDay % 7; if(($this->config->project->weekend == 2 and $weekDay == 6) or $weekDay == 0) $days--; } } if(!$interval) $interval = floor($days / $this->config->project->maxBurnDay); $dateList = array(); $spaces = (int)$interval; $counter = $spaces; $weekDay = $beginWeekDay; for($date = $begin; $date <= $end; $date += 24 * 3600, $weekDay++) { /* Remove weekend when type is noweekend.*/ if($type == 'noweekend') { $weekDay = $weekDay % 7; if(($this->config->project->weekend == 2 and $weekDay == 6) or $weekDay == 0) continue; } $counter ++; if($counter <= $spaces) continue; $counter = 0; $dateList[] = date($format, $date); } return array($dateList, $interval); } /** * Get total estimate. * * @param int $projectID * @access public * @return float */ public function getTotalEstimate($projectID) { $estimate = $this->dao->select('SUM(estimate) as estimate')->from(TABLE_TASK)->where('deleted')->eq('0')->andWhere('project')->eq($projectID)->fetch('estimate'); return round($estimate); } /** * Check the privilege. * * @param object $project * @access public * @return bool */ public function getLimitedProject() { /* If is admin, return true. */ if($this->app->user->admin) return true; /* Get all teams of all projects and group by projects, save it as static. */ $projects = $this->dao->select('root, limited')->from(TABLE_TEAM)->where('type')->eq('project')->andWhere('account')->eq($this->app->user->account)->andWhere('limited')->eq('yes')->orderBy('root asc')->fetchPairs('root', 'root'); $_SESSION['limitedProjects'] = join(',', $projects); } /** * Fix order. * * @access public * @return void */ public function fixOrder() { $projects = $this->dao->select('id,`order`')->from(TABLE_PROJECT)->orderBy('order')->fetchPairs('id', 'order'); $i = 0; foreach($projects as $id => $order) { $i++; $newOrder = $i * 5; if($order == $newOrder) continue; $this->dao->update(TABLE_PROJECT)->set('`order`')->eq($newOrder)->where('id')->eq($id)->exec(); } } /** * Get branches of project. * * @param int $projectID * @access public * @return array */ public function getProjectBranches($projectID) { $productBranchPairs = $this->dao->select('product, branch')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($projectID) ->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; } /** * Build bug search form. * * @param array $products * @param int $queryID * @param string $actionURL * @access public * @return void */ public function buildBugSearchForm($products, $queryID, $actionURL) { $modules = array(); $builds = array('' => '', 'trunk' => $this->lang->trunk); foreach($products as $product) { $productModules = $this->loadModel('tree')->getOptionMenu($product->id); $productBuilds = $this->loadModel('build')->getProductBuildPairs($product->id, 0, $params = 'noempty|notrunk'); 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), 'noempty'); $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($product->branch and isset($branchGroups[$product->id][$product->branch])) { $branchPairs[$product->branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$product->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'] = 'projectBug'; $this->config->bug->search['actionURL'] = $actionURL; $this->config->bug->search['queryID'] = $queryID; unset($this->config->bug->search['fields']['project']); $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 $projectID * @param array $projects * @param int $queryID * @param string $actionURL * @access public * @return void */ public function buildTaskSearchForm($projectID, $projects, $queryID, $actionURL) { $this->config->project->search['actionURL'] = $actionURL; $this->config->project->search['queryID'] = $queryID; $this->config->project->search['params']['project']['values'] = array(''=>'', $projectID => $projects[$projectID], 'all' => $this->lang->project->allProject); $showAllModule = isset($this->config->project->task->allModule) ? $this->config->project->task->allModule : ''; $this->config->project->search['params']['module']['values'] = $this->loadModel('tree')->getTaskOptionMenu($projectID, 0, 0, $showAllModule ? 'allModule' : ''); $this->loadModel('search')->setSearchParams($this->config->project->search); } /** * Get Kanban tasks * * @param int $projectID * @param string $orderBy * @param object $pager * @access public * @return void */ public function getKanbanTasks($projectID, $orderBy = 'status_asc, id_desc', $pager = null) { $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.project')->eq((int)$projectID) ->andWhere('t1.deleted')->eq(0) ->andWhere('t1.parent')->ge(0) ->orderBy($orderBy) ->page($pager) ->fetchAll(); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task'); 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 $projectID * @param array $kanbanDatas * @access public * @return void */ public function saveKanbanData($projectID, $kanbanDatas) { $data = array(); foreach($kanbanDatas as $type => $kanbanData) $data[$type] = array_keys($kanbanData); $this->loadModel('setting')->setItem("null.project.kanban.project$projectID", json_encode($data)); } /** * Get Prev Kanban. * * @param int $projectID * @access public * @return array */ public function getPrevKanban($projectID) { $prevKanbans = $this->loadModel('setting')->getItem("owner=null&module=project§ion=kanban&key=project$projectID"); return json_decode($prevKanbans, true); } /** * Get kanban setting. * * @access public * @return object */ public function getKanbanSetting() { $allCols = '1'; $showOption = '0'; if(isset($this->config->project->kanbanSetting->allCols)) $allCols = $this->config->project->kanbanSetting->allCols; $colorList = $this->config->project->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 $projectID * @param array $dateList * @param string $type * @param string $burnBy * @access public * @return array */ public function buildBurnData($projectID, $dateList, $type, $burnBy = 'left') { $this->loadModel('report'); $burnBy = $burnBy ? $burnBy : 'left'; $sets = $this->getBurnDataFlot($projectID, $burnBy); $limitJSON = '[]'; $baselineJSON = '[]'; $firstBurn = empty($sets) ? 0 : reset($sets); $firstTime = !empty($firstBurn->$burnBy) ? $firstBurn->$burnBy : (!empty($firstBurn->value) ? $firstBurn->value : 0); $days = count($dateList) - 1; $rate = $days ? $firstTime / $days : ''; $baselineJSON = '['; foreach($dateList as $i => $date) $baselineJSON .= round(($days - $i) * $rate, 1) . ','; $baselineJSON = rtrim($baselineJSON, ',') . ']'; $chartData['labels'] = $this->report->convertFormat($dateList, DT_DATE5); $chartData['burnLine'] = $this->report->createSingleJSON($sets, $dateList); $chartData['baseLine'] = $baselineJSON; return $chartData; } /** * Gets the top-level project name. * * @access private * @return void */ public function getProjectParentName($parentID = 0) { if($parentID == 0) return ''; static $parent; $parent = $this->dao->select('id,parent,name')->from(TABLE_PROJECT)->where('id')->eq($parentID)->fetch(); if($parent->parent) $this->getProjectParentName($parent->parent); return $parent->name; } /** * Fill tasks in tree. * @param object $tree * @param int $projectID * @access public * @return object */ public function fillTasksInTree($node, $projectID) { $node = (object)$node; static $storyGroups, $taskGroups; if(empty($storyGroups)) { $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)$projectID) ->andWhere('t2.deleted')->eq(0) ->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('project')->eq((int)$projectID) ->andWhere('deleted')->eq(0) ->andWhere('parent')->lt(1) ->orderBy('id_desc') ->fetchAll(); $childTasks = $this->dao->select('*')->from(TABLE_TASK) ->where('project')->eq((int)$projectID) ->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, $projectID); /* 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 = $users[$story->openedBy]; $storyItem->assignedTo = zget($users, $story->assignedTo); $storyItem->url = helper::createLink('story', 'view', "storyID=$story->id&version=$story->version&from=project¶m=$projectID"); $storyItem->taskCreateUrl = helper::createLink('task', 'batchCreate', "projectID={$projectID}&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', "projectID=$task->project&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 projects tree data * @param int $projectID * @access public * @return array */ public function getProjectTree($projectID) { $fullTrees = $this->loadModel('tree')->getTaskStructure($projectID, 0); array_unshift($fullTrees, array('id' => 0, 'name' => '/', 'type' => 'task', 'actions' => false, 'root' => $projectID)); 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, $projectID); 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 plans by $productID. * * @param int|array $productID * * @return mixed */ public function getPlans($products) { $this->loadModel('productplan'); $productPlans = array(); foreach($products as $productID => $product) { $productPlans[$productID] = $this->productplan->getPairs($product->id, isset($product->branch) ? $product->branch : ''); } return $productPlans; } /** * Print html for tree. * * @param object $trees * @access pubic * @return string */ public function printTree($trees) { $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('project', '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'); $html .= '
  • '; $html .= '' . $this->lang->productCommon . "" . $tree->name . ''; break; case 'story': $this->app->loadLang('story'); $link = helper::createLink('project', '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; } /** * Update user view of project and it's product. * * @param int|array $projectID * @param string $objectType * @param array $users * @access public * @return void */ public function updateUserView($projectID, $objectType = 'sprint', $users = array()) { $this->loadModel('user')->updateUserView($projectID, $objectType, $users); $products = $this->getProducts($projectID, $withBranch = false); if(!empty($products)) $this->user->updateUserView(array_keys($products), 'product', $users); } }