zentao二次开发FAQ

非专业php选手,兼职Zentao开发。

1. 前言

在筹备整个DevOps推进时,针对软件研发的需求管理,笔者在多方调研之后其实更为倾向JIRA,但奈何公司已经在禅道的使用上渐行渐远,胳膊拧不过大腿之下只能将注意力集中到禅道上。

年初在花费了半个月的时间将禅道的官方文档整个捋了一遍,并经过半年努力终于在部分部门内部开始被真正接受,接着就是无数基于公司自身流程特点,和操作便捷性等的需求开始不断涌现,活生生把笔者这个以Javaer逼成了个半吊子php选手。

以下从一个半吊子选手的视角,总结一些在禅道扩展开发过程中,经常会遇到的需求和相关的解决方案。

2. 起手式

从一个php外行选手,到能够进行常规禅道自定义扩展需求的开发,首先你需要的肯定是阅读相关的官方文档,这里笔者贴出认为比较有必要的:

类别相关链接说明
禅道整体流程,以及所包含的相关功能的介绍1. 禅道开源版 - 最简使用
2. 禅道开源版 - 基本使用
3. 禅道开源版 - 进阶使用
之所以你需要先了解这些内容,是因为大部分使用禅道的人对禅道的了解非常有限,有时候他们提出的所谓"扩展"其实禅道已经内置了,这个时候作为扩展开发人员的你就应该有这方面的辨识能力,别直接闷头干了还洋洋自得。
禅道扩展机制1. 禅道开源版 - 定制开发
2. zentaoPHP框架手册
3. zentaoPHP二次开发
1. 其中第一个禅道开源版 - 定制开发是需要最先读的,首先是其包含的项少,内容也都是一些新手需要尽快熟悉的,例如"找到要修改的文件""如何登记权限"等等。
2. 另外两个链接里的内容也是需要尽快了解的,它会告诉如何更好地进行禅道的扩展——例如你应该将扩展代码写在哪里,而不是在禅道现有源码上直接开干。

关于禅道的数据库结构,官方文档上存在延迟,建议直接去禅道里在线查看:
禅道 - 数据库

3. 代码块snippet

3.1 后端backend

// =========================== 当前用户
$this->app->user->account

// =========================== 当前时间
//	helper类定义在 ./framework/base/helper.class.php 中                                                                                    义的DT_DATETIME1常量。
$now   = helper::now();

// =========================== 当前时间,经常用于数据库操作时
// 在 {zentaoRootPath}module\common\lang\zh-cn.php 定义,上面的 helper::now() 正是基于它又封了一层。
DT_DATETIME1


// =========================== base64编码
#   范例: D:\xampp\zentao\module\user\control.php   ajaxGetMore()  方法中
base64_decode($str1)   比如如果前端传递来的参数不可控,可以考虑前端进行base64编码,后端进行相应deocode一下,确保参数信息不丢失。

// =========================== 基于模板生成文本, 类似Java中的Velocity, Freemaker, Thymeleaf.
#	
// 1. 以下赋值的变量, 在模板文件中可以直接引用
$this->view->versionDownloadUrl      = $versionDownloadUrl;
// 从数据库查询信息, 作为模板文件的数据源
$xTestReport = $this->dao->select("*")
	->from("zt_x_testreport")
	->where("releaseID")->eq((int)$releaseID)
	->fetch();	
$this->view->xTestReport      = $xTestReport;
// 2. 临时设置当前viewTyle为html, 确保模板文件被正确转换
$oldViewType = $this->viewType;
if($oldViewType == 'json') $this->viewType = 'html';
// 3. 实际的转换工作
$release->desc  = $release->desc . "<br/> <br/>" . $this->parse('release', 'testReportOfRelease');
$this->viewType == $oldViewType;
// 4. 关键. 确保模板转换不会影响当前主体页面的绘制.
$this->clear();

// =========================== HTTP调用
// POST
$response = common::http(sprintf("http://{ip}:{port}/xxx.action?name=%s&pswd=%s","xuxiantao","XuXian123"), '', array("x"=>"123"));

// GET
$response = common::http(sprintf("http://{ip}:{port}/xxx.action?name=%s&pswd=%s","xuxiantao","XuXian123"));
3.2 前端frontend
# 按钮不可用
class="disabled"

# 分组按钮

