<\/div><\/div>/g" zentaoxx/module/client/view/checkupgrade.html.php
+ sed -i '/var serverVersions/d' zentaoxx/module/client/js/checkupgrade.js
+ sed -i '/var currentVersion/d' zentaoxx/module/client/js/checkupgrade.js
+ sed -i 's/v\.//g' zentaoxx/module/client/js/checkupgrade.js
+ sed -i 's/xxb_/zt_/g' zentaoxx/db/*.sql
mkdir zentaoxx/tools; cp tools/cn2tw.php zentaoxx/tools; cd zentaoxx/tools; php cn2tw.php
rm -rf zentaoxx/tools
zip -rqm -9 zentaoxx.$(VERSION).zip zentaoxx/*
@@ -109,18 +120,6 @@ pms:
make package
zip -rq -9 ZenTaoPMS.$(VERSION).zip zentaopms
rm -fr zentaopms zentaoxx zentaoxx.*.zip
-en:
- make common
- cd zentaopms/; grep -rl 'zentao.net'|xargs sed -i 's/zentao.net/zentao.pm/g';
- cd zentaopms/; grep -rl 'http://www.zentao.pm'|xargs sed -i 's/http:\/\/www.zentao.pm/https:\/\/www.zentao.pm/g';
- cd zentaopms/config/; echo >> config.php; echo '$$config->isINT = true;' >> config.php
- make package
- zip -r -9 ZenTaoPMS.$(VERSION).int.zip zentaopms
- rm -fr zentaopms
- echo $(VERSION).int > VERSION
- make deb
- make rpm
- echo $(VERSION) > VERSION
deb:
mkdir buildroot
cp -r build/debian/DEBIAN buildroot
@@ -148,6 +147,46 @@ rpm:
rpmbuild -ba ~/rpmbuild/SPECS/zentaopms.spec
cp ~/rpmbuild/RPMS/noarch/zentaopms-${VERSION}-1.noarch.rpm ./
rm -rf ~/rpmbuild
+en:
+ make common
+ cd zentaopms/; grep -rl 'zentao.net'|xargs sed -i 's/zentao.net/zentao.pm/g';
+ cd zentaopms/; grep -rl 'http://www.zentao.pm'|xargs sed -i 's/http:\/\/www.zentao.pm/https:\/\/www.zentao.pm/g';
+ cd zentaopms/config/; echo >> config.php; echo '$$config->isINT = true;' >> config.php
+ make package
+ mv zentaopms zentaoalm
+ zip -r -9 ZenTaoALM.$(VERSION).int.zip zentaoalm
+ rm -fr zentaoalm
+ echo $(VERSION).int > VERSION
+ make endeb
+ make enrpm
+ echo $(VERSION) > VERSION
+endeb:
+ mkdir buildroot
+ cp -r build/debian/DEBIAN buildroot
+ sed -i '/^Version/cVersion: ${VERSION}' buildroot/DEBIAN/control
+ mkdir buildroot/opt
+ mkdir buildroot/etc/apache2/sites-enabled/ -p
+ cp build/debian/zentaopms.conf buildroot/etc/apache2/sites-enabled/
+ cp ZenTaoALM.${VERSION}.zip buildroot/opt
+ cd buildroot/opt; unzip ZenTaoALM.${VERSION}.zip; mv zentaoalm zentao; rm ZenTaoALM.${VERSION}.zip
+ sed -i 's/index.php/\/zentao\/index.php/' buildroot/opt/zentao/www/.htaccess
+ sudo dpkg -b buildroot/ ZenTaoALM_${VERSION}_1_all.deb
+ rm -rf buildroot
+enrpm:
+ mkdir ~/rpmbuild/SPECS -p
+ cp build/rpm/zentaopms.spec ~/rpmbuild/SPECS
+ sed -i '/^Version/cVersion:${VERSION}' ~/rpmbuild/SPECS/zentaopms.spec
+ mkdir ~/rpmbuild/SOURCES
+ cp ZenTaoALM.${VERSION}.zip ~/rpmbuild/SOURCES
+ mkdir ~/rpmbuild/SOURCES/etc/httpd/conf.d/ -p
+ cp build/debian/zentaopms.conf ~/rpmbuild/SOURCES/etc/httpd/conf.d/zentaoalm.conf
+ mkdir ~/rpmbuild/SOURCES/opt/ -p
+ cd ~/rpmbuild/SOURCES; unzip ZenTaoALM.${VERSION}.zip; mv zentaoalm opt/zentao;
+ sed -i 's/index.php/\/zentao\/index.php/' ~/rpmbuild/SOURCES/opt/zentao/www/.htaccess
+ cd ~/rpmbuild/SOURCES; tar -czvf zentaoalm-${VERSION}.tar.gz etc opt; rm -rf ZenTaoALM.${VERSION}.zip etc opt;
+ rpmbuild -ba ~/rpmbuild/SPECS/zentaopms.spec
+ cp ~/rpmbuild/RPMS/noarch/zentaoalm-${VERSION}-1.noarch.rpm ./
+ rm -rf ~/rpmbuild
patchphpdoc:
sudo cp misc/doc/phpdoc/*.tpl /usr/share/php/data/PhpDocumentor/phpDocumentor/Converters/HTML/frames/templates/phphtmllib/templates/
phpdoc:
diff --git a/db/update11.4.1.sql b/db/update11.4.1.sql
new file mode 100644
index 0000000000..5f456e6cf6
--- /dev/null
+++ b/db/update11.4.1.sql
@@ -0,0 +1,5 @@
+ALTER TABLE `zt_entry` ADD `calledTime` int(10) unsigned NOT NULL DEFAULT '0' AFTER `createdDate`;
+ALTER TABLE `zt_story` CHANGE `toBug` `toBug` mediumint(8) unsigned NOT NULL AFTER `closedReason`;
+UPDATE `zt_story` SET `stage` = 'closed' WHERE `status` = 'closed';
+ALTER TABLE `zt_story` ADD `stagedBy` char(30) COLLATE 'utf8_general_ci' NOT NULL AFTER `stage`;
+ALTER TABLE `zt_storystage` ADD `stagedBy` char(30) COLLATE 'utf8_general_ci' NOT NULL;
diff --git a/db/zentao.sql b/db/zentao.sql
index 6f1175eb92..848a4e137d 100644
--- a/db/zentao.sql
+++ b/db/zentao.sql
@@ -579,7 +579,7 @@ CREATE TABLE IF NOT EXISTS `zt_story` (
`closedBy` varchar(30) NOT NULL default '',
`closedDate` datetime NOT NULL,
`closedReason` varchar(30) NOT NULL,
- `toBug` mediumint(9) NOT NULL,
+ `toBug` mediumint(8) unsigned NOT NULL,
`childStories` varchar(255) NOT NULL,
`linkStories` varchar(255) NOT NULL,
`duplicateStory` mediumint(8) unsigned NOT NULL,
@@ -604,6 +604,7 @@ CREATE TABLE IF NOT EXISTS `zt_storystage` (
`story` mediumint(8) unsigned NOT NULL,
`branch` mediumint(8) unsigned NOT NULL,
`stage` varchar(50) NOT NULL,
+ UNIQUE KEY `story_branch` (`story`,`branch`),
KEY `story` (`story`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- DROP TABLE IF EXISTS `zt_suitecase`;
@@ -899,6 +900,7 @@ CREATE TABLE IF NOT EXISTS `zt_entry` (
`desc` text NOT NULL,
`createdBy` varchar(30) NOT NULL,
`createdDate` datetime NOT NULL,
+ `calledTime` int(10) unsigned NOT NULL DEFAULT '0',
`editedBy` varchar(30) NOT NULL,
`editedDate` datetime NOT NULL,
`deleted` enum('0', '1') NOT NULL DEFAULT '0',
diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php
index 9618185b8c..9fc58645a3 100755
--- a/module/action/lang/zh-cn.php
+++ b/module/action/lang/zh-cn.php
@@ -129,6 +129,12 @@ $lang->action->desc->diff2 = '修改了 %s,
$lang->action->desc->diff3 = '将文件名 %s 改为 %s 。' . "\n";
$lang->action->desc->linked2bug = '$date 由 $actor 关联到版本 $extra';
+/* 子任务修改父任务的历史操作记录 */
+
+$lang->action->desc->linkchildtask = '$date, 由 $actor 关联子任务 $extra。' . "\n";
+$lang->action->desc->unlinkchildrentask = '$date, 由 $actor 移除子任务 $extra。' . "\n";
+$lang->action->desc->linkparenttask = '$date, 由 $actor 关联父任务 $extra。' . "\n";
+
/* 关联用例和移除用例时的历史操作记录。*/
$lang->action->desc->linkrelatedcase = '$date, 由 $actor 关联相关用例 $extra。' . "\n";
$lang->action->desc->unlinkrelatedcase = '$date, 由 $actor 移除相关用例 $extra。' . "\n";
diff --git a/module/action/model.php b/module/action/model.php
index 353c5adae1..77bcb5d07b 100755
--- a/module/action/model.php
+++ b/module/action/model.php
@@ -36,7 +36,7 @@ class actionModel extends model
$actor = $actor ? $actor : $this->app->user->account;
$actionType = strtolower($actionType);
if($actor == 'guest' and $actionType == 'logout') return false;
-
+
$action = new stdclass();
$objectType = str_replace('`', '', $objectType);
@@ -291,7 +291,7 @@ class actionModel extends model
$title = $this->dao->select('title')->from(TABLE_STORY)->where('id')->eq($action->extra)->fetch('title');
if($title) $action->extra = common::hasPriv('story', 'view') ? html::a(helper::createLink('story', 'view', "storyID=$action->extra"), "#$action->extra " . $title) : "#$action->extra " . $title;
}
- elseif($actionName == 'totask')
+ elseif($actionName == 'totask' or $actionName == 'linkchildtask' or $actionName == 'unlinkchildrentask' or $actionName == 'linkparenttask')
{
$name = $this->dao->select('name')->from(TABLE_TASK)->where('id')->eq($action->extra)->fetch('name');
if($name) $action->extra = common::hasPriv('task', 'view') ? html::a(helper::createLink('task', 'view', "taskID=$action->extra"), "#$action->extra " . $name) : "#$action->extra " . $name;
@@ -346,7 +346,11 @@ class actionModel extends model
{
$linkedProducts = $this->dao->select('id,name')->from(TABLE_PRODUCT)->where('id')->in($action->extra)->fetchPairs('id', 'name');
$action->extra = '';
- if($linkedProducts) $action->extra = sprintf($this->lang->project->action->extra, '' . join(', ', $linkedProducts) . '');
+ if($linkedProducts)
+ {
+ foreach($linkedProducts as $productID => $productName) $linkedProducts[$productID] = html::a(helper::createLink('product', 'browse', "productID=$productID"), "#{$productID} {$productName}");
+ $action->extra = sprintf($this->lang->project->action->extra, '' . join(', ', $linkedProducts) . '');
+ }
}
$action->history = isset($histories[$actionID]) ? $histories[$actionID] : array();
@@ -365,7 +369,7 @@ class actionModel extends model
if($history->field == 'git') $history->diff = str_replace('+', '%2B', $history->diff);
}
}
-
+
$action->comment = $this->file->setImgSize($action->comment, $this->config->action->commonImgSize);
$actions[$actionID] = $action;
}
@@ -623,7 +627,11 @@ class actionModel extends model
if(!$actions) return array();
$this->loadModel('common')->saveQueryCondition($this->dao->get(), 'action');
- return $this->transformActions($actions);
+
+ $actions = $this->transformActions($actions);
+ $actioned = $this->loadModel('setting')->getItem("owner=system&module=common§ion=global&key=setdynamic");
+ foreach($actions as $index => $action) if(strpos($actioned, $action->action) === false) unset($actions[$index]);
+ return $actions;
}
/**
diff --git a/module/branch/model.php b/module/branch/model.php
index c4528fb9b8..1b7e612e50 100644
--- a/module/branch/model.php
+++ b/module/branch/model.php
@@ -46,7 +46,8 @@ class branchModel extends model
$product = $this->loadModel('product')->getById($productID);
if(!$product or $product->type == 'normal') return array();
- $branches = array('0' => $this->lang->branch->all . $this->lang->product->branchName[$product->type]) + $branches;
+ $space = $this->app->getClientLang() == 'en' ? ' ' : '';
+ $branches = array('0' => $this->lang->branch->all . $space . $this->lang->product->branchName[$product->type]) + $branches;
}
return $branches;
}
diff --git a/module/bug/control.php b/module/bug/control.php
index 27bc43a2d7..f1365cc19b 100644
--- a/module/bug/control.php
+++ b/module/bug/control.php
@@ -193,7 +193,7 @@ class bug extends control
$chartFunc = 'getDataOf' . $chart;
$chartData = $this->bug->$chartFunc();
$chartOption = $this->lang->bug->report->$chart;
- if(!empty($chartType)) $chartOption->type = $chartType;
+ if(!empty($chartType) and $chartType != 'default') $chartOption->type = $chartType;
$this->bug->mergeChartOption($chart);
$this->view->charts[$chart] = $chartOption;
diff --git a/module/bug/css/create.css b/module/bug/css/create.css
index af26c3d110..68c6c10e6b 100644
--- a/module/bug/css/create.css
+++ b/module/bug/css/create.css
@@ -8,6 +8,7 @@
#buildBoxActions {padding-left: 15px;}
#module_chosen.chosen-container .chosen-drop {min-width: 400px; border-top: 1px solid #ddd!important}
+#os_chosen.chosen-container .chosen-drop{min-width: 200px}
#openedBuild_chosen.chosen-container .chosen-drop {min-width: 450px; border-top: 1px solid #ddd!important}
#module + .chosen-container-single .chosen-single,
#task + .chosen-container-single .chosen-single,
@@ -37,4 +38,3 @@
.title-group #severity + .chosen-container > .chosen-single {border-radius: 0!important;}
.title-group #pri + .chosen-container > .chosen-single {border-top-left-radius: 0!important; border-bottom-left-radius: 0!important;}
.title-group .has-icon-right{min-width:700px}
-#dataform .chosen[name ^= "os"] + .chosen-container .chosen-drop{min-width: 200px}
diff --git a/module/bug/js/common.js b/module/bug/js/common.js
index 89d2ec866c..2933bb843f 100644
--- a/module/bug/js/common.js
+++ b/module/bug/js/common.js
@@ -468,17 +468,20 @@ function notice()
if($('#openedBuild').find('option').length <= 1)
{
var html = '';
- if($('#project').val() == '')
+ if($('#project').length == 0 || $('#project').val() == '')
{
- branch = $('#branch').val();
+ var branch = $('#branch').val();
if(typeof(branch) == 'undefined') branch = 0;
- html += '' + createRelease + ' ';
+ var link = createLink('release', 'create', 'productID=' + $('#product').val() + '&branch=' + branch);
+ if(typeof(flow) != 'undefined' && flow == 'onlyTest') link = createLink('build', 'create','projectID=' + $('#product').val());
+ html += '' + createBuild + ' ';
html += '' + refresh + '';
}
else
{
- html += '' + createBuild + ' ';
- html += '' + refresh + '';
+ projectID = $('#project').val();
+ html += '' + createBuild + ' ';
+ html += '' + refresh + '';
}
var $bba = $('#buildBoxActions');
if($bba.length)
diff --git a/module/bug/model.php b/module/bug/model.php
index 5e04ef616e..cd81273fda 100644
--- a/module/bug/model.php
+++ b/module/bug/model.php
@@ -1670,7 +1670,12 @@ class bugModel extends model
{
$datas = $this->dao->select('openedBuild as name, count(openedBuild) as value')->from(TABLE_BUG)->where($this->reportCondition())->groupBy('openedBuild')->orderBy('value DESC')->fetchAll('name');
if(!$datas) return array();
- $builds = $this->loadModel('build')->getProductBuildPairs($this->session->product, $branch = 0, $params = '');
+ /* Judge if all product or not. */
+ $products = $this->session->product;
+ preg_match('/`product` IN \((?P.+)\)/', $this->reportCondition(), $matchs);
+ if(!empty($matchs) and isset($matchs['productIdList'])) $products = str_replace('\'', '', $matchs['productIdList']);
+ $builds = $this->loadModel('build')->getProductBuildPairs($products, $branch = 0, $params = '');
+
/* Deal with the situation that a bug maybe associate more than one openedBuild. */
foreach($datas as $buildIDList => $data)
{
diff --git a/module/bug/view/create.html.php b/module/bug/view/create.html.php
index 4039170c43..0f1c77a5f9 100644
--- a/module/bug/view/create.html.php
+++ b/module/bug/view/create.html.php
@@ -19,7 +19,7 @@ js::set('page', 'create');
js::set('createRelease', $lang->release->create);
js::set('createBuild', $lang->build->create);
js::set('refresh', $lang->refresh);
-js::set('confirmDeleteTemplate', $lang->bug->confirmDeleteTemplate);
+js::set('flow', $config->global->flow);
?>
diff --git a/module/common/lang/en.php b/module/common/lang/en.php
index 073c6ceb58..7095707dd9 100644
--- a/module/common/lang/en.php
+++ b/module/common/lang/en.php
@@ -21,7 +21,7 @@ $lang->percent = '%';
$lang->dash = '-';
$lang->zentaoPMS = 'ZenTao';
-$lang->welcome = "%s";
+$lang->welcome = "%s ALM";
$lang->logout = 'Logout';
$lang->login = 'Login';
$lang->help = 'Help';
@@ -94,13 +94,6 @@ $lang->preShortcutKey = '[Shortcut:←]';
$lang->nextShortcutKey = '[Shortcut:→]';
$lang->backShortcutKey = '[Shortcut:Alt+↑]';
-$lang->saveTemplate = 'Save as Template';
-$lang->setPublic = 'Set as Public Template';
-$lang->deleteTemplate = 'Delete Template';
-$lang->setTemplateTitle = 'Please enter the title of template.';
-$lang->applyTemplate = 'Template';
-$lang->confirmDeleteTemplate = 'Do you want to delete this template?';
-
$lang->select = 'Select';
$lang->selectAll = 'Select All';
$lang->selectReverse = 'Select Inverse';
@@ -518,7 +511,7 @@ $lang->proVersion = "nextShortcutKey = '[快捷键:→]';
$lang->backShortcutKey = '[快捷键:Alt+↑]';
-$lang->saveTemplate = '保存模板';
-$lang->setPublic = '设为公共模板';
-$lang->deleteTemplate = '删除模板';
-$lang->setTemplateTitle = '请输入模板标题';
-$lang->applyTemplate = '应用模板';
-$lang->confirmDeleteTemplate = '您确认要删除该模板吗?';
-
$lang->select = '选择';
$lang->selectAll = '全选';
$lang->selectReverse = '反选';
diff --git a/module/common/model.php b/module/common/model.php
index 49a6cae306..a6331566c8 100644
--- a/module/common/model.php
+++ b/module/common/model.php
@@ -1645,7 +1645,6 @@ EOD;
public function checkEntry()
{
$this->loadModel('entry');
-
if($this->session->valid_entry)
{
if(!$this->session->entry_code) $this->response('SESSION_CODE_MISSING');
@@ -1661,7 +1660,7 @@ EOD;
if(!$entry->key) $this->response('EMPTY_KEY');
if(empty($entry->account)) $this->response('ACCOUNT_UNBOUND');
if(!$this->checkIP($entry->ip)) $this->response('IP_DENIED');
- if(!$this->checkEntryToken($entry->key)) $this->response('INVALID_TOKEN');
+ if(!$this->checkEntryToken($entry)) $this->response('INVALID_TOKEN');
$this->loadModel('user');
$user = $this->dao->findByAccount($entry->account)->from(TABLE_USER)->fetch();
@@ -1676,6 +1675,14 @@ EOD;
$this->session->set('VALID_ENTRY', md5(md5($this->get->code) . $this->server->remote_addr));
$this->loadModel('entry')->saveLog($entry->id, $this->server->request_uri);
+ /* Add for task #5384. */
+ if($_SERVER['REQUEST_METHOD'] == 'POST' and empty($_POST))
+ {
+ $post = file_get_contents("php://input");
+ if(!empty($post)) $post = json_decode($post, true);
+ if(!empty($post)) $_POST = $post;
+ }
+
unset($_GET['code']);
unset($_GET['token']);
}
@@ -1683,16 +1690,30 @@ EOD;
/**
* Check token of an entry.
*
- * @param string $key
+ * @param object $entry
* @access public
- * @return void
+ * @return bool
*/
- public function checkEntryToken($key)
+ public function checkEntryToken($entry)
{
parse_str($this->server->query_String, $queryString);
unset($queryString['token']);
+
+ /* Change for task #5384. */
+ if(isset($queryString['time']))
+ {
+ $result = $this->get->token == md5($entry->code . $entry->key . $queryString['time']);
+ if($result)
+ {
+ if($queryString['time'] <= $entry->calledTime) $this->response('CALLED_TIME');
+ $this->loadModel('entry')->updateTime($entry->code, $queryString['time']);
+ unset($_GET['time']);
+ return $result;
+ }
+ }
+
$queryString = http_build_query($queryString);
- return $this->get->token == md5(md5($queryString) . $key);
+ return $this->get->token == md5(md5($queryString) . $entry->key);
}
/**
diff --git a/module/custom/control.php b/module/custom/control.php
index 87c4c623b8..473052fa3c 100644
--- a/module/custom/control.php
+++ b/module/custom/control.php
@@ -323,6 +323,31 @@ class custom extends control
$this->display();
}
+ /**
+ *Set dynamic recode what action
+ *
+ * @access public
+ * @return void
+ */
+ public function setDynamic()
+ {
+ if($_POST)
+ {
+ $actions = $this->post->actions;
+ $actions = implode(',', array_filter($actions));
+ $this->loadModel('setting')->setItem('system.common.global.setdynamic', $actions);
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'reload'));
+
+ }
+ $this->app->loadLang('action');
+ $this->view->title = $this->lang->custom->setDynamic;
+ $this->view->position[] = $this->lang->custom->common;
+ $this->view->position[] = $this->view->title;
+ $this->view->actions = $this->lang->action->search->label;
+ $this->view->actioned = $this->loadModel('setting')->getItem("owner=system&module=common§ion=global&key=setdynamic");
+ $this->display();
+ }
+
/**
* Timezone.
*
diff --git a/module/custom/lang/en.php b/module/custom/lang/en.php
index da3e8158fe..f452987682 100644
--- a/module/custom/lang/en.php
+++ b/module/custom/lang/en.php
@@ -18,6 +18,8 @@ $lang->custom->required = 'Required';
$lang->custom->score = 'Point';
$lang->custom->timezone = 'Timezone';
$lang->custom->scoreReset = 'Reset Point';
+$lang->custom->setDynamic = 'Set Dynamic';
+$lang->custom->setAction = 'Set Action';
$lang->custom->object['story'] = 'Story';
$lang->custom->object['task'] = 'Task';
@@ -77,7 +79,7 @@ $lang->custom->user->fields['statusList'] = 'Status';
$lang->custom->user->fields['contactField'] = 'Available Contact';
$lang->custom->user->fields['deleted'] = 'Deleted User';
-$lang->custom->system = array('flow', 'working', 'required', 'score');
+$lang->custom->system = array('flow', 'working', 'required', 'score', 'setDynamic');
$lang->custom->block->fields['closed'] = 'Closed Block';
diff --git a/module/custom/lang/zh-cn.php b/module/custom/lang/zh-cn.php
index f840afed4e..1e9a6a804a 100644
--- a/module/custom/lang/zh-cn.php
+++ b/module/custom/lang/zh-cn.php
@@ -18,6 +18,8 @@ $lang->custom->required = '必填项';
$lang->custom->score = '积分';
$lang->custom->timezone = '时区';
$lang->custom->scoreReset = '重置积分';
+$lang->custom->setDynamic = '动态设置';
+$lang->custom->setAction = '设置动作';
$lang->custom->object['story'] = '需求';
$lang->custom->object['task'] = '任务';
@@ -77,7 +79,7 @@ $lang->custom->user->fields['statusList'] = '状态';
$lang->custom->user->fields['contactField'] = '可用联系方式';
$lang->custom->user->fields['deleted'] = '列出已删除用户';
-$lang->custom->system = array('flow', 'working', 'required', 'score');
+$lang->custom->system = array('flow', 'working', 'required', 'score', 'setDynamic');
$lang->custom->block->fields['closed'] = '关闭的区块';
diff --git a/module/custom/view/setdynamic.html.php b/module/custom/view/setdynamic.html.php
new file mode 100644
index 0000000000..7f001825cf
--- /dev/null
+++ b/module/custom/view/setdynamic.html.php
@@ -0,0 +1,40 @@
+
+ * @package custom
+ * @version $Id$
+ * @link http://www.zentao.net
+ */
+?>
+
+Z Public License . It integrates with Product Management, Project Management, QA Management, Document Management, Todos Management, Company Management etc. ZenTao is the best choice for software project management.
+ZenTao ALM is an Open Source software released under Z Public License. It integrates with Product Management, Project Management, QA Management, Document Management, Todos Management, Company Management etc. ZenTao is the best choice for software project management.
-ZenTao is built on PHP + MySQL and based on ZenTaoPHP framework, an independent framework developed by our team. Third party developers/organizations can develop plug-ins or customize for their requirements.
+ZenTao ALM is built on PHP + MySQL and based on ZenTaoPHP framework, an independent framework developed by our team. Third party developers/organizations can develop plug-ins or customize for their requirements.
EOT;
$lang->install->links = <<Nature Easy Soft Co., LTD.
diff --git a/module/product/view/browse.html.php b/module/product/view/browse.html.php
index 97ab02ecd1..2eff935556 100644
--- a/module/product/view/browse.html.php
+++ b/module/product/view/browse.html.php
@@ -273,6 +273,7 @@
foreach($lang->story->stageList as $key => $stage)
{
if(empty($key)) continue;
+ if(strpos('tested|verified|released|closed', $key) === false) continue;
$actionLink = $this->createLink('story', 'batchChangeStage', "stage=$key");
echo "" . html::a('#', $stage, '', "onclick=\"setFormAction('$actionLink','hiddenwin')\"") . " ";
}
diff --git a/module/product/view/view.html.php b/module/product/view/view.html.php
index 38cdc8fff0..892ce3fe29 100644
--- a/module/product/view/view.html.php
+++ b/module/product/view/view.html.php
@@ -15,6 +15,8 @@
+
+
+
+
diff --git a/module/doc/model.php b/module/doc/model.php
index 0edddcd766..91fb377946 100644
--- a/module/doc/model.php
+++ b/module/doc/model.php
@@ -1588,6 +1588,7 @@ class docModel extends model
$parantMoudles = $this->dao->select('id, name')->from(TABLE_MODULE)
->where('id')->in($path)
->andWhere('deleted')->eq(0)
+ ->orderBy('`grade`')
->fetchAll('id');
foreach($parantMoudles as $parentID => $moduleName)
diff --git a/module/entry/config.php b/module/entry/config.php
index 850e01d109..98913d48b4 100644
--- a/module/entry/config.php
+++ b/module/entry/config.php
@@ -17,3 +17,4 @@ $config->entry->errcode['SESSION_VERIFY_FAILED'] = 401;
$config->entry->errcode['IP_DENIED'] = 403;
$config->entry->errcode['ACCOUNT_UNBOUND'] = 403;
$config->entry->errcode['EMPTY_ENTRY'] = 404;
+$config->entry->errcode['CALLED_TIME'] = 405;
diff --git a/module/entry/lang/en.php b/module/entry/lang/en.php
index d60c2abec1..33d1a0741a 100644
--- a/module/entry/lang/en.php
+++ b/module/entry/lang/en.php
@@ -46,3 +46,4 @@ $lang->entry->errmsg['SESSION_VERIFY_FAILED'] = 'Session verification failed.';
$lang->entry->errmsg['IP_DENIED'] = 'IP is denied.';
$lang->entry->errmsg['ACCOUNT_UNBOUND'] = 'Account is not bound.';
$lang->entry->errmsg['EMPTY_ENTRY'] = 'Entry does not exist.';
+$lang->entry->errmsg['CALLED_TIME'] = 'Token has expired';
diff --git a/module/entry/lang/zh-cn.php b/module/entry/lang/zh-cn.php
index d11bf4bd68..092587b9af 100644
--- a/module/entry/lang/zh-cn.php
+++ b/module/entry/lang/zh-cn.php
@@ -46,3 +46,4 @@ $lang->entry->errmsg['SESSION_VERIFY_FAILED'] = 'session验证失败';
$lang->entry->errmsg['IP_DENIED'] = '该IP被限制访问';
$lang->entry->errmsg['ACCOUNT_UNBOUND'] = '未绑定用户';
$lang->entry->errmsg['EMPTY_ENTRY'] = '应用不存在';
+$lang->entry->errmsg['CALLED_TIME'] = 'Token已失效';
diff --git a/module/entry/model.php b/module/entry/model.php
index 240b65981c..7aaff645a8 100644
--- a/module/entry/model.php
+++ b/module/entry/model.php
@@ -125,6 +125,20 @@ class entryModel extends model
return common::createChanges($oldEntry, $entry);
}
+ /**
+ * Update called time.
+ *
+ * @param string $code
+ * @param int $time
+ * @access public
+ * @return bool
+ */
+ public function updateTime($code, $time)
+ {
+ $this->dao->update(TABLE_ENTRY)->set('calledTime')->eq($time)->where('code')->eq($code)->exec();
+ return !dao::isError();
+ }
+
/**
* Save log of an entry.
*
diff --git a/module/file/control.php b/module/file/control.php
index ed426ae67d..3cd4b85890 100644
--- a/module/file/control.php
+++ b/module/file/control.php
@@ -287,29 +287,7 @@ class file extends control
*/
public function sendDownHeader($fileName, $fileType, $content)
{
- /* Clean the ob content to make sure no space or utf-8 bom output. */
- $obLevel = ob_get_level();
- for($i = 0; $i < $obLevel; $i++) ob_end_clean();
-
- /* Set the downloading cookie, thus the export form page can use it to judge whether to close the window or not. */
- setcookie('downloading', 1);
-
- /* Append the extension name auto. */
- $extension = '.' . $fileType;
- if(strpos($fileName, $extension) === false) $fileName .= $extension;
-
- /* urlencode the filename for ie. */
- if(strpos($this->server->http_user_agent, 'MSIE') !== false or strpos($this->server->http_user_agent, 'Trident') !== false or strpos($this->server->http_user_agent, 'Edge') !== false) $fileName = urlencode($fileName);
-
- /* Judge the content type. */
- $mimes = $this->config->file->mimes;
- $contentType = isset($mimes[$fileType]) ? $mimes[$fileType] : $mimes['default'];
-
- header("Content-type: $contentType");
- header("Content-Disposition: attachment; filename=\"$fileName\"");
- header("Pragma: no-cache");
- header("Expires: 0");
- die($content);
+ $this->file->sendDownHeader($fileName, $fileType, $content);
}
/**
diff --git a/module/file/model.php b/module/file/model.php
index 3baa7a8089..09efd6110d 100644
--- a/module/file/model.php
+++ b/module/file/model.php
@@ -821,4 +821,40 @@ class fileModel extends model
unset($_SESSION['album'][$uid]);
}
}
+
+ /**
+ * Send the download header to the client.
+ *
+ * @param string $fileName
+ * @param string $fileType
+ * @param string $content
+ * @access public
+ * @return void
+ */
+ public function sendDownHeader($fileName, $fileType, $content)
+ {
+ /* Clean the ob content to make sure no space or utf-8 bom output. */
+ $obLevel = ob_get_level();
+ for($i = 0; $i < $obLevel; $i++) ob_end_clean();
+
+ /* Set the downloading cookie, thus the export form page can use it to judge whether to close the window or not. */
+ setcookie('downloading', 1);
+
+ /* Append the extension name auto. */
+ $extension = '.' . $fileType;
+ if(strpos($fileName, $extension) === false) $fileName .= $extension;
+
+ /* urlencode the filename for ie. */
+ if(strpos($this->server->http_user_agent, 'MSIE') !== false or strpos($this->server->http_user_agent, 'Trident') !== false or strpos($this->server->http_user_agent, 'Edge') !== false) $fileName = urlencode($fileName);
+
+ /* Judge the content type. */
+ $mimes = $this->config->file->mimes;
+ $contentType = isset($mimes[$fileType]) ? $mimes[$fileType] : $mimes['default'];
+
+ header("Content-type: $contentType");
+ header("Content-Disposition: attachment; filename=\"$fileName\"");
+ header("Pragma: no-cache");
+ header("Expires: 0");
+ die($content);
+ }
}
diff --git a/module/install/lang/en.php b/module/install/lang/en.php
index 7c70bf8179..f928ea976a 100644
--- a/module/install/lang/en.php
+++ b/module/install/lang/en.php
@@ -25,9 +25,9 @@ $lang->install->seeLatestRelease = 'View latest version';
$lang->install->welcome = 'Thanks for choosing ZenTao!';
$lang->install->license = 'ZenTao is under Z PUBLIC LICENSE(ZPL) 1.2';
$lang->install->desc = <<
+
+
-
@@ -50,7 +52,8 @@
+
+
" + createStory + " ");
}
-
- if(isonlybody)
- {
- $('.main-actions').css('width', '100%')
- }
+
+ /* Add for task #5385. */
+ if(config.onlybody == 'yes') $('.main-actions').css('width', '100%')
});
diff --git a/module/story/js/x.create.js b/module/story/js/x.create.js
index 08d2d8b0f5..2c309298ce 100644
--- a/module/story/js/x.create.js
+++ b/module/story/js/x.create.js
@@ -1,6 +1,6 @@
$(function()
{
- $('#sourceNote').parent().find('span').remove();
+ $('#sourceNote').prev('span').remove();
$('#sourceNote').remove();
$('table tbody tr th').addClass('w-70px');
});
diff --git a/module/story/model.php b/module/story/model.php
index e7094c4395..579e52dceb 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -515,6 +515,7 @@ class storyModel extends model
if(empty($_POST['branch'])) $story->branch = 0;
if(!empty($_POST['stages']))
{
+ $oldStages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->fetchAll('branch');
$this->dao->delete()->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->exec();
$stageList = join(',', array_keys($this->lang->story->stageList));
@@ -522,7 +523,17 @@ class storyModel extends model
$minStage = '';
foreach($this->post->stages as $branch => $stage)
{
- $this->dao->insert(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq($stage)->exec();
+ $newStage = new stdclass();
+ $newStage->story = $storyID;
+ $newStage->branch = $branch;
+ $newStage->stage = $stage;
+ if(isset($oldStages[$branch]))
+ {
+ $oldStage = $oldStages[$branch];
+ $newStage->stagedBy = $oldStage->stagedBy;
+ if($stage != $oldStage->stage) $newStage->stagedBy = (strpos('tested|verified|released|closed', $stage) !== false) ? $this->app->user->account : '';
+ }
+ $this->dao->insert(TABLE_STORYSTAGE)->data($newStage)->exec();
if(strpos($stageList, $stage) !== false and strpos($stageList, $stage) < $minStagePos)
{
$minStage = $stage;
@@ -531,6 +542,7 @@ class storyModel extends model
}
$story->stage = $minStage;
}
+ if($oldStory->stage != $story->stage) $story->stagedBy = (strpos('tested|verified|released|closed', $story->stage) !== false) ? $this->app->user->account : '';
$this->dao->update(TABLE_STORY)
->data($story)
@@ -585,7 +597,7 @@ class storyModel extends model
foreach($storyIDList as $storyID)
{
if($data->pris[$storyID] == 'ditto') $data->pris[$storyID] = isset($prev['pri']) ? $prev['pri'] : 0;
- if($data->branches[$storyID] == 'ditto') $data->branches[$storyID] = isset($prev['branch']) ? $prev['branch'] : 0;
+ if(isset($data->branches) and $data->branches[$storyID] == 'ditto') $data->branches[$storyID] = isset($prev['branch']) ? $prev['branch'] : 0;
if($data->modules[$storyID] == 'ditto') $data->modules[$storyID] = isset($prev['module']) ? $prev['module'] : 0;
if($data->plans[$storyID] == 'ditto') $data->plans[$storyID] = isset($prev['plan']) ? $prev['plan'] : 0;
if($data->sources[$storyID] == 'ditto') $data->sources[$storyID] = isset($prev['source']) ? $prev['source'] : '';
@@ -628,6 +640,7 @@ class storyModel extends model
$story->duplicateStory = isset($data->duplicateStories[$storyID]) ? $data->duplicateStories[$storyID] : $oldStory->duplicateStory;
$story->childStories = isset($data->childStoriesIDList[$storyID]) ? $data->childStoriesIDList[$storyID] : $oldStory->childStories;
$story->version = $story->title == $oldStory->title ? $oldStory->version : $oldStory->version + 1;
+ if($story->stage != $oldStory->stage) $story->stagedBy = (strpos('tested|verified|released|closed', $story->stage) !== false) ? $this->app->user->account : '';
if($story->title != $oldStory->title) $story->status = 'changed';
if($story->plan !== false and $story->plan == '') $story->plan = 0;
@@ -854,7 +867,6 @@ class storyModel extends model
->setDefault('assignedDate', $now)
->removeIF($this->post->closedReason != 'duplicate', 'duplicateStory')
->removeIF($this->post->closedReason != 'subdivided', 'childStories')
- ->setIF($this->post->closedReason == 'done', 'stage', 'released')
->setIF($this->post->closedReason != 'done', 'plan', 0)
->remove('comment')
->get();
@@ -864,7 +876,7 @@ class storyModel extends model
->batchCheck($this->config->story->close->requiredFields, 'notempty')
->checkIF($story->closedReason == 'duplicate', 'duplicateStory', 'notempty')
->where('id')->eq($storyID)->exec();
- if(!dao::isError() && $this->post->closedReason == 'done') $this->loadModel('score')->create('story', 'close', $storyID);
+ if(!dao::isError()) $this->loadModel('score')->create('story', 'close', $storyID);
return common::createChanges($oldStory, $story);
}
@@ -903,7 +915,6 @@ class storyModel extends model
$story->duplicateStory = $data->duplicateStoryIDList[$storyID] ? $data->duplicateStoryIDList[$storyID] : $oldStory->duplicateStory;
$story->childStories = $data->childStoriesIDList[$storyID] ? $data->childStoriesIDList[$storyID] : $oldStory->childStories;
- if($story->closedReason == 'done') $story->stage = 'released';
if($story->closedReason != 'done') $story->plan = 0;
$stories[$storyID] = $story;
@@ -929,7 +940,7 @@ class storyModel extends model
{
die(js::error('story#' . $storyID . dao::getError(true)));
}
- if(!dao::isError() && $story->stage == 'released') $this->loadModel('score')->create('story', 'close', $storyID);
+ if(!dao::isError()) $this->loadModel('score')->create('story', 'close', $storyID);
}
return $allChanges;
@@ -1045,6 +1056,7 @@ class storyModel extends model
{
$now = helper::now();
$allChanges = array();
+ $account = $this->app->user->account;
$oldStories = $this->getByList($storyIDList);
$ignoreStories = '';
foreach($storyIDList as $storyID)
@@ -1057,12 +1069,13 @@ class storyModel extends model
}
$story = new stdclass();
- $story->lastEditedBy = $this->app->user->account;
+ $story->lastEditedBy = $account;
$story->lastEditedDate = $now;
$story->stage = $stage;
+ $story->stagedBy = $account;
$this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq((int)$storyID)->exec();
- $this->dao->update(TABLE_STORYSTAGE)->set('stage')->eq($stage)->where('story')->eq((int)$storyID)->exec();
+ $this->dao->update(TABLE_STORYSTAGE)->set('stage')->eq($stage)->set('stagedBy')->eq($account)->where('story')->eq((int)$storyID)->exec();
if(!dao::isError()) $allChanges[$storyID] = common::createChanges($oldStory, $story);
}
if($ignoreStories) echo js::alert(sprintf($this->lang->story->ignoreChangeStage, $ignoreStories));
@@ -1165,8 +1178,12 @@ class storyModel extends model
$storyID = (int)$storyID;
/* Get projects which status is doing. */
+ $oldStages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->fetchAll('branch');
$this->dao->delete()->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->exec();
- $story = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch();
+
+ $story = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch();
+ if(!empty($story->stagedBy)) return false;
+
$product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fetch();
$projects = $this->dao->select('t1.project,t3.branch')->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
@@ -1188,7 +1205,19 @@ class storyModel extends model
{
$this->dao->update(TABLE_STORY)->set('stage')->eq('wait')->where('id')->eq($storyID)->andWhere('plan')->eq('')->exec();
- foreach($stages as $branch => $stage) $this->dao->insert(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq($stage)->exec();
+ foreach($stages as $branch => $stage)
+ {
+ if(isset($oldStages[$branch]))
+ {
+ $oldStage = $oldStages[$branch];
+ if(!empty($oldStage->stagedBy))
+ {
+ $this->dao->replace(TABLE_STORYSTAGE)->data($oldStage)->exec();
+ continue;
+ }
+ }
+ $this->dao->replace(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq($stage)->exec();
+ }
$this->dao->update(TABLE_STORY)->set('stage')->eq('planned')->where('id')->eq($storyID)->andWhere("(plan != '' AND plan != '0')")->exec();
}
@@ -1210,7 +1239,19 @@ class storyModel extends model
/* No tasks, then the stage is projected. */
if(!$tasks and $projects)
{
- foreach($stages as $branch => $stage) $this->dao->insert(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq('projected')->exec();
+ foreach($stages as $branch => $stage)
+ {
+ if(isset($oldStages[$branch]))
+ {
+ $oldStage = $oldStages[$branch];
+ if(!empty($oldStage->stagedBy))
+ {
+ $this->dao->replace(TABLE_STORYSTAGE)->data($oldStage)->exec();
+ continue;
+ }
+ }
+ $this->dao->replace(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq('projected')->exec();
+ }
$this->dao->update(TABLE_STORY)->set('stage')->eq('projected')->where('id')->eq($storyID)->exec();
}
@@ -1285,7 +1326,16 @@ class storyModel extends model
$minStage = '';
foreach($stages as $branch => $stage)
{
- $this->dao->insert(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq($stage)->exec();
+ $this->dao->replace(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq($branch)->set('stage')->eq($stage)->exec();
+ if(isset($oldStages[$branch]))
+ {
+ $oldStage = $oldStages[$branch];
+ if(!empty($oldStage->stagedBy))
+ {
+ $this->dao->replace(TABLE_STORYSTAGE)->data($oldStage)->exec();
+ $stage = $oldStage->$stage;
+ }
+ }
if(strpos($stageList, $stage) !== false and strpos($stageList, $stage) < $minStagePos)
{
$minStage = $stage;
@@ -2425,7 +2475,7 @@ class storyModel extends model
echo "";
echo '';
diff --git a/module/story/view/view.html.php b/module/story/view/view.html.php
index 651c87e670..c0d452a799 100644
--- a/module/story/view/view.html.php
+++ b/module/story/view/view.html.php
@@ -14,7 +14,6 @@
session->storyList != false ? $app->session->storyList : $this->createLink('product', 'browse', "productID=$story->product&branch=$story->branch&moduleID=$story->module");?>
-
product->latestDynamic;?>
diff --git a/module/release/control.php b/module/release/control.php
index ed06532073..046a5da42e 100644
--- a/module/release/control.php
+++ b/module/release/control.php
@@ -136,10 +136,11 @@ class release extends control
$release = $this->release->getById((int)$releaseID, true);
if(!$release) die(js::error($this->lang->notFound) . js::locate('back'));
-
+
$stories = $this->dao->select('*')->from(TABLE_STORY)->where('id')->in($release->stories)->andWhere('deleted')->eq(0)
- ->beginIF($type == 'story')->orderBy($orderBy)->fi()
- ->fetchAll('id');
+ ->beginIF($type == 'story')->orderBy($orderBy)->fi()
+ ->fetchAll('id');
+
$this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story');
$stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->in($release->stories)->andWhere('branch')->eq($release->branch)->fetchPairs('story', 'stage');
foreach($stages as $storyID => $stage)$stories[$storyID]->stage = $stage;
diff --git a/module/story/config.php b/module/story/config.php
index d77f07bf8a..376aed98be 100644
--- a/module/story/config.php
+++ b/module/story/config.php
@@ -102,7 +102,7 @@ $config->story->datatable->fieldList['estimate']['required'] = 'no';
$config->story->datatable->fieldList['stage']['title'] = 'stageAB';
$config->story->datatable->fieldList['stage']['fixed'] = 'no';
-$config->story->datatable->fieldList['stage']['width'] = '80';
+$config->story->datatable->fieldList['stage']['width'] = '95';
$config->story->datatable->fieldList['stage']['required'] = 'no';
$config->story->datatable->fieldList['openedBy']['title'] = 'openedByAB';
diff --git a/module/story/control.php b/module/story/control.php
index 0cdddbf495..4cbe235d0b 100644
--- a/module/story/control.php
+++ b/module/story/control.php
@@ -1453,7 +1453,8 @@ class story extends control
}
else
{
- $stmt = $this->dbh->query($this->session->storyQueryCondition . ($this->post->exportType == 'selected' ? " AND t1.id IN({$this->cookie->checkedItem})" : '') . " ORDER BY " . strtr($orderBy, '_', ' '));
+ $field = $projectID ? 't2.id' : 't1.id';
+ $stmt = $this->dbh->query($this->session->storyQueryCondition . ($this->post->exportType == 'selected' ? " AND $field IN({$this->cookie->checkedItem})" : '') . " ORDER BY " . strtr($orderBy, '_', ' '));
while($row = $stmt->fetch()) $stories[$row->id] = $row;
}
diff --git a/module/story/js/view.js b/module/story/js/view.js
index 090486d5cd..c3c455a03e 100644
--- a/module/story/js/view.js
+++ b/module/story/js/view.js
@@ -5,9 +5,7 @@ $(function()
link = createLink('story', 'create', 'productID=' + productID + "&branch=" + branch + '&moduleID=' + moduleID);
$('#modulemenu ul.nav li.right:first').before("