Merge remote-tracking branch 'origin/master' into Merge184

This commit is contained in:
tanghucheng
2023-11-21 19:43:29 -05:00
149 changed files with 1597 additions and 1842 deletions
+9 -9
View File
@@ -55,15 +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->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();
}
+51 -208
View File
@@ -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. */
@@ -1,32 +0,0 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
zdTable('user')->gen(5);
su('admin');
zdTable('project')->gen(50);
$task = zdTable('task');
$task->project->range('1-20');
$task->gen(100);
/**
title=测试 programModel::computeProjectHours();
timeout=0
cid=1
*/
global $tester;
$tester->loadModel('program');
$tasks = $tester->program->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('project')->eq('11')
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
r($tester->program->computeProjectHours(array())) && p() && e('0');
r($tester->program->computeProjectHours($tasks)) && p('11:totalConsumed,totalEstimate,totalLeft,progress') && e('15,30,10,60');
@@ -1,49 +0,0 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/program.class.php';
su('admin');
$program = zdTable('project');
$program->id->range('1-8');
$program->name->setFields(array(
array('field' => 'name1', 'range' => '项目集{2},项目{3},迭代{3}'),
array('field' => 'name2', 'range' => '1-8')
));
$program->type->range('program{2},project{3},sprint{3}');
$program->status->range('doing');
$program->parent->range('0,0,1,1,2,3,4,5');
$program->grade->range('1{2},2{3},1{3}');
$program->project->range('0{5},3-5');
$program->path->range('1,2,`1,3`,`1,4`,`2,5`,`3,6`,`4,7`,`5,8`')->prefix(',')->postfix(',');
$program->begin->range('20220112 000000:0')->type('timestamp')->format('YYYY-MM-DD');
$program->end->range('20230212 000000:0')->type('timestamp')->format('YYYY-MM-DD');
$program->gen(8);
$task = zdTable('task');
$task->name->prefix('任务')->range('1-3');
$task->project->range('3-5');
$task->execution->range('6-8');
$task->estimate->range('6-8');
$task->consumed->range('1,3,2');
$task->left->range('5,4,6');
$task->status->range('doing');
$task->assignedTo->range('admin');
$task->gen(3);
zdTable('team')->gen(0);
/**
title=测试 programModee::getProgressList();
timeout=0
cid=1
*/
$programTester = new programTest();
$progressList = $programTester->getProgressListTest();
r(count($progressList)) && p() && e('5'); // 获取项目和项目集的个数
r($progressList) && p('2') && e('25'); // 获取id=2的项目的进度
r($progressList) && p('4') && e('43'); // 获取id=4的项目集的进度
-11
View File
@@ -293,17 +293,6 @@ class programTest
return $this->program->getTeamMemberPairs($programID);
}
/**
* Get progress list.
*
* @access public
* @return array
*/
public function getProgressListTest()
{
return $this->program->getProgressList();
}
/**
* Test save state.
*
+3 -4
View File
@@ -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. */
+8 -10
View File
@@ -52,7 +52,7 @@ $fnGenerateProgramRowData = function($programID, $program) use ($config, $users)
if(!isset($program['programName']) || $config->systemMode != 'ALM') return null;
/* ALM mode with more data. */
$totalStories = $program['finishClosedStories'] + $program['unclosedStories'];
$totalStories = $program['totalStories'];
$pmName = '';
if(!empty($program['programPM']))
{
@@ -70,9 +70,9 @@ $fnGenerateProgramRowData = function($programID, $program) use ($config, $users)
$item->PM = $pmName;
$item->createdDate = '';
$item->createdBy = '';
$item->totalUnclosedStories = $program['unclosedStories'];
$item->totalUnclosedStories = $totalStories - $program['closedStories'];
$item->totalStories = $totalStories;
$item->closedStoryRate = ($totalStories == 0 ? 0 : round($program['finishClosedStories'] / $totalStories, 3) * 100);
$item->closedStoryRate = ($totalStories == 0 ? 0 : round($program['closedStories'] / $totalStories, 3) * 100);
$item->totalPlans = $program['plans'];
$item->totalProjects = 0;
$item->totalExecutions = 0;
@@ -125,8 +125,6 @@ $fnGenerateLineRowData = function($programID, $lineID, $line) use ($config, &$li
/* Closure for generating product row data. */
$fnGenerateProductRowData = function($lineID, $product) use ($users)
{
$totalStories = $product->stories['finishClosed'] + $product->stories['unclosed'];
$item = new stdClass();
$item->type = 'product';
$item->id = $product->id;
@@ -135,15 +133,15 @@ $fnGenerateProductRowData = function($lineID, $product) use ($users)
$item->PM = !empty($product->PO) ? zget($users, $product->PO) : '';
$item->createdDate = $product->createdDate;
$item->createdBy = $product->createdBy;
$item->totalUnclosedStories = $product->stories['unclosed'];
$item->totalStories = $totalStories;
$item->closedStoryRate = empty($totalStories) ? 0 : round($product->stories['finishClosed'] / $totalStories, 3) * 100;
$item->totalUnclosedStories = $product->totalStories - $product->closedStories;
$item->totalStories = $product->totalStories;
$item->closedStoryRate = empty($product->totalStories) ? 0 : round($product->closedStories / $product->totalStories, 3) * 100;
$item->totalPlans = $product->plans;
$item->totalProjects = $product->projects;
$item->totalExecutions = $product->executions;
$item->testCaseCoverage = $product->coverage;
$item->totalActivatedBugs = $product->activeBugs;
$item->totalBugs = $product->bugs;
$item->totalActivatedBugs = $product->unresolvedBugs;
$item->totalBugs = $product->totalBugs;
$item->fixedRate = empty($item->totalBugs) ? 0 : round($product->fixedBugs / $item->totalBugs, 3) * 100;
$item->totalReleases = $product->releases;
$item->latestReleaseDate = $product->latestReleaseDate;