Merge branch dev/story-73930 of zentao/zentaopms (#8835)

This commit is contained in:
刘刚
2025-05-23 09:53:06 +08:00
committed by Gitfox
43 changed files with 5169 additions and 143 deletions
+1 -3
View File
@@ -101,15 +101,13 @@ spec:
-f misc/ci/Dockerfile.test .
- name: unit test
type: run
when: |
build.target == "main"
spec:
envs:
MYSQL_INTERNAL: "true"
container:
image: hub.qc.oop.cc/zentao-ztf:${{ build.number }}-${{ build.after }}
script:
- git rev-list $(git rev-parse origin/main )..$(git rev-parse HEAD)
- git rev-list $(git rev-parse origin/${{ GITFOX_TARGET_BRANCH }} )..$(git rev-parse HEAD)
- sh -c 'nohup /usr/bin/entrypoint.sh >/dev/null 2>&1 &'
- . /etc/s6/s6-init/envs
- sleep 5
+1 -1
View File
@@ -1 +1 @@
21.7
21.7.1
+3 -3
View File
@@ -6,13 +6,13 @@
},
"zentaoext": {
"gitRepo": "zentao/zentaoext",
"gitVersion": "patch/11.7"
"gitVersion": "release/12.0"
},
"zentaomax": {
"gitVersion": "patch/6.7"
"gitVersion": "release/7.0"
},
"zentaoipd": {
"gitVersion": "patch/3.7"
"gitVersion": "release/4.0"
},
"devops": {
"gitRepo": "zentao/devops"
+1 -1
View File
@@ -16,7 +16,7 @@ if(!class_exists('config')){class config{}}
if(!function_exists('getWebRoot')){function getWebRoot(){}}
/* 基本设置。Basic settings. */
$config->version = '21.7'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->version = '21.7.1'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->liteVersion = '1.2'; // 迅捷版版本。 The version of Lite.
$config->charset = 'UTF-8'; // ZenTaoPHP的编码。 The encoding of ZenTaoPHP.
$config->cookieLife = time() + 2592000; // Cookie的生存时间。The cookie life time.
File diff suppressed because it is too large Load Diff
+9
View File
@@ -1,3 +1,12 @@
DELETE FROM `zt_workflowaction` WHERE `module` = 'story' AND `action` = 'browse';
DELETE FROM `zt_workflowlayout` WHERE `module` = 'story' AND `action` = 'browse';
DELETE FROM `zt_workflowaction` WHERE `module` = 'epic' AND `action` = 'browse';
DELETE FROM `zt_workflowlayout` WHERE `module` = 'epic' AND `action` = 'browse';
DELETE FROM `zt_workflowaction` WHERE `module` = 'requirement' AND `action` = 'browse';
DELETE FROM `zt_workflowlayout` WHERE `module` = 'requirement' AND `action` = 'browse';
UPDATE `zt_workflowaction` SET `module` = 'story', `action` = 'browse' WHERE `module` = 'product' AND `action` = 'browse';
UPDATE `zt_workflowlayout` SET `module` = 'story', `action` = 'browse' WHERE `module` = 'product' AND `action` = 'browse';
+44
View File
@@ -1,3 +1,47 @@
2025-05-30 21.7.1
完成的需求
旗舰版:
74181 前台提交交付物的相关页面中增加提交文件限制
74126 编辑已提交付物的独立执行和顶级执行不允许关联父执行
74125 流程模板中交付物配置改变时不会影响已创建的项目
74091 后台系统功能配置的通用功能中增加交付物功能开关
74088 实现项目交付物历史记录功能
73952 项目流程模板详情的历史记录中记录交付物配置的动态
73827 实现维护执行交付物的权限配置
73826 实现维护项目交付物的权限配置
73824 实现流程模板交付物配置的权限配置
73822 实现后台交付物配置的权限配置
73716 实现创建项目页面中的交付物组件
73671 实现项目交付物维护页面
73621 实现独立执行和顶级执行交付物维护弹窗
73616 项目列表支持查看交付物的提交比例
73607 独立执行和顶级执行列表支持查看交付物的提交比例
73587 项目菜单打印交付物
73585 关闭最后一个未关闭的子执行后引导用户关闭顶级执行
73583 批量关闭执行时提示无法关闭进行中的执行
73552 实现交付物列表的搜索功能
73551 实现按标签筛选列表中的交付物
73549 实现添加交付物页面模板字段的互斥逻辑
73548 实现交付物页面适用范围下拉菜单与后台阶段类型同步
73488 调整独立执行和顶级执行的类型导致交付物发生改变时保留已提交的交付物
73475 关闭进行中的独立执行和顶级执行时支持提交交付物
73457 关闭进行中的项目时支持提交交付物
73453 创建项目时支持提交交付物
73447 交付物配置页打印保存和返回按钮
73446 交付物配置页始终显示帮助说明
73445 交付物配置页标题可以依据不同流程模板命名
73443 交付物配置页支持按对象动作关联交付物
73442 项目流程模板列表操作列打印交付物配置按钮
73439 实现删除交付物功能
73438 实现编辑交付物功能
73436 实现交付物详情页面
73429 实现交付物列表页面
73347 添加交付物页面支持定义交付物的基本信息
73346 交付物列表页打印添加交付物的按钮
73329 项目模型配置的通用菜单中打印交付物
73327 后台模型配置名称调整为项目模型配置
修复的Bug
2025-05-16 21.7
完成的需求
开源版:
+2
View File
@@ -527,6 +527,8 @@ class control extends baseControl
$flow = $this->loadModel('workflow')->getByModule($moduleName, false, $groupID);
if(!$flow) return $fields;
if($groupID && $moduleName == 'project' && $methodName == 'create') $groupID = 0; // 项目创建页面用通用的工作流配置
$action = $this->loadModel('workflowaction')->getByModuleAndAction($flow->module, $methodName, $groupID);
if(!$action || $action->extensionType != 'extend') return $fields;
+163 -20
View File
@@ -820,10 +820,11 @@ class baseDAO
* 处理sql语句,替换表和字段。
* Process the sql, replace the table, fields.
*
* @param string $filterTpl
* @access public
* @return string the sql string after process.
*/
public function processSQL()
public function processSQL($filterTpl = true)
{
$sql = $this->sqlobj->get();
@@ -858,20 +859,48 @@ class baseDAO
$sql .= '(`' . implode('`,`', array_keys($values)) . '`)' . ' VALUES(' . implode(',', $values) . ')';
}
elseif($this->method == 'select')
elseif($this->method == 'select' && $filterTpl)
{
/* 过滤模板类型的数据 */
foreach(array('project', 'task') as $table)
{
$table = $this->config->db->prefix . $table;
if(strpos($sql, "`$table`") === false) continue;
if(preg_match("/isTpl\s*=\s*('1'|1)/", $sql)) continue; // 指定查询模板类型的数据则不过滤
preg_match_all('/\(([^()]*SELECT\b.+?\bFROM\b[^()]*)\)/i', $sql, $matches);
if(preg_match("/`isTpl`\s*=\s*('1'|1)/", $sql) || preg_match("/isTpl\s*=\s*('1'|1)/", $sql)) continue; // 指定查询模板类型的数据则不过滤
if(!$matches[1])
{
$alias = preg_match("/`$table`\s+as\s+(\w+)/i", $sql, $matches) ? $matches[1] : '';
$alias = preg_match("/`$table`\s+as\s+(\w+)/i", $sql, $matches) ? $matches[1] : '';
$replace = $alias ? "wHeRe $alias.`isTpl` = '0' AND" : "wHeRe `isTpl` = '0' AND";
$sql = str_ireplace("wHeRe", $replace, $sql);
}
else
{
foreach($matches[1] as $index => $subSQL)
{
$sql = str_ireplace($subSQL, "$$index", $sql);
}
$replace = $alias ? "wHeRe $alias.isTpl = '0' AND" : "wHeRe isTpl = '0' AND";
$sql = str_ireplace("wHeRe", $replace, $sql);
if(strpos($sql, "`$table`") !== false)
{
$alias = preg_match("/`$table`\s+as\s+(\w+)/i", $sql, $mainMatches) ? $mainMatches[1] : '';
$replace = $alias ? "wHeRe $alias.`isTpl` = '0' AND" : "wHeRe `isTpl` = '0' AND";
$sql = str_ireplace("wHeRe", $replace, $sql);
}
foreach($matches[1] as $index => $subSQL)
{
if(strpos($sql, "`$table`") !== false && !preg_match("/`isTpl`\s*=\s*('1'|1)/", $subSQL))
{
$alias = preg_match("/`$table`\s+as\s+(\w+)/i", $subSQL, $subMatches) ? $subMatches[1] : '';
$replace = $alias ? "wHeRe $alias.`isTpl` = '0' AND" : "wHeRe isTpl = '0' AND";
$subSQL = str_ireplace("wHeRe", $replace, $subSQL);
}
$sql = str_ireplace("$$index", $subSQL, $sql);
}
}
}
}
@@ -1240,7 +1269,7 @@ class baseDAO
*/
public function fetch($field = '')
{
$sql = $this->processSQL();
$sql = $this->processSQL(false);
$key = $this->createCacheKey('fetch', md5($sql));
$result = $this->getCache($key);
if($result === self::CACHE_MISS)
@@ -1493,15 +1522,7 @@ class baseDAO
**/
else
{
/*
* 使用$arg0, $arg1... 生成调用的参数。
* Create the max counts of sql class methods, and then create $arg0, $arg1...
**/
for($i = 0; $i < SQL::MAX_ARGS; $i ++)
{
${"arg$i"} = isset($funcArgs[$i]) ? $funcArgs[$i] : null;
}
$this->sqlobj->$funcName($arg0, $arg1, $arg2);
$this->sqlobj->$funcName(...$funcArgs);
return $this;
}
}
@@ -1949,6 +1970,21 @@ class baseDAO
{
return $this->dbh->getVersion();
}
/**
* 创建临时表。
* Create temporary table.
*
* @param int $ids 用于创建临时表的 id 列表,字符串或数组。
* @param bool $filterDuplicate 是否过滤重复的表名,默认值为 true。
* @param int $limit 用于创建临时表的 id 列表的数量限制,数量小于这个值时不创建临时表,0 表示不限制数量。
* @access public
* @return false|string
*/
public function createTemporaryTable($idList, $filterDuplicate = true, $limit = 100)
{
return $this->sqlobj->createTemporaryTable($idList, $filterDuplicate, $limit);
}
}
/**
@@ -1975,6 +2011,15 @@ class baseSQL
*/
public $sql = '';
/**
* 全局对象$app
* The global app object.
*
* @var object
* @access public
*/
public $app;
/**
* 全局变量$dbh。
* The global $dbh.
@@ -2101,6 +2146,15 @@ class baseSQL
*/
public $currentTable;
/**
* 已创建的临时表。
* The temporary tables.
*
* @var array
* @access public
*/
public $tempTables = [];
/**
* 构造方法。
* The construct function.
@@ -2110,7 +2164,8 @@ class baseSQL
*/
public function __construct($table = '')
{
global $dbh;
global $app, $dbh;
$this->app = $app;
$this->dbh = $dbh;
$this->data = new stdclass();
$this->skipFields = '';
@@ -2622,17 +2677,105 @@ class baseSQL
* 创建IN部分。
* Create in part.
*
* @param string|array $ids ','分割的字符串或者数组 list string by ',' or an array
* @param string|array $ids ','分割的字符串或者数组。List string by ',' or an array.
* @param bool $useTemporaryTable 是否使用临时表。 Use temporary table or not.
* @param bool $filterDuplicate 是否过滤重复的表名,默认值为 true。Filter duplicate table name or not.
* @access public
* @return static|sql the sql object.
*/
public function in($ids)
public function in($ids, $useTemporaryTable = false, $filterDuplicate = true)
{
if($this->inCondition and !$this->conditionIsTrue) return $this;
$this->sql .= is_null($ids) ? ' IS NULL' : helper::dbIN($ids);
if(is_null($ids))
{
$this->sql .= ' IS NULL';
return $this;
}
if($useTemporaryTable)
{
$tableName = $this->createTemporaryTable($ids, $filterDuplicate);
if($tableName)
{
$this->sql .= " IN (SELECT id FROM $tableName)";
return $this;
}
}
$this->sql .= helper::dbIN($ids);
return $this;
}
/**
* 创建临时表。
* Create temporary table.
*
* @param int $ids 用于创建临时表的 id 列表,字符串或数组。
* @param bool $filterDuplicate 是否过滤重复的表名,默认值为 true。
* @param int $limit 用于创建临时表的 id 列表的数量限制,数量小于这个值时不创建临时表,0 表示不限制数量。
* @access public
* @return false|string
*/
public function createTemporaryTable($ids, $filterDuplicate = true, $limit = 100)
{
if(!is_string($ids) && !is_array($ids))
{
$this->app->triggerError('The idList must be a string or an array.', __FILE__, __LINE__);
return false;
}
if(is_string($ids)) $ids = explode(',', $ids);
$idList = [];
foreach($ids as $id)
{
if(empty($id)) continue;
$intID = (int)$id;
if($intID != $id) continue;
$idList[$intID] = $intID;
}
if($limit && count($idList) < $limit)
{
$this->app->triggerError("The idList count must be greater than $limit", __FILE__, __LINE__);
return false;
}
$tableName = '';
if($filterDuplicate)
{
/* 如果开启了唯一表名,那么将表名设置为 md5(idList). */
asort($idList);
$rows = '(' . implode('),(', $idList) . ')';
$tableName = "temp_" . md5($rows);
if(isset($this->tempTables[$tableName])) return $tableName;
}
else
{
$tableName = 'temp_' . uniqid();
$rows = '(' . implode('),(', $idList) . ')';
}
try
{
$this->dbh->exec("CREATE TEMPORARY TABLE IF NOT EXISTS {$tableName} (id int PRIMARY KEY)");
$this->dbh->exec("INSERT INTO {$tableName} VALUES {$rows}");
}
catch(PDOException $e)
{
$message = $e->getMessage() . ' ' . helper::checkDB2Repair($e);
$this->app->triggerError($message, __FILE__, __LINE__);
}
if($filterDuplicate) $this->tempTables[$tableName] = $tableName;
return $tableName;
}
/**
* 创建'NOT IN'部分。
* Create not in part.
+51 -12
View File
@@ -161,6 +161,7 @@ class cache
{
$this->app = $app;
$this->dao = $app->dao;
$this->dbh = $app->dbh;
$this->config = $app->config;
if(empty($this->config->cache->enable)) return $this->log('The cache is not enabled', __FILE__, __LINE__);
@@ -369,12 +370,13 @@ class cache
$objectIdList = $this->cache->get($setCacheKey);
if(!is_array($objectIdList)) return;
$objectID = $this->dao->lastInsertID();
$objectID = $this->dbh->lastInsertID();
if(!$objectID) return $this->log('Failed to fetch last insert id.', __FILE__, __LINE__);
/* 获取新增的数据。Get the new data. */
$object = $this->dao->select('*')->from($this->table)->where('id')->eq($objectID)->fetch();
if(!$object) return $this->log('Failed to fetch new object. The sql is: ' . $this->dao->get(), __FILE__, __LINE__);
$sql = $this->dao->select('*')->from($this->table)->where('id')->eq($objectID)->get();
$object = $this->query($sql, 'fetch');
if(!$object) return $this->log('Failed to fetch new object. The sql is: ' . $sql, __FILE__, __LINE__);
/* 把新增的数据保存到缓存中。Save the new data to cache. */
$field = $this->getTableField();
@@ -408,8 +410,9 @@ class cache
$objectIdList = array_map(function($object) use ($field) { return $object->$field; }, $this->objects);
/* 获取更新后的数据。Get the updated data. */
$objects = $this->dao->select('*')->from($this->table)->where($field)->in($objectIdList)->fetchAll($field, false);
if(!$objects) return $this->log('Failed to fetch updated objects. The sql is: ' . $this->dao->get(), __FILE__, __LINE__);
$sql = $this->dao->select('*')->from($this->table)->where($field)->in($objectIdList)->get();
$objects = $this->query($sql, 'fetchAll', $field);
if(!$objects) return $this->log('Failed to fetch updated objects. The sql is: ' . $sql, __FILE__, __LINE__);
/* 把更新后的数据保存到缓存中。Save the updated data to cache. */
$values = [];
@@ -506,6 +509,28 @@ class cache
$this->cache->deleteMultiple($keys);
}
/**
* 执行数据库查询操作。
* Execute database query operation.
*
* @param string $sql SQL语句。
* @param string $function 查询函数。
* @param string $field 字段名。
* @access private
* @return array|object
*/
private function query($sql, $function = 'fetchAll', $field = ''): array|object
{
$rows = $this->dao->query($sql)->$function();
if($function == 'fetchAll' && !empty($field))
{
$objects = [];
foreach($rows as $row) $objects[$row->$field] = $row;
return $objects;
}
return $rows;
}
/**
* 检查表是否有缓存设置。
* Check if the table has cache settings.
@@ -583,7 +608,8 @@ class cache
private function initTableCache(): array
{
$field = $this->getTableField();
$objects = $this->dao->select('*')->from($this->table)->fetchAll($field, false);
$sql = $this->dao->select('*')->from($this->table)->get();
$objects = $this->query($sql, 'fetchAll', $field);
if(!$objects) return [];
$values = [];
@@ -693,7 +719,8 @@ class cache
{
$lostObjects = [];
$diffIdList = array_keys(array_diff($keys, array_keys($objects)));
$diffObjects = $this->dao->select('*')->from($table)->where('id')->in($diffIdList)->fetchAll('', false);
$sql = $this->dao->select('*')->from($table)->where('id')->in($diffIdList)->get();
$diffObjects = $this->query($sql, 'fetchAll');
foreach($diffObjects as $object)
{
$rawCacheKey = $this->getRawCacheKey($code, $object->id);
@@ -757,18 +784,29 @@ class cache
$field = $this->getTableField();
$dao = $this->dao->select($field)->from($table)->where('1=1');
foreach($conditions as $condition) $dao->andWhere($condition['field'])->eq($condition['value']);
$idList = array_values($dao->fetchPairs());
$sql = $dao->get();
$objects = $this->query($sql, 'fetchAll');
foreach($objects as $object) $idList[] = $object->$field;
$this->saveByKey($cacheKey, $idList);
/* 把缓存键写入数据库以便执行 dao::exec() 时自动更新。Write the cache key to the database for automatic update when executing dao::exec(). */
$fields = implode(',', array_column($conditions, 'field'));
$autoCache = $this->dao->select('1')->from(TABLE_AUTOCACHE)->where('code')->eq($code)->andWhere('fields')->eq($fields)->fetch();
$sql = $this->dao->select('1')->from(TABLE_AUTOCACHE)->where('code')->eq($code)->andWhere('fields')->eq($fields)->get();
$autoCache = $this->query($sql, 'fetch');
if(!$autoCache)
{
$autoCache = new \stdClass();
$autoCache->code = $code;
$autoCache->fields = $fields;
$this->dao->insert(TABLE_AUTOCACHE)->data($autoCache)->exec();
$sql = $this->dao->insert(TABLE_AUTOCACHE)->data($autoCache)->get();
try
{
$this->dbh->exec($sql);
}
catch(PDOException $e)
{
$this->dao->sqlError($e);
}
}
}
if(!$idList) return [];
@@ -980,7 +1018,8 @@ class cache
/* 执行操作后数据已经被修改,所以需要提前获取被影响的数据。*/
$field = $this->getTableField();
$objects = $this->dao->select('*')->from($table)->beginIF($where)->where($where)->fi()->fetchAll($field, false);
$sql = $this->dao->select('*')->from($table)->beginIF($where)->where($where)->fi()->get();
$objects = $this->query($sql, 'fetchAll', $field);
$this->setWhere($where);
$this->setObjects($objects);
@@ -1046,4 +1085,4 @@ class cache
{
return $this->cache->memory($type);
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ window.getDeliverableFileActions = function(file, deliverable)
actions[2] = {icon: 'trash', key: 'delete'};
/* 可以预览的文件。 */
if(['txt', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'mp4'].includes(file.extension) && canDownload)
if(['txt', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'mp4'].includes(file.extension) && canDownload && typeof file.id === 'number')
{
actions[3] = {icon: 'eye', key: 'view', url: $.createLink('file', 'download', `fileID=${file.id}&mouse=left`), 'data-toggle' : 'modal', 'data-size' : 'lg'};
}
+29
View File
@@ -2091,6 +2091,33 @@ function getSpaceFetcher(spaceType, spaceID = 0)
}
}
function initOfficePreview(options)
{
const officeTypes = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf']);
zui.Editor.setGlobalConfig(
{
canPreviewFile: (file) => {
const extension = file.name.split('.').pop();
return officeTypes.has(extension);
},
onPreviewFile: (file) => {
const doc = getDocApp().doc || {id: 0};
$.getJSON(zui.formatString(options.fileInfoUrl, {
gid: btoa(`g-${file.key}`),
objectType: 'doc',
objectID: doc.id,
title: '',
extra: '',
})).then(res => {
if(res.result === 'success' && res.data)
{
window.open($.createLink('file', 'download', `fileID=${res.data.id}&mouse=left`), '_blank');
}
});
}
});
}
/**
* 设置文档应用组件选项。
* Set the doc app options.
@@ -2102,6 +2129,8 @@ window.setDocAppOptions = function(_, options)
userPrivs = options.privs;
docLangData = options.langData;
if(options.canPreviewOffice && options.fileInfoUrl) initOfficePreview(options);
return $.extend(
{
defaultState : {listType: '', libReleaseMap: {}},
+4
View File
@@ -144,6 +144,7 @@ class docApp extends wg
$libSummariesFetcher = createLink('doc', 'ajaxGetLibSummaries', 'spaceType={spaceType}&spaceList={spaceList}');
$uploadUrl = createLink('file', 'ajaxUpload', 'uid={uid}&objectType={objectType}&objectID={objectID}&extra={extra}&field={field}&api={api}&gid={gid}');
$downloadUrl = createLink('file', 'ajaxQuery', 'fileID={gid}&objectType={objectType}&objectID={objectID}&title={title}&extra={extra}');
$fileInfoUrl = createLink('file', 'ajaxQuery', 'fileID={gid}&objectType={objectType}&objectID={objectID}&title={title}&extra={extra}', 'json');
/**
* 定义文档界面上的文件下载链接。
@@ -269,6 +270,7 @@ class docApp extends wg
}
$historyPanelProps = array('fileListProps' => $fileListProps);
$canPreviewOffice = $canDownload && isset($config->file->libreOfficeTurnon) and $config->file->libreOfficeTurnon == 1;
return zui::docApp
(
@@ -313,6 +315,8 @@ class docApp extends wg
set::langData($langData),
set::historyPanel($historyPanelProps),
set::showToolbar(true),
set::canPreviewOffice($canPreviewOffice),
set::fileInfoUrl($fileInfoUrl),
jsCall('setZentaoSlashMenu', $this->getZentaoListMenu(), $lang->doc->zentaoData, $config->vision, $config->doc->zentaoListMenuPosition)
);
}
-2
View File
@@ -3,5 +3,3 @@
/* 半透明黄色 */
.monaco-editor .yellow-decoration {background-color: rgba(255, 255, 0, 0.2);}
.monaco-editor .margin .line-numbers {padding-left: 16px;}
+1 -1
View File
@@ -154,7 +154,7 @@ function resize(id)
mainMenuHeight = mainMenuHeight ? mainMenuHeight : 0;
mainNavbar = mainNavbar ? mainNavbar : 0;
var codeHeight = windowHeight - headerHeight - appsBarHeight - tabsHeight - appTabsHeight - mainMenuHeight - mainNavbar;
var codeHeight = windowHeight - headerHeight - appsBarHeight - tabsHeight - appTabsHeight - mainMenuHeight - mainNavbar;
if(codeHeight > 0) $.cookie.set(id + 'Height', codeHeight);
+2 -5
View File
@@ -2967,13 +2967,9 @@ class execution extends control
*/
public function whitelist(int $executionID = 0, string $module='execution', string $objectType = 'sprint', string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1)
{
/* use first execution if executionID does not exist. */
if(!isset($this->executions[$executionID])) $executionID = key($this->executions);
/* Set the menu. If the executionID = 0, use the indexMenu instead. */
$this->execution->setMenu($executionID);
$execution = $this->commonAction($executionID);
$execution = $this->execution->getByID($executionID);
if(!empty($execution->acl) and $execution->acl != 'private') return $this->sendError($this->lang->whitelistNotNeed, $this->createLink('execution', 'task', "executionID=$executionID"));
echo $this->fetch('personnel', 'whitelist', "objectID=$executionID&module=$module&browseType=$objectType&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID");
@@ -3153,6 +3149,7 @@ class execution extends control
*/
public function doc(int $executionID = 0, int $libID = 0, int $moduleID = 0, string $browseType = 'all', string $orderBy = 'order_asc', int $param = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1, string $mode = 'list', int $docID = 0, string $search = '')
{
$this->commonAction($executionID);
echo $this->fetch('doc', 'app', "type=execution&spaceID=$executionID&libID=$libID&moduleID=$moduleID&docID=$docID&mode=$mode&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID&filterType=$browseType&search=$search&noSpace=true");
}
+3 -1
View File
@@ -222,6 +222,8 @@ window.getItem = function(info)
let assignedTo = '';
let avatar = "<span class='avatar rounded-full size-xs ml-1 bg-gray-100 text-canvas' title='" + noAssigned + "'><i class='icon icon-person'></i></span>";
info.item.originalTitle = info.item.title;
if(begin < '1970-01-01' && end > '1970-01-01')
{
beginAndEnd = end + ' ' + cardLang.deadlineAB;
@@ -315,7 +317,7 @@ window.getItem = function(info)
info.item.titleUrl = $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}&from=execution`);
}
info.item.titleAttrs = {'data-toggle': 'modal', 'data-size': 'lg', 'title': info.item.title, 'class': 'card-title clip'};
info.item.titleAttrs = {'data-toggle': 'modal', 'data-size': 'lg', 'title': info.item.originalTitle, 'class': 'card-title clip'};
info.item.content = {html: content};
if(info.item.color && info.item.color != '#fff') info.item.className = 'color-' + info.item.color.replace('#', '');
+39 -6
View File
@@ -107,8 +107,8 @@ class executionModel extends model
$executions = $this->fetchPairs($execution->project, 'all');
if(!$executionID && $this->session->execution) $executionID = $this->session->execution;
if(!$executionID) $executionID = key($executions);
if($execution->multiple and !isset($executions[$executionID])) $executionID = key($executions);
if($execution->multiple and $executions and (!isset($executions[$executionID]) or !$this->checkPriv($executionID))) return $this->accessDenied();
if($execution->multiple && !$execution->isTpl && !isset($executions[$executionID])) $executionID = key($executions);
if($execution->multiple && !$execution->isTpl && $executions && (!isset($executions[$executionID]) || !$this->checkPriv($executionID))) return $this->accessDenied();
if(empty($executionID)) return;
/* Replaces the iterated language with the stage. */
@@ -195,8 +195,10 @@ class executionModel extends model
if(!$executionID && isset($this->config->execution->lastExecution)) $executionID = (int)$this->config->execution->lastExecution;
}
/* 项目模板不校验访问权限。 */
$isTpl = $this->dao->select('isTpl')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('isTpl');
/* If the execution doesn't exist in the list, use the first execution in the list. */
if(!isset($executions[$executionID]))
if(!$isTpl && !isset($executions[$executionID]))
{
/* Check execution. */
if($executionID)
@@ -1437,7 +1439,7 @@ class executionModel extends model
{
/* Construct the query SQL at search executions. */
$executionQuery = $browseType == 'bySearch' ? $this->getExecutionQuery($param) : '';
$projectModel = $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('model');
$project = $this->dao->select('model,isTpl')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch();
return $this->dao->select('t1.*,t2.name projectName, t2.model as projectModel')->from(TABLE_EXECUTION)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
@@ -1446,7 +1448,8 @@ class executionModel extends model
->andWhere('t1.deleted')->eq('0')
->andWhere('t1.vision')->eq($this->config->vision)
->andWhere('t1.multiple')->eq('1')
->beginIF($projectModel == 'ipd')->andWhere('t1.enabled')->eq('on')->fi()
->beginIF($project->model == 'ipd')->andWhere('t1.enabled')->eq('on')->fi()
->beginIF($project->isTpl)->andWhere('t1.isTpl')->eq('1')->fi()
->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->sprints)->fi()
->beginIF(!empty($executionQuery))->andWhere($executionQuery)->fi()
->beginIF($productID)->andWhere('t3.product')->eq($productID)->fi()
@@ -1489,7 +1492,7 @@ class executionModel extends model
$today = helper::today();
$burns = $this->getBurnData($executions);
$parentExecutions = $this->dao->select('parent,parent')->from(TABLE_EXECUTION)->where('parent')->ne(0)->andWhere('deleted')->eq(0)->fetchPairs();
$statusGroup = $this->dao->select('parent,status')->from(TABLE_EXECUTION)->where('parent')->in(array_keys($executions))->fetchGroup('parent', 'status');
$statusGroup = $this->dao->select('parent,status')->from(TABLE_EXECUTION)->where('parent')->in(array_keys($executions))->andWhere('deleted')->eq(0)->fetchGroup('parent', 'status');
/* Get workingDays. */
$earliestEnd = $today;
@@ -5017,6 +5020,7 @@ class executionModel extends model
$executionData->openedBy = $this->app->user->account;
$executionData->openedDate = helper::now();
$executionData->parent = $projectID;
$executionData->isTpl = $project->isTpl;
if($project->code) $executionData->code = $project->code;
$projectProducts = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->fetchAll();
@@ -5311,4 +5315,33 @@ class executionModel extends model
return array($toList, $ccList);
}
/**
* 检查迭代是否可以关闭。
* Check if the execution can be closed.
*
* @param object $execution
* @access public
* @return bool
*/
public function canCloseByDeliverable(object $execution): bool
{
if(empty($execution->deliverable)) return true;
$deliverables = json_decode($execution->deliverable, true);
foreach($deliverables as $methods)
{
foreach($methods as $itemList)
{
foreach($itemList as $item)
{
if(!empty($item['required']) && empty($item['file']) && empty($item['doc'])) return false;
}
}
}
return true;
}
}
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/execution.unittest.class.php';
$execution = zenData('project')->gen(5);
$execution->id->range('1-5');
$execution->name->range('迭代1,迭代2,迭代3,迭代4,迭代5');
$execution->type->range('stage');
$execution->status->range('doing');
$execution->parent->range('0,0,1,1,1');
$execution->grade->range('1{2},2{3}');
$execution->path->range('1,2,`1,3`,`1,4`,`1,5`')->prefix(',')->postfix(',');
$execution->begin->range('20230102 000000:0')->type('timestamp')->format('YY/MM/DD');
$execution->end->range('20230212 000000:0')->type('timestamp')->format('YY/MM/DD');
$execution->deliverable->range('`{"product_waterfall_request":{"whenClosed":[{"deliverable":"1","required":true,"category":"test","template":""}]}}`');
$execution->gen(5);
su('admin');
/**
title=executionModel->getByID();
timeout=0
cid=1
- 查看执行1是否可关闭 @1
- 查看执行2是否可关闭 @1
- 查看执行3是否可关闭 @1
- 查看执行4是否可关闭 @1
- 查看执行5是否可关闭 @1
*/
global $tester;
$tester->loadModel('execution');
$execution1 = $tester->execution->getByID(1);
$execution2 = $tester->execution->getByID(2);
$execution3 = $tester->execution->getByID(3);
$execution4 = $tester->execution->getByID(4);
$execution5 = $tester->execution->getByID(5);
r($tester->execution->canCloseByDeliverable($execution1)) && p('') && e('1'); // 查看执行1是否可关闭
r($tester->execution->canCloseByDeliverable($execution2)) && p('') && e('1'); // 查看执行2是否可关闭
r($tester->execution->canCloseByDeliverable($execution3)) && p('') && e('1'); // 查看执行3是否可关闭
r($tester->execution->canCloseByDeliverable($execution4)) && p('') && e('1'); // 查看执行4是否可关闭
r($tester->execution->canCloseByDeliverable($execution5)) && p('') && e('1'); // 查看执行5是否可关闭
+12
View File
@@ -571,6 +571,11 @@ class file extends control
}
if($file)
{
if($this->viewType === 'json')
{
echo json_encode(array('result' => 'success', 'data' => array('id' => $file->id, 'title' => $file->title, 'extension' => $file->extension, 'size' => $file->realPath, 'gid' => $file->gid, 'addedBy' => $file->addedBy, 'addedDate' => $file->addedDate, 'objectType' => $file->objectType, 'objectID' => $file->objectID)));
return;
}
$fileID = $file->id;
return $this->fetch('file', 'read', "fileID=$fileID&stream=$stream");
}
@@ -582,6 +587,13 @@ class file extends control
$file = $this->file->query($objectType, $objectID, $title, $extra);
if(empty($file)) return $this->send(array('result' => 'fail', 'message' => $this->lang->file->fileNotFound, 'load' => helper::createLink('my', 'index'), 'closeModal' => true));
$fileID = $file->id;
if($this->viewType === 'json')
{
echo json_encode(array('result' => 'success', 'data' => array('id' => $file->id, 'title' => $file->title, 'extension' => $file->extension, 'size' => $file->realPath, 'gid' => $file->gid, 'addedBy' => $file->addedBy, 'addedDate' => $file->addedDate, 'objectType' => $file->objectType, 'objectID' => $file->objectID)));
return;
}
return $this->fetch('file', 'read', "fileID=$fileID&stream=$stream");
}
+1
View File
@@ -114,6 +114,7 @@ $lang->misc->feature->promptExecImage = 'theme/default/images/main/prompt_exec
$lang->misc->feature->promptLearnMore = 'https://www.zentao.net/book/zentaopms/1097.html';
/* Release Date. */
$lang->misc->releaseDate['21.7.1'] = '2025-05-30';
$lang->misc->releaseDate['21.7'] = '2025-05-16';
$lang->misc->releaseDate['21.6.1'] = '2025-04-30';
$lang->misc->releaseDate['21.6'] = '2025-04-11';
+1
View File
@@ -114,6 +114,7 @@ $lang->misc->feature->promptExecImage = 'theme/default/images/main/prompt_exec
$lang->misc->feature->promptLearnMore = 'https://www.zentao.net/book/zentaopms/1097.html';
/* Release Date. */
$lang->misc->releaseDate['21.7.1'] = '2025-05-30';
$lang->misc->releaseDate['21.7'] = '2025-05-16';
$lang->misc->releaseDate['21.6.1'] = '2025-04-30';
$lang->misc->releaseDate['21.6'] = '2025-04-11';
+1
View File
@@ -114,6 +114,7 @@ $lang->misc->feature->promptExecImage = 'theme/default/images/main/prompt_exec
$lang->misc->feature->promptLearnMore = 'https://www.zentao.net/book/zentaopms/1097.html';
/* Release Date. */
$lang->misc->releaseDate['21.7.1'] = '2025-05-30';
$lang->misc->releaseDate['21.7'] = '2025-05-16';
$lang->misc->releaseDate['21.6.1'] = '2025-04-30';
$lang->misc->releaseDate['21.6'] = '2025-04-11';
+2
View File
@@ -114,6 +114,7 @@ $lang->misc->feature->promptExecImage = 'theme/default/images/main/prompt_exec
$lang->misc->feature->promptLearnMore = 'https://www.zentao.net/book/zentaopms/1097.html';
/* Release Date. */
$lang->misc->releaseDate['21.7.1'] = '2025-05-30';
$lang->misc->releaseDate['21.7'] = '2025-05-16';
$lang->misc->releaseDate['21.6.1'] = '2025-04-30';
$lang->misc->releaseDate['21.6'] = '2025-04-11';
@@ -248,6 +249,7 @@ $lang->misc->releaseDate['7.1.stable'] = '2015-03-07';
$lang->misc->releaseDate['6.3.stable'] = '2014-11-07';
/* Release Detail. */
$lang->misc->feature->all['21.7.1'][] = array('title' => '', 'desc' => '');
$lang->misc->feature->all['21.7'][] = array('title' => '', 'desc' => '');
$lang->misc->feature->all['21.6.1'][] = array('title' => '修复文档相关Bug。', 'desc' => '');
$lang->misc->feature->all['21.6'][] = array('title' => 'Jira导入优化,文档多人协作。', 'desc' => '');
+1 -1
View File
@@ -2041,7 +2041,7 @@ class productModel extends model
if($module == 'bug' && $method == 'view') return helper::createLink('bug', 'browse', "productID=%s&branch={$branchID}&extra=$extra");
if($module == 'bug' && $method == 'report') return helper::createLink('bug', 'browse', "productID=%s{$branchParam}");
if($module == 'qa' && $method == 'index') return helper::createLink('bug', 'browse', "productID=%s{$branchParam}");
if($module == 'story' && $method == 'report') return helper::createLink($module, $method, "productID=%s&branch={$branchID}&extra=$extra");
if($module == 'story' && $method == 'report') return helper::createLink($module, $method, "productID=%s&branch={$branchID}&stroyType=$module");
if($module == 'testcase' && $method == 'browse') return helper::createLink($module, $method, "productID=%s&branch={$branchID}" . ($extra ? "&browseType=$extra" : ''));
if($module == 'testreport' && $method == 'create') return helper::createLink($module, $method, "objectID=&objectType=testtask&extra=%s");
if($module == 'testtask' && $method == 'browse') return helper::createLink($module, $method, "productID=%s&branch={$branchID}");
+4 -4
View File
@@ -17,12 +17,12 @@ cid=0
- 当前页面是product-dynamic,切换1.5级导航时跳转链接。 @/product-dynamic-%s-.html
- 当前页面是project-bug,切换1.5级导航时跳转链接。 @/project-bug--%s.html
- 当前页面是qa-index,切换1.5级导航时跳转链接。 @/bug-browse-%s.html
- 当前页面是story-report,切换1.5级导航时跳转链接。 @/story-report-%s-all-.html
- 当前页面是story-report,切换1.5级导航时跳转链接。 @/story-report-%s-all-story.html
- 当前页面是testcase-browse,切换1.5级导航时跳转链接。 @/testcase-browse-%s-all.html
- 当前页面是testreport-create,切换1.5级导航时跳转链接。 @/testreport-create--testtask-%s.html
- 当前页面是testreport-edit,切换1.5级导航时跳转链接。 @/testreport-browse-%s.html
- 当前页面是testtask-browseUnits,切换1.5级导航时跳转链接。 @/testtask-browseUnits-%s-newest-id_desc-0-0-1.html
- 当前页面是testtask-browse,切换1.5级导航时跳转链接。 @/testtask-browse-%s-all-.html
- 当前页面是testtask-browse,切换1.5级导航时跳转链接。 @/testtask-browse-%s-all.html
- 当前页面是execution-bug,切换1.5级导航时跳转链接。 @/execution-bug--%s.html
- 当前页面是execution-testcase,切换1.5级导航时跳转链接。 @/execution-testcase--%s.html
- 当前页面是product-doc,切换1.5级导航时跳转链接。 @/product-doc-%s.html
@@ -81,7 +81,7 @@ r($productModel->getProductLink('product', 'project')) && p() && e('/pro
r($productModel->getProductLink('product', 'dynamic')) && p() && e('/product-dynamic-%s-.html'); //当前页面是product-dynamic,切换1.5级导航时跳转链接。
r($productModel->getProductLink('project', 'bug')) && p() && e('/project-bug--%s.html'); //当前页面是project-bug,切换1.5级导航时跳转链接。
r($productModel->getProductLink('qa', 'index')) && p() && e('/bug-browse-%s.html'); //当前页面是qa-index,切换1.5级导航时跳转链接。
r($productModel->getProductLink('story', 'report')) && p() && e('/story-report-%s-all-.html'); //当前页面是story-report,切换1.5级导航时跳转链接。
r($productModel->getProductLink('story', 'report')) && p() && e('/story-report-%s-all-story.html'); //当前页面是story-report,切换1.5级导航时跳转链接。
r($productModel->getProductLink('testcase', 'browse')) && p() && e('/testcase-browse-%s-all.html'); //当前页面是testcase-browse,切换1.5级导航时跳转链接。
r($productModel->getProductLink('testreport', 'create')) && p() && e('/testreport-create--testtask-%s.html'); //当前页面是testreport-create,切换1.5级导航时跳转链接。
r($productModel->getProductLink('testreport', 'edit')) && p() && e('/testreport-browse-%s.html'); //当前页面是testreport-edit,切换1.5级导航时跳转链接。
@@ -124,4 +124,4 @@ r($productModel->getProductLink('feedback', 'view')) && p() && e('/feedback-brow
$productModel->app->tab = 'project';
r($productModel->getProductLink('testcase', 'groupCase', '1')) && p() && e('/testcase-groupcase-%s-all--1.html#app=project'); //当前页面是testcase-groupcase,在项目视图下,切换1.5级导航时跳转链接。
r($productModel->getProductLink('testcase', 'zeroCase', 'projectID=1')) && p() && e('/testcase-zerocase-%s-all--1.html#app=project'); //当前页面是testcase-zerocase,在项目视图下,切换1.5级导航时跳转链接。
r($productModel->getProductLink('testcase', 'zeroCase', 'projectID=1')) && p() && e('/testcase-zerocase-%s-all--1.html#app=project'); //当前页面是testcase-zerocase,在项目视图下,切换1.5级导航时跳转链接。
-1
View File
@@ -8,7 +8,6 @@ $fields->field('parent')
->control(array('control' => 'picker', 'required' => true))
->items(data('parents'))
->value(data('parentProgram.id') ? data('parentProgram.id') : 0)
->disabled(!empty(data('parentProgram.id')))
->data('parentBudget', data('parentProgram.budget'))
->data('parentBegin', data('parentProgram.begin'))
->data('parentEnd', data('parentProgram.end'));
+2 -2
View File
@@ -238,7 +238,7 @@ window.handleRenderRow = function($row, index, data)
{
$row.find('[data-name="ACTIONS"]').find('[data-type="sort"]').addClass('disabled').attr('title', sortableTip);
$row.find('[data-name="ACTIONS"]').find('[data-type="addSibling"]').prop('disabled', true).attr('title', addSiblingTip);
$row.find('[data-name="ACTIONS"]').find('[data-type="delete"]').prop('disabled', true);
$row.find('[data-name="ACTIONS"]').find('[data-type="delete"]').addClass('hidden');
}
$row.find('[data-name="attribute"]').find('.picker-box').on('inited', function(e, info){ info[0].render({disabled: true}); });
@@ -525,4 +525,4 @@ window.moveChildren = function(id, processedIds = new Set())
$parent.after(element);
moveChildren($(element).attr('data-gid'), processedIds);
});
}
}
+6 -2
View File
@@ -76,8 +76,10 @@ class projectModel extends model
return $this->dao->select($fields)->from(TABLE_PROJECT)
->where('type')->eq('project')
->beginIF($this->config->vision)->andWhere('vision')->eq($this->config->vision)->fi()
->andWhere('deleted')->eq(0)
->andWhere('(deleted')->eq(0)
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
->orWhere('isTpl')->eq('1')
->markRight(1)
->orderBy('order_asc,id_desc')
->fetchAll('id');
}
@@ -215,7 +217,9 @@ class projectModel extends model
if(!$projectID) $projectID = $this->session->project ? $this->session->project : (int)key($projects);
}
if(!isset($projects[$projectID]))
/* 项目模板不校验访问权限。 */
$isTpl = $this->dao->select('isTpl')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('isTpl');
if(!$isTpl && !isset($projects[$projectID]))
{
if($projectID && strpos(",{$this->app->user->view->projects},", ",{$projectID},") === false && !empty($projects))
{
+2 -2
View File
@@ -37,8 +37,8 @@
#related .nav-item {gap: 0;}
.nav-tabs>li>a.active {font-weight: 700;color: #313c52!important}
#relationTabs .tab-content{overflow: auto;}
.repoCode .view-line-icon {position: absolute; right: 50px; top: 2px; font-weight: bold; cursor: pointer;background-color: white;padding: 1px 8px}
.repoCode .view-line-icon.add-bug {left: 5px;}
.repoCode .view-line-icon {position: absolute; top: 2px; font-weight: bold; cursor: pointer;background-color: white;padding: 1px 8px}
.repoCode .view-line-icon.add-bug {left: 10px;}
.repoCode .view-line-icon.add-bug .icon {font-weight: bold;}
.repoCode .view-line-icon .bug-count {vertical-align: middle; padding-left: 5px;}
.repoCode #bugForm {margin: 0; padding: 10px 20px 10px 0;}
+3 -1
View File
@@ -16,8 +16,10 @@ foreach($fieldParams as $name => $param)
$condition->name = $name;
$condition->control = $param['control'];
$condition->defaultOperator = $param['operator'];
if(isset($config->search->controlProps[$module][$name])) $condition->controlProps = $config->search->controlProps[$module][$name];
$condition->placeholder = '';
$condition->controlProps = isset($config->search->controlProps[$module][$name]) ? $config->search->controlProps[$module][$name] : array();
if($condition->control == 'select' && !isset($condition->controlProps['maxItemsCount'])) $condition->controlProps['maxItemsCount'] = 200;
if(isset($param['class']) && in_array('date', explode(' ', $param['class'])))
{
+13 -1
View File
@@ -935,7 +935,19 @@ class storyModel extends model
$this->action->logHistory($actionID, $changes);
if(isset($story->finalResult))
{
$action = $story->finalResult == 'clarify' ? 'ReviewClarified' : 'review' . $story->finalResult . 'ed';
if($story->finalResult == 'clarify')
{
$action = 'reviewclarified';
}
elseif($story->finalResult == 'active')
{
$action = 'reviewpassed';
}
else
{
$action = 'review' . $story->finalResult . 'ed';
}
$this->action->create('story', $storyID, $action, '', "{$story->finalResult}|$oldStatus");
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ $fields['story'] = array_merge(array('name' => 'story', 'label' => $la
$fields['name'] = array_merge(array('name' => 'name', 'label' => $lang->task->name, 'width' => '250px', 'ditto' => false, 'defaultDitto' => 'off', 'filter' => 'trim', 'control' => 'text'), $config->task->form->batchcreate['name']);
$fields['type'] = array_merge(array('name' => 'type', 'label' => $lang->typeAB, 'width' => '140px', 'ditto' => true, 'defaultDitto' => 'off', 'items' => $lang->task->typeList, 'control' => 'picker'), $config->task->form->batchcreate['type']);
$fields['assignedTo'] = array_merge(array('name' => 'assignedTo', 'label' => $lang->task->assignedTo, 'width' => '128px', 'ditto' => false, 'defaultDitto' => 'off', 'items' => $members, 'control' => array('control' => 'taskAssignedTo', 'manageLink' => ($manageLink ? $manageLink : ''))), $config->task->form->batchcreate['assignedTo']);
$fields['estimate'] = array_merge(array('name' => 'estimate', 'label' => $lang->task->estimateAB, 'width' => '120px', 'ditto' => true, 'defaultDitto' => 'on', 'filter' => 'trim', 'control' => 'text'), $config->task->form->batchcreate['estimate']);
$fields['estimate'] = array_merge(array('name' => 'estimate', 'label' => $lang->task->estimateAB, 'width' => '120px', 'ditto' => true, 'defaultDitto' => 'off', 'filter' => 'trim', 'control' => 'text'), $config->task->form->batchcreate['estimate']);
$fields['estStarted'] = array_merge(array('name' => 'estStarted', 'label' => $lang->task->estStarted, 'width' => '120px', 'ditto' => true, 'defaultDitto' => 'on', 'filter' => 'trim', 'control' => 'date'), $config->task->form->batchcreate['estStarted']);
$fields['deadline'] = array_merge(array('name' => 'deadline', 'label' => $lang->task->deadline, 'width' => '120px', 'ditto' => true, 'defaultDitto' => 'on', 'filter' => 'trim', 'control' => 'date'), $config->task->form->batchcreate['deadline']);
$fields['pri'] = array_merge(array('name' => 'pri', 'label' => $lang->task->pri, 'width' => '110px', 'ditto' => true, 'defaultDitto' => 'on', 'items' => $lang->task->priList, 'control' => 'priPicker'), $config->task->form->batchcreate['pri']);
+1 -1
View File
@@ -191,7 +191,7 @@ detailBody
(
array('text' => $lang->save, 'data-status' => 'active', 'class' => 'primary', 'btnType' => 'submit'),
array('text' => $lang->story->doNotSubmit, 'data-status' => 'draft', 'class' => 'secondary', 'btnType' => 'submit'),
array('text' => $lang->goback, 'data-back' => 'APP', 'class' => 'open-url')
isInModal() ? null : array('text' => $lang->goback, 'data-back' => 'APP', 'class' => 'open-url')
)),
sectionList($formItems),
h::hr(),
+18 -16
View File
@@ -293,21 +293,21 @@ class tutorialModel extends model
{
$execution = $this->getExecution();
$execution->progress = 0;
$execution->estimate = 0;
$execution->consumed = 0;
$execution->left = 0;
$execution->leftTasks = '—';
$execution->teamMembers = array_keys($this->getTeamMembers());
$execution->teamCount = count($execution->teamMembers);
$execution->hasProduct = '1';
$execution->multiple = '';
$execution->order = 1;
$execution->burns = array();
$execution->type = 'sprint';
$execution->projectName = '';
$execution->projectModel = 'scrum';
$execution->deliverable = '';
$execution->progress = 0;
$execution->estimate = 0;
$execution->consumed = 0;
$execution->left = 0;
$execution->leftTasks = '—';
$execution->teamMembers = array_keys($this->getTeamMembers());
$execution->teamCount = count($execution->teamMembers);
$execution->hasProduct = '1';
$execution->multiple = '';
$execution->order = 1;
$execution->burns = array();
$execution->type = 'sprint';
$execution->projectName = '';
$execution->projectModel = 'scrum';
$execution->deliverable = '';
if($browseType && $browseType != 'all') $execution->name .= '-' . $browseType;
@@ -542,7 +542,7 @@ class tutorialModel extends model
*/
public function getExecutionPairs(): array
{
return array(3 => 'Test execution');
return array(2 => 'Test Project', 3 => 'Test execution');
}
/**
@@ -639,6 +639,8 @@ class tutorialModel extends model
$execution->openedDate = helper::now();
$execution->closedDate = helper::now();
$execution->milestone = 0;
$execution->workflowGroup = 0;
$execution->deliverable = '';
list($guide, $guideTask, $guideStepIndex) = empty($_SERVER['HTTP_X_ZIN_TUTORIAL']) ? array('', '', '') : explode('-', $_SERVER['HTTP_X_ZIN_TUTORIAL']);
if($guide && strpos($guide, 'scrumProjectManage') !== false)
+6 -7
View File
@@ -67,7 +67,8 @@ $config->upgrade->maxVersion['max6_5'] = '21_5';
$config->upgrade->maxVersion['max6_6_beta'] = '21_6_beta';
$config->upgrade->maxVersion['max6_6'] = '21_6';
$config->upgrade->maxVersion['max6_6_1'] = '21_6_1';
$config->upgrade->maxVersion['max6_7'] = '21_7'; // max insert position.
$config->upgrade->maxVersion['max6_7'] = '21_7';
$config->upgrade->maxVersion['max7_0'] = '21_7_1'; // max insert position.
$config->upgrade->bizVersion = array();
$config->upgrade->bizVersion['biz1_0'] = '9_5_1';
@@ -173,7 +174,8 @@ $config->upgrade->bizVersion['biz11_5'] = '21_5';
$config->upgrade->bizVersion['biz11_6_beta'] = '21_6_beta';
$config->upgrade->bizVersion['biz11_6'] = '21_6';
$config->upgrade->bizVersion['biz11_6_1'] = '21_6_1';
$config->upgrade->bizVersion['biz11_7'] = '21_7'; // biz insert position.
$config->upgrade->bizVersion['biz11_7'] = '21_7';
$config->upgrade->bizVersion['biz12_0'] = '21_7_1'; // biz insert position.
$config->upgrade->proVersion = array();
$config->upgrade->proVersion['pro1_0'] = '3_1';
@@ -316,7 +318,8 @@ $config->upgrade->ipdVersion['ipd3_5'] = '21_5';
$config->upgrade->ipdVersion['ipd3_6_beta'] = '21_6_beta';
$config->upgrade->ipdVersion['ipd3_6'] = '21_6';
$config->upgrade->ipdVersion['ipd3_6_1'] = '21_6_1';
$config->upgrade->ipdVersion['ipd3_7'] = '21_7'; // ipd insert position.
$config->upgrade->ipdVersion['ipd3_7'] = '21_7';
$config->upgrade->ipdVersion['ipd4_0'] = '21_7_1'; // ipd insert position.
$config->upgrade->lowerTables = array();
$config->upgrade->lowerTables[$config->db->prefix . 'caseStep'] = $config->db->prefix . 'casestep';
@@ -1312,10 +1315,6 @@ $config->delete['20_1'][] = 'execution/ipd/testcase/ext/ui/view.effort.html.hook
$config->delete['20_1_1'][] = 'extension/max/design/ext/ui/browse.zentamax.html.hook.php';
$config->delete['20_1_1'][] = 'extension/max/doc/ext/ui/create.zentamax.html.hook.php';
$config->delete['20_1_1'][] = 'extension/max/doc/ext/view/create.zentamax.html.hook.php';
$config->delete['20_1_1'][] = 'extension/max/execution/ext/model/zentaomax.php';
$config->delete['20_1_1'][] = 'extension/ipd/execution/ext/model/zentaomax.php';
$config->delete['20_1_1'][] = 'extension/max/execution/ext/model/class/zentaomax.class.php';
$config->delete['20_1_1'][] = 'extension/ipd/execution/ext/model/class/zentaomax.class.php';
$config->delete['20_3_0'][] = 'extension/ipd/story/ext/tao/class/ipd.class.php';
$config->delete['20_3_0'][] = 'extension/ipd/story/ext/tao/ipd.php';
+8 -4
View File
@@ -212,7 +212,8 @@ $lang->upgrade->fromVersions['21_5'] = '21.5';
$lang->upgrade->fromVersions['21_6_beta'] = '21.6.beta';
$lang->upgrade->fromVersions['21_6'] = '21.6';
$lang->upgrade->fromVersions['21_6_1'] = '21.6.1';
$lang->upgrade->fromVersions['21_7'] = '21.7'; // pms insert position.
$lang->upgrade->fromVersions['21_7'] = '21.7';
$lang->upgrade->fromVersions['21_7_1'] = '21.7.1'; // pms insert position.
global $config;
/* Lite. */
@@ -425,7 +426,8 @@ $lang->upgrade->fromVersions['biz11_5'] = 'Biz11.5';
$lang->upgrade->fromVersions['biz11_6_beta'] = 'Biz11.6.beta';
$lang->upgrade->fromVersions['biz11_6'] = 'Biz11.6';
$lang->upgrade->fromVersions['biz11_6_1'] = 'Biz11.6.1';
$lang->upgrade->fromVersions['biz11_7'] = 'Biz11.7'; // biz insert position.
$lang->upgrade->fromVersions['biz11_7'] = 'Biz11.7';
$lang->upgrade->fromVersions['biz12_0'] = 'Biz12.0'; // biz insert position.
/* Max. */
$lang->upgrade->fromVersions['max2_0_beta4'] = 'Max2.0.beta4';
@@ -496,7 +498,8 @@ $lang->upgrade->fromVersions['max6_5'] = 'Max6.5';
$lang->upgrade->fromVersions['max6_6_beta'] = 'Max6.6.beta';
$lang->upgrade->fromVersions['max6_6'] = 'Max6.6';
$lang->upgrade->fromVersions['max6_6_1'] = 'Max6.6.1';
$lang->upgrade->fromVersions['max6_7'] = 'Max6.7'; // max insert position.
$lang->upgrade->fromVersions['max6_7'] = 'Max6.7';
$lang->upgrade->fromVersions['max7_0'] = 'Max7.0'; // max insert position.
/* Ipd */
$lang->upgrade->fromVersions['ipd1_0_beta1'] = 'Ipd1.0.beta1';
@@ -528,4 +531,5 @@ $lang->upgrade->fromVersions['ipd3_4'] = 'Ipd3.4';
$lang->upgrade->fromVersions['ipd3_5'] = 'Ipd3.5';
$lang->upgrade->fromVersions['ipd3_6_beta'] = 'Ipd3.6.beta';
$lang->upgrade->fromVersions['ipd3_6'] = 'Ipd3.6';
$lang->upgrade->fromVersions['ipd3_6_1'] = 'Ipd3.6.1'; // ipd insert position.
$lang->upgrade->fromVersions['ipd3_6_1'] = 'Ipd3.6.1';
$lang->upgrade->fromVersions['ipd3_7'] = 'Ipd3.7'; // ipd insert position.
+11 -3
View File
@@ -1910,7 +1910,8 @@ class userModel extends model
if(empty($account)) return $userView;
$userView = $this->dao->select('*')->from(TABLE_USERVIEW)->where('account')->eq($account)->fetch();
if(!empty($userView) && !$force) return $userView;
$isEmpty = empty($userView);
if(!$isEmpty && !$force) return $userView;
/* Init objects. */
list($allProducts, $allProjects, $allPrograms, $allSprints, $teams, $whiteList, $stakeholders) = $this->initViewObjects($force);
@@ -1945,7 +1946,14 @@ class userModel extends model
}
/* 更新访问权限表。 */
$this->dao->replace(TABLE_USERVIEW)->data($userView)->exec();
if($isEmpty)
{
$this->dao->insert(TABLE_USERVIEW)->data($userView)->exec();
}
else
{
$this->dao->update(TABLE_USERVIEW)->data($userView)->where('account')->eq($account)->exec();
}
return $userView;
}
@@ -2131,7 +2139,7 @@ class userModel extends model
$userView->sprints = rtrim($userView->sprints, ',') . ',' . join(',', $openedSprints);
$canViewSprints = $this->dao->select('executions')->from(TABLE_PROJECTADMIN)->where('account')->eq($account)->fetch('executions');
if($canViewSprints) $userView->sprints .= ',' . $canViewSprints;
if($canViewSprints != 'all') $userView->sprints .= ',' . $canViewSprints;
return $userView;
}
+203 -21
View File
@@ -55,13 +55,22 @@ class coverage
/**
* Save traces and restart code coverage.
*
* @param string $rawModule
* @param string $rawMethod
* @access public
* @return void
*/
public function saveAndRestartCodeCoverage(): void
public function saveAndRestartCodeCoverage(string $url = ''): void
{
$traces = xdebug_get_code_coverage();
$this->saveTraces($traces);
if($url)
{
$this->saveTracesByIndex($traces, $url);
}
else
{
$this->saveTraces($traces);
}
xdebug_stop_code_coverage();
xdebug_start_code_coverage();
@@ -114,6 +123,32 @@ class coverage
return file_put_contents($this->traceFile, json_encode($log));
}
/**
* Save traces to file.
*
* @param array $traces
* @access private
* @return bool
*/
private function saveTracesByIndex(array $traces, string $url): bool
{
$tracePath = $this->zentaoRoot . "/tmp/webcoverage";
if(!is_dir($tracePath)) mkdir($tracePath, 0777, true);
$traceFile = $tracePath . '/' . urlencode($url) . '.json';
$traces = $this->filterTraces($traces);
$traces = $this->groupTraceByIndex($traces);
$log = new stdclass;
$log->time = date('Y-m-d H:i:s');
$log->ztfPath = getenv('ZTF_REPORT_DIR');
$log->traces = $traces;
if(!$log->traces) return false;
return file_put_contents($traceFile, json_encode($log));
}
/**
* Compute the line coverage test method.
* @param array $traces
@@ -197,6 +232,61 @@ class coverage
return $methodCount;
}
/**
* 获取class方法行数。
* Get the total number of methods in class.
*
* @param string $moduleName
* @param string $type
* @access private
* @return int
*/
private function getClassMethodLines(string $moduleName, string $type): int
{
if(file_exists($this->getClassFile($moduleName, $type)) === false) return 0;
$classFileContents = file_get_contents($this->getClassFile($moduleName, $type));
$lines = explode("\n", $classFileContents);
$inComment = false;
$lineCount = 0;
foreach($lines as $line)
{
$trimmed = trim($line);
// 跳过class定义行
if(preg_match('/^class\s+/i', $trimmed)) continue;
// 跳过function定义行
if(strpos($trimmed, 'function') !== false) continue;
// 跳过php定义行
if(strpos($trimmed, '<?php') !== false || strpos($trimmed, '?>') !== false) continue;
// 处理多行注释
if($inComment)
{
if(strpos($trimmed, '*/') !== false) $inComment = false;
continue;
}
if(strpos($trimmed, '/*') === 0)
{
$inComment = true;
if(strpos($trimmed, '*/') !== false) $inComment = false;
continue;
}
// 移除单行注释
$cleanLine = preg_replace('@//.*$@', '', $line);
$cleanLine = preg_replace('@#.*$@', '', $cleanLine);
if(!empty(trim($cleanLine))) $lineCount++;
}
return $lineCount;
}
/**
* Get test method file.
*
@@ -279,6 +369,31 @@ class coverage
return $groupedTraces;
}
/**
* Group traces by module.
*
* @param array $traces
* @access private
* @return array
*/
private function groupTraceByIndex(array $traces): array
{
$groupedTraces = array();
foreach($traces as $filePath => $fileTrace)
{
$moduleName = $this->getModuleByFilePath($filePath);
if($moduleName == 'xuan') continue;
$fileName = basename($filePath, '.php');
$groupedTraces['module'] = $moduleName;
$groupedTraces['type'] = $fileName;
$groupedTraces['executeLines'] = $fileTrace;
}
return $groupedTraces;
}
/**
* Get current fileTrace belog to which module.
*
@@ -304,10 +419,11 @@ class coverage
* Generate module stats report.
*
* @param array $traces
* @param string $type
* @access public
* @return string
*/
private function genModuleStatsReport(array $traces): string
private function genModuleStatsReport(array $traces, string $type = ''): string
{
$summaryTable = <<<EOT
<table border=1 id='summaryTable'>
@@ -333,7 +449,7 @@ EOT;
foreach($traces as $module => $moduleTraces)
{
$summaryTable .= $this->genStatsTableByModule($module, $moduleTraces);
$summaryTable .= $this->genStatsTableByModule($module, $moduleTraces, $type);
}
$summaryTable .= '</tbody></table>' . PHP_EOL;
@@ -345,10 +461,11 @@ EOT;
*
* @param string $module
* @param array $moduleTraces
* @param string $reportType
* @access private
* @return void
*/
private function genStatsTableByModule(string $module, array $moduleTraces)
private function genStatsTableByModule(string $module, array $moduleTraces, string $reportType = '')
{
$modelMethodesCount = $this->getClassMethodCount($module, 'model');
$controlMethodesCount = $this->getClassMethodCount($module, 'control');
@@ -364,7 +481,11 @@ EOT;
foreach($moduleTraces as $type => $methods)
{
$moduleCoverageList[$type] = 0;
foreach($methods as $methodCoverage) $moduleCoverageList[$type] += $methodCoverage['coverage'];
foreach($methods as $methodCoverage)
{
if(empty($methodCoverage['coverage'])) continue;
$moduleCoverageList[$type] += $methodCoverage['coverage'];
}
}
$summaryTable = '<tr>' . PHP_EOL;
@@ -381,9 +502,23 @@ EOT;
foreach($this->unfilteredTraces as $fileType)
{
$fileType = str_replace('.php', '', $fileType);
$methodCount = $this->getClassMethodCount($module, $fileType);
$summaryTable .= isset($moduleCoverageList[$fileType]) ? "<td><a href='?module=$module&file=$fileType'>" . round($moduleCoverageList[$fileType] / $methodCount, 2) * 100 . '%</a></td>' . PHP_EOL : '<td>0%</td>' . PHP_EOL;
if(empty($moduleTraces[$fileType]))
{
$summaryTable .= '<td>0%</td>' . PHP_EOL;
continue;
}
if($reportType == 'web')
{
$methodLines = $this->getClassMethodLines($module, $fileType);
$execLines = empty($moduleTraces[$fileType]['executeLines']) ? 0 : count($moduleTraces[$fileType]['executeLines']);
$coverage = $methodLines == 0 ? 0 : round($execLines / $methodLines, 2) * 100;
$summaryTable .= "<td><a href='?module=$module&file=$fileType'>" . $coverage . '%</a></td>' . PHP_EOL;
}
else
{
$methodCount = $this->getClassMethodCount($module, $fileType);
$summaryTable .= "<td><a href='?module=$module&file=$fileType'>" . round($moduleCoverageList[$fileType] / $methodCount, 2) * 100 . '%</a></td>' . PHP_EOL;
}
}
$summaryTable .= '</tr>' . PHP_EOL;
@@ -396,10 +531,11 @@ EOT;
* @param string $module
* @param string $file
* @param array $fileTraces
* @param string $reportType
* @access private
* @return string
*/
private function genCoverageTableByFile(string $module, string $file, array $fileTraces): string
private function genCoverageTableByFile(string $module, string $file, array $fileTraces, string $reportType = ''): string
{
$file = $this->getClassFile($module, $file);
$coverageTable = <<<EOT
@@ -417,9 +553,10 @@ EOT;
$content = file($file);
$coverageLineList = array();
foreach($fileTraces as $method => $methodInfo)
foreach($fileTraces as $methodInfo)
{
foreach($methodInfo['executeLines'] as $executeLine => $count) $coverageLineList[$executeLine] = $count;
$executeLine = $reportType == 'web' ? $methodInfo : $methodInfo['executeLines'];
foreach($executeLine as $executeLine => $count) $coverageLineList[$executeLine] = $count;
}
foreach($content as $line => $code)
@@ -444,10 +581,11 @@ EOT;
*
* @param string $filePath
* @param string $key
* @param string $type
* @return array|string|false
*
*/
public function loadTraceFromFiles(string $filePath, string $key = ''): array|string|false
public function loadTraceFromFiles(string $filePath, string $key = '', $type = ''): array|string|false
{
$tracesFiles = glob("{$filePath}/*.json");
if(!$tracesFiles) return false ;
@@ -456,11 +594,25 @@ EOT;
foreach($tracesFiles as $file)
{
$tracesInfo = json_decode(file_get_contents($file), true);
if(!isset($tracesInfo['traces'])) continue;
if(empty($tracesInfo['traces'])) continue;
$traces = $tracesInfo['traces'];
$tracesList['traces'][$traces['module']][$traces['type']][$traces['method']]['executeLines'] = $traces['executeLines'];
$tracesList['traces'][$traces['module']][$traces['type']][$traces['method']]['coverage'] = $traces['coverage'];
if($type == 'web')
{
if(empty($tracesList[$traces['module']][$traces['type']]['executeLines']))
{
$tracesList['traces'][$traces['module']][$traces['type']]['executeLines'] = $traces['executeLines'];
}
else
{
$tracesList['traces'][$traces['module']][$traces['type']]['executeLines'] = array_merge($tracesList[$traces['module']][$traces['type']]['executeLines'], $traces['executeLines']);
}
}
else
{
$tracesList['traces'][$traces['module']][$traces['type']][$traces['method']]['executeLines'] = $traces['executeLines'];
$tracesList['traces'][$traces['module']][$traces['type']][$traces['method']]['coverage'] = $traces['coverage'];
}
$tracesList['time'] = $tracesInfo['time'];
$tracesList['ztfPath'] = $tracesInfo['ztfPath'];
@@ -499,15 +651,44 @@ EOT;
return $reportHtml;
}
/**
* Generate web summary report.
*
* @param string $module
* @param string $file
* @return string
*/
public function genWebSummaryReport(string $module='', string $file=''): string
{
/* Get trace from file. */
$tracesPath = $this->zentaoRoot . '/tmp/webcoverage';
$traces = $this->loadTraceFromFiles($tracesPath, 'traces', 'web');
/* Generate report. */
$reportHtml = empty($file) ? '<style>td { border: 1px solid #ccc; padding: 8px; text-align: center;}</style>' . PHP_EOL : '<style>td { border: 1px solid #ccc; padding: 8px;}</style>' . PHP_EOL;
if(empty($file))
{
$reportHtml .= $this->genModuleStatsReport($traces, 'web');
}
else
{
$reportHtml .= $this->genCoverageTableByFile($module, $file, $traces[$module][$file], 'web');
}
$reportHtml .= '</body></html>';
return $reportHtml;
}
/**
* Get ztf report.
*
* @param string $type
* @access public
* @return object
*/
public function getZtfReport(): object|false
public function getZtfReport($type = ''): object|false
{
$reportFile = $this->getZtfReportFile();
$reportFile = $this->getZtfReportFile($type);
if(!$reportFile) return false;
$content = file_get_contents($reportFile);
@@ -528,15 +709,16 @@ EOT;
/**
* Get ztf report.
*
* @param string $type
* @access public
* @return string|false
*/
public function getZtfReportFile(): string|false
public function getZtfReportFile($type): string|false
{
$latestTime = 0;
$latestFile = '';
$tracePath = $this->zentaoRoot . "/tmp/coverage/";
$reportPath = $this->loadTraceFromFiles($tracePath, 'ztfPath');
$reportPath = $this->loadTraceFromFiles($tracePath, 'ztfPath', $type);
exec("find $reportPath -type f -name result.json", $files, $returnCode);
if($returnCode !== 0 || empty($files)) return false;
+1 -2
View File
@@ -14,8 +14,7 @@ if($ztfReport)
}
else
{
$ztfPath = $coverage->loadTraceFromFile('ztfPath');
$ztfHtml = "<p>在{$ztfPath}/ 没有找到ZTF测试报告。</p>";
$ztfHtml = "<p>没有找到ZTF测试报告。</p>";
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+136
View File
@@ -0,0 +1,136 @@
<?php
include_once(dirname(__FILE__, 2) . "/test/lib/coverage.php");
global $zentaoRoot;
$zentaoRoot = dirname(__FILE__, 2);
$type = isset($_GET['module']) ? 'module' : 'summary';
$coverage = new coverage();
$report = '';
$ztfReport = $coverage->getZtfReport('web');
if($ztfReport)
{
$ztfHtml = "<div class='report'><strong>%s</strong> 执行 <strong>%s个</strong>用例,耗时 <strong>%s秒</strong>。<strong>%s (%s%%) </strong>通过,<strong>%s (%s%%)</strong> 失败,<strong>%s (%s%%)</strong> 忽略。</div>";
$ztfHtml = sprintf($ztfHtml, $ztfReport->time, $ztfReport->total, $ztfReport->duration, $ztfReport->pass, $ztfReport->passPercent, $ztfReport->fail, $ztfReport->failPercent, $ztfReport->skip, $ztfReport->skipPercent);
}
else
{
$ztfHtml = "<p>没有找到ZTF测试报告。</p>";
}
switch($type)
{
case 'summary':
$report = $coverage->genWebSummaryReport();
break;
case 'module':
$module = $_GET['module'];
$file = $_GET['file'];
$report = $coverage->genWebSummaryReport($module, $file);
break;
default:
$report = $coverage->genWebSummaryReport();
break;
}
?>
<!DOCTYPE html>
<html lang="zh-cn" xml:lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>单元测试行覆盖率报告</title>
</head>
<style>
body {font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5; margin: 0; padding: 20px; }
table {border-collapse: collapse; max-width: 100%; width: 100%; margin: 20px 0; }
th {border: 1px solid #ccc; padding: 8px; text-align: center; background-color: #eee; white-space: nowrap; }
caption{font-weight: bold; margin: 10px 0; font-size: 18px; }
h2 {margin-top: 20px; font-size: 24px; }
.red {color: red; }
.green {color: green; }
tbody tr:hover {background-color: #f5f5f5; }
tbody tr:nth-child(even) {background-color: #f9f9f9; }
h1 {text-align: center; }
</style>
<body>
<h1>单元测试行覆盖率报告</h1>
<?php
echo $ztfHtml;
echo $report;
echo "<script>var type = '$type';</script>";
?>
<script src="./js/jquery/lib.js"></script>
<script>
$().ready(function()
{
if(type == 'summary')
{
renderColorByCoveragePercent()
implementExpand();
implementSort();
}
});
function implementSort()
{
var table = $('#summaryTable');
var tbody = table.find('tbody');
var rowsArr = tbody.find('tr').toArray();
rowsArr.sort(function(row1, row2)
{
/* Get seventh row and translate it's value into int. */
var val1 = $(row1).find('th:eq(0)').text();
var val2 = $(row2).find('th:eq(0)').text();
if (val1 < val2)
{
return -1;
}
else if (val1 > val2)
{
return 1;
}
else
{
return 0;
}
});
$.each(rowsArr, function(index, row)
{
tbody.append(row);
});
}
function renderColorByCoveragePercent()
{
$('table tbody tr td').each(function()
{
var text = $(this).text();
if(text.indexOf('%') > -1)
{
var percent = parseInt(text);
if(percent < 50)
{
$(this).css('color', 'red');
}
else if(percent < 80)
{
$(this).css('color', 'orange');
}
else
{
$(this).css('color', 'green');
}
}
});
}
function implementExpand()
{
$("tr[name$='-child']").hide();
$("tr[name$='-parent']").click(function()
{
$(this).nextUntil("tr[name$='-parent']").slideToggle('fast');
});
}
</script>