* @package product * @link https://www.zentao.net */ class productZen extends product { /** * 为控制层的all函数设置共享环境数据。 * Set shared environment data for all function of control layer. * * @access protected * @return void */ protected function setMenu4All() { /* Set redirect URI. */ $this->session->set('productList', $this->app->getURI(true), 'product'); /* Set activated menu for mobile view. */ if($this->app->viewType == 'mhtml') { $productID = $this->product->checkAccess(0, $this->products); $this->product->setMenu($productID); } } /** * 设置项目页面导航菜单。 * Set navigation menus for project page. * * @param int $productID * @param string $branch * @param string $preBranch * @access protected * @return void */ protected function setProjectMenu($productID, $branch, $preBranch) { $branch = ($preBranch !== '' and $branch === '') ? $preBranch : $branch; helper::setcookie('preBranch', $branch); $this->session->set('createProjectLocate', $this->app->getURI(true), 'product'); $this->product->setMenu($productID, $branch); } /** * 为创建产品设置导航数据,主要是替换占位符。 * Set menu for create product page. * * @param int $programID * @access protected * @return void */ protected function setCreateMenu($programID) { if($this->app->tab == 'program') common::setMenuVars('program', $programID); if($this->app->tab == 'doc') unset($this->lang->doc->menu->product['subMenu']); if($this->app->getViewType() != 'mhtml') return; if($this->app->rawModule == 'projectstory' and $this->app->rawMethod == 'story') return $this->loadModel('project')->setMenu(); $this->product->setMenu(); } /** * 为编辑产品设置导航数据,主要是替换占位符。 * Set menu for edit product page. * * @param int $productID * @param int $programID * @access protected * @return void */ protected function setEditMenu($productID, $programID) { if($programID) return common::setMenuVars('program', $programID); $this->product->setMenu($productID); } /** * 设置跟踪矩阵导航 * Set menu for track * * @param int $productID * @param string $branch * @param int $projectID * @access protected * @return void */ protected function setTrackMenu($productID, $branch, $projectID) { helper::setcookie('preBranch', $branch); /* Save session. */ $uri = $this->app->getURI(true); $this->session->set('storyList', $uri, 'product'); $this->session->set('taskList', $uri, 'execution'); $this->session->set('designList', $uri, 'project'); $this->session->set('bugList', $uri, 'qa'); $this->session->set('caseList', $uri, 'qa'); $this->session->set('revisionList', $uri, 'repo'); if($projectID) return $this->loadModel('project')->setMenu($projectID); $productID = $this->product->checkAccess($productID, $this->products); $this->product->setMenu($productID, $branch); } /** * 为showErrorNone方法,根据不同模块,设置不同的二级或三级导航配置。 * Set menu for showErrorNone page. * * @param string $moduleName project|qa|execution * @param string $activeMenu * @param int $objectID * @access protected * @return void */ protected function setShowErrorNoneMenu($moduleName, $activeMenu, $objectID) { if($this->app->getViewType() == 'mhtml') return $this->product->setMenu(); if($moduleName == 'qa') $this->setShowErrorNoneMenu4QA($activeMenu); if($moduleName == 'project') $this->setShowErrorNoneMenu4Project($activeMenu, $objectID); if($moduleName == 'execution') $this->setShowErrorNoneMenu4Execution($activeMenu, $objectID); } /** * 为showErrorNone方法,设置在测试视图中的二级导航配置。 * Set menu for showErrorNone page in qa. * * @param string $activeMenu * @access private * @return void */ private function setShowErrorNoneMenu4QA($activeMenu) { $this->loadModel('qa')->setMenu(); $this->view->moduleName = 'qa'; $this->app->rawModule = $activeMenu; if($activeMenu == 'testcase') unset($this->lang->qa->menu->testcase['subMenu']); if($activeMenu == 'testsuite') unset($this->lang->qa->menu->testcase['subMenu']); if($activeMenu == 'testtask') unset($this->lang->qa->menu->testtask['subMenu']); if($activeMenu == 'testreport') unset($this->lang->qa->menu->testtask['subMenu']); } /** * 为showErrorNone方法,设置在项目视图中的二级或三级导航配置。 * Set menu for showErrorNone page in project. * * @param string $activeMenu * @param int $projectID * @access private * @return void */ private function setShowErrorNoneMenu4Project($activeMenu, $projectID) { $this->loadModel('project')->setMenu($projectID); $this->app->rawModule = $activeMenu; $project = $this->project->getByID($projectID); $model = zget($project, 'model', 'scrum'); $this->lang->project->menu = $this->lang->{$model}->menu; $this->lang->project->menuOrder = $this->lang->{$model}->menuOrder; if($activeMenu == 'bug') $this->lang->{$model}->menu->qa['subMenu']->bug['subModule'] = 'product'; if($activeMenu == 'testcase') $this->lang->{$model}->menu->qa['subMenu']->testcase['subModule'] = 'product'; if($activeMenu == 'testtask') $this->lang->{$model}->menu->qa['subMenu']->testtask['subModule'] = 'product'; if($activeMenu == 'testreport') $this->lang->{$model}->menu->qa['subMenu']->testreport['subModule'] = 'product'; if($activeMenu == 'projectrelease') $this->lang->{$model}->menu->release['subModule'] = 'projectrelease'; } /** * 为showErrorNone方法,设置在执行视图中的三级导航配置。 * Set menu for showErrorNone page in execution. * * @param string $activeMenu * @param int $executionID * @access private * @return void */ private function setShowErrorNoneMenu4Execution($activeMenu, $executionID) { $this->loadModel('execution')->setMenu($executionID); $this->app->rawModule = $activeMenu; if($activeMenu == 'bug') $this->lang->execution->menu->qa['subMenu']->bug['subModule'] = 'product'; if($activeMenu == 'testcase') $this->lang->execution->menu->qa['subMenu']->testcase['subModule'] = 'product'; if($activeMenu == 'testtask') $this->lang->execution->menu->qa['subMenu']->testtask['subModule'] = 'product'; if($activeMenu == 'testreport') $this->lang->execution->menu->qa['subMenu']->testreport['subModule'] = 'product'; } /** * 通过extra获取返回链接。 * Get goback link for create by extra. * * @param string $extra * @access private * @return string */ protected function getBackLink4Create($extra) { $extra = str_replace(array(',', ' '), array('&', ''), $extra); parse_str($extra, $output); $backLink = ''; $from = zget($output, 'from', ''); if($from == 'qa') $backLink = $this->createLink('qa', 'index'); if($from == 'global') $backLink = $this->createLink('product', 'all'); return $backLink; } /** * 设置表单字段下拉选项。 * Set form select options. * * @param int $programID * @param array $fields * @access private * @return array */ private function setSelectFormOptions($programID, $fields) { $users = $this->loadModel('user')->getPairs('nodeleted|noclosed'); /* 追加字段的name、title属性,展开user数据。 */ foreach($fields as $field => $attr) { if(isset($attr['options']) and $attr['options'] == 'users') $fields[$field]['options'] = $users; if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field; if(!isset($fields[$field]['title'])) $fields[$field]['title'] = zget($this->lang->product, $field); } /* 设置下拉菜单内容。 */ if(isset($fields['groups'])) $fields['groups']['options'] = $this->loadModel('group')->getPairs(); if(isset($fields['program'])) $fields['program']['options'] = $this->loadModel('program')->getTopPairs('noclosed'); if(isset($fields['line'])) $fields['line']['options'] = $this->product->getLinePairs($programID, true); if($this->config->edition != 'open' && isset($fields['workflowGroup'])) { $groupPairs = $this->loadModel('workflowGroup')->getPairs('product', 'scrum', 1, 'normal', '0'); $fields['workflowGroup']['options'] = $this->workflowGroup->appendBuildinLabel($groupPairs); } return $fields; } /** * 获取创建产品页面的表单配置。 * Get form fields for create. * * @param int $programID * @param string $extra * @access protected * @return array */ protected function getFormFields4Create($programID = 0, $extra = '') { $extra = str_replace(array(',', ' '), array('&', ''), $extra); parse_str($extra, $output); $fields = $this->setSelectFormOptions($programID, $this->config->product->form->create); $fields['program']['default'] = $programID ? $programID : ''; $fields['PO']['default'] = $this->app->user->account; /* Set required. */ foreach($fields as $field => $attr) { if(!empty($output[$field])) $fields[$field]['default'] = $output[$field]; if(strpos(",{$this->config->product->create->requiredFields},", ",$field,") !== false) $fields[$field]['required'] = true; } return $fields; } /** * 获取编辑产品页面的表单配置。 * Get form fields for create. * * @param object $product * @access private * @return array */ protected function getFormFields4Edit($product) { /* Init fields. */ $programID = (int)$product->program; $fields = $this->setSelectFormOptions($programID, $this->config->product->form->edit); $fields['changeProjects'] = array('type' => 'string', 'control' => 'hidden', 'required' => false, 'default' => ''); /* Check program priv, and append to program list that is not exist product's program. */ $hasPrivPrograms = $this->app->user->view->programs; if($programID and strpos(",{$hasPrivPrograms},", ",{$programID},") === false) $fields['program']['control'] = 'hidden'; if(isset($fields['program']) and !isset($fields['program']['options'][$programID]) and $programID) { $program = $this->loadModel('program')->getByID($programID); $fields['program']['options'] += array($programID => $program->name); } /* Set default value by product. */ foreach($fields as $field => $attr) { if(isset($product->{$field})) $fields[$field]['default'] = $product->{$field}; if(strpos(",{$this->config->product->edit->requiredFields},", ",$field,") !== false) $fields[$field]['required'] = true; } return $fields; } /** * 获取关闭产品页面的表单配置。 * Get form fields for close product page. * * @access private * @return array */ protected function getFormFields4Close() { /* Init fields. */ $fields = $this->config->product->form->close; $fields['comment'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '', 'width' => 'full'); return $fields; } /** * 获取激活产品页面的表单配置。 * Get form fields for activate product page. * * @access private * @return array */ protected function getFormFields4Activate() { /* Init fields. */ $fields = $this->config->product->form->activate; $fields['comment'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '', 'width' => 'full'); return $fields; } /** * Get product lines and product lines of program. * * @param array $programIdList * @access protected * @return array */ protected function getProductLines($programIdList = array()) { /* Get all product lines. */ $productLines = $this->product->getLines($programIdList); /* Collect product lines of program lines. */ $linePairs = array(); foreach($programIdList as $programID) $linePairs[$programID] = array(); foreach($productLines as $line) $linePairs[$line->root][$line->id] = $line->name; return array($productLines, $linePairs); } /** * 获取产品导出字段。 * Get export fields. * * @access protected * @return array */ protected function getExportFields() { $fieldList = $this->loadModel('datatable') ->getSetting('product', 'all'); $extendFieldList = $this->product->getFlowExtendFields(); foreach($extendFieldList as $field => $name) { if(strpos(strtolower($fieldName), 'epic') !== false && !$this->config->enableER) continue; if(strpos(strtolower($fieldName), 'requirement') !== false && !$this->config->URAndSR) continue; $fieldName = trim($fieldName); $fieldPairs[$fieldName] = zget($productLang, $fieldName); $extCol = $config->product->dtable->extendField; $extCol['name'] = $field; $extCol['title'] = $name; $fieldList[$field] = $extCol; } $fieldPairs = array(); foreach($fieldList as $fieldKey => $field) { if(isset($field['headerGroup'])) $field['title'] = $field['headerGroup'] . ' - ' . $field['title']; $fieldPairs[$fieldKey] = $field['title']; } if($this->config->systemMode == 'light') unset($fieldPairs['productLine'], $fieldPairs['program']); return $fieldPairs; } /** * 获取导出产品数据。 * Get export product data. * * @param int $programID * @param string $browseType * @param string $orderBy * @param int $param * @param object $pager * @access protected * @return array */ protected function getExportData($programID, $browseType, $orderBy, $param = 0, $pager = null) { $users = $this->user->getPairs('noletter'); $products = strtolower($browseType) == 'bysearch' ? $this->product->getListBySearch((int)$param) : $this->product->getList($programID, $browseType); $productStats = $this->product->getStats(array_keys($products), $orderBy, $pager, 'story', $programID); $data = array(); foreach($productStats as $product) $data[] = $this->product->formatDataForList($product, $users); return $data; } /** * 根据产品和已授权项目集,获取关联产品的未授权项目集。 * Get unauthorized programs by products and authorized programs. * * @param array $products * @param array $authPrograms * @access private * @return array */ private function getUnauthProgramsOfProducts($products, $authPrograms) { $unauthPrograms = array(); $programIdList = array(); foreach($products as $product) { if($product->program and !isset($authPrograms[$product->program])) $programIdList[$product->program] = $product->program; } if($programIdList) $unauthPrograms = $this->program->getPairsByList($programIdList); return $unauthPrograms; } /** * 获取在ajaxGetDropMenu方法中使用的产品。 * Get products for ajaxGetDropMenu method. * * @param string $shadow 0|all * @param string $module * @access protected * @return array */ protected function getProducts4DropMenu($shadow = '0', $module = '') { if($this->config->edition != 'open') { $flow = $this->loadModel('workflow')->getByModule($module); if($flow && $flow->belong == 'product') return $this->product->getList(0, 'all', 0, 0, $shadow); } if($this->app->tab == 'project') return $this->product->getProducts($this->session->project); if($this->app->tab == 'feedback') return $this->loadModel('feedback')->getGrantProducts(false, false, 'all'); return $this->product->getList(0, 'all', 0, 0, $shadow); } /** * 创建完成后,做页面跳转。 * Locate after create product. * * @param int $productID * @param int $programID * @access private * @return array */ private function getCreatedLocate($productID, $programID) { $tab = $this->app->tab; $moduleName = $tab == 'program' ? 'program' : $this->moduleName; $methodName = $tab == 'program' ? 'product' : 'browse'; $param = $tab == 'program' ? "programID=$programID" : "productID=$productID"; $location = isInModal() ? true : $this->createLink($moduleName, $methodName, $param); if($tab == 'doc') $location = $this->createLink('doc', 'productSpace', "objectID=$productID"); return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $location, 'closeModal' => true); } /** * 编辑完成后,做页面跳转。 * Locate after edit product. * * @param int $productID * @param int $programID * @access private * @return array */ private function getEditedLocate($productID, $programID) { $moduleName = $programID ? 'program' : 'product'; $methodName = $programID ? 'product' : 'view'; $param = $programID ? "programID=$programID" : "product=$productID"; $location = $this->createLink($moduleName, $methodName, $param); if(!$programID) $this->session->set('productList', $this->createLink('product', 'browse', $param), 'product'); return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $location); } /** * 构建批量编辑产品页面数据。 * Build form for batch edit page * * @param int $programID * @param array $productIdList * @access protected * @return void */ protected function buildBatchEditForm($programID, $productIdList) { $products = $this->product->getByIdList($productIdList); /* 获取项目集和产品线,并将项目集分成授权和非授权。以便view的下拉菜单使用对应的数据。 */ $authPrograms = array(); $unauthPrograms = array(); $lines = array(); if(in_array($this->config->systemMode, array('ALM', 'PLM'))) { $authPrograms = $this->loadModel('program')->getTopPairs(); $unauthPrograms = $this->getUnauthProgramsOfProducts($products, $authPrograms); /* Get product lines by programs.*/ $programIdList = array_merge(array(0 => ''), array_keys($authPrograms), array_keys($unauthPrograms)); list(, $lines) = $this->getProductLines($programIdList); } /* 给view层赋值。 */ $this->view->title = $this->lang->product->batchEdit; $this->view->lines = $lines; $this->view->products = $products; $this->view->fields = $this->setSelectFormOptions(0, $this->config->product->form->batchEdit); $this->view->programID = $programID; $this->view->authPrograms = $authPrograms; $this->view->unauthPrograms = $unauthPrograms; unset($this->lang->product->typeList['']); $this->display(); } /** * 构建创建产品的数据。 * Build product data for create. * * @param int $workflowGroup * @access protected * @return object */ protected function buildProductForCreate($workflowGroup = 0) { $productData = form::data($this->config->product->form->create, 0, $workflowGroup) ->setIF($this->config->systemMode == 'light', 'program', (int)zget($this->config->global, 'defaultProgram', 0)) ->setIF($this->post->acl == 'open', 'whitelist', '') ->setDefault('vision', $this->config->vision) ->get(); return $this->loadModel('file')->processImgURL($productData, $this->config->product->editor->create['id'], $this->post->uid); } /** * 构建编辑产品的数据。 * Build product data for edit. * * @param int $productID * @param int $workflowGroup * @access protected * @return object */ protected function buildProductForEdit($productID, $workflowGroup = 0) { $productData = form::data($this->config->product->form->edit, $productID, $workflowGroup) ->setIF($this->post->acl == 'open', 'whitelist', '') ->get(); return $this->loadModel('file')->processImgURL($productData, $this->config->product->editor->edit['id'], $this->post->uid); } /** * 构建激活产品数据。 * Build product data for activate. * * @param int $productID * @access protected * @return object */ protected function buildProductForActivate($productID) { $productData = form::data($this->config->product->form->activate, $productID) ->setIF($this->config->vision == 'or', 'status', 'normal') ->get(); return $this->loadModel('file')->processImgURL($productData, $this->config->product->editor->activate['id'], $this->post->uid); } /** * 构建关闭产品数据。 * Build product data for close. * * @param int $productID * @access protected * @return object */ protected function buildProductForClose($productID) { $productData = form::data($this->config->product->form->close, $productID)->get(); return $this->loadModel('file')->processImgURL($productData, $this->config->product->editor->close['id'], $this->post->uid); } /** * 预处理产品线数据。 * Prepare manage line extras. * * @param form $form * @access protected * @return array|false */ protected function prepareManageLineExtras($form) { $data = $form->get(); /* 拼装产品线列表。列表项目集编号为键,该项目集下的产品线名称列表为值。 */ /* Build product line list. The program id as key, line name list as value. */ $lines = array(); foreach($data->modules as $id => $name) { if(empty($name)) continue; if(!helper::hasFeature('program')) { $lines[0][$id] = $name; continue; } $programID = $data->programs[$id]; if(!isset($lines[$programID])) $lines[$programID] = array(); if(in_array($name, $lines[$programID])) { dao::$errors["modules[$id]"] = sprintf($this->lang->product->nameIsDuplicate, $name); return false; } $lines[$programID][$id] = $name; } return $lines; } /** * 成功插入产品数据后,其他的额外操作。 * Process after create product. * * @param int $productID * @param int $programID * @access protected * @return array */ protected function responseAfterCreate($productID, $programID) { $message = $this->executeHooks($productID); if($message) $this->lang->saveSuccess = $message; /* 移动到control。*/ if($this->viewType == 'json') return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $productID); return $this->getCreatedLocate($productID, $programID); } /** * 成功更新产品数据后,其他的额外操作。 * Process after edit product. * * @param int $productID * @param int $programID * @access protected * @return array */ protected function responseAfterEdit($productID, $programID) { $message = $this->executeHooks($productID); if($message) $this->lang->saveSuccess = $message; return $this->getEditedLocate($productID, $programID); } /** * 成功批量更新产品数据后,其他的额外操作。 * Response after batch edit products. * * @param int $programID * @access protected * @return array */ protected function responseAfterBatchEdit($programID) { /* Get location. */ $location = $this->createLink('program', 'product', "programID=$programID"); if(empty($programID)) $location = $this->createLink('program', 'productView'); if($this->app->tab == 'product') $location = $this->createLink('product', 'all'); return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $location); } /** * 获取研发需求列表页面关联的产品信息。 * Get the product of the browse page. * * @param int $productID * @access protected * @return object|null */ protected function getBrowseProduct($productID) { $product = $this->product->getById($productID); if(empty($product)) return null; /* If product does not exist in $this->products list, then attach it to the list. */ if(!isset($this->products[$product->id])) $this->products[$product->id] = $product->name; return $product; } /** * 获取产品分支ID。 * Get branchID. * * @param object|null $product * @param string $branch * @access protected * @return string */ protected function getBranchID($product, $branch) { if(empty($product) || $product->type == 'normal') return 'all'; /* 获取分支和分支ID。*/ $branchPairs = $this->loadModel('branch')->getPairs($product->id, 'all'); return ($this->cookie->preBranch !== '' and $branch === '' and isset($branchPairs[$this->cookie->preBranch])) ? (string)$this->cookie->preBranch : $branch; } /** * 设置导航菜单。 * Set navigation menu. * * @param int $projectID * @param int $productID * @param string $branch * @param string $storyType * @access protected * @return void */ protected function setMenu4Browse($projectID, $productID, $branch, $storyType) { if($this->app->tab == 'project') { $this->loadModel('project')->setMenu($projectID); return; } $this->product->setMenu($productID, $branch, $storyType); } /** * browse方法保存并修改cookie数据。 * Save and modify cookies for browse function. * * @param int $productID * @param string $branch * @param int $param * @param string $storyType * @param string $orderBy * @access protected * @return void * @param string $browseType */ protected function saveAndModifyCookie4Browse($productID, $branch, $param, $browseType, $orderBy) { /* Set module ID of story. */ if($this->cookie->preProductID != $productID or $this->cookie->preBranch != $branch or $browseType == 'bybranch') { $_COOKIE['storyModule'] = 0; helper::setcookie('storyModule', '0', 0); } if($browseType == 'bymodule' or $browseType == '') { helper::setcookie('storyModule', (string)$param, 0); /* The module ID from project app. */ if($this->app->tab == 'project') helper::setcookie('storyModuleParam', (string)$param, 0); /* Re-init the story branch. */ $_COOKIE['storyBranch'] = 'all'; helper::setcookie('storyBranch', 'all', 0); /* For rendering module tree. */ if($browseType == '') helper::setcookie('treeBranch', $branch, 0); } if($browseType == 'bybranch') helper::setcookie('storyBranch', $branch, 0); /* Save sort order of product stories list. */ helper::setcookie('productStoryOrder', $orderBy, 0); /* Set product ID and branch of the pre visited product. */ helper::setcookie('preProductID', (string)$productID); helper::setcookie('preBranch', $branch, 0); } /** * 获取模块ID。 * Get module ID. * * @param int $param * @param string $browseType * @access protected * @return int */ protected function getModuleId($param, $browseType) { if($browseType == 'bymodule') return $param; $cookieModule = $this->app->tab == 'project' ? $this->cookie->storyModuleParam : $this->cookie->storyModule; if($browseType != 'bysearch' && $browseType != 'bybranch' && $cookieModule) return (int)$cookieModule; return 0; } /** * 获取模块的树形数据结构。 * Get the tree structure of modules. * * @param int $projectID * @param int $productID * @param string $branch * @param int $param * @param string $storyType * @param string $browseType * @access protected * @return array|string */ protected function getModuleTree($projectID, $productID, &$branch, $param, $storyType, $browseType) { if(common::isTutorialMode()) return array(); /* Set moduleTree. */ $createModuleLink = 'createStoryLink'; if($storyType == 'requirement') $createModuleLink = 'createRequirementLink'; if($storyType == 'epic') $createModuleLink = 'createEpicLink'; if($browseType == '') { $browseType = 'unclosed'; $branch = is_bool($this->cookie->treeBranch) && empty($this->cookie->treeBranch) ? 'all' : $this->cookie->treeBranch; } /* If invoked by projectstory module and not choose product, then get the modules of project story. */ if(!isset($this->tree)) $this->loadModel('tree'); $project = $projectID ? $this->loadModel('project')->getByID($projectID) : null; if(!empty($projectID) && !empty($project->hasProduct) && $this->app->rawModule == 'projectstory') { return $this->tree->getProjectStoryTreeMenu($projectID, 0, array('treeModel', $createModuleLink), array('storyType' => $storyType)); } /* Pre generate parameters. */ $userFunc = array('treeModel', $createModuleLink); $extra = array('projectID' => $projectID, 'productID' => $productID); return $this->tree->getTreeMenu($productID, 'story', 0, $userFunc, $extra, $branch, "¶m=$param&storyType=$storyType"); } /** * 获取项目下的产品列表。 * Get products of the project. * * @param int $projectID * @param bool $isProjectStory * @access protected * @return array */ protected function getProjectProductList($projectID, $isProjectStory) { if($isProjectStory) return $this->product->getProducts($projectID); return array(); } /** * 获取产品计划。 * Get product plans of the project. * * @param array $projectProducts * @param int $projectID * @param string $storyType * @param bool $isProjectStory * @access protected * @return array */ protected function getProductPlans($projectProducts, $projectID, $storyType, $isProjectStory) { if($isProjectStory && $storyType == 'story') return $this->loadModel('execution')->getPlans(array_keys($projectProducts), 'skipParent,unexpired,noclosed', $projectID); return array(); } /** * 获取需求列表及分页对象。 * Get stories. * * @param int $projectID * @param int $productID * @param string $branchID * @param int $moduleID * @param int $param * @param string $storyType requirement|story * @param string $browseType * @param string $orderBy * @param object $pager * @access protected * @return array */ public function getStories($projectID, $productID, $branchID = '', $moduleID = 0, $param = 0, $storyType = 'all', $browseType = 'allstory', $orderBy = 'id_desc', $pager = null) { /* Append id for second sort. */ $sort = common::appendOrder($orderBy); if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort); $isProjectStory = $this->app->rawModule == 'projectstory'; if(!isset($this->story)) $this->loadModel('story'); /* Get stories. */ if($isProjectStory) { $this->products = $this->product->getProducts($projectID, 'all', '', false); if($browseType == 'bybranch') $param = $branchID; $stories = $this->story->getExecutionStories($projectID, $productID, $sort, $browseType, (string)$param, 'all', '', $pager); } else { $queryID = ($browseType == 'bysearch') ? $param : 0; $stories = $this->product->getStories($productID, $branchID, $browseType, (int)$queryID, $moduleID, $storyType, $sort, $pager); } if(!empty($stories)) $stories = $this->story->mergeReviewer($stories); return $stories; } /** * 根据需求类型获取需求。 * Get stories by story type. * * @param int $productID * @param string $branch * @param string $storyType * @param string $orderBy * @param object $pager * @access public * @return array */ public function getStoriesByStoryType($productID, $branch = '', $storyType = 'all', $orderBy = 'id_desc', $pager = null) { /* Append id for second sort. */ $sort = common::appendOrder($orderBy); if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort); $parentIdList = array(); if($storyType == 'requirement,story' && !empty($this->config->enableER)) $parentIdList = $this->dao->select('id')->from(TABLE_STORY)->where('type')->eq('epic')->andWhere('isParent')->eq(1)->fetchPairs('id', 'id'); if($storyType == 'story' && (!empty($this->config->URAndSR) || $this->config->edition == 'ipd')) $parentIdList = $this->dao->select('id')->from(TABLE_STORY)->where('type')->eq('requirement')->andWhere('isParent')->eq(1)->fetchPairs('id', 'id'); $productQuery = $this->loadModel('story')->buildProductsCondition($productID, $branch); $stories = $this->dao->select("*,plan,path")->from(TABLE_STORY) ->where('deleted')->eq(0) ->andWhere($productQuery) ->andWhere('parent', true)->eq(0) ->beginIF($parentIdList)->orWhere("parent")->in($parentIdList)->fi() ->markRight(1) ->andWhere("FIND_IN_SET('{$this->config->vision}', vision)") ->beginIF($storyType != 'all')->andWhere('type')->in($storyType)->fi() ->beginIF(empty($this->config->enableER))->andWhere('type')->ne('epic')->fi() ->beginIF(empty($this->config->URAndSR) && $this->config->edition != 'ipd')->andWhere('type')->ne('requirement')->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); return $stories; } /** * 获取分支的配置数据。 * Get branch options. * * @param array $projectProducts * @param int $projectID * @access protected * @return array */ protected function getBranchOptions($projectProducts, $projectID) { $this->loadModel('branch'); $branchOptions = array(); foreach($projectProducts as $product) { if(!$product || $product->type == 'normal') continue; $branches = $this->branch->getList($product->id, $projectID, 'all'); foreach($branches as $branchInfo) $branchOptions[$product->id][$branchInfo->id] = $branchInfo->name; } return $branchOptions; } /** * 获取分支和分支标签的显示项。 * Get options of branch and branch tag. * * @param int $productID * @param object|null $product * @param bool $isProjectStory * @access protected * @return array[] * @param int $projectID */ protected function getBranchAndTagOption($projectID, $product, $isProjectStory) { $branchOption = array(); $branchTagOption = array(); if(empty($product) and $isProjectStory) return [$branchOption, $branchTagOption]; if($product and $product->type != 'normal') { $branches = $this->loadModel('branch')->getList($product->id, $projectID, 'all'); foreach($branches as $branchInfo) { $branchOption[$branchInfo->id] = $branchInfo->name; $branchTagOption[$branchInfo->id] = $branchInfo->name . ($branchInfo->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : ''); } } return [$branchOption, $branchTagOption]; } /** * 获取产品看板。 * Get kanban list for product. * * @param string $browseType * @access protected * @return array */ protected function getKanbanList($browseType = 'my') { $kanbanList = array(); list($productList, $planList, $projectList, $projectProduct, $latestExecutions, $releaseList) = $this->product->getStats4Kanban($browseType); $programPairs = array(0 => helper::hasFeature('program') ? $this->lang->project->noProgram : '') + $this->loadModel('program')->getPairs(true, 'order_asc'); $productList = $this->getProductList4Kanban($productList, $planList, $projectList, $releaseList, $projectProduct); foreach($productList as $programID => $productList) { $region = array(); $heading = new stdclass(); $heading->title = zget($programPairs, $programID, $programID); $region['key'] = $programID; $region['id'] = $programID; $region['heading'] = $heading; $lanes = array(); $items = array(); $columnCards = array(); foreach($productList as $laneKey => $laneData) { $lanes[] = array('name' => $laneKey, 'title' => $laneData->name); $columns = array(); foreach(array('unexpiredPlan', 'doing', 'doingProject', 'doingExecution', 'normalRelease') as $columnKey) { $column = array('name' => $columnKey, 'title' => $this->lang->product->{$columnKey}); if($columnKey == 'doingExecution' || $columnKey == 'doingProject') $column['parentName'] = 'doing'; $columns[] = $column; $cardList = !empty($laneData->{$columnKey}) ? $laneData->{$columnKey} : array(); foreach($cardList as $card) { $items[$laneKey][$columnKey][] = array('id' => $card->id, 'name' => $card->id, 'title' => isset($card->name) ? $card->name : $card->title, 'status' => isset($card->status) ? $card->status : '', 'cardType' => $columnKey, 'delay' => !empty($card->delay) ? $card->delay : 0, 'progress' => isset($card->progress) ? $card->progress : 0, 'marker' => isset($card->marker) ? $card->marker : 0); if(!isset($columnCards[$columnKey])) $columnCards[$columnKey] = 0; $columnCards[$columnKey] ++; if($columnKey == 'doingProject') { if(!empty($latestExecutions[$card->id])) { $execution = $latestExecutions[$card->id]; $items[$laneKey]['doingExecution'][] = array('id' => $execution->id, 'name' => $execution->id, 'title' => $execution->name, 'status' => $execution->status, 'cardType' => 'doingExecution', 'delay' => !empty($execution->delay) ? $execution->delay : 0, 'progress' => $execution->progress); if(!isset($columnCards['doingExecution'])) $columnCards['doingExecution'] = 0; $columnCards['doingExecution'] ++; } } } } } foreach($columns as $key => $column) $columns[$key]['cards'] = !empty($columnCards[$column['name']]) ? $columnCards[$column['name']] : 0; $groupData['key'] = $programID; $groupData['data']['lanes'] = $lanes; $groupData['data']['cols'] = $columns; $groupData['data']['items'] = $items; $region['items'] = array($groupData); $kanbanList[] = $region; } return $kanbanList; } /** * 保存需求页面session变量。 * Save session variables for browse page. * * @param object|null $product * @param string $browseType * @access protected * @return void */ protected function saveSession4Browse($product, $browseType) { $uri = $this->app->getURI(true); /* For setMenu. */ if($this->app->tab == 'project') { $this->session->set('storyList', $uri, 'project'); } else { $this->session->set('productList', $uri, 'product'); $this->session->set('storyList', $uri, 'product'); } if(!empty($product)) $this->session->set('currentProductType', $product->type); /* Save browse type into session for buildSearchForm. */ if($browseType != 'bymodule' && $browseType != 'bybranch') $this->session->set('storyBrowseType', $browseType); } /** * 构建搜索表单。 * Build search form. * * @param object|bool $project * @param int $projectID * @param int $productID * @param string $branch * @param int $param * @param string $storyType * @param string $browseType * @param bool $isProjectStory * @access protected * @return void * @param string $from * @param int $blockID */ protected function buildSearchFormForBrowse($project, $projectID, &$productID, $branch, $param, $storyType, $browseType, $isProjectStory, $from, $blockID) { if($isProjectStory && !$productID && !empty($this->products)) $productID = (int)key($this->products); // If toggle a project by the #swapper component on the story page of the projectstory module, the $productID may be empty. Make sure it has value. if($this->config->edition == 'ipd' && $storyType == 'story') unset($this->config->product->search['fields']['roadmap']); if(isset($project->hasProduct) && empty($project->hasProduct)) { /* The none-product project don't need display the product in the search form. */ unset($this->config->product->search['fields']['product']); unset($this->config->product->search['params']['product']); /* The none-product and none-scrum project don't need display the plan in the search form. */ if($project->model != 'scrum') { unset($this->config->product->search['fields']['plan']); unset($this->config->product->search['params']['plan']); } } if($this->config->edition == 'ipd' && $storyType == 'requirement') { $this->config->product->search['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID); } /* Build search form. */ $params = $isProjectStory ? "projectID=$projectID&productID=0" : "productID=$productID"; $actionURL = $this->createLink($this->app->rawModule, $this->app->rawMethod, $params . "&branch=$branch&browseType=bySearch&queryID=myQueryID&storyType=$storyType&orderBy=&recTotal=0&recPerPage=20&pageID=1&projectID=$projectID&from=$from&blockID=$blockID"); $this->config->product->search['onMenuBar'] = 'yes'; $this->config->product->search['module'] = $storyType; if($this->app->rawModule != 'product') $this->config->product->search['module'] = $this->app->rawModule; $queryID = ($browseType == 'bysearch') ? $param : 0; $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $storyType, $branch, $projectID); } /** * 构建矩阵搜索表单。 * Build search form for track. * * @param int $productID * @param string $branch * @param int $projectID * @param string $browseType * @param int $param * @param string $storyType * @access protected * @return void */ protected function buildSearchFormForTrack(&$productID, $branch, $projectID, $browseType, $param, $storyType) { if($this->config->edition == 'ipd' && $storyType == 'story') unset($this->config->product->search['fields']['roadmap']); if($this->config->edition == 'ipd' && $storyType == 'requirement') { $this->config->product->search['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID); } /* Build search form. */ $actionURL = $this->createLink('product', 'track', "productID={$productID}&branch={$branch}&projectID={$projectID}&browseType=bySearch¶m=myQueryID&storyType=$storyType"); if($this->app->rawModule == 'projectstory') $actionURL = $this->createLink('projectstory', 'track', "projectID={$projectID}&productID={$productID}&branch={$branch}&browseType=bySearch¶m=myQueryID&storyType=$storyType"); $this->config->product->search['module'] = $this->app->rawModule . 'Track'; $queryID = ($browseType == 'bysearch') ? (int)$param : 0; $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $storyType, $branch, $projectID); } /** * 获取需求的ID列表。 * Get ID list of stories. * * @param array $stories * @access protected * @return array */ protected function getStoryIdList($stories) { $storyIdList = array(); foreach($stories as $story) { $storyIdList[$story->id] = $story->id; if(!empty($story->children)) { foreach($story->children as $child) $storyIdList[$child->id] = $child->id; } } return $storyIdList; } /** * 路线图页面保存session数据。 * Save session variables data. * * @access protected * @return void */ protected function saveSession4Roadmap() { $this->session->set('releaseList', $this->app->getURI(true), 'product'); $this->session->set('productPlanList', $this->app->getURI(true), 'product'); } /** * 回复产品不存在提示消息。 * Response product not found message. * * @access protected * @return void */ protected function responseNotFound4View() { if(defined('RUN_MODE') && RUN_MODE == 'api') { $this->send(array('status' => 'fail', 'code' => 404, 'message' => '404 Not found')); return; } $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->notFound, 'locate' => $this->createLink('product', 'all')))); return; } /** * 将返回链接保存到session中。 * Save back uri in session. * * @access protected * @return void */ protected function saveBackUriSessionForDynamic() { $uri = $this->app->getURI(true); $this->session->set('productList', $uri, 'product'); $this->session->set('productPlanList', $uri, 'product'); $this->session->set('releaseList', $uri, 'product'); $this->session->set('storyList', $uri, 'product'); $this->session->set('projectList', $uri, 'project'); $this->session->set('executionList', $uri, 'execution'); $this->session->set('taskList', $uri, 'execution'); $this->session->set('buildList', $uri, 'execution'); $this->session->set('bugList', $uri, 'qa'); $this->session->set('caseList', $uri, 'qa'); $this->session->set('testtaskList', $uri, 'qa'); } /** * 获取操作记录。 * Get actions of the product. * * @param string $account * @param string $orderBy * @param int $productID * @param string $type * @param string $date * @param string $direction next|pre * @access protected * @return [array, object] */ protected function getActionsForDynamic($account, $orderBy, $productID, $type, $date, $direction) { /* Build parameters. */ $period = $type == 'account' ? 'all' : $type; $date = empty($date) ? '' : date('Y-m-d', (int)$date); $actions = $this->loadModel('action')->getDynamicByProduct($productID, $account, $period, $orderBy, 50, $date, $direction); $dateGroups = $this->action->buildDateGroup($actions, $direction); return array($actions, $dateGroups); } /** * 获取产品仪表盘操作记录。 * Get actions of the product for dashboard. * * @param int $productID * @access protected * @return array */ protected function getActions4Dashboard($productID) { /* Load pager. */ $this->app->loadClass('pager', true); $pager = new pager(0, 30, 1); return $this->loadModel('action')->getDynamic('all', 'all', 'date_desc', $pager, $productID); } /** * 保存产品仪表盘的返回链接session。 * Save back uri to session. * * @access protected * @return void */ protected function saveBackUriSession4Dashboard() { $uri = $this->app->getURI(true); $this->session->set('productPlanList', $uri, 'product'); $this->session->set('releaseList', $uri, 'product'); } /** * 获取产品看板页面的产品列表。 * Get product list for Kanban. * * @param array $productList * @param array $planList * @param array $projectList * @param array $releaseList * @param array $projectProduct * @access protected * @return array */ protected function getProductList4Kanban($productList, $planList, $projectList, $releaseList, $projectProduct) { $kanbanList = array(); foreach($productList as $productID => $product) { if($product->status != 'normal') continue; $projects = array(); if(isset($projectProduct[$productID])) { foreach($projectProduct[$productID] as $projectID => $project) { if(isset($projectList[$projectID])) $projects[$projectID] = $projectList[$projectID]; } } $product->unexpiredPlan = zget($planList, $productID, array()); $product->normalRelease = zget($releaseList, $productID, array()); $product->doingProject = $projects; if(!helper::hasFeature('program')) $product->program = 0; $kanbanList[$product->program][$productID] = $product; } return $kanbanList; } /** * 获取空的小时对象数据。 * Get empty hour data object. * * @access protected * @return object */ protected function getEmptyHour() { $hour = new stdclass(); $hour->totalEstimate = 0; $hour->totalConsumed = 0; $hour->totalLeft = 0; $hour->progress = 0; return $hour; } /** * 设置需求列表数据。 * Set the data of requirement list. * * @param array $stories * @param string $browseType * @param string $storyType * @param bool $isProjectStory * @param object|null $product * @param object|null $project * @param string $branch * @param string $branchID * @param string $from * @access protected * @return void */ protected function assignBrowseData($stories, $browseType, $storyType, $isProjectStory, $product, $project, $branch, $branchID, $from = '') { $productID = $product ? (int)$product->id : 0; $projectID = $project ? (int)$project->id : 0; $productName = ($isProjectStory && empty($product)) ? $this->lang->product->all : $this->products[$productID]; $storyIdList = $this->getStoryIdList($stories); $projectProducts = $this->getProjectProductList($projectID, $isProjectStory); list($branchOpt, $branchTagOpt) = $this->getBranchAndTagOption($projectID, $product, $isProjectStory); $gradeList = $this->loadModel('story')->getGradeList(''); $gradeGroup = array(); foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; /* Set show module by config. */ $showModule = empty($this->config->product->browse->showModule) ? 0 : $this->config->product->browse->showModule; if($isProjectStory) $showModule = empty($this->config->projectstory->story->showModule) ? 0 : $this->config->projectstory->story->showModule; $this->view->title = $productName . $this->lang->hyphen . ($storyType === 'story' ? $this->lang->product->browse : $this->lang->product->requirement); $this->view->productID = $productID; $this->view->product = $product; $this->view->projectID = $projectID; $this->view->project = $project; $this->view->stories = $stories; $this->view->gradeGroup = $gradeGroup; $this->view->gradeMenu = $this->loadModel('story')->getGradeMenu($storyType, $project); $this->view->gradePairs = $this->story->getGradePairs($storyType); $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); $this->view->storyType = $storyType; $this->view->browseType = $browseType; $this->view->isProjectStory = $isProjectStory; $this->view->branch = $branch; $this->view->branchID = $branchID; $this->view->modulePairs = !empty($showModule) ? $this->loadModel('tree')->getModulePairs($productID, 'story', $showModule) : array(); $this->view->branchOptions = (empty($product) && $isProjectStory) ? $this->getBranchOptions($projectProducts, $projectID) : array($productID => $branchOpt); $this->view->branchTagOption = $branchTagOpt; $this->view->projectProducts = $projectProducts; $module = ($this->app->tab == 'product' || $from == 'doc' || $from == 'ai') ? $storyType : $this->app->tab; $this->view->showGrades = !empty($this->config->{$module}->showGrades) ? $this->config->{$module}->showGrades : $this->story->getDefaultShowGrades($this->view->gradeMenu); $storyType = $isProjectStory ? 'all' : $storyType; $this->view->summary = $this->product->summary($stories, $storyType); $this->view->plans = $this->loadModel('productplan')->getPairs($productID, isset($projectProducts[$productID]) ? array(BRANCH_MAIN) + $projectProducts[$productID]->branches : (($branch === 'all' || empty($branch)) ? '' : $branch), 'unexpired,noclosed', true); $this->view->users = $this->loadModel('user')->getPairs('noletter|pofirst|nodeleted'); $this->view->modules = $this->loadModel('tree')->getOptionMenu($productID, 'story', 0, $branchID); $this->view->storyTasks = $this->loadModel('task')->getStoryTaskCounts($storyIdList); $this->view->storyBugs = $this->loadModel('bug')->getStoryBugCounts($storyIdList); $this->view->storyCases = $this->loadModel('testcase')->getStoryCaseCounts($storyIdList); $this->view->reports = in_array($this->config->edition, array('max', 'ipd')) ? $this->loadModel('researchreport')->getPairs() : array(); $this->view->roadmaps = ($this->config->edition == 'ipd' && $storyType != 'story') ? array(0 => '') + $this->loadModel('roadmap')->getPairs($productID) : array(); $this->view->noclosedRoadmaps = ($this->config->edition == 'ipd' && $storyType != 'story') ? array(0 => '') + $this->loadModel('roadmap')->getPairs($productID, '', 'noclosed') : array(); $this->view->hasFrozenStories = $isProjectStory ? $this->loadModel('project')->hasFrozenObject($projectID, 'SRS') : false; $this->display(); } /** * 处理项目列表展示数据。 * Process project list display data. * * @param array $projectList * @access protected * @return array */ protected function processProjectListData($projectList) { $userPairs = $this->loadModel('user')->getPairs(); $userList = $this->user->getListByAccounts(array_keys($userPairs), 'account'); $this->loadModel('story'); $this->loadModel('execution'); $this->loadModel('project'); foreach($projectList as $project) { $project = $this->project->formatDataForList($project, $userList); $projectStories = $this->story->getExecutionStories($project->id); $project->storyCount = count($projectStories); $project->storyPoints = 0; foreach($projectStories as $story) $project->storyPoints += $story->estimate; $project->storyPoints .= ' ' . $this->config->hourUnit; $executions = $this->execution->getStatData($project->id, 'all', 0, 0, false, 'skipParent'); $project->executionCount = count($executions); $project->from = 'project'; $project->actions = $this->project->buildActionList($project); } return array_values($projectList); } /** * 为跟踪矩阵设置自定义列。 * Get custom fields for track. * * @param string $storyType epic|requirement|story * @access protected * @return array */ public function getCustomFieldsForTrack($storyType) { $listFields = array(); $listFields['requirement'] = $this->lang->URCommon; $listFields['story'] = $this->lang->SRCommon; $listFields['project'] = $this->lang->story->project; $listFields['execution'] = $this->lang->story->execution; $listFields['design'] = $this->lang->story->design; $listFields['commit'] = $this->lang->story->repoCommit; $listFields['task'] = $this->lang->story->tasks; $listFields['bug'] = $this->lang->story->bugs; $listFields['case'] = $this->lang->story->cases; if($storyType == 'requirement' || $storyType == 'story') unset($listFields['requirement']); if($storyType == 'story') unset($listFields['story']); $systemShowFields = $this->loadModel('setting')->getItem("owner=system&module=product§ion=trackFields&key={$storyType}"); $accountShowFields = $this->setting->getItem("owner={$this->app->user->account}&module=product§ion=trackFields&key={$storyType}"); $showFields = empty($accountShowFields) ? $systemShowFields : $accountShowFields; $showFields = empty($showFields) ? array_keys($listFields) : explode(',', $showFields); return array('list' => $listFields, 'show' => array_merge(array($storyType), $showFields)); } /** * 获取根据矩阵可用的需求类型。 * Get active storyType list for track. * * @param int $projectID * @param int $productID * @access protected * @return array */ public function getActiveStoryTypeForTrack($projectID = 0, $productID = 0) { $storyTypeList = $this->lang->story->typeList; /* Check story type by linked story's type. */ if($this->app->rawModule == 'projectstory' && $projectID) { $stories = $this->getStories($projectID, $productID); if($stories) { $havingType = array(); foreach($stories as $story) $havingType[$story->type] = $story->type; if(!isset($havingType['epic'])) unset($storyTypeList['epic']); if(!isset($havingType['epic']) && !isset($havingType['requirement'])) unset($storyTypeList['requirement']); } } return $storyTypeList; } /** * 格式化导出数据。 * Format export data. * * @param array $products * @access public * @return array */ public function formatExportData($products) { if(empty($products) || $this->config->vision == 'or') return $products; foreach($products as $product) { $product->testCaseCoverage = isset($product->testCaseCoverage) ? $product->testCaseCoverage . '%' : '0%'; $product->epicCompleteRate = isset($product->epicCompleteRate) ? $product->epicCompleteRate . '%' : '0%'; $product->requirementCompleteRate = isset($product->requirementCompleteRate) ? $product->requirementCompleteRate . '%' : '0%'; $product->storyCompleteRate = isset($product->storyCompleteRate) ? $product->storyCompleteRate . '%' : '0%'; $product->bugFixedRate = isset($product->bugFixedRate) ? $product->bugFixedRate . '%' : '0%'; } return $products; } }