* @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; $acls = $this->app->user->rights['acls']; if(!empty($acls['projects']) and !in_array($project->id, $acls['projects'])) return false; /* If project is open, return true. */ if($project->acl == 'open') return true; /* Get all teams of all projects and group by projects, save it as static. */ static $teams; if(empty($teams)) $teams = $this->dao->select('project, account')->from(TABLE_TEAM)->fetchGroup('project', 'account'); $currentTeam = isset($teams[$project->id]) ? $teams[$project->id] : array(); /* If project is private, only members can access. */ if($project->acl == 'private') { return isset($currentTeam[$this->app->user->account]); } /* Project's acl is custom, check the groups. */ if($project->acl == 'custom') { if(isset($currentTeam[$this->app->user->account])) return true; $userGroups = $this->app->user->groups; $projectGroups = explode(',', $project->whitelist); foreach($userGroups as $groupID) { if(in_array($groupID, $projectGroups)) return true; } return false; } } /** * Set menu. * * @param array $projects * @param int $projectID * @param string $extra * @access public * @return void */ public function setMenu($projects, $projectID, $extra = '') { /* Check the privilege. */ $project = $this->getById($projectID); /* 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->bug); unset($this->lang->project->menu->build); unset($this->lang->project->menu->testtask); } if($projects and !isset($projects[$projectID]) and !$this->checkPriv($project)) { echo(js::alert($this->lang->project->accessDenied)); $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(inlink('index'))); die(js::locate('back')); } $moduleName = $this->app->getModuleName(); $methodName = $this->app->getMethodName(); if($this->cookie->projectMode == 'noclosed' and $project->status == 'done') { setcookie('projectMode', 'all'); $this->cookie->projectMode = 'all'; } $selectHtml = $this->select($projects, $projectID, $moduleName, $methodName, $extra); foreach($this->lang->project->menu as $key => $menu) { $replace = $key == 'list' ? $selectHtml : $projectID; common::setMenuVars($this->lang->project->menu, $key, $replace); } } /** * Create the select code of projects. * * @param array $projects * @param int $projectID * @param string $currentModule * @param string $currentMethod * @param string $extra * @access public * @return string */ public function select($projects, $projectID, $currentModule, $currentMethod, $extra = '') { if(!$projectID) return; $isMobile = $this->app->viewType == 'mhtml'; setCookie("lastProject", $projectID, $this->config->cookieLife, $this->config->webRoot); $currentProject = $this->getById($projectID); $output = "{$currentProject->name}
"; if($isMobile) $output = "{$currentProject->name} "; return $output; } /** * Get project tree menu. * * @access public * @return void */ public function tree() { $products = $this->loadModel('product')->getPairs('nocode'); $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 > 0) { echo(js::alert($this->lang->project->accessDenied)); $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(inlink('index'))); die(js::locate('back')); } } return $this->session->project; } /** * Create a project. * * @access public * @return void */ public function create($copyProjectID = '') { $this->lang->project->team = $this->lang->project->teamname; $project = fixer::input('post') ->setDefault('status', 'wait') ->setIF($this->post->acl != 'custom', 'whitelist', '') ->setDefault('openedVersion', $this->config->version) ->setDefault('team', substr($this->post->name,0, 30)) ->join('whitelist', ',') ->stripTags($this->config->project->editor->create['id'], $this->config->allowedTags) ->remove('products, workDays, delta, branch,uid') ->get(); $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->create['id'], $this->post->uid); $this->dao->insert(TABLE_PROJECT)->data($project) ->autoCheck($skipFields = 'begin,end') ->batchcheck($this->config->project->create->requiredFields, 'notempty') ->checkIF($project->begin != '', 'begin', 'date') ->checkIF($project->end != '', 'end', 'date') ->checkIF($project->end != '', 'end', 'gt', $project->begin) ->check('name', 'unique', "deleted='0'") ->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'); /* Copy team of project. */ if($copyProjectID != '') { $members = $this->dao->select('*')->from(TABLE_TEAM)->where('project')->eq($copyProjectID)->fetchAll(); foreach($members as $member) { $member->project = $projectID; $member->join = $today; $member->days = $project->days; $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) { $member = new stdclass(); $member->project = $projectID; $member->account = $this->app->user->account; $member->role = $this->lang->user->roleList[$this->app->user->role]; $member->join = $today; $member->days = $project->days; $member->hours = $this->config->project->defaultWorkhours; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } /* Create doc lib. */ $this->app->loadLang('doc'); $lib = new stdclass(); $lib->project = $projectID; $lib->name = $this->lang->doclib->main['project']; $lib->main = '1'; $lib->acl = $project->acl == 'open' ? 'open' : 'private'; $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); $this->loadModel('score')->create('project', 'create', $projectID); return $projectID; } } /** * Update a project. * * @param int $projectID * @access public * @return array */ public function update($projectID) { $oldProject = $this->dao->findById((int)$projectID)->from(TABLE_PROJECT)->fetch(); $team = $this->getTeamMemberPairs($projectID); $this->lang->project->team = $this->lang->project->teamname; $projectID = (int)$projectID; $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', '') ->setDefault('team', $this->post->name) ->join('whitelist', ',') ->stripTags($this->config->project->editor->edit['id'], $this->config->allowedTags) ->remove('products,branch,uid') ->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('name', 'unique', "id!=$projectID and deleted='0'") ->check('code', 'unique', "id!=$projectID and deleted='0'") ->where('id')->eq($projectID) ->limit(1) ->exec(); 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->project = (int)$projectID; $member->account = $value; $member->join = helper::today(); $member->role = $this->lang->project->$fieldName; $member->days = $project->days; $member->hours = $this->config->project->defaultWorkhours; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } } } if(!dao::isError()) { if($project->acl != $oldProject->acl) $this->dao->update(TABLE_DOCLIB)->set('acl')->eq($project->acl == 'open' ? 'open' : 'private')->where('project')->eq($projectID)->exec(); $this->file->updateObjectID($this->post->uid, $projectID, 'project'); return common::createChanges($oldProject, $project); } } /** * Batch update. * * @access public * @return void */ public function batchUpdate() { $projects = array(); $allChanges = array(); $data = fixer::input('post')->get(); $oldProjects = $this->getByIdList($this->post->projectIDList); foreach($data->projectIDList as $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]->type = $data->types[$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 = $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('name', 'unique', "id!=$projectID and deleted='0'") ->check('code', 'unique', "id!=$projectID and deleted='0'") ->where('id')->eq($projectID) ->limit(1) ->exec(); 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->project = (int)$projectID; $member->account = $value; $member->join = helper::today(); $member->role = $this->lang->project->$fieldName; $member->days = 0; $member->hours = $this->config->project->defaultWorkhours; $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } } } 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') ->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') ->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 void */ public function close($projectID) { $oldProject = $this->getById($projectID); $now = helper::now(); $project = fixer::input('post') ->setDefault('status', 'done') ->remove('comment')->get(); $this->dao->update(TABLE_PROJECT)->data($project) ->autoCheck() ->where('id')->eq((int)$projectID) ->exec(); $this->loadModel('score')->create('project', 'close', $oldProject); if(!dao::isError()) return common::createChanges($oldProject, $project); } /** * Get project pairs. * * @param string $mode all|noclosed or empty * @access public * @return array */ public function getPairs($mode = '') { 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", status) < 2, 0, 1) AS isDone')->from(TABLE_PROJECT) ->where('iscat')->eq(0) ->beginIF(strpos($mode, 'withdelete') === false)->andWhere('deleted')->eq(0)->fi() ->orderBy($orderBy) ->fetchAll(); $pairs = array(); foreach($projects as $project) { if(strpos($mode, 'noclosed') !== false and $project->status == 'done') continue; if($this->checkPriv($project)) $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]) and $this->checkPriv($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) { 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.deleted')->eq(0) ->andWhere('t2.iscat')->eq(0) ->beginIF($status == 'undone')->andWhere('t2.status')->ne('done')->fi() ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($status == 'isdoing')->andWhere('t2.status')->ne('done')->andWhere('t2.status')->ne('suspended')->fi() ->beginIF($status != 'all' and $status != 'isdoing' and $status != 'undone')->andWhere('status')->in($status)->fi() ->orderBy('order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } else { return $this->dao->select('*, IF(INSTR(" done", status) < 2, 0, 1) AS isDone')->from(TABLE_PROJECT)->where('iscat')->eq(0) ->beginIF($status == 'undone')->andWhere('status')->ne('done')->fi() ->beginIF($status == 'isdoing')->andWhere('status')->ne('done')->andWhere('status')->ne('suspended')->fi() ->beginIF($status != 'all' and $status != 'isdoing' and $status != 'undone')->andWhere('status')->in($status)->fi() ->andWhere('deleted')->eq(0) ->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.project=t2.id') ->where('t1.product')->eq($productID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.iscat')->eq(0) ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi() ->andWhere('t2.openedBy', true)->eq($this->app->user->account) ->orWhere('t3.account')->eq($this->app->user->account) ->markRight(1) ->orderBy('order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } else { return $this->dao->select('t1.*, IF(INSTR(" done", t1.status) < 2, 0, 1) AS isDone')->from(TABLE_PROJECT)->alias('t1') ->leftJoin(TABLE_TEAM)->alias('t2')->on('t2.project=t1.id') ->where('t1.iscat')->eq(0) ->andWhere('t1.openedBy', true)->eq($this->app->user->account) ->orWhere('t2.account')->eq($this->app->user->account) ->markRight(1) ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.order_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id'); } } /** * 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) ->fetchGroup('product'); $noProducts = array(); $projects = $this->getList(); foreach($list as $id => $product) { foreach($product as $ID => $project) { if(!$this->checkPriv($projects[$project->id])) { unset($list[$id][$ID]); } if(!$project->product) { if($this->checkPriv($projects[$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 int $pager * @access public * @return void */ public function getProjectStats($status = 'undone', $productID = 0, $branch = 0, $itemCounts = 30, $orderBy = 'order_desc', $pager = null) { /* Init vars. */ $projects = $this->getList($status, 0, $productID, $branch); foreach($projects as $projectID => $project) { if(!$this->checkPriv($project)) unset($projects[$projectID]); } $projects = $this->dao->select('*')->from(TABLE_PROJECT) ->where('id')->in(array_keys($projects)) ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); $projectKeys = array_keys($projects); $stats = array(); $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('deleted')->eq(0) ->fetchGroup('project', 'id'); /* Compute totalEstimate, totalConsumed, totalLeft. */ foreach($tasks as $projectID => $projectTasks) { $hour = (object)$emptyHour; foreach($projectTasks as $task) { $hour->totalEstimate += $task->estimate; $hour->totalConsumed += $task->consumed; $hour->totalLeft += ($task->status != 'cancel' and $task->closedReason != 'cancel') ? $task->left : 0; } $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) ->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; $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. */ foreach($projects as $key => $project) { // Process the end time. $project->end = date("Y-m-d", strtotime($project->end)); /* Judge whether the project is delayed. */ if($project->status != 'done' 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; $stats[] = $project; } return $stats; } /** * 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; } /* 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 void */ 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 != 'suspended') { $delay = helper::diffDate(helper::today(), $project->end); if($delay > 0) $project->delay = $delay; } $total = $this->dao->select(' SUM(estimate) AS totalEstimate, SUM(consumed) AS totalConsumed, SUM(`left`) AS totalLeft') ->from(TABLE_TASK) ->where('project')->eq((int)$projectID) ->andWhere('status')->ne('cancel') ->andWhere('deleted')->eq(0) ->fetch(); $project->days = $project->days ? $project->days : ''; $project->totalHours = $this->dao->select('sum(days * hours) AS totalHours')->from(TABLE_TEAM)->where('project')->eq($project->id)->fetch('totalHours'); $project->totalEstimate = round($total->totalEstimate, 1); $project->totalConsumed = round($total->totalConsumed, 1); $project->totalLeft = round($total->totalLeft, 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($this->config->global->flow == 'onlyTask') return array(); 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')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2') ->on('t1.product = t2.id') ->where('t1.project')->eq((int)$projectID); if(!$withBranch) return $query->fetchPairs('id', 'name'); return $query->fetchAll('id'); } /** * 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) { $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; unset($this->config->product->search['fields']['stage']); unset($this->config->product->search['params']['stage']); } $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) ->andWhere('deleted')->eq(0) ->orderBy('id desc') ->fetchAll('id'); $pairs = array(); $now = date('Y-m-d'); foreach($projects as $id => $project) { if($this->checkPriv($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) { $deletedProducts = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->fetchPairs('product', 'product'); $this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq((int)$projectID)->exec(); if(!isset($_POST['products'])) return; $products = $_POST['products']; $branches = isset($_POST['branch']) ? $_POST['branch'] : array(); $existedProducts = array(); $addedProducts = array(); foreach($products as $i => $productID) { if(empty($productID)) continue; if(isset($existedProducts[$productID])) continue; if(isset($deletedProducts[$productID])) { unset($deletedProducts[$productID]); } else { $addedProducts[$productID] = $productID; } $data = new stdclass(); $data->project = $projectID; $data->product = $productID; $data->branch = isset($branches[$i]) ? $branches[$i] : 0; $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); $existedProducts[$productID] = true; } } /** * 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.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'); /* Update tasks. */ $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; if($task->status == 'cancel') { $data->canceledBy = ''; $data->canceledDate = NULL; } $data->status = $task->consumed > 0 ? 'doing' : 'wait'; $this->dao->update(TABLE_TASK)->data($data)->where('id')->in($this->post->tasks)->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('project')->eq($preProjectID)->andWhere('account')->eq($account)->fetch(); $role->project = $projectID; $role->join = helper::today(); $this->dao->insert(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(); } } } /** * 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(); $modules = $this->loadModel('tree')->getTaskOptionMenu($projectID); $bugToTasks = fixer::input('post')->get(); $bugs = $this->bug->getByList(array_keys($bugToTasks->import)); foreach($bugToTasks->import as $key => $value) { $bug = $bugs[$key]; $task = new stdClass(); $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->consumed = 0; $task->status = 'wait'; $task->openedDate = $now; $task->openedBy = $this->app->user->account; if(!empty($bugToTasks->estimate[$key])) { $task->estimate = $bugToTasks->estimate[$key]; $task->left = $task->estimate; } if(!empty($bugToTasks->assignedTo[$key])) { $task->assignedTo = $bugToTasks->assignedTo[$key]; $task->assignedDate = $now; } 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($bugToTasks->estimate[$key] != '', '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 * @access public * @return void */ public function linkStory($projectID) { if($this->post->stories == false) return false; $this->loadModel('action'); $versions = $this->loadModel('story')->getVersions($this->post->stories); $lastOrder = (int)$this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('project')->eq($projectID)->orderBy('order_desc')->limit(1)->fetch('order'); foreach($this->post->stories as $key => $storyID) { $productID = (int)$this->post->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); } } /** * 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'); $this->dao->update(TABLE_TASK)->set('status')->eq('cancel')->where('id')->in($tasks)->exec(); foreach($tasks as $taskID) { $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(); 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.project')->eq((int)$projectID) ->fetchAll('account'); } /** * Get team members in pair. * * @param int $projectID * @param string $params * @access public * @return array */ public function getTeamMemberPairs($projectID, $params = '') { if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getTeamMembersPairs(); $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.project')->eq((int)$projectID) ->beginIF($params == 'nodeleted' or empty($this->config->user->showDeleted)) ->andWhere('t2.deleted')->eq(0) ->fi() ->fetchPairs(); if(!$users) return array(); foreach($users as $account => $realName) { $firstLetter = ucfirst(substr($account, 0, 1)) . ':'; $users[$account] = $firstLetter . ($realName ? $realName : $account); } return array('' => '') + $users; } /** * 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.project, t2.name as projectName') ->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t1.account')->eq($account) ->andWhere('t1.project')->ne($currentProject) ->groupBy('t1.project') ->orderBy('t1.project 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('project')->eq($project) ->andWhere('account')->notIN($currentMembers) ->fetchAll('account'); } /** * Manage team members. * * @param int $projectID * @access public * @return void */ public function manageMembers($projectID) { $data = (array)fixer::input('post')->get(); extract($data); $accounts = array_unique($accounts); 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->limitedUser = $limitedUser[$key]; $mode = $modes[$key]; if($mode == 'update') { $this->dao->update(TABLE_TEAM) ->data($member) ->where('project')->eq((int)$projectID) ->andWhere('account')->eq($account) ->exec(); } else { $member->project = (int)$projectID; $member->account = $account; $member->join = helper::today(); $this->dao->insert(TABLE_TEAM)->data($member)->exec(); } } } /** * Unlink a member. * * @param int $projectID * @param string $account * @access public * @return void */ public function unlinkMember($projectID, $account) { $this->dao->delete()->from(TABLE_TEAM)->where('project')->eq((int)$projectID)->andWhere('account')->eq($account)->exec(); } /** * 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,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('status')->notin('cancel,closed') ->groupBy('project') ->fetchAll(); foreach($burns as $Key => $burn) { $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('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 * @access public * @return void */ public function getBurnDataFlot($projectID = 0) { /* 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, `left` AS value, estimate')->from(TABLE_BURN)->where('project')->eq((int)$projectID)->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(); $this->loadModel('task')->processTasks($tasks); return $tasks; } /** * Get bugs by search in project. * * @param int $products * @param int $projectID * @param int $sql * @param int $pager * @param int $orderBy * @access public * @return void */ 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; $totalLeft += (($task->status == 'cancel' or $task->closedReason == 'cancel') ? 0 : $task->left); $statusVar = 'status' . ucfirst($task->status); $$statusVar ++; } return sprintf($this->lang->project->taskSummary, count($tasks), $statusWait, $statusDoing, $totalEstimate, round($totalConsumed, 1), $totalLeft); } /** * 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 != 'done'; 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 == 'done'; 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 * @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('project, limitedUser')->from(TABLE_TEAM)->where('account')->eq($this->app->user->account)->andWhere('limitedUser')->eq('yes')->orderBy('project asc')->fetchPairs('project', 'project'); $_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 int $products * @param int $queryID * @param int $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) { $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); $this->config->project->search['params']['module']['values'] = $this->loadModel('tree')->getTaskOptionMenu($projectID, $startModuleID = 0); $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) ->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(!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(!isset($kanbanGroup[$groupKey])) $kanbanGroup[$groupKey] = new stdclass(); $kanbanGroup[$groupKey]->bugs[$status][] = $bug; } else { $noKeyBugs[$status][] = $bug; } } $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("ower=null&module=project§ion=kanban&key=project$projectID"); return json_decode($prevKanbans, true); } /** * Get kanban setting. * * @param int $projectID * @access public * @return object */ public function getKanbanSetting($projectID) { $allCols = '1'; $showOption = '0'; if(isset($this->config->project->kanbanSetting->allCols)) $allCols = $this->config->project->kanbanSetting->allCols; if(isset($this->config->project->kanbanSetting->showOption)) $showOption = $this->config->project->kanbanSetting->showOption; $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; } /** * Build burn data. * * @param int $projectID * @param array $dateList * @param string $type * @access public * @return array */ public function buildBurnData($projectID, $dateList, $type) { $this->loadModel('report'); $sets = $this->getBurnDataFlot($projectID); $limitJSON = '[]'; $baselineJSON = '[]'; $firstBurn = empty($sets) ? 0 : reset($sets); $firstTime = !empty($firstBurn->estimate) ? $firstBurn->estimate : (!empty($firstBurn->value) ? $firstBurn->value : 0); $days = count($dateList) - 1; $rate = $firstTime / $days; $baselineJSON = '['; foreach($dateList as $i => $date) $baselineJSON .= ($days - $i) * $rate . ','; $baselineJSON = rtrim($baselineJSON, ',') . ']'; $chartData['labels'] = $this->report->convertFormat($dateList, 'j/n'); $chartData['burnLine'] = $this->report->createSingleJSON($sets, $dateList); $chartData['baseLine'] = $baselineJSON; return $chartData; } /** * 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')->eq(0) ->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') { $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->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->estimate = $task->estimate; $taskItem->consumed = $task->consumed; $taskItem->left = $task->left; $taskItem->assignedTo = $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($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, $manage = false); 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); } }