Merge branch dev/story-73930 of zentao/zentaopms (#8835)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
完成的需求
|
||||
开源版:
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Vendored
+51
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'};
|
||||
}
|
||||
|
||||
@@ -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: {}},
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
|
||||
/* 半透明黄色 */
|
||||
.monaco-editor .yellow-decoration {background-color: rgba(255, 255, 0, 0.2);}
|
||||
|
||||
.monaco-editor .margin .line-numbers {padding-left: 16px;}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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('#', '');
|
||||
|
||||
@@ -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
@@ -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是否可关闭
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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' => '');
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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级导航时跳转链接。
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
+16
-16
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
Reference in New Issue
Block a user