diff --git a/module/bug/css/create.css b/module/bug/css/create.css
index 746ea7a8b8..f7b68c49fb 100644
--- a/module/bug/css/create.css
+++ b/module/bug/css/create.css
@@ -57,4 +57,4 @@ html[lang='de'] #keywordsAddonLabel, .task {width: 73px;}
#openedBuildLabel {display: inline; position: relative; width: 50%; left: 14%;}
#bugTypeInputGroup > .table-col {width: 40% !important;}
-.pri-text > .label-pri {padding: 1px 5px;}
+.pri-text > .label-pri {padding: 2px 6px;}
diff --git a/module/execution/control.php b/module/execution/control.php
index acce62c2ea..e3d6492020 100644
--- a/module/execution/control.php
+++ b/module/execution/control.php
@@ -1579,6 +1579,7 @@ class execution extends control
$execution = $this->commonAction($executionID);
$executionID = $execution->id;
+ $deptID = $this->app->user->admin ? 0 : $this->app->user->dept;
$title = $execution->name . $this->lang->colon . $this->lang->execution->team;
$position[] = html::a($this->createLink('execution', 'browse', "executionID=$executionID"), $execution->name);
@@ -1586,7 +1587,7 @@ class execution extends control
$this->view->title = $title;
$this->view->position = $position;
- $this->view->deptUsers = $this->loadModel('dept')->getDeptUserPairs($this->app->user->dept, 'id');
+ $this->view->deptUsers = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id');
$this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable.
$this->display();
diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php
index ce258165b5..decdee30da 100644
--- a/module/execution/lang/zh-cn.php
+++ b/module/execution/lang/zh-cn.php
@@ -351,8 +351,8 @@ $lang->execution->howToUpdateBurn = "关联{$lang->SRCommon}。";
-$lang->execution->haveBranchDraft = "有%s条草稿状态或非本{$lang->executionCommon}关联分支的{$lang->SRCommon}无法关联到该{$lang->executionCommon}";
-$lang->execution->haveDraft = "有%s条草稿状态的{$lang->SRCommon}无法关联到该{$lang->executionCommon}";
+$lang->execution->haveBranchDraft = "有%s条非激活状态或不是{$lang->executionCommon}关联分支的{$lang->SRCommon}无法导入";
+$lang->execution->haveDraft = "有%s条非激活状态的{$lang->SRCommon}无法导入";
$lang->execution->doneExecutions = '已结束';
$lang->execution->selectDept = '选择部门';
$lang->execution->selectDeptTitle = '选择一个部门的成员';
diff --git a/module/project/control.php b/module/project/control.php
index 76f901238a..6a5787404b 100755
--- a/module/project/control.php
+++ b/module/project/control.php
@@ -1610,11 +1610,12 @@ class project extends control
$this->project->setMenu($projectID);
$project = $this->project->getById($projectID);
+ $deptID = $this->app->user->admin ? 0 : $this->app->user->dept;
$this->view->title = $project->name . $this->lang->colon . $this->lang->project->team;
$this->view->projectID = $projectID;
$this->view->teamMembers = $this->project->getTeamMembers($projectID);
- $this->view->deptUsers = $this->loadModel('dept')->getDeptUserPairs($this->app->user->dept, 'id');
+ $this->view->deptUsers = $this->loadModel('dept')->getDeptUserPairs($deptID, 'id');
$this->view->canBeChanged = common::canModify('project', $project);
$this->display();
diff --git a/module/project/model.php b/module/project/model.php
index 21467dd1af..8d4afe8a87 100644
--- a/module/project/model.php
+++ b/module/project/model.php
@@ -1583,7 +1583,7 @@ class projectModel extends model
->exec();
if(dao::isError()) return false;
- if(!$oldProject->hasProduct and ($oldProject->name != $project->name or $oldProject->parent != $project->parent)) $this->updateShadowProduct($project);
+ if(!$oldProject->hasProduct and ($oldProject->name != $project->name or $oldProject->parent != $project->parent or $oldProject->acl != $project->acl)) $this->updateShadowProduct($project);
/* Get team and language item. */
$this->loadModel('user');
@@ -1744,7 +1744,7 @@ class projectModel extends model
if(!dao::isError())
{
- if(!$oldProject->hasProduct and ($oldProject->name != $project->name or $oldProject->parent != $project->parent)) $this->updateShadowProduct($project);
+ if(!$oldProject->hasProduct and ($oldProject->name != $project->name or $oldProject->parent != $project->parent or $oldProject->acl != $project->acl)) $this->updateShadowProduct($project);
if(isset($project->parent))
{
@@ -2048,7 +2048,7 @@ class projectModel extends model
{
$product = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($project->id)->fetch('product');
$topProgram = !empty($project->parent) ? $this->loadModel('program')->getTopByID($project->parent) : 0;
- $this->dao->update(TABLE_PRODUCT)->set('name')->eq($project->name)->set('program')->eq($topProgram)->where('id')->eq($product)->exec();
+ $this->dao->update(TABLE_PRODUCT)->set('name')->eq($project->name)->set('program')->eq($topProgram)->set('acl')->eq($project->acl)->where('id')->eq($product)->exec();
return !dao::isError();
}
diff --git a/module/projectrelease/control.php b/module/projectrelease/control.php
index e69975257a..abcca88bf6 100644
--- a/module/projectrelease/control.php
+++ b/module/projectrelease/control.php
@@ -249,13 +249,14 @@ class projectrelease extends control
if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort);
$storyPager = new pager($type == 'story' ? $recTotal : 0, $recPerPage, $type == 'story' ? $pageID : 1);
- $stories = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY)
- ->where('id')->in($release->stories)
- ->andWhere('deleted')->eq(0)
+ $stories = $this->dao->select("t1.*,t2.id as buildID, t2.name as buildName,IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY)->alias('t1')
+ ->leftJoin(TABLE_BUILD)->alias('t2')->on("FIND_IN_SET(t1.id, t2.stories)")
+ ->where('t1.id')->in($release->stories)
+ ->andWhere('t1.deleted')->eq(0)
->beginIF($type == 'story')->orderBy($sort)->fi()
->page($storyPager)
->fetchAll('id');
- $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story');
+ $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false);
$stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->in(array_keys($stories))->andWhere('branch')->eq($release->branch)->fetchPairs('story', 'stage');
foreach($stages as $storyID => $stage) $stories[$storyID]->stage = $stage;
@@ -304,6 +305,7 @@ class projectrelease extends control
$this->view->storyPager = $storyPager;
$this->view->bugPager = $bugPager;
$this->view->leftBugPager = $leftBugPager;
+ $this->view->builds = $this->loadModel('build')->getBuildPairs($release->product, 'all', 'withbranch|hasproject', 0, 'execution', '', false);
$this->display();
}
diff --git a/module/projectrelease/view/view.html.php b/module/projectrelease/view/view.html.php
index b3ef89b38b..19ed80d00d 100644
--- a/module/projectrelease/view/view.html.php
+++ b/module/projectrelease/view/view.html.php
@@ -71,6 +71,7 @@
priAB);?> |
story->title);?> |
+ build->common;?> |
openedByAB);?> |
story->estimateAB);?> |
statusAB);?> |
@@ -98,6 +99,7 @@
echo html::a($storyLink,$story->title, '', "class='preview'");
?>
+ buildName?> |
openedBy);?> |
estimate . $config->hourUnit;?> |
@@ -167,6 +169,7 @@
| bug->title);?> |
bug->status);?> |
+ bug->resolvedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
bug->resolvedByAB);?> |
@@ -191,6 +194,8 @@
processStatus('bug', $bug);?>
|
+ resolvedBuild, '');?>
+ |
openedBy);?> |
openedDate) ? '' : substr($bug->openedDate, 5, 11);?> |
resolvedBy);?> |
@@ -257,6 +262,7 @@
bug->severity;?>>bug->severityAB);?> |
bug->title);?> |
bug->status);?> |
+ bug->openedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
actions;?> |
@@ -295,6 +301,11 @@
title, '', "class='preview'");?> |
processStatus('bug', $bug);?> |
+ openedBuild) as $buildID) $openedBuildName .= zget($builds, $buildID, '') . ' ';
+ ?>
+ |
openedBy);?> |
openedDate?> |
diff --git a/module/release/control.php b/module/release/control.php
index 1fedaf1531..5e2b5508f0 100644
--- a/module/release/control.php
+++ b/module/release/control.php
@@ -185,14 +185,15 @@ class release extends control
if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort);
$storyPager = new pager($type == 'story' ? $recTotal : 0, $recPerPage, $type == 'story' ? $pageID : 1);
- $stories = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY)
- ->where('id')->in($release->stories)
- ->andWhere('deleted')->eq(0)
+ $stories = $this->dao->select("t1.*,t2.id as buildID, t2.name as buildName, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder")->from(TABLE_STORY)->alias('t1')
+ ->leftJoin(TABLE_BUILD)->alias('t2')->on("FIND_IN_SET(t1.id, t2.stories)")
+ ->where('t1.id')->in($release->stories)
+ ->andWhere('t1.deleted')->eq(0)
->beginIF($type == 'story')->orderBy($sort)->fi()
->page($storyPager)
->fetchAll('id');
- $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story');
+ $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false);
$stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->in(array_keys($stories))->andWhere('branch')->eq($release->branch)->fetchPairs('story', 'stage');
foreach($stages as $storyID => $stage) $stories[$storyID]->stage = $stage;
@@ -238,6 +239,7 @@ class release extends control
$this->view->storyPager = $storyPager;
$this->view->bugPager = $bugPager;
$this->view->leftBugPager = $leftBugPager;
+ $this->view->builds = $this->loadModel('build')->getBuildPairs($release->product, 'all', 'withbranch|hasproject', 0, 'execution', '', false);
if($this->app->getViewType() == 'json')
{
diff --git a/module/release/view/view.html.php b/module/release/view/view.html.php
index 6ec80c73bd..727313e5d5 100644
--- a/module/release/view/view.html.php
+++ b/module/release/view/view.html.php
@@ -73,6 +73,7 @@
| pri;?>>priAB);?> |
story->title);?> |
+ build->common;?> |
openedByAB);?> |
story->estimateAB);?> |
statusAB);?> |
@@ -100,6 +101,7 @@
echo html::a($storyLink,$story->title, '', "class='preview'");
?>
+ buildName?> |
openedBy);?> |
estimate . $config->hourUnit;?> |
@@ -169,6 +171,7 @@
| bug->title);?> |
bug->status);?> |
+ bug->resolvedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
bug->resolvedByAB);?> |
@@ -193,6 +196,8 @@
processStatus('bug', $bug);?>
|
+ resolvedBuild, '');?>
+ |
openedBy);?> |
openedDate, 5, 11)?> |
resolvedBy);?> |
@@ -259,6 +264,7 @@
bug->severity;?>>bug->severityAB);?> |
bug->title);?> |
bug->status);?> |
+ bug->openedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
actions;?> |
@@ -297,6 +303,11 @@
title, '', "class='preview'");?> |
processStatus('bug', $bug);?> |
+ openedBuild) as $buildID) $openedBuildName .= zget($builds, $buildID, '') . ' ';
+ ?>
+ |
openedBy);?> |
openedDate?> |
diff --git a/module/testsuite/control.php b/module/testsuite/control.php
index 98b4023dc1..87254e67b7 100644
--- a/module/testsuite/control.php
+++ b/module/testsuite/control.php
@@ -89,7 +89,7 @@ class testsuite extends control
$privateNum++;
}
}
- $suitesNum = !empty(count($suites, 0)) ? count($suites, 0) : 0;
+ $suitesNum = !empty($suites) ? count($suites) : 0;
$publicNum = $suitesNum - $privateNum;
$summary = str_replace(array('%total%', '%public%', '%private%'), array($suitesNum, $publicNum, $privateNum), $this->lang->testsuite->summary);
diff --git a/module/upgrade/model.php b/module/upgrade/model.php
index 8edafef735..f3262ff000 100644
--- a/module/upgrade/model.php
+++ b/module/upgrade/model.php
@@ -756,6 +756,9 @@ class upgradeModel extends model
case 'max3_3':
$this->addReviewIssusApprovalData();
break;
+ case 'max4_0_beta1':
+ $this->initReviewEfforts();
+ break;
}
}
@@ -7969,4 +7972,31 @@ class upgradeModel extends model
}
return true;
}
+
+ /**
+ * Init review efforts.
+ *
+ * @access public
+ * @return void
+ */
+ public function initReviewEfforts()
+ {
+ $nodes = $this->dao->select('t1.id,t3.id as reviewID,t3.title,t2.id as approvalID,t1.extra as consumed')->from(TABLE_APPROVALNODE)->alias('t1')
+ ->leftJoin(TABLE_APPROVAL)->alias('t2')->on("t1.approval=t2.id")
+ ->leftJoin(TABLE_REVIEW)->alias('t3')->on("t2.objectID=t3.id")
+ ->where('t3.deleted')->eq('0')
+ ->andWhere('t2.deleted')->eq('0')
+ ->andWhere('t2.objectType')->eq('review')
+ ->andWhere('t1.extra')->ne('')
+ ->andWhere('t1.extra')->ne(0)
+ ->orderBy('t1.approval,t1.id')
+ ->fetchAll('id');
+ $this->loadModel('effort');
+ foreach($nodes as $node)
+ {
+ $this->dao->delete()->from(TABLE_EFFORT)->where('objectType')->eq('review')->andWhere('objectID')->eq($node->reviewID)->exec();
+ $this->effort->create('review', $node->reviewID, (int)$node->consumed, $node->title, $node->approvalID);
+ }
+ return true;
+ }
}
diff --git a/www/theme/default/style.css b/www/theme/default/style.css
index 39a049cd7c..d878316af2 100644
--- a/www/theme/default/style.css
+++ b/www/theme/default/style.css
@@ -219,11 +219,11 @@ tbody tr td.c-actions .dividing-line {width: 1px; height: 16px; display: inline-
.pri-text > .label-pri {padding: 1px 5px;}
.pri-text > .label-pri, .pri-text > .label-severity {height: 20px;}
-.pri-text > .label-severity {background-size: 80%; background-position: 2px -130px;}
-.pri-text > .label-severity[data-severity="1"] {color: #FB2B2B; background-position: 2px 1px;}
-.pri-text > .label-severity[data-severity="2"] {color: #FF8058; background-position: 2px -31px;}
-.pri-text > .label-severity[data-severity="3"] {color: #FAAE1A; background-position: 2px -64px;}
-.pri-text > .label-severity[data-severity="4"] {color: #B89664; background-position: 2px -97px;}
+.pri-text > .label-severity {background-size: 85%; background-position: 1px -139px;}
+.pri-text > .label-severity[data-severity="1"] {color: #FB2B2B; background-position: 2px 0px;}
+.pri-text > .label-severity[data-severity="2"] {color: #FF8058; background-position: 2px -34px;}
+.pri-text > .label-severity[data-severity="3"] {color: #FAAE1A; background-position: 1px -69px;}
+.pri-text > .label-severity[data-severity="4"] {color: #B89664; background-position: 1px -104px;}
.flow-block .footerbar span.label-success {background-color: #0DBB7D;}
.flow-block .footerbar span.label-default {background-color: #9EA3B0;}
diff --git a/xuanxuan/extension/xuan/im/ext/bot/zentao.bot.php b/xuanxuan/extension/xuan/im/ext/bot/zentao.bot.php
new file mode 100644
index 0000000000..940530236b
--- /dev/null
+++ b/xuanxuan/extension/xuan/im/ext/bot/zentao.bot.php
@@ -0,0 +1,991 @@
+name = '禅道';
+ if($_SESSION['clientLang'] == 'zh-tw') $this->name = '禪道';
+ }
+ }
+
+ /**
+ * Init bot, load model.
+ *
+ * @access public
+ * @return void
+ */
+ public function init()
+ {
+ $this->lang = $this->im->lang->im->bot->zentaoBot;
+
+ foreach(array('view', 'start', 'close', 'finish') as $command)
+ {
+ $this->commands[] = array('command' => $command, 'alias' => $this->lang->commands->$command->alias, 'description' => $this->lang->commands->$command->description, 'internal' => true);
+ }
+
+ $this->im->loadModel('task');
+ $this->im->loadModel('user');
+ $this->im->app->loadClass('pager', $static = true);
+
+ $this->users = array_filter($this->im->user->getPairs('noclosed|noletter'));
+ $this->taskStatusList = array_filter($this->im->lang->task->statusList);
+ $this->help = $this->lang->help;
+
+ $this->inited = true;
+ }
+
+ /**
+ * Close command.
+ *
+ * @param array $args bot command arguments
+ * @access public
+ * @return object|string
+ */
+ public function close($args = array())
+ {
+ $args = $this->removeEqualSign($args);
+ foreach($args as $key => $value)
+ {
+ if(in_array(strtolower($value), $this->lang->condKeywords['task'])) unset($args[$key]);
+ }
+ $task = $this->closeTask($args);
+ return $task;
+ }
+
+ /**
+ * Finish command.
+ *
+ * @param array $args bot command arguments
+ * @access public
+ * @return object|string
+ */
+ public function finish($args = array())
+ {
+ $originArgs = $args;
+ $args = array_filter(explode('=', implode('=', $args)));
+ foreach($args as $key => $value)
+ {
+ if(in_array(strtolower($value), $this->lang->condKeywords['task'])) unset($args[$key]);
+ }
+ return $this->finishTask($args, $originArgs);
+ }
+
+ /**
+ * View command.
+ *
+ * @param array $args bot command arguments
+ * @param int $userID
+ * @param object $user
+ * @access public
+ * @return object|string
+ */
+ public function view($args = array(), $userID = 0, $user = null)
+ {
+ $pager = pager::init(0, 10, 1);
+
+ $pageSearchRegex = $this->lang->pageSearchRegex;
+ $pageIDKeywords = $this->lang->condKeywords['pageID'];
+
+ $args = array_filter($args, function($arg) use ($pager, $pageSearchRegex, $pageIDKeywords)
+ {
+ $matches = array();
+ preg_match($pageSearchRegex, $arg, $matches);
+ if(count($matches) > 2)
+ {
+ if(in_array(strtolower($matches[1]), $pageIDKeywords))
+ {
+ $pager->pageID = $matches[2];
+ }
+ else
+ {
+ $pager->recPerPage = $matches[2];
+ }
+ return false;
+ }
+ return true;
+ });
+ $originArgs = $args;
+
+ $args = $this->removeEqualSign($args);
+
+ foreach($args as $key => $value)
+ {
+ if(in_array(strtolower($value), $this->lang->condKeywords['task']))
+ {
+ unset($args[$key]);
+ $tasks = $this->viewTask($args, $user, $pager);
+ return $this->renderTask($tasks, $originArgs, $pager);
+ }
+ }
+ }
+
+ /**
+ * Parse bot command arguments.
+ *
+ * @param array $args bot command arguments
+ * @param array $keys
+ * @access public
+ * @return object
+ */
+ public function parseArguments($args = array(), $keys = array())
+ {
+ $assignedToList = '';
+ $priList = '';
+ $statusList = '';
+ $idList = '';
+ $taskName = '';
+ $comment = '';
+ while(!empty($args))
+ {
+ $arg = array_shift($args);
+ if(isset($keys['pri']))
+ {
+ if(in_array(strtolower($arg), $this->lang->condKeywords['pri']))
+ {
+ $pris = array_shift($args);
+ $priList = $this->buildCondParamByModifier($pris, $priList, 'p');
+ continue;
+ }
+ if(preg_match('/p[1-4]/i', $arg) == 1)
+ {
+ $priList = $this->buildCondParamByModifier($arg, $priList, 'p');
+ continue;
+ }
+ }
+ if(isset($keys['id']))
+ {
+ if(in_array(strtolower($arg), $this->lang->condKeywords['id']))
+ {
+ $ids = array_shift($args);
+ $idList .= $this->buildCondParamByModifier($ids, $idList, '#');
+ continue;
+ }
+ if(preg_match('/#\d+/', $arg) == 1)
+ {
+ $idList .= $this->buildCondParamByModifier($arg, $idList, '#');
+ continue;
+ }
+ }
+ if(isset($keys['status']))
+ {
+ if(in_array(strtolower($arg), $this->lang->condKeywords['status']))
+ {
+ $statuses = array_shift($args);
+ $statusList = $this->buildCondParam($statuses, $statusList, $this->taskStatusList);
+ continue;
+ }
+ if($this->checkCondType($arg, $this->taskStatusList))
+ {
+ $statusList = $this->buildCondParam($arg, $statusList, $this->taskStatusList);
+ continue;
+ }
+ }
+ if(isset($keys['assignTo']))
+ {
+ if(in_array(strtolower($arg), $this->lang->condKeywords['assignTo']))
+ {
+ $usernames = array_shift($args);
+ $assignedToList = $this->buildCondParam($usernames, $assignedToList, $this->users);
+ continue;
+ }
+ if($this->checkCondType($arg, $this->users))
+ {
+ $assignedToList = $this->buildCondParam($arg, $assignedToList, $this->users);
+ continue;
+ }
+ }
+ if(isset($keys['comment']))
+ {
+ if(in_array(strtolower($arg), $this->lang->condKeywords['comment']))
+ {
+ $comment = array_shift($args);
+ }
+ else {
+ $comment = $arg;
+ }
+ continue;
+ }
+ if(isset($keys['taskName']))
+ {
+ if(in_array(strtolower($arg), $this->lang->condKeywords['taskName']))
+ {
+ $taskName = array_shift($args);
+ }
+ else
+ {
+ $taskName = $arg;
+ }
+ }
+ }
+
+ $conds = new stdClass();
+ if(isset($keys['pri'])) $conds->priList = trim($priList, ',');
+ if(isset($keys['assignTo'])) $conds->assignedToList = trim($assignedToList, ',');
+ if(isset($keys['status'])) $conds->statusList = trim($statusList, ',');
+ if(isset($keys['id'])) $conds->idList = trim($idList, ',');
+ if(isset($keys['taskName'])) $conds->taskName = $taskName;
+ if(isset($keys['comment'])) $conds->comment = $comment;
+ return $conds;
+ }
+
+ /**
+ * Check condition type.
+ *
+ * @param string $arg
+ * @param array $list
+ * @access public
+ * @return bool
+ */
+ public function checkCondType($arg, $list)
+ {
+ $args = explode(',', $arg);
+ $array_inter = array_intersect($args, $list);
+ if(!empty($array_inter)) return true;
+
+ $list = array_flip($list);
+ $array_inter = array_intersect($args, $list);
+ if(!empty($array_inter)) return true;
+
+ return false;
+ }
+
+ /**
+ * Build condition param.
+ *
+ * @param string $arg bot command arguments
+ * @param string $condParam
+ * @param array $condArr
+ * @access public
+ * @return string
+ */
+ public function buildCondParam($arg, $condParam, $condArr)
+ {
+ $arr = explode(',', $arg);
+ foreach($arr as $cond)
+ {
+ if(array_key_exists($cond, $condArr))
+ {
+ $condParam .= ',' . $cond;
+ }
+ else
+ {
+ $key = array_search($cond, $condArr);
+ if($key !== false) $condParam .= ',' . $key;
+ }
+ }
+ return $condParam;
+ }
+
+ /**
+ * Remove equal sign from bot command arguments.
+ *
+ * @param array $args bot command arguments
+ * @access public
+ * @return array
+ */
+ public function removeEqualSign($args)
+ {
+ return array_filter(explode('=', implode('=', $args)));
+ }
+
+ /**
+ * Build condition param by modifier.
+ *
+ * @param array $args bot command arguments
+ * @param string $condParam
+ * @param string $modifier
+ * @access public
+ * @return string
+ */
+ public function buildCondParamByModifier($arg, $condParam, $modifier)
+ {
+ $arg = strtolower($arg);
+ $arg = str_replace($modifier, '', $arg);
+ $condParam .= ',' . $arg;
+ return $condParam;
+ }
+
+ /**
+ * View task command.
+ *
+ * @param array $args bot command arguments
+ * @param object $user
+ * @param object $pager
+ * @access public
+ * @return object|string
+ */
+ public function viewTask($args = array(), $user = null, $pager = null)
+ {
+ if(empty($args))
+ {
+ $conds = new stdClass();
+ $conds->assignedToList = is_object($user) ? $user->account : false;
+ $conds->statusList = 'wait,doing,done,pause,cancel';
+ return $this->im->task->getListByConds($conds, 'status_asc', 0, $pager);
+ }
+
+ $keys = array();
+ $keys['pri'] = true;
+ $keys['id'] = true;
+ $keys['status'] = true;
+ $keys['assignTo'] = true;
+ $keys['taskName'] = true;
+
+ $conds = $this->parseArguments($args, $keys);
+ return $this->im->task->getListByConds($conds, 'status_asc', 0, $pager);
+ }
+
+ /**
+ * Close task command.
+ *
+ * @param array $args bot command arguments
+ * @access public
+ * @return object|string
+ */
+ public function closeTask($args)
+ {
+ $keys = array();
+ $keys['id'] = true;
+ $keys['comment'] = true;
+
+ $conds = $this->parseArguments($args, $keys);
+ $tasks = array_filter(explode(',', $conds->idList));
+ $taskID = array_pop($tasks);
+ $comment = $conds->comment;
+
+ if(!is_numeric($taskID)) return $this->lang->errors->invalidCommand;
+
+ $task = $this->loadEntry('task', 'get', array('taskID'=> $taskID));
+ if(empty($task)) return $this->lang->errors->emptyResult;
+
+ if($task->status != 'done' && $task->status != 'cancel') return sprintf($this->lang->errors->invalidStatus, $this->taskStatusList[$task->status]);
+
+ $task = $this->loadEntry('taskclose', 'post', array('taskID' => $taskID, 'comment' => $comment));
+ if($task->result == 'fail') return $task->message;
+
+ $link = str_replace('x.php', 'index.php', helper::createLink('task', 'view', "taskID=$taskID", 'html'));
+ $messages = new stdClass();
+ $messages->type = 'url';
+ $messages->url = common::getSysURL() . $link;
+ return array($this->lang->success, $messages);
+ }
+
+ /**
+ * Finish task.
+ *
+ * @param array $args
+ * @param array $originArgs
+ * @access public
+ * @return object|string
+ */
+ private function finishTask($args, $originArgs)
+ {
+ $taskID = '';
+ $params = array();
+ while(!empty($args))
+ {
+ $arg = array_shift($args);
+ if(in_array(strtolower($arg), $this->lang->condKeywords['id']))
+ {
+ $taskID = array_shift($args);
+ $taskID = str_replace('#', '', $taskID);
+ }
+ elseif(preg_match('/#\d+/', $arg) == 1)
+ {
+ $taskID = str_replace('#', '', $arg);
+ }
+ else
+ {
+ parse_str(str_replace('amp;', '', end($originArgs)), $params);
+ }
+ }
+
+ if(!is_numeric($taskID)) return $this->lang->errors->invalidCommand;
+
+ $task = $this->loadEntry('task', 'get', array('taskID'=> $taskID));
+ if(!common::hasPriv('task', 'finish')) return $this->lang->errors->unauthorized;
+
+ if(empty($task) || isset($task->error)) return $this->lang->errors->emptyResult;
+ if($task->status != 'wait' && $task->status != 'doing') return sprintf($this->lang->errors->invalidStatus, $this->taskStatusList[$task->status]);
+
+ $reply = new stdClass();
+ $reply->messages = array();
+ $reply->responses = array();
+
+ $realStarted = $this->formatDate($task, 'realStarted');
+ $finishedDate = $this->formatDate($task, 'finishedDate');
+
+ if(empty($params))
+ {
+ $originCommand = rawurlencode($this->lang->finishTask . ' ' . implode(' ', $originArgs));
+
+ $json = (object)array
+ (
+ 'title' => "#{$taskID} {$task->name}",
+ 'submitLabel' => $this->im->lang->task->finish,
+ 'inputs' =>
+ array(
+ (object)array
+ (
+ 'name' => $this->im->lang->task->hasConsumed,
+ 'type' => 'input',
+ 'label' => $this->im->lang->task->hasConsumed,
+ 'value' => $task->consumed,
+ 'disabled' => true,
+ 'addon' => $this->im->lang->task->hour,
+ ),
+ (object)array
+ (
+ 'name' => $this->im->lang->task->currentConsumed,
+ 'type' => 'input',
+ 'label' => $this->im->lang->task->currentConsumed,
+ 'value' => 0,
+ 'addon' => $this->im->lang->task->hour,
+ ),
+ (object)array
+ (
+ 'name' => $this->im->lang->task->realStarted,
+ 'type' => 'datetime-local',
+ 'label' => $this->im->lang->task->realStarted,
+ 'value' => $realStarted,
+ ),
+ (object)array
+ (
+ 'name' => $this->im->lang->task->finishedDate,
+ 'type' => 'datetime-local',
+ 'label' => $this->im->lang->task->finishedDate,
+ 'value' => $finishedDate,
+ ),
+ ),
+ );
+ $json = rawurlencode(json_encode($json));
+
+ $reply->messages[] = $this->lang->finish->tip;
+ $reply->messages[] = "[{$this->lang->finish->tipLinkTitle}](xxc://openFormAndSendToServerBySendbox/$json/$originCommand)";
+ }
+ else
+ {
+ $messageID = isset($params['messageId']) ? $params['messageId'] : '';
+ unset($params['messageId']);
+
+ $currentConsumed = 0;
+
+ foreach($params as $key => $value)
+ {
+ if(in_array(strtolower($key), $this->lang->condKeywords['realStarted'])) $realStarted = $value;
+ if(in_array(strtolower($key), $this->lang->condKeywords['finishedDate'])) $finishedDate = $value;
+ if(in_array(strtolower($key), $this->lang->condKeywords['currentConsumed'])) $currentConsumed = $value;
+ };
+
+ if(empty($realStarted) || empty($finishedDate)) return '';
+
+ $params = array
+ (
+ 'currentConsumed' => $currentConsumed,
+ 'realStarted' => $realStarted,
+ 'finishedDate' => $finishedDate,
+ );
+
+ $task = $this->loadEntry('taskfinish', 'post', array_merge(array('taskID' => $taskID, 'assignedTo' => '', 'comment' => ''), $params));
+ if(isset($task->error))
+ {
+ foreach($task->error as $error) $reply->messages[] = is_array($error) ? implode(',', $error): $error;
+ return $reply;
+ }
+
+ $finishedDate = $this->formatDate($task, 'finishedDate', '', 'Y-m-d H:i:s');
+
+ $reply->messages[] = sprintf($this->lang->finish->done, $taskID, $finishedDate, $task->consumed);
+ $reply->messages[] = $this->renderTaskTable(array($task));
+
+ $reply->responses[] = $this->disableMarkdownLinkInMessage($messageID);
+
+ if(!empty($task->fromBug))
+ {
+ $reply->messages[] = sprintf($this->lang->finish->bugTip, $taskID);
+ $link = commonModel::getSysURL() . str_replace('x.php', 'index.php', helper::createLink('bug', 'view', "bugID={$task->fromBug}", 'html'));
+ $href = urlencode($link);
+ $reply->messages[] = "[{$this->lang->finish->bugTipLinkTitle}](xxc:openInApp/zentao-integrated/{$href})";
+ }
+ }
+
+ return $reply;
+ }
+
+ /**
+ * Format date.
+ *
+ * @param object $task
+ * @param string $field
+ * @param string $default
+ * @param string $format
+ * @return string
+ */
+ private function formatDate($task, $field, $default = '', $format = 'Y-m-d\TH:i')
+ {
+ try
+ {
+ $datetime = new DateTime(empty($task->$field) ? 'now' : $task->$field, new DateTimeZone('UTC'));
+ $datetime->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ return $datetime->format($format);
+ }
+ catch(Exception $e)
+ {
+ return $default;
+ }
+ }
+
+ /**
+ * Render tasks as table html.
+ *
+ * @param array $tasks
+ * @param array $originArgs
+ * @param pager $pager
+ * @return string|array
+ */
+ public function renderTask($tasks, $originArgs, $pager)
+ {
+ $lang = $this->im->lang;
+ if(!$tasks || $pager->recTotal === 0) return $lang->task->noTask;
+
+ $sysURL = common::getSysURL();
+
+ if($pager->recTotal == 1)
+ {
+ $task = current($tasks);
+ $link = str_replace('x.php', 'index.php', helper::createLink('task', 'view', "taskID=$task->id", 'html'));
+
+ $messages = new stdclass();
+ $messages->type = 'url';
+ $messages->url = $sysURL . $link;
+ return array(sprintf($this->lang->tasksFound, $pager->recTotal), $messages);
+ }
+
+ $messages = array();
+ if($pager->pageID == 1)
+ {
+ $messages[] = sprintf($this->lang->tasksFound, $pager->recTotal);
+ }
+
+ $taskTable = $this->renderTaskTable($tasks);
+
+ $originCommand = $this->lang->viewTask . ' ' . implode(' ', $originArgs);
+ $paging = "$pager->pageID / $pager->pageTotal";
+ if($pager->pageID != 1)
+ {
+ $previousPageCommand = $originCommand . " {$this->lang->condKeywords['pageID'][1]}=" . ($pager->pageID - 1) . " {$this->lang->condKeywords['recPerPage'][1]}=" . $pager->recPerPage;
+ $previousPageCommand = rawurlencode($previousPageCommand);
+ $paging .= " [{$this->lang->prevPage}](xxc://sendContentToServerBySendbox/{$previousPageCommand})";
+ }
+
+ if($pager->pageID != $pager->pageTotal)
+ {
+ $nextPageCommand = $originCommand . " {$this->lang->condKeywords['pageID'][1]}=" . ($pager->pageID + 1) . " {$this->lang->condKeywords['recPerPage'][1]}=" . $pager->recPerPage;
+ $nextPageCommand = rawurlencode($nextPageCommand);
+ $paging .= " [{$this->lang->nextPage}](xxc://sendContentToServerBySendbox/{$nextPageCommand})";
+ }
+ $paging = " $paging ";
+
+ $messages[] = $taskTable . $paging;
+
+ return $messages;
+ }
+
+ /**
+ * Render tasks as markdown table.
+ *
+ * @param array $tasks
+ * @access public
+ * @return void
+ */
+ public function renderTaskTable($tasks)
+ {
+ $lang = $this->im->lang;
+ $sysURL = common::getSysURL();
+ $headMap = array('id'=>'ID', 'pri' => $lang->task->pri, 'name' => $lang->task->name, 'assignedTo' => $lang->task->assignedTo, 'status' => $lang->task->status, 'estimate' => $lang->task->estimateAB, 'consumed' => $lang->task->consumedAB, 'left' => $lang->task->leftAB, 'actions' => $lang->actions);
+ $thead = '';
+
+ foreach($headMap as $value) $thead .= " | {$value} | ";
+
+ $thead = "{$thead}
";
+ $tbody = '';
+ foreach($tasks as $task)
+ {
+ $tr = '';
+ $link = str_replace('x.php', 'index.php', helper::createLink('task', 'view', "taskID=$task->id", 'html'));
+ $href = urlencode($sysURL . $link);
+ $isParent = $task->parent == -1;
+ $isChild = $task->parent > 0;
+ $isMulti = !empty($task->mode);
+ foreach($headMap as $key => $value)
+ {
+ switch($key)
+ {
+ case 'name':
+ $tr .= "";
+ if($isParent) $tr .= " {$lang->task->parentAB} ";
+ if($isChild) $tr .= " {$lang->task->childrenAB} ";
+ if($isMulti) $tr .= " " . zget($lang->task->modeList, $task->mode) . " ";
+ $tr .= "{$task->$key}";
+ $tr .= " | ";
+ break;
+ case 'status':
+ $tr .= "{$this->taskStatusList[$task->$key]} | ";
+ break;
+ case 'assignedTo':
+ if($task->mode == 'multi')
+ {
+ $tr .= "{$lang->task->team} | ";
+ }
+ else if($task->$key == 'closed')
+ {
+ $tr .= "Closed | ";
+ }
+ else
+ {
+ $tr .= "{$this->users[$task->$key]} | ";
+ }
+ break;
+ case 'actions':
+ $startUrl = 'xxc://sendContentToServerBySendbox/' . "{$this->lang->startTask} #$task->id";
+ $finishUrl = 'xxc://sendContentToServerBySendbox/' . "{$this->lang->finishTask} #$task->id";
+ $closeUrl = 'xxc://sendContentToServerBySendbox/' . "{$this->lang->closeTask} #$task->id";
+
+ $canStart = in_array($task->status, array('wait', 'pause'));
+ $canFinish = in_array($task->status, array('wait', 'pause', 'doing'));
+ $canClose = in_array($task->status, array('done', 'cancel'));
+
+ /* Disable all if task has children. */
+ if($isParent)
+ {
+ $canStart = false;
+ $canFinish = false;
+ $canClose = false;
+ }
+
+ $tr .= '';
+ $tr .= $canStart ? " " : "
";
+ $tr .= $canFinish ? " " : "
";
+ $tr .= $canClose ? " " : "
";
+ $tr .= ' | ';
+ break;
+ case 'estimate':
+ case 'consumed':
+ case 'left':
+ $tr .= "{$task->$key}h | ";
+ break;
+ default:
+ $tr .= "{$task->$key} | ";
+ }
+ }
+ $tbody .= "{$tr}
";
+ }
+ return "";
+ }
+
+ /**
+ * Start task command.
+ *
+ * @param array $args bot command arguments
+ * @param int $userID
+ * @access public
+ * @return object|string
+ */
+ public function start($args = array(), $userID = 0)
+ {
+ $originArgs = $args;
+ foreach($args as $key => $value)
+ {
+ if(in_array(strtolower($value), $this->lang->condKeywords['task']))
+ {
+ unset($args[$key]);
+ }
+ }
+
+ if(count($args) == 0) return $this->lang->errors->taskIDRequired;
+
+ $taskID = 0;
+ foreach($args as $key => $value)
+ {
+ if(strpos($value, '#') !== false)
+ {
+ $id = str_replace('#', '', $value);
+ if($id && is_numeric($id))
+ {
+ $taskID = $id;
+ unset($args[$key]);
+ }
+ else
+ {
+ if($args[$key + 1] && is_numeric($args[$key + 1]))
+ {
+ $taskID = $args[$key + 1];
+ unset($args[$key]);
+ unset($args[$key + 1]);
+ }
+ }
+ }
+ }
+ if(!$taskID) return $this->lang->errors->taskIDRequired;
+
+ $task = $this->loadEntry('task', 'get', array('taskID' => $taskID));
+ if(!$task) return $this->lang->errors->taskNotFound;
+ if($task->status != 'wait') return sprintf($this->lang->errors->invalidStatus, $this->taskStatusList[$task->status]);
+
+ $reply = new stdClass();
+ $reply->messages = array();
+ $reply->responses = array();
+
+ if(count($args) == 0)
+ {
+ $originCommand = rawurlencode($this->lang->startTask . ' ' . implode(' ', $originArgs));
+
+ $realStarted = $this->formatDate($task, 'realStarted');
+
+ $json = (object)array
+ (
+ 'title' => "#{$taskID} {$task->name}",
+ 'submitLabel' => $this->im->lang->task->start,
+ 'inputs' => array
+ (
+ (object)array
+ (
+ 'name' => $this->im->lang->task->realStarted,
+ 'type' => 'datetime-local',
+ 'label' => $this->im->lang->task->realStarted,
+ 'value' => $realStarted,
+ ),
+ (object)array
+ (
+ 'name' => $this->im->lang->task->consumed,
+ 'type' => 'number',
+ 'label' => $this->im->lang->task->consumed,
+ 'value' => $task->consumed,
+ 'addon' => $this->im->lang->task->hour,
+ ),
+ (object)array
+ (
+ 'name' => $this->im->lang->task->left,
+ 'type' => 'number',
+ 'label' => $this->im->lang->task->left,
+ 'value' => $task->left,
+ 'addon' => $this->im->lang->task->hour,
+ 'helpText' => $this->lang->start->finishWithZeroLeft,
+ )
+ ),
+ );
+ $json = rawurlencode(json_encode($json));
+
+ $reply->messages[] = sprintf($this->lang->start->tip, $taskID);
+ $reply->messages[] = "[{$this->lang->start->tipLinkTitle}](xxc://openFormAndSendToServerBySendbox/$json/$originCommand)";
+ }
+ else
+ {
+ $hasFormArgs = false;
+
+ $left = $task->left;
+ $consumed = $task->consumed;
+ $realStarted = $task->realStarted;
+
+ foreach($args as $arg)
+ {
+ if(strpos($arg, 'messageId'))
+ {
+ $hasFormArgs = true;
+ $formArgs = explode('&', $arg);
+ $replyArgs = array();
+ foreach($formArgs as $formArg)
+ {
+ $item = explode('=', $formArg);
+ if(count($item) == 2)
+ {
+ $replyArgs[$item[0]] = $item[1];
+ }
+ }
+ foreach($replyArgs as $key => $value)
+ {
+ if(in_array(strtolower($key), $this->lang->condKeywords['consumed'])) $consumed = $value;
+ if(in_array(strtolower($key), $this->lang->condKeywords['realStarted'])) $realStarted = $value;
+ if(in_array(strtolower($key), $this->lang->condKeywords['left'])) $left = $value;
+ }
+ if($consumed == 0 && $left == 0) return $this->im->lang->task->noticeTaskStart;
+
+ $task = $this->loadEntry('taskStart', 'post', array
+ (
+ 'taskID' => $taskID,
+ 'left' => $left,
+ 'realStarted' => $realStarted,
+ 'consumed' => $consumed,
+ ));
+ if($task->result and $task->result == 'fail')
+ {
+ $reply->messages[] = $task->message;
+ }
+ elseif(isset($task->error))
+ {
+ foreach($task->error as $error) $reply->messages[] = is_array($error) ? implode(',', $error): $error;
+ }
+ else
+ {
+ $reply->messages[] = sprintf($this->lang->effortRecorded, $taskID);
+ $reply->messages[] = $this->renderTaskTable(array($task));
+
+ $reply->responses[] = $this->disableMarkdownLinkInMessage($replyArgs['messageId']);
+ }
+ }
+ }
+
+ if(!$hasFormArgs)
+ {
+ while(!empty($args))
+ {
+ $arg = array_shift($args);
+ if(in_array(strtolower($arg), $this->lang->condKeywords['left']))
+ {
+ $left = array_shift($args);
+ }
+ elseif(in_array(strtolower($arg), $this->lang->condKeywords['consumed']))
+ {
+ $consumed = array_shift($args);
+ }
+ elseif(in_array(strtolower($arg), $this->lang->condKeywords['realStarted']))
+ {
+ $realStarted = array_shift($args);
+ }
+ }
+
+ $task = $this->loadEntry('taskStart', 'post', array
+ (
+ 'taskID' => $taskID,
+ 'left' => $left,
+ 'realStarted' => $realStarted,
+ 'consumed' => $consumed,
+ ));
+ if($task->result and $task->result == 'fail')
+ {
+ $reply->messages[] = $task->message;
+ }
+ elseif(isset($task->error))
+ {
+ foreach($task->error as $error) $reply->messages[] = is_array($error) ? implode(',', $error): $error;
+ }
+ else
+ {
+ $reply->messages[] = sprintf($this->lang->effortRecorded, $taskID);
+ $reply->messages[] = $this->renderTaskTable(array($task));
+ }
+ }
+ }
+
+ return $reply;
+ }
+
+ /**
+ * Remove a Markdown link in the message and generate a response.
+ *
+ * @param $messageID
+ * @access public
+ * @return object
+ */
+ private function disableMarkdownLinkInMessage($messageID)
+ {
+ if(!$messageID) return null;
+ $message = $this->im->message->getList('', array($messageID));
+ $message = current($message);
+ if($message->user != 'xuanbot') return null;
+ $message->content = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $message->content);
+ $this->im->message->setMessage($message);
+
+ $userID = array_filter(explode('&', $message->cgid), function($item)
+ {
+ return is_numeric($item);
+ });
+
+ $response = new stdclass();
+ $response->result = 'success';
+ $response->method = 'messageUpdate';
+ $response->users = $userID;
+ $response->data = array($message);
+
+ return $response;
+ }
+
+ /**
+ * Used to call zentao's API.
+ *
+ * @param string $entry
+ * @param string $action
+ * @param array $params
+ * @param string $version
+ * @access public
+ * @return object
+ */
+ public function loadEntry($entry, $action, $params = array(), $version = 'v1')
+ {
+ try
+ {
+ $this->im->loadModel('user');
+
+ $user = $this->im->user->getByID($this->im->app->input['userID'], 'id');
+ /* Authorize him and save to session. */
+ $user->rights = $this->im->user->authorize($user->account);
+ $user->groups = $this->im->user->getGroups($user->account);
+ $user->view = $this->im->user->grantUserView($user->account, $user->rights['acls'], $user->rights['projects']);
+ $user->admin = strpos($this->im->app->company->admins, ",{$user->account},") !== false;
+
+ global $app;
+ $app->action = $action;
+ $app->user = $user;
+
+ include_once($this->im->app->appRoot . "framework/api/entry.class.php");
+ include_once($this->im->app->appRoot . "api/{$version}/entries/" . strtolower($entry) . ".php");
+
+ $entryName = $entry . 'Entry';
+ $entry = new $entryName();
+
+ if($action == 'post' || $action == 'put')
+ {
+ $entry->requestBody = (object)$params;
+ }
+
+ $content = call_user_func_array(array($entry, $action), $params);
+ return json_decode($content);
+ }
+ catch(EndResponseException $endResponseException)
+ {
+ return $endResponseException->getContent();
+ }
+ }
+}
diff --git a/xuanxuan/extension/xuan/im/ext/lang/de/xuanxuan.php b/xuanxuan/extension/xuan/im/ext/lang/de/xuanxuan.php
index cde654dd0b..c2285839a0 100644
--- a/xuanxuan/extension/xuan/im/ext/lang/de/xuanxuan.php
+++ b/xuanxuan/extension/xuan/im/ext/lang/de/xuanxuan.php
@@ -36,3 +36,89 @@ $lang->im->xxd->aes = 'Server-side AES';
$lang->im->xxdAESTip = 'This only affects server-side AES encryption between XXB and XXD.';
$lang->im->aesOptions['on'] = 'Enabled';
$lang->im->aesOptions['off'] = 'Disabled';
+
+$lang->im->bot->zentaoBot = new stdclass();
+$lang->im->bot->zentaoBot->name = 'ZenTao';
+$lang->im->bot->zentaoBot->pageSearchRegex = '/(pageID|recPerPage)=(\d+)/';
+
+$lang->im->bot->zentaoBot->commands = new stdclass();
+$lang->im->bot->zentaoBot->commands->view = new stdclass();
+$lang->im->bot->zentaoBot->commands->view->description = 'View task';
+$lang->im->bot->zentaoBot->commands->start = new stdclass();
+$lang->im->bot->zentaoBot->commands->start->description = 'Start task';
+$lang->im->bot->zentaoBot->commands->close = new stdclass();
+$lang->im->bot->zentaoBot->commands->close->description = 'Close task';
+$lang->im->bot->zentaoBot->commands->finish = new stdclass();
+$lang->im->bot->zentaoBot->commands->finish->description = 'Finish task';
+
+$lang->im->bot->zentaoBot->condKeywords = array();
+$lang->im->bot->zentaoBot->condKeywords['task'] = array('task');
+$lang->im->bot->zentaoBot->condKeywords['pri'] = array('pri');
+$lang->im->bot->zentaoBot->condKeywords['status'] = array('status');
+$lang->im->bot->zentaoBot->condKeywords['assignTo'] = array('assignto', 'user');
+$lang->im->bot->zentaoBot->condKeywords['id'] = array('id');
+$lang->im->bot->zentaoBot->condKeywords['taskName'] = array('taskname');
+$lang->im->bot->zentaoBot->condKeywords['comment'] = array('comment');
+$lang->im->bot->zentaoBot->condKeywords['left'] = array('left');
+$lang->im->bot->zentaoBot->condKeywords['consumed'] = array('consumed');
+$lang->im->bot->zentaoBot->condKeywords['realStarted'] = array('realStarted');
+$lang->im->bot->zentaoBot->condKeywords['pageID'] = array('pageID');
+$lang->im->bot->zentaoBot->condKeywords['recPerPage'] = array('recPerPage');
+$lang->im->bot->zentaoBot->condKeywords['finishedDate'] = array('finishedDate');
+$lang->im->bot->zentaoBot->condKeywords['currentConsumed'] = array('currentConsumed');
+
+$lang->im->bot->zentaoBot->success = 'Command executed successfully.';
+$lang->im->bot->zentaoBot->tasksFound = 'Found %d tasks.';
+$lang->im->bot->zentaoBot->prevPage = 'Prev Page';
+$lang->im->bot->zentaoBot->nextPage = 'Next Page';
+$lang->im->bot->zentaoBot->effortRecorded = 'Effort recorded for task #%d.';
+
+$lang->im->bot->zentaoBot->finishTask = 'finish';
+$lang->im->bot->zentaoBot->closeTask = 'close';
+$lang->im->bot->zentaoBot->startTask = 'start';
+$lang->im->bot->zentaoBot->viewTask = 'view';
+
+$lang->im->bot->zentaoBot->errors = new stdclass();
+$lang->im->bot->zentaoBot->errors->emptyResult = 'No task found.';
+$lang->im->bot->zentaoBot->errors->invalidCommand = 'Invalid command.';
+$lang->im->bot->zentaoBot->errors->invalidStatus = 'Cannot perform such action on task with %s status.';
+$lang->im->bot->zentaoBot->errors->unauthorized = 'You are not authorized to perform this action.';
+$lang->im->bot->zentaoBot->errors->taskIDRequired = 'Task ID is required.';
+$lang->im->bot->zentaoBot->errors->taskNotFound = 'Task not found.';
+
+$lang->im->bot->zentaoBot->finish = new stdclass();
+$lang->im->bot->zentaoBot->finish->tip = 'Click the link below to finish the task. Time consumed and starting time of this task are required.';
+$lang->im->bot->zentaoBot->finish->tipLinkTitle = 'Finish Task';
+$lang->im->bot->zentaoBot->finish->done = 'Task #%d is finished, ended at:%s, time consumed: %.1f hours.';
+$lang->im->bot->zentaoBot->finish->bugTip = 'Task #%d is associated with a bug, you may mark the bug as resolved by clicking the link below.';
+$lang->im->bot->zentaoBot->finish->bugTipLinkTitle = 'Resolve Bug';
+
+$lang->im->bot->zentaoBot->start = new stdclass();
+$lang->im->bot->zentaoBot->start->tip = 'Click the link below to start task #%d.';
+$lang->im->bot->zentaoBot->start->tipLinkTitle = 'Start Task';
+$lang->im->bot->zentaoBot->start->finishWithZeroLeft = 'Hours left is 0 thus the task is finished.';
+
+$lang->im->bot->zentaoBot->help = <<im->xxd->aes = 'Server-side AES';
$lang->im->xxdAESTip = 'This only affects server-side AES encryption between XXB and XXD.';
$lang->im->aesOptions['on'] = 'Enabled';
$lang->im->aesOptions['off'] = 'Disabled';
+
+$lang->im->bot->zentaoBot = new stdclass();
+$lang->im->bot->zentaoBot->name = 'ZenTao';
+$lang->im->bot->zentaoBot->pageSearchRegex = '/(pageID|recPerPage)=(\d+)/';
+
+$lang->im->bot->zentaoBot->commands = new stdclass();
+$lang->im->bot->zentaoBot->commands->view = new stdclass();
+$lang->im->bot->zentaoBot->commands->view->description = 'View task';
+$lang->im->bot->zentaoBot->commands->start = new stdclass();
+$lang->im->bot->zentaoBot->commands->start->description = 'Start task';
+$lang->im->bot->zentaoBot->commands->close = new stdclass();
+$lang->im->bot->zentaoBot->commands->close->description = 'Close task';
+$lang->im->bot->zentaoBot->commands->finish = new stdclass();
+$lang->im->bot->zentaoBot->commands->finish->description = 'Finish task';
+
+$lang->im->bot->zentaoBot->condKeywords = array();
+$lang->im->bot->zentaoBot->condKeywords['task'] = array('task');
+$lang->im->bot->zentaoBot->condKeywords['pri'] = array('pri');
+$lang->im->bot->zentaoBot->condKeywords['status'] = array('status');
+$lang->im->bot->zentaoBot->condKeywords['assignTo'] = array('assignto', 'user');
+$lang->im->bot->zentaoBot->condKeywords['id'] = array('id');
+$lang->im->bot->zentaoBot->condKeywords['taskName'] = array('taskname');
+$lang->im->bot->zentaoBot->condKeywords['comment'] = array('comment');
+$lang->im->bot->zentaoBot->condKeywords['left'] = array('left');
+$lang->im->bot->zentaoBot->condKeywords['consumed'] = array('consumed');
+$lang->im->bot->zentaoBot->condKeywords['realStarted'] = array('realStarted');
+$lang->im->bot->zentaoBot->condKeywords['pageID'] = array('pageID');
+$lang->im->bot->zentaoBot->condKeywords['recPerPage'] = array('recPerPage');
+$lang->im->bot->zentaoBot->condKeywords['finishedDate'] = array('finishedDate');
+$lang->im->bot->zentaoBot->condKeywords['currentConsumed'] = array('currentConsumed');
+
+$lang->im->bot->zentaoBot->success = 'Command executed successfully.';
+$lang->im->bot->zentaoBot->tasksFound = 'Found %d tasks.';
+$lang->im->bot->zentaoBot->prevPage = 'Prev Page';
+$lang->im->bot->zentaoBot->nextPage = 'Next Page';
+$lang->im->bot->zentaoBot->effortRecorded = 'Effort recorded for task #%d.';
+
+$lang->im->bot->zentaoBot->finishTask = 'finish';
+$lang->im->bot->zentaoBot->closeTask = 'close';
+$lang->im->bot->zentaoBot->startTask = 'start';
+$lang->im->bot->zentaoBot->viewTask = 'view';
+
+$lang->im->bot->zentaoBot->errors = new stdclass();
+$lang->im->bot->zentaoBot->errors->emptyResult = 'No task found.';
+$lang->im->bot->zentaoBot->errors->invalidCommand = 'Invalid command.';
+$lang->im->bot->zentaoBot->errors->invalidStatus = 'Cannot perform such action on task with %s status.';
+$lang->im->bot->zentaoBot->errors->unauthorized = 'You are not authorized to perform this action.';
+$lang->im->bot->zentaoBot->errors->taskIDRequired = 'Task ID is required.';
+$lang->im->bot->zentaoBot->errors->taskNotFound = 'Task not found.';
+
+$lang->im->bot->zentaoBot->finish = new stdclass();
+$lang->im->bot->zentaoBot->finish->tip = 'Click the link below to finish the task. Time consumed and starting time of this task are required.';
+$lang->im->bot->zentaoBot->finish->tipLinkTitle = 'Finish Task';
+$lang->im->bot->zentaoBot->finish->done = 'Task #%d is finished, ended at:%s, time consumed: %.1f hours.';
+$lang->im->bot->zentaoBot->finish->bugTip = 'Task #%d is associated with a bug, you may mark the bug as resolved by clicking the link below.';
+$lang->im->bot->zentaoBot->finish->bugTipLinkTitle = 'Resolve Bug';
+
+$lang->im->bot->zentaoBot->start = new stdclass();
+$lang->im->bot->zentaoBot->start->tip = 'Click the link below to start task #%d.';
+$lang->im->bot->zentaoBot->start->tipLinkTitle = 'Start Task';
+$lang->im->bot->zentaoBot->start->finishWithZeroLeft = 'Hours left is 0 thus the task is finished.';
+
+$lang->im->bot->zentaoBot->help = <<im->xxd->aes = 'Server-side AES';
$lang->im->xxdAESTip = 'This only affects server-side AES encryption between XXB and XXD.';
$lang->im->aesOptions['on'] = 'Enabled';
$lang->im->aesOptions['off'] = 'Disabled';
+
+$lang->im->bot->zentaoBot = new stdclass();
+$lang->im->bot->zentaoBot->name = 'ZenTao';
+$lang->im->bot->zentaoBot->pageSearchRegex = '/(pageID|recPerPage)=(\d+)/';
+
+$lang->im->bot->zentaoBot->commands = new stdclass();
+$lang->im->bot->zentaoBot->commands->view = new stdclass();
+$lang->im->bot->zentaoBot->commands->view->description = 'View task';
+$lang->im->bot->zentaoBot->commands->start = new stdclass();
+$lang->im->bot->zentaoBot->commands->start->description = 'Start task';
+$lang->im->bot->zentaoBot->commands->close = new stdclass();
+$lang->im->bot->zentaoBot->commands->close->description = 'Close task';
+$lang->im->bot->zentaoBot->commands->finish = new stdclass();
+$lang->im->bot->zentaoBot->commands->finish->description = 'Finish task';
+
+$lang->im->bot->zentaoBot->condKeywords = array();
+$lang->im->bot->zentaoBot->condKeywords['task'] = array('task');
+$lang->im->bot->zentaoBot->condKeywords['pri'] = array('pri');
+$lang->im->bot->zentaoBot->condKeywords['status'] = array('status');
+$lang->im->bot->zentaoBot->condKeywords['assignTo'] = array('assignto', 'user');
+$lang->im->bot->zentaoBot->condKeywords['id'] = array('id');
+$lang->im->bot->zentaoBot->condKeywords['taskName'] = array('taskname');
+$lang->im->bot->zentaoBot->condKeywords['comment'] = array('comment');
+$lang->im->bot->zentaoBot->condKeywords['left'] = array('left');
+$lang->im->bot->zentaoBot->condKeywords['consumed'] = array('consumed');
+$lang->im->bot->zentaoBot->condKeywords['realStarted'] = array('realStarted');
+$lang->im->bot->zentaoBot->condKeywords['pageID'] = array('pageID');
+$lang->im->bot->zentaoBot->condKeywords['recPerPage'] = array('recPerPage');
+$lang->im->bot->zentaoBot->condKeywords['finishedDate'] = array('finishedDate');
+$lang->im->bot->zentaoBot->condKeywords['currentConsumed'] = array('currentConsumed');
+
+$lang->im->bot->zentaoBot->success = 'Command executed successfully.';
+$lang->im->bot->zentaoBot->tasksFound = 'Found %d tasks.';
+$lang->im->bot->zentaoBot->prevPage = 'Prev Page';
+$lang->im->bot->zentaoBot->nextPage = 'Next Page';
+$lang->im->bot->zentaoBot->effortRecorded = 'Effort recorded for task #%d.';
+
+$lang->im->bot->zentaoBot->finishTask = 'finish';
+$lang->im->bot->zentaoBot->closeTask = 'close';
+$lang->im->bot->zentaoBot->startTask = 'start';
+$lang->im->bot->zentaoBot->viewTask = 'view';
+
+$lang->im->bot->zentaoBot->errors = new stdclass();
+$lang->im->bot->zentaoBot->errors->emptyResult = 'No task found.';
+$lang->im->bot->zentaoBot->errors->invalidCommand = 'Invalid command.';
+$lang->im->bot->zentaoBot->errors->invalidStatus = 'Cannot perform such action on task with %s status.';
+$lang->im->bot->zentaoBot->errors->unauthorized = 'You are not authorized to perform this action.';
+$lang->im->bot->zentaoBot->errors->taskIDRequired = 'Task ID is required.';
+$lang->im->bot->zentaoBot->errors->taskNotFound = 'Task not found.';
+
+$lang->im->bot->zentaoBot->finish = new stdclass();
+$lang->im->bot->zentaoBot->finish->tip = 'Click the link below to finish the task. Time consumed and starting time of this task are required.';
+$lang->im->bot->zentaoBot->finish->tipLinkTitle = 'Finish Task';
+$lang->im->bot->zentaoBot->finish->done = 'Task #%d is finished, ended at:%s, time consumed: %.1f hours.';
+$lang->im->bot->zentaoBot->finish->bugTip = 'Task #%d is associated with a bug, you may mark the bug as resolved by clicking the link below.';
+$lang->im->bot->zentaoBot->finish->bugTipLinkTitle = 'Resolve Bug';
+
+$lang->im->bot->zentaoBot->start = new stdclass();
+$lang->im->bot->zentaoBot->start->tip = 'Click the link below to start task #%d.';
+$lang->im->bot->zentaoBot->start->tipLinkTitle = 'Start Task';
+$lang->im->bot->zentaoBot->start->finishWithZeroLeft = 'Hours left is 0 thus the task is finished.';
+
+$lang->im->bot->zentaoBot->help = <<im->xxd->aes = '服务端通信 AES';
$lang->im->xxdAESTip = '该设置仅针对 XXB 和 XXD 之间的通讯加密,不影响客户端通讯加密。';
$lang->im->aesOptions['on'] = '开启';
$lang->im->aesOptions['off'] = '关闭';
+
+$lang->im->bot->commonName = '阿道';
+$lang->im->bot->welcome->title = '哈喽~我是你的助手阿道';
+$lang->im->bot->upgradeWelcome->title = '哈喽~我是你的助手阿道';
+
+$lang->im->bot->zentaoBot = new stdclass();
+$lang->im->bot->zentaoBot->name = '禅道';
+$lang->im->bot->zentaoBot->pageSearchRegex = '/(pageID|recPerPage|页码|每页数量|頁碼|每頁數量)=(\d+)/';
+
+$lang->im->bot->zentaoBot->commands = new stdclass();
+$lang->im->bot->zentaoBot->commands->view = new stdclass();
+$lang->im->bot->zentaoBot->commands->view->description = '查看任务';
+$lang->im->bot->zentaoBot->commands->view->alias = array('查看', '搜索', '查询', '筛选');
+$lang->im->bot->zentaoBot->commands->start = new stdclass();
+$lang->im->bot->zentaoBot->commands->start->description = '开始任务';
+$lang->im->bot->zentaoBot->commands->start->alias = array('开始', '开始任务');
+$lang->im->bot->zentaoBot->commands->close = new stdclass();
+$lang->im->bot->zentaoBot->commands->close->description = '关闭任务';
+$lang->im->bot->zentaoBot->commands->close->alias = array('关闭', '关闭任务');
+$lang->im->bot->zentaoBot->commands->finish = new stdclass();
+$lang->im->bot->zentaoBot->commands->finish->description = '完成任务';
+$lang->im->bot->zentaoBot->commands->finish->alias = array('完成', '完成任务');
+
+$lang->im->bot->zentaoBot->condKeywords = array();
+$lang->im->bot->zentaoBot->condKeywords['task'] = array('任务', 'task');
+$lang->im->bot->zentaoBot->condKeywords['pri'] = array('优先级', 'pri');
+$lang->im->bot->zentaoBot->condKeywords['status'] = array('状态', 'status');
+$lang->im->bot->zentaoBot->condKeywords['assignTo'] = array('指派人', '指派给', 'assignto', 'user');
+$lang->im->bot->zentaoBot->condKeywords['id'] = array('编号', 'id');
+$lang->im->bot->zentaoBot->condKeywords['taskName'] = array('任务名', '任务名称', 'taskname');
+$lang->im->bot->zentaoBot->condKeywords['comment'] = array('备注', 'comment');
+$lang->im->bot->zentaoBot->condKeywords['left'] = array('预计剩余', 'left');
+$lang->im->bot->zentaoBot->condKeywords['consumed'] = array('总计消耗', 'consumed');
+$lang->im->bot->zentaoBot->condKeywords['realStarted'] = array('实际开始', 'realStarted');
+$lang->im->bot->zentaoBot->condKeywords['pageID'] = array('pageID', '页码', '頁碼');
+$lang->im->bot->zentaoBot->condKeywords['recPerPage'] = array('recPerPage', '每页数量', '每頁數量');
+$lang->im->bot->zentaoBot->condKeywords['finishedDate'] = array('实际完成', 'finishedDate');
+$lang->im->bot->zentaoBot->condKeywords['currentConsumed'] = array('本次消耗', 'currentConsumed');
+
+$lang->im->bot->zentaoBot->success = '指令执行完成';
+$lang->im->bot->zentaoBot->tasksFound = '为您匹配到 %d 项任务';
+$lang->im->bot->zentaoBot->prevPage = '上一页';
+$lang->im->bot->zentaoBot->nextPage = '下一页';
+$lang->im->bot->zentaoBot->effortRecorded = '任务 #%d 已完成工时信息填写';
+
+$lang->im->bot->zentaoBot->finishTask = '完成任务';
+$lang->im->bot->zentaoBot->closeTask = '关闭任务';
+$lang->im->bot->zentaoBot->startTask = '开始任务';
+$lang->im->bot->zentaoBot->viewTask = '查看任务';
+
+$lang->im->bot->zentaoBot->errors = new stdclass();
+$lang->im->bot->zentaoBot->errors->emptyResult = '未查询到相关匹配信息';
+$lang->im->bot->zentaoBot->errors->invalidCommand = '无法识别该指令';
+$lang->im->bot->zentaoBot->errors->invalidStatus = '检测到该任务为%s状态,无法实现指令操作';
+$lang->im->bot->zentaoBot->errors->unauthorized = '您无权操作此任务';
+$lang->im->bot->zentaoBot->errors->taskIDRequired = '请输入任务编号';
+$lang->im->bot->zentaoBot->errors->taskNotFound = '任务不存在';
+
+$lang->im->bot->zentaoBot->finish = new stdclass();
+$lang->im->bot->zentaoBot->finish->tip = '完成任务指令需要填入工时与记录起始时间,请点击下方入口';
+$lang->im->bot->zentaoBot->finish->tipLinkTitle = '工时记录';
+$lang->im->bot->zentaoBot->finish->done = '任务 #%d 已完成,完成时间:%s,消耗:%.1f 小时';
+$lang->im->bot->zentaoBot->finish->bugTip = '检测到任务 #%d 关联相关 Bug,您可以点击以下链接进行处理';
+$lang->im->bot->zentaoBot->finish->bugTipLinkTitle = '关联 Bug 处理';
+
+$lang->im->bot->zentaoBot->start = new stdclass();
+$lang->im->bot->zentaoBot->start->tip = '点击链接开始任务 #%d';
+$lang->im->bot->zentaoBot->start->tipLinkTitle = '开始任务';
+$lang->im->bot->zentaoBot->start->finishWithZeroLeft = '剩余工时为 0,任务将被标记为"已完成"';
+
+$lang->im->bot->zentaoBot->help = << array(), 'assignedToList' => array(), 'statusList' => array(), 'idList' => array(), 'taskName' => '') as $condKey => $defaultValue)
+ {
+ if(!isset($conds->$condKey)) $conds->$condKey = $defaultValue;
+ }
+
+ return $this->dao->select('*')->from(TABLE_TASK)
+ ->where('deleted')->eq(0)
+ ->beginIF(!empty($conds->priList))->andWhere('pri')->in($conds->priList)->fi()
+ ->beginIF(!empty($conds->assignedToList))->andWhere('assignedTo')->in($conds->assignedToList)->fi()
+ ->beginIF(!empty($conds->statusList))->andWhere('status')->in($conds->statusList)->fi()
+ ->beginIF(!empty($conds->idList))->andWhere('id')->in($conds->idList)->fi()
+ ->beginIF(!empty($conds->taskName))->andWhere('name')->like("%{$conds->taskName}%")
+ ->orderBy($orderBy)
+ ->beginIF($limit > 0)->limit($limit)->fi()
+ ->page($pager)
+ ->fetchAll('id');
+}
diff --git a/xuanxuan/www/data/image/xuanbot.png b/xuanxuan/www/data/image/xuanbot.png
new file mode 100644
index 0000000000..ff8f3fa2d7
Binary files /dev/null and b/xuanxuan/www/data/image/xuanbot.png differ