diff --git a/db/update15.0.sql b/db/update15.0.sql
new file mode 100644
index 0000000000..edfd21a4ec
--- /dev/null
+++ b/db/update15.0.sql
@@ -0,0 +1,5 @@
+ALTER TABLE `zt_project`
+CHANGE `type` `type` varchar(20) COLLATE 'utf8_general_ci' NOT NULL DEFAULT 'waterfall' AFTER `catID`,
+ADD `program` mediumint(8) NOT NULL DEFAULT '0' AFTER `type`,
+ADD `budget` varchar(30) NOT NULL DEFAULT '0' AFTER `program`,
+ADD `budgetUnit` char(30) NOT NULL DEFAULT 'yuan' AFTER `budget`;
diff --git a/module/common/lang/menuOrder.php b/module/common/lang/menuOrder.php
index b5451721b9..e2233d4372 100644
--- a/module/common/lang/menuOrder.php
+++ b/module/common/lang/menuOrder.php
@@ -1,14 +1,15 @@
menuOrder[5] = 'my';
-$lang->menuOrder[10] = 'product';
-$lang->menuOrder[15] = 'project';
-$lang->menuOrder[20] = 'qa';
-$lang->menuOrder[25] = 'ci';
-$lang->menuOrder[30] = 'doc';
-$lang->menuOrder[35] = 'report';
-$lang->menuOrder[40] = 'company';
-$lang->menuOrder[45] = 'admin';
+$lang->menuOrder[10] = 'program';
+$lang->menuOrder[15] = 'product';
+$lang->menuOrder[20] = 'project';
+$lang->menuOrder[25] = 'qa';
+$lang->menuOrder[30] = 'ci';
+$lang->menuOrder[35] = 'doc';
+$lang->menuOrder[40] = 'report';
+$lang->menuOrder[45] = 'company';
+$lang->menuOrder[50] = 'admin';
/* index menu order. */
$lang->index->menuOrder[5] = 'product';
diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php
index d236defa09..25d6f1f916 100644
--- a/module/common/lang/zh-cn.php
+++ b/module/common/lang/zh-cn.php
@@ -118,17 +118,20 @@ $lang->common = new stdclass();
$lang->common->common = '公有模块';
/* 主导航菜单。*/
-$lang->menu = new stdclass();
-$lang->menu->my = ' 地盘|my|index';
-$lang->menu->product = ' 项目|product|index|locate=no';
-$lang->menu->doc = ' 文档|doc|index';
-$lang->menu->report = ' 报表|report|index';
-$lang->menu->company = ' 组织|company|index';
-$lang->menu->admin = ' 后台|admin|index';
-$lang->menu->recent = ' 近期|recent|index';
+$lang->mainNav = new stdclass();
+$lang->mainNav->my = ' 地盘|my|index|';
+$lang->mainNav->program = ' 项目|program|index|';
+$lang->mainNav->doc = ' 文档|doc|index|';
+$lang->mainNav->report = ' 报表|report|index|';
+$lang->mainNav->company = ' 组织|company|index|';
+$lang->mainNav->admin = ' 后台|admin|index|';
+$lang->mainNav->recent = ' 近期|recent|index|';
$lang->dividerMenu = ',admin,recent,';
+$lang->menu = new stdclass();
+$lang->menu->project = new stdclass();
+
/* 查询条中可以选择的对象列表。*/
$lang->searchObjects['bug'] = 'Bug';
$lang->searchObjects['story'] = "{$lang->storyCommon}";
@@ -501,6 +504,23 @@ $lang->menugroup->jenkins = 'ci';
$lang->menugroup->compile = 'ci';
$lang->menugroup->job = 'ci';
+$lang->navGroup = array();
+$lang->navGroup['product'] = 'program';
+$lang->navGroup['story'] = 'program';
+$lang->navGroup['productplan'] = 'program';
+$lang->navGroup['release'] = 'program';
+$lang->navGroup['tree'] = 'program';
+$lang->navGroup['project'] = 'program';
+$lang->navGroup['task'] = 'program';
+$lang->navGroup['qa'] = 'program';
+$lang->navGroup['bug'] = 'program';
+$lang->navGroup['testcase'] = 'program';
+$lang->navGroup['testtask'] = 'program';
+$lang->navGroup['testreport'] = 'program';
+$lang->navGroup['testsuite'] = 'program';
+$lang->navGroup['caselib'] = 'program';
+$lang->navGroup['feedback'] = 'program';
+
/* 错误提示信息。*/
$lang->error = new stdclass();
$lang->error->companyNotFound = "您访问的域名 %s 没有对应的公司。";
diff --git a/module/common/model.php b/module/common/model.php
index 5e90461df2..3fb37007f0 100644
--- a/module/common/model.php
+++ b/module/common/model.php
@@ -437,6 +437,32 @@ class commonModel extends model
}
}
+ /**
+ * Print the main nav.
+ *
+ * @param string $moduleName
+ * @param string $methodName
+ *
+ * @static
+ * @access public
+ * @return void
+ */
+ public static function printMainNav($moduleName, $methodName = '')
+ {
+ global $app, $lang;
+
+ echo "
\n";
+ foreach($lang->mainNav as $group => $nav)
+ {
+ $active = '';
+ list($title, $currentModule, $currentMethod, $vars) = explode('|', $nav);
+ if($moduleName == $group) $active = 'active';
+ if(zget($lang->navGroup, $moduleName, '') == $group) $active = 'active';
+ if(common::hasPriv($currentModule, $currentMethod)) echo "- " . html::a(helper::createLink($currentModule, $currentMethod, $vars), $title) . '
';
+ }
+ echo "
\n";
+ }
+
/**
* Print the main menu.
*
diff --git a/module/common/view/header.html.php b/module/common/view/header.html.php
index 59ade367a4..ba008d8af3 100755
--- a/module/common/view/header.html.php
+++ b/module/common/view/header.html.php
@@ -14,12 +14,13 @@ if($extHookFiles) foreach($extHookFiles as $extHookFile) include $extHookFile;
app->loadConfig('sso');?>
sso->redirect)) js::set('ssoRedirect', $config->sso->redirect);?>
+navGroup[$moduleName]) && $lang->navGroup[$this->moduleName] == 'program');?>
+
+
sso->redirect))
{
diff --git a/module/program/config.php b/module/program/config.php
new file mode 100644
index 0000000000..dd9b6aff81
--- /dev/null
+++ b/module/program/config.php
@@ -0,0 +1,10 @@
+program = new stdclass();
+$config->program->editor = new stdclass();
+$config->program->editor->create = array('id' => 'desc', 'tools' => 'simpleTools');
+$config->program->editor->edit = array('id' => 'desc', 'tools' => 'simpleTools');
+$config->program->editor->close = array('id' => 'comment', 'tools' => 'simpleTools');
+$config->program->editor->start = array('id' => 'comment', 'tools' => 'simpleTools');
+$config->program->editor->activate = array('id' => 'comment', 'tools' => 'simpleTools');
+$config->program->editor->finish = array('id' => 'comment', 'tools' => 'simpleTools');
+$config->program->editor->suspend = array('id' => 'comment', 'tools' => 'simpleTools');
diff --git a/module/program/control.php b/module/program/control.php
new file mode 100644
index 0000000000..3b893f37da
--- /dev/null
+++ b/module/program/control.php
@@ -0,0 +1,262 @@
+loadModel('project');
+ }
+
+ public function index($status = 'all', $orderBy = 'order_desc', $recTotal = 0, $recPerPage = 10, $pageID = 1)
+ {
+ if(common::hasPriv('program', 'create')) $this->lang->pageActions = html::a($this->createLink('program', 'create'), " " . $this->lang->program->create, '', "class='btn btn-primary'");
+
+ //$programType = $this->cookie->programType;
+ $programType = 'bylist';
+
+ if($programType == 'bylist')
+ {
+ $this->app->loadClass('pager', $static = true);
+ $pager = new pager($recTotal, $recPerPage, $pageID);
+ $projectList = $this->project->getProjectStats($status, 0, 0, 30, $orderBy, $pager, 'program');
+ $this->view->pager = $pager;
+ }
+
+ if(!$programType || $programType == 'bygrid') $projectList = $this->project->getList($status, 0, 0, 0, 'program');
+
+ $this->view->projectList = $projectList;
+ $this->view->status = $status;
+ $this->view->orderBy = $orderBy;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->title = $this->lang->program->index;
+ $this->view->position[] = $this->lang->program->index;
+ $this->view->programType = $programType;
+ $this->display();
+ }
+
+ public function create()
+ {
+ if($_POST)
+ {
+ $projectID = $this->program->create();
+ if(dao::isError())
+ {
+ $this->send(array('result' => 'fail', 'message' => $this->processErrors(dao::getError())));
+ }
+ $this->loadModel('action')->create('project', $projectID, 'opened');
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('index')));
+ }
+
+ $this->view->pmUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted|pmfirst');
+ $this->view->title = $this->lang->program->create;
+ $this->view->position[] = $this->lang->program->create;
+ $this->view->groups = $this->loadModel('group')->getPairs();
+ $this->display();
+ }
+
+ public function edit($projectID = 0)
+ {
+ $project = $this->project->getByID($projectID);
+
+ if($_POST)
+ {
+ $changes = $this->project->update($projectID);
+ if(dao::isError()) $this->send(array('result' => 'fail', 'message' => $this->processErrors(dao::getError())));
+ if($changes)
+ {
+ $actionID = $this->loadModel('action')->create('project', $projectID, 'edited');
+ $this->action->logHistory($actionID, $changes);
+ }
+ $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('index')));
+ }
+
+ $this->view->pmUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted|pmfirst', $project->PM);
+ $this->view->title = $this->lang->project->edit;
+ $this->view->position[] = $this->lang->project->edit;
+ $this->view->project = $project;
+ $this->view->groups = $this->loadModel('group')->getPairs();
+ $this->display();
+ }
+
+ /**
+ * Start project.
+ *
+ * @param int $projectID
+ * @access public
+ * @return void
+ */
+ public function start($projectID)
+ {
+ $project = $this->project->getByID($projectID);
+ $projectID = $project->id;
+
+ if(!empty($_POST))
+ {
+ $this->loadModel('action');
+ $changes = $this->project->start($projectID);
+ if(dao::isError()) die(js::error(dao::getError()));
+
+ if($this->post->comment != '' or !empty($changes))
+ {
+ $actionID = $this->action->create('project', $projectID, 'Started', $this->post->comment);
+ $this->action->logHistory($actionID, $changes);
+ }
+ $this->executeHooks($projectID);
+ die(js::reload('parent.parent'));
+ }
+
+ $this->view->title = $this->lang->project->start;
+ $this->view->position[] = $this->lang->project->start;
+ $this->view->project = $project;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->actions = $this->loadModel('action')->getList('project', $projectID);
+ $this->display();
+ }
+
+ /**
+ * Finish project.
+ *
+ * @param int $projectID
+ * @access public
+ * @return void
+ */
+ public function finish($projectID)
+ {
+ $project = $this->project->getByID($projectID);
+ $projectID = $project->id;
+
+ if(!empty($_POST))
+ {
+ $this->loadModel('action');
+ $changes = $this->project->finish($projectID);
+ if(dao::isError()) die(js::error(dao::getError()));
+
+ if($this->post->comment != '' or !empty($changes))
+ {
+ $actionID = $this->action->create('project', $projectID, 'Finished', $this->post->comment);
+ $this->action->logHistory($actionID, $changes);
+ }
+ $this->executeHooks($projectID);
+ die(js::reload('parent.parent'));
+ }
+
+ $this->view->title = $this->lang->project->finish;
+ $this->view->position[] = $this->lang->project->finish;
+ $this->view->project = $project;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->actions = $this->loadModel('action')->getList('project', $projectID);
+ $this->display();
+ }
+
+ public function delete($projectID, $confirm = 'no')
+ {
+ if($confirm == 'no')
+ {
+ echo js::confirm(sprintf($this->lang->project->confirmDelete, $this->projects[$projectID]), $this->createLink('project', 'delete', "projectID=$projectID&confirm=yes"));
+ exit;
+ }
+ else
+ {
+ $this->project->delete(TABLE_PROJECT, $projectID);
+ die(js::locate(inlink('index'), 'parent'));
+ }
+ }
+
+ public function suspend($projectID)
+ {
+ $project = $this->project->getByID($projectID);
+
+ if(!empty($_POST))
+ {
+ $this->loadModel('action');
+ $changes = $this->project->suspend($projectID);
+ if(dao::isError()) die(js::error(dao::getError()));
+
+ if($this->post->comment != '' or !empty($changes))
+ {
+ $actionID = $this->action->create('project', $projectID, 'Suspended', $this->post->comment);
+ $this->action->logHistory($actionID, $changes);
+ }
+ $this->executeHooks($projectID);
+ die(js::reload('parent.parent'));
+ }
+
+ $this->view->title = $this->lang->project->suspend;
+ $this->view->position[] = $this->lang->project->suspend;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->actions = $this->loadModel('action')->getList('project', $projectID);
+ $this->view->project = $project;
+ $this->display('project', 'suspend');
+ }
+
+ public function activate($projectID)
+ {
+ $project = $this->project->getByID($projectID);
+
+ if(!empty($_POST))
+ {
+ $this->loadModel('action');
+ $changes = $this->project->activate($projectID);
+ if(dao::isError()) die(js::error(dao::getError()));
+
+ if($this->post->comment != '' or !empty($changes))
+ {
+ $actionID = $this->action->create('project', $projectID, 'Activated', $this->post->comment);
+ $this->action->logHistory($actionID, $changes);
+ }
+ $this->executeHooks($projectID);
+ die(js::reload('parent.parent'));
+ }
+
+ $newBegin = date('Y-m-d');
+ $dateDiff = helper::diffDate($newBegin, $project->begin);
+ $newEnd = date('Y-m-d', strtotime($project->end) + $dateDiff * 24 * 3600);
+
+ $this->view->title = $this->lang->project->activate;
+ $this->view->position[] = $this->lang->project->activate;
+ $this->view->project = $project;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->actions = $this->loadModel('action')->getList('project', $projectID);
+ $this->view->newBegin = $newBegin;
+ $this->view->newEnd = $newEnd;
+ $this->view->project = $project;
+ $this->display('project', 'activate');
+ }
+
+ public function close($projectID)
+ {
+ $project = $this->project->getByID($projectID);
+
+ if(!empty($_POST))
+ {
+ $this->loadModel('action');
+ $changes = $this->project->close($projectID);
+ if(dao::isError()) die(js::error(dao::getError()));
+
+ if($this->post->comment != '' or !empty($changes))
+ {
+ $actionID = $this->action->create('project', $projectID, 'Closed', $this->post->comment);
+ $this->action->logHistory($actionID, $changes);
+ }
+ $this->executeHooks($projectID);
+ die(js::reload('parent.parent'));
+ }
+
+ $this->view->title = $this->lang->project->close;
+ $this->view->position[] = $this->lang->project->close;
+ $this->view->project = $project;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->actions = $this->loadModel('action')->getList('project', $projectID);
+ $this->display('project', 'close');
+ }
+
+ public function processErrors($errors)
+ {
+ foreach($errors as $field => $error)
+ {
+ $errors[$field] = str_replace($this->lang->program->stage, $this->lang->program->common, $error);
+ }
+
+ return $errors;
+ }
+}
diff --git a/module/program/css/common.css b/module/program/css/common.css
new file mode 100644
index 0000000000..2033bfefb9
--- /dev/null
+++ b/module/program/css/common.css
@@ -0,0 +1,7 @@
+#whitelistBox .checkbox-inline, #whitelistBox .radio-inline
+{
+ margin-left: 10px;
+ display: inline-block;
+ padding-left: 0px;
+ padding-right: 10px;
+}
diff --git a/module/program/css/index.css b/module/program/css/index.css
new file mode 100644
index 0000000000..fc77748c40
--- /dev/null
+++ b/module/program/css/index.css
@@ -0,0 +1,3 @@
+.panel-title {height:55px; padding:10px;}
+.main-table.flow-block {height:203px;}
+.main-table.panel-heading > strong,.main-table.panel-heading > span {margin-bottom:5px}
diff --git a/module/program/js/common.js b/module/program/js/common.js
new file mode 100644
index 0000000000..92397e88dc
--- /dev/null
+++ b/module/program/js/common.js
@@ -0,0 +1,16 @@
+$().ready(function()
+{
+ $('#navbar').find('li').removeClass('active');
+ if(typeof(status) == 'undefined' || !status) status = 'all';
+ $('#navbar').find('a[href*=' + status + ']').parent().addClass('active');
+});
+function setWhite(acl)
+{
+ acl == 'custom' ? $('#whitelistBox').removeClass('hidden') : $('#whitelistBox').addClass('hidden');
+}
+
+function setProgramType(type)
+{
+ $.cookie('programType', type, {expires:config.cookieLife, path:config.webRoot});
+ location.href = location.href;
+}
diff --git a/module/program/lang/zh-cn.php b/module/program/lang/zh-cn.php
new file mode 100644
index 0000000000..0057a534cf
--- /dev/null
+++ b/module/program/lang/zh-cn.php
@@ -0,0 +1,35 @@
+program->index = '项目主页';
+$lang->program->common = '项目';
+$lang->program->stage = '阶段';
+$lang->program->name = '项目名称';
+$lang->program->type = '项目类型';
+$lang->program->desc = '项目描述';
+$lang->program->code = '项目代号';
+$lang->program->begin = '计划开始日期';
+$lang->program->end = '计划完成日期';
+$lang->program->PM = '项目负责人';
+$lang->program->create = '添加项目';
+$lang->program->edit = '编辑项目';
+$lang->program->all = '所有项目';
+$lang->program->start = '启动';
+$lang->program->finish = '完成';
+$lang->program->suspend = '挂起';
+$lang->program->delete = '删除';
+$lang->program->close = '关闭';
+$lang->program->activate = '激活';
+$lang->program->budget = '项目预算';
+$lang->program->dateRange = '起止时间';
+$lang->program->to = '至';
+$lang->program->realFinished = '实际完成日期';
+$lang->program->realStarted = '实际开始日期';
+
+$lang->program->durationEstimation = '工作量估算';
+$lang->program->noProgram = '暂时没有项目';
+
+$lang->program->unitList[''] = '';
+$lang->program->unitList['yuan'] = '元';
+$lang->program->unitList['dollar'] = 'Dollars';
+
+$lang->program->typeList['scrum'] = "Scrum";
+$lang->program->typeList['cmmi'] = "瀑布";
diff --git a/module/program/model.php b/module/program/model.php
new file mode 100644
index 0000000000..87d0c5bd06
--- /dev/null
+++ b/module/program/model.php
@@ -0,0 +1,90 @@
+lang->project->team = $this->lang->project->teamname;
+ $project = fixer::input('post')
+ ->setDefault('status', 'wait')
+ ->setDefault('type', 'program')
+ ->setIF($this->post->acl != 'custom', 'whitelist', '')
+ ->setDefault('openedBy', $this->app->user->account)
+ ->setDefault('openedDate', helper::now())
+ ->setDefault('team', substr($this->post->name,0, 30))
+ ->join('whitelist', ',')
+ ->cleanInt('budget')
+ ->stripTags($this->config->project->editor->create['id'], $this->config->allowedTags)
+ ->remove('products, workDays, delta, branch, uid, plans')
+ ->get();
+
+ $project = $this->loadModel('file')->processImgURL($project, $this->config->project->editor->create['id'], $this->post->uid);
+ $this->dao->insert(TABLE_PROJECT)->data($project)
+ ->autoCheck()
+ ->batchcheck($this->config->project->create->requiredFields, 'notempty')
+ ->check('name', 'unique', "deleted='0'")
+ ->check('code', 'unique', "deleted='0'")
+ ->exec();
+
+ /* Add the creater to the team. */
+ if(!dao::isError())
+ {
+ $projectID = $this->dao->lastInsertId();
+ $today = helper::today();
+
+ /* Save order. */
+ $this->dao->update(TABLE_PROJECT)->set('`order`')->eq($projectID * 5)->where('id')->eq($projectID)->exec();
+ $this->file->updateObjectID($this->post->uid, $projectID, 'project');
+
+ $product = new stdclass();
+ $product->name = $project->name;
+ $product->project = $projectID;
+ $product->status = 'normal';
+ $product->createdBy = $this->app->user->account;
+ $product->createdDate = helper::now();
+
+ $this->dao->insert(TABLE_PRODUCT)->data($product)->exec();
+
+ $productID = $this->dao->lastInsertId();
+ $this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($productID * 5)->where('id')->eq($productID)->exec();
+
+ /* Create doc lib. */
+ $this->app->loadLang('doc');
+ $lib = new stdclass();
+ $lib->product = $productID;
+ $lib->name = $this->lang->doclib->main['product'];
+ $lib->type = 'product';
+ $lib->main = '1';
+ $lib->acl = 'default';
+ $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
+
+ $docLibID = $this->dao->lastInsertId();
+ $this->loadModel('doc')->syncDocModule($docLibID);
+
+ /* Insert into projectproduct. */
+ //$data = new stdclass();
+ //$data->project = $projectID;
+ //$data->product = $productID;
+ //$this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec();
+
+ return $projectID;
+ }
+ }
+
+ public static function isClickable($project, $action)
+ {
+ $action = strtolower($action);
+
+ if($action == 'start') return $project->status == 'wait' or $project->status == 'suspended';
+ if($action == 'finish') return $project->status == 'wait' or $project->status == 'doing';
+ if($action == 'close') return $project->status != 'closed';
+ if($action == 'suspend') return $project->status == 'wait' or $project->status == 'doing';
+ if($action == 'activate') return $project->status == 'done';
+
+ return true;
+ }
+
+ public function getProducts($program)
+ {
+ return $this->dao->select('*')->from(TABLE_PRODUCT)->where('project')->eq($program)->fetchAll('id');
+ }
+}
diff --git a/module/program/view/create.html.php b/module/program/view/create.html.php
new file mode 100644
index 0000000000..60e6c00369
--- /dev/null
+++ b/module/program/view/create.html.php
@@ -0,0 +1,114 @@
+
+ * @package project
+ * @version $Id: create.html.php 4728 2013-05-03 06:14:34Z chencongzhi520@gmail.com $
+ * @link http://www.zentao.net
+ */
+?>
+
+ createLink('project', 'task', 'projectID=' . $projectID);?>
+
+
+
+
+