znt/module/product/model.php

2228 lines
94 KiB
PHP
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* The model 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 Chunsheng Wang <chunsheng@cnezsoft.com>
* @package product
* @link https://www.zentao.net
*/
class productModel extends model
{
/**
* 检查是否有权限访问该产品。
* Check privilege.
*
* @param int $product
* @access public
* @return bool
*/
public function checkPriv(int $productID): bool
{
if(common::isTutorialMode()) return true;
return !empty($productID) && ($this->app->user->admin || (strpos(",{$this->app->user->view->products},", ",{$productID},") !== false));
}
/**
* 根据id获取产品。
* Get product by id.
*
* @param int $productID
* @access public
* @return object|false
*/
public function getByID(int $productID): object|false
{
if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProduct();
$product = $this->fetchById($productID);
if(!$product) return false;
return $this->loadModel('file')->replaceImgURL($product, 'desc');
}
/**
* 根据id列表获取产品列表。
* Get products by id list.
*
* @param array $productIdList
* @access public
* @return array
*/
public function getByIdList(array $productIdList): array
{
return $this->dao->select('*')->from(TABLE_PRODUCT)->where('id')->in($productIdList)->fetchAll('id', false);
}
/**
* 根据权限控制范围获取产品。
* Get products by acl.
*
* @param string $acl
* @access public
* @return array
*/
public function getListByAcl(string $acl): array
{
return $this->dao->select('id, program, PO, QD, RD, feedback, ticket, acl, whitelist, reviewer, PMT, createdBy')->from(TABLE_PRODUCT)->where('acl')->in($acl)->fetchAll('id');
}
/**
* 通过session 中的搜索条件查询产品列表。
* Get product list by search.
*
* @param int $queryID
* @access public
* @return array
*/
public function getListBySearch(int $queryID = 0): array
{
$this->loadModel('search')->setQuery('product', $queryID);
return $this->dao->select('*')->from(TABLE_PRODUCT)->where($this->session->productQuery)
->andWhere('deleted')->eq(0)
->andWhere('shadow')->eq(0)
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->products)->fi()
->beginIF($this->config->vision != 'or')->andWhere("FIND_IN_SET('{$this->config->vision}', vision)")->fi()
->filterTpl('skip')
->orderBy('order_asc')
->fetchAll('id', false);
}
/**
* 获取产品键值对列表。
* Get product pairs.
*
* @param string $mode all|noclosed
* @param int $programID
* @param string|array $append
* @param string|int $shadow all|0|1
* @return int[]
*/
public function getPairs(string $mode = '', int $programID = 0, string|array $append = '', string|int $shadow = 0): array
{
if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProductPairs();
return $this->productTao->fetchPairs($mode, $programID, $append, $shadow);
}
/**
* 根据项目获取关联的产品键值对列表。
* Get product pairs by project.
*
* @param int|array $projectID
* @param string $status all|noclosed
* @param string|array $append
* @param bool $noDeleted
* @param bool $noShadow
* @access public
* @return array
*/
public function getProductPairsByProject(int|array $projectID = 0, string $status = 'all', string $append = '', bool $noDeleted = true, bool $noShadow = false): array
{
if(empty($projectID)) return array();
return $this->getProducts($projectID, $status, '', false, $append, $noDeleted, $noShadow);
}
/**
* 获取所有与某类型的项目所关联的产品。
* Get product pairs by project model.
*
* @param string $model all|scrum|waterfall|kanban
* @access public
* @return array
*/
public function getPairsByProjectModel(string $model = 'all'): array
{
return $this->dao->select('t3.id, t3.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id')
->where('t3.deleted')->eq(0)
->beginIF(!$this->app->user->admin)->andWhere('t3.id')->in($this->app->user->view->products)->fi()
->andWhere("FIND_IN_SET('{$this->config->vision}', t3.vision)")
->beginIF($model != 'all')->andWhere('t2.model')->eq($model)->fi()
->fetchPairs('id', 'name');
}
/**
* 获取关联项目的产品。
* Get products by project.
*
* @param int|array $projectID
* @param string $status all|noclosed
* @param string $orderBy
* @param bool $withBranch
* @param string|array $append
* @param bool $noDeleted
* @param bool $noShadow
* @access public
* @return int[]
*/
public function getProducts(int|array $projectID = 0, string $status = 'all', string $orderBy = '', bool $withBranch = true, string|array $append = '', bool $noDeleted = true, bool $noShadow = false): array
{
/* 如果是新手教程模式,直接返回测试数据。*/
if(commonModel::isTutorialMode())
{
$this->loadModel('tutorial');
if(!$withBranch) return $this->tutorial->getProductPairs();
return $this->tutorial->getExecutionProducts();
}
/* 初始化变量。 */
$projectProducts = $this->productTao->getProductsByProjectID($projectID, $append, $status, $orderBy, $noDeleted, $noShadow);
$products = array();
/* 如果不返回分支信息,则返回 id=>name 的键值对。 */
if(!$withBranch)
{
foreach($projectProducts as $product) $products[$product->id] = $product->deleted ? $product->name . "({$this->lang->product->deleted})" : $product->name;
return $products;
}
/* 将分支 ID 、计划 ID 合并到产品数据中。*/
foreach($projectProducts as $product)
{
if(!isset($products[$product->id]))
{
$products[$product->id] = $product;
$products[$product->id]->branches = array();
$products[$product->id]->plans = array();
}
$products[$product->id]->branches[$product->branch] = $product->branch;
if($product->plan) $products[$product->id]->plans[$product->plan] = $product->plan;
unset($product->branch, $product->plan);
}
return $products;
}
/**
* 通过项目id查询关联的产品id。
* Get product id list or first id by project.
*
* @param int $projectID
* @param bool $isFirst
* @access public
* @return object[]|int
*/
public function getProductIDByProject(int $projectID, bool $isFirst = true): array|int
{
$products = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)
->where('1=1')
->beginIF(!$this->app->user->admin)->andWhere('product')->in($this->app->user->view->products)->fi()
->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
->fetchPairs();
if($isFirst === false) return $products;
return empty($products) ? 0 : current($products);
}
/**
* 根据项目id获取其对应的影子产品。
* Get shadow product by project id.
*
* @param int $projectID
* @access public
* @return object|false
*/
public function getShadowProductByProject(int $projectID): object|false
{
return $this->dao->select('products.*')->from(TABLE_PRODUCT)->alias('products')
->leftJoin(TABLE_PROJECTPRODUCT)->alias('relations')->on('products.id = relations.product')
->where('products.shadow')->eq(1)
->andWhere('relations.project')->eq($projectID)
->fetch();
}
/**
* 遍历产品列表,将产品线拼接到产品名上。
* Iterate products, concatenating product line onto product name.
*
* @param array $products
* @access public
* @return array
*/
public function concatProductLine(array $products): array
{
$lines = $this->getLinePairs();
$productsWithLine = array();
$productsNoLine = array();
foreach($products as $product)
{
if(array_key_exists($product->line, $lines))
{
$lineName = $lines[$product->line];
if(in_array($this->config->systemMode, array('ALM', 'PLM'))) $product->name = $lineName . '/' . $product->name;
$productsWithLine[] = $product;
}
else
{
$productsNoLine[] = $product;
}
}
return array_merge($productsWithLine, $productsNoLine);
}
/**
* 获取排序后的产品列表,顺序为:我的产品、其他人的产品、关闭的产品。
* Get ordered products.
*
* @param string $status
* @param int $num
* @param int $projectID
* @param string|int $shadow all|0|1
* @access public
* @return array
*/
public function getOrderedProducts(string $status, int $num = 0, int $projectID = 0, int|string $shadow = 0): array
{
if(common::isTutorialMode()) return $this->loadModel('tutorial')->getProductStats();
$products = array();
if($projectID)
{
$pairs = $this->getProducts($projectID, $status == 'normal' ? 'noclosed' : '');
$products = $this->getByIdList(array_keys($pairs));
}
else
{
$products = $this->getList(0, $status, $num, 0, $shadow);
}
if(empty($products)) return $products;
$productList = $this->concatProductLine($products);
$currentUser = $this->app->user->account;
$orderedProducts = $mineProducts = $othersProducts = $closedProducts = array();
foreach($productList as $product)
{
if(!$this->app->user->admin and !$this->checkPriv($product->id)) continue;
if($product->status == 'normal' and $product->PO == $currentUser) $mineProducts[$product->id] = $product;
if($product->status == 'normal' and $product->PO != $currentUser) $othersProducts[$product->id] = $product;
if($product->status == 'closed') $closedProducts[$product->id] = $product;
}
$orderedProducts = $mineProducts + $othersProducts + $closedProducts;
if(empty($num)) return $orderedProducts;
return array_slice($orderedProducts, 0, $num, true);
}
/**
* 获取多分支产品和多平台产品。
* Get Multi-branch and Multi-platform product pairs.
*
* @param int $programID
* @access public
* @return array
*/
public function getMultiBranchPairs(int $programID = 0): array
{
return $this->dao->select('id')->from(TABLE_PRODUCT)
->where('deleted')->eq(0)
->beginIF($programID)->andWhere('program')->eq($programID)->fi()
->andWhere('type')->in('branch,platform')
->fetchPairs();
}
/**
* 获取以所属项目集分组的产品列表。
* Get products group by program.
*
* @access public
* @return array
*/
public function getProductsGroupByProgram(): array
{
$products = $this->getList(0, 'noclosed');
$programIdList = array_unique(array_column($products, 'program'));
$programs = helper::hasFeature('program') ? $this->loadModel('program')->getPairsByList($programIdList) : array();
$productGroup = array();
foreach($products as $product) $productGroup[$product->program][$product->id] = zget($programs, $product->program, '') . '/' . $product->name;
return $productGroup;
}
/**
* 用对象数据创建产品
* Create a product.
*
* @param object $product
* @param string $lineName
* @access public
* @return int|false
*/
public function create(object $product, string $lineName = ''): int|false
{
/* Insert product and get the product ID. */
$this->lang->error->unique = $this->lang->error->repeat;
$this->dao->insert(TABLE_PRODUCT)->data($product)->autoCheck()
->checkIF((!empty($product->name) && isset($product->program)), 'name', 'unique', "`program` = " . zget($product, 'program') . " AND `deleted` = '0'")
->checkIF((!empty($product->code) && isset($product->program)), 'code', 'unique', "`program` = " . zget($product, 'program') . " AND `deleted` = '0'")
->batchCheck($this->config->product->create->requiredFields, 'notempty')
->checkFlow()
->exec();
if(dao::isError()) return false;
$productID = $this->dao->lastInsertID();
/* Fix order and line fields for product. */
$fixData = new stdclass();
$fixData->order = $productID * 5;
if(!empty($lineName))
{
$lineID = $this->productTao->createLine(!empty($product->program) ? (int)$product->program : 0, $lineName);
if($lineID) $fixData->line = $lineID;
}
$this->dao->update(TABLE_PRODUCT)->data($fixData)->where('id')->eq($productID)->exec();
/* Update and create linked data. */
$this->loadModel('action')->create('product', $productID, 'opened');
$uid = empty($this->post->uid) ? '' : $this->post->uid;
$this->loadModel('file')->updateObjectID($uid, $productID, 'product');
$this->productTao->createMainLib($productID);
if($product->whitelist) $this->loadModel('personnel')->updateWhitelist(explode(',', $product->whitelist), 'product', $productID);
if($product->acl != 'open') $this->loadModel('user')->updateUserView(array($productID), 'product');
return $productID;
}
/**
* 更新产品数据。
* Update a product.
*
* @param int $productID
* @param object $product
* @access public
* @return array|false
*/
public function update(int $productID, object $product): array|false
{
$oldProduct = $this->fetchByID($productID);
$this->lang->error->unique = $this->lang->error->repeat;
$result = $this->productTao->doUpdate($product, $productID, zget($product, 'program', $oldProduct->program));
if(!$result) return false;
/* Update objectID field of file recode, that upload by editor. */
$this->loadModel('file')->updateObjectID($this->post->uid, $productID, 'product');
$whitelist = explode(',', $product->whitelist);
$this->loadModel('personnel')->updateWhitelist($whitelist, 'product', $productID);
if($oldProduct->acl != $product->acl and $product->acl != 'open') $this->loadModel('user')->updateUserView(array($productID), 'product');
if($product->type == 'normal' and $oldProduct->type != 'normal') $this->loadModel('branch')->unlinkBranch4Project(array($productID));
if($product->type != 'normal' and $oldProduct->type == 'normal') $this->loadModel('branch')->linkBranch4Project($productID);
/* Save action and changes. */
$changes = common::createChanges($oldProduct, $product);
if($changes)
{
$actionID = $this->loadModel('action')->create('product', $productID, 'edited');
$this->action->logHistory($actionID, $changes);
}
return $changes;
}
/**
* 批量更新产品信息。
* Batch update products.
*
* @param array $products
* @access public
* @return array
*/
public function batchUpdate(array $products): array
{
if(empty($products)) return array();
/* 初始化变量。*/
$oldProducts = $this->getByIdList(array_keys($products));
$allChanges = array();
$updateViewProducts = array();
$unlinkProducts = array();
$linkProducts = array();
/* 根据产品ID循环更新的产品信息. */
$this->loadModel('personnel');
$this->lang->error->unique = $this->lang->error->repeat;
foreach($products as $productID => $product)
{
$oldProduct = $oldProducts[$productID];
$programID = (int)zget($product, 'program', $oldProduct->program);
$result = $this->productTao->doUpdate($product, $productID, $programID);
if(!$result) return array('result' => 'fail', 'message' => 'product#' . $productID . dao::getError(true));
/* When acl is open, white list set empty. */
if($product->acl == 'open') $this->personnel->updateWhitelist(array(), 'product', $productID);
/* 如果产品类型或权限有修改,则标记该产品,用做后面处理。*/
if($oldProduct->acl != $product->acl and $product->acl != 'open') $updateViewProducts[] = $productID;
if($product->type == 'normal' and $oldProduct->type != 'normal') $unlinkProducts[] = $productID;
if($product->type != 'normal' and $oldProduct->type == 'normal') $linkProducts[] = $productID;
$allChanges[$productID] = common::createChanges($oldProduct, $product);
}
/* 对标记的产品,批量做对应的后续处理。*/
if(!empty($updateViewProducts)) $this->loadModel('user')->updateUserView($updateViewProducts, 'product');
if(!empty($unlinkProducts)) $this->loadModel('branch')->unlinkBranch4Project($unlinkProducts);
if(!empty($linkProducts)) $this->loadModel('branch')->linkBranch4Project($linkProducts);
/* Save actions. */
$this->loadModel('action');
foreach($allChanges as $productID => $changes)
{
if(empty($changes)) continue;
$actionID = $this->action->create('product', $productID, 'Edited');
$this->action->logHistory($actionID, $changes);
}
return $allChanges;
}
/**
* 关闭产品。
* Close product.
*
* @param int $productID
* @param object $product must have status field.
* @param string|false $comment
* @access public
* @return array|false
*/
public function close(int $productID, object $product, string|false $comment = ''): array|false
{
$oldProduct = $this->getByID($productID);
if(empty($product)) return false;
$this->dao->update(TABLE_PRODUCT)->data($product)->autoCheck()
->checkFlow()
->where('id')->eq($productID)
->exec();
if(dao::isError()) return false;
$changes = common::createChanges($oldProduct, $product);
if(!empty($comment) or !empty($changes))
{
$actionID = $this->loadModel('action')->create('product', $productID, 'Closed', $comment);
$this->action->logHistory($actionID, $changes);
}
return $changes;
}
/**
* 激活产品。
* Activate a product.
*
* @param int $productID
* @param object $product must have status field.
* @param string|false $comment
* @access public
* @return array|false
*/
public function activate(int $productID, object $product, string|false $comment = ''): array|false
{
$oldProduct = $this->getByID($productID);
if(empty($product)) return false;
$this->dao->update(TABLE_PRODUCT)->data($product)->autoCheck()
->checkFlow()
->where('id')->eq($productID)
->exec();
if(dao::isError()) return false;
$changes = common::createChanges($oldProduct, $product);
if(!empty($comment) or !empty($changes))
{
$actionID = $this->loadModel('action')->create('product', $productID, 'Activated', $comment);
$this->action->logHistory($actionID, $changes);
}
return $changes;
}
/**
* 更新排序。
* Sort order field.
*
* @param array $sortedIdList
* @access public
* @return void
*/
public function updateOrder(array $sortedIdList): void
{
/* Remove programID. */
$sortedIdList = array_values(array_filter(array_map(function($id){return (is_numeric($id) and $id > 0) ? $id : null;}, $sortedIdList)));
if(empty($sortedIdList)) return;
/* Get the list of products before sorting. */
$products = $this->dao->select('`order`, id')->from(TABLE_PRODUCT)->where('id')->in($sortedIdList)->orderBy('order_asc')->fetchPairs('order', 'id');
/* Update order by sorted id list. */
foreach($products as $order => $id)
{
$newID = array_shift($sortedIdList);
if($id == $newID) continue;
$this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($order)->where('id')->eq($newID)->exec();
}
}
/**
* 添加或更新产品线。
* Add or update product line.
*
* @param array $lines
* @access public
* @return array
*/
public function manageLine(array $lines): array
{
/* Init product line object. */
$line = new stdclass();
$line->type = 'line';
$line->parent = 0;
$line->grade = 1;
/* Get the max order number. */
$maxOrder = (int)$this->dao->select("`order`")->from(TABLE_MODULE)->where('type')->eq('line')->orderBy('`order`_desc')->limit(1)->fetch('order');
foreach($lines as $programID => $lineNameList)
{
foreach($lineNameList as $lineID => $lineName)
{
$isInsert = is_numeric($lineID);
if(!$isInsert) $lineID = str_replace('id', '', $lineID);
/* Build product line data. */
$line->name = strip_tags(trim($lineName));
$line->root = $programID;
if($isInsert)
{
$maxOrder += 10; //Reserve for extension. Increment the order number by 10.
$line->order = $maxOrder;
/* Insert product line. */
$this->dao->insert(TABLE_MODULE)->data($line)->exec();
$lineID = $this->dao->lastInsertID();
/* Compute product line path and update it. */
$path = ",$lineID,";
$this->dao->update(TABLE_MODULE)->set('path')->eq($path)->where('id')->eq($lineID)->exec();
}
else
{
unset($line->order);
$this->dao->update(TABLE_MODULE)->data($line)->where('id')->eq($lineID)->exec();
}
if(!dao::isError()) $this->productTao->syncProgramToProduct($programID, (int)$lineID);
}
}
if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError());
return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => true);
}
/**
* 获取需求列表。
* Get stories.
*
* @param int|array $productID
* @param string $branch
* @param string $browseType bymodule|unclosed|allstory|assignedtome|openedbyme|reviewbyme|draftstory|reviewedbyme|assignedbyme|closedbyme|activestory|changingstory|reviewingstory|willclose|closedstory
* @param int $queryID
* @param int $moduleID
* @param string $type requirement|story
* @param string $sort
* @param object $pager
* @access public
* @return array
*/
public function getStories(int|array $productID, string $branch, string $browseType, int $queryID, int $moduleID, string $type = 'story', string $sort = 'id_desc', ?object $pager = null): array
{
if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getStories();
/* Set modules and browse type. */
$modules = $moduleID ? $this->loadModel('tree')->getAllChildID($moduleID) : '0';
$browseType = $browseType == 'bybranch' ? 'bymodule' : $browseType;
$browseType = ($browseType == 'bymodule' and $this->session->storyBrowseType and $this->session->storyBrowseType != 'bysearch') ? $this->session->storyBrowseType : $browseType;
/* Get stories by browseType. */
if(!isset($this->story)) $this->loadModel('story');
if($browseType == 'assignedtome') return $this->story->getByAssignedTo($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
if($browseType == 'openedbyme') return $this->story->getByOpenedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
if($browseType == 'reviewedbyme') return $this->story->getByReviewedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
if($browseType == 'reviewbyme') return $this->story->getByReviewBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
if($browseType == 'closedbyme') return $this->story->getByClosedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
if($browseType == 'assignedbyme') return $this->story->getByAssignedBy($productID, $branch, $modules, $this->app->user->account, $type, $sort, $pager);
if($browseType == 'unplan') return $this->story->getByPlan($productID, $queryID, $modules, '', $type, $sort, $pager);
if($browseType == 'bymodule') return $this->story->getProductStories($productID, $branch, $modules, 'all', $type, $sort, true, '', $pager);
if($browseType == 'bysearch') return $this->story->getBySearch($productID, $branch, $queryID, $sort, 0, $type, '', '', $pager);
if($browseType == 'willclose') return $this->story->get2BeClosed($productID, $branch, $modules, $type, $sort, $pager);
if($browseType == 'fromfeedback') return $this->story->getFeedbackStories($productID, $branch, $modules, $type, $sort, $pager);
/* all or allstory. */
if(strpos($browseType, 'all') !== false) return $this->story->getProductStories($productID, $branch, $modules, 'all', $type, $sort, true, '', $pager);
if($browseType == 'unclosed')
{
$unclosedStatus = $this->lang->story->statusList;
unset($unclosedStatus['closed']);
$stories = $this->story->getProductStories($productID, $branch, $modules, array_keys($unclosedStatus), $type, $sort, true, '', $pager);
foreach($stories as $storyID => $story)
{
if($story->type != 'story' && $story->stage == 'closed') unset($stories[$storyID]);
}
$this->app->loadClass('pager', true);
$pager = new pager(count($stories), $pager ? $pager->recPerPage : 20, $pager ? $pager->pageID : 1);
return $stories ? current(array_chunk($stories, $pager->recPerPage, true)) : array();
}
/* Set default function called, when browseType is (draftstory, activestory, changingstory, reviewingstory, closedstory, developingstory, launchedstory). */
return $this->story->getByStatus($productID, $branch, $modules, substr($browseType, 0, -5), $type, $sort, $pager);
}
/**
* 批量获取需求的阶段数据。
* Batch get story stage.
*
* @param array $stories.
* @access public
* @return array
*/
public function batchGetStoryStage(array $stories): array
{
/* Set story id list. */
$storyIdList = array();
foreach($stories as $story) $storyIdList[$story->id] = $story->id;
return $this->loadModel('story')->batchGetStoryStage($storyIdList);
}
/**
* 在需求列表页面构建搜索表单。
* Build search form for story list page.
*
* @param int $productID
* @param array $products
* @param int $queryID
* @param int $actionURL
* @param string $storyType
* @param string $branch
* @param int $projectID
* @access public
* @return void
*/
public function buildSearchForm(int $productID, array $products, int $queryID, string $actionURL, string $storyType = 'story', string $branch = '', int $projectID = 0): void
{
$searchConfig = $this->config->product->search;
$searchConfig['queryID'] = $queryID;
$searchConfig['actionURL'] = $actionURL;
/* Get product data. */
if(empty($productID) && empty($products))
{
$products = $this->getPairs('all', 0, '', 'all');
$productID = key($products);
$showAll = true;
}
$product = ($this->app->tab == 'project' && empty($productID)) || !empty($showAll) ? $products : array();
if(empty($product) && isset($products[$productID])) $product = array($productID => $products[$productID]);
$searchConfig['params']['product']['values'] = array('' => '') + $product + array('all' => $this->lang->product->allProduct);
/* Get module data. */
$projectID = ($this->app->tab == 'project' && empty($projectID)) ? $this->session->project : $projectID;
$searchConfig['params']['module']['values'] = empty($showAll) ? $this->productTao->getModulesForSearchForm($productID, $products, $branch, (int)$projectID) : $this->loadModel('tree')->getAllModulePairs('story');
$gradePairs = $this->loadModel('story')->getGradePairs($storyType, 'all');
if($projectID || $storyType == 'all')
{
$project = $this->loadModel('project')->fetchById($projectID);
$gradePairs = array();
$gradeList = $this->loadModel('story')->getGradeList('');
$storyTypes = isset($project->storyType) ? $project->storyType : 'epic,story,requirement';
foreach($gradeList as $grade)
{
if(strpos($storyTypes, $grade->type) === false) continue;
$key = (string)$grade->type . (string)$grade->grade;
$gradePairs[$key] = $grade->name;
}
asort($gradePairs);
$this->lang->story->title = $this->lang->story->name;
}
elseif($storyType != 'story')
{
$replacement = $storyType == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon;
$this->lang->story->title = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->title);
$this->lang->story->create = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->create);
$searchConfig['fields']['title'] = $this->lang->story->title;
}
$searchConfig['params']['grade']['values'] = $gradePairs;
if($this->config->edition == 'ipd') $searchConfig['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID);
/* Get product plan data. */
$productIdList = ($this->app->tab == 'project' && empty($productID)) || !empty($showAll) ? array_keys($products) : array($productID);
$branchParam = ($this->app->tab == 'project' && empty($productID)) || !empty($showAll) ? '' : $branch;
$searchConfig['params']['plan']['values'] = $this->loadModel('productplan')->getPairs($productIdList, (empty($branchParam) || $branchParam == 'all') ? '' : $branchParam);
/* Get branch data. */
if($productID && empty($showAll))
{
$productInfo = $this->getByID($productID);
if(!empty($productInfo->shadow))
{
unset($searchConfig['fields']['product']);
unset($searchConfig['params']['product']);
}
if($productInfo->type == 'normal' || $this->app->tab == 'assetlib')
{
unset($searchConfig['fields']['branch']);
unset($searchConfig['params']['branch']);
}
else
{
$searchConfig['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productInfo->type]);
$searchConfig['params']['branch']['values'] = array('' => '', '0' => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($productID, 'noempty');
}
}
else
{
unset($searchConfig['fields']['branch']);
unset($searchConfig['params']['branch']);
}
/* Reset the stage list for epic and requirement. */
if($storyType != 'all')
{
$this->app->loadLang($storyType);
$searchConfig['params']['stage']['values'] = $this->lang->$storyType->stageList;
}
$this->loadModel('search')->setSearchParams($searchConfig);
}
/**
* 在产品列表页面构建搜索表单。
* Build search form for product list page.
*
* @param int $queryID
* @param string $actionURL
* @access public
* @return void
*/
public function buildProductSearchForm(int $queryID, string $actionURL): void
{
$this->config->product->all->search['queryID'] = $queryID;
$this->config->product->all->search['actionURL'] = $actionURL;
if(in_array($this->config->systemMode, array('ALM', 'PLM')))
{
if(helper::hasFeature('program')) $this->config->product->all->search['params']['program']['values'] = $this->loadModel('program')->getTopPairs('noclosed');
$this->config->product->all->search['params']['line']['values'] = $this->getLinePairs();
}
$this->loadModel('search')->setSearchParams($this->config->product->all->search);
}
/**
* 根据产品列表获取关联的项目列表。
* Get project pairs by product.
*
* @param array $productIdList
* @access public
* @return array
*/
public function getProjectPairsByProductIdList(array $productIdList): array
{
return $this->dao->select('t2.id, t2.name')->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
->where('t1.product')->in($productIdList)
->andWhere('t2.type')->eq('project')
->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
->andWhere('t2.deleted')->eq('0')
->orderBy('t1.product,order_asc')
->fetchPairs('id', 'name');
}
/**
* 获取关联某产品的项目键值对列表。
* Get project pairs by product.
*
* @param int $productID
* @param string $branch 'all'|''|int
* @param string|array $appendProject
* @param string $status all|noclosed|closed
* @param string $param multiple|
* @access public
* @return array
*/
public function getProjectPairsByProduct(int $productID, string $branch = '0', string|array $appendProject = '', string $status = '', string $param = ''): array
{
$product = $this->getByID($productID);
if(empty($product)) return array();
$appendProject = $this->productTao->formatAppendParam($appendProject);
return $this->dao->select('t2.id, t2.name')->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')
->andWhere('t2.deleted')->eq('0')
->beginIF($status == 'noclosed')->andWhere('t2.status')->ne('closed')->fi()
->beginIF($status == 'closed')->andWhere('t2.status')->eq('closed')->fi()
->beginIF(strpos($param, 'multiple') !== false)->andWhere('t2.multiple')->ne('0')->fi()
->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->projects)->fi()
->beginIF($product->type != 'normal' and $branch !== '' and $branch != 'all')->andWhere('t1.branch')->in($branch)->fi()
->markRight(1)
->beginIF($appendProject)->orWhere('t2.id')->in($appendProject)->fi()
->orderBy('order_asc')
->fetchPairs('id', 'name');
}
/**
* 获取与该产品关联的项目列表。
* Get project list by product.
*
* @param int $productID
* @param string $browseType all|undone|wait|doing|done
* @param string $branch
* @param bool $involved $this->cookie->involved or $involved
* @param string $orderBy
* @param object|null $pager
* @access public
* @return array
*/
public function getProjectListByProduct(int $productID, string $browseType = 'all', string $branch = '0', bool $involved = false, string $orderBy = 'order_desc', ?object $pager = null): array
{
$branch = $branch ? $branch : '0';
if(!$involved) $projectList = $this->productTao->fetchAllProductProjects($productID, $browseType, $branch, $orderBy, $pager);
if($involved) $projectList = $this->productTao->fetchInvolvedProductProjects($productID, $browseType, $branch, $orderBy, $pager);
/* Determine how to display the name of the program. */
$programList = $this->loadModel('program')->getParentPairs('', 'all');
foreach($projectList as $id => $project)
{
$programName = $project->parent ? zget($programList, $project->parent, '') : '';
$projectList[$id]->programName = preg_replace('/\//', '', $programName, 1);
}
return $projectList;
}
/**
* 根据产品,获取与该产品关联的项目的统计信息。
* Get project stats by product.
*
* @param int $productID
* @param string $browseType
* @param int $branch
* @param bool $involved $this->cookie->involved or $involved
* @param string $orderBy
* @param object $pager
* @access public
* @return int[]
*/
public function getProjectStatsByProduct(int $productID, string $browseType = 'all', string $branch = '0', bool $involved = false, string $orderBy = 'order_desc', ?object $pager = null): array
{
$projects = $this->getProjectListByProduct($productID, $browseType, $branch, $involved, $orderBy, $pager);
if(empty($projects)) return array();
return $this->program->appendStatToProjects($projects);
}
/**
* 获取关联了某产品的执行列表。
* Get executions by product and project.
*
* @param int $productID
* @param string $branch
* @param int $projectID
* @param string $mode stagefilter|noclosed|multiple
* @param array $unAllowedStage
* @access public
* @return array
*/
public function getExecutionPairsByProduct(int $productID, string $branch = '', int $projectID = 0, string $mode = '', array $unAllowedStage = array()): array
{
if(empty($productID)) return array();
/* Determine executions order. */
$projects = $this->loadModel('project')->getByIdList(array($projectID));
$hasWaterfall = in_array('waterfall', helper::arrayColumn($projects, 'model'));
$hasWaterfallplus = in_array('waterfallplus', helper::arrayColumn($projects, 'model'));
$waterfallOrderBy = ($hasWaterfall || $hasWaterfallplus) ? 't2.begin_asc,t2.id_asc' : 't2.begin_desc,t2.id_desc';
$executions = $this->dao->select('t2.id,t2.name,t2.project,t2.grade,t2.path,t2.parent,t2.attribute,t2.multiple,t3.name as projectName')->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project = t2.id')
->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project = t3.id')
->where('t1.product')->eq($productID)
->andWhere('t2.type')->in('sprint,kanban,stage')
->beginIF($projectID)->andWhere('t2.project')->in($projectID)->fi()
->beginIF($branch !== '' and strpos($branch, 'all') === false)->andWhere('t1.branch')->in($branch)->fi()
->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi()
->beginIF(strpos($mode, 'noclosed') !== false)->andWhere('t2.status')->ne('closed')->fi()
->beginIF(strpos($mode, 'multiple') !== false)->andWhere('t2.multiple')->eq('1')->fi()
->beginIF(!empty($unAllowedStage))->andWhere('t2.attribute')->notIn($unAllowedStage)->fi() //针对瀑布项目,过滤掉不允许的阶段
->andWhere('t2.deleted')->eq('0')
->orderBy($waterfallOrderBy)
->fetchAll('id');
if(empty($executions)) return array();
if($projectID) $executions = $this->loadModel('execution')->resetExecutionSorts($executions);
return $this->productTao->buildExecutionPairs($executions, $mode, empty($projectID));
}
/**
* 获取产品路线图数据。
* Get roadmap of product.
*
* @param int $productID
* @param string $branch all|0|1
* @param int $count
* @access public
* @return array
*/
public function getRoadmap(int $productID, string $branch = '0', int $count = 0): array
{
/* Get group roadmap data. */
list($groupRoadmap, $return) = $this->getGroupRoadmapData($productID, $branch, $count);
if($return) return $groupRoadmap;
/* Get last 5 roadmap. */
$lastKeys = array_slice(array_keys($groupRoadmap), 0, 5);
$lastRoadmap = array();
$lastRoadmap['total'] = 0;
foreach($lastKeys as $key)
{
if($key == '2030')
{
$lastRoadmap[$this->lang->productplan->future] = $groupRoadmap[$key];
}
else
{
$lastRoadmap[$key] = $groupRoadmap[$key];
}
foreach($groupRoadmap[$key] as $branchRoadmap) $lastRoadmap['total'] += (count($branchRoadmap, 1) - count($branchRoadmap));
}
return $lastRoadmap;
}
/**
* Process roadmap.
*
* @param array $roadmap
* @param int $branch
* @access public
* @return array
*/
public function processRoadmap(array $roadmapGroups, string $branch): array
{
$newRoadmap = array();
foreach($roadmapGroups as $branchRoadmap)
{
foreach($branchRoadmap as $branchID => $roadmap)
{
if($branch != $branchID) continue;
foreach($roadmap as $roadmapItem) $newRoadmap[] = $roadmapItem;
}
}
krsort($newRoadmap);
return $newRoadmap;
}
/**
* 使用产品ID获取产品统计信息。
* Get product stat data by product ID.
*
* @param int $productID
* @param string $storyType
* @access public
* @return object|bool
*/
public function getStatByID($productID, $storyType = 'story')
{
/* Check privilege. */
if(!$this->checkPriv($productID))
{
$this->accessDenied($this->lang->product->accessDenied);
return false;
}
/* Get product. */
$product = $this->getByID($productID);
if(empty($product)) return false;
/* Attach statistic data. */
$product->stories = $this->getStoryStatusCountByID($productID, $storyType);
$product->plans = $this->productTao->getStatCountByID(TABLE_PRODUCTPLAN, $productID);
$product->builds = $this->productTao->getStatCountByID(TABLE_BUILD, $productID);
$product->cases = $this->productTao->getStatCountByID(TABLE_CASE, $productID);
$product->bugs = $this->productTao->getStatCountByID(TABLE_BUG, $productID);
$product->docs = $this->productTao->getStatCountByID(TABLE_DOC, $productID);
$product->releases = $this->productTao->getStatCountByID(TABLE_RELEASE, $productID);
$product->projects = $this->productTao->getStatCountByID(TABLE_PROJECTPRODUCT, $productID);
$product->executions = $this->productTao->getStatCountByID('executions', $productID);
$product->progress = $this->productTao->getStatCountByID('progress', $productID);
$product->storyDeliveryRate = 0;
$storyDeliveryRate = $this->loadModel('metric')->getResultByCode('rate_of_delivery_story_in_product', array('product' => $productID));
if($storyDeliveryRate)
{
$storyDeliveryRate = current($storyDeliveryRate);
$product->storyDeliveryRate = $storyDeliveryRate['value'] * 100;
}
return $product;
}
/**
* 获取产品统计信息。
* Get product stats.
*
* @param array $productIdList
* @param string $orderBy order_asc|program_asc
* @param object|null $pager
* @param string $storyType requirement|story
* @param int $programID
* @access public
* @return array
*/
public function getStats(array $productIdList, string $orderBy = 'order_asc', ?object $pager = null, string $storyType = 'story', int $programID = 0): array
{
/* Call the getProductStats method of the tutorial module if you are in tutorial mode.*/
if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProductStats();
if(empty($productIdList)) return array();
$stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager);
$latestReleases = $this->productTao->getStatisticByType($productIdList, 'latestReleases');
$projectCountPairs = $this->productTao->getProjectCountPairs($productIdList);
$executionCountPairs = $this->productTao->getExecutionCountPairs($productIdList);
$coveragePairs = $this->getCaseCoveragePairs($productIdList);
foreach($stats as $product)
{
$product->projects = zget($projectCountPairs, $product->id, 0);
$product->executions = zget($executionCountPairs, $product->id, 0);
$product->coverage = zget($coveragePairs, $product->id, 0);
$latestRelease = isset($latestReleases[$product->id]) ? $latestReleases[$product->id][0] : null;
$product->latestRelease = $latestRelease ? $latestRelease->name : '';
$product->latestReleaseDate = $latestRelease ? $latestRelease->date : '';
}
return $stats;
}
/**
* 获取看板页面的产品统计信息。
* Get stats for product kanban.
*
* @param string $browseType
* @access public
* @return array
*/
public function getStats4Kanban($browseType = 'my'): array
{
/* Get base data. */
$productList = $this->getList();
foreach($productList as $id => $product)
{
if($browseType == 'my')
{
if($product->PO != $this->app->user->account) unset($productList[$id]);
}
else
{
if($product->PO == $this->app->user->account) unset($productList[$id]);
}
}
$projectList = $this->loadModel('program')->getProjectStats(0, 'doing');
$productIdList = array_keys($productList);
$projectProduct = $this->productTao->getProjectProductList($productList);
$planList = $this->productTao->getPlanList($productIdList);
$executionList = $this->productTao->getExecutionList(array_keys($projectList), $productIdList);
$releaseList = $this->productTao->getReleaseList($productIdList);
/* Filter latest executions. */
$projectLatestExecutions = array();
$latestExecutionList = array();
$today = helper::today();
foreach($executionList as $projectID => $executions)
{
foreach($executions as &$execution)
{
/* Calculate delayed execution. */
if($execution->status != 'done' && $execution->status != 'closed' && $execution->status != 'suspended')
{
$delay = helper::diffDate($today, $execution->end);
if($delay > 0) $execution->delay = $delay;
}
}
/* Used to computer execution progress. */
$latestExecutionList[key($executions)] = current($executions);
/* Used for display in page. */
$projectLatestExecutions[$projectID] = current($executions);
}
/* Build result. */
$statsData = array($productList, $planList, $projectList, $projectProduct, $projectLatestExecutions, $releaseList);
/* Convert predefined HTML entities to characters. */
$statsData = $this->convertHtmlSpecialChars($statsData);
return $statsData;
}
/**
* 获取项目集下的产品线。
* Get product line pairs by program.
*
* @param int|array $programID
* @param bool $filterRoot
* @access public
* @return string[]
*/
public function getLinePairs(int|array $programIdList = 0, bool $filterRoot = false): array
{
return $this->dao->select('id,name')->from(TABLE_MODULE)
->where('type')->eq('line')
->beginIF(helper::hasFeature('program') && ($programIdList || $filterRoot))->andWhere('root')->in($programIdList)->fi()
->andWhere('deleted')->eq(0)
->orderBy('order_asc')
->fetchPairs('id', 'name');
}
/*
* 根据项目集编号获取产品线列表。
* Get product lines by program id list.
*
* @param array $programIdList
* @access public
* @return object[]
*/
public function getLines(array $programIdList = array()): array
{
return $this->dao->select('*')->from(TABLE_MODULE)
->where('type')->eq('line')
->beginIF($programIdList)->andWhere('root')->in($programIdList)->fi()
->andWhere('deleted')->eq(0)
->orderBy('`order`')
->fetchAll();
}
/**
* 获取产品需求的摘要信息。
* Get the summary of product's stories.
*
* @param array $stories
* @param string $storyType story|requirement
* @access public
* @return string
*/
public function summary(array $stories, string $storyType = 'story'): string
{
$totalEstimate = 0.0;
$storyIdList = array();
$rateCount = 0;
$total = 0;
$SRTotal = 0;
$URTotal = 0;
$ERTotal = 0;
$idList = array();
foreach($stories as $story) $idList[] = $story->id;
$childTypes = $this->dao->select('parent, type')->from(TABLE_STORY)->where('parent')->in($idList)->andWhere('deleted')->eq('0')->fetchGroup('parent', 'type');
foreach($stories as $story)
{
$total ++;
if($story->type == 'story') $SRTotal += 1;
if($story->type == 'requirement') $URTotal += 1;
if($story->type == 'epic') $ERTotal += 1;
/* 产品软需列表仅统计叶子需求的规模。
如果是产品用需、业需列表,则追加统计子需求类型和父需求不一样的父需求规模。
如果是项目需求列表,则只统计叶子需求的规模。
*/
$isLeafStory = !isset($childTypes[$story->id]) || (isset($childTypes[$story->id]) && !isset($childTypes[$story->id][$story->type]));
if(($storyType == $story->type && $isLeafStory) || ($storyType == 'all' && $story->isParent == '0'))
{
$totalEstimate += $story->estimate;
$story->needSummaryEstimate = true;
}
if($story->type != 'story') continue;
if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed'))))
{
$storyIdList[] = $story->id;
$rateCount ++;
}
}
$casesCount = count($this->productTao->filterNoCasesStory($storyIdList));
$rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 4);
if($storyType == 'all') return sprintf($this->lang->product->allSummary, $total, $totalEstimate, $rate * 100 . "%");
if($storyType == 'epic') return sprintf($this->lang->product->epicSummary, $ERTotal, $totalEstimate);
if($storyType == 'requirement') return sprintf($this->lang->product->requirementSummary, $URTotal, $totalEstimate);
return sprintf($this->lang->product->storySummary, $SRTotal, $totalEstimate, $rate * 100 . "%");
}
/**
* 判断某操作是否可点击。
* Judge an action is clickable.
*
* @param object $product
* @param string $action
* @access public
* @return bool
*/
public static function isClickable(object $product, string $action): bool
{
$action = strtolower($action);
if($action == 'close') return $product->status != 'closed';
if($action == 'activate') return $product->status == 'closed';
return true;
}
/**
* Setting parameters for link.
*
* @param string $module
* @param string $link
* @param int $projectID
* @param int $productID
* @access public
* @return void
*/
public function setParamsForLink(string $module, string $link, int $projectID, int $productID): string
{
return $module == 'programplan' ? sprintf($link, $projectID, $productID) : sprintf($link, $productID);
}
/**
* 将预定义的HTML实体转换为字符。
* Convert predefined HTML entities to characters.
*
* @param array $statsData
* @return array
*/
public function convertHtmlSpecialChars(array $statsData): array
{
if(empty($statsData)) return array();
foreach($statsData as $key => $data)
{
if(empty($data)) continue;
if($key === 'productList' || $key === 'projectList') array_map(function($item){return $item->name = htmlspecialchars_decode($item->name, ENT_QUOTES);}, $data);
if($key === 'planList')
{
foreach($data as $plan)
{
if(empty($plan)) continue;
array_map(function($planItem){return $planItem->title = htmlspecialchars_decode($planItem->title, ENT_QUOTES);}, $plan);
}
}
}
return $statsData;
}
/**
* 把访问的产品ID等状态信息保存到session和cookie中。
* Save the product id user last visited to session and cookie.
*
* @param int $productID
* @param array $products
* @access public
* @return int
*/
public function checkAccess(int $productID, array $products): int
{
if(commonModel::isTutorialMode()) return $productID;
$productID = $this->getAccessibleProductID($productID, $products);
$this->session->set('product', $productID, $this->app->tab);
/* If preProductID changed, then reset preBranch. */
if($this->cookie->preProductID != $this->session->product)
{
$this->cookie->set('preBranch', 0);
helper::setcookie('preBranch', '0');
}
helper::setcookie('preProductID', (string)$productID);
return $productID;
}
/**
* 获取可访问的产品ID。
* Get accessible product ID.
*
* @param int $productID
* @param array $products
* @access protected
* @return int
*/
protected function getAccessibleProductID(int $productID, array $products): int
{
if(empty($productID))
{
if($this->cookie->preProductID and isset($products[$this->cookie->preProductID])) $productID = (int)$this->cookie->preProductID;
if(empty($productID) && empty($this->session->product)) $productID = (int)key($products);
}
/* 产品ID已经被删除不存在于产品列表中。*/
/* Product ID does not exist in products list, it may be deleted. */
/* Confirm if product exist. */
$product = $this->getByID($productID);
if(empty($product) or $product->deleted == 1) $productID = (int)key($products);
/* If product is invisible for current user, respond access denied message. */
if($productID && !$this->checkPriv($productID))
{
$productID = (int)key($products);
$this->accessDenied($this->lang->product->accessDenied);
}
return (int)$productID;
}
/**
* 输出访问被拒绝提示信息。
* Show accessDenied response.
*
* @param string $tips
* @access public
* @return void
*/
public function accessDenied(string $tips): bool
{
if(commonModel::isTutorialMode()) return true;
return $this->app->control->sendError($tips, helper::createLink('product', 'all'));
}
/**
* 设置导航菜单。
* Set menu.
*
* @param int $productID
* @param string|int $branch all|''|int
* @param string $extra requirement|story
* @access public
* @return bool
*/
public function setMenu(int $productID = 0, string|int $branch = '', string $extra = ''): bool
{
if(!commonModel::isTutorialMode() and $productID != 0 and !$this->checkPriv($productID))
{
$this->accessDenied($this->lang->product->accessDenied);
return true;
}
/* 用真实数据替换导航配置的占位符,并删除无用配置项。 */
$params = array('branch' => $branch);
common::setMenuVars('product', $productID, $params);
if(strpos($extra, 'requirement') !== false) unset($this->lang->product->moreSelects['willclose']);
$product = $this->getByID($productID);
if(!$product) return false;
/* 设置1.5级导航数据。*/
$this->lang->switcherMenu = $this->getSwitcher($productID, $extra, $branch);
/* 设置导航中分支的显示数据。*/
/* 如果产品类型是正常的,隐藏导航中分支的显示。*/
if($product->type == 'normal')
{
unset($this->lang->product->menu->settings['subMenu']->branch);
return true;
}
/* 如果产品类型是多分支、多平台的,将真实数据替换@branch@匹配符。*/
$branchLink = $this->lang->product->menu->settings['subMenu']->branch['link'];
$this->lang->product->menu->settings['subMenu']->branch['link'] = str_replace('@branch@', $this->lang->product->branchName[$product->type], $branchLink);
$this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]);
return true;
}
/**
* 删除产品。
* Delete product by ID.
*
* @param int $productID
* @access protected
* @return bool
*/
protected function deleteByID(int $productID): bool
{
$this->delete(TABLE_PRODUCT, $productID);
$this->dao->update(TABLE_DOCLIB)->set('deleted')->eq(1)->where('product')->eq($productID)->exec();
$this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('product')->eq($productID)->exec();
return !dao::isError();
}
/**
* 格式化数据表格输出数据。
* Format data for list.
*
* @param object $product
* @param array $users
* @access public
* @return object
*/
public function formatDataForList(object $product, array $users): object
{
$product->type = 'product';
$product->productLine = $product->lineName;
$product->PO = !empty($product->PO) ? zget($users, $product->PO) : '';
$product->PMT = !empty($product->PMT) ? zget($users, $product->PMT) : '';
$product->createdBy = !empty($product->createdBy) ? zget($users, $product->createdBy) : '';
$product->createdDate = substr($product->createdDate, 0, 10);
if($this->config->vision == 'or') return $product;
$product->testCaseCoverage = $product->coverage;
$product->epicCompleteRate = $product->totalEpics == 0 ? 0 : round($product->finishedEpics / $product->totalEpics, 3) * 100;
$product->requirementCompleteRate = $product->totalRequirements == 0 ? 0 : round($product->finishedRequirements / $product->totalRequirements, 3) * 100;
$product->storyCompleteRate = $product->totalStories == 0 ? 0 : round($product->finishedStories / $product->totalStories, 3) * 100;
$product->bugFixedRate = ($product->unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100;
return $product;
}
/**
* 构建路线图页面数据。
* Build roadmap for UI.
*
* @param array $roadmaps
* @param int $branchKey
* @access public
* @return array
*/
public function buildRoadmapForUI(array $roadmaps, int $branchKey = 0): array
{
unset($roadmaps['total']);
$data = array();
foreach($roadmaps as $year => $yearRoadmaps)
{
if(!isset($yearRoadmaps[$branchKey])) continue;
foreach($yearRoadmaps[$branchKey] as $roadmapData)
{
$yearNodes = array();
foreach($roadmapData as $roadmap)
{
$isPlan = (isset($roadmap->begin) && isset($roadmap->end));
$moduleName = $isPlan ? 'productplan' : 'release';
$node = array();
$node['href'] = common::hasPriv($moduleName, 'view') ? helper::createLink($moduleName, 'view', "id={$roadmap->id}") : '###';
$node['version'] = $isPlan ? $roadmap->title : $roadmap->name;
$node['date'] = $isPlan ? $roadmap->begin . '~' . $roadmap->end : (empty($roadmap->releasedDate) ? $roadmap->date : $roadmap->releasedDate);
$node['marker'] = !empty($roadmap->marker);
$yearNodes[] = $node;
}
$count = count($yearNodes);
$cols = 5;
$lines = array();
for($i = 0; $i * $cols < $count; $i ++) $lines[] = array_slice($yearNodes, $i * $cols, $cols);
$data[$year] = array_reverse($lines);
}
}
return $data;
}
/**
* 删除一个产品线。
* Delete a product line.
*
* @param int $lineID
* @access public
* @return bool
*/
public function deleteLine(int $lineID): bool
{
$this->dao->update(TABLE_MODULE)->set('deleted')->eq(1)->where('id')->eq($lineID)->exec();
$this->loadModel('action')->create('productline', $lineID, 'deleted', '', ACTIONMODEL::CAN_UNDELETED);
$this->dao->update(TABLE_PRODUCT)->set('line')->eq('0')->where('line')->eq($lineID)->exec();
return !dao::isError();
}
/**
* 获取产品列表。
* Get products list.
*
* @param int $programID
* @param string $status all|noclosed|involved|review|normal|closed
* @param int $limit
* @param int $line
* @param string|int $shadow all | 0 | 1
* @param string $fields * or fieldList, such as id,name,program
* @access protected
* @return array
*/
protected function getList(int $programID = 0, string $status = 'all', int $limit = 0, int $line = 0, string|int $shadow = 0, string $fields = '*'): array
{
$fields = explode(',', $fields);
$fields = trim(implode(',t1.', $fields), ',');
$programField = $programID ? '' : 't2.order,';
$programOrder = $programID ? '' : 't2.order_asc,';
return $this->dao->select("DISTINCT t1.{$fields},{$programField}t1.line,t1.order")->from(TABLE_PRODUCT)->alias('t1')
->beginIF(!$programID)->leftJoin(TABLE_PROGRAM)->alias('t2')->on('t1.program = t2.id')->fi() // 如果指定了项目集,则不需要关联项目集表。
->beginIF($status == 'involved')->leftJoin(TABLE_PROJECTPRODUCT)->alias('t3')->on('t3.product = t1.id')->fi()
->beginIF($status == 'involved')->leftJoin(TABLE_TEAM)->alias('t4')->on("t4.root = t3.project and t4.type='project'")->fi()
->where('t1.deleted')->eq(0)
->beginIF($shadow !== 'all')->andWhere('t1.shadow')->eq((int)$shadow)->fi()
->beginIF($programID)->andWhere('t1.program')->eq($programID)->fi()
->beginIF($line > 0)->andWhere('t1.line')->eq($line)->fi()
->beginIF(strpos($status, 'feedback') === false && !$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi()
->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)")
->beginIF(strpos($status, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi()
->beginIF(strpos($status, 'nowait') !== false)->andWhere('t1.status')->ne('wait')->fi()
->beginIF(strpos($status, '|') === false && !in_array($status, array('all', 'noclosed', 'involved', 'review'), true))->andWhere('t1.status')->in($status)->fi()
->beginIF($status == 'involved')
->andWhere('t1.PO', true)->eq($this->app->user->account)
->orWhere('t1.QD')->eq($this->app->user->account)
->orWhere('t1.RD')->eq($this->app->user->account)
->orWhere('t1.createdBy')->eq($this->app->user->account)
->orWhere('t4.account')->eq($this->app->user->account)
->markRight(1)
->fi()
->beginIF($status == 'review')
->andWhere("FIND_IN_SET('{$this->app->user->account}', t1.reviewers)")
->andWhere('t1.reviewStatus')->eq('doing')
->fi()
->filterTpl('skip')
->orderBy("{$programOrder}t1.line_desc, t1.order_asc")
->beginIF($limit > 0)->limit($limit)->fi()
->fetchAll('id');
}
/**
* 获取用于统计的产品列表。
* Get products list for statistic.
*
* @param int[] $productIdList
* @param bool $appendProgram
* @param string $orderBy
* @param object|null $pager
* @access public
* @return array
*/
public function getStatsProducts(array $productIdList, bool $appendProgram, string $orderBy, ?object $pager = null): array
{
if($orderBy == 'program_asc')
{
$products = $this->productTao->getPagerProductsWithProgramIn($productIdList, $pager);
}
else
{
$products = $this->productTao->getPagerProductsIn($productIdList, $pager, $orderBy);
}
/* Fetch product lines. */
$linePairs = $this->getLinePairs();
foreach($products as $product) $product->lineName = zget($linePairs, $product->line, '');
if(!$appendProgram) return $products;
$programIdList = array();
foreach($products as $product) $programIdList[] = $product->program;
$programs = $this->loadModel('program')->getBaseDataList(array_unique($programIdList));
$finishClosedStory = $this->dao->select('product, count(1) as finish')->from(TABLE_STORY)
->where('deleted')->eq(0)
->andWhere('status')->eq('closed')
->andWhere('closedReason')->eq('done')
->groupBy('product')
->fetchPairs('product', 'finish');
$launchedStory = $this->dao->select('product, count(1) as launched')->from(TABLE_STORY)
->where('deleted')->eq(0)
->andWhere('status')->eq('launched')
->groupBy('product')
->fetchPairs('product', 'launched');
$developingStory = $this->dao->select('product, count(1) as developing')->from(TABLE_STORY)
->where('deleted')->eq(0)
->andWhere('status')->eq('developing')
->groupBy('product')
->fetchPairs('product', 'developing');
foreach($products as $productID => $product)
{
$product->programName = isset($programs[$product->program]) ? $programs[$product->program]->name : '';
$product->programPM = isset($programs[$product->program]) ? $programs[$product->program]->PM : '';
$products[$productID]->finishClosedStories = isset($finishClosedStory[$productID]) ? $finishClosedStory[$productID] : 0;
$products[$productID]->launchedStories = isset($launchedStory[$productID]) ? $launchedStory[$productID] : 0;
$products[$productID]->developingStories = isset($developingStory[$productID]) ? $developingStory[$productID] : 0;
}
return $products;
}
/**
* 获取用于数据统计的研发需求和用户需求列表。
* Get dev stories and user requirements for statistics.
*
* @param array $productIdList
* @param string $storyType
* @access public
* @return array[]
*/
public function getStatsStoriesAndRequirements(array $productIdList, string $storyType): array
{
$this->loadModel('story');
$stories = $this->story->getStoriesCountByProductIDs($productIdList, 'story');
$requirements = $this->story->getStoriesCountByProductIDs($productIdList);
/* Padding the stories to sure all products have records. */
$defaultStory = array_keys($this->lang->story->statusList);
foreach($productIdList as $productID)
{
if(!isset($stories[$productID])) $stories[$productID] = $defaultStory;
if(!isset($requirements[$productID])) $requirements[$productID] = $defaultStory;
}
/* Collect count for each status of stories. */
foreach($stories as $key => $story)
{
foreach(array_keys($this->lang->story->statusList) as $status) $story[$status] = isset($story[$status]) ? $story[$status]->count : 0;
$stories[$key] = $story;
}
/* Collect count for each status of requirements. */
foreach($requirements as $key => $requirement)
{
foreach(array_keys($this->lang->story->statusList) as $status) $requirement[$status] = isset($requirement[$status]) ? $requirement[$status]->count : 0;
$requirements[$key] = $requirement;
}
/* Story type is 'requirement'. */
if($storyType == 'requirement') $stories = $requirements;
return array($stories, $requirements);
}
/**
* 获取多个产品关联需求用例覆盖率的键值对。
* Get K-V pairs of product ID and test case coverage.
*
* @param int[] $productIdList
* @access public
* @return array
*/
public function getCaseCoveragePairs(array $productIdList): array
{
if(empty($productIdList)) return array();
/* Get storie list by product ID list. */
$storyList = $this->loadModel('story')->getStoriesByProductIdList($productIdList, 'story', false);
/* Get case count of each story. */
$storyIdList = array();
$productStoryList = array();
foreach($storyList as $story)
{
$storyIdList[] = $story->id;
if(!isset($productStoryList[$story->product])) $productStoryList[$story->product] = array();
$productStoryList[$story->product][] = $story->id;
}
$caseCountPairs = $this->productTao->getCaseCountByProductIdList($productIdList);
/* Calculate coverage. */
$coveragePairs = array();
foreach($productStoryList as $productID => $list)
{
$total = count($list);
$totalCovered = 0;
foreach($list as $storyID)
{
if(isset($caseCountPairs[$storyID])) $totalCovered++;
}
$coveragePairs[$productID] = $total ? round($totalCovered * 100 / $total) : 0;
}
return $coveragePairs;
}
/**
* 从产品统计数据中统计项目集。
* Statistics program data from statistics data of product.
*
* @param array $productStats
* @access public
* @return array
*/
public function statisticProgram(array $productStats): array
{
if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProductStats(true);
$programStructure = array();
foreach($productStats as $product)
{
$programStructure[$product->program][$product->line]['products'][$product->id] = $product;
/* Generate line data. */
if($product->line && $this->config->vision != 'or')
{
$programStructure[$product->program][$product->line]['lineName'] = $product->lineName;
$programStructure[$product->program][$product->line] = $this->productTao->statisticProductData('line', $programStructure, $product);
}
/* Generate program data. */
if($product->program && $this->config->vision != 'or')
{
$programStructure[$product->program]['programName'] = $product->programName;
$programStructure[$product->program]['programPM'] = $product->programPM;
$programStructure[$product->program]['id'] = $product->program;
$programStructure[$product->program] = $this->productTao->statisticProductData('program', $programStructure, $product);
}
}
return $programStructure;
}
/**
* 通过产品ID查询产品关联需求的各状态统计总数。每一个需求状态对应一个数量。
* Get stories total count of each status by product ID. Every story status has a count value.
*
* @param int $productID
* @param string $storyType
* @access public
* @return object[]
*/
public function getStoryStatusCountByID(int $productID, string $storyType = 'story'): array
{
/* 通过产品ID获取每一个需求状态对应的数量。Get count of each story status of the product. */
$statusCountList = $this->loadModel('story')->getStoriesCountByProductID($productID, $storyType);
foreach($statusCountList as $status => $stat) $statusCountList[$status] = $stat->count;
/* 确保每一种需求状态都是有值的。Padding the stories to make sure all status have records. */
foreach(array_keys($this->lang->story->statusList) as $status)
{
if(!isset($statusCountList[$status])) $statusCountList[$status] = 0;
}
return $statusCountList;
}
/**
* 获取产品路线图的分组数据。
* Get group roadmap data of product.
*
* @param int $productID
* @param string $branch all|0|1
* @param int $count
* @access public
* @return [array, bool]
*/
public function getGroupRoadmapData(int $productID, string $branch, int $count): array
{
$roadmap = array();
$return = false;
/* Get product plans. */
$planList = $this->loadModel('productplan')->getList($productID, $branch);
/* Filter the valid plans, then get the ordered and parents plans. */
list($orderedPlans, $parentPlans) = $this->productTao->filterOrderedAndParentPlans($planList);
/* Get roadmaps of product plans. */
list($roadmap, $total, $return) = $this->getRoadmapOfPlans($orderedPlans, $parentPlans, $branch, $count);
if($return) return array($roadmap, $return);
/* Get roadmpas of product releases. */
$releases = $this->loadModel('release')->getList($productID, $branch);
list($roadmap, $subTotal, $return) = $this->getRoadmapOfReleases($roadmap, $releases, $branch, $count);
if($return) return array($roadmap, $return);
$total += $subTotal;
/* Re-group with branch ID. */
$groupRoadmap = array();
foreach($roadmap as $year => $branchRoadmap)
{
foreach($branchRoadmap as $branch => $roadmapItems) $groupRoadmap[$year][$branch][] = array_reverse($roadmapItems);
}
return array($groupRoadmap, $return);
}
/**
* 获取发布的路线图数据。
* Get roadmap of releases.
*
* @param array $roadmap
* @param array $parents
* @param string $branch
* @param int $count
* @access public
* @return [array, int, bool]
*/
public function getRoadmapOfReleases(array $roadmap, array $releases, string $branch, int $count): array
{
$total = 0;
$return = false;
$today = helper::today();
$orderedReleases = array();
/* Collect releases. */
foreach($releases as $release)
{
if(helper::isZeroDate($release->releasedDate) || $release->releasedDate > $today || $release->status != 'normal') continue;
$orderedReleases[$release->releasedDate][] = $release;
}
krsort($orderedReleases);
foreach($orderedReleases as $releases)
{
krsort($releases);
foreach($releases as $release)
{
$year = substr($release->date, 0, 4);
$branchIdList = trim($release->branch, ',');
$branchIdList = empty($branchIdList) ? array(0) : explode(',', $branchIdList);
$branchIdList = array_unique($branchIdList);
foreach($branchIdList as $branchID)
{
if($branchID === '') continue;
$roadmap[$year][$branchID][] = $release;
}
$total++;
/* Exceed required count .*/
if($count > 0 and $total >= $count)
{
$roadmap = $this->processRoadmap($roadmap, $branch);
$return = true;
return array($roadmap, $total, $return);
}
}
}
if($count > 0)
{
$roadmap = $this->processRoadmap($roadmap, $branch);
$return = true;
}
return array($roadmap, $total, $return);
}
/**
* 获取计划的路线图数据。
* Get roadmap of plans.
*
* @param array $orderedPlans
* @param array $parents
* @param string $branch
* @param int $count
* @access public
* @return [array, int, bool]
*/
public function getRoadmapOfPlans(array $orderedPlans, array $parents, string $branch, int $count): array
{
$return = false;
$total = 0;
$roadmap = array();
foreach($orderedPlans as $plans)
{
krsort($plans);
foreach($plans as $plan)
{
/* Attach parent plan. */
if($plan->parent > 0 and isset($parents[$plan->parent])) $plan->title = $parents[$plan->parent] . ' / ' . $plan->title;
$year = substr($plan->end, 0, 4);
$branchIdList = explode(',', trim($plan->branch, ','));
$branchIdList = array_unique($branchIdList);
foreach($branchIdList as $branchID)
{
if($branchID === '') continue;
$roadmap[$year][$branchID][] = $plan;
}
$total++;
/* Exceed requested count. */
if($count > 0 and $total >= $count)
{
$roadmap = $this->processRoadmap($roadmap, $branch);
$return = true;
break;
}
}
}
return array($roadmap, $total, $return);
}
/**
* 获取产品1.5级导航下拉项跳转链接。
* Get product link for 1.5 level navigation.
*
* @param string $module
* @param string $method
* @param string $extra
* @param bool $branch
* @access public
* @return string
*/
public function getProductLink(string $module, string $method, string $extra = '', bool $branch = false): string
{
$branchID = $branch ? "%s" : 'all';
$branchParam = $branch ? "&branch=%s" : '';
$extraParam = $extra ? "&extras=$extra" : '';
$params = explode(',', $extra);
$method = strtolower($method);
if($module == 'project' && $method == 'bug') return helper::createLink($module, $method, "projectID={$params[0]}&productID=%s{$branchParam}");
if($module == 'bug' && $method == 'view') return helper::createLink('bug', 'browse', "productID=%s&branch={$branchID}&extra=$extra");
if($module == 'bug' && $method == 'report') return helper::createLink('bug', 'browse', "productID=%s{$branchParam}");
if($module == 'qa' && $method == 'index') return helper::createLink('bug', 'browse', "productID=%s{$branchParam}");
if($module == 'story' && $method == 'report') return helper::createLink($module, $method, "productID=%s&branch={$branchID}&stroyType=$module");
if($module == 'testcase' && $method == 'browse') return helper::createLink($module, $method, "productID=%s&branch={$branchID}" . ($extra ? "&browseType=$extra" : ''));
if($module == 'testreport' && $method == 'create') return helper::createLink($module, $method, "objectID=&objectType=testtask&extra=%s");
if($module == 'testtask' && $method == 'browse') return helper::createLink($module, $method, "productID=%s&branch={$branchID}");
if($module == 'testsuite' && $method != 'create') return helper::createLink('testsuite', 'browse', "productID=%s");
if($module == 'product' && $method == 'project') return helper::createLink($module, $method, "status=all&productID=%s{$branchParam}");
if($module == 'product' && $method == 'dynamic') return helper::createLink($module, $method, "productID=%s&type=$extra");
if($module == 'project' && $method == 'testcase') return helper::createLink($module, $method, "projectID={$params[0]}&productID=%s&branch={$branchID}&browseType={$params[1]}");
if($module == 'testtask' && $method == 'browseunits') return helper::createLink($module, 'browseUnits', "productID=%s&browseType=newest&orderBy=id_desc&recTotal=0&recPerPage=0&pageID=1" . ($this->app->tab == 'project' ? "&projectID={$this->session->project}" : ''));
if($module == 'testtask' && $method == 'unitcases') return helper::createLink($module, 'browseUnits', "productID=%s&browseType=newest&orderBy=id_desc&recTotal=0&recPerPage=0&pageID=1" . ($this->app->tab == 'project' ? "&projectID={$this->session->project}" : ''));
if(in_array($module, array('story', 'epic', 'requirement')) && in_array($method, array('create', 'edit', 'batchcreate', 'view'))) return helper::createLink('product', 'browse', "productID=%s&branch={$branchID}&browseType=" . ($this->config->vision == 'or' ? 'assignedtome' : 'unclosed') . "&param=0&storyType=$module");
if($module == 'product' && $method == 'browse') return helper::createLink('product', 'browse', "productID=%s&branch={$branchID}&browseType=" . ($this->config->vision == 'or' ? 'assignedtome' : 'unclosed') . "&param=0&storyType=$extra");
if($module == 'story' && $method == 'storyview') return helper::createLink('product', 'browse', "productID=%s&branch={$branchID}");
if($this->config->edition != 'open')
{
$flow = $this->loadModel('workflow')->getByModule($module);
if($flow && in_array($flow->app, array('scrum', 'waterfall', 'kanbanProject'))) $flow->app = 'project';
if(!empty($flow) && $flow->buildin == '0') return helper::createLink('flow', 'ajaxSwitchBelong', "objectID=%s&moduleName=$module") . "#app=$flow->app";
}
if($module == 'execution' && in_array($method, array('bug', 'testcase'))) return helper::createLink($module, $method, "executionID={$params[0]}&productID=%s{$branchParam}");
if($module == 'product' && in_array($method, array('doc', 'view'))) return helper::createLink($module, $method, "productID=%s");
if($module == 'product' && in_array($method, array('create', 'showimport'))) return helper::createLink($module, 'browse', "productID=%s&type=$extra");
if($module == 'bug' && in_array($method, array('edit', 'view'))) return helper::createLink($module, 'browse', "productID=%s");
if($module == 'ticket' && in_array($method, array('browse', 'view', 'edit', 'createstory'))) return helper::createLink('ticket', 'browse', "browseType=byProduct&productID=%s");
if($module == 'testreport' && in_array($method, array('edit', 'browse'))) return helper::createLink($module, 'browse', "objectID=%s");
if($module == 'feedback' && $this->config->vision == 'lite') return helper::createLink('feedback', 'browse', "browseType=byProduct&productID=%s");
if($module == 'tree' && !empty($extra)) return helper::createLink('tree', $method, "productID=%s&view={$extra}&currentModuleID=0{$branchParam}");
if($module == 'design') return helper::createLink('design', 'browse', "productID=%s");
if($module == 'execution') return helper::createLink('execution', $method, "objectID=$extra&productID=%s");
if($module == 'feedback') return helper::createLink('feedback', 'admin', "browseType=byProduct&productID=%s");
if($module == 'programplan') return helper::createLink('programplan', 'browse', "projectID=%s&productID=%s&type=" . ($extra ? $extra : 'gantt'));
if($module == 'project') return helper::createLink('project', $method, "objectID=$extra&productID=%s");
if($module == 'tree') return helper::createLink('tree', $method, "productID=%s&view=story&currentModuleID=0{$branchParam}");
if($module == 'ticket') return helper::createLink('ticket', $method, 'productID=%s');
if($module == 'testtask') return helper::createLink('testtask', 'browse', "productID=%s&branch={$branchID}");
if(in_array($module, array('productplan', 'release', 'roadmap')) && $method != 'create') return helper::createLink($module, 'browse', "productID=%s{$branchParam}");
if($module == 'api' || $module == 'doc') return helper::createLink('doc', 'productSpace', "objectID=%s");
if($module == 'productplan' || $module == 'release') return helper::createLink($module, $method, "productID=%s{$branchParam}");
if($module == 'testcase' && in_array($method, array('groupcase', 'zerocase')) && $this->app->tab == 'project')
{
$projectID = $extra;
if(str_contains($extra, 'projectID'))
{
parse_str($extra, $output);
$projectID = zget($output, 'projectID', 0);
}
return helper::createLink($module, $method, "productID=%s&branch={$branchID}&groupBy=&projectID=$projectID") . "#app=project";
}
if($module == 'testcase' && !in_array($method, array('browse', 'browsescene', 'groupcase', 'zerocase', 'create'))) return helper::createLink($module, 'browse', "productID=%s");
if($module == 'story' && $this->config->vision == 'or') return helper::createLink('story', 'create', "productID=%s&branch=0&moduleID=0&storyID=0&objectID=0&bugID=0&planID0&todoID=0&extra=&storyType=requirement");
return helper::createLink($module, $method, "productID=%s{$branchParam}{$extraParam}");
}
/**
* 刷新产品的统计信息。
* Refresh stats info of products.
*
* @param bool $refreshAll
* @access public
* @return bool
*/
public function refreshStats(bool $refreshAll = false): bool
{
$updateTime = zget($this->app->config->global, 'productStatsTime', '');
if($updateTime && time() - strtotime($updateTime) < $this->config->product->refreshInterval && !$refreshAll) return true;
/*
* If productStatsTime is before two weeks ago, refresh all products directly.
* Else only refresh the latest products in action table.
*/
$productActions = array();
$products = array();
if($updateTime < date('Y-m-d', strtotime('-14 days')) || $refreshAll)
{
$products = $this->dao->select('id')->from(TABLE_PRODUCT)->fetchPairs('id');
}
else
{
$productActions = $this->dao->select('distinct product')->from(TABLE_ACTION)
->where('date')->ge($updateTime)
->andWhere('product')->notin(array(',0,', ',,'))
->fetchPairs('product');
if(empty($productActions)) return true;
foreach($productActions as $productAction)
{
foreach(explode(',', trim($productAction, ',')) as $product) $products[$product] = $product;
}
}
$now = helper::now();
/* 1. Get summary of products to be refreshed. */
$stats = $this->productTao->getProductStats($products);
/* 2. Refresh stats to db. */
foreach($stats as $productID=> $product)
{
$this->dao->update(TABLE_PRODUCT)->data($product)->where('id')->eq($productID)->exec();
}
/* 3. Update projectStatsTime in config. */
$this->loadModel('setting')->setItem('system.common.global.productStatsTime', $now);
$this->app->config->global->productStatsTime = $now;
/* 4. Clear actions older than 30 days. */
$this->loadModel('action')->cleanActions();
return !dao::isError();
}
/*
* 获取1.5级导航数据。
* Get product switcher.
*
* @param int $productID
* @param string $extra
* @access public
* @return string
*/
public function getSwitcher(int $productID = 0, string $extra = ''): string
{
/* 获取产品名称,产品类型。 */
$currentProductName = $this->lang->productCommon;
if($productID)
{
$currentProduct = $this->getByID($productID);
$currentProductName = $currentProduct->name;
$this->session->set('currentProductType', $currentProduct->type);
}
/* Init locateModule and locateMethod for report and story. */
list($locateModule, $locateMethod) = $this->productTao->computeLocate4DropMenu();
/* 生成异步获取产品下拉菜单的链接。*/
$this->loadModel('index');
$fromModule = $this->app->tab == 'qa' ? 'qa' : '';
$dropMenuModule = $this->app->tab == 'qa' ? 'product' : $this->app->tab;
$dropMenuMethod = in_array("{$this->app->moduleName}-{$this->app->methodName}", $this->config->index->oldPages) ? 'ajaxGetOldDropMenu' : 'ajaxGetDropMenu';
$dataRide = in_array("{$this->app->moduleName}-{$this->app->methodName}", $this->config->index->oldPages) ? 'searchList' : 'dropmenu' ;
$dropMenuLink = helper::createLink($dropMenuModule, $dropMenuMethod, "objectID=$productID&module=$locateModule&method=$locateMethod&extra=$extra&from=$fromModule");
/* 构建产品1.5级导航数据。*/
$output = "<div class='btn-group header-btn' id='swapper'><button data-toggle='dropdown' type='button' class='btn' id='currentItem' title='{$currentProductName}'><span class='text'>{$currentProductName}</span> <span class='caret' style='margin-bottom: -1px'></span></button><div id='dropMenu' class='dropdown-menu search-list' data-ride='{$dataRide}' data-url='$dropMenuLink'>";
$output .= '<div class="input-control search-box has-icon-left has-icon-right search-example"><input type="search" class="form-control search-input" /><label class="input-control-icon-left search-icon"><i class="icon icon-search"></i></label><a class="input-control-icon-right search-clear-btn"><i class="icon icon-close icon-sm"></i></a></div>';
$output .= "</div></div>";
return $output;
}
/**
* Build search config for story list.
*
* @param int $productID
* @param string $storyType
* @access public
* @return array
*/
public function buildSearchConfig(int $productID, string $storyType): array
{
$searchConfig = $this->config->product->search;
$this->app->loadLang($storyType);
unset($searchConfig['fields']['product']);
unset($searchConfig['params']['product']);
/* Get module data. */
$searchConfig['params']['module']['values'] = $this->productTao->getModulesForSearchForm($productID, $products = array());
$searchConfig['params']['grade']['values'] = $this->loadModel('story')->getGradePairs($storyType, 'all');
$searchConfig['params']['plan']['values'] = $this->loadModel('productplan')->getPairs(array($productID), '');
$searchConfig['params']['stage']['values'] = $this->lang->$storyType->stageList;
/* Get branch data. */
if($productID)
{
$productInfo = $this->getByID($productID);
if($productInfo->type == 'normal')
{
unset($searchConfig['fields']['branch']);
unset($searchConfig['params']['branch']);
}
else
{
$searchConfig['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$productInfo->type]);
$searchConfig['params']['branch']['values'] = array('' => '', '0' => $this->lang->branch->main) + $this->loadModel('branch')->getPairs($productID, 'noempty');
}
}
if($this->config->edition == 'ipd') $searchConfig['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID);
if($storyType != 'story')
{
$replacement = $storyType == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon;
$this->lang->story->title = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->title);
$this->lang->story->create = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->create);
$searchConfig['fields']['title'] = $this->lang->story->title;
}
return $searchConfig;
}
}