| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995 |
- <?php
- /**
- * 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 chen.tao<chentao@easycorp.ltd>
- * @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);
- }
- }
- }
- }
- }
|