D:\xampp\zentao\module\caselib\view\browse.html.php  56<div class="btn-group dropdown-hover">
  <button class="btn btn-link" data-toggle="dropdown"><i class="icon icon-export muted"></i> <span class="text"><?php echo $lang->export;?></span> <span class="caret"></span></button>
  <ul class="dropdown-menu pull-right" id='exportActionMenu'>
	<?php
	$class = common::hasPriv('task', 'export') ? '' : "class=disabled";
	$misc  = common::hasPriv('task', 'export') ? "class='export'" : "class=disabled";
	$link  = common::hasPriv('task', 'export') ? $this->createLink('task', 'export', "project=$projectID&orderBy=$orderBy&type=$browseType") : '#';
	echo "<li class='disabled'>" . html::a($link, "导出用例", '', "class='export'") . "</li>";
	?>
  </ul>
 </div>

# 多选框
// 样例参见: http://zentaoIp:zentaoPort/zentao/my-managecontacts-0-new.html
<?php
 echo html::select('users[]', ["1","2"], '', "multiple class='form-control chosen' data-drop_direction='down'");
 ?>

4. 常用数据库操作

# D:\xampp\zentao\lib\dao\dao.class.php

# 简单的
$originCaselib = $this->dao->select("*")->from(TABLE_TESTSUITE)
			->where("id")->eq($libID)
			->andWhere("type")->eq("library")
			->andWhere("deleted")->eq("0")
			->fetch();

# 各类fetch
fetch()
fetch("id")   # 返回值以 "id" 为key
fetchPairs("name", "name") # 返回值以name所查询到的对应值为key, name所查询到的对应值为value; 形成键值对
fetchAll()

# 左连接
$unfinishedStory = $this->dao->select("s.province, COUNT(1) AS 'total'")->from(TABLE_STORY)->alias("s")
	->leftJoin(TABLE_PROJECTSTORY)->alias("p")->on("p.story = s.id")
	->where("p.project")->eq($projectID)
	->andWhere("s.deleted")->eq("0")
	->groupBy('s.province')
	->fetchAll();


