* @package testcase * @link https://www.zentao.net */ class testcaseZen extends testcase { /** * 设置列表页面的 cookie。 * Set browse cookie. * * @param int $productID * @param string $branch * @param string $browseType * @param int $param * @access protected * @return void */ protected function setBrowseCookie(string $productID, string $branch, string $browseType, string $param): void { helper::setcookie('preProductID', $productID); helper::setcookie('preBranch', $branch); if($this->cookie->preProductID != $productID || $this->cookie->preBranch != $branch) { $_COOKIE['caseModule'] = 0; helper::setcookie('caseModule', '0'); } if($browseType == 'bymodule') helper::setcookie('caseModule', $param); if($browseType == 'bysuite') helper::setcookie('caseSuite', $param); } /** * 设置列表页面的 session。 * Set Browse session. * * @param int $productID * @param int $moduleID * @param string $browseType * @param string $orderBy * @access protected * @return void */ protected function setBrowseSession(int $productID, int $moduleID, string $browseType, string $orderBy): void { if($browseType != 'bymodule') $this->session->set('caseBrowseType', $browseType); $this->session->set('caseList', $this->app->getURI(true), $this->app->tab); $this->session->set('productID', $productID); $this->session->set('moduleID', $moduleID); $this->session->set('browseType', $browseType); $this->session->set('orderBy', $orderBy); $this->session->set('testcaseOrderBy', '`sort` asc', $this->app->tab); $this->session->set('testcaseOrderBy', '`sort` asc'); } /** * 设置列表页面的导航。 * Set menu in browse. * * @param int $productID * @param string $branch * @param string $browseType * @param int $projectID * @access protected * @return void */ protected function setBrowseMenu(int $productID, string $branch, string $browseType, int $projectID): void { /* 在不同的应用中,设置不同的导航。 */ /* Set menu, save session. */ if($this->app->tab == 'project') { $linkedProducts = $this->product->getProducts($projectID, 'all', '', false); $this->products = array('0' => $this->lang->product->all) + $linkedProducts; $hasProduct = $this->dao->findById($projectID)->from(TABLE_PROJECT)->fetch('hasProduct'); if(!$hasProduct) unset($this->config->testcase->search['fields']['product']); $branch = intval($branch) > 0 ? $branch : 'all'; $this->loadModel('project')->setMenu($projectID); $this->view->products = $this->products; $this->view->hasProduct = $hasProduct; } else { $this->qa->setMenu($this->products, $productID, $branch, $browseType); } } /** * 设置菜单。 * Set menu. * * @param int $projectID * @param int $executionID * @param int $productID * @param string $branch * @param string $case * @access protected * @return void */ protected function setMenu(int $projectID, int $executionID, int $productID, string $branch) { if($this->app->tab == 'project') $this->loadModel('project')->setMenu($projectID); if($this->app->tab == 'execution') $this->loadModel('execution')->setMenu($executionID); if($this->app->tab == 'qa') $this->testcase->setMenu($this->products, $productID, $branch); } /** * 构建搜索表单。 * Build the search form. * * @param int $productID * @param string $branch * @param int $queryID * @param int $projectID * @access protected * @return void */ protected function buildBrowseSearchForm(int $productID, string $branch, int $queryID, int $projectID): void { $this->config->testcase->search['onMenuBar'] = 'yes'; $currentModule = $this->app->tab == 'project' ? 'project' : 'testcase'; $currentMethod = $this->app->tab == 'project' ? 'testcase' : 'browse'; $projectParam = $this->app->tab == 'project' ? "projectID={$this->session->project}&" : ''; $actionURL = $this->createLink($currentModule, $currentMethod, $projectParam . "productID=$productID&branch=$branch&browseType=bySearch&queryID=myQueryID"); $searchProducts = $this->product->getPairs('', 0, '', 'all'); $this->testcase->buildSearchForm($productID, $searchProducts, $queryID, $actionURL, $projectID); } /** * 展示创建 testcase 的相关变量。 * Show the variables associated with the creation testcase. * * @param int $productID * @param string $branch * @param int $moduleID * @param string $from * @param int $param * @param int $storyID * @access protected * @return void */ protected function assignCreateVars(int $productID, string $branch = '', int $moduleID = 0, string $from = '', int $param = 0, int $storyID = 0) { $product = $this->product->getById($productID); if(!isset($this->products[$productID])) $this->products[$productID] = $product->name; $executionID = $from == 'execution' ? $param : 0; $testcaseID = $from && strpos('testcase|work|contribute', $from) !== false ? $param : 0; /* 设置分支。 */ /* Set branches. */ if($this->app->tab == 'project' || $this->app->tab == 'execution') { $objectID = $this->app->tab == 'project' ? $this->session->project : $executionID; $productBranches = isset($product->type) && $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct(array($productID), $objectID, 'noclosed|withMain') : array(); $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); $branch = key($branches); } else { $branches = isset($product->type) && $product->type != 'normal' ? $this->loadModel('branch')->getPairs($productID, 'active') : array(); } /* 设置菜单。 */ /* Set menu. */ $this->setMenu((int)$this->session->project, $executionID ? $executionID : (int)$this->session->execution, $productID, $branch); /* 初始化用例数据。 */ /* Initialize the testcase. */ $case = $this->initTestcase($storyID, $testcaseID, $from == 'bug' ? $param : 0); /* 设置模块和需求。 */ /* Set modules. */ $this->assignModulesAndStoiresForCreate($productID, $moduleID, $branch, $case->story, $branches); $this->view->product = $product; $this->view->projectID = isset($projectID) ? $projectID : 0; $this->view->currentSceneID = $testcaseID > 0 ? $case->scene : (int)$this->cookie->lastCaseScene; $this->view->case = $case; $this->view->executionID = $executionID; $this->view->branch = $branch; $this->view->branches = $branches; } /** * 展示创建场景的相关变量。 * Show the variables associated with the creation scene. * * @param int $productID * @param string $branch * @param int $moduleID * @access protected * @return void */ protected function assignCreateSceneVars(int $productID, string $branch = '', int $moduleID = 0): void { $product = $this->product->getById($productID); if(!isset($this->products[$productID])) $this->products[$productID] = $product->name; /* 获取分支键值对及当前分支。*/ /* Get branch pairs and set current branch. */ if($this->app->tab == 'project' || $this->app->tab == 'execution') { /* 在项目或执行中显示时只显示项目或执行关联的分支键值对。*/ /* When showing in a project or execution, only the branch key-value pairs associated with the project or execution are shown. */ $objectID = $this->app->tab == 'project' ? $this->session->project : $executionID; $productBranches = isset($product->type) && $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct(array($productID), $objectID, 'noclosed|withMain') : array(); $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); $branch = key($branches); } else { $branches = isset($product->type) && $product->type != 'normal' ? $this->loadModel('branch')->getPairs($productID, 'active') : array(); } /* 设置菜单。 */ /* Set menu. */ $this->setMenu((int)$this->session->project, (int)$this->session->execution, $productID, $branch); $this->view->title = $this->products[$productID] . $this->lang->colon . $this->lang->testcase->newScene; $this->view->modules = $this->tree->getOptionMenu($productID, $viewType = 'case', $startModuleID = 0, ($branch === 'all' || !isset($branches[$branch])) ? 0 : $branch); $this->view->scenes = $this->testcase->getSceneMenu($productID, $moduleID, $viewType = 'case', $startSceneID = 0, ($branch === 'all' || !isset($branches[$branch])) ? 0 : $branch); $this->view->moduleID = $moduleID ? (int)$moduleID : (int)$this->cookie->lastCaseModule; $this->view->parent = (int)$this->cookie->lastCaseScene; $this->view->product = $product; $this->view->branch = $branch; $this->view->branches = $branches; } /** * 展示创建 testcase 的需求和模块变量。 * Show the modules and stoires associated with the creation testcase. * * @param int $productID * @param int $moduleID * @param string $branch * @param int $storyID * @param array $branches * @access protected * @return void */ protected function assignModulesAndStoiresForCreate(int $productID, int $moduleID, string $branch, int $storyID, array $branches) { if($storyID) { $story = $this->loadModel('story')->getByID($storyID); if(empty($moduleID)) $moduleID = $story->module; } $currentModuleID = $moduleID ? (int)$moduleID : (int)$this->cookie->lastCaseModule; $modules = array(); if($currentModuleID) { $productModules = $this->loadModel('tree')->getOptionMenu($productID, 'story'); $storyModuleID = array_key_exists($currentModuleID, $productModules) ? $currentModuleID : 0; $modules = $this->tree->getStoryModule($storyModuleID); $modules = $this->tree->getAllChildID($modules); } /* 获取未关闭的需求。 */ /* Get the status of stories are not closed. */ $stories = $this->loadModel('story')->getProductStoryPairs($productID, $branch, $modules, 'active', 'id_desc', 50, 'full', 'story', false); if($this->app->tab != 'qa' && $this->app->tab != 'product') { $projectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution; $stories = $this->story->getExecutionStoryPairs($projectID, $productID, $branch, $modules); } if($storyID && !isset($stories[$storyID])) $stories = $this->story->formatStories(array($storyID => $storyID)) + $stories; $this->view->stories = $stories; $this->view->currentModuleID = $currentModuleID; $this->view->moduleOptionMenu = $this->tree->getOptionMenu($productID, 'case', 0, $branch === 'all' || !isset($branches[$branch]) ? 0 : $branch); $this->view->sceneOptionMenu = $this->testcase->getSceneMenu($productID, $moduleID, 'case', 0, $branch === 'all' || !isset($branches[$branch]) ? 0 : $branch); } /** * 指定用例和场景。 * Assign browse cases and scenes. * * @param int $productID * @param string $branch * @param string $browseType * @param int $queryID * @param int $moduleID * @param string $caseType * @param string $orderBy * @param object $pager * @access protected * @return void */ protected function assignCasesAndScenesForBrowse(int $productID, string $branch, string $browseType, int $queryID, int $moduleID, string $caseType, string $orderBy, object $pager): void { $cases = array(); $caseIdList = array(); $queryCondition = ''; /* 不是仅场景的时候获取用例列表。*/ /* Get test cases when the browseType is not onlyscene. */ if($browseType != 'onlyscene') { $sort = common::appendOrder($orderBy); $cases = $this->testcase->getTestCases($productID, $branch, $browseType, $queryID, $moduleID, $caseType, $sort, null); $queryCondition = $this->dao->get(); $caseIdList = array_column($cases, 'id'); } $productParam = $productID; if(intval($productID) <= 0) { $productParam = array_keys($this->products); if(count($productParam) > 1) unset($productParam[0]); } /* 获取顶级的场景和案例。*/ /* Get top level cases and scenes.*/ $topObjects = array(); if(!$this->cookie->onlyAutoCase) { $topObjects = $this->testcase->getList($productParam, $branch, $moduleID, $caseIdList, $pager, 'top', array(), $browseType); if(empty($topObjects) && $pageID > 1) { $pager = pager::init(0, $recPerPage, 1); $topObjects = $this->testcase->getList($productParam, $branch, $moduleID, $caseIdList, $pager, 'top', array(), $browseType); } } /* 获取用例和场景列表。*/ /* Get children cases and scenes.*/ /* Process case for check story changed. */ $scenes = $this->testcase->getList($productParam, $branch, $moduleID, $caseIdList, null, 'child', array_keys($topObjects), $browseType, $queryCondition); $scenes = $this->loadModel('story')->checkNeedConfirm($scenes); $scenes = $this->testcase->appendData($scenes); /* 保存查询的 session。*/ /* save session. */ $this->loadModel('common')->saveQueryCondition($queryCondition, 'testcase', false); $this->view->cases = $cases; $this->view->scenes = $scenes; $this->view->orderBy = $orderBy; $this->view->pager = $pager; } /** * * 指定模块树。 * Assign module tree in browse page. * * @param int $productID * @param string $branch * @param int $projectID * @access protected * @return void */ protected function assignModuleTreeForBrowse(int $productID, string $branch, int $projectID): void { if($projectID && empty($productID)) { $this->view->moduleTree = $this->tree->getCaseTreeMenu($projectID, $productID, 0, array('treeModel', 'createCaseLink')); } else { $this->view->moduleTree = $this->tree->getTreeMenu($productID, 'case', 0, array('treeModel', 'createCaseLink'), array('projectID' => $projectID, 'productID' => $productID), $branch); } } /** * * 指定产品和分支。 * Assign product and branch. * * @param int $productID * @param int $projectID * @access protected * @return void */ protected function assignProductAndBranchForBrowse(int $productID, string $branch, int $projectID): void { /* 根据产品类型判断是否展示分支,获取分支选项信息和带标签的分支选项信息。*/ /* Judge whether to show branch according to the type of product, get branch option and branch tag option. */ $product = $this->product->getByID($productID); $showBranch = false; $branchOption = array(); $branchTagOption = array(); if($product && $product->type != 'normal') { /* Display of branch label. */ $showBranch = $this->loadModel('branch')->showBranch($productID); /* Display status of branch. */ $branches = $this->loadModel('branch')->getList($productID, $projectID, 'all'); foreach($branches as $branchInfo) { $branchOption[$branchInfo->id] = $branchInfo->name; $branchTagOption[$branchInfo->id] = $branchInfo->name . ($branchInfo->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : ''); } } $this->view->productID = $productID; $this->view->productName = $this->products[$productID]; $this->view->product = $product; $this->view->branch = (!empty($product) and $product->type != 'normal') ? $branch : 0; $this->view->branchOption = $branchOption; $this->view->branchTagOption = $branchTagOption; } /** * 指定变量。 * Assign variables. * * @param int $productID * @param string $branch * @param string $browseType * @param int $projectID * @param int $param * @param int $moduleID * @param int $suiteID * @param string $caseType * @access protected * @return void */ protected function assignForBrowse(int $productID, string $branch, string $browseType, int $projectID, int $param, int $moduleID, int $suiteID, string $caseType): void { $showModule = !empty($this->config->testcase->browse->showModule) ? $this->config->testcase->browse->showModule : ''; $tree = $moduleID ? $this->tree->getByID($moduleID) : ''; $this->view->title = $this->products[$productID] . $this->lang->colon . $this->lang->testcase->common; $this->view->projectID = $projectID; $this->view->projectType = !empty($projectID) ? $this->dao->select('model')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch('model') : ''; $this->view->browseType = $browseType; $this->view->param = $param; $this->view->moduleID = $moduleID; $this->view->moduleName = $moduleID ? $tree->name : $this->lang->tree->all; $this->view->suiteID = $suiteID; $this->view->caseType = $caseType; $this->view->users = $this->user->getPairs('noletter'); $this->view->modules = $this->tree->getOptionMenu($productID, $viewType = 'case', $startModuleID = 0, $branch == 'all' ? '0' : $branch); $this->view->iscenes = $this->testcase->getSceneMenu($productID, $moduleID, $viewType = 'case', $startSceneID = 0, 0); $this->view->suiteList = $this->loadModel('testsuite')->getSuites($productID); $this->view->modulePairs = $showModule ? $this->tree->getModulePairs($productID, 'case', $showModule) : array(); $this->view->libraries = $this->loadModel('caselib')->getLibraries(); $this->view->stories = array('') + $this->loadModel('story')->getPairs($productID); $this->view->automation = $this->loadModel('zanode')->getAutomationByProduct($productID); } /** * 处理更新请求数据。 * Processing request data. * * @param form $formData * @param object $oldBug * @access protected * @return object|false */ protected function prepareEditExtras(form $formData, object $oldCase): object|false { foreach($formData->data->expects as $key => $value) { if(!empty($value) && empty($formData->data->steps[$key])) { dao::$errors[] = sprintf($this->lang->testcase->stepsEmpty, $key); return false; } } if(!empty($_FILES['scriptFile'])) unset($_FILES['scriptFile']); $result = $this->testcase->getStatus('update', $oldCase); if(!$result || !is_array($result)) return $result; list($stepChanged, $status) = $result; $case = $formData->add('id', $oldCase->id) ->add('version', $stepChanged ? (int)$oldCase->version + 1 : (int)$oldCase->version) ->add('lastEditedBy', $this->app->user->account) ->add('lastEditedDate', helper::now()) ->add('stepChanged', $stepChanged) ->setForce('status', $status) ->setDefault('story,branch', 0) ->setDefault('stage', '') ->setDefault('deleteFiles', array()) ->setDefault('storyVersion', $oldCase->storyVersion) ->setIF($formData->data->story != false && $formData->data->story != $oldCase->story, 'storyVersion', $this->loadModel('story')->getVersion($formData->data->story)) ->setIF(!$formData->data->linkCase, 'linkCase', '') ->setIF($formData->data->auto, 'auto', 'auto') ->setIF($formData->data->auto && $formData->data->script, 'script', htmlentities($formData->data->script)) ->setIF(!$formData->data->auto, 'auto', 'no') ->setIF(!$formData->data->auto, 'script', '') ->join('stage', ',') ->join('linkCase', ',') ->cleanInt('story,product,branch,module') ->stripTags($this->config->testcase->editor->edit['id'], $this->config->allowedTags) ->remove('files,labels,scriptFile,scriptName') ->get(); return $this->loadModel('file')->processImgURL($case, $this->config->testcase->editor->edit['id'], $this->post->uid); } /** * 提前处理用例数据。 * Preprocess case. * * @param object $case * @access protected * @return object */ protected function preProcessForEdit(object $case): object { /* 处理单元测试用例列表的标签。*/ /* Process the sub menu of unit test case. */ if($case->auto == 'unit') { $this->lang->testcase->subMenu->testcase->feature['alias'] = ''; $this->lang->testcase->subMenu->testcase->unit['alias'] = 'view'; $this->lang->testcase->subMenu->testcase->unit['subModule'] = 'testcase'; } /* 初始化用例步骤。*/ /* Unit the steps of case. */ if(empty($case->steps)) { $step = new stdclass(); $step->type = 'step'; $step->desc = ''; $step->expect = ''; $case->steps[] = $step; } return $case; } /** * 设置编辑用例库用例的导航。 * Set menu for editing lib case. * * @param object $case * @param array $libraries * @access protected * @return void */ protected function setMenuForLibCaseEdit(object $case, array $libraries): void { if($this->app->tab == 'project') { $this->loadModel('project')->setMenu($this->session->project); } else { $this->loadModel('caselib')->setLibMenu($libraries, $case->lib); } } /** * 设置编辑用例的导航。 * Set menu for editing case. * * @param object $case * @access protected * @return void */ protected function setMenuForCaseEdit(object $case): void { if($this->app->tab == 'project') $this->loadModel('project')->setMenu($case->project); if($this->app->tab == 'execution') { if(!$executionID) $executionID = $case->execution; $this->loadModel('execution')->setMenu($executionID); } if($this->app->tab == 'qa') $this->testcase->setMenu($this->products, $case->product, $case->branch); } /** * 指定编辑用例库用例的数据。 * Assign data for editint lib case. * * @param object $case * @param array $libraries * @access protected * @return void */ protected function assignForEditLibCase(object $case, array $libraries): void { $this->view->title = "CASE #$case->id $case->title - " . $libraries[$case->lib]; $this->view->isLibCase = true; $this->view->libraries = $libraries; $this->view->moduleOptionMenu = $this->tree->getOptionMenu($case->lib, $viewType = 'caselib', $startModuleID = 0); } /** * 指定编辑用例的数据。 * Assign for editing case. * * @param object $case * @access protected * @return void */ protected function assignForEditCase(object $case): void { $product = $this->product->getByID($case->product); if(!isset($this->products[$case->product])) $this->products[$case->product] = $product->name; $this->view->title = $this->products[$case->product] . $this->lang->colon . $this->lang->testcase->edit; $this->view->isLibCase = false; $this->view->product = $product; $this->view->products = $this->products; $this->assignBranchForEdit($case); $this->assignStoriesForEdit($case); $this->assignModuleOptionMenuForEdit($case); } /** * 指定编辑用例的分支。 * Assign branch data for editing case. * * @param object $case * @access private * @return void */ private function assignBranchForEdit(object $case): void { $objectID = 0; if($this->app->tab == 'execution') $objectID = $executionID; if($this->app->tab == 'project') $objectID = $case->project; $branches = $this->loadModel('branch')->getList($case->product, $objectID, 'all'); $branchTagOption = array(); foreach($branches as $branchInfo) $branchTagOption[$branchInfo->id] = $branchInfo->name . ($branchInfo->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : ''); if(!isset($branchTagOption[$case->branch])) { $caseBranch = $this->branch->getByID($case->branch, $case->product, ''); $branchTagOption[$case->branch] = $case->branch == BRANCH_MAIN ? $caseBranch : ($caseBranch->name . ($caseBranch->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : '')); } $this->view->branchTagOption = $branchTagOption; } /** * 指定编辑用例的需求。 * Assign stories for editing case. * * @param object $case * @access private * @return void */ private function assignStoriesForEdit(object $case): void { $moduleIdList = array(); if($case->module) $moduleIdList = $this->tree->getAllChildID($case->module); if($this->app->tab == 'execution') { $stories = $this->loadModel('story')->getExecutionStoryPairs($case->execution, $case->product, $case->branch, $moduleIdList); } else { $stories = $this->loadModel('story')->getProductStoryPairs($case->product, $case->branch, $moduleIdList, 'all','id_desc', 0, 'full', 'story', false); } $this->view->stories = $stories; } /** * 指定编辑用的模块选项。 * Assign module option menu for editing case. * * @param object $case * @access private * @return void */ private function assignModuleOptionMenuForEdit(object $case): void { $moduleOptionMenu = $this->tree->getOptionMenu($case->product, $viewType = 'case', $startModuleID = 0, $case->branch); if($case->lib && $case->fromCaseID) { $lib = $this->loadModel('caselib')->getByID($case->lib); $libModules = $this->tree->getOptionMenu($case->lib, 'caselib'); foreach($libModules as $moduleID => $moduleName) { if($moduleID == 0) continue; $moduleOptionMenu[$moduleID] = $lib->name . $moduleName; } } if(!isset($moduleOptionMenu[$case->module])) $moduleOptionMenu += $this->tree->getModulesName((array)$case->module); $this->view->moduleOptionMenu = $moduleOptionMenu; } /** * 指定编辑用例的其他数据。 * Assign other data for editing case. * * @param int $productID * @param object $case * @param array $testtasks * @access protected * @return void */ protected function assignForEdit(int $productID, object $case, array $testtasks): void { $sceneOptionMenu = $this->testcase->getSceneMenu($productID, $case->module, $viewType = 'case', $startSceneID = 0, 0); if(!isset($sceneOptionMenu[$case->scene])) $sceneOptionMenu += $this->testcase->getScenesName($case->scene); $forceNotReview = $this->testcase->forceNotReview(); if($forceNotReview) unset($this->lang->testcase->statusList['wait']); $this->view->case = $case; $this->view->testtasks = $testtasks; $this->view->forceNotReview = $forceNotReview; $this->view->sceneOptionMenu = $sceneOptionMenu; $this->view->users = $this->user->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList('case', $case->id); } /** * 指定批量编辑用例的数据。 * Assign for editing case. * * @param int $productID * @param string $branch * @param string $type * @param array $cases * @access protected * @return void */ protected function assignForBatchEdit(int $productID, string $branch, string $type, array $cases): void { list($productIdList, $libIdList) = $this->assignTitleForBatchEdit($productID, $branch, $type, $cases); /* 设置模块。 */ /* Set modules. */ $modules = array(); $branchProduct = false; $branchTagOption = array(); $products = $this->product->getByIdList($productIdList); $branches = array(0 => 0); foreach($products as $product) { if($product->type != 'normal') { $branches = $this->loadModel('branch')->getList($product->id, 0, 'all'); foreach($branches as $branchInfo) $branchTagOption[$product->id][$branchInfo->id] = "/{$product->name}/{$branchInfo->name}" . ($branchInfo->status == 'closed' ? " ({$this->lang->branch->statusList['closed']})" : ''); $branchProduct = true; } $modulePairs = $this->tree->getOptionMenu($product->id, 'case', 0, array_keys($branches)); foreach($modulePairs as $branchID => $branchModules) { $modules['case'][$product->id][$branchID] = array(); foreach($branchModules as $moduleID => $module) $modules['case'][$product->id][$branchID][] = array('text' => $module, 'value' => $moduleID); } } foreach($libIdList as $libID) { $libModules = $this->tree->getOptionMenu($libID, 'caselib'); $modules['lib'][$libID][0] = array(); foreach($libModules as $moduleID => $module) $modules['lib'][$libID][0][] = array('text' => $module, 'value' => $moduleID); } if($this->app->tab == 'project') $branchTagOption = $this->loadModel('branch')->getPairsByProjectProduct($this->session->project, $productID); /* 指派模块和场景。 */ /* Assign modules and scenes. */ $this->assignModuleAndSceneForBatchEdit($productID, $branch, $branches, $cases, $modules); /* 设置自定义字段。 */ /* Set custom fields. */ foreach(explode(',', $this->config->testcase->customBatchEditFields) as $field) $customFields[$field] = $this->lang->testcase->$field; $this->view->customFields = $customFields; $this->view->showFields = $this->config->testcase->custom->batchEditFields; $this->view->branchTagOption = $branchTagOption; $this->view->products = $products; $this->view->branchProduct = $branchProduct; } /** * 指定批量编辑用例的页面标题。 * Assign title for editing case. * * @param int $productID * @param string $branch * @param string $type * @param array $cases * @access protected * @return array */ protected function assignTitleForBatchEdit(int $productID, string $branch, string $type, array $cases): array { $productIdList = array(); $libIdList = array(); /* 指派用例库用例。 */ /* Assign lib cases. */ if($type == 'lib') { $libID = $productID; $libIdList = array($libID); $libraries = $this->loadModel('caselib')->getLibraries(); /* Remove story custom fields from caselib */ $this->config->testcase->customBatchEditFields = str_replace(',story', '', $this->config->testcase->customBatchEditFields); $this->config->testcase->custom->batchEditFields = str_replace(',story', '', $this->config->testcase->custom->batchEditFields); /* Set caselib menu. */ $this->caselib->setLibMenu($libraries, $libID); $this->view->title = $libraries[$libID] . $this->lang->colon . $this->lang->testcase->batchEdit; } /* 指派测试用例。 */ /* Assign test cases. */ elseif($productID) { $product = $this->product->getByID($productID); $productIdList = array($productID); $this->setMenu((int)$this->session->project, (int)$this->session->execution, $productID, $branch); $this->view->title = $product->name . $this->lang->colon . $this->lang->testcase->batchEdit; } /* 指派地盘标签下的用例。 */ /* Assign cases of my tab. */ else { foreach($cases as $case) { if($case->lib == 0) $productIdList[$case->product] = $case->product; if($case->lib > 0) $libIdList[$case->lib] = $case->lib; } $this->app->loadLang('my'); $this->lang->testcase->menu = $this->lang->my->menu->work; $this->lang->my->menu->work['subModule'] = 'testcase'; $this->view->title = $this->lang->testcase->batchEdit; } return array($productIdList, $libIdList); } /** * 指定批量编辑用例的模块和场景标题。 * Assign modules and scenes for editing case. * * @param int $productID * @param string $branch * @param array $branches * @param array $cases * @param array $modules * @access protected * @return void */ protected function assignModuleAndSceneForBatchEdit(int $productID, string $branch, array $branches, array $cases, array $modules): void { $moduleScenes = array(); $moduleScenesPairs = array(); $modulePairs = array(); $scenePairs = array(); foreach($cases as $case) { /* 设置用例模块。 */ /* Set case module. */ $objectID = $case->lib > 0 ? $case->lib : $case->product; $objectType = $case->lib > 0 ? 'lib' : 'case'; if(isset($modules[$objectType][$objectID][$case->branch])) { $modulePairs[$case->id] = $modules[$objectType][$objectID][$case->branch]; } elseif(isset($modules[$objectType][$objectID])) { $modulePairs[$case->id] = $modules[$objectType][$objectID][0]; } else { $module = $this->tree->getByID($case->module); $modulePairs[$case->id][] = array('text' => zget($module, 'name', ''), 'value' => $case->module); } /* 设置用例场景。 */ /* Set case 场景. */ if(!isset($moduleScenesPairs[$case->module])) { $moduleScenesPairs[$case->module] = array(); $moduleScenes = $this->testcase->getSceneMenu($productID, $case->module, 'case', 0, $branch === 'all' || !isset($branches[$branch]) ? 0 : $branch); foreach($moduleScenes as $sceneID => $scene) $moduleScenesPairs[$case->module][] = array('text' => $scene, 'value' => $sceneID); } $scenePairs[$case->id][] = $moduleScenesPairs[$case->module]; if(!isset($scenes[$case->scene])) $scenePairs[$case->id][] = array('text' => '/' . $this->testcase->fetchSceneName($case->scene), 'value' => $case->scene); } $this->view->scenePairs = $scenePairs; $this->view->modulePairs = $modulePairs; } /** * Add edit action. * * @param int $caseID * @param string $oldStatus * @param string $status * @param array $changes * @param string $comment * @access protected * @return void */ protected function addEditAction(int $caseID, string $oldStatus, string $status, array $changes = array(), string $comment = ''): void { $this->loadModel('action'); $action = !empty($changes) ? 'Edited' : 'Commented'; $actionID = $this->action->create('case', $caseID, $action, $comment); $this->action->logHistory($actionID, $changes); if($oldStatus != 'wait' && $status == 'wait') $this->action->create('case', $caseID, 'submitReview'); } /** * 指定 testcase 查看的关联的变量。 * Assign testcase related variables. * * @param object $case * @param string $from * @param int $taskID * @access protected * @return void */ protected function assignCaseForView(object $case, string $from, int $taskID) { $case = $this->loadModel('story')->checkNeedConfirm($case); if($from == 'testtask') { $run = $this->loadModel('testtask')->getRunByCase($taskID, $case->id); $case->assignedTo = $run->assignedTo; $case->lastRunner = $run->lastRunner; $case->lastRunDate = $run->lastRunDate; $case->lastRunResult = $run->lastRunResult; $case->caseStatus = $case->status; $case->status = $run->status; $results = $this->testtask->getResults($run->id); $result = array_shift($results); if($result) { $case->xml = $result->xml; $case->duration = $result->duration; } } $case = $this->testcase->appendCaseFails($case, $from, $taskID); $case = $this->processStepsForMindMap($case); $this->view->from = $from; $this->view->taskID = $taskID; $this->view->runID = $from == 'testcase' ? 0 : $run->id; $this->view->case = $case; $this->view->caseFails = $case->caseFails; $this->view->modulePath = $this->tree->getParents($case->module); $this->view->caseModule = empty($case->module) ? '' : $this->tree->getById($case->module); $this->view->preAndNext = !isOnlybody() ? $this->loadModel('common')->getPreAndNextObject('testcase', $case->id) : ''; $this->view->users = $this->user->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList('case', $case->id); } /** * 指定批量创建用例的相关变量。 * Assign for batch creating case. * * @param int $productID * @param string $branch * @param int $moduleID * @param int $storyID * @access protected * @return void */ protected function assignForBatchCreate(int $productID, string $branch = '', int $moduleID = 0, int $storyID = 0) { $product = $this->product->getById($productID); /* 设置分支。 */ /* Set branches. */ if($this->app->tab == 'project' and $product->type != 'normal') { $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $productBranches = $this->loadModel('execution')->getBranchByProduct($productID, $this->session->project, 'noclosed|withMain'); $branches = isset($productBranches[$productID]) ? $productBranches[$productID] : array(); $branch = key($branches); } else { $branches = $this->loadModel('branch')->getPairs($productID, 'active'); } /* 设置自定义字段和展示字段。 */ /* Set custom fields and show fields. */ foreach(explode(',', $this->config->testcase->customBatchCreateFields) as $field) { if($product->type != 'normal') $customFields[$product->type] = $this->lang->product->branchName[$product->type]; $customFields[$field] = $this->lang->testcase->$field; } $showFields = sprintf($this->config->testcase->custom->batchCreateFields, $product->type); if($product->type == 'normal') { $showFields = str_replace(array(",branch,", ",platform,"), '', ",$showFields,"); $showFields = trim($showFields, ','); } /* 设置需求键对。 */ /* Set story pairs. */ $story = $storyID ? $this->story->getByID($storyID) : ''; $storyPairs = $this->loadModel('story')->getProductStoryPairs($productID, $branch === 'all' ? 0 : $branch); $storyPairs += $storyID ? array($storyID => $story->id . ':' . $story->title) : array(); if($storyID && empty($moduleID)) $moduleID = $story->module; $this->view->product = $product; $this->view->branches = $branches; $this->view->customFields = $customFields; $this->view->showFields = $showFields; $this->view->story = $story; $this->view->storyPairs = $storyPairs; $this->view->sceneOptionMenu = $this->testcase->getSceneMenu($productID, $moduleID, 'case', 0, $branch === 'all' || !isset($branches[$branch]) ? 0 : $branch); $this->view->currentModuleID = $moduleID; } /** * 构建创建 testcase 页面数据。 * Build form fields for creating testcase. * * @param string $from * @param int $param * @access protected * @return void */ protected function buildCaseForCase(string $from, int $param): object { return form::data($this->config->testcase->form->create) ->setIF($from == 'bug', 'fromBug', $param) ->setIF($this->post->auto, 'auto', 'auto') ->setIF($this->post->auto && $this->post->script, 'script', $this->post->script ? htmlentities($this->post->script) : '') ->setIF($this->testcase->forceNotReview() || $this->post->forceNotReview, 'status', 'normal') ->setIF($this->app->tab == 'project', 'project', $this->session->project) ->setIF($this->app->tab == 'execution', 'execution', $this->session->execution) ->setIF($this->post->story, 'storyVersion', $this->loadModel('story')->getVersion($this->post->story)) ->get(); } /** * 构建批量创建用例的数据。 * Build cases for batch creating. * * @param int $productID * @access protected * @return array */ protected function buildCasesForBathcCreate(int $productID): array { $now = helper::now(); $account = $this->app->user->account; $forceNotReview = $this->testcase->forceNotReview(); $storyVersions = array(); $testcases = form::batchData($this->config->testcase->form->batchCreate)->get(); foreach($testcases as $testcase) { $testcase->product = $productID; if($this->app->tab == 'project') $testcase->project = $this->session->project; $testcase->openedBy = $account; $testcase->openedDate = $now; $testcase->status = $forceNotReview || $testcase->review == 0 ? 'normal' : 'wait'; $testcase->version = 1; $testcase->storyVersion = isset($storyVersions[$testcase->story]) ? $storyVersions[$testcase->story] : 0; $testcase->steps = array(); $testcase->expects = array(); $testcase->stepType = array(); if($testcase->story && !isset($storyVersions[$testcase->story])) { if(count($storyVersions) == 0) $this->loadModel('story'); $testcase->storyVersion = $this->story->getVersion($testcase->story); $storyVersions[$testcase->story] = $testcase->storyVersion; } unset($testcase->review); } return $testcases; } /** * 构建批量编辑用例的数据。 * Build cases for batch editing. * * @param array $oldCases * @access protected * @return array */ protected function buildCasesForBathcEdit(array $oldCases): array { $caseIdList = array_keys($oldCases); if(empty($caseIdList)) return array(); $now = helper::now(); $account = $this->app->user->account; $cases = form::batchData($this->config->testcase->form->batchEdit)->get(); foreach($cases as $caseID => $case) { $oldCase = $oldCases[$caseID]; $case->id = $caseID; $case->product = $oldCase->product; $case->lastEditedBy = $account; $case->lastEditedDate = $now; } return $cases; } /** * 创建测试用例前检验表单数据是否正确。 * check from data for create case. * * @param object $case * @access protected * @return bool */ protected function checkCreateFormData(object $case): bool { $steps = $case->steps; $expects = $case->expects; foreach($expects as $key => $value) { if(!empty($value) and empty($steps[$key])) dao::$errors['message']["steps$key"] = sprintf($this->lang->testcase->stepsEmpty, $key); } if(dao::isError()) return false; $param = ''; if(!empty($case->lib)) $param = "lib={$case->lib}"; if(!empty($case->product)) $param = "product={$case->product}"; $result = $this->loadModel('common')->removeDuplicate('case', $case, $param); if($result and $result['stop']) { return $this->send(array('result' => 'fail', 'message' => sprintf($this->lang->duplicate, $this->lang->testcase->common), 'locate' => $this->createLink('testcase', 'view', "caseID={$result['duplicate']}"))); } return true; } /** * 批量创建测试用例前检验数据是否正确。 * Check testcases for batch creating. * * @param array $testcases * @param int $productID * @access protected * @return array */ protected function checkTestcasesForBatchCreate(array $testcases, int $productID): array { $this->loadModel('common'); $requiredErrors = array(); foreach($testcases as $i => $testcase) { /* 检查重复项。 */ /* Check duplicate. */ $result = $this->common->removeDuplicate('testcase', $testcase, "product={$productID}"); if(zget($result, 'stop', false) !== false) { unset($testcases[$i]); continue; } /* 检验必填项。 */ /* Check reuqired. */ foreach(explode(',', $this->config->testcase->create->requiredFields) as $field) { $field = trim($field); if($field && empty($testcases[$i]->{$field})) { $fieldName = $this->config->testcase->form->batchCreate[$field]['type'] != 'array' ? "{$field}[{$i}]" : "{$field}[{$i}][]"; $requiredErrors[$fieldName] = sprintf($this->lang->error->notempty, $this->lang->testcase->{$field}); } } } if(!empty($requiredErrors)) dao::$errors = $requiredErrors; return $testcases; } /** * 批量创建测试用例前检验数据是否正确。 * Check testcases for batch editing. * * @param array $cases * @access protected * @return array */ protected function checkCasesForBatchEdit(array $cases): array { $this->loadModel('common'); $requiredErrors = array(); foreach($cases as $i => $testcase) { /* 检验必填项。 */ /* Check reuqired. */ foreach(explode(',', $this->config->testcase->edit->requiredFields) as $field) { $field = trim($field); if($field && empty($cases[$i]->{$field})) { $fieldName = $this->config->testcase->form->batchEdit[$field]['type'] != 'array' ? "{$field}[{$i}]" : "{$field}[{$i}][]"; $requiredErrors[$fieldName] = sprintf($this->lang->error->notempty, $this->lang->testcase->{$field}); } } } if(!empty($requiredErrors)) dao::$errors = $requiredErrors; return $cases; } /** * 初始化用例数据。 * Initialize the testcase. * * @param int $storyID * @param int $testcaseID * @param int $bugID * @access protected * @return object */ protected function initTestcase(int $storyID, int $testcaseID, int $bugID): object { /* 初始化用例。 */ /* Initialize the testcase. */ $case = new stdclass(); $case->type = 'feature'; $case->stage = ''; $case->pri = 3; $case->scene = 0; $case->story = $storyID; $case->title = ''; $case->precondition = ''; $case->keywords = ''; $case->steps = array(); $case->color = ''; $case->auto = ''; /* 如果用例 id 大于 0,使用这个用例数据作为模板。 */ /* If testcaseID large than 0, use this testcase as template. */ if($testcaseID > 0) { $testcase = $this->testcase->getById($testcaseID); $case->product = $testcase->product; $case->type = $testcase->type ? $testcase->type : 'feature'; $case->stage = $testcase->stage; $case->pri = $testcase->pri; $case->scene = $testcase->scene; $case->story = $testcase->story; $case->title = $testcase->title; $case->precondition = $testcase->precondition; $case->keywords = $testcase->keywords; $case->steps = $testcase->steps; $case->color = $testcase->color; $case->auto = $testcase->auto; } /* 如果 bug id 大于 0,使用这个 bug 数据作为模板。 */ /* If bugID large than 0, use this bug as template. */ if($bugID > 0) { $bug = $this->loadModel('bug')->getById($bugID); $case->type = $bug->type; $case->pri = $bug->pri ? $bug->pri : $bug->severity; $case->story = $bug->story; $case->title = $bug->title; $case->keywords = $bug->keywords; $case->steps = $this->testcase->createStepsFromBug($bug->steps); } /* 追加步骤到默认的步骤数。 */ /* Append the steps to the default steps count. */ $case->steps = $this->testcase->appendSteps(!empty($case->steps) ? $case->steps : array()); return $case; } /** * 处理xmind数据 * Process scene data. * * @param array $result * @access protected * @return array */ protected function processScene(array $result): array { $scenes['id'] = $result['id']; $scenes['text'] = $result['title']; $scenes['type'] = 'root'; if(!empty($result['children'])) $scenes['children'] = $this->processChildScene($result['children']['attached'], $result['id'], 'sub'); return $scenes; } /** * 处理xmind的节点数据 * process scene child data. * * @param array $results * @param string $parent * @param string $type * @access protected * @return void */ protected function processChildScene(array $results, string $parent, string $type) { $scenes = array(); foreach($results as $result) { $scene['id'] = $result['id']; $scene['text'] = $result['title']; $scene['parent'] = $parent; $scene['type'] = $type; if(!empty($result['children'])) $scene['children'] = $this->processChildScene($result['children']['attached'], $result['id'], 'node'); $scenes[] = $scene; } return $scenes; } /** * 为展示脑图计算步骤数据。 * Process steps for mindmap. * * @param object $case * @access protected * @return object */ protected function processStepsForMindMap(object $case): object { $mindMapSteps = array(); $mindMapSteps['id'] = $case->id; $mindMapSteps['text'] = $case->title; $mindMapSteps['type'] = 'root'; $stepItem['subSide'] = 'right'; $reverseSteps = array_reverse($case->steps); $stepList = array(); $parentSteps = array(); foreach($reverseSteps as $step) { if(empty($step->id)) continue; $stepItem = array(); $stepItem['id'] = $step->id; $stepItem['text'] = $step->step; $stepItem['type'] = $step->grade == 1 ? 'sub' : 'node'; $stepItem['parent'] = $step->parent > 0 ? $step->parent : $case->id; $stepItem['subSide'] = 'right'; if(isset($parentSteps[$step->id])) $stepItem['children'] = array_reverse($parentSteps[$step->id]); if($step->parent > 0) { $parentSteps[$step->parent][] = $stepItem; } else { $stepList[] = $stepItem; } } $mindMapSteps['children'] = array_reverse($stepList); $case->mindMapSteps = $mindMapSteps; return $case; } /** * 获取分组用例。 * Get group cases. * * @param int $productID * @param string $branch * @param string $groupBy * @param string $caseType * @access protected * @return array */ protected function getGroupCases(int $productID, string $branch, string $groupBy, string $caseType): array { /* 获取用例。 */ /* Get cases. */ $cases = $this->testcase->getModuleCases($productID, $branch, 0, '', 'no', $caseType, $groupBy); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'testcase', false); $cases = $this->loadModel('story')->checkNeedConfirm($cases); $cases = $this->testcase->appendData($cases); /* 获取用例的需求分组。 */ /* Get story groups of cases. */ $groupCases = array(); if($groupBy == 'story') { foreach($cases as $case) $groupCases[$case->story][] = $case; } /* 设置用例的需求分组的行占比。 */ /* Set row span of story group in cases. */ $story = null; foreach($cases as $index => $case) { if($case->storyDeleted) { unset($cases[$index]); continue; } $case->rowspan = 0; if($story !== $case->story) { $story = $case->story; if(!empty($groupCases[$case->story])) $case->rowspan = count($groupCases[$case->story]); } } return $cases; } /** * 创建完 testcase 后的相关处理。 * Relevant processing after create testcase. * * @param object $case * @param int $caseID * @access protected * @return void */ protected function afterCreate(object $case, int $caseID) { /* 设置 cookie。 */ /* Set cookie. */ helper::setcookie('lastCaseModule', (string)$case->module); helper::setcookie('lastCaseScene', (string)$case->scene); helper::setcookie('caseModule', '0'); /* 如果需求关联到项目,把用例也关联到项目。 */ /* If the story is linked project, make the case link the project. */ $this->testcase->syncCase2Project($case, $caseID); } /** * 返回创建 testcase 的结果。 * Respond after creating testcase. * * @param int $caseID * @access protected * @return array */ protected function responseAfterCreate(int $caseID): array { $message = $this->executeHooks($caseID); if(!$message) $message = $this->lang->saveSuccess; if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $message, 'id' => $caseID)); /* If link from no head then reload. */ if(isonlybody()) return $this->send(array('result' => 'success', 'message' => $message, 'closeModal' => true)); /* 判断是否当前一级菜单不是 QA,并且 caseList session 存在,并且 caseList 不是动态页面。 */ /* Use this session link, when the tab is not QA, a session of the case list exists, and the session is not from the Dynamic page. */ $useSession = ($this->app->tab != 'qa' and $this->session->caseList and strpos($this->session->caseList, 'dynamic') === false); $locateLink = $this->app->tab == 'project' ? $this->createLink('project', 'testcase', "projectID={$this->session->project}") : $this->createLink('testcase', 'browse', "productID={$this->post->product}&branch={$this->post->branch}"); return $this->send(array('result' => 'success', 'message' => $message, 'load' => $useSession ? $this->session->caseList : $locateLink)); } /** * 返回批量创建 testcase 的结果。 * Respond after batch creating testcase. * * @param int $productID * @param string|int $branch * @access protected * @return void */ protected function responseAfterBatchCreate(int $productID, string|int $branch): array { if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); if(helper::isAjaxRequest('modal')) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => true)); if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $caseIdList)); helper::setcookie('caseModule', '0'); $currentModule = $this->app->tab == 'project' ? 'project' : 'testcase'; $currentMethod = $this->app->tab == 'project' ? 'testcase' : 'browse'; $projectParam = $this->app->tab == 'project' ? "projectID={$this->session->project}&" : ''; return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $this->createLink($currentModule, $currentMethod, "{$projectParam}productID={$productID}&branch={$branch}&browseType=all¶m=0&caseType=&orderBy=id_desc"))); } /** * 处理评审数据。 * Prepare review data. * * @param int $caseID * @param object $oldCase * @access protected * @return bool|object */ protected function prepareReviewData(int $caseID, object $oldCase): bool|object { $now = helper::now(); $status = $this->testcase->getStatus('review', $oldCase); $case = form::data($this->config->testcase->form->review)->add('id', $caseID) ->setForce('status', $status) ->setDefault('reviewedDate', substr($now, 0, 10)) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) ->join('reviewedBy', ',') ->stripTags($this->config->testcase->editor->review['id'], $this->config->allowedTags) ->get(); if(!$case->result) { dao::$errors['result'] = $this->lang->testcase->mustChooseResult; return false; } return $this->loadModel('file')->processImgURL($case, $this->config->testcase->editor->review['id'], $this->post->uid); } /** * 构建关联用例页面的搜索表单。 * Build search form for link cases. * * @param object $case * @param int $queryID * @access protected * @return void */ protected function buildLinkCasesSearchForm(object $case, int $queryID): void { $actionURL = $this->createLink('testcase', 'linkCases', "caseID={$case->id}&browseType=bySearch&queryID=myQueryID", '', true); $objectID = 0; if($this->app->tab == 'project') $objectID = $case->project; if($this->app->tab == 'execution') $objectID = $case->execution; unset($this->config->testcase->search['fields']['product']); $this->testcase->buildSearchForm($case->product, $this->products, $queryID, $actionURL, $objectID); } /** * 构建关联 bug 页面的搜索表单。 * Build search form for link bugs. * * @param object $case * @param int $queryID * @access protected * @return void */ protected function buildLinkBugsSearchForm(object $case, int $queryID): void { $actionURL = $this->createLink('testcase', 'linkBugs', "caseID={$case->id}&browseType=bySearch&queryID=myQueryID", '', true); $objectID = 0; if($this->app->tab == 'project') $objectID = $case->project; if($this->app->tab == 'execution') $objectID = $case->execution; /* 删除单一项目的计划字段。*/ /* Unset search field 'plan' in single project. */ unset($this->config->bug->search['fields']['product']); if($case->project && ($this->app->tab == 'project' || $this->app->tab == 'execution')) { $project = $this->loadModel('project')->getByID($case->project); if(!$project->hasProduct && $project->model == 'waterfall') unset($this->config->bug->search['fields']['plan']); } $this->bug->buildSearchForm($case->product, $this->products, $queryID, $actionURL, $objectID); } }