* @package product * @link https://www.zentao.net */ class productTao extends productModel { /** * 初始化类的时候,根据是否有产品,还有其他条件,判断是否需要跳转到创建产品页面 * Check locate create product page whether or not, by exist products and other. * * @param array $products * @access protected * @return bool */ protected function checkLocateCreate($products) { if(!empty($products)) return false; $methodName = $this->app->getMethodName(); if(strpos($this->config->product->skipRedirectMethod, ",{$methodName},") !== false) return false; $viewType = $this->app->getViewType(); if($viewType == 'mhtml') return false; //If client device is mobile, then not locate. if($viewType == 'json' or (defined('RUN_MODE') and RUN_MODE == 'api')) return false; //If request type is api, then not locate. return true; } /** * 从数据表获取符合条件的id=>name的键值对。 * Fetch pairs like id=>name. * * @param string $mode all|noclosed * @param int $programID * @param string|array $append * @param string|int $shadow all|0|1 * @access protected * @return int[] */ protected function fetchPairs($mode = '', $programID = 0, $append = '', $shadow = 0) { $append = $this->formatAppendParam($append); return $this->dao->select("t1.*, IF(INSTR(' closed', t1.status) < 2, 0, 1) AS isClosed")->from(TABLE_PRODUCT)->alias('t1') ->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id') ->where('t1.name')->ne('') ->beginIF($this->config->vision == 'rnd')->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)")->fi() ->beginIF($this->app->tab == 'feedback') ->andWhere('t1.status')->eq('normal') ->andWhere('t1.vision')->ne('lite') ->fi() ->beginIF($shadow !== 'all')->andWhere('t1.shadow')->eq((int)$shadow)->fi() ->andWhere('(1=1') ->beginIF(strpos($mode, 'all') === false)->andWhere('t1.deleted')->eq(0)->fi() ->beginIF($programID)->andWhere('t1.program')->eq($programID)->fi() ->beginIF(strpos($mode, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi() ->beginIF(strpos("|$mode|", '|closed|') !== false)->andWhere('t1.status')->eq('closed')->fi() ->beginIF(!$this->app->user->admin && $this->config->vision != 'lite' && $this->app->rawModule != 'repo')->andWhere('t1.id')->in($this->app->user->view->products)->fi() ->markRight(1) ->beginIF($append)->orWhere('(t1.id')->in($append)->markRight(1)->fi() ->filterTpl('skip') ->orderBy("isClosed,t2.order_asc,t1.line_desc,t1.order_asc") ->fetchPairs('id', 'name'); } /** * 获取该产品下,所有符合参数条件的关联项目。 * Fetch all projects link this product. * * @param int $productID * @param string $browseType all|undone|wait|doing|done * @param string $branch * @param string $orderBy * @param object|null $pager * @access protected * @return array */ protected function fetchAllProductProjects($productID, $browseType = 'all', $branch = 'all', $orderBy = 'order_desc', $pager = null) { return $this->dao->select('DISTINCT t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t1.product')->eq($productID) ->andWhere('t2.type')->eq('project') ->beginIF($browseType == 'undone')->andWhere('t2.status')->in('wait,doing')->fi() ->beginIF(strpos(",all,undone,", ",$browseType,") === false)->andWhere('t2.status')->eq($browseType)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi() ->beginIF($branch !== 'all')->andWhere('t1.branch')->in($branch)->fi() ->andWhere('t2.deleted')->eq('0') ->orderBy($orderBy) ->page($pager, 't2.id') ->fetchAll('id'); } /** * 只获取该产品下,我参与的,符合参数条件的关联项目。 * Fetch involved projects link product. * * @param int $productID * @param string $browseType all|undone|wait|doing|done * @param string $branch * @param string $orderBy * @param object|null $pager * @access protected * @return array */ protected function fetchInvolvedProductProjects($productID, $browseType = 'all', $branch = 'all', $orderBy = 'order_desc', $pager = null) { return $this->dao->select('DISTINCT t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->leftJoin(TABLE_TEAM)->alias('t3')->on('t2.id=t3.root') ->leftJoin(TABLE_STAKEHOLDER)->alias('t4')->on('t2.id=t4.objectID') ->where('t1.product')->eq($productID) ->andWhere('t2.type')->eq('project') ->andWhere('t3.type')->eq('project') ->beginIF($browseType == 'undone')->andWhere('t2.status')->in('wait,doing')->fi() ->beginIF(strpos(",all,undone,", ",$browseType,") === false)->andWhere('t2.status')->eq($browseType)->fi() ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi() ->andWhere('t2.openedBy', true)->eq($this->app->user->account) ->orWhere('t2.PM')->eq($this->app->user->account) ->orWhere('t3.account')->eq($this->app->user->account) ->orWhere('(t4.user')->eq($this->app->user->account) ->andWhere('t4.deleted')->eq(0) ->markRight(1) ->orWhere("CONCAT(',', t2.whitelist, ',')")->like("%,{$this->app->user->account},%") ->markRight(1) ->beginIF($branch !== 'all')->andWhere('t1.branch')->in($branch)->fi() ->andWhere('t2.deleted')->eq('0') ->orderBy($orderBy) ->page($pager, 't2.id') ->fetchAll('id'); } /** * 获取产品ID数组中带有项目集信息的产品分页列表。 * Get products with program data that in the ID list. * * @param array $productIDs * @param object $pager * @access protected * @return array */ protected function getPagerProductsWithProgramIn($productIDs, $pager) { return $this->dao->select('t1.*')->from(TABLE_PRODUCT)->alias('t1') ->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id') ->where('t1.id')->in($productIDs) ->filterTpl('skip') ->orderBy('t2.order_asc, t1.line_desc, t1.order_asc') ->page($pager) ->fetchAll('id'); } /** * 获取产品ID数组中的产品排序分页列表。 * Get products in the ID list. * * @param array $productIDs * @param object $pager * @param string $orderBy * @access protected * @return array */ protected function getPagerProductsIn($productIDs, $pager, $orderBy) { return $this->dao->select('*')->from(TABLE_PRODUCT) ->where('id')->in($productIDs) ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); } /** * 获取在需求列表页面的搜索表单中,模块字段的下拉选项。包括产品、项目下的需求页面。 * Get modules for search form, in product, project story page. * * @param int $productID * @param array $products * @param string $branch * @param int $projectID * @access protected * @return array */ protected function getModulesForSearchForm($productID, $products, $branch = 'all', $projectID = 0) { $this->loadModel('tree'); if($this->app->tab != 'project') return $this->tree->getOptionMenu($productID, 'story', 0, $branch); if($productID) { $modules = array(); $branchList = $this->loadModel('branch')->getPairs($productID, '', $projectID); unset($branchList['all']); $branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchList)); foreach($branchModuleList as $branchModules) $modules += $branchModules; return $modules; } /* 在项目需求页面,获取项目关联产品的模块。 */ $moduleList = array(); $modules = array('/'); $branchGroup = $this->loadModel('execution')->getBranchByProduct(array_keys($products), $projectID, ''); foreach($products as $productID => $productName) { if(isset($branchGroup[$productID])) { $branchModuleList = $this->tree->getOptionMenu($productID, 'story', 0, array_keys($branchGroup[$productID])); foreach($branchModuleList as $branchModules) { if(is_array($branchModules)) $moduleList += $branchModules; } } else { $moduleList = $this->tree->getOptionMenu($productID, 'story', 0, $branch); } foreach($moduleList as $moduleID => $moduleName) { if(empty($moduleID)) continue; $modules[$moduleID] = $productName . $moduleName; } } return $modules; } /** * 根据产品ID列表和类型,获取统计数据。 * Get statistic data by product ID list and type. * * @param array $productIdList * @param string $type * @access protected * @return array */ protected function getStatisticByType($productIdList, $type) { $this->app->loadClass('date', true); $weekDate = date::getThisWeek(); $fields = 'product,COUNT(1) AS count'; $tableName = zget($this->config->objectTables, $type, TABLE_BUG); if($type == 'plans') $tableName = TABLE_PRODUCTPLAN; if($type == 'releases') $tableName = TABLE_RELEASE; if($type == 'latestReleases') { $fields = 'product, name, date'; $tableName = TABLE_RELEASE; } $dao = $this->dao->select($fields) ->from($tableName) ->where('product')->in($productIdList) ->andWhere('deleted')->eq(0) ->beginIF($type == 'plans')->andWhere('end')->gt(helper::now())->fi() ->beginIF($type == 'assignToNull')->andWhere('assignedto')->eq('')->fi() ->beginIF($type == 'fixedBugs')->andWhere('resolution')->eq('fixed')->fi() ->beginIF($type == 'unResolved')->orWhere('resolution')->eq('postponed')->fi() ->beginIF($type == 'closedBugs' || $type == 'fixedBugs')->andWhere('status')->eq('closed')->fi() ->beginIF($type == 'unResolved' || $type == 'activeBugs')->andWhere('status')->eq('active')->fi() ->beginIF($type == 'thisWeekBugs')->andWhere('openedDate')->between($weekDate['begin'], $weekDate['end'])->fi() ->beginIF($type != 'latestReleases')->groupBy('product')->fi() ->beginIF($type == 'latestReleases')->orderBy('id_desc')->fi(); return $type == 'latestReleases' ? $dao->fetchGroup('product') : $dao->fetchPairs(); } /** * 计算在点击1.5级导航下拉选项后,跳转的模块名和方法名。 * Compute locate for drop menu in PC. * * @access protected * @return array */ protected function computeLocate4DropMenu() { $currentModule = (string)$this->app->rawModule; $currentMethod = (string)$this->app->rawMethod; /* Init currentModule and currentMethod for report and story. */ if($currentModule == 'story') { if(strpos(",track,create,batchcreate,batchclose,", ",{$currentMethod},") === false) $currentModule = 'product'; if($currentMethod == 'view' or $currentMethod == 'change' or $currentMethod == 'review') $currentMethod = 'browse'; } if($currentModule == 'testcase' and strpos(',view,edit,', ",$currentMethod,") !== false) $currentMethod = 'browse'; if($currentModule == 'bug' and $currentMethod == 'edit') $currentMethod = 'browse'; if($currentMethod == 'report') $currentMethod = 'browse'; return array($currentModule, $currentMethod); } /** * 格式化append参数,保证输出用逗号间隔的id列表。 * Format append param. * * @param string|array append * @access protected * @return string */ protected function formatAppendParam($append = '') { if(empty($append)) return ''; if(is_string($append)) $append = explode(',', $append); $append = array_map(function($item){;return is_array($item) ? $item->branch : (int)$item;}, $append); $append = array_unique(array_filter($append)); sort($append); return implode(',', $append); } /** * 创建产品线。 * Create product line. * * @param int programID * @param string lineName * @access protected * @return int|false */ protected function createLine($programID, $lineName) { if($programID < 0) return false; if(empty($lineName)) return false; $line = new stdClass(); $line->type = 'line'; $line->parent = 0; $line->grade = 1; $line->name = htmlSpecialString($lineName); $line->root = in_array($this->config->systemMode, array('ALM', 'PLM')) && helper::hasFeature('program') ? $programID : 0; $existedLineID = (int)$this->dao->select('id')->from(TABLE_MODULE)->where('type')->eq('line')->andWhere('root')->eq($line->root)->andWhere('name')->eq($line->name)->fetch('id'); if($existedLineID) return $existedLineID; $this->dao->insert(TABLE_MODULE)->data($line)->exec(); if(dao::isError()) return false; $lineID = $this->dao->lastInsertID(); $path = ",$lineID,"; $this->dao->update(TABLE_MODULE)->set('path')->eq($path)->set('`order`')->eq($lineID)->where('id')->eq($lineID)->exec(); return $lineID; } /** * 关联创建产品主库 * Create main lib for product * * @param int productID * @access protected * @return int|false */ protected function createMainLib($productID) { if($productID <= 0) return false; $existedLibID = (int)$this->dao->select('id')->from(TABLE_DOCLIB)->where('product')->eq($productID) ->andWhere('type')->eq('product') ->andWhere('main')->eq('1') ->fetch('id'); if($existedLibID) return $existedLibID; $this->app->loadLang('doc'); $lib = new stdclass(); $lib->product = $productID; $lib->name = $this->lang->doclib->main['product']; $lib->type = 'product'; $lib->main = '1'; $lib->acl = 'default'; $lib->vision = $this->config->vision; $lib->addedBy = $this->app->user->account; $lib->addedDate = helper::now(); $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec(); $productSettingList = isset($this->config->global->productSettingList) ? json_decode($this->config->global->productSettingList, true) : array(); if($productSettingList) { $productSettingList[] = $productID; $this->loadModel('setting')->setItem('system.common.global.productSettingList', json_encode($productSettingList)); } if(dao::isError())return false; return $this->dao->lastInsertID(); } /** * 执行SQL,更新到对应的产品信息。 * Do update this product. * * @param object $product * @param int $productID * @param int $programID * @access protected * @return bool */ protected function doUpdate($product, $productID, $programID) { if(empty($productID)) return false; if(count(get_object_vars($product)) == 0) return false; $this->dao->update(TABLE_PRODUCT)->data($product)->autoCheck() ->checkIF(!empty($product->name), 'name', 'unique', "id != {$productID} and `program` = {$programID} and `deleted` = '0'") ->checkIF(!empty($product->code), 'code', 'unique', "id != {$productID} and `program` = {$programID} and `deleted` = '0'") ->batchCheck($this->config->product->edit->requiredFields, 'notempty') ->checkFlow() ->where('id')->eq($productID) ->exec(); return !dao::isError(); } /** * 获取需求列表关联的用例总数。 * Get cases count of stories. * * @param array $storyIdList * @access protected * @return int */ protected function getStoryCasesCount($storyIdList) { if(empty($storyIdList)) return 0; return $this->dao->select('COUNT(story) AS count')->from(TABLE_CASE)->where('story')->in($storyIdList)->andWhere('deleted')->eq('0')->fetch('count'); } /** * 过滤无用例的需求。 * Filter no cases story. * * @param array $storyIDList * @access protected * @return array */ protected function filterNoCasesStory($storyIDList) { if(empty($storyIDList)) return array(); return $this->dao->select('story')->from(TABLE_CASE)->where('story')->in($storyIDList)->andWhere('deleted')->eq(0)->fetchAll('story'); } /** * 获取项目关联的产品。 * Get products by project ID. * * @param int|array $projectID * @param string|array $append '1,2,3' * @param string $status * @param string $orderBy * @param bool $withDeleted * @param bool $noShadow * @access protected * @return int[] */ protected function getProductsByProjectID($projectID, $append, $status, $orderBy, $withDeleted = false, $noShadow = false) { /* 处理要用的到变量信息。 */ $append = $this->formatAppendParam($append); $orderBy = ($orderBy ? "{$orderBy}," : '') . 't2.order asc'; return $this->dao->select("t1.branch, t1.plan, t2.*")->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where("(FIND_IN_SET('{$this->config->vision}', t2.vision)") ->beginIF(!$withDeleted)->andWhere('t2.deleted')->eq(0)->fi() ->beginIF($noShadow)->andWhere('t2.shadow')->eq(0)->fi() ->beginIF(!empty($projectID))->andWhere('t1.project')->in($projectID)->fi() ->beginIF(!$this->app->user->admin and $this->config->vision != 'lite')->andWhere('t2.id')->in($this->app->user->view->products)->fi() ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('t2.status')->ne('closed')->fi() ->markRight(1) ->beginIF($append && empty($projectID))->orWhere('(t1.product')->in($append)->fi() ->beginIF($append && !empty($projectID)) ->orWhere('(t1.product')->in($append) ->andWhere('t1.project')->in($projectID) ->markRight(1) ->fi() ->orderBy($orderBy) ->fetchAll(); } /** * 过滤有效的产品计划, 并返回所有父级计划。 * Filter valid product plans. * * @param array $planList * @access protected * @return array[] */ protected function filterOrderedAndParentPlans($planList) { $parentPlans = array(); $orderedPlans = array(); foreach($planList as $planID => $plan) { /* Collect and remove parent plan. */ if($plan->parent == '-1') { $parentPlans[$planID] = $plan->title; unset($planList[$planID]); continue; } /* Remove exceeded plan and long-time plan. */ if((!helper::isZeroDate($plan->end) and $plan->end < date('Y-m-d')) or $plan->end == '2030-01-01') continue; $orderedPlans[$plan->end][] = $plan; } krsort($orderedPlans); return array($orderedPlans, $parentPlans); } /** * 通过产品ID查询产品关联的统计数据。 * Get stat count by product ID. * * @param int $productID * @access protected * @return int|float * @param string $tableName */ protected function getStatCountByID($tableName, $productID) { switch($tableName) { case TABLE_PRODUCTPLAN: /* Get unclosed plans count. */ return $this->dao->select('COUNT(1) AS count')->from(TABLE_PRODUCTPLAN)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->andWhere('end')->gt(helper::now())->fetch('count'); case TABLE_BUILD: /* Get builds count. */ return $this->dao->select('COUNT(1) AS count')->from(TABLE_BUILD)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->fetch('count'); case TABLE_CASE: /* Get cases count. */ return $this->dao->select('COUNT(1) AS count')->from(TABLE_CASE)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->fetch('count'); case TABLE_BUG: /* Get bugs count. */ return $this->dao->select('COUNT(1) AS count')->from(TABLE_BUG)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->fetch('count'); case TABLE_DOC: /* Get docs count. */ return $this->dao->select('COUNT(1) AS count')->from(TABLE_DOC)->where('product')->eq("$productID")->andWhere('deleted')->eq('0')->andWhere('type')->ne('chapter')->fetch('count'); case TABLE_RELEASE: /* Get releases count. */ return $this->dao->select('COUNT(1) AS count')->from(TABLE_RELEASE)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->fetch('count'); case TABLE_PROJECTPRODUCT: return $this->dao->select('COUNT(DISTINCT(t1.project)) AS count') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.deleted')->eq('0') ->andWhere('t1.product')->eq("$productID") ->andWhere('t2.type')->eq('project') ->fetch('count'); case 'executions': return $this->dao->select('COUNT(DISTINCT(t1.project)) AS count') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.deleted')->eq('0') ->andWhere('t1.product')->eq("$productID") ->andWhere('t2.type')->in('sprint,stage,kanban') ->fetch('count'); case 'progress': $closedTotal = $this->dao->select('COUNT(id) AS count')->from(TABLE_STORY)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->andWhere('status')->eq('closed')->fetch('count'); if(empty($closedTotal)) return 0; $allTotal = $this->dao->select('COUNT(id) AS count')->from(TABLE_STORY)->where('deleted')->eq('0')->andWhere('product')->eq("$productID")->fetch('count'); return round($closedTotal / $allTotal * 100, 1); default: return 0; } } /** * 获取产品与项目关联的项目集数据列表。 * Get the progam info of the releated project and product by product list. * * @param array $productList * @access protected * @return array */ protected function getProjectProductList($productList) { $projectProductList = $this->dao->select('t1.product,t1.project,t2.parent,t2.path') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id') ->where('t1.product')->in(array_keys($productList)) ->andWhere('t1.project')->in($this->app->user->view->projects) ->andWhere('t2.type')->eq('project') ->andWhere('t2.status')->eq('doing') ->andWhere('t2.deleted')->eq('0') ->fetchGroup('product', 'project'); if(!in_array($this->config->systemMode, array('ALM', 'PLM')) || $this->config->product->showAllProjects) return $projectProductList; /* ALM mode and don't show all projects. */ foreach($projectProductList as $productID => $projects) { if(!isset($productList[$productID])) continue; $product = $productList[$productID]; foreach($projects as $projectID => $project) { if($project->parent == $product->program || strpos($project->path, ",{$product->program},") === 0) continue; /* Filter invalid product. */ unset($projectProductList[$productID][$projectID]); } } return $projectProductList; } /** * 获取产品相关计划列表。 * Get plan list by product ID list. * * @param array $productIdList * @access protected * @return array */ protected function getPlanList($productIdList) { $date = date('Y-m-d'); return $this->dao->select('id,product,title,parent,begin,end') ->from(TABLE_PRODUCTPLAN) ->where('product')->in($productIdList) ->andWhere('deleted')->eq('0') ->andWhere('end')->ge($date) ->andWhere('parent')->ne(-1) ->orderBy('begin desc') ->fetchGroup('product', 'id'); } /** * 获取产品相关执行列表。 * Get execution list by project and product ID list. * * @param array $projectIdList * @param array $productIdList * @access protected * @return array */ protected function getExecutionList($projectIdList, $productIdList) { return $this->dao->select('t1.product as productID,t2.*') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id') ->where('t2.type')->in('stage,sprint,kanban') ->andWhere('t2.project')->in($projectIdList) ->beginIF(!$this->app->user->admin) ->andWhere('t1.project')->in($this->app->user->view->sprints) ->fi() ->andWhere('t1.product')->in($productIdList) ->andWhere('t2.status')->eq('doing') ->andWhere('t2.multiple')->ne('0') ->andWhere('t2.deleted')->eq('0') ->orderBy('t2.id_desc') ->fetchGroup('project', 'id'); } /** * 获取产品相关发布列表。 * Get release list by product ID list. * * @param array $productIdList * @access protected * @return array */ protected function getReleaseList($productIdList) { return $this->dao->select('id,product,name,marker') ->from(TABLE_RELEASE) ->where('deleted')->eq('0') ->andWhere('product')->in($productIdList) ->andWhere('status')->eq('normal') ->fetchGroup('product', 'id'); } /** * 获取多个产品下进行中的执行数的键值对。 * Get K-V pairs of product ID and doing executions count. * * @param array $productIdList * @access protected * @return array */ protected function getExecutionCountPairs($productIdList) { return $this->dao->select('t1.product, COUNT(1) AS count') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.deleted')->eq('0') ->andWhere('t1.product')->in($productIdList) ->andWhere('t2.type')->in('sprint,stage,kanban') ->andWhere('t2.status')->eq('doing') ->groupBy('t1.product') ->fetchPairs('product'); } /** * 获取多个产品关联的项目数的键值对。 * Retrieve key-value pairs of product IDs and their corresponding project counts. * * @param array $productIdList * @access protected * @return array */ protected function getProjectCountPairs($productIdList) { return $this->dao->select('t1.product, COUNT(1) AS count') ->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t2.deleted')->eq('0') ->andWhere('t1.product')->in($productIdList) ->andWhere('t2.type')->eq('project') ->andWhere('t2.status')->eq('doing') ->groupBy('t1.product') ->fetchPairs('product'); } /** * 通过产品ID列表获取每个需求关联的用例数。 * Get case count of each story in the story list. * * @param int[] $productIdList * @access protected * @return array */ protected function getCaseCountByProductIdList($productIdList) { return $this->dao->select('story, COUNT(1) AS count')->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->where('t1.deleted')->eq('0') ->andWhere('t2.deleted')->eq('0') ->andWhere('t2.product')->in($productIdList) ->groupBy('story') ->fetchPairs('story'); } /** * 构建执行键值对,主要处理子阶段的情况。 * Build execution pairs. * * @param array $executions * @param string $mode stagefilter|hasparent * @param bool $withProjectName * @access protected * @return array */ protected function buildExecutionPairs($executions, $mode = '', $withProjectName = false) { $projectIdList = array_column($executions, 'project'); /* 现在只有阶段有子阶段,所以只查询阶段类型的执行。 */ $stages = $this->dao->select('id,name,attribute,parent,path')->from(TABLE_EXECUTION)->where('type')->eq('stage')->andWhere('project')->in($projectIdList)->andWhere('deleted')->eq('0')->fetchAll('id'); $stageFilter = strpos($mode, 'stagefilter') !== false; foreach($stages as $exec) $parents[$exec->parent] = true; /* 根据条件整理数据,将子阶段放到父阶段中。 */ $executionPairs = array(); foreach($executions as $id => $execution) { if(isset($parents[$id])) continue; if($stageFilter && in_array($execution->attribute, array('request', 'design', 'review'))) continue; // Some stages of waterfall not need. if(isset($execution->children)) { $executionPairs = $executionPairs + $execution->children; continue; } /* Set execution name. */ $executionName = ''; if(isset($stages[$id])) { $paths = array_slice(explode(',', trim($execution->path, ',')), 1); foreach($paths as $path) { if(isset($stages[$path])) $executionName .= '/' . $stages[$path]->name; } } else { $executionName = $withProjectName ? '/' . $execution->name : $execution->name; } if($withProjectName) $executionName = $execution->projectName . $executionName; if(empty($execution->multiple)) $executionName = $execution->projectName . "({$this->lang->project->disableExecution})"; $executionPairs[$id] = $executionName; } return $executionPairs; } /** * 统计项目集内的产品数据 * Statistic product data. * * @param string $type line|program * @param array $programStructure * @param object $product * @access protected * @return array */ protected function statisticProductData($type, $programStructure, $product) { if(empty($programStructure)) return $programStructure; /* Init vars. */ $data = $type == 'program' ? $programStructure[$product->program] : $programStructure[$product->program][$product->line]; foreach($this->config->product->statisticFields as $key => $fields) { foreach($fields as $field) { if(!isset($data[$field])) $data[$field] = 0; $data[$field] += $product->$field; } } return $data; } /** * 获取产品的统计数据。 * Get summary of products to be refreshed. * * @param array $productIdList * @access protected * @return array */ protected function getProductStats($productIdList) { $productStories = $this->getStoryStats($productIdList); $productBugs = $this->getBugStats($productIdList); $plans = $this->dao->select('product, count(id) AS c') ->from(TABLE_PRODUCTPLAN) ->where('deleted')->eq(0) ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi() ->andWhere('end')->gt(helper::now()) ->groupBy('product') ->fetchPairs(); $releases = $this->dao->select('product, count(id) AS c') ->from(TABLE_RELEASE) ->where('deleted')->eq(0) ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi() ->groupBy('product') ->fetchPairs(); /* If products is empty, get all products. */ if(empty($productIdList)) $productIdList = $this->dao->select('id')->from(TABLE_PRODUCT)->where('deleted')->eq(0)->fetchPairs(); $stats = array(); foreach($productIdList as $productID) { $product = new stdclass(); $product->draftEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['draft'] : 0; $product->activeEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['active'] : 0; $product->changingEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['changing'] : 0; $product->reviewingEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['reviewing'] : 0; $product->closedEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['closed'] : 0; $product->finishedEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['finished'] : 0; $product->totalEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['total'] : 0; $product->draftRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['draft'] : 0; $product->activeRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['active'] : 0; $product->changingRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['changing'] : 0; $product->reviewingRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['reviewing'] : 0; $product->closedRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['closed'] : 0; $product->finishedRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['finished'] : 0; $product->totalRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['total'] : 0; $product->draftStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['draft'] : 0; $product->activeStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['active'] : 0; $product->changingStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['changing'] : 0; $product->reviewingStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['reviewing'] : 0; $product->closedStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['closed'] : 0; $product->finishedStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['finished'] : 0; $product->totalStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['total'] : 0; $product->unresolvedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['unresolved'] : 0; $product->closedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['closed'] : 0; $product->fixedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['fixed'] : 0; $product->totalBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['total'] : 0; $product->plans = isset($plans[$productID]) ? $plans[$productID] : 0; $product->releases = isset($releases[$productID]) ? $releases[$productID] : 0; $stats[$productID] = $product; } return $stats; } /** * 获取产品下需求的统计数据。 * Get story statistic data. * * @param array $productIdList * @access protected * @return array */ protected function getStoryStats($productIdList) { $stories = $this->dao->select('product, status, `closedReason`, count(id) AS c, type') ->from(TABLE_STORY) ->where('deleted')->eq(0) ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi() ->groupBy('product, status, `closedReason`, type') ->fetchAll(); $productStories = array(); foreach($stories as $story) { if(!isset($productStories[$story->product][$story->type])) $productStories[$story->product][$story->type] = array('draft' => 0, 'active' => 0, 'changing' => 0, 'reviewing' => 0, 'finished' => 0, 'closed' => 0, 'total' => 0); if($story->status == 'draft') $productStories[$story->product][$story->type]['draft'] += $story->c; if($story->status == 'active') $productStories[$story->product][$story->type]['active'] += $story->c; if($story->status == 'changing') $productStories[$story->product][$story->type]['changing'] += $story->c; if($story->status == 'reviewing') $productStories[$story->product][$story->type]['reviewing'] += $story->c; if($story->status == 'closed') $productStories[$story->product][$story->type]['closed'] += $story->c; $productStories[$story->product][$story->type]['total'] += $story->c; if($story->status == 'closed' && $story->closedReason == 'done') $productStories[$story->product][$story->type]['finished'] += $story->c; } return $productStories; } /** * 获取产品下bug的统计数据。 * Get bug statistic data. * * @param array $productIdList * @access protected * @return array */ protected function getBugStats($productIdList) { $bugs = $this->dao->select('product,status,resolution, count(id) AS c') ->from(TABLE_BUG) ->where('deleted')->eq(0) ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi() ->groupBy('product, status, resolution') ->fetchAll(); $productBugs = array(); foreach($bugs as $bug) { if(!isset($productBugs[$bug->product])) $productBugs[$bug->product] = array('unresolved' => 0, 'fixed' => 0, 'closed' => 0, 'total' => 0); if($bug->status == 'active' || ($bug->status == 'closed' && $bug->resolution == 'postponed')) $productBugs[$bug->product]['unresolved'] += $bug->c; if($bug->status == 'closed' && $bug->resolution == 'fixed') $productBugs[$bug->product]['fixed'] += $bug->c; if($bug->status == 'closed') $productBugs[$bug->product]['closed'] += $bug->c; $productBugs[$bug->product]['total'] += $bug->c; } return $productBugs; } /** * 当产品线的项目集发生变化时,更新产品线下产品的项目集。 * Sync program to product when line's program changed. * * @param int $programID * @param int $lineID * @access protected * @return void */ protected function syncProgramToProduct($programID, $lineID) { $this->loadModel('action'); $oldProducts = $this->dao->select('*')->from(TABLE_PRODUCT)->where('line')->eq($lineID)->fetchAll('id'); $this->dao->update(TABLE_PRODUCT)->set('program')->eq($programID)->where('line')->eq($lineID)->exec(); if(!dao::isError()) { $products = $this->dao->select('*')->from(TABLE_PRODUCT)->where('line')->eq($lineID)->fetchAll('id'); foreach($products as $productID => $product) { if($oldProducts[$productID]->program != $product->program) { $changes = common::createChanges($oldProducts[$productID], $product); $actionID = $this->action->create('product', $productID, 'ChangedProgram'); $this->action->logHistory($actionID, $changes); } } } } }