diff --git a/db/zentao.sql b/db/zentao.sql index 055668213d..f9f104ca7f 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -1552,5 +1552,11 @@ INSERT INTO `zt_groupPriv` (`company`, `group`, `module`, `method`) VALUES (1, 2, 'todo', 'batchEdit'), (1, 3, 'todo', 'batchEdit'), (1, 4, 'todo', 'batchEdit'), -(1, 5, 'todo', 'batchEdit'); +(1, 5, 'todo', 'batchEdit'), +(1, 1, 'task', 'batchEdit'), +(1, 2, 'task', 'batchEdit'), +(1, 3, 'task', 'batchEdit'), +(1, 4, 'task', 'batchEdit'), +(1, 5, 'task', 'batchEdit'); + diff --git a/module/common/lang/en.php b/module/common/lang/en.php index bf9d60c427..b1ae18fa1a 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -294,6 +294,8 @@ $lang->zentaoDESC = "ZenTaoPMS is an open sourced project management system. +$lang->suhosinInfo = "Warming:data is too large! Please enlarge the setting of sohusin.post.max_vars and sohusin.request.max_vars in php.ini. Otherwise partial data can't be saved."; + /* Date times. */ define('DT_DATETIME1', 'Y-m-d H:i:s'); define('DT_DATETIME2', 'y-m-d H:i'); diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index d0db5636e8..d828d1209b 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -294,6 +294,8 @@ $lang->zentaoDESC = "禅道项目管理软件(ZenTaoPMS)是一款国产的 它集产品管理、项目管理、测试管理于一体,同时还包含了事务管理、组织管理等诸多功能,是中小型企业项目管理的首选。禅道项目管理软件使用PHP + MySQL开发, 基于自主的PHP开发框架──ZenTaoPHP而成。第三方开发者或者企业可以非常方便的开发插件或者进行定制。禅道在手,项目无忧!"; +$lang->suhosinInfo = "警告:数据太多,请在php.ini中修改sohusin.post.max_vars和sohusin.request.max_vars(设置更大的数)。 保存并重新启动apache,否则会造成部分数据无法保存。"; + /* 时间格式设置。*/ define('DT_DATETIME1', 'Y-m-d H:i:s'); define('DT_DATETIME2', 'y-m-d H:i'); diff --git a/module/common/model.php b/module/common/model.php index 37e0008e19..1ddc5fa394 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -364,4 +364,24 @@ class commonModel extends model ksort($diff); return implode("\n", $diff); } + + /** + * Judge Suhosin Setting whether the actual size of post data is large than the setting size. + * + * @param int $numberOfItems + * @param int $columns + * @access public + * @return void + */ + public function judgeSuhosinSetting($numberOfItems, $columns) + { + if(extension_loaded('suhosin')) + { + $maxPostVars = ini_get('suhosin.post.max_vars'); + $maxRequestVars = ini_get('suhosin.request.max_vars'); + if($numberOfItems * $columns > $maxPostVars or $numberOfItems * $columns > $maxRequestVars) return true; + } + + return false; + } } diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index d8df2d4354..1f6d8c4af2 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -119,6 +119,7 @@ $lang->resource->project->ajaxGetProducts= 'ajaxGetProducts'; /* Task. */ $lang->resource->task->create = 'create'; $lang->resource->task->batchCreate = 'batchCreate'; +$lang->resource->task->batchEdit = 'batchEdit'; $lang->resource->task->edit = 'edit'; $lang->resource->task->assignTo = 'assign'; $lang->resource->task->start = 'start'; @@ -371,3 +372,4 @@ $lang->changelog['3.1'][] = 'todo-batchCreate'; $lang->changelog['3.2'][] = 'my-changePassword'; $lang->changelog['3.2'][] = 'todo-batchEdit'; +$lang->changelog['3.2'][] = 'task-batchEdit'; diff --git a/module/project/view/task.html.php b/module/project/view/task.html.php index af390e5fbd..fe846f71fa 100644 --- a/module/project/view/task.html.php +++ b/module/project/view/task.html.php @@ -17,108 +17,118 @@ var browseType = '';
'>
- - id&status=$status&parma=$param&orderBy=%s&recTotal=$recTotal&recPerPage=$recPerPage"; ?> - - - - - - - +id&from=projectTask&status=$status¶m=$param&orderBy=$orderBy");?>'> +
idAB);?> priAB);?> task->name);?>statusAB);?> task->deadlineAB);?>
+ id&status=$status&parma=$param&orderBy=%s&recTotal=$recTotal&recPerPage=$recPerPage"; ?> + + + + + + + - cookie->windowWidth > $this->config->wideSize):?> - - + cookie->windowWidth > $this->config->wideSize):?> + + - - + + - cookie->windowWidth > $this->config->wideSize):?> - - + cookie->windowWidth > $this->config->wideSize):?> + + - - - - - - - - - - - assignedTo == $app->user->account ? 'style=color:red' : '';?> - estimate; - $totalConsumed += $task->consumed; - $totalLeft += (($task->status == 'cancel' or $task->closedReason == 'cancel') ? 0 : $task->left); - $statusVar = 'status' . ucfirst($task->status); - $$statusVar ++; - ?> - - - - - - + + + + + + + + + + + assignedTo == $app->user->account ? 'style=color:red' : '';?> + estimate; + $totalConsumed += $task->consumed; + $totalLeft += (($task->status == 'cancel' or $task->closedReason == 'cancel') ? 0 : $task->left); + $statusVar = 'status' . ucfirst($task->status); + $$statusVar ++; + ?> + + + + + + - cookie->windowWidth > $this->config->wideSize):?> - - + + - cookie->windowWidth > $this->config->wideSize):?> - - - - - - - - - - - cookie->windowWidth > $this->config->wideSize ? 14 : 12;?> - - - -
idAB);?> priAB);?> task->name);?>statusAB);?> task->deadlineAB);?> task->openedDateAB);?> task->openedDateAB);?> task->assignedToAB);?> task->finishedByAB);?> task->assignedToAB);?> task->finishedByAB);?> task->finishedDateAB);?> task->finishedDateAB);?> task->estimateAB);?> task->consumedAB);?> task->leftAB);?>task->story);?>actions;?>
id", sprintf('%03d', $task->id))) printf('%03d', $task->id);?>task->priList[$task->pri];?> - fromBug != 0) echo "[BUG] "; - else echo "[TASK] "; - if(!common::printLink('task', 'view', "task=$task->id", $task->name)) echo $task->name; - ?> - status;?> > - storyStatus == 'active' and $task->latestStoryVersion > $task->storyVersion); - $storyChanged ? print("{$lang->story->changed} ") : print($lang->task->statusList[$task->status]); - ?> - delay)) echo 'delayed';?>>deadline, 0, 4) > 0) echo substr($task->deadline, 5, 6);?> task->estimateAB);?> task->consumedAB);?> task->leftAB);?>task->story);?>actions;?>
+ + id", sprintf('%03d', $task->id))) printf('%03d', $task->id);?> + task->priList[$task->pri];?> + fromBug != 0) echo "[BUG] "; + else echo "[TASK] "; + if(!common::printLink('task', 'view', "task=$task->id", $task->name)) echo $task->name; + ?> + status;?> > + storyStatus == 'active' and $task->latestStoryVersion > $task->storyVersion); + $storyChanged ? print("{$lang->story->changed} ") : print($lang->task->statusList[$task->status]); + ?> + delay)) echo 'delayed';?>>deadline, 0, 4) > 0) echo substr($task->deadline, 5, 6);?>openedDate, 5, 6);?> - + cookie->windowWidth > $this->config->wideSize):?> + openedDate, 5, 6);?> + - >assignedToRealName;?>finishedBy];?>>assignedToRealName;?>finishedBy];?>finishedDate, 5, 6);?> - + cookie->windowWidth > $this->config->wideSize):?> + finishedDate, 5, 6);?> + - estimate;?>consumed;?>left;?> - storyID and common::hasPriv('story', 'view')) $story = html::a($this->createLink('story', 'view', "storyid=$task->storyID"), $task->storyTitle); - if($task->storyID and !common::hasPriv('story', 'view')) $story = $task->storyTitle; - echo $story; - ?> - - status == 'wait' or $task->status == 'doing') and common::printLink('task', 'finish', "taskID=$task->id", $lang->task->buttonDone))) echo $lang->task->buttonDone . ' '; - if(!(($task->status == 'done' or $task->status == 'cancel') and common::printLink('task', 'close', "taskID=$task->id", $lang->task->buttonClose))) echo $lang->task->buttonClose . ' '; - if(!common::printLink('task', 'edit', "taskID=$task->id", $lang->task->buttonEdit)) echo $lang->task->buttonEdit . ' '; - if($browseType == 'needconfirm') common::printLink('task', 'confirmStoryChange', "taskid=$task->id", $lang->confirm, 'hiddenwin'); - ?> -
-
project->taskSummary, count($tasks), $statusWait, $statusDoing, $totalEstimate, $totalConsumed, $totalLeft);?>
- show();?> -
+ estimate;?> + consumed;?> + left;?> + + storyID and common::hasPriv('story', 'view')) $story = html::a($this->createLink('story', 'view', "storyid=$task->storyID"), $task->storyTitle); + if($task->storyID and !common::hasPriv('story', 'view')) $story = $task->storyTitle; + echo $story; + ?> + + + status == 'wait' or $task->status == 'doing') and common::printLink('task', 'finish', "taskID=$task->id", $lang->task->buttonDone))) echo $lang->task->buttonDone . ' '; + if(!(($task->status == 'done' or $task->status == 'cancel') and common::printLink('task', 'close', "taskID=$task->id", $lang->task->buttonClose))) echo $lang->task->buttonClose . ' '; + if(!common::printLink('task', 'edit', "taskID=$task->id", $lang->task->buttonEdit)) echo $lang->task->buttonEdit . ' '; + if($browseType == 'needconfirm') common::printLink('task', 'confirmStoryChange', "taskid=$task->id", $lang->confirm, 'hiddenwin'); + ?> + + + + + + + cookie->windowWidth > $this->config->wideSize ? 14 : 12;?> + +
+ task->batchEdit); + printf($lang->project->taskSummary, count($tasks), $statusWait, $statusDoing, $totalEstimate, $totalConsumed, $totalLeft); + ?> +
+ show();?> + + + + + diff --git a/module/task/control.php b/module/task/control.php index 95a502cd6b..87302499c3 100644 --- a/module/task/control.php +++ b/module/task/control.php @@ -211,6 +211,129 @@ class task extends control $this->display(); } + /** + * Batch edit task. + * + * @param int $projectID + * @param string $from example:projectTask, taskBatchEdit + * @param string $status + * @param int $param + * @param string $orderBy + * @access public + * @return void + */ + public function batchEdit($projectID = 0, $from = '', $status = 'all', $param = 0, $orderBy = '') + { + /* Initialize vars. */ + $browseType = strtolower($status); + $queryID = ($browseType == 'bysearch') ? (int)$param : 0; + $showSuhosinInfo = false; + $parentLink = $this->session->taskList; + $project = $this->project->getById($projectID); + $allTasks = array(); + $editedTasks = array(); + $taskIDList = array(); + $columns = 13; + if(!$orderBy) $orderBy = $this->cookie->projectTaskOrder ? $this->cookie->projectTaskOrder : 'status,id_desc'; + + /* Set project menu. */ + $this->project->setMenu($this->project->getPairs(), $project->id); + + /* Get all tasks. */ + if($browseType != "bysearch") + { + $allTasks = $this->task->getProjectTasks($projectID, $status, $orderBy, null); + } + else + { + if($queryID) + { + $query = $this->loadModel('search')->getQuery($queryID); + if($query) + { + $this->session->set('taskQuery', $query->sql); + $this->session->set('taskForm', $query->form); + } + else + { + $this->session->set('taskQuery', ' 1 = 1'); + } + } + else + { + if($this->session->taskQuery == false) $this->session->set('taskQuery', ' 1 = 1'); + } + $taskQuery = str_replace("`project` = 'all'", '1', $this->session->taskQuery); // Search all project. + $allTasks = $this->project->getSearchTasks($taskQuery, null, $orderBy); + } + + /* Get form data for project-task. */ + if($from == 'projectTask') + { + if($this->post->taskIDList) + { + $taskIDList = $this->post->taskIDList; + } + elseif(!isset($this->post->taskIDList) and $this->session->taskIDList) + { + $taskIDList = $this->session->taskIDList; + } + + /* Initialize the tasks whose need to edited. */ + foreach($allTasks as $task) if(in_array($task->id, $taskIDList)) $editedTasks[$task->id] = $task; + + $showSuhosinInfo = $this->loadModel('common')->judgeSuhosinSetting(count($editedTasks), $columns); + + /* Set the sessions. */ + $this->app->session->set('taskIDList', $taskIDList); + $this->app->session->set('showSuhosinInfo', $showSuhosinInfo); + } + /* Get form data for task-batchEdit. */ + elseif($from == 'taskBatchEdit') + { + $allChanges = $this->task->batchUpdate(); + + if(!empty($allChanges)) + { + foreach($allChanges as $taskID => $changes) + { + if(!empty($changes)) + { + $actionID = $this->loadModel('action')->create('task', $taskID, 'Edited'); + $this->action->logHistory($actionID, $changes); + $this->sendmail($taskID, $actionID); + + $task = $this->task->getById($taskID); + if($task->fromBug != 0) + { + foreach($changes as $change) + { + if($change['field'] == 'status') + { + $confirmURL = $this->createLink('bug', 'view', "id=$task->fromBug"); + $cancelURL = $this->server->HTTP_REFERER; + die(js::confirm(sprintf($this->lang->task->remindBug, $task->fromBug), $confirmURL, $cancelURL, 'parent', 'parent')); + } + } + } + } + } + } + die(js::locate($parentLink)); + } + + $this->view->header['title'] = $project->name . $this->lang->colon . $this->lang->task->batchEdit; + $this->view->position[] = $this->lang->task->batchEdit; + + if($showSuhosinInfo) $this->view->suhosinInfo = $this->lang->suhosinInfo; + $this->view->projectID = $project->id; + $this->view->editedTasks = $editedTasks; + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->members = $this->project->getTeamMemberPairs($projectID, 'nodeleted'); + + $this->display(); + } + /** * Update assign of task * diff --git a/module/task/css/batchedit.css b/module/task/css/batchedit.css new file mode 100755 index 0000000000..807ac72522 --- /dev/null +++ b/module/task/css/batchedit.css @@ -0,0 +1,2 @@ +.w-300px{width:300px} + diff --git a/module/task/lang/en.php b/module/task/lang/en.php index ad7c7e8abb..c43911cd3f 100644 --- a/module/task/lang/en.php +++ b/module/task/lang/en.php @@ -12,6 +12,7 @@ $lang->task->index = "Index"; $lang->task->create = "Create"; $lang->task->batchCreate = "Batch create"; +$lang->task->batchEdit = "Batch edit"; $lang->task->import = "Import undone"; $lang->task->edit = "Update"; $lang->task->delete = "Delete"; diff --git a/module/task/lang/zh-cn.php b/module/task/lang/zh-cn.php index bf763047dc..c296573468 100644 --- a/module/task/lang/zh-cn.php +++ b/module/task/lang/zh-cn.php @@ -12,6 +12,7 @@ $lang->task->index = "任务一览"; $lang->task->create = "新增任务"; $lang->task->batchCreate = "批量添加"; +$lang->task->batchEdit = "批量编辑"; $lang->task->import = "导入之前未完任务"; $lang->task->edit = "编辑任务"; $lang->task->delete = "删除任务"; diff --git a/module/task/model.php b/module/task/model.php index 86dd3623ed..0d14fe2e3f 100644 --- a/module/task/model.php +++ b/module/task/model.php @@ -204,6 +204,126 @@ class taskModel extends model if(!dao::isError()) return common::createChanges($oldTask, $task); } + /** + * Batch update task. + * + * @param array $taskIDList + * @access public + * @return void + */ + public function batchUpdate() + { + $tasks = array(); + $allChanges = array(); + $now = helper::now(); + $taskIDList = $this->post->taskIDList ? $this->post->taskIDList : array(); + + /* Adjust whether the post data is complete, if not, remove the last element of $taskIDList. */ + if($this->session->showSuhosinInfo) array_pop($taskIDList); + + /* Initialize tasks from the post data.*/ + if(!empty($taskIDList)) + { + foreach($taskIDList as $taskID) + { + $oldTask = $this->getById($taskID); + + $task->name = $this->post->names[$taskID]; + $task->assignedTo = $this->post->assignedTos[$taskID]; + $task->type = $this->post->types[$taskID]; + $task->status = $this->post->statuses[$taskID]; + $task->pri = $this->post->pris[$taskID]; + $task->estimate = $this->post->estimates[$taskID]; + $task->consumed = $this->post->consumeds[$taskID]; + $task->left = $this->post->lefts[$taskID]; + $task->finishedBy = $this->post->finishedBys[$taskID]; + $task->canceledBy = $this->post->canceledBys[$taskID]; + $task->closedBy = $this->post->closedBys[$taskID]; + $task->closedReason = $this->post->closedReasons[$taskID]; + $task->finishedDate = ""; + $task->canceledDate = ""; + $task->closedDate = ""; + $task->lastEditedBy = $this->app->user->account; + $task->lastEditedDate = $now; + if(isset($this->post->assignedTos[$taskID])) + { + $task->assignedDate = $this->post->assignedTos[$taskID] == $oldTask->assignedTo ? $oldTask->assignedDate : $now; + } + + switch($task->status) + { + case 'done': + { + $task->left = 0; + if(!$task->finishedBy) $task->finishedBy = $this->app->user->account; + if(!$task->finishedDate) $task->finishedDate = $now; + }break; + case 'cancel': + { + $task->assignedTo = $oldTask->openedBy; + $task->assignedDate = $now; + + if(!$task->canceledBy) $task->canceledBy = $this->app->user->account; + if(!$task->canceledDate) $task->canceledDate = $now; + }break; + case 'closed': + { + if(!$task->closedBy) $task->closedBy = $this->app->user->account; + if(!$task->closedDate) $task->closedDate = $now; + }break; + case 'wait': + { + if($task->consumed > 0 and $task->left > 0) $task->status = 'doing'; + if($task->left == $oldTask->left and $task->consumed == 0) $task->left = $task->estimate; + } + default:break; + } + if($task->assignedTo) $task->assignedDate = $now; + + $tasks[$taskID] = $task; + unset($task); + } + + /* Update task data. */ + foreach($tasks as $taskID => $task) + { + $oldTask = $this->getById($taskID); + $this->dao->update(TABLE_TASK)->data($task) + ->autoCheck() + ->batchCheckIF($task->status != 'cancel', $this->config->task->edit->requiredFields, 'notempty') + + ->checkIF($task->estimate != false, 'estimate', 'float') + ->checkIF($task->consumed != false, 'consumed', 'float') + ->checkIF($task->left != false, 'left', 'float') + ->checkIF($task->left == 0 and $task->status != 'cancel' and $task->status != 'closed' and $task->consumed != 0, 'status', 'equal', 'done') + + ->batchCheckIF($task->status == 'wait' or $task->status == 'doing', 'finishedBy, finishedDate,canceledBy, canceledDate, closedBy, closedDate, closedReason', 'empty') + + ->checkIF($task->status == 'done', 'consumed', 'notempty') + ->checkIF($task->status == 'done' and $task->closedReason, 'closedReason', 'equal', 'done') + ->batchCheckIF($task->status == 'done', 'canceledBy, canceledDate', 'empty') + + ->checkIF($task->status == 'closed', 'closedReason', 'notempty') + ->batchCheckIF($task->closedReason == 'cancel', 'finishedBy, finishedDate', 'empty') + ->where('id')->eq((int)$taskID) + ->exec(); + + if($oldTask->story != false) $this->loadModel('story')->setStage($oldTask->story); + if(!dao::isError()) + { + $allChanges[$taskID] = common::createChanges($oldTask, $task); + } + else + { + echo js::error(dao::getError()); + die(js::locate('back')); + } + } + } + + return $allChanges; + } + /** * Assign a task to a user again. * diff --git a/module/task/view/batchedit.html.php b/module/task/view/batchedit.html.php new file mode 100755 index 0000000000..c600c89f53 --- /dev/null +++ b/module/task/view/batchedit.html.php @@ -0,0 +1,56 @@ + + * @package task + * @version $Id$ + * @link http://www.zentao.net + */ +?> + +
"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
task->common . $lang->colon . $lang->task->batchEdit;?>
idAB;?> task->name;?>task->assignedTo;?>typeAB;?>task->status;?>task->pri;?>task->estimateAB;?>task->consumedAB;?>task->leftAB;?>task->finishedBy;?>task->canceledBy;?>task->closedBy;?>task->closedReason;?>
id . html::hidden("taskIDList[$task->id]", $task->id);?>id]", $task->name, 'class=text-1'); echo "*";?>id]", $members, $task->assignedTo, 'class=select-1');?>id]", $lang->task->typeList, $task->type, 'class=select-1');?>id]", $lang->task->statusList, $task->status, 'class=select-1');?>id]", (array)$lang->task->priList, $task->pri, 'class=select-1');?>id]", $task->estimate, 'class=text-1'); echo "*";?>id]", $task->consumed, 'class=text-1'); echo "*";?>id]", $task->left, 'class=text-1'); echo "*";?>id]", $members, $task->finishedBy, 'class=select-1');?>id]", $members, $task->canceledBy, 'class=select-1');?>id]", $members, $task->closedBy, 'class=select-1');?>id]", $lang->task->reasonList, $task->closedReason, 'class=select-1');?>
+
+ diff --git a/www/js/my.full.js b/www/js/my.full.js index 996ee9b17f..444c572193 100644 --- a/www/js/my.full.js +++ b/www/js/my.full.js @@ -261,7 +261,7 @@ function switchProject(projectID, module, method) } /* Process task and build modules. */ - if(module == 'task' && (method == 'view' || method == 'edit')) + if(module == 'task' && (method == 'view' || method == 'edit' || method == 'batchedit')) { module = 'project'; method = 'task';