# 直接执行SQL语句
$result = $this->dao->query(sprintf("select COUNT(1) AS recTotal from (select t1.objectID from zt_action t1
	left join `zt_task` AS t2 ON t1.objectID = t2.id 
	LEFT JOIN `zt_project` AS t3  ON t1.project = t3.id
	where  t1.actor  = '%s' AND  t1.objectType  = 'task'
	group by t1.objectid) as z", $account))->fetch(PDO::FETCH_OBJ);

5. 关于调试

如此重要,以至于单独辟出一小节进行介绍

# 直接在页面上打印
var_dump($var1);

# 弹出框
die(js::alert(json_encode($var1)));

# Tips 
1. 辅助以 if($this->app->user->account === 'admin')  可以实现调试和用户使用并行,得益于php的动态编译,只要别出现语法问题,禅道使用者是感知不到的。


# 日志文件
这是非常重要的一环了。
1. 开启详细日志,你需要去 D:\xampp\zentao\config\my.php  配置文件中 将debug项设置为true2. 生成的日志文件存放位置:D:\xampp\zentao\tmp\log 。按天生成,其中:
	2.1 sql.{yyyyMMdd}.log.php  执行过的SQL,按每次请求进行分隔,非常便于阅读和理解。

6. 一些关键文件

文件路径含义
lib\base\front\front.class.php封装常见的前端工具类代码,例如html::select()的方法就是定义在这个里面
lib\base\dao\dao.class.php数据库操作的方法
www\js\my.full.jsajaxDelete() 方法 ajax处理前端代码范例
module\common\model.php包含多个全局方法,例如hasPriv()方法就是用来检查当前访问是否满足权限?

7. 完整示例

7.1 新增菜单项

Office Site - 如何登记菜单

实际例子:

# 这一步的作用应该就是将 module\common\ext\lang\zh-cn/kanq.php 中定义的  
$lang->project->menu->baobiao    = array('link' => 'kanq报表|project|kanqbaobiao|projectID=%s', 'alias' => 'edit,start,suspend,putoff,close');
#	将其中的 %s 进行变量替换
# 范例参见 D:\xampp\zentao\module\project\model.php 中的 setMenu($projects, $projectID, $buildID = 0, $extra = '') 方法
# 同样的操作出现在 product\\model.php 中的 setMenu($products, $productID, $branch = 0, $module = 0, $moduleType = '', $extra = '') 方法底部
common::setMenuVars($this->lang->project->menu, $key, $projectID);


# 为了确保 确保同级的菜单可以正常点击回去,  你需要在自己定义的菜单按钮响应control方法中调用如下方法
#	该方法定义在基类中
$this->commonAction($projectID);
7.2 登记菜单权限

Office Site - 如何登记权限

##### D:\xampp\zentao\module\group\ext\lang\zh-cn\  路径下 (其它语言同理)

### 新建XXX.php,名字无所谓,望文知意即可
# 1. 下面这句的目的是在  http://xxx.xx.xx.xx:zzz/zentao/group-managepriv-byGroup-1.html   权限配置页面下显示我们定义的权限
$lang->resource->project->zzzbaobiao="zzzbaobiao";
# 2. 这一句是配置上面的 "zzzbaobiao" 对应的中文名 , 同样在XXX.php文件中
$lang->project->zzzbaobiao="kanq报表";
# 3. 访问禅道页面 http://xxx.xx.xx.xx:zzz/zentao/group-managepriv-byGroup-1.html   为对应group配置 kanq报表权限
下图

在这里插入图片描述

7.3 datatable操作

先看看效果:
在这里插入图片描述1. 基础的datatable展示信息。
2. 复选框,可以进行指定项选中。
3. 底部的统计栏,根据选中项来动态统计相关数据。

参考示例说明:

内容
参考模块{zentaoRootPath}\module\product\view\browse.html.php
参考模块访问URLhttp://{CONTEXT}/product-browse-{productID}.html
额外相关moduledatatable

示例代码:

// =========================== HTML页面
<!-- 这两个模块让datatable正常显示 -->
<?php include '../../common/view/header.html.php';?>
<?php include '../../common/view/datatable.fix.html.php';?>

<!--  data-ride="table" 千万别加在form上; 否则底部js失效 --> 
<form id='projectUsersForm' class="main-table table-xXXXtask" method="post">
  <div class="table-responsive">
    <table class="table has-sort-head table-fixed" id='zprnameList'>
      <?php $vars = "type=$type&orderBy=%s&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"; ?>
      <thead>
        <tr>
		<?php
		$setting = $this->datatable->getSetting('project');
		foreach($setting as $key => $value) { if($value->show){$this->datatable->printHead($value, "zprname", "", true);} }
		?>
		 <!-- 当然你也可以不用上面的迭代方式, 直接用下面这种 -->
         <!-- <th class='c-pri w-40px'>       <?php common::printOrderLink('zprname',     $orderBy, $vars, "名称");?></th> -->
        </tr>
      </thead>
      <tbody>
        <?php foreach($tasks as $task):?>
			// 这里自定义的data-xx 是用作复选框选中时, 统计用的
			<tr data-id='<?php echo $task->zprname?>' data-total='<?php echo $task->total?>'>
		   // 这里的 printXxxxCell 是需要你自定义的, 范例参考: {zentaoRootPath}/module/story/model.php ; {zentaoRootPath}/module/task/model.php 中的同名方法
			<?php foreach($setting as $key => $value) $this->project->printXxxxCell($value, $task);?>
		 
			// 当然你也可以不用上面的迭代方式, 直接用下面这种
			// <td class='c-total'><?php echo $task->total;?></td>

        </tr>
        <?php endforeach;?>
      </tbody>
    </table>
  </div>
  <div class="table-footer">
	<!-- 底部的全选按钮 -->
	<div class="checkbox-primary check-all"><label><?php echo $lang->selectAll?></label></div>
	<!-- 统计信息栏, 内容随着用户选中datatable中每行的复选框而变化 -->
	<div class="table-statistic"><?php echo $summary;?></div>
  </div>
</form>

<!-- 统计栏更新 -->
<script>
// <script> 中不能有 type="text/javascript" 
$(function()
{
	
	$(function()
	{   // COPY FROM: {zentaoRootPath}\module\project\view\task.html.php
		// Update table summary text
		$('#projectUsersForm').table(
		{
			statisticCreator: function(table)
			{
				.....
				return "XXX";
			}
		})
	});

});
</script>
// =========================== Control
public function index($projectID = '', $type = 'personstatus')
{
	
	/* Load datatable. */
	// 用作view层
	$this->loadModel('datatable');
	// 确保同级的菜单可以正常点击回去, 该方法定义在基类中
	$this->commonAction($projectID);


	$this->view->projectID  = $projectID;
	$this->view->tasks      = $this->loadModel('xXXX')->statisticByUser($projectID);
	$this->view->title      = "人员状态统计";
	
	......
	
	$this->display();
}

// =============================== CONFIG
$config->x->datatable = new stdclass();
$config->x->datatable->defaultField = array('id');

$config->x->datatable->fieldList['id']['title']    = '名称';
$config->x->datatable->fieldList['id']['fixed']    = 'left';
$config->x->datatable->fieldList['id']['width']    = '60';
$config->x->datatable->fieldList['id']['required'] = 'yes';


// =============================== {zentaoRootPath}\module\datatable\config.php
// 这个文件中注册datatable时候需要查找的配置项; 以下配置项的意思是: 当调用 x 模块的index方法来展示datatable时, 使用到的配置项目为 $config->x 这就和上面的CONFIG 对应上了
$config->datatable->moduleAlias['x-index'] = 'x';

以上文件按下图进行放置:(别怀疑,这个例子是笔者专门建的,所以名字虽然看着随意点,但确实是有效的)
在这里插入图片描述

7.4 表单操作

表单操作常出现在需要新增业务字段时。

7.4.1 必填字段
// ------------------- HTML
// # 表单必填验证 : class="required"
<?php echo html::input('province', $story->province, "class='form-control' required");?>

// ------------------ CONFIG
$config->story->create->requiredFields = 'title, province';

// ------------------ LANG
$lang->story->province = "省";

// ------------------ MODLE 
$requiredFields = "," . $this->config->story->create->requiredFields . ",";
$requiredFields = trim($requiredFields, ',');

$this->dao->insert(TABLE_STORY)->data($story, 'spec,verify')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec();
7.4.2 字段必须为数值类型

范例参见: {zentaoRootPath}\module\task\view\recordestimate.html.php

// ------------------- HTML
//  {zentaoRootPath}\module\task\view\recordestimate.html.php
<td><?php echo html::input("consumed[$i]", '', "class='form-control text-center'");?></td>

// ------------------ MODLE 
// {zentaoRootPath}\module\task\model.php recordEstimate($taskID)方法
foreach($record->left as $id => $item) if(!is_numeric($item) and !empty($item)) dao::$errors[] = 'ID #' . $id . ' ' . $this->lang->task->error->leftNumber;
if(dao::isError()) return false;
7.4.3 Ajax操作

# ajax请求
// ====================== 前端
<?php
	$mirrorUrl = $this->createLink('kanq', 'mirrorCaseLib', "libID=$libID&confirm=yes");
	// 注意这里的 class='btn  btn-link' 不要添加 export 的标记
	echo html::a("javascript:ajaxDelete(\"$mirrorUrl\", \"xx\", \"镜像当前用例库?\")", '<i class="icon icon-copy"></i>', '', "title='镜像当前用例库' class='btn  btn-link'");
?>  

相应的 ajaxDelete() 定义在 ./www/js/my.full.js 文件中。

// ====================== 后端
// 示例为: 
 /* if ajax request, send result. */
 if($this->server->ajax)
 {
     if(dao::isError())
     {
         $response['result']  = 'fail';
         $response['message'] = dao::getError();
     }
     else
     {
         $response['result']  = 'kanq';
         $response['message'] = '启动成功! 请前往<a href="http://admin:admin@172.16.3.231:8080/jenkins/job/%E6%8E%A5%E5%8F%A3%E6%B5%8B%E8%AF%95/job/000-LQ-BatchExecuteJmxByModule/"  target="_blank" rel="noopener" ><strong style="font-size: 20px;">JMeter执行控制台</strong></a>查看进度';
     }
     $this->send($response);
 }
 die(js::reload('parent'));

7.5 甘特图

这里只出后台代码部分。(笔者个人是非常赞同花点钱支持国产的,奈何公司… ┑( ̄Д  ̄)┍)

<?php 
include '../../control.php'; 
class myProject extends project
{
    /**

     */
    public function gantt($projectID = '', $ganttType='assignedTo')
    {
		// 确保同级的菜单可以正常点击回去, 该方法定义在基类中
		$this->commonAction($projectID);
			
		$projectData = $this->dao->select("t.id, t.pri, DATE_FORMAT(t.estStarted,'%d-%m-%Y') AS start_date, CONCAT('#', t.id,t.name) AS text, t.deadline, t.assignedTo AS owner_id, datediff(deadline, estStarted) AS duration, 'true' as open, CONCAT('-' ,u.id) AS parent, u.realName AS userName")->from(TABLE_TASK)->alias("t")
		->leftJoin(TABLE_USER)->alias("u")->on("t.assignedTo = u.account AND u.deleted = '0'")
		->where("t.project")->eq($projectID)
		->andWhere("t.deleted")->eq("0")
		->andWhere("t.status")->ne("closed")
		->fetchAll("id");
		
		$userNameList = array();
		foreach($projectData as $taskID=>$task){
			// 未正确设置"预计开始时间"的任务,直接不绘制相应的甘特图进度条; 效果见下方小节
			if($task->start_date == "00-00-0000"){
				$task->unscheduled = true;
			}
			if(array_key_exists($task->owner_id, $userNameList)){
				continue;
			}
			$userNameList[$task->owner_id] = $task;
		}	

		$userList = array();
		foreach($userNameList as $userAccount=>$user){
			$item = new stdclass();
			$item->id = $user->parent;
			$item->text = $user->userName;
			$item->parent = "0";
			$item->progress = "0";
			$projectData[] = $item;
			
			//
			$item2 = new stdclass();
			$item2->key = $userID;
			$item2->value = $user->userName;
			$userList[] = $item2;
		}

		$projectData = array("data" => array_values($projectData));		
		$this->view->projectData = json_encode($projectData);
		$this->view->userList = $userList;
		$this->view->projectName = "XXX";
		
		$this->view->ganttType = $ganttType;
		$this->view->projectID = $projectID;
		
		$this->display();
    }
	
}
7.5.1 优化 - 未填写预计开始时间的任务不绘制进度

言语太苍白,咱们直接上图说明。

禅道中如果任务被创建时候没有输入"预计开始时间"字段,则在禅道中其默认值为"0000-00-00"。这种情况下的甘特图会出现非常长的横向滚动条,如下图:
在这里插入图片描述
遇到上面这种情况,其实最正确的方式应该是将任务打回,要求相应人员正确填写;但奈何业务压力大,相关管理人员能力尚不足等原因,我们只能退而求其次地先消除这些超长进度条。

最终在官网文档下找到了这篇API说明 - Unscheduled Tasks , 只需要将相应地数据源中添加一个名为unscheduled=true的键值对即可。最终效果如下:
在这里插入图片描述

7.6 搜索栏增加额外查询字段

依然是先看效果:
在这里插入图片描述

可以看出,zentao在扩展性方面下了不少功夫,以上需求只需要修改一个配置文件即可。

// D:\xampp\zentao\module\product\config.php

$config->product->search['fields']['province']    = "所在省";
// 这里如果进一步优化,可以设置为选择框,而非输入框
$config->product->search['params']['province'] = array('operator' => 'include', 'control' => 'input',  'values' => '');
7.7 datatable默认列名的自定义

禅道里的datatable列表中的默认展示列有两种定义形式,一种是在 xxx.html.php 中直接使用<table><th>xx</th></table>进行定义,这种方式对有开发经验的人员进行自定义扩展来说很直观。

除此之外的第二种方式则如下图 —— 直接使用界面化配置的方式实现 (这种方式的好处是无需研发资源的再次投入,当事用户即可自己实现自定义需求)。
在这里插入图片描述

本小节的关注点就是对于上述第二种形式,如何实现默认展示列的调整。

相关的修改涉及两个位置:

  1. 配置文件 config.php 。这里定义了默认需要展示的列名集合。用户初次使用禅道时所展示的列名正是来自这里。
  2. 数据库表 zt_config 。 这里定义了每个用户自定义的需要需要列名集合。
# 1. 配置文件 `config.php` (定义datatable默认展示列)
# 样例:{zentaoRootPath}\module\bug\config.php
# 注意:调整完这里之后,还需要删除数据库表zt_config中的对应记录,让本配置生效; 具体参见接下来马上出现的步骤2
$config->bug->datatable->defaultField = array('id', 'severity', 'pri', 'title', 'status', 'openedDate', 'deadline', , 'type', 'assignedTo', 'resolution', 'actions');

# 2. 数据库表 `zt_config`
# 上面截图里用户自定义的列配置最终会被存放在这里. 而且如果在这里没有找到,则会将上面config.php里的配置往这里写一份.  (这也是为啥本次二开会同时涉及配置文件和数据库的原因)
delete from zt_config where section = 'bugBrowse'  and `key` = 'tablecols'
select * from zt_config where section = 'bugBrowse'  and `key` = 'tablecols' and owner = 'admin'


8. 参考

  1. PHP 教程 - W3school
  2. 禅道 - 最简使用
  3. Zentao - 二次开发文档
  4. zentaoPHP框架提供的DAO功能
  5. 依靠工具,让员工天天简简单单的管理自己的工作。 - 没错,笔者就是那"谁喝多了搞禅道二次开发的"。
  6. dhtmlx - Gantt API
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值