Merge branch '15.0.beta3' of https://gitlab.zcorp.cc/easycorp/zentaopms into 15.0.beta3

This commit is contained in:
hufangzhou
2021-05-14 15:25:47 +08:00
21 changed files with 295 additions and 8 deletions
+1
View File
@@ -138,6 +138,7 @@ define('TABLE_STORY', '`' . $config->db->prefix . 'story`');
define('TABLE_STORYSPEC', '`' . $config->db->prefix . 'storyspec`');
define('TABLE_STORYREVIEW', '`' . $config->db->prefix . 'storyreview`');
define('TABLE_STORYSTAGE', '`' . $config->db->prefix . 'storystage`');
define('TABLE_STORYESTIMATE', '`' . $config->db->prefix . 'storyestimate`');
define('TABLE_PRODUCTPLAN', '`' . $config->db->prefix . 'productplan`');
define('TABLE_PLANSTORY', '`' . $config->db->prefix . 'planstory`');
define('TABLE_RELEASE', '`' . $config->db->prefix . 'release`');
+10
View File
@@ -8,6 +8,16 @@ CREATE TABLE IF NOT EXISTS `zt_storyreview` (
UNIQUE KEY `story` (`story`,`version`,`reviewer`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- DROP TABLE IF EXISTS `zt_storyestimate`;
CREATE TABLE IF NOT EXISTS `zt_storyestimate` (
`story` mediumint(9) NOT NULL,
`round` smallint(6) NOT NULL,
`estimate` text NOT NULL,
`average` float(10,2) NOT NULL,
`openedBy` varchar(30) NOT NULL,
`openedDate` datetime NOT NULL,
UNIQUE KEY `story` (`story`,`round`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
ALTER TABLE `zt_project` CHANGE `lifetime` `lifetime` char(30) NOT NULL DEFAULT '';
ALTER TABLE `zt_story` ADD `category` varchar(30) NOT NULL DEFAULT 'feature' AFTER `type`;
+10
View File
@@ -872,6 +872,16 @@ CREATE TABLE IF NOT EXISTS `zt_storyreview` (
`reviewDate` datetime NOT NULL,
UNIQUE KEY `story` (`story`,`version`,`reviewer`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- DROP TABLE IF EXISTS `zt_storyestimate`;
CREATE TABLE IF NOT EXISTS `zt_storyestimate` (
`story` mediumint(9) NOT NULL,
`round` smallint(6) NOT NULL,
`estimate` text NOT NULL,
`average` float(10,2) NOT NULL,
`openedBy` varchar(30) NOT NULL,
`openedDate` datetime NOT NULL,
UNIQUE KEY `story` (`story`,`round`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- DROP TABLE IF EXISTS `zt_storystage`;
CREATE TABLE IF NOT EXISTS `zt_storystage` (
`story` mediumint(8) unsigned NOT NULL,
+9 -6
View File
@@ -65,6 +65,13 @@ class actionModel extends model
$action->project = ($actionType == 'unlinkedfromproject' or $actionType == 'linked2project') ? (int)$extra : (int)$relation['project'];
$action->execution = ($actionType == 'unlinkedfromexecution' or $actionType == 'linked2execution') ? (int)$extra : (int)$relation['execution'];
if($objectType == 'story' and ($actionType == 'linked2build' or $actionType == 'unlinkedfrombuild'))
{
$build = $this->dao->select('project,execution')->from(TABLE_BUILD)->where('id')->eq((int)$extra)->fetch();
$action->project = $build->project;
$action->execution = $build->execution;
}
if($objectType == 'whitelist' and $extra == 'product') $action->product = $objectID;
if($objectType == 'whitelist' and $extra == 'project') $action->project = $objectID;
if($objectType == 'whitelist' and ($extra == 'sprint' or $extra == 'stage')) $action->execution = $objectID;
@@ -181,7 +188,7 @@ class actionModel extends model
}
/* Only process these object types. */
if(strpos(',story,productplan,release,task,build,bug,case,testtask,doc,issue,risk,repo,team,', ",{$objectType},") !== false)
if(strpos(',story,productplan,release,task,build,bug,case,testtask,doc,issue,risk,team,', ",{$objectType},") !== false)
{
if(!isset($this->config->objectTables[$objectType])) return $emptyRecord;
@@ -196,11 +203,7 @@ class actionModel extends model
$record = $this->dao->select($fields)->from($this->config->objectTables[$objectType])->where('id')->eq($objectID)->fetch();
/* Process story, release and task. */
if($objectType == 'story')
{
$record->project = $this->dao->select('project')->from(TABLE_PROJECTSTORY)->where('story')->eq($objectID)->orderBy('project_desc')->limit(1)->fetch('project');
$record->execution = $this->dao->select('project')->from(TABLE_PROJECTSTORY)->where('story')->eq($objectID)->orderBy('project_desc')->limit(1)->fetch('project');
}
if($objectType == 'story') $record->project = $this->dao->select('project')->from(TABLE_PROJECTSTORY)->where('story')->eq($objectID)->orderBy('project_desc')->limit(1)->fetch('project');
if($objectType == 'release') $record->project = $this->dao->select('project')->from(TABLE_BUILD)->where('id')->eq($record->build)->fetch('project');
if($objectType == 'team')
{
+30
View File
@@ -2694,6 +2694,36 @@ class execution extends control
}
}
/**
* Story estimate.
*
* @param int $executionID
* @param int $storyID
* @param int $round
* @access public
* @return void
*/
public function storyEstimate($executionID, $storyID, $round = 0)
{
$this->loadModel('story');
if($_POST)
{
$this->story->saveEstimateInfo($storyID);
$this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->createLink('execution', 'storyEstimate', "executionID=$executionID&storyID=$storyID")));
}
$estimateInfo = $this->story->getEstimateInfo($storyID, $round);
$this->view->estimateInfo = $estimateInfo;
$this->view->round = !empty($estimateInfo->round) ? $estimateInfo->round : 0;
$this->view->rounds = $this->story->getEstimateRounds($storyID);
$this->view->users = $this->loadModel('user')->getPairs('noletter');
$this->view->team = $this->execution->getTeamMembers($executionID);
$this->view->executionID = $executionID;
$this->view->storyID = $storyID;
$this->display();
}
/**
* All execution.
*
+36
View File
@@ -0,0 +1,36 @@
$('.new-estimate input').keyup(function()
{
computeAverage();
})
function computeAverage()
{
var summary = 0;
var count = 0;
var average = 0;
$('.new-estimate').each(function()
{
var value = $(this).find('input').val();
if(!value) return false;
value = parseFloat(value);
if(isNaN(value)) value = 0;
summary += value;
count += 1;
})
if(count) average = summary / count;
$('#average').val(average.toFixed(2));
$('#showAverage').html(average.toFixed(2));
}
function showNewEstimate()
{
$('.th-new-estimate').removeClass('hide');
$('.new-estimate').removeClass('hide');
$('.form-actions').removeClass('hide');
}
function selectRound(round)
{
location.href = createLink('execution', 'storyEstimate', 'executionID=' + executionID + '&storyID=' + storyID + '&round=' + round);
}
+5
View File
@@ -80,6 +80,11 @@ $lang->execution->product = $lang->execution->products;
$lang->execution->readjustTime = 'Start und Ende anpassen';
$lang->execution->readjustTask = 'Fälligkeit der Aufgabe anpassen';
$lang->execution->effort = 'Aufwand';
$lang->execution->storyEstimate = 'Story Estimate';
$lang->execution->newEstimate = 'New Estimate';
$lang->execution->reestimate = 'Reestimate';
$lang->execution->selectRound = 'Select Round';
$lang->execution->average = 'Average';
$lang->execution->relatedMember = 'Teammitglieder';
$lang->execution->watermark = 'Exported by ZenTao';
$lang->execution->burnXUnit = '(Date)';
+5
View File
@@ -80,6 +80,11 @@ $lang->execution->product = $lang->execution->products;
$lang->execution->readjustTime = "Adjust {$lang->executionCommon} Begin and End";
$lang->execution->readjustTask = 'Adjust Task Begin and End';
$lang->execution->effort = 'Effort';
$lang->execution->storyEstimate = 'Story Estimate';
$lang->execution->newEstimate = 'New Estimate';
$lang->execution->reestimate = 'Reestimate';
$lang->execution->selectRound = 'Select Round';
$lang->execution->average = 'Average';
$lang->execution->relatedMember = 'Team';
$lang->execution->watermark = 'Exported by ZenTao';
$lang->execution->burnXUnit = '(Date)';
+5
View File
@@ -80,6 +80,11 @@ $lang->execution->product = $lang->execution->products;
$lang->execution->readjustTime = "Ajuster Début et Fin du {$lang->executionCommon}";
$lang->execution->readjustTask = 'Ajuster Début et Fin de la Tâche';
$lang->execution->effort = 'Effort';
$lang->execution->storyEstimate = 'Story Estimate';
$lang->execution->newEstimate = 'New Estimate';
$lang->execution->reestimate = 'Reestimate';
$lang->execution->selectRound = 'Select Round';
$lang->execution->average = 'Average';
$lang->execution->relatedMember = 'Equipe';
$lang->execution->watermark = 'Exporté par ZenTao';
$lang->execution->burnXUnit = '(Date)';
+5
View File
@@ -80,6 +80,11 @@ $lang->execution->product = $lang->execution->products;
$lang->execution->readjustTime = "Điều chỉnh {$lang->executionCommon} Thời gian";
$lang->execution->readjustTask = 'Điều chỉnh nhiệm vụ Thời gian';
$lang->execution->effort = 'Chấm công';
$lang->execution->storyEstimate = 'Story Estimate';
$lang->execution->newEstimate = 'New Estimate';
$lang->execution->reestimate = 'Reestimate';
$lang->execution->selectRound = 'Select Round';
$lang->execution->average = 'Average';
$lang->execution->relatedMember = 'Đội nhóm';
$lang->execution->watermark = 'Đã xuất bởi ZenTao';
$lang->execution->burnXUnit = '(Date)';
+5
View File
@@ -80,6 +80,11 @@ $lang->execution->product = $lang->execution->products;
$lang->execution->readjustTime = "调整{$lang->executionCommon}起止时间";
$lang->execution->readjustTask = '顺延任务的起止时间';
$lang->execution->effort = '日志';
$lang->execution->storyEstimate = '需求估算';
$lang->execution->newEstimate = '新一轮估算';
$lang->execution->reestimate = '重新估算';
$lang->execution->selectRound = '选择轮次';
$lang->execution->average = '平均值';
$lang->execution->relatedMember = '相关成员';
$lang->execution->watermark = '由禅道导出';
$lang->execution->burnXUnit = '(日期)';
+6 -1
View File
@@ -196,7 +196,7 @@
<th title='<?php echo $lang->story->taskCount?>' class='w-30px'><?php echo $lang->story->taskCountAB;?></th>
<th title='<?php echo $lang->story->bugCount?>' class='w-30px'><?php echo $lang->story->bugCountAB;?></th>
<th title='<?php echo $lang->story->caseCount?>' class='w-30px'><?php echo $lang->story->caseCountAB;?></th>
<th class='c-actions-4 text-center {sorter:false}'><?php echo $lang->actions;?></th>
<th class='c-actions-5 text-center {sorter:false}'><?php echo $lang->actions;?></th>
</tr>
</thead>
<tbody id='storyTableList' class='sortable'>
@@ -276,6 +276,11 @@
echo html::a($this->createLink('testcase', 'create', "productID=$story->product&branch=$story->branch&moduleID=$story->module&form=&param=0&storyID=$story->id"), '<i class="icon-testcase-create icon-sitemap"></i>', '', "title='{$lang->testcase->create}' data-app='qa'");
}
if($canBeChanged and common::hasPriv('execution', 'storyEstimate', $execution))
{
common::printIcon('execution', 'storyEstimate', "executionID=$execution->id&storyID=$story->id", '', 'list', 'bell', '', 'iframe', true, "data-width='600px'");
}
if($canBeChanged and common::hasPriv('execution', 'unlinkStory', $execution))
{
common::printIcon('execution', 'unlinkStory', "executionID=$execution->id&storyID=$story->id&confirm=no", '', 'list', 'unlink', 'hiddenwin');
@@ -0,0 +1,82 @@
<?php
/**
* The create storyestimate view of execution module of ZenTaoPMS.
*
* @copyright Copyright 2009-2021 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
* @license ZPL (http://zpl.pub/page/zplv12.html)
* @author Liyuchun <liyuchun@cnezsoft.com>
* @package execution
* @version $Id
* @link http://www.zentao.net
*/
?>
<style>#showAverage {margin: 0;}</style>
<?php include '../../common/view/header.html.php';?>
<?php js::set('executionID', $executionID);?>
<?php js::set('storyID', $storyID);?>
<div id='mainContent' class='main-content'>
<div class="main-header">
<h2><?php echo $lang->execution->storyEstimate;?></h2>
</div>
<div class='btn-toolbar pull-left'>
<?php if(!empty($rounds)):?>
<div class='input-group space w-200px'>
<span class='input-group-addon'><?php echo $lang->execution->selectRound?></span>
<?php echo html::select('round', $rounds, $round, "class='form-control chosen' onchange='selectRound(this.value)'");?>
</div>
<div id='reestimate' class='input-group space w-100px'>
<?php echo html::a("javascript:showNewEstimate()", $lang->execution->reestimate, '', 'class="btn btn-primary"');?>
</div>
<?php endif;?>
</div>
<form class='main-form form-ajax' method='post' target='hiddenwin' id="estimateForm">
<table class='table table-form'>
<thead>
<tr class='text-center'>
<th class='w-120px'><?php echo $lang->execution->team;?></th>
<th class='w-90px'> <?php echo $lang->story->estimate;?></th>
<th class='w-90px th-new-estimate hide'><?php echo $lang->execution->newEstimate;?></th>
</tr>
</thead>
<tbody>
<?php if(!empty($estimateInfo->estimate)):?>
<?php foreach($estimateInfo->estimate as $estimate):?>
<tr class='text-center'>
<td><?php echo zget($users, $estimate->account);?></td>
<?php echo html::hidden('account[]', $estimate->account, "class='form-control'");?>
<td><?php echo $estimate->estimate;?></td>
<td class='new-estimate hide'><?php echo html::input('estimate[]', '', "class='form-control'");?></td>
</tr>
<?php endforeach;?>
<tr class='text-center'>
<td><?php echo $lang->execution->average;?></td>
<td><?php echo $estimateInfo->average;?></td>
<td class='new-estimate hide'><p id='showAverage'></p></td>
<?php echo html::hidden('average', '', "class='form-control'");?>
</tr>
<?php else:?>
<?php foreach($team as $user):?>
<tr class='text-center'>
<td><?php echo zget($users, $user->account);?></td>
<?php echo html::hidden('account[]', $user->account, "class='form-control'");?>
<td class='new-estimate'><?php echo html::input('estimate[]', '', "class='form-control'");?></td>
</tr>
<?php endforeach;?>
<tr class='text-center'>
<td><strong><?php echo $lang->execution->average;?></strong></td>
<td><p id='showAverage'></p></td>
<?php echo html::hidden('average', '', "class='form-control'");?>
</tr>
<?php endif;?>
</tbody>
<tfoot>
<tr>
<td colspan='3' class="text-center form-actions <?php if(!empty($rounds)) echo 'hide';?>">
<?php echo html::submitButton(); ?>
</td>
</tr>
</tfoot>
</table>
</form>
</div>
<?php include '../../common/view/footer.html.php';?>
+2
View File
@@ -585,6 +585,7 @@ $lang->resource->execution->storySort = 'storySort';
$lang->resource->execution->whitelist = 'whitelist';
$lang->resource->execution->addWhitelist = 'addWhitelist';
$lang->resource->execution->unbindWhitelist = 'unbindWhitelist';
$lang->resource->execution->storyEstimate = 'storyEstimate';
//if($config->systemMode == 'classic') $lang->resource->project->list = 'list';
//$lang->execution->methodOrder[0] = 'index';
@@ -640,6 +641,7 @@ $lang->execution->methodOrder[240] = 'storySort';
$lang->execution->methodOrder[245] = 'whitelist';
$lang->execution->methodOrder[250] = 'addWhitelist';
$lang->execution->methodOrder[255] = 'unbindWhitelist';
$lang->execution->methodOrder[260] = 'storyEstimate';
/* Task. */
$lang->resource->task = new stdclass();
+1
View File
@@ -82,6 +82,7 @@ $lang->story->skipStory = '%s is a parent story. It cannot be closed.';
$lang->story->closedStory = 'Story %s is closed and will not be closed.';
$lang->story->batchToTaskTips = "This action will create a task with the same name as the selected {$lang->SRCommon} and link {$lang->SRCommon} to the task. The closed {$lang->SRCommon} will not be converted into tasks.";
$lang->story->successToTask = "Converted to task.";
$lang->story->storyRound = '%s time estimation';
$lang->story->common = 'Story';
$lang->story->id = 'ID';
+1
View File
@@ -83,6 +83,7 @@ $lang->story->skipStory = '%s is a parent story. It cannot be closed.';
$lang->story->closedStory = 'Story %s is closed and will not be closed.';
$lang->story->batchToTaskTips = "This action will create a task with the same name as the selected {$lang->SRCommon} and link {$lang->SRCommon} to the task. The closed {$lang->SRCommon} will not be converted into tasks.";
$lang->story->successToTask = "Converted to task.";
$lang->story->storyRound = '%s time estimation';
$lang->story->common = 'Story';
$lang->story->id = 'ID';
+1
View File
@@ -82,6 +82,7 @@ $lang->story->skipStory = '%s is a parent story. It cannot be closed.';
$lang->story->closedStory = 'Story %s is closed and will not be closed.';
$lang->story->batchToTaskTips = "This action will create a task with the same name as the selected {$lang->SRCommon} and link {$lang->SRCommon} to the task. The closed {$lang->SRCommon} will not be converted into tasks.";
$lang->story->successToTask = "Converted to task.";
$lang->story->storyRound = '%s time estimation';
$lang->story->common = 'Story';
$lang->story->id = 'ID';
+1
View File
@@ -82,6 +82,7 @@ $lang->story->skipStory = '%s is a parent story. It cannot be closed.';
$lang->story->closedStory = 'Story %s is closed and will not be closed.';
$lang->story->batchToTaskTips = "This action will create a task with the same name as the selected {$lang->SRCommon} and link {$lang->SRCommon} to the task. The closed {$lang->SRCommon} will not be converted into tasks.";
$lang->story->successToTask = "Converted to task.";
$lang->story->storyRound = '%s time estimation';
$lang->story->common = 'Câu chuyện';
$lang->story->id = 'ID';
+1
View File
@@ -83,6 +83,7 @@ $lang->story->skipStory = '需求:%s 为父需求,将不会被关闭
$lang->story->closedStory = '需求:%s 已关闭,将不会被关闭。';
$lang->story->batchToTaskTips = "此操作会创建与所选{$lang->SRCommon}同名的任务,并将{$lang->SRCommon}关联到任务中,已关闭的需求不会转为任务。";
$lang->story->successToTask = '批量转任务成功';
$lang->story->storyRound = '第 %s 轮估算';
$lang->story->common = $lang->SRCommon;
$lang->story->id = '编号';
+78
View File
@@ -3938,6 +3938,84 @@ class storyModel extends model
return $relations;
}
/**
* Get estimate info
*
* @param int $storyID
* @param int $round
* @access public
* @return bool|array
*/
public function getEstimateInfo($storyID, $round = 0)
{
$estimateInfo = $this->dao->select('*')->from(TABLE_STORYESTIMATE)
->where('story')->eq($storyID)
->beginIf($round)->andWhere('round')->eq($round)->fi()
->orderBy('round_desc')
->fetch();
if(!empty($estimateInfo)) $estimateInfo->estimate = json_decode($estimateInfo->estimate);
return $estimateInfo;
}
/**
* Get estimate rounds.
*
* @param int $storyID
* @access public
* @return array
*/
public function getEstimateRounds($storyID)
{
$lastRound = $this->dao->select('round')->from(TABLE_STORYESTIMATE)
->where('story')->eq($storyID)
->orderBy('round_desc')
->fetch('round');
if(!$lastRound) return array();
$rounds = array();
for($i = 1; $i <= $lastRound; $i++)
{
$rounds[$i] = sprintf($this->lang->story->storyRound, $i);
}
return $rounds;
}
/**
* Save estimate information.
*
* @param int $storyID
* @access public
* @return void
*/
public function saveEstimateInfo($storyID)
{
$data = fixer::input('post')->get();
$lastRound = $this->dao->select('round')->from(TABLE_STORYESTIMATE)
->where('story')->eq($storyID)
->orderBy('round_desc')
->fetch('round');
$estimates = array();
foreach($data->account as $key => $account)
{
$estimates[$key]['account'] = $account;
$estimates[$key]['estimate'] = $data->estimate[$key];
}
$storyEstimate = new stdclass();
$storyEstimate->story = $storyID;
$storyEstimate->round = empty($lastRound) ? 1 : $lastRound + 1;
$storyEstimate->estimate = json_encode($estimates);
$storyEstimate->average = $data->average;
$storyEstimate->openedBy = $this->app->user->account;
$storyEstimate->openedDate = helper::now();
$this->dao->insert(TABLE_STORYESTIMATE)->data($storyEstimate)->exec();
}
/**
* Update the story order according to the plan.
*
+1 -1
View File
@@ -329,7 +329,7 @@ class testcase extends control
if(isonlybody()) $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true));
setcookie('caseModule', 0, 0, $this->config->webRoot, '', $this->config->cookieSecure, false);
$response['locate'] = $this->session->caseList ? $this->session->caseList : $this->createLink('testcase', 'browse', "productID={$this->post->product}&branch={$this->post->branch}&browseType=all&param=0&orderBy=id_desc");
$response['locate'] = ($this->session->caseList and strpos($this->session->caseList, 'dynamic') === false) ? $this->session->caseList : $this->createLink('testcase', 'browse', "productID={$this->post->product}&branch={$this->post->branch}&browseType=all&param=0&orderBy=id_desc");
$this->send($response);
}
if(empty($this->products)) $this->locate($this->createLink('product', 'create'));