diff --git a/framework/router.class.php b/framework/router.class.php index a7bd03d858..b6814b2e97 100755 --- a/framework/router.class.php +++ b/framework/router.class.php @@ -716,4 +716,24 @@ class router extends baseRouter return parent::mergeParams($defaultParams, $passedParams); } + + /** + * 加载一个模块: + * + * Load a module. + * + * @access public + * @return bool|object if the module object of die. + */ + public function loadModule() + { + /* 不能直接请求基类的方法 Cannot call methods of base control class. */ + if(method_exists('Control', $this->methodName)) + { + echo 'Cannot call methods of base control class.'; + return false; + } + + return parent::loadModule(); + } } diff --git a/module/block/ui/scrumlistblock.html.php b/module/block/ui/scrumlistblock.html.php index 1bb6033a6f..f017fc2bd3 100644 --- a/module/block/ui/scrumlistblock.html.php +++ b/module/block/ui/scrumlistblock.html.php @@ -15,10 +15,10 @@ jsVar('delayInfo', $lang->project->delayInfo); foreach($executionStats as $scrum) { - $scrum->totalEstimate = zget($scrum->hours, 'totalEstimate', 0) . $lang->execution->workHourUnit; - $scrum->totalConsumed = zget($scrum->hours, 'totalConsumed', 0) . $lang->execution->workHourUnit; - $scrum->totalLeft = zget($scrum->hours, 'totalLeft', 0) . $lang->execution->workHourUnit; - $scrum->progress = zget($scrum->hours, 'progress', 0); + $scrum->totalEstimate = zget($scrum, 'estimate', 0) . $lang->execution->workHourUnit; + $scrum->totalConsumed = zget($scrum, 'consumed', 0) . $lang->execution->workHourUnit; + $scrum->totalLeft = zget($scrum, 'left', 0) . $lang->execution->workHourUnit; + $scrum->progress = zget($scrum, 'progress', 0); } if(!$longBlock) diff --git a/module/block/zen.php b/module/block/zen.php index 701c448be0..4fcbcd9173 100644 --- a/module/block/zen.php +++ b/module/block/zen.php @@ -755,21 +755,7 @@ class blockZen extends block $type = isset($block->params->type) ? $block->params->type : 'all'; $orderBy = isset($block->params->orderBy) ? $block->params->orderBy : 'id_desc'; - $projects = $this->loadModel('project')->getOverviewList($type, 0, $orderBy, $count); - - /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('project')->in(array_keys($projects)) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('project', 'id'); - $hours = $this->loadModel('program')->computeProjectHours($tasks); - - $projects = $this->program->appendStatToProjects($projects, 'hours', array('hours' => $hours)); - foreach($projects as $project) $project->progress = $project->hours->progress; - - $this->view->projects = $projects; + $this->view->projects = $this->loadModel('project')->getOverviewList($type, 0, $orderBy, $count); $this->view->users = $this->loadModel('user')->getPairs('noletter', '', 0, array_unique(array_column($this->view->projects, 'PM'))); } diff --git a/module/cron/control.php b/module/cron/control.php index da2a63ed35..2aa3dda32f 100644 --- a/module/cron/control.php +++ b/module/cron/control.php @@ -115,64 +115,6 @@ class cron extends control return $this->sendSuccess(array('load' => true)); } - /** - * Schedule cron task by RoadRunner. - * - * @access public - * @return void - */ - public function rrSchedule() - { - if('cli' !== PHP_SAPI) return; - - set_time_limit(0); - session_write_close(); - - $this->loadModel('common'); - - $execId = mt_rand(); - while(true) - { - dao::$cache = array(); - if(empty($this->config->global->cron) || !$this->canSchedule($execId)) - { - sleep(60); - continue; - } - - $this->schedule($execId); - sleep(30); - } - } - - /** - * Consume cron task by RoadRunner. - * - * @access public - * @return void - */ - public function rrConsume() - { - if('cli' !== PHP_SAPI) return; - - set_time_limit(0); - session_write_close(); - - $this->loadModel('common'); - - while(true) - { - if(empty($this->config->global->cron)) - { - sleep(60); - continue; - } - - $this->execTasks(mt_rand()); - sleep(10); - } - } - /** * 使用Ajax请求执行定时任务. * Ajax execute cron. diff --git a/module/editor/control.php b/module/editor/control.php index 472162befb..a762c99d54 100644 --- a/module/editor/control.php +++ b/module/editor/control.php @@ -73,7 +73,7 @@ class editor extends control $extension = 'php'; if($filePath) { - $filePath = helper::safe64Decode($filePath); + $filePath = realpath($filePath); if(strpos(strtolower($filePath), strtolower($this->app->getBasePath())) !== 0) return print($this->lang->editor->editFileError); if($action == 'extendOther' and file_exists($filePath)) $this->view->showContent = file_get_contents($filePath); diff --git a/module/execution/control.php b/module/execution/control.php index 3bf371e8c1..2afc5ff4e2 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -2691,6 +2691,8 @@ class execution extends control $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); + $this->loadModel('program')->refreshStats(); // Refresh stats fields of projects. + $queryID = ($status == 'bySearch') ? (int)$param : 0; $actionURL = $this->createLink('execution', 'all', "status=bySearch&orderBy=$orderBy&productID=$productID¶m=myQueryID"); $this->execution->buildSearchForm($queryID, $actionURL); diff --git a/module/execution/model.php b/module/execution/model.php index ea2d81d0b7..8c335d0b73 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -1610,14 +1610,11 @@ class executionModel extends model $parentList = array(); $emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0); $today = helper::today(); - $hours = $this->loadModel('project')->computeProgress($executions); $burns = $this->getBurnData($executions); foreach($executions as $execution) { $execution->productName = isset($productList[$execution->id]) ? trim($productList[$execution->id]->productName, ',') : ''; $execution->end = date(DT_DATE1, strtotime($execution->end)); - $execution->hours = isset($hours[$execution->id]) ? $hours[$execution->id] : (object)$emptyHour; - $execution->teamCount = isset($memberGroup[$execution->id]) ? $memberGroup[$execution->id]->teams : 0; if(isset($executions[$execution->parent])) $executions[$execution->parent]->isParent = 1; if(empty($productID) && !empty($productList[$execution->id])) $execution->product = trim($productList[$execution->id]->product, ','); @@ -4658,10 +4655,6 @@ class executionModel extends model $execution->project = $execution->projectName; $execution->parent = ($execution->parent && $execution->grade > 1) ? 'pid' . (string)$execution->parent : ''; $execution->isParent = !empty($execution->isParent) or !empty($execution->tasks); - $execution->progress = $execution->hours->progress; - $execution->totalEstimate = $execution->hours->totalEstimate; - $execution->totalConsumed = $execution->hours->totalConsumed; - $execution->totalLeft = $execution->hours->totalLeft; $execution->actions = array(); foreach($this->config->projectExecution->dtable->fieldList['actions'][$execution->projectModel] as $actionKey) { diff --git a/module/product/config.php b/module/product/config.php index 848e0d94f8..546b8bf50c 100644 --- a/module/product/config.php +++ b/module/product/config.php @@ -47,8 +47,8 @@ $config->product->report->planLabels[] = ''; $config->product->statisticFields = array(); $config->product->statisticFields['requirements'] = array('draftRequirements', 'activeRequirements', 'changingRequirements', 'reviewingRequirements', 'closedRequirements'); -$config->product->statisticFields['stories'] = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishClosedStories', 'unclosedStories'); -$config->product->statisticFields['bugs'] = array('unResolvedBugs', 'closedBugs', 'fixedBugs'); +$config->product->statisticFields['stories'] = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishedStories', 'totalStories'); +$config->product->statisticFields['bugs'] = array('unresolvedBugs', 'closedBugs', 'fixedBugs'); $config->product->statisticFields['plans'] = array('plans'); $config->product->statisticFields['releases'] = array('releases'); diff --git a/module/product/config/dtable.php b/module/product/config/dtable.php index 9e2e630128..72c0ba5893 100644 --- a/module/product/config/dtable.php +++ b/module/product/config/dtable.php @@ -98,12 +98,12 @@ $config->product->dtable->fieldList['testCaseCoverage']['type'] = 'progress' $config->product->dtable->fieldList['testCaseCoverage']['sortType'] = false; $config->product->dtable->fieldList['testCaseCoverage']['border'] = 'right'; -$config->product->dtable->fieldList['unResolvedBugs']['name'] = 'unResolvedBugs'; -$config->product->dtable->fieldList['unResolvedBugs']['title'] = $lang->product->activatedBug; -$config->product->dtable->fieldList['unResolvedBugs']['minWidth'] = 64; -$config->product->dtable->fieldList['unResolvedBugs']['type'] = 'number'; -$config->product->dtable->fieldList['unResolvedBugs']['sortType'] = false; -$config->product->dtable->fieldList['unResolvedBugs']['align'] = 'center'; +$config->product->dtable->fieldList['unresolvedBugs']['name'] = 'unresolvedBugs'; +$config->product->dtable->fieldList['unresolvedBugs']['title'] = $lang->product->activatedBug; +$config->product->dtable->fieldList['unresolvedBugs']['minWidth'] = 64; +$config->product->dtable->fieldList['unresolvedBugs']['type'] = 'number'; +$config->product->dtable->fieldList['unresolvedBugs']['sortType'] = false; +$config->product->dtable->fieldList['unresolvedBugs']['align'] = 'center'; $config->product->dtable->fieldList['bugFixedRate']['name'] = 'bugFixedRate'; $config->product->dtable->fieldList['bugFixedRate']['title'] = $lang->product->bugFixedRate; @@ -240,13 +240,13 @@ $config->product->all->dtable->fieldList['plans']['show'] = true; $config->product->all->dtable->fieldList['plans']['sortType'] = false; $config->product->all->dtable->fieldList['plans']['group'] = 'g5'; -$config->product->all->dtable->fieldList['execution']['name'] = 'execution'; -$config->product->all->dtable->fieldList['execution']['title'] = $lang->execution->common; -$config->product->all->dtable->fieldList['execution']['width'] = 64; -$config->product->all->dtable->fieldList['execution']['type'] = 'number'; -$config->product->all->dtable->fieldList['execution']['show'] = true; -$config->product->all->dtable->fieldList['execution']['sortType'] = false; -$config->product->all->dtable->fieldList['execution']['group'] = 'g6'; +$config->product->all->dtable->fieldList['executions']['name'] = 'executions'; +$config->product->all->dtable->fieldList['executions']['title'] = $lang->execution->common; +$config->product->all->dtable->fieldList['executions']['width'] = 64; +$config->product->all->dtable->fieldList['executions']['type'] = 'number'; +$config->product->all->dtable->fieldList['executions']['show'] = true; +$config->product->all->dtable->fieldList['executions']['sortType'] = false; +$config->product->all->dtable->fieldList['executions']['group'] = 'g6'; $config->product->all->dtable->fieldList['testCaseCoverage']['name'] = 'testCaseCoverage'; $config->product->all->dtable->fieldList['testCaseCoverage']['title'] = $lang->product->testCaseCoverage; @@ -257,14 +257,14 @@ $config->product->all->dtable->fieldList['testCaseCoverage']['show'] = true; $config->product->all->dtable->fieldList['testCaseCoverage']['sortType'] = false; $config->product->all->dtable->fieldList['testCaseCoverage']['group'] = 'g7'; -$config->product->all->dtable->fieldList['totalActivatedBugs']['name'] = 'totalActivatedBugs'; -$config->product->all->dtable->fieldList['totalActivatedBugs']['title'] = $lang->product->activatedBug; -$config->product->all->dtable->fieldList['totalActivatedBugs']['width'] = 64; -$config->product->all->dtable->fieldList['totalActivatedBugs']['minWidth'] = 86; -$config->product->all->dtable->fieldList['totalActivatedBugs']['type'] = 'number'; -$config->product->all->dtable->fieldList['totalActivatedBugs']['show'] = true; -$config->product->all->dtable->fieldList['totalActivatedBugs']['sortType'] = false; -$config->product->all->dtable->fieldList['totalActivatedBugs']['group'] = 'g8'; +$config->product->all->dtable->fieldList['unresolvedBugs']['name'] = 'unresolvedBugs'; +$config->product->all->dtable->fieldList['unresolvedBugs']['title'] = $lang->product->activatedBug; +$config->product->all->dtable->fieldList['unresolvedBugs']['width'] = 64; +$config->product->all->dtable->fieldList['unresolvedBugs']['minWidth'] = 86; +$config->product->all->dtable->fieldList['unresolvedBugs']['type'] = 'number'; +$config->product->all->dtable->fieldList['unresolvedBugs']['show'] = true; +$config->product->all->dtable->fieldList['unresolvedBugs']['sortType'] = false; +$config->product->all->dtable->fieldList['unresolvedBugs']['group'] = 'g8'; $config->product->all->dtable->fieldList['totalBugs']['name'] = 'totalBugs'; $config->product->all->dtable->fieldList['totalBugs']['title'] = $lang->product->totalBugs; diff --git a/module/product/control.php b/module/product/control.php index 3dc80a840a..c3078285ee 100755 --- a/module/product/control.php +++ b/module/product/control.php @@ -587,6 +587,9 @@ class product extends control $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); + + $this->product->refreshStats(); // Refresh stats fields of products. + $productStatList = $this->product->getStats(array_keys($products), $orderBy, $pager, 'story', $programID); /* Generate root program list. */ diff --git a/module/product/model.php b/module/product/model.php index 3985e7b5eb..5ea69bd7e1 100755 --- a/module/product/model.php +++ b/module/product/model.php @@ -880,26 +880,7 @@ class productModel extends model $projects = $this->getProjectListByProduct($productID, $browseType, $branch, $involved, $orderBy, $pager); if(empty($projects)) return array(); - $projectKeys = array_keys($projects); - - /* 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'); - $hours = $this->loadModel('program')->computeProjectHours($tasks); - - /* Get the number of project teams. */ - $teams = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1') - ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') - ->where('t1.root')->in($projectKeys) - ->andWhere('t1.type')->eq('project') - ->andWhere('t2.deleted')->eq(0) - ->fetchGroup('root', 'account'); - - return $this->program->appendStatToProjects($projects, 'hours,teamCount', array('hours' => $hours, 'teams' => $teams)); + return $this->program->appendStatToProjects($projects); } /** @@ -1066,44 +1047,22 @@ class productModel extends model if(empty($productIdList)) return array(); - /* Get stats data. */ - $products = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager); - $unclosedStory = $this->loadModel('story')->getUnClosedTotal(); - $finishClosedStory = $this->story->getFinishClosedTotal(); + $stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager); - $modules = array('plans', 'releases', 'latestReleases', 'bugs', 'unResolved', 'activeBugs', 'fixedBugs', 'closedBugs', 'thisWeekBugs', 'assignToNull'); - foreach($modules as $module) $$module = $this->productTao->getStatisticByType($productIdList, $module); - list($stories, $requirements) = $this->getStatsStoriesAndRequirements($productIdList, $storyType); + $latestReleases = $this->productTao->getStatisticByType($productIdList, 'latestReleases'); $executionCountPairs = $this->productTao->getExecutionCountPairs($productIdList); $coveragePairs = $this->getCaseCoveragePairs($productIdList); - $projectsPairs = $this->productTao->getProjectCountPairs($productIdList); - /* Render statistic result to each product. */ - $stats = array(); - foreach($products as $productID => $product) + foreach($stats as $productID => $product) { - foreach($modules as $field) $product->$field = zget($$field, $productID, 0); - $product->stories = zget($stories, $product->id, array()); - $product->stories['finishClosed'] = zget($finishClosedStory, $product->id, 0); - $product->stories['unclosed'] = zget($unclosedStory, $product->id, 0); - $product->activeStories = zget($product->stories, 'active', 0); - $product->requirements = zget($requirements, $product->id, array()); $product->executions = zget($executionCountPairs, $product->id, 0); $product->coverage = zget($coveragePairs, $product->id, 0); - $product->projects = zget($projectsPairs, $product->id, 0); $latestRelease = isset($latestReleases[$product->id]) ? $latestReleases[$product->id][0] : null; $product->latestRelease = $latestRelease ? $latestRelease->name : ''; $product->latestReleaseDate = $latestRelease ? $latestRelease->date : ''; - - /* Calculate product progress. */ - $closedTotal = $product->stories['closed'] + $product->requirements['closed']; - $allTotal = array_sum($product->stories) + array_sum($product->requirements); - $product->progress = empty($closedTotal) ? 0 : round($closedTotal / $allTotal * 100, 1); - - $stats[$productID] = $product; } return $stats; @@ -1494,35 +1453,14 @@ class productModel extends model */ public function formatDataForList(object $product, array $users): object { - $totalStories = $product->stories['closed'] + $product->stories['unclosed']; - $totalBugs = $product->unResolved + $product->fixedBugs; + $product->type = 'product'; + $product->productLine = $product->lineName; + $product->PO = !empty($product->PO) ? zget($users, $product->PO) : ''; + $product->testCaseCoverage = $product->coverage; + $product->storyCompleteRate = $totalStories == 0 ? 0 : round($product->finishedStories / $totalStories, 3) * 100; + $product->bugFixedRate = ($product->unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100; - $item = new stdClass(); - $item->type = 'product'; - $item->id = $product->id; - $item->name = $product->name; - $item->productLine = $product->lineName; - $item->PO = !empty($product->PO) ? zget($users, $product->PO) : ''; - $item->createdDate = $product->createdDate; - $item->createdBy = $product->createdBy; - $item->draftStories = $product->stories['draft']; - $item->activeStories = $product->stories['active']; - $item->changingStories = $product->stories['changing']; - $item->reviewingStories = $product->stories['reviewing']; - $item->totalStories = $totalStories; - $item->storyCompleteRate = ($totalStories == 0 ? 0 : round($product->stories['finishClosed'] / $totalStories, 3) * 100); - $item->plans = $product->plans; - $item->execution = $product->executions; - $item->testCaseCoverage = $product->coverage; - $item->totalActivatedBugs= $product->activeBugs; - $item->totalBugs = $product->bugs; - $item->bugFixedRate = ($totalBugs == 0 ? 0 : round($product->fixedBugs / $totalBugs, 3) * 100); - $item->releases = $product->releases; - $item->latestReleaseDate = $product->latestReleaseDate; - $item->latestRelease = $product->latestRelease; - if(isset($product->actions)) $item->actions = $product->actions; - - return $item; + return $product; } /** @@ -2172,5 +2110,4 @@ class productModel extends model /* 4. Clear actions older than 30 days. */ $this->loadModel('action')->cleanActions(); } - } diff --git a/module/product/tao.php b/module/product/tao.php index 187567e512..bd69eaaaa9 100644 --- a/module/product/tao.php +++ b/module/product/tao.php @@ -804,41 +804,10 @@ class productTao extends productModel $data = $type == 'program' ? $programStructure[$product->program] : $programStructure[$product->program][$product->line]; foreach($this->config->product->statisticFields as $key => $fields) { - /* Get the total number of requirements and stories. */ - if(strpos('stories|requirements', $key) !== false) - { - $totalObjects = 0; - foreach($product->$key as $status => $number) if(isset($this->lang->story->statusList[$status])) $totalObjects += $number; - - $fieldType = $key == 'stories' ? 'Stories' : 'Requirements'; - if(!isset($data['total' . $fieldType])) $data['total' . $fieldType] = 0; - $data['total' . $fieldType] += $totalObjects; - } - elseif($key == 'bugs') - { - $fieldType = 'Bugs'; - } - foreach($fields as $field) { if(!isset($data[$field])) $data[$field] = 0; - - $status = $field; - if(strpos($field, 'Requirements') !== false or strpos($field, 'Stories') !== false or $field == 'unResolvedBugs') - { - $length = strpos($field, $fieldType); - $status = substr($field, 0, $length); - } - - if(strpos('requirements|stories', $key) !== false) - { - $objects = $product->$key; - $data[$field] += $objects[$status]; - } - else - { - $data[$field] += $product->$status; - } + $data[$field] += $product->$field; } } diff --git a/module/product/zen.php b/module/product/zen.php index 9130e595d0..8118fd7b0f 100644 --- a/module/product/zen.php +++ b/module/product/zen.php @@ -434,15 +434,7 @@ class productZen extends product { $product->line = zget($lines, $product->line, ''); $product->manager = zget($users, $product->PO, ''); - $product->draftStories = (int)$product->stories['draft']; - $product->activeStories = (int)$product->stories['active']; - $product->changedStories = (int)$product->stories['changing']; - $product->reviewingStories = (int)$product->stories['reviewing']; - $product->closedStories = (int)$product->stories['closed']; - $product->totalStories = $product->activeStories + $product->changedStories + $product->draftStories + $product->closedStories + $product->reviewingStories; $product->storyCompleteRate = ($product->totalStories == 0 ? 0 : round($product->closedStories / $product->totalStories, 3) * 100) . '%'; - $product->unResolvedBugs = (int)$product->unResolved; - $product->assignToNullBugs = (int)$product->assignToNull; $product->bugFixedRate = (($product->unResolved + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unResolved + $product->fixedBugs), 3) * 100) . '%'; $product->program = $product->programName; } diff --git a/module/program/control.php b/module/program/control.php index b107735f5a..9119dc06c0 100644 --- a/module/program/control.php +++ b/module/program/control.php @@ -45,6 +45,8 @@ class program extends control $this->app->loadClass('pager', $static = true); $pager = new pager($recTotal, $recPerPage, $pageID); + $this->program->refreshStats(); // Refresh stats fields of projects. + $programs = $this->programZen->getProgramsByType($status, $orderBy, $param, $pager); $PMList = $this->programZen->getPMListByPrograms($programs); @@ -53,16 +55,15 @@ class program extends control $this->config->program->search['actionURL'] = $actionURL; $this->loadModel('search')->setSearchParams($this->config->program->search); - $this->view->title = $this->lang->program->browse; - $this->view->programs = $programs; - $this->view->status = $status; - $this->view->orderBy = $orderBy; - $this->view->pager = $pager; - $this->view->users = $this->loadModel('user')->getPairs('noletter'); - $this->view->usersAvatar = $this->user->getAvatarPairs(''); - $this->view->PMList = $PMList; - $this->view->progressList = $this->program->getProgressList(); - $this->view->param = $param; + $this->view->title = $this->lang->program->browse; + $this->view->programs = $programs; + $this->view->status = $status; + $this->view->orderBy = $orderBy; + $this->view->pager = $pager; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->usersAvatar = $this->user->getAvatarPairs(''); + $this->view->PMList = $PMList; + $this->view->param = $param; $this->display(); } diff --git a/module/program/model.php b/module/program/model.php index 6f1060b346..103846afe7 100644 --- a/module/program/model.php +++ b/module/program/model.php @@ -287,8 +287,8 @@ class programModel extends model $programs = $this->getTopPairs('noclosed'); /* Group data by product. */ - list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours) = $this->getKanbanStatisticData($programs); - $productGroup = $this->processProductsForKanban($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours); + list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions) = $this->getKanbanStatisticData($programs); + $productGroup = $this->processProductsForKanban($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions); /* Group data by program. */ $kanbanGroup = array(); @@ -359,20 +359,7 @@ class programModel extends model $executionPairs = array(); foreach($doingExecutions as $execution) $executionPairs[$execution->id] = $execution->id; - /* Compute executions and projects progress. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('(execution')->in($executionPairs) - ->orWhere('project')->in($projectIdList) - ->markRight(1) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('execution', 'id'); - - $hours = $this->computeProjectHours($tasks); - $projectHours = $this->getProgressList(); - - return array($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours); + return array($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions); } /** @@ -384,12 +371,10 @@ class programModel extends model * @param array $releaseGroup * @param array $projectGroup * @param array $doingExecutions - * @param array $hours - * @param array $projectHours * @access public * @return array */ - public function processProductsForKanban(array $productGroup, array $planGroup, array $releaseGroup, array $projectGroup, array $doingExecutions, array $hours, array $projectHours): array + public function processProductsForKanban(array $productGroup, array $planGroup, array $releaseGroup, array $projectGroup, array $doingExecutions): array { if(empty($productGroup)) return $productGroup; @@ -414,14 +399,10 @@ class programModel extends model $status = $project->status == 'wait' ? 'wait' : 'doing'; $execution = zget($doingExecutions, $project->id, array()); - if(!empty($execution)) - { - $execution->hours = zget($hours, $execution->id, array()); - if(helper::diffDate($today, $execution->end) > 0) $execution->delay = 1; - } + if(!empty($execution) && helper::diffDate($today, $execution->end) > 0) $execution->delay = 1; $project->execution = $execution; - $project->hours['progress'] = zget($projectHours, $project->id, array()); + $project->hours = array('progress' => $project->progress); /* Convert predefined HTML entities to characters. */ $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES); @@ -527,67 +508,18 @@ class programModel extends model } /** - * 根据按照项目分组的任务,统计各个项目的工时和进度 - * Compute hours and progress for project or execution. - * - * @param array $tasks - * @access public - * @return array - */ - public function computeProjectHours(array $tasks): array - { - if(empty($tasks)) return array(); - - $hours = array(); - foreach($tasks as $projectID => $projectTasks) - { - /* Init hour. */ - $hour = new stdclass(); - $hour->totalConsumed = 0; - $hour->totalEstimate = 0; - $hour->totalLeft = 0; - - /* Compute totalEstimate, totalConsumed, totalLeft to them. */ - foreach($projectTasks as $task) - { - $hour->totalConsumed += $task->consumed; - $hour->totalEstimate += $task->estimate; - if(strpos('|cancel|closed|done|', "|{$task->status}|") === false) $hour->totalLeft += (float)$task->left; - } - $hours[$projectID] = $hour; - } - - /* 计算进度。四舍五入totalEstimate, totalConsumed, totalLeft. */ - $progressList = $this->loadModel('project')->getWaterfallProgress(array_keys($hours)); - foreach($hours as $projectID => $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 = zget($progressList, $projectID, ($hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0)) ; - } - - return $hours; - } - - /** - * 将工时、团队人数、剩余任务数、团队成员等统计信息,追加到对应的项目中。 + * 将团队成员等统计信息,追加到对应的项目中。 * Append statistics fields to projects. * * @param array $projects - * @param string $appendFields hours,teamCount,leftTasks,teamMembers - * @param array $data array keys are hours, teams and leftTasks. + * @param array $teams array keys are hours, teams and leftTasks. * @access public * @return array */ - public function appendStatToProjects(array $projects, string $appendFields = '', array $data = array()): array + public function appendStatToProjects(array $projects, array $teams = array()): array { if(empty($projects)) return array(); - if(empty($appendFields) || empty($data)) return $projects; - $appendFields = explode(',', $appendFields); - $emptyHour = json_decode(json_encode(array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0))); /* Process projects. */ $stats = array(); foreach($projects as $projectID => $project) @@ -601,20 +533,16 @@ class programModel extends model if($delay > 0) $project->delay = $delay; } - /* Merge the hours. */ - if(in_array('hours', $appendFields)) - { - $project->hours = $emptyHour; - if(isset($data['hours'])) $project->hours = zget($data['hours'], $project->id, $emptyHour); - } - - /* Merge the team and left tasks. */ - if(in_array('teamCount', $appendFields)) $project->teamCount = isset($data['teams'][$project->id]) ? count($data['teams'][$project->id]) : 0; - if(in_array('teamMembers', $appendFields)) $project->teamMembers = isset($data['teams'][$project->id]) ? array_keys($data['teams'][$project->id]) : array(); - if(in_array('leftTasks', $appendFields)) $project->leftTasks = isset($data['leftTasks'][$project->id]) ? $data['leftTasks'][$project->id]->tasks : '—'; + /* Merge project team. */ + if(!empty($teams)) + { + $project->teamCount = isset($teams[$project->id]) ? count($teams[$project->id]) : 0; + $project->teamMembers = isset($teams[$project->id]) ? array_keys($teams[$project->id]) : array(); + } $stats[$projectID] = $project; } + return $stats; } @@ -728,59 +656,6 @@ class programModel extends model ->fetchAll(); } - /** - * 计算项目和项目集的进度。 - * Get program and project progress list. - * - * @access public - * @return array - */ - public function getProgressList(): array - { - $totalProgress = array(); - $projectCount = array(); - $userPRJCount = array(); - $progressList = array(); - $programPairs = $this->getPairs(); - $projectStats = $this->getProjectStats(0, 'all', 0, 'id_desc', '', true); - - /* Add program progress. */ - foreach(array_keys($programPairs) as $programID) - { - $totalProgress[$programID] = 0; - $projectCount[$programID] = 0; - $userPRJCount[$programID] = 0; - $progressList[$programID] = 0; - - foreach($projectStats as $project) - { - if(strpos($project->path, ',' . $programID . ',') === false) continue; - - /* The number of projects under this program that the user can view. */ - if(strpos(',' . $this->app->user->view->projects . ',', ',' . $project->id . ',') !== false) $userPRJCount[$programID] ++; - - $totalProgress[$programID] += $project->hours->progress; - $projectCount[$programID] ++; - } - - if(empty($projectCount[$programID])) continue; - - /* Program progress can't see when this user don't have all projects priv. */ - if(!$this->app->user->admin && $userPRJCount[$programID] != $projectCount[$programID]) - { - unset($progressList[$programID]); - continue; - } - - $progressList[$programID] = round($totalProgress[$programID] / $projectCount[$programID]); - } - - /* Add project progress. */ - foreach($projectStats as $project) $progressList[$project->id] = $project->hours->progress; - - return $progressList; - } - /** * 创建项目集。 * Create a program. @@ -1324,44 +1199,10 @@ class programModel extends model { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProjectStats($browseType); - /* Init vars. */ $projects = $this->getProjectList($programID, $browseType, $queryID, $orderBy, $programTitle, $queryAll, $pager); if(empty($projects)) return array(); - $projectKeys = array_keys($projects); - $executions = $this->loadModel('project')->getExecutionList($projectKeys); - - /* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */ - $tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution') - ->from(TABLE_TASK) - ->where('project')->in($projectKeys) - ->andWhere('execution')->in(array_keys($executions)) - ->andWhere('parent')->lt(1) - ->andWhere('deleted')->eq(0) - ->fetchGroup('project', 'id'); - $hours = $this->computeProjectHours($tasks); - - /* Get the number of left tasks. */ - $leftTasks = array(); - if($this->cookie->projectType && $this->cookie->projectType == 'bycard') - { - $leftTasks = $this->dao->select('t2.parent as project, count(*) as tasks')->from(TABLE_TASK)->alias('t1') - ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id') - ->where('t1.execution')->in(array_keys($executions)) - ->andWhere('t1.status')->notIn('cancel,closed') - ->groupBy('t2.parent') - ->fetchAll('project'); - } - - /* Get the members of project teams. */ - $teamMembers = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1') - ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') - ->where('t1.root')->in($projectKeys) - ->andWhere('t1.type')->eq('project') - ->andWhere('t2.deleted')->eq(0) - ->fetchGroup('root', 'account'); - - $stats = $this->appendStatToProjects($projects, 'hours,teamCount,teamMembers,leftTasks', array('hours' => $hours, 'teams' => $teamMembers, 'leftTasks' => $leftTasks)); + $stats = $this->appendStatToProjects($projects); foreach($stats as $project) $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES); // Convert predefined HTML entities to characters. return $stats; @@ -1570,13 +1411,30 @@ class programModel extends model } /* 1. Get summary and members of executions to be refreshed. */ - $summary = $this->dao->select('execution, SUM(t1.estimate) AS totalEstimate, SUM(t1.consumed) AS totalConsumed, SUM(t1.`left`) AS totalLeft')->from(TABLE_TASK)->alias('t1') + $tasks = $this->dao->select('t1.id, execution, t1.estimate, t1.consumed, t1.`left`, t1.status')->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.deleted')->eq(0) - ->andWhere('t1.parent')->le(0) // Ignore child task. + ->andWhere('t1.parent')->ge(0) // Ignore parent task. ->beginIF(!empty($projects))->andWhere('t1.project')->in(array_keys($projects))->fi() - ->groupBy('execution') - ->fetchAll(); + ->fetchAll('id'); + + $summary = array(); + foreach($tasks as $task) + { + if(empty($task->execution)) continue; + if(!isset($summary[$task->execution])) + { + $summary[$task->execution] = new stdclass(); + $summary[$task->execution]->totalEstimate = 0; + $summary[$task->execution]->totalConsumed = 0; + $summary[$task->execution]->totalLeft = 0; + } + + $summary[$task->execution]->execution = $task->execution; + $summary[$task->execution]->totalEstimate += $task->estimate; + $summary[$task->execution]->totalConsumed += $task->consumed; + $summary[$task->execution]->totalLeft += ($task->status == 'closed' or $task->status == 'cancel') ? 0 : $task->left; + } $teamMembers = $this->dao->select('t1.root, COUNT(1) AS members')->from(TABLE_TEAM)->alias('t1') ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account') @@ -1606,6 +1464,8 @@ class programModel extends model $stats[$nodeID]['totalEstimate'] += $execution->totalEstimate; $stats[$nodeID]['totalConsumed'] += $execution->totalConsumed; $stats[$nodeID]['totalLeft'] += $execution->totalLeft; + + // Check $execution->execution and $nodeID(path) is not deleted. if(empty($projectsPairs[$execution->execution]) && empty($projectsPairs[$nodeID])) { $stats[$nodeID]['totalConsumedNotDel'] += $execution->totalConsumed; @@ -1614,22 +1474,6 @@ class programModel extends model } } - $this->loadModel('project'); - foreach($projects as $projectID => $project) - { - if($project->model != 'waterfall') continue; - - $projectStats = $this->project->getWaterfallPVEVAC($projectID); - $stats[$projectID]['totalEstimate'] = $projectStats['PV']; - $stats[$projectID]['totalConsumed'] = $projectStats['AC']; - $stats[$projectID]['totalLeft'] = $projectStats['left']; - if(empty($project->deleted)) - { - $stats[$projectID]['totalConsumedNotDel'] = $projectStats['AC']; - $stats[$projectID]['totalLeftNotDel'] = $projectStats['left']; - } - } - foreach($teamMembers as $projectID => $teamCount) { if(!isset($stats[$projectID])) $stats[$projectID] = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'teamCount' => 0, 'totalConsumedNotDel' => 0, 'totalLeftNotDel' => 0); @@ -1645,14 +1489,14 @@ class programModel extends model ->set('progress')->eq($progress) ->set('teamCount')->eq($project['teamCount']) ->set('estimate')->eq($project['totalEstimate']) - ->set('consumed')->eq($project['totalConsumed']) - ->set('left')->eq($project['totalLeft']) + ->set('consumed')->eq($project['totalConsumedNotDel']) + ->set('left')->eq($project['totalLeftNotDel']) ->where('id')->eq($projectID) ->exec(); } /* 5. Update programStatsTime. */ - $projectList = $this->dao->select('id,progress,path')->from(TABLE_PROJECT) + $projectList = $this->dao->select('id,progress,path,consumed,`left`')->from(TABLE_PROJECT) ->where('type')->eq('project') ->andWhere('parent')->ne(0) ->andWhere('deleted')->eq(0) @@ -1665,19 +1509,18 @@ class programModel extends model foreach($path as $programID) { if($programID == $projectID) continue; - $programProgress[$programID][] = $project->progress; + + if(!isset($programProgress[$programID])) $programProgress[$programID] = array('consumed' => 0, 'left' => 0); + $programProgress[$programID]['consumed'] += $project->consumed; + $programProgress[$programID]['left'] += $project->left; } } - foreach($programProgress as $programID => $progress) + foreach($programProgress as $programID => $hours) { - $count = count($progress); - $sum = array_sum($progress); - $average = round($sum / $count, 2); - $this->dao->update(TABLE_PROJECT) - ->set('progress')->eq($average) - ->where('id')->eq($programID) - ->exec(); + $progress = ($hours['consumed'] + $hours['left']) ? floor($hours['consumed'] / ($hours['consumed'] + $hours['left']) * 1000) / 1000 * 100 : 0; + + $this->dao->update(TABLE_PROJECT)->set('progress')->eq($progress)->where('id')->eq($programID)->exec(); } /* 6. Update projectStatsTime in config. */ diff --git a/module/program/ui/browse.html.php b/module/program/ui/browse.html.php index 57bedc53dd..6da58bc160 100644 --- a/module/program/ui/browse.html.php +++ b/module/program/ui/browse.html.php @@ -51,9 +51,7 @@ foreach($programs as $program) $program->budget = !empty($program->budget) ? zget($lang->project->currencySymbol, $program->budgetUnit) . ' ' . $programBudget : $lang->project->future; $parents[$program->parent][] = $program->id; - /* Progress. */ - if(isset($progressList[$program->id])) $program->progress = round($progressList[$program->id]); - + $program->progress = round($program->progress); $program->isParent = false; if($program->parent > 0 and isset($programs[$program->parent])) $programs[$program->parent]->isParent = true; if($program->parent > 0 and !isset($programs[$program->parent])) @@ -78,7 +76,8 @@ foreach($programs as $program) } /* Set invested hours. */ - if(!isset($program->invested)) $program->invested = 0; + $program->invested = !empty($this->config->execution->defaultWorkhours) ? round($program->consumed / $this->config->execution->defaultWorkhours, 2) : 0; + if(!is_null($program->end) && str_contains($program->end, LONG_TIME)) $program->end = $lang->program->longTime; /* Actions. */ diff --git a/module/project/control.php b/module/project/control.php index 794f616a09..5cfa4f6f53 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -323,8 +323,10 @@ class project extends control $actionURL = $this->createLink('project', 'browse', "&programID=$programID&browseType=bySearch&queryID=myQueryID"); $this->project->buildSearchForm($queryID, $actionURL); + $this->loadModel('program')->refreshStats(); // Refresh stats fields of projects. + $programTitle = $this->loadModel('setting')->getItem("owner={$this->app->user->account}&module=project&key=programTitle"); - $projectStats = $this->loadModel('program')->getProjectStats($programID, $browseType, $queryID, $orderBy, $programTitle, false, $pager); + $projectStats = $this->program->getProjectStats($programID, $browseType, $queryID, $orderBy, $programTitle, false, $pager); $this->view->title = $this->lang->project->browse; $this->view->projectStats = $this->projectZen->processProjectListData($projectStats); diff --git a/module/project/js/browse.ui.js b/module/project/js/browse.ui.js index 1e322cc19a..de2727dd3c 100644 --- a/module/project/js/browse.ui.js +++ b/module/project/js/browse.ui.js @@ -3,7 +3,7 @@ window.programMenuOnClick = function(data, url) location.href = url.replace('%d', data.item.key); } -window.renderReleaseCountCell = function(result, {col, row}) +window.renderCell = function(result, {col, row}) { if(col.name === 'name') { diff --git a/module/project/model.php b/module/project/model.php index 97fe03f0f2..bf3b8e635b 100755 --- a/module/project/model.php +++ b/module/project/model.php @@ -244,7 +244,7 @@ class projectModel extends model * @access public * @return object[] */ - public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 15, string $excludedModel = ''): array + public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 10, string $excludedModel = ''): array { /* Get project list by query. */ $projects = $this->projectTao->fetchProjectListByQuery($status, $projectID, $orderBy, $limit, $excludedModel); @@ -256,24 +256,15 @@ class projectModel extends model $projects = array($projectID => $projects[$projectID]); } - /* Get team members under the project. */ - $projectIdList = array_keys($projects); - $teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList); - - /* Get all consumed and all estimate under the project. */ - $hours = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, 'consumed,estimate'); - /* Get bug, task and story summary under the project. */ - $bugSummary = $this->projectTao->getTotalBugByProject($projectIdList); - $taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList); - $storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList); + $projectIdList = array_keys($projects); + $bugSummary = $this->projectTao->getTotalBugByProject($projectIdList); + $taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList); + $storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList); /* Set project attribute. */ foreach($projects as $projectID => $project) { - $project->teamCount = zget($teamCount, $projectID, 0); - $project->consumed = isset($hours[$projectID]) ? round((float)$hours[$projectID]->consumed, 1) : 0; - $project->estimate = isset($hours[$projectID]) ? round((float)$hours[$projectID]->estimate, 1) : 0; $project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0; $project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0; $project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0; @@ -2495,13 +2486,13 @@ class projectModel extends model $project->budget = $project->budget != 0 ? zget($this->lang->project->currencySymbol, $project->budgetUnit) . ' ' . $projectBudget : $this->lang->project->future; $project->statusTitle = $this->processStatus('project', $project); - $project->estimate = $project->hours->totalEstimate . $this->lang->project->workHourUnit; - $project->consume = $project->hours->totalConsumed . $this->lang->project->workHourUnit; - $project->surplus = $project->hours->totalLeft . $this->lang->project->workHourUnit; - $project->progress = $project->hours->progress; + $project->estimate = $project->estimate . $this->lang->project->workHourUnit; + $project->consume = $project->consumed . $this->lang->project->workHourUnit; + $project->surplus = $project->left . $this->lang->project->workHourUnit; + $project->progress = $project->progress; $project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end; $project->hasProduct = zget($this->lang->project->projectTypeList, $project->hasProduct); - $project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->hours->totalConsumed / $this->config->execution->defaultWorkhours, 2) : 0; + $project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->consumed / $this->config->execution->defaultWorkhours, 2) : 0; if($project->PM) { diff --git a/module/project/ui/browsebylist.html.php b/module/project/ui/browsebylist.html.php index 61fca6eaf9..7183e97987 100644 --- a/module/project/ui/browsebylist.html.php +++ b/module/project/ui/browsebylist.html.php @@ -10,6 +10,9 @@ declare(strict_types=1); */ namespace zin; +jsVar('langManDay', $lang->program->manDay); +jsVar('langPostponed', $lang->project->statusList['delay']); + /* zin: Define the feature bar on main menu. */ featureBar ( @@ -108,6 +111,7 @@ dtable set::sortLink(createLink('project', 'browse', "programID=$programID&browseType=$browseType¶m=$param&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")), set::footPager(usePager()), set::customCols(true), + set::onRenderCell(jsRaw('window.renderCell')), set::emptyTip($lang->project->empty), set::createTip($lang->project->create), set::createLink(hasPriv('project', 'create') ? createLink('project', 'createGuide') : ''),