|
@@ -49,7 +50,12 @@
| pri;?>' title='story->priList, $story->pri, $story->pri)?>'>story->priList, $story->pri, $story->pri)?> |
planTitle;?> |
module];?> |
- createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'");?> |
+
+ parent > 0) echo "{$lang->story->childrenAB}";
+ echo html::a($this->createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'");
+ ?>
+ |
openedBy);?> |
assignedTo);?> |
estimate;?> |
diff --git a/module/productplan/view/view.html.php b/module/productplan/view/view.html.php
index 266d99e53b..6f74b29c35 100644
--- a/module/productplan/view/view.html.php
+++ b/module/productplan/view/view.html.php
@@ -145,7 +145,12 @@
|
pri;?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?> |
module, '');?> |
- title);?> |
+
+ parent > 0) echo "{$lang->story->childrenAB}";
+ echo html::a($viewLink , $story->title);
+ ?>
+ |
openedBy);?> |
assignedTo);?> |
estimate;?> |
diff --git a/module/project/control.php b/module/project/control.php
index 89b4fa5168..789184bf02 100644
--- a/module/project/control.php
+++ b/module/project/control.php
@@ -2008,7 +2008,11 @@ class project extends control
}
else
{
- $allStories = $this->story->getProductStories(array_keys($products), $branches, $moduleID = '0', $status = 'active');
+ $allStories = $this->story->getProductStories(array_keys($products), $branches, $moduleID = '0', $status = 'active', $pager = null, $hasParent = false);
+ }
+ foreach($allStories as $id => $story)
+ {
+ if(isset($prjStories[$story->id])) unset($allStories[$id]);
}
$prjStories = $this->story->getProjectStoryPairs($projectID);
diff --git a/module/project/view/linkstory.html.php b/module/project/view/linkstory.html.php
index e42f48b3e1..a060ff0157 100644
--- a/module/project/view/linkstory.html.php
+++ b/module/project/view/linkstory.html.php
@@ -64,7 +64,12 @@
pri;?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?> |
createLink('product', 'browse', "productID=$story->product&branch=$story->branch"), $products[$story->product]->name, '_blank');?> |
'>module, '')?> |
- title);?> |
+
+ parent > 0) echo "{$lang->story->childrenAB}";
+ echo html::a($storyLink, $story->title);
+ ?>
+ |
planTitle;?> |
story->stageList, $story->stage);?> |
diff --git a/module/project/view/story.html.php b/module/project/view/story.html.php
index bce2c41a31..8a15781079 100644
--- a/module/project/view/story.html.php
+++ b/module/project/view/story.html.php
@@ -158,6 +158,7 @@
pri?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?> |
product][$story->branch])) echo "" . $branchGroups[$story->product][$story->branch] . '';?>
+ parent > 0) echo "{$lang->story->childrenAB}";?>
title, null, "style='color: $story->color'");?>
|
openedBy);?> |
diff --git a/module/release/view/linkstory.html.php b/module/release/view/linkstory.html.php
index f9d4b127c2..923f5fbdaf 100644
--- a/module/release/view/linkstory.html.php
+++ b/module/release/view/linkstory.html.php
@@ -50,7 +50,12 @@
id);?>
pri;?>' title='story->priList, $story->pri)?>'>story->priList, $story->pri)?> |
- createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'");?> |
+
+ parent > 0) echo "{$lang->story->childrenAB}";
+ echo html::a($this->createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', "data-toggle='modal' data-type='iframe' data-width='90%'");
+ ?>
+ |
openedBy);?> |
assignedTo);?> |
estimate;?> |
diff --git a/module/release/view/view.html.php b/module/release/view/view.html.php
index 8d24b32c67..d46696709e 100644
--- a/module/release/view/view.html.php
+++ b/module/release/view/view.html.php
@@ -105,7 +105,12 @@
id);?>
pri;?>' title='story->priList, $story->pri, $story->pri);?>'>story->priList, $story->pri, $story->pri);?> |
- title, '', "class='preview'");?> |
+
+ parent > 0) echo "{$lang->story->childrenAB}";
+ echo html::a($storyLink,$story->title, '', "class='preview'");
+ ?>
+ |
openedBy);?> |
estimate;?> |
diff --git a/module/repo/lang/de.php b/module/repo/lang/de.php
index 4e4d05b42d..c425ff526e 100644
--- a/module/repo/lang/de.php
+++ b/module/repo/lang/de.php
@@ -44,7 +44,7 @@ $lang->repo->name = 'Name';
$lang->repo->path = 'Path';
$lang->repo->prefix = 'Prefix';
$lang->repo->config = 'Config';
-$lang->repo->desc = 'Describe';
+$lang->repo->desc = 'Description';
$lang->repo->account = 'Username';
$lang->repo->password = 'Password';
$lang->repo->encoding = 'Encoding';
@@ -166,7 +166,7 @@ $lang->repo->example->client = new stdclass();
$lang->repo->example->path = new stdclass();
$lang->repo->example->client->git = "e.g. /usr/bin/git";
$lang->repo->example->client->svn = "e.g. /usr/bin/svn";
-$lang->repo->example->path->git = "e.g. /homt/user/myproject";
+$lang->repo->example->path->git = "e.g. /home/user/myproject";
$lang->repo->example->path->svn = "e.g. http://example.googlecode.com/svn/trunk/myproject";
$lang->repo->example->config = "Config directory is required in https. Use '--config-dir' to generate config dir.";
$lang->repo->example->encoding = "input encoding of files";
diff --git a/module/repo/lang/en.php b/module/repo/lang/en.php
index 4e4d05b42d..c425ff526e 100644
--- a/module/repo/lang/en.php
+++ b/module/repo/lang/en.php
@@ -44,7 +44,7 @@ $lang->repo->name = 'Name';
$lang->repo->path = 'Path';
$lang->repo->prefix = 'Prefix';
$lang->repo->config = 'Config';
-$lang->repo->desc = 'Describe';
+$lang->repo->desc = 'Description';
$lang->repo->account = 'Username';
$lang->repo->password = 'Password';
$lang->repo->encoding = 'Encoding';
@@ -166,7 +166,7 @@ $lang->repo->example->client = new stdclass();
$lang->repo->example->path = new stdclass();
$lang->repo->example->client->git = "e.g. /usr/bin/git";
$lang->repo->example->client->svn = "e.g. /usr/bin/svn";
-$lang->repo->example->path->git = "e.g. /homt/user/myproject";
+$lang->repo->example->path->git = "e.g. /home/user/myproject";
$lang->repo->example->path->svn = "e.g. http://example.googlecode.com/svn/trunk/myproject";
$lang->repo->example->config = "Config directory is required in https. Use '--config-dir' to generate config dir.";
$lang->repo->example->encoding = "input encoding of files";
diff --git a/module/repo/lang/fr.php b/module/repo/lang/fr.php
index 4e4d05b42d..c425ff526e 100644
--- a/module/repo/lang/fr.php
+++ b/module/repo/lang/fr.php
@@ -44,7 +44,7 @@ $lang->repo->name = 'Name';
$lang->repo->path = 'Path';
$lang->repo->prefix = 'Prefix';
$lang->repo->config = 'Config';
-$lang->repo->desc = 'Describe';
+$lang->repo->desc = 'Description';
$lang->repo->account = 'Username';
$lang->repo->password = 'Password';
$lang->repo->encoding = 'Encoding';
@@ -166,7 +166,7 @@ $lang->repo->example->client = new stdclass();
$lang->repo->example->path = new stdclass();
$lang->repo->example->client->git = "e.g. /usr/bin/git";
$lang->repo->example->client->svn = "e.g. /usr/bin/svn";
-$lang->repo->example->path->git = "e.g. /homt/user/myproject";
+$lang->repo->example->path->git = "e.g. /home/user/myproject";
$lang->repo->example->path->svn = "e.g. http://example.googlecode.com/svn/trunk/myproject";
$lang->repo->example->config = "Config directory is required in https. Use '--config-dir' to generate config dir.";
$lang->repo->example->encoding = "input encoding of files";
diff --git a/module/repo/lang/zh-cn.php b/module/repo/lang/zh-cn.php
index 3771bdb711..bec02c0421 100644
--- a/module/repo/lang/zh-cn.php
+++ b/module/repo/lang/zh-cn.php
@@ -167,7 +167,7 @@ $lang->repo->example->client = new stdclass();
$lang->repo->example->path = new stdclass();
$lang->repo->example->client->git = "例如:/usr/bin/git";
$lang->repo->example->client->svn = "例如:/usr/bin/svn";
-$lang->repo->example->path->git = "例如:/homt/user/myproject";
+$lang->repo->example->path->git = "例如:/home/user/myproject";
$lang->repo->example->path->svn = "例如:http://example.googlecode.com/svn/trunk/myproject";
$lang->repo->example->config = "https需要填写配置目录的位置,通过config-dir选项生成配置目录";
$lang->repo->example->encoding = "填写版本库中文件的编码";
diff --git a/module/repo/lang/zh-tw.php b/module/repo/lang/zh-tw.php
index 2033791753..9a6e580bb4 100644
--- a/module/repo/lang/zh-tw.php
+++ b/module/repo/lang/zh-tw.php
@@ -167,7 +167,7 @@ $lang->repo->example->client = new stdclass();
$lang->repo->example->path = new stdclass();
$lang->repo->example->client->git = "例如:/usr/bin/git";
$lang->repo->example->client->svn = "例如:/usr/bin/svn";
-$lang->repo->example->path->git = "例如:/homt/user/myproject";
+$lang->repo->example->path->git = "例如:/home/user/myproject";
$lang->repo->example->path->svn = "例如:http://example.googlecode.com/svn/trunk/myproject";
$lang->repo->example->config = "https需要填寫配置目錄的位置,通過config-dir選項生成配置目錄";
$lang->repo->example->encoding = "填寫版本庫中檔案的編碼";
diff --git a/module/story/config.php b/module/story/config.php
index e7395ee18b..bf40278686 100644
--- a/module/story/config.php
+++ b/module/story/config.php
@@ -194,5 +194,5 @@ $config->story->datatable->fieldList['caseCount']['name'] = $lang->story->ca
$config->story->datatable->fieldList['actions']['title'] = 'actions';
$config->story->datatable->fieldList['actions']['fixed'] = 'right';
-$config->story->datatable->fieldList['actions']['width'] = '150';
+$config->story->datatable->fieldList['actions']['width'] = '180';
$config->story->datatable->fieldList['actions']['required'] = 'yes';
diff --git a/module/story/control.php b/module/story/control.php
index 2bb81f7953..7400f88687 100644
--- a/module/story/control.php
+++ b/module/story/control.php
@@ -294,6 +294,13 @@ class story extends control
*/
public function batchCreate($productID = 0, $branch = 0, $moduleID = 0, $storyID = 0, $project = 0, $plan = 0, $type = 'story')
{
+ /* Check can subdivide or not. */
+ if($storyID)
+ {
+ $story = $this->story->getById($storyID);
+ if($story->status != 'active' or $story->stage != 'wait' or $story->parent > 0) die(js::alert($this->lang->story->errorNotSubdivide));
+ }
+
if(!empty($_POST))
{
$mails = $this->story->batchCreate($productID, $branch, $type);
@@ -307,7 +314,7 @@ class story extends control
if($storyID)
{
if(empty($mails)) die(js::closeModal('parent.parent', 'this'));
- $actionID = $this->story->subdivide($storyID, $mails);
+ $this->story->subdivide($storyID, $stories);
if(dao::isError()) die(js::error(dao::getError()));
@@ -474,6 +481,7 @@ class story extends control
$this->view->title = $this->lang->story->edit . "STORY" . $this->lang->colon . $this->view->story->title;
$this->view->position[] = $this->lang->story->edit;
$this->view->story = $story;
+ $this->view->stories = $this->story->getParentStoryPairs($story->product, $story->parent);
$this->view->users = $this->user->getPairs('pofirst|nodeleted', "$story->assignedTo,$story->openedBy,$story->closedBy");
$this->view->product = $product;
$this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($story->product);
@@ -766,6 +774,9 @@ class story extends control
*/
public function delete($storyID, $confirm = 'no')
{
+ $story = $this->story->getById($storyID);
+ if($story->parent < 0) die(js::alert($this->lang->story->cannotDeleteParent));
+
if($confirm == 'no')
{
echo js::confirm($this->lang->story->confirmDelete, $this->createLink('story', 'delete', "story=$storyID&confirm=yes"), '');
@@ -774,6 +785,11 @@ class story extends control
else
{
$this->story->delete(TABLE_STORY, $storyID);
+ if($story->parent > 0)
+ {
+ $this->story->updateParentStatus($story->id);
+ $this->loadModel('action')->create('story', $task->parent, 'deleteChildrenStory', '', $storyID);
+ }
$this->executeHooks($storyID);
@@ -1316,7 +1332,7 @@ class story extends control
* @access public
* @return void
*/
- public function ajaxGetProductStories($productID, $branch = 0, $moduleID = 0, $storyID = 0, $onlyOption = 'false', $status = '', $limit = 0, $type = 'full')
+ public function ajaxGetProductStories($productID, $branch = 0, $moduleID = 0, $storyID = 0, $onlyOption = 'false', $status = '', $limit = 0, $type = 'full', $hasParent = 1)
{
if($moduleID)
{
@@ -1332,7 +1348,7 @@ class story extends control
$storyStatus = array_keys($storyStatus);
}
- $stories = $this->story->getProductStoryPairs($productID, $branch ? "0,$branch" : $branch, $moduleID, $storyStatus, 'id_desc', $limit, $type);
+ $stories = $this->story->getProductStoryPairs($productID, $branch ? "0,$branch" : $branch, $moduleID, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent);
$select = html::select('story', empty($stories) ? array('' => '') : $stories, $storyID, "class='form-control'");
/* If only need options, remove select wrap. */
@@ -1511,6 +1527,32 @@ class story extends control
while($row = $stmt->fetch()) $stories[$row->id] = $row;
}
+ if($stories)
+ {
+ $children = array();
+ foreach($stories as $story)
+ {
+ if($story->parent > 0 and isset($stories[$story->parent]))
+ {
+ $children[$story->parent][$story->id] = $story;
+ unset($stories[$story->id]);
+ }
+ }
+ if(!empty($children))
+ {
+ $position = 0;
+ foreach($stories as $story)
+ {
+ $position ++;
+ if(isset($children[$story->id]))
+ {
+ array_splice($stories, $position, 0, $children[$story->id]);
+ $position += count($children[$story->id]);
+ }
+ }
+ }
+ }
+
/* Get users, products and projects. */
$users = $this->loadModel('user')->getPairs('noletter');
$products = $this->loadModel('product')->getPairs('nocode');
@@ -1667,6 +1709,8 @@ class story extends control
}
$story->reviewedBy = rtrim($story->reviewedBy, ',');
+ /* Set child story title. */
+ if($story->parent > 0 && strpos($story->title, htmlentities('>')) !== 0) $story->title = '>' . $story->title;
}
if($projectID)
diff --git a/module/story/lang/zh-cn.php b/module/story/lang/zh-cn.php
index b4286f9271..92a4648b37 100644
--- a/module/story/lang/zh-cn.php
+++ b/module/story/lang/zh-cn.php
@@ -61,6 +61,7 @@ $lang->story->viewAll = '查看全部';
$lang->story->common = $lang->storyCommon;
$lang->story->id = '编号';
+$lang->story->parent = '父需求';
$lang->story->product = "所属{$lang->productCommon}";
$lang->story->branch = "分支/平台";
$lang->story->module = '所属模块';
@@ -101,6 +102,8 @@ $lang->story->version = '版本号';
$lang->story->plan = '所属计划';
$lang->story->planAB = '计划';
$lang->story->comment = '备注';
+$lang->story->children = "子{$lang->storyCommon}";
+$lang->story->childrenAB = "子";
$lang->story->linkStories = "相关{$lang->storyCommon}";
$lang->story->childStories = "细分{$lang->storyCommon}";
$lang->story->duplicateStory = "重复{$lang->storyCommon}ID";
@@ -213,10 +216,12 @@ $lang->story->needNotReview = '不需要评审';
$lang->story->successSaved = "{$lang->storyCommon}成功添加,";
$lang->story->confirmDelete = "您确认删除该{$lang->storyCommon}吗?";
$lang->story->errorEmptyChildStory = "『细分{$lang->storyCommon}』不能为空。";
+$lang->story->errorNotSubdivide = "状态不是激活,或者阶段不是未开始的{$lang->storyCommon},或者是子需求,则不能细分。";
$lang->story->mustChooseResult = '必须选择评审结果';
$lang->story->mustChoosePreVersion = '必须选择回溯的版本';
$lang->story->noStory = "暂时没有{$lang->storyCommon}。";
$lang->story->ignoreChangeStage = "{$lang->storyCommon} %s 为草稿状态,没有修改其阶段。";
+$lang->story->cannotDeleteParent = "不能删除父{$lang->storyCommon}";
$lang->story->form = new stdclass();
$lang->story->form->area = "该{$lang->storyCommon}所属范围";
diff --git a/module/story/model.php b/module/story/model.php
index 135d0b17fb..0109ec631b 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -33,6 +33,9 @@ class storyModel extends model
$story->spec = isset($spec->spec) ? $spec->spec : '';
$story->verify = isset($spec->verify) ? $spec->verify : '';
+ /* Check parent story. */
+ if($story->parent > 0) $story->parentName = $this->dao->findById($story->parent)->from(TABLE_STORY)->fetch('title');
+
$story = $this->loadModel('file')->replaceImgURL($story, 'spec,verify');
if($setImgSize) $story->spec = $this->file->setImgSize($story->spec);
if($setImgSize) $story->verify = $this->file->setImgSize($story->verify);
@@ -62,6 +65,10 @@ class storyModel extends model
if($story->childStories) $extraStories = array_merge($extraStories, explode(',', $story->childStories));
$extraStories = array_unique($extraStories);
if(!empty($extraStories)) $story->extraStories = $this->dao->select('id,title')->from(TABLE_STORY)->where('id')->in($extraStories)->fetchPairs();
+
+ $story->children = array();
+ if($story->parent == '-1') $story->children = $this->dao->select('*')->from(TABLE_STORY)->where('parent')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll('id');
+
return $story;
}
@@ -574,11 +581,179 @@ class storyModel extends model
$this->dao->replace(TABLE_PROJECTPRODUCT)->data($data)->exec();
}
}
+
+ $this->loadModel('action');
+ $changed = $story->parent != $oldStory->parent;
+ if($oldStory->parent > 0)
+ {
+ $oldParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($oldStory->parent)->fetch();
+ $this->updateParentStatus($storyID, $oldStory->parent, !$changed);
+
+ if($changed)
+ {
+ $oldChildren = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($oldStory->parent)->andWhere('deleted')->eq(0)->fetchPairs('id', 'id');
+ if(empty($oldChildren)) $this->dao->update(TABLE_STORY)->set('parent')->eq(0)->where('id')->eq($oldStory->parent)->exec();
+ $this->dao->update(TABLE_STORY)->set('childStories')->eq(join(',', $oldChildren))->set('lastEditedBy')->eq($this->app->user->account)->set('lastEditedDate')->eq(helper::now())->where('id')->eq($oldStory->parent)->exec();
+ $this->action->create('story', $storyID, 'unlinkParentStory', '', $oldStory->parent, '', false);
+
+ $actionID = $this->action->create('story', $oldStory->parent, 'unLinkChildrenStory', '', $storyID, '', false);
+
+ $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($oldStory->parent)->fetch();
+ $changes = common::createChanges($oldParentStory, $newParentStory);
+ if(!empty($changes)) $this->action->logHistory($actionID, $changes);
+ }
+ }
+
+ if($story->parent > 0)
+ {
+ $parentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($story->parent)->fetch();
+ $this->dao->update(TABLE_STORY)->set('parent')->eq(-1)->where('id')->eq($story->parent)->exec();
+ $this->updateParentStatus($storyID, $story->parent, !$changed);
+
+ if($changed)
+ {
+ $children = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($story->parent)->andWhere('deleted')->eq(0)->fetchPairs('id', 'id');
+ $this->dao->update(TABLE_STORY)
+ ->set('parent')->eq('-1')
+ ->set('childStories')->eq(join(',', $children))
+ ->set('lastEditedBy')->eq($this->app->user->account)
+ ->set('lastEditedDate')->eq(helper::now())
+ ->where('id')->eq($story->parent)
+ ->exec();
+
+ $this->action->create('story', $storyID, 'linkParentStory', '', $story->parent, '', false);
+ $actionID = $this->action->create('story', $story->parent, 'linkChildStory', '', $storyID, '', false);
+
+ $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($story->parent)->fetch();
+ $changes = common::createChanges($parentStory, $newParentStory);
+ if(!empty($changes)) $this->action->logHistory($actionID, $changes);
+ }
+ }
+
if(isset($story->closedReason) and $story->closedReason == 'done') $this->loadModel('score')->create('story', 'close');
+
+ unset($oldStory->parent);
+ unset($story->parent);
return common::createChanges($oldStory, $story);
}
}
+ /**
+ * Update parent status.
+ *
+ * @param int $storyID
+ * @param int $parentID
+ * @param bool $createAction
+ * @access public
+ * @return mixed
+ */
+ public function updateParentStatus($storyID, $parentID = 0, $createAction = true)
+ {
+ $childStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($storyID)->fetch();
+ if(empty($parentID)) $parentID = $childStory->parent;
+ if($parentID <= 0) return true;
+
+ $oldParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($parentID)->andWhere('deleted')->eq(0)->fetch();
+ if(empty($oldParentStory)) return $this->dao->update(TABLE_STORY)->set('parent')->eq('0')->where('id')->eq($storyID)->exec();
+ if($oldParentStory->parent != '-1') $this->dao->update(TABLE_STORY)->set('parent')->eq('-1')->where('id')->eq($parentID)->exec();
+ $this->computeEstimate($parentID);
+
+ $childrenStatus = $this->dao->select('id,status')->from(TABLE_STORY)->where('parent')->eq($parentID)->andWhere('deleted')->eq(0)->fetchPairs('status', 'status');
+ if(empty($childrenStatus)) return $this->dao->update(TABLE_STORY)->set('parent')->eq('0')->where('id')->eq($parentID)->exec();
+
+ $status = $oldParentStory->status;
+ if(count($childrenStatus) == 1 and $oldParentStory->status != 'changed')
+ {
+ $status = current($childrenStatus);
+ if($status == 'draft' or $status == 'changed') $status = 'active';
+ }
+ elseif(count($childrenStatus) != 1 and $oldParentStory->status == 'closed')
+ {
+ $status = 'active';
+ }
+
+ if($status and $oldParentStory->status != $status)
+ {
+ $now = helper::now();
+ $story = new stdclass();
+ $story->status = $status;
+ $story->stage = 'wait';
+ if($status == 'active')
+ {
+ $story->assignedTo = $oldParentStory->openedBy;
+ $story->assignedDate = $now;
+ $story->closedBy = '';
+ $story->closedReason = '';
+ $story->closedDate = '0000-00-00';
+ $story->reviewedBy = '';
+ $story->reviewedDate = '0000-00-00';
+ }
+
+ if($status == 'closed')
+ {
+ $story->assignedTo = 'closed';
+ $story->assignedDate = $now;
+ $story->closedBy = $this->app->user->account;
+ $story->closedDate = $now;
+ $story->closedReason = 'done';
+ $story->closedReason = 'done';
+ }
+
+ $story->lastEditedBy = $this->app->user->account;
+ $story->lastEditedDate = $now;
+ $story->parent = '-1';
+ $this->dao->update(TABLE_STORY)->data($story)->where('id')->eq($parentID)->exec();
+ if(!dao::isError())
+ {
+ if(!$createAction) return $story;
+
+ $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($parentID)->fetch();
+ $changes = common::createChanges($oldParentStory, $newParentStory);
+ $action = '';
+ if($status == 'active') $action = 'Activated';
+ if($status == 'closed') $action = 'Closed';
+ if($action)
+ {
+ $actionID = $this->loadModel('action')->create('story', $parentID, $action, '', '', '', false);
+ $this->action->logHistory($actionID, $changes);
+ }
+ }
+ }
+ else
+ {
+ if(!dao::isError())
+ {
+ $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($parentID)->fetch();
+ $changes = common::createChanges($oldParentStory, $newParentStory);
+ if($changes)
+ {
+ $actionID = $this->loadModel('action')->create('story', $parentID, 'Edited', '', '', '', false);
+ $this->action->logHistory($actionID, $changes);
+ }
+ }
+ }
+ }
+
+ /**
+ * Compute parent story estimate.
+ *
+ * @param int $storyID
+ * @access public
+ * @return bool
+ */
+ public function computeEstimate($storyID)
+ {
+ if(!$storyID) return true;
+
+ $stories = $this->dao->select('`id`,`estimate`,status')->from(TABLE_STORY)->where('parent')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll('id');
+ if(empty($stories)) return true;
+
+ $estimate = 0;
+ foreach($stories as $story) $estimate += $story->estimate;
+ $this->dao->update(TABLE_STORY)->set('estimate')->eq($estimate)->autoCheck()->where('id')->eq($storyID)->exec();
+ return !dao::isError();
+ }
+
/**
* Batch update stories.
*
@@ -810,40 +985,36 @@ class storyModel extends model
* @param int $storyID
* @param array $stories
* @access public
- * @return int
+ * @return void
*/
public function subdivide($storyID, $stories)
{
$now = helper::now();
$oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch();
+ /* Set parent to child story. */
+ $this->dao->update(TABLE_STORY)->set('parent')->eq($storyID)->where('id')->in($stories)->exec();
+ $this->computeEstimate($storyID);
+
/* Set childStories. */
- $childStories = '';
- foreach($stories as $story) $childStories .= $story->storyID . ',';
- $childStories = trim($childStories, ',');
+ $childStories = join(',', $stories);
$newStory = new stdClass();
+ $newStory->parent = '-1';
$newStory->plan = 0;
$newStory->lastEditedBy = $this->app->user->account;
$newStory->lastEditedDate = $now;
- $newStory->closedDate = $now;
- $newStory->closedBy = $this->app->user->account;
- $newStory->assignedTo = 'closed';
- $newStory->assignedDate = $now;
- $newStory->status = 'closed';
- $newStory->closedReason = 'subdivided';
- $newStory->childStories = $childStories;
+ $newStory->childStories = trim($oldStory->childStories . ',' . $childStories, ',');
- /* Subdivide story and close it. */
- $this->dao->update(TABLE_STORY)->data($newStory)
- ->autoCheck()
- ->batchCheck($this->config->story->close->requiredFields, 'notempty')
- ->where('id')->eq($storyID)->exec();
- $changes = common::createChanges($oldStory, $newStory);
- $actionID = $this->loadModel('action')->create('story', $storyID, 'Closed', '', 'Subdivided');
- $this->action->logHistory($actionID, $changes);
+ /* Subdivide story. */
+ $this->dao->update(TABLE_STORY)->data($newStory)->autoCheck()->where('id')->eq($storyID)->exec();
- return $actionID;
+ $changes = common::createChanges($oldStory, $newStory);
+ if($changes)
+ {
+ $actionID = $this->loadModel('action')->create('story', $storyID, 'createChildrenStory', '', $childStories);
+ $this->action->logHistory($actionID, $changes);
+ }
}
/**
@@ -876,6 +1047,10 @@ class storyModel extends model
->batchCheck($this->config->story->close->requiredFields, 'notempty')
->checkIF($story->closedReason == 'duplicate', 'duplicateStory', 'notempty')
->where('id')->eq($storyID)->exec();
+
+ /* Update parent story status. */
+ if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
+
if(!dao::isError()) $this->loadModel('score')->create('story', 'close', $storyID);
return common::createChanges($oldStory, $story);
}
@@ -934,6 +1109,9 @@ class storyModel extends model
if(!dao::isError())
{
+ /* Update parent story status. */
+ if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
+
$allChanges[$storyID] = common::createChanges($oldStory, $story);
}
else
@@ -1164,6 +1342,9 @@ class storyModel extends model
$this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq($storyID)->exec();
$this->setStage($storyID);
+ /* Update parent story status. */
+ if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
+
return common::createChanges($oldStory, $story);
}
@@ -1392,11 +1573,12 @@ class storyModel extends model
* @param string $type requirement|story
* @param string $orderBy
* @param object $pager
+ * @param bool $hasParent
*
* @access public
* @return array
*/
- public function getProductStories($productID = 0, $branch = 0, $moduleIdList = 0, $status = 'all', $type = 'story', $orderBy = 'id_desc', $pager = null)
+ public function getProductStories($productID = 0, $branch = 0, $moduleIdList = 0, $status = 'all', $type = 'story', $orderBy = 'id_desc', $pager = null, $hasParent = true)
{
if(defined('TUTORIAL')) return $this->loadModel('tutorial')->getStories();
@@ -1408,12 +1590,13 @@ class storyModel extends model
}
$stories = $this->dao->select('*')->from(TABLE_STORY)
->where('product')->in($productID)
+ ->beginIF(!$hasParent)->andWhere("parent")->ge(0)->fi()
->beginIF($branch)->andWhere("branch")->in($branch)->fi()
->beginIF(!empty($moduleIdList))->andWhere('module')->in($moduleIdList)->fi()
->beginIF($status and $status != 'all')->andWhere('status')->in($status)->fi()
->andWhere('deleted')->eq(0)
->andWhere('type')->eq($type)
- ->orderBy($orderBy)->page($pager)->fetchAll();
+ ->orderBy($orderBy)->page($pager)->fetchAll('id');
return $this->mergePlanTitle($productID, $stories, $branch);
}
@@ -1427,10 +1610,11 @@ class storyModel extends model
* @param int $limit
* @param string $type
* @param string $storyType requirement|story
+ * @param bool $hasParent
* @access public
* @return array
*/
- public function getProductStoryPairs($productID = 0, $branch = 0, $moduleIdList = 0, $status = 'all', $order = 'id_desc', $limit = 0, $type = 'full', $storyType = 'story')
+ public function getProductStoryPairs($productID = 0, $branch = 0, $moduleIdList = 0, $status = 'all', $order = 'id_desc', $limit = 0, $type = 'full', $storyType = 'story', $hasParent = true)
{
if($branch) $branch = "0,$branch";//Fix bug 1059.
$stories = $this->dao->select('t1.id, t1.title, t1.module, t1.pri, t1.estimate, t2.name AS product')
@@ -1439,6 +1623,7 @@ class storyModel extends model
->beginIF($productID)->andWhere('t1.product')->in($productID)->fi()
->beginIF($moduleIdList)->andWhere('t1.module')->in($moduleIdList)->fi()
->beginIF($branch)->andWhere('t1.branch')->in($branch)->fi()
+ ->beginIF(!$hasParent)->andWhere('t1.parent')->ge(0)->fi()
->beginIF($status and $status != 'all')->andWhere('t1.status')->in($status)->fi()
->andWhere('t1.deleted')->eq(0)
->andWhere('t1.type')->eq($storyType)
@@ -1571,7 +1756,7 @@ class storyModel extends model
->beginIF($operator == 'include')->andWhere($fieldName)->like("%$fieldValue%")->fi()
->orderBy($orderBy)
->page($pager)
- ->fetchAll();
+ ->fetchAll('id');
return $this->mergePlanTitle($productID, $stories, $branch);
}
@@ -1877,6 +2062,18 @@ class storyModel extends model
->fetchAll();
}
+ public function getParentStoryPairs($productID, $append = '')
+ {
+ $stories = $this->dao->select('id, title')->from(TABLE_STORY)
+ ->where('deleted')->eq(0)
+ ->andWhere('parent')->le(0)
+ ->andWhere('status')->notin('closed,draft')
+ ->andWhere('product')->eq($productID)
+ ->beginIF($append)->orWhere('id')->in($append)->fi()
+ ->fetchPairs();
+ return array(0 => '') + $stories ;
+ }
+
/**
* Get stories of a user.
*
@@ -1903,7 +2100,7 @@ class storyModel extends model
->fi()
->orderBy($orderBy)
->page($pager)
- ->fetchAll();
+ ->fetchAll('id');
$this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false);
$productIdList = array();
@@ -1984,7 +2181,7 @@ class storyModel extends model
*/
public function getZeroCase($productID, $orderBy = 'id_desc')
{
- $allStories = $this->getProductStories($productID, 0, 0, 'all', 'story', $orderBy);
+ $allStories = $this->getProductStories($productID, 0, 0, 'all', 'story', $orderBy, null, $hasParent = false);
$casedStories = $this->dao->select('DISTINCT story')->from(TABLE_CASE)->where('product')->eq($productID)->andWhere('story')->ne(0)->andWhere('deleted')->eq(0)->fetchAll('story');
foreach($allStories as $key => $story)
@@ -2413,11 +2610,15 @@ class storyModel extends model
{
$action = strtolower($action);
+ if($story->parent < 0 and $action != 'edit' and $action != 'batchcreate') return false;
+
if($action == 'change') return $story->status != 'closed';
if($action == 'review') return $story->status == 'draft' or $story->status == 'changed';
if($action == 'close') return $story->status != 'closed';
if($action == 'activate') return $story->status == 'closed';
if($action == 'assignto') return $story->status != 'closed';
+ if($action == 'batchcreate' and $story->parent > 0) return false;
+ if($action == 'batchcreate' and ($story->status != 'active' or $story->stage != 'wait')) return false;
return true;
}
@@ -2446,8 +2647,31 @@ class storyModel extends model
->andWhere('deleted')->eq(0)
->fetchPairs('id', 'title');
+ $parents = array();
+ $tmpStories = array();
foreach($stories as $story)
{
+ $tmpStories[$story->id] = $story;
+ if($story->parent > 0) $parents[$story->parent] = $story->parent;
+ }
+ $parents = $this->dao->select('*')->from(TABLE_STORY)->where('id')->in($parents)->fetchAll('id');
+
+ foreach($stories as $storyID => $story)
+ {
+ if($story->parent > 0)
+ {
+ if(isset($stories[$story->parent]))
+ {
+ $stories[$story->parent]->children[$story->id] = $story;
+ unset($stories[$storyID]);
+ }
+ else
+ {
+ $parent = $parents[$story->parent];
+ $story->parentName = $story->title;
+ }
+ }
+
$story->planTitle = '';
$storyPlans = explode(',', trim($story->plan, ','));
foreach($storyPlans as $planID) $story->planTitle .= zget($plans, $planID, '') . ' ';
@@ -2484,14 +2708,21 @@ class storyModel extends model
$class = "c-{$id}";
$title = '';
- if ($id == 'assignedTo')
+ if($id == 'assignedTo')
{
$title = zget($users, $story->assignedTo, $story->assignedTo);
if($story->assignedTo == $account) $class .= ' red';
}
- else if($id == 'openedBy') $title = zget($users, $story->openedBy, $story->openedBy);
- else if($id == 'title') $title = $story->title;
- else if($id == 'plan')
+ elseif($id == 'openedBy')
+ {
+ $title = zget($users, $story->openedBy, $story->openedBy);
+ }
+ elseif($id == 'title')
+ {
+ $title = $story->title;
+ if(!empty($story->children)) $class .= ' has-child';
+ }
+ elseif($id == 'plan')
{
$title = $story->planTitle;
$class .= ' text-ellipsis';
@@ -2524,9 +2755,12 @@ class storyModel extends model
echo "";
break;
case 'title':
+ if($story->parent > 0 and isset($story->parentName)) $story->title = "{$story->parentName} / {$story->title}";
if($story->branch and isset($branches[$story->branch])) echo "{$branches[$story->branch]} ";
if($story->module and isset($modulePairs[$story->module])) echo "{$modulePairs[$story->module]} ";
+ if($story->parent > 0) echo '' . $this->lang->story->childrenAB . ' ';
echo $canView ? html::a($storyLink, $story->title, '', "style='color: $story->color'") : "{$story->title}";
+ if(!empty($story->children)) echo '';
break;
case 'plan':
echo $story->planTitle;
@@ -2634,6 +2868,7 @@ class storyModel extends model
common::printIcon('story', 'close', $vars, $story, 'list', '', '', 'iframe', true);
common::printIcon('story', 'edit', $vars, $story, 'list');
if($this->config->global->flow != 'onlyStory') common::printIcon('story', 'createCase', "productID=$story->product&branch=$story->branch&module=0&from=¶m=0&$vars", $story, 'list', 'sitemap');
+ common::printIcon('story', 'batchCreate', "productID=$story->product&branch=$story->branch&module=0&storyID=$story->id", $story, 'list', 'treemap-alt', '', '', '', '', $this->lang->story->subdivide);
break;
}
echo ' | ';
diff --git a/module/story/view/batchcreate.html.php b/module/story/view/batchcreate.html.php
index f3955135f8..7b16d7bd21 100644
--- a/module/story/view/batchcreate.html.php
+++ b/module/story/view/batchcreate.html.php
@@ -13,7 +13,7 @@
-
story->batchCreate;?>
+
lang->story->subdivide : $this->lang->story->batchCreate;?>