Files
EasySoft-ZenTaoPMS/module/product/zen.php
T
2024-01-26 11:10:48 +08:00

1485 lines
56 KiB
PHP

<?php
declare(strict_types=1);
/**
* The zen file of product module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author chen.tao<chentao@easycorp.ltd>
* @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(int $productID, string $branch, string $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(int $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(int $productID, int $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(int $productID, string $branch, int $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(string $moduleName, string $activeMenu, int $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(string $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(string $activeMenu, int $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(string $activeMenu, int $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(string $extra): string
{
$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(int $programID, array $fields): array
{
/* 准备数据。*/
$this->loadModel('user');
$poUsers = $this->user->getPairs('nodeleted|pofirst|noclosed');
$qdUsers = $this->user->getPairs('nodeleted|qdfirst|noclosed');
$rdUsers = $this->user->getPairs('nodeleted|devfirst|noclosed');
$users = $this->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['PO'])) $fields['PO']['options'] = $poUsers;
if(isset($fields['QD'])) $fields['QD']['options'] = $qdUsers;
if(isset($fields['RD'])) $fields['RD']['options'] = $rdUsers;
if(isset($fields['groups'])) $fields['groups']['options'] = $this->loadModel('group')->getPairs();
if(isset($fields['program'])) $fields['program']['options'] = $this->loadModel('program')->getTopPairs('noclosed');
if($programID and isset($fields['line'])) $fields['line']['options'] = $this->product->getLinePairs($programID);
return $fields;
}
/**
* 获取创建产品页面的表单配置。
* Get form fields for create.
*
* @param int $programID
* @access private
* @return array
*/
protected function getFormFields4Create(int $programID = 0): array
{
$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(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(object $product): array
{
/* 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->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 config for batch edit page.
*
* @access private
* @return array
*/
private function getFormFields4BatchEdit(): array
{
/* Init fields. */
$fields = $this->setSelectFormOptions(0, $this->config->product->form->batchEdit);
/* Remove hidden fields. */
$shownFields = explode(',', $this->config->product->custom->batchEditFields);
foreach($fields as $field => $attr)
{
if(!in_array($field, $shownFields) and !$attr['required']) unset($fields[$field]);
}
return $fields;
}
/**
* 获取关闭产品页面的表单配置。
* Get form fields for close product page.
*
* @access private
* @return array
*/
protected function getFormFields4Close(): array
{
/* 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(): array
{
/* 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(array $programIdList = array()): 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 $programID => $line) $linePairs[$programID][$line->id] = $line->name;
return array($productLines, $linePairs);
}
/**
* 获取产品导出字段。
* Get export fields.
*
* @access protected
* @return array
*/
protected function getExportFields(): array
{
$productLang = $this->lang->product;
$productConfig = $this->config->product;
/* 获取字段语言项。 */
$fields = explode(',', $productConfig->list->exportFields);
$fieldPairs = array();
foreach($fields as $fieldName)
{
$fieldName = trim($fieldName);
$fieldPairs[$fieldName] = zget($productLang, $fieldName);
if($this->config->systemMode == 'light' && ($fieldName == 'line' or $fieldName == 'program')) unset($fieldPairs[$fieldName]);
}
return $fieldPairs;
}
/**
* 获取导出产品数据。
* Get export product data.
*
* @param int $programID
* @param string $status
* @param string $orderBy
* @param int $param
* @access protected
* @return array
*/
protected function getExportData(int $programID, string $status, string $orderBy, int $param = 0): array
{
$lines = $this->product->getLinePairs();
$users = $this->user->getPairs('noletter');
$products = strtolower($status) == 'bysearch' ? $this->product->getListBySearch($param) : $this->product->getList($programID, $status);
$productStats = $this->product->getStats(array_keys($products), $orderBy, null, $status);
foreach($productStats as $product)
{
$product->line = zget($lines, $product->line, '');
$product->manager = zget($users, $product->PO, '');
$product->changedStories = (int)$product->changingStories;
$product->storyCompleteRate = ($product->totalStories == 0 ? 0 : round($product->closedStories / $product->totalStories, 3) * 100) . '%';
$product->bugFixedRate = (($product->unResolved + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unResolved + $product->fixedBugs), 3) * 100) . '%';
$product->unResolvedBugs = (int)$product->unresolvedBugs;
$product->program = $product->programName;
}
return $productStats;
}
/**
* 获取导出的合并单元格信息。
* Get rowspan for export.
*
* @param array $products
* @access protected
* @return array
*/
protected function getExportRowspan(array $products): array
{
$lastRecord = array('program' => '', 'line' => '');
$colIndexs = array('program' => 0, 'line' => 0);
$rowspan = array();
foreach($products as $i => $product)
{
foreach($lastRecord as $field => $lastID)
{
if($product->{$field} !== $lastID)
{
$rowspan[$i]['rows'][$field] = 1;
$colIndexs[$field] = $i;
}
else
{
$colIndex = $colIndexs[$field];
$rowspan[$colIndex]['rows'][$field] ++;
}
$lastRecord[$field] = $product->{$field};
}
}
return $rowspan;
}
/**
* 根据产品和已授权项目集,获取关联产品的未授权项目集。
* Get unauthorized programs by products and authorized programs.
*
* @param array $products
* @param array $authPrograms
* @access private
* @return array
*/
private function getUnauthProgramsOfProducts(array $products, array $authPrograms): array
{
$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
* @access protected
* @return array
*/
protected function getProducts4DropMenu(string $shadow = '0'): array
{
if($this->app->tab == 'project') return $this->product->getProducts($this->session->project);
if($this->app->tab == 'feedback') return $this->loadModel('feedback')->getGrantProducts(false);
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(int $productID, int $programID): array
{
$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(int $productID, int $programID): array
{
$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(int $programID, array $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_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->getFormFields4BatchEdit();
$this->view->programID = $programID;
$this->view->authPrograms = $authPrograms;
$this->view->unauthPrograms = $unauthPrograms;
unset($this->lang->product->typeList['']);
$this->display();
}
/**
* 构建创建产品的数据。
* Build product data for create.
*
* @access protected
* @return object
*/
protected function buildProductForCreate(): object
{
$editorFields = array_keys(array_filter(array_map(function($config){return (!empty($config['control']) && $config['control'] == 'editor');}, $this->config->product->form->create)));
$productData = form::data($this->config->product->form->create)
->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, $editorFields, $this->post->uid);
}
/**
* 构建编辑产品的数据。
* Build product data for edit.
*
* @access protected
* @return object
*/
protected function buildProductForEdit(): object
{
$editorFields = array_keys(array_filter(array_map(function($config){return (!empty($config['control']) && $config['control'] == 'editor');}, $this->config->product->form->edit)));
$productData = form::data($this->config->product->form->edit)
->setIF($this->post->acl == 'open', 'whitelist', '')
->get();
return $this->loadModel('file')->processImgURL($productData, $editorFields, $this->post->uid);
}
/**
* 构建激活产品数据。
* Build product data for activate.
*
* @access protected
* @return object
*/
protected function buildProductForActivate(): object
{
$productData = form::data($this->config->product->form->activate)
->setIF($this->config->vision == 'or', 'wait', 'normal')
->get();
return $this->loadModel('file')->processImgURL($productData, $this->config->product->editor->activate['id'], $this->post->uid);
}
/**
* 构建关闭产品数据。
* Build product data for close.
*
* @access protected
* @return object
*/
protected function buildProductForClose(): object
{
$productData = form::data($this->config->product->form->close)->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 $form): array|false
{
$data = $form->get();
/* When there are products under the line, the program cannot be modified */
if(!$this->canChangeProgram($data)) return false;
/* 拼装产品线列表。列表项目集编号为键,该项目集下的产品线名称列表为值。 */
/* 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(in_array($this->config->systemMode, array('ALM', 'PLM')) and empty($data->programs[$id]))
{
dao::$errors["programs[{$id}]"] = $this->lang->product->programEmpty;
return false;
}
$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(int $productID, int $programID): array
{
$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(int $productID, int $programID): array
{
$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(int $programID): array
{
/* 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(int $productID): object|null
{
$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(object|null $product, string $branch): string
{
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(int $projectID, int $productID, string $branch, string $storyType): void
{
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
*/
protected function saveAndModifyCookie4Browse(int $productID, string $branch, int $param, string $browseType, string $orderBy): void
{
/* 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(int $param, string $browseType): int
{
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(int $projectID, int $productID, string &$branch, int $param, string $storyType, string $browseType): array|string
{
/* Set moduleTree. */
$createModuleLink = $storyType == 'story' ? 'createStoryLink' : 'createRequirementLink';
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');
if(!empty($projectID) && $this->app->rawModule == 'projectstory')
{
return $this->tree->getProjectStoryTreeMenu($projectID, 0, array('treeModel', $createModuleLink));
}
/* Pre generate parameters. */
$userFunc = array('treeModel', $createModuleLink);
$extra = array('projectID' => $projectID, 'productID' => $productID);
return $this->tree->getTreeMenu($productID, 'story', 0, $userFunc, $extra, $branch, "&param=$param&storyType=$storyType");
}
/**
* 获取项目下的产品列表。
* Get products of the project.
*
* @param int $projectID
* @param string $storyType
* @param bool $isProjectStory
* @access protected
* @return array
*/
protected function getProjectProductList(int $projectID, string $storyType, bool $isProjectStory): array
{
if($isProjectStory && $storyType == 'story') 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(array $projectProducts, int $projectID, string $storyType, bool $isProjectStory): array
{
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 string $moduleID
* @param int $param
* @param string $storyType requirement|story
* @param string $browseType
* @param string $orderBy
* @param object $pager
* @access protected
* @return array
*/
protected function getStories(int $projectID, int $productID, string $branchID, int $moduleID, int $param, string $storyType, string $browseType, string $orderBy, object $pager): array
{
/* 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, $storyType, '', $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 branch options.
*
* @param array $projectProducts
* @param int $projectID
* @access protected
* @return array
*/
protected function getBranchOptions(array $projectProducts, int $projectID): array
{
$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[]
*/
protected function getBranchAndTagOption(int $projectID, object|null $product, bool $isProjectStory): array
{
$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(string $browseType = 'my'): array
{
$kanbanList = array();
list($productList, $planList, $projectList, $projectProduct, $latestExecutions, $releaseList) = $this->product->getStats4Kanban($browseType);
$programPairs = array(0 => $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 : '', 'type' => $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, 'type' => '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 $storyType
* @param string $browseType
* @param bool $isProjectStory
* @access protected
* @return void
*/
protected function saveSession4Browse(object|null $product, string $storyType, string $browseType, bool $isProjectStory): void
{
$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');
}
/* For getStoriesAndPager. */
if($isProjectStory && $storyType == 'story' && !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);
if(($browseType == 'bymodule' || $browseType == 'bybranch') && $this->session->storyBrowseType == 'bysearch') $this->session->set('storyBrowseType', 'unclosed');
}
/**
* 构建搜索表单。
* 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
*/
protected function buildSearchFormForBrowse(object|null $project, int $projectID, int &$productID, string $branch, int $param, string $storyType, string $browseType, bool $isProjectStory): void
{
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(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']);
}
}
/* Build search form. */
$params = $isProjectStory ? "projectID=$projectID&" : '';
$actionURL = $this->createLink($this->app->rawModule, $this->app->rawMethod, $params . "productID=$productID&branch=$branch&browseType=bySearch&queryID=myQueryID&storyType=$storyType");
$this->config->product->search['onMenuBar'] = 'yes';
$queryID = ($browseType == 'bysearch') ? $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(array $stories): array
{
$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(): void
{
$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(): void
{
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(): void
{
$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 int $recTotal
* @param string $date
* @param string $direction next|pre
* @access protected
* @return [array, object]
*/
protected function getActionsForDynamic(string $account, string $orderBy, int $productID, string $type, int $recTotal, string $date, string $direction): array
{
/* Build parameters. */
$period = $type == 'account' ? 'all' : $type;
$date = empty($date) ? '' : date('Y-m-d', (int)$date);
$actions = $this->loadModel('action')->getDynamic($account, $period, $orderBy, 50, $productID, 'all', 'all', $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(int $productID): array
{
/* 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(): void
{
$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(array $productList, array $planList, array $projectList, array $releaseList, array $projectProduct): array
{
$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;
$kanbanList[$product->program][$productID] = $product;
}
return $kanbanList;
}
/**
* 获取空的小时对象数据。
* Get empty hour data object.
*
* @access protected
* @return object
*/
protected function getEmptyHour(): object
{
$hour = new stdclass();
$hour->totalEstimate = 0;
$hour->totalConsumed = 0;
$hour->totalLeft = 0;
$hour->progress = 0;
return $hour;
}
/**
* 如果修改产品线的项目集,检查该产品线下是否有关联产品。如果有关联产品将不能修改项目集。
* If the program of a product line changed, check whether there are related products under the product line. If there is an associated product, the program can not be modified.
*
* @param object $lines
* @access protected
* @return bool
*/
protected function canChangeProgram(object $lines): bool
{
if(!in_array($this->config->systemMode, array('ALM', 'PLM'))) return true;
/* 获取修改项目集的产品线。 */
/* Get the product lines which program change. */
$oldLines = $this->product->getLines();
$changedProgramLines = array();
foreach($oldLines as $oldLine)
{
$oldLineID = 'id' . $oldLine->id;
if($lines->programs[$oldLineID] != $oldLine->root) $changedProgramLines[$oldLine->id] = $oldLine->name;
}
/* 检查修改项目集的产品线下是否有关联产品。 */
/* Check whether there are related products under the product line. */
if($changedProgramLines)
{
$hasProductLines = $this->dao->select('id,line')->from(TABLE_PRODUCT)->where('line')->in(array_keys($changedProgramLines))->fetchPairs('line', 'line');
foreach($hasProductLines as $lineID) dao::$errors["id{$lineID}"][] = sprintf($this->lang->product->changeLineError, $changedProgramLines[$lineID]);
if(dao::isError()) return false;
}
return true;
}
/**
* 设置需求列表数据。
* 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
* @access protected
* @return void
*/
protected function assignBrowseData(array $stories, string $browseType, string $storyType, bool $isProjectStory, object|null $product, object|null $project, string $branch, string $branchID)
{
$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, $storyType, $isProjectStory);
list($branchOpt, $branchTagOpt) = $this->getBranchAndTagOption($projectID, $product, $isProjectStory);
/* 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->colon . ($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->storyType = $storyType;
$this->view->browseType = $browseType;
$this->view->isProjectStory = $isProjectStory;
$this->view->branch = $branch;
$this->view->branchID = $branchID;
$this->view->modulePairs = !empty($showModule) ? $this->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;
$this->view->summary = $this->product->summary($stories, $storyType);
$this->view->plans = $this->loadModel('productplan')->getPairs($productID, ($branch === 'all' || empty($branch)) ? '' : $branch, 'unexpired,noclosed', true);
$this->view->users = $this->loadModel('user')->getPairs('noletter|pofirst|nodeleted');
$this->view->modules = $this->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->display();
}
}