diff --git a/db/update18.4.beta1.sql b/db/update18.4.beta1.sql index 9f4584d725..f47a7dcae0 100644 --- a/db/update18.4.beta1.sql +++ b/db/update18.4.beta1.sql @@ -1 +1,16 @@ INSERT INTO `zt_cron` (`m`, `h`, `dom`, `mon`, `dow`, `command`, `remark`, `type`, `buildin`, `status`) VALUES ('0','*','*','*','*','moduleName=misc&methodName=cleanCache', '清理缓存文件','zentao', 1, 'normal'); + +ALTER TABLE `zt_product` +ADD `draftStories` mediumint(8) NOT NULL DEFAULT '0' AFTER `reviewer`, +ADD `activeStories` mediumint(8) NOT NULL DEFAULT '0' AFTER `draftStories`, +ADD `changingStories` mediumint(8) NOT NULL DEFAULT '0' AFTER `activeStories`, +ADD `reviewingStories` mediumint(8) NOT NULL DEFAULT '0' AFTER `changingStories`, +ADD `finishedStories` mediumint NOT NULL DEFAULT '0' AFTER `reviewingStories`, +ADD `closedStories` mediumint(8) NOT NULL DEFAULT '0' AFTER `reviewingStories`, +ADD `totalStories` mediumint(8) NOT NULL DEFAULT '0' AFTER `closedStories`, +ADD `unresolvedBugs` mediumint(8) NOT NULL DEFAULT '0' AFTER `totalStories`, +ADD `closedBugs` mediumint(8) NOT NULL DEFAULT '0' AFTER `unresolvedBugs`, +ADD `fixedBugs` mediumint(8) NOT NULL DEFAULT '0' AFTER `closedBugs`, +ADD `totalBugs` mediumint(8) NOT NULL DEFAULT '0' AFTER `fixedBugs`, +ADD `plans` mediumint(8) NOT NULL DEFAULT '0' AFTER `totalBugs`, +ADD `releases` mediumint(8) NOT NULL DEFAULT '0' AFTER `plans`; diff --git a/module/product/config.php b/module/product/config.php index 119ba58aa0..d1c266f803 100644 --- a/module/product/config.php +++ b/module/product/config.php @@ -156,8 +156,8 @@ $config->product->report->planLabels = array(); $config->product->report->planLabels[] = ''; $config->product->statisticFields = array(); -$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/control.php b/module/product/control.php index bb34cc6864..1c605d327e 100755 --- a/module/product/control.php +++ b/module/product/control.php @@ -1295,6 +1295,8 @@ class product extends control $this->app->loadClass('pager', $static = true); $pager = new pager($recTotal, $recPerPage, $pageID); + $this->product->refreshStats(); // Refresh stats fields of products. + /* Process product structure. */ if($this->config->systemMode == 'light' and $orderBy == 'program_asc') $orderBy = 'order_asc'; $productStats = $this->product->getStats($orderBy, $pager, $browseType, '', 'story', '', $queryID); diff --git a/module/product/model.php b/module/product/model.php index 99f4fc6ef1..c4f17ac1d5 100755 --- a/module/product/model.php +++ b/module/product/model.php @@ -1769,6 +1769,131 @@ class productModel extends model return $product; } + /** + * Refresh stats info of products. + * + * @access public + * @return void + */ + public function refreshStats() + { + $updateTime = zget($this->app->config->global, 'productStatsTime', ''); + $now = helper::now(); + + /* + * If productStatsTime is before two weeks ago, refresh all products directly. + * Else only refresh the latest products in action table. + */ + $productActions = array(); + if($updateTime > date('Y-m-d', strtotime('-14 days'))) + { + $productActions = $this->dao->select('distinct product')->from(TABLE_ACTION) + ->where('date')->ge($updateTime) + ->andWhere('product')->notin(array(',0,', ',,')) + ->fetchPairs('product'); + if(empty($productActions)) return; + } + + $products = array(); + foreach($productActions as $productAction) + { + foreach(explode(',', trim($productAction, ',')) as $product) + { + $products[$product] = $product; + } + } + + /* 1. Get summary and members of products to be refreshed. */ + $stories = $this->dao->select('product, status, `closedReason`, count(id) AS c') + ->from(TABLE_STORY) + ->where('deleted')->eq(0) + ->beginIF(!empty($products))->andWhere('product')->in($products)->fi() + ->andWhere('type')->eq('story') + ->groupBy('product, status, `closedReason`') + ->fetchAll(); + $productStories = array(); + foreach($stories as $story) + { + if(!isset($productStories[$story->product])) $productStories[$story->product] = array('draft' => 0, 'active' => 0, 'changing' => 0, 'reviewing' => 0, 'finished' => 0, 'closed' => 0, 'total' => 0); + if($story->status == 'draft') $productStories[$story->product]['draft'] += $story->c; + if($story->status == 'active') $productStories[$story->product]['active'] += $story->c; + if($story->status == 'changing') $productStories[$story->product]['changing'] += $story->c; + if($story->status == 'reviewing') $productStories[$story->product]['reviewing'] += $story->c; + if($story->status == 'closed') $productStories[$story->product]['closed'] += $story->c; + $productStories[$story->product]['total'] += $story->c; + + if($story->status == 'closed' && $story->closedReason == 'done') $productStories[$story->product]['finished'] += $story->c; + } + + $bugs = $this->dao->select('product,status,resolution, count(id) AS c') + ->from(TABLE_BUG) + ->where('deleted')->eq(0) + ->beginIF(!empty($products))->andWhere('product')->in($products)->fi() + ->groupBy('product') + ->fetchAll(); + $productBugs = array(); + foreach($bugs as $bug) + { + if(!isset($productBugs[$bug->product])) $productBugs[$bug->product] = array('unresolved' => 0, 'fixed' => 0, 'closed' => 0, 'total' => 0); + if($bug->status == 'active' or $bug->resolution == 'postponed') $productBugs[$bug->product]['unresolved'] += $bug->c; + if($bug->status == 'closed' and $bug->resolution == 'fixed') $productBugs[$bug->product]['fixed'] += $bug->c; + if($bug->status == 'closed') $productBugs[$bug->product]['closed'] += $bug->c; + $productBugs[$bug->product]['total'] += $bug->c; + } + + $plans = $this->dao->select('product, count(id) AS c') + ->from(TABLE_PRODUCTPLAN) + ->where('deleted')->eq(0) + ->beginIF(!empty($products))->andWhere('product')->in($products)->fi() + ->andWhere('end')->gt(helper::now()) + ->groupBy('product') + ->fetchPairs(); + + $releases = $this->dao->select('product, count(id) AS c') + ->from(TABLE_RELEASE) + ->where('deleted')->eq(0) + ->beginIF(!empty($products))->andWhere('product')->in($products)->fi() + ->groupBy('product') + ->fetchPairs(); + + /* If products is empty, get all products. */ + if(empty($products)) $products = $this->dao->select('id')->from(TABLE_PRODUCT)->where('deleted')->eq(0)->fetchPairs(); + + $stats = array(); + foreach($products as $productID) + { + $product = new stdclass(); + + $product->draftStories = isset($productStories[$productID]) ? $productStories[$productID]['draft'] : 0; + $product->activeStories = isset($productStories[$productID]) ? $productStories[$productID]['active'] : 0; + $product->changingStories = isset($productStories[$productID]) ? $productStories[$productID]['changing'] : 0; + $product->reviewingStories = isset($productStories[$productID]) ? $productStories[$productID]['reviewing'] : 0; + $product->closedStories = isset($productStories[$productID]) ? $productStories[$productID]['closed'] : 0; + $product->finishedStories = isset($productStories[$productID]) ? $productStories[$productID]['finished'] : 0; + $product->totalStories = isset($productStories[$productID]) ? $productStories[$productID]['total'] : 0; + + $product->unresolvedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['unresolved'] : 0; + $product->closedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['closed'] : 0; + $product->fixedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['fixed'] : 0; + $product->totalBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['total'] : 0; + + $product->plans = isset($plans[$productID]) ? $plans[$productID] : 0; + $product->releases = isset($releases[$productID]) ? $releases[$productID] : 0; + + $stats[$productID] = $product; + } + + /* 2. Refresh stats to db. */ + foreach($stats as $productID=> $product) + { + $this->dao->update(TABLE_PRODUCT)->data($product)->where('id')->eq($productID)->exec(); + } + + /* 3. Update projectStatsTime in config. */ + $this->loadModel('setting')->setItem('system.common.global.productStatsTime', $now); + $this->app->config->global->productStatsTime = $now; + } + /** * Get product stats. * @@ -1831,134 +1956,7 @@ class productModel extends model } } - $stories = $this->dao->select('product, status, count(id) AS count') - ->from(TABLE_STORY) - ->where('deleted')->eq(0) - ->andWhere('type')->eq('story') - ->andWhere('product')->in($productKeys) - ->groupBy('product, status') - ->fetchGroup('product', 'status'); - - /* Padding the stories to sure all products have records. */ - foreach($productKeys as $productID) if(!isset($stories[$productID])) $stories[$productID] = array(); - - /* Padding the stories to sure all status have records. */ - foreach($stories as $key => $story) - { - foreach(array_keys($this->lang->story->statusList) as $status) - { - $story[$status] = isset($story[$status]) ? $story[$status]->count : 0; - } - $stories[$key] = $story; - } - - $finishClosedStory = $this->dao->select('product, count(id) as finish')->from(TABLE_STORY) - ->where('deleted')->eq(0) - ->andWhere('status')->eq('closed') - ->andWhere('type')->eq('story') - ->andWhere('closedReason')->eq('done') - ->groupBy('product') - ->fetchPairs(); - - $unclosedStory = $this->dao->select('product, count(id) as unclosed')->from(TABLE_STORY) - ->where('deleted')->eq(0) - ->andWhere('type')->eq('story') - ->andWhere('status')->ne('closed') - ->groupBy('product') - ->fetchPairs(); - - $plans = $this->dao->select('product, count(id) AS count') - ->from(TABLE_PRODUCTPLAN) - ->where('deleted')->eq(0) - ->andWhere('product')->in($productKeys) - ->andWhere('end')->gt(helper::now()) - ->groupBy('product') - ->fetchPairs(); - - $releases = $this->dao->select('product, count(id) AS count') - ->from(TABLE_RELEASE) - ->where('deleted')->eq(0) - ->andWhere('product')->in($productKeys) - ->groupBy('product') - ->fetchPairs(); - - $bugs = $this->dao->select('product,count(id) AS conut') - ->from(TABLE_BUG) - ->where('product')->in($productKeys) - ->andWhere('deleted')->eq(0) - ->groupBy('product') - ->fetchPairs(); - - $unResolved = $this->dao->select('product,count(id) AS count') - ->from(TABLE_BUG) - ->where('status')->eq('active') - ->orWhere('resolution')->eq('postponed') - ->andWhere('product')->in($productKeys) - ->andWhere('deleted')->eq(0) - ->groupBy('product') - ->fetchPairs(); - - $fixedBugs = $this->dao->select('product,count(id) AS count') - ->from(TABLE_BUG) - ->where('status')->eq('closed') - ->andWhere('product')->in($productKeys) - ->andWhere('deleted')->eq(0) - ->andWhere('resolution')->eq('fixed') - ->groupBy('product') - ->fetchPairs(); - - $closedBugs = $this->dao->select('product,count(id) AS count') - ->from(TABLE_BUG) - ->where('status')->eq('closed') - ->andWhere('product')->in($productKeys) - ->andWhere('deleted')->eq(0) - ->groupBy('product') - ->fetchPairs(); - - $this->app->loadClass('date', true); - $weekDate = date::getThisWeek(); - $thisWeekBugs = $this->dao->select('product,count(id) AS count') - ->from(TABLE_BUG) - ->where('openedDate')->between($weekDate['begin'], $weekDate['end']) - ->andWhere('product')->in($productKeys) - ->andWhere('deleted')->eq(0) - ->groupBy('product') - ->fetchPairs(); - - $assignToNull = $this->dao->select('product,count(id) AS count') - ->from(TABLE_BUG) - ->where('assignedTo')->eq('') - ->andWhere('product')->in($productKeys) - ->andWhere('deleted')->eq(0) - ->groupBy('product') - ->fetchPairs(); - - $stats = array(); - foreach($products as $key => $product) - { - $product->stories = $stories[$product->id]; - $product->stories['finishClosed'] = isset($finishClosedStory[$product->id]) ? $finishClosedStory[$product->id] : 0; - $product->stories['unclosed'] = isset($unclosedStory[$product->id]) ? $unclosedStory[$product->id] : 0; - $product->stories['totalStories'] = $product->stories['unclosed'] + $product->stories['closed']; - - $product->plans = isset($plans[$product->id]) ? $plans[$product->id] : 0; - $product->releases = isset($releases[$product->id]) ? $releases[$product->id] : 0; - - $product->bugs = isset($bugs[$product->id]) ? $bugs[$product->id] : 0; - $product->unResolved = isset($unResolved[$product->id]) ? $unResolved[$product->id] : 0; - $product->closedBugs = isset($closedBugs[$product->id]) ? $closedBugs[$product->id] : 0; - $product->fixedBugs = isset($fixedBugs[$product->id]) ? $fixedBugs[$product->id] : 0; - $product->thisWeekBugs = isset($thisWeekBugs[$product->id]) ? $thisWeekBugs[$product->id] : 0; - $product->assignToNull = isset($assignToNull[$product->id]) ? $assignToNull[$product->id] : 0; - - $closedTotal = $product->stories['closed']; - $allTotal = array_sum($product->stories); - $product->progress = empty($closedTotal) ? 0 : round($closedTotal / $allTotal * 100, 1); - - $stats[$key] = $product; - } - - return $stats; + return $products; } /** @@ -2194,44 +2192,10 @@ class productModel extends model $data = $type == 'program' ? $productStructure[$product->program] : $productStructure[$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; } } return $data; diff --git a/module/product/view/all.html.php b/module/product/view/all.html.php index 817ff60e84..1bf4a755ae 100644 --- a/module/product/view/all.html.php +++ b/module/product/view/all.html.php @@ -129,9 +129,9 @@ - % - - % + % + + % ";?> @@ -170,9 +170,9 @@ - % - - % + % + + % ";?> @@ -184,7 +184,7 @@ $product):?> stories['totalStories']; + $totalStories = $product->totalStories; $trClass = ''; if($product->line and $this->config->systemMode == 'ALM') @@ -229,13 +229,13 @@ } ?> - stories['draft'];?> - stories['active'];?> - stories['changing'];?> - stories['reviewing'];?> - stories['finishClosed'] / $totalStories, 3) * 100;?>% - unResolved;?> - unResolved + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unResolved + $product->fixedBugs), 3) * 100;?>% + draftStories;?> + activeStories;?> + changingStories;?> + reviewingStories;?> + finishedStories / $totalStories, 3) * 100;?>% + unresolvedBugs;?> + unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100;?>% plans;?> releases;?> " . $this->loadModel('flow')->getFieldValue($extendField, $product) . "";?> diff --git a/module/program/model.php b/module/program/model.php index 7c6e93c5cf..fb2a74b526 100644 --- a/module/program/model.php +++ b/module/program/model.php @@ -629,55 +629,6 @@ class programModel extends model ->fetchAll(); } - /** - * Get program and project progress list. - * - * @param array $programIdList - * @access public - * @return array - */ - public function getProgressList($programIdList = array()) - { - $programPairs = $this->getPairs(); - $existProgramIdList = array_intersect($programIdList, array_keys($programPairs)); - $projectStats = $this->getProjectStats(0, 'all', 0, 'id_desc', null, 0, 0, true); - - $totalProgress = array(); - $projectCount = array(); - $userPRJCount = array(); - $progressList = array(); - - foreach($existProgramIdList as $programID) - { - $totalProgress[$programID] = 0; - $projectCount[$programID] = 0; - $userPRJCount[$programID] = 0; - $progressList[$programID] = 0; - - foreach($projectStats as $projectID => $project) - { - if(strpos($projectStats[$projectID]->path, ",{$programID},") === false) continue; - - /* The number of projects under this program that the user can view. */ - if(strpos(",{$this->app->user->view->projects},", ",{$projectID},") !== false) $userPRJCount[$programID] ++; - - $totalProgress[$programID] += $projectStats[$projectID]->progress; - $projectCount[$programID] ++; - } - - if(empty($projectCount[$programID])) continue; - - $progressList[$programID] = round($totalProgress[$programID] / $projectCount[$programID]); - } - - /* Add project progress. */ - /* First calculate the overall progress of the program, and then filter the unqualified projects. */ - $existProjectIdList = array_intersect($programIdList, array_keys($projectStats)); - foreach($existProjectIdList as $projectID) $progressList[$projectID] = $projectStats[$projectID]->progress; - - return $progressList; - } - /** * Create a program. *