Merge remote-tracking branch 'origin/master' into Merge184

This commit is contained in:
tanghucheng
2023-11-21 19:43:29 -05:00
149 changed files with 1597 additions and 1842 deletions
+3 -2
View File
@@ -230,7 +230,8 @@ $config->openMethods[] = 'project.createguide';
$config->openMethods[] = 'task.editteam';
$config->openMethods[] = 'feedback.mergeproductmodule';
$config->openMethods[] = 'zanode.nodelist';
$config->openMethods[] = 'action.restoreStages';
$config->openMethods[] = 'action.restorestages';
$config->openMethods[] = 'action.commentzin';
$config->openMethods[] = 'execution.browse';
$config->openMethods[] = 'testcase.showimport';
$config->openMethods[] = 'testsuite.index';
@@ -710,7 +711,7 @@ $config->bi->pickerHeight = 150;
$config->db->sqliteBlacklist = array('sqlite_queue', 'cron');
$config->hasDropmenuApps = array('program', 'project', 'product', 'execution', 'qa', 'admin', 'bi');
$config->hasBranchMenuModules = array('product', 'story', 'release', 'bug', 'testcase', 'testtask');
$config->excludeDropmenuList = array('program-browse', 'program-productview', 'product-all', 'product-index', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'qa-index');
$config->excludeDropmenuList = array('program-browse', 'program-productview', 'program-kanban', 'product-all', 'product-index', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'qa-index');
$config->hasSwitcherModules = array('design');
$config->hasSwitcherMethods = array('project-bug', 'project-testcase', 'execution-bug', 'execution-testcase', 'testtask-cases', 'testtask-view', 'testtask-report', 'testtask-groupcase', 'testtask-linkcase');
$config->excludeSwitcherList = array();
-1
View File
@@ -90,4 +90,3 @@ INSERT INTO `zt_config` ( `vision`, `owner`, `module`, `section`, `key`, `value`
REPLACE INTO `zt_chart`(`id`, `name`, `dimension`, `type`, `group`, `dataset`, `desc`, `settings`, `filters`, `step`, `fields`, `langs`, `sql`, `stage`, `builtin`, `objects`, `createdBy`, `createdDate`, `editedBy`, `editedDate`, `deleted`) VALUES (1030, '宏观数据-禅道使用时长', 1, 'card', '58', '', '', '{\"value\": {\"type\": \"value\", \"field\": \"period\", \"agg\": \"value\"}, \"title\": {\"type\": \"text\", \"name\": \"\"},\n\"type\": \"value\"\n}', '[]', 0, '', NULL, ' SELECT if(t2.`year` > 0, concat(t2.`year`, \'年\', t2.`day`, \'天\'), concat(t2.`day`, \'天\')) as period from (\r\nSELECT TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) AS `year`,DATEDIFF(DATE_SUB(t1.today,INTERVAL TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) YEAR), t1.firstDay) AS `day` \r\nFROM (SELECT `value` AS firstDay, now() AS today FROM zt_config WHERE `owner` = \'system\' AND `key` = \'installedDate\') AS t1\r\n) t2', 'published', 1, '', 'system', '2022-12-07 14:59:41', '', '2022-12-07 14:59:41', 0);
REPLACE INTO `zt_chart`(`id`, `name`, `dimension`, `type`, `group`, `dataset`, `desc`, `settings`, `filters`, `step`, `fields`, `langs`, `sql`, `stage`, `builtin`, `objects`, `createdBy`, `createdDate`, `editedBy`, `editedDate`, `deleted`) VALUES (20015, '使用数据分析-上线时间', 1, 'card', '58', '0', ' ', '{\"value\": {\"type\": \"value\", \"field\": \"date\", \"agg\": \"value\"}, \"title\": {\"type\": \"text\", \"name\": \"\"}, \"type\": \"value\"}', '[]', 0, ' ', NULL, 'select `value` as date from zt_config where `owner` = \'system\' and `key` = \'installedDate\'', 'published', 1, ' ', 'system', '2023-08-16 15:32:10', 'admin', '2023-08-16 15:32:17', 0);
+20
View File
@@ -716,4 +716,24 @@ class router extends baseRouter
return parent::mergeParams($defaultParams, $passedParams);
}
/**
* 加载一个模块:
*
* Load a module.
*
* @access public
* @return bool|object if the module object of die.
*/
public function loadModule()
{
/* 不能直接请求基类的方法 Cannot call methods of base control class. */
if(method_exists('Control', $this->methodName))
{
echo 'Cannot call methods of base control class.';
return false;
}
return parent::loadModule();
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ class formGroup extends wg
zui::width($width),
set($this->getRestProps()),
setCssVar('form-grid-label-width', $labelWidth),
empty($label) ? null : new formLabel
empty($label) && $label !== '0' ? null : new formLabel
(
set::className($labelClass, $strong ? 'font-bold' : null),
set::required($required),
+47 -37
View File
@@ -139,38 +139,40 @@ class header extends wg
if(!isset($app->user)) return;
$user = $app->user;
$isGuest = $user->account == 'guest';
$items = array();
$user = $app->user;
$isGuest = $user->account == 'guest';
$items = array();
$modalClass = (isset($config->zin->mode) && $config->zin->mode == 'compatible') ? 'open-in-parent' : null;
if(!$isGuest)
{
$noRole = empty($user->role) || !isset($lang->user->roleList[$user->role]);
$items[] = array
(
'url' => createLink('my', 'profile', '', '', true),
'url' => createLink('my', 'profile', '', '', true),
'leadingClass' => 'row items-center gap-2 px-2 py-1 text-inherit',
'icon' => ' hidden',
'title' => empty($user->realname) ? $user->account : $user->realname,
'titleClass' => 'text-lg',
'subtitle' => $noRole ? null : $lang->user->roleList[$user->role],
// 'style' => array('padding-left' => 0),
'data-toggle' => 'modal',
'data-size' => 700,
'data-id' => 'profile',
'leading' => array('html' => userAvatar(set::user($user))->render(), 'className' => 'center mr-1')
'icon' => ' hidden',
'title' => empty($user->realname) ? $user->account : $user->realname,
'titleClass' => 'text-lg',
'subtitle' => $noRole ? null : $lang->user->roleList[$user->role],
'innerClass' => $modalClass,
'data-toggle' => 'modal',
'data-size' => 700,
'data-id' => 'profile',
'leading' => array('html' => userAvatar(set::user($user))->render(), 'className' => 'center mr-1')
);
$items[] = array('type' => 'divider');
$items[] = array
(
'url' => createLink('my', 'profile', '', '', true),
'icon' => 'account',
'text' => $lang->profile,
'url' => createLink('my', 'profile', '', '', true),
'icon' => 'account',
'text' => $lang->profile,
'innerClass' => $modalClass,
'data-toggle' => 'modal',
'data-size' => 700,
'data-id' => 'profile'
'data-size' => 700,
'data-id' => 'profile'
);
if($app->config->vision === 'rnd')
@@ -179,26 +181,28 @@ class header extends wg
{
$items[] = array
(
'url' => createLink('tutorial', 'start'),
'icon' => 'guide',
'text' => $lang->tutorialAB,
'class' => '800',
'outerClass' => 'user-tutorial',
'data-width' => 700,
'url' => createLink('tutorial', 'start'),
'icon' => 'guide',
'text' => $lang->tutorialAB,
'class' => '800',
'outerClass' => 'user-tutorial',
'data-width' => 700,
'data-class-name' => 'modal-inverse',
'data-headerless' => true,
'data-backdrop' => true,
'data-keyboard' => true,
'data-toggle' => 'modal'
'data-backdrop' => true,
'data-keyboard' => true,
'innerClass' => $modalClass,
'data-toggle' => 'modal'
);
}
$items[] = array
(
'url' => createLink('my', 'preference', 'showTip=false', '', true),
'icon' => 'controls',
'text' => $lang->preference,
'data-width' => 700,
'url' => createLink('my', 'preference', 'showTip=false', '', true),
'icon' => 'controls',
'text' => $lang->preference,
'data-width' => 700,
'innerClass' => $modalClass,
'data-toggle' => 'modal'
);
}
@@ -207,11 +211,12 @@ class header extends wg
{
$items[] = array
(
'url' => createLink('my', 'changepassword', '', '', true),
'icon' => 'cog-outline',
'text' => $lang->changePassword,
'url' => createLink('my', 'changepassword', '', '', true),
'icon' => 'cog-outline',
'text' => $lang->changePassword,
'innerClass' => $modalClass,
'data-toggle' => 'modal',
'data-size' => 'sm'
'data-size' => 'sm'
);
}
@@ -241,12 +246,12 @@ class header extends wg
$helpItems = array();
$manualUrl = ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme'];
$helpItems[] = array('text' => $lang->manual, 'url' => $manualUrl, 'attrs' => array('data-app' => 'help'));
$helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal');
$helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
$items[] = array('text' => $lang->help, 'icon' => 'help', 'items' => $helpItems);
/* printClientLink */
$items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal');
$items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
$items[] = array('type' => 'html', 'className' => 'menu-item', 'html' => $lang->designedByAIUX);
$items[] = array('type' => 'divider');
@@ -290,6 +295,7 @@ class header extends wg
/* Initialize the default values. */
$showCreateList = $needPrintDivider = false;
$modalClass = (isset($config->zin->mode) && $config->zin->mode == 'compatible') ? 'open-in-parent' : null;
/* Get default product id. */
$productID = isset($_SESSION['product']) ? $_SESSION['product'] : 0;
@@ -347,6 +353,7 @@ class header extends wg
case 'doc':
$params = "objectType=&objectID=0&libID=0";
$createMethod = 'selectLibType';
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
break;
case 'project':
@@ -358,6 +365,7 @@ class header extends wg
{
$params = "programID=0&from=global";
$createMethod = 'createGuide';
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
}
else
@@ -411,11 +419,13 @@ class header extends wg
break;
case 'kanbanspace':
$isOnlyBody = true;
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
$item['data-width'] = '75%';
break;
case 'kanban':
$isOnlyBody = true;
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
$item['data-width'] = '75%';
break;
+11 -1
View File
@@ -20,6 +20,7 @@ class history extends wg
'actions?: array', // 操作列表数据。
'users?: array', // 用户 Map 数据。
'commentUrl?: string', // 备注对话框 URL。
'editCommentUrl?: string', // 修改备注对话框 URL。
'title?: string|array', // 标题。
'commentBtn?: string|array' // 是否允许添加备注。
);
@@ -74,7 +75,15 @@ class history extends wg
protected function build(): wg
{
list($panel, $objectID, $objectType, $title, $actions, $commentUrl, $commentBtn) = $this->prop(array('panel', 'objectID', 'objectType', 'title', 'actions', 'commentUrl', 'commentBtn'));
global $config;
list($panel, $objectID, $objectType, $title, $actions, $commentUrl, $editCommentUrl, $commentBtn) = $this->prop(array('panel', 'objectID', 'objectType', 'title', 'actions', 'commentUrl', 'editCommentUrl', 'commentBtn'));
if(isset($config->zin->mode) && $config->zin->mode == 'compatible')
{
if(is_null($commentUrl)) $commentUrl = createLink('action', 'commentZin', "objectType=$objectType&objectID=$objectID");
if(is_null($editCommentUrl)) $editCommentUrl = createLink('action', 'editCommentZin', 'actionID={actionID}');
}
return zui::historyPanel
(
@@ -84,6 +93,7 @@ class history extends wg
set::actions($actions),
set::title($title),
set::commentUrl($commentUrl),
set::editCommentUrl($editCommentUrl),
set::commentBtn($commentBtn),
set($this->getRestProps())
);
+1
View File
@@ -1,3 +1,4 @@
#mainNavbar {padding-left: 1rem; padding-right: 1rem;}
#mainNavbar .main-navbar-left {position: absolute; z-index: 2;}
#mainNavbar .main-navbar-left #switcher {position: relative; top: 5px; border: 1px solid rgb(var(--color-primary-500-rgb));}
#mainNavbar .main-navbar-left #switcher > .dropmenu-btn {background: #FFF;}
+1 -1
View File
@@ -139,7 +139,7 @@ class mainNavbar extends nav
if(in_array("$moduleName-$methodName", is_array($config->excludeSwitcherList) ? $config->excludeSwitcherList : array())) return null;
if(in_array($moduleName, is_array($config->hasSwitcherModules) ? $config->hasSwitcherModules : array()) || in_array("$moduleName-$methodName", is_array($config->hasSwitcherMethods) ? $config->hasSwitcherMethods : array()))
if(in_array($moduleName, (isset($config->hasSwitcherModules) && is_array($config->hasSwitcherModules)) ? $config->hasSwitcherModules : array()) || in_array("$moduleName-$methodName", (isset($config->hasSwitcherMethods) && is_array($config->hasSwitcherMethods)) ? $config->hasSwitcherMethods : array()))
{
$ajaxMethod = 'ajaxSwitcherMenu';
if($moduleName == 'testcase' && $app->tab == 'project') $moduleName = 'project';
+6 -2
View File
@@ -146,7 +146,11 @@ class moduleMenu extends wg
private function buildCloseBtn(): wg|null
{
$closeLink = $this->prop('closeLink');
$closeLink = $this->prop('closeLink');
$tab = $this->prop('app');
$titleAttrs = array();
if($tab) $titleAttrs['app'] = $tab;
if(isInModal()) $titleAttrs['load'] = 'modal';
if(!$closeLink) return null;
$activeKey = $this->prop('activeKey');
@@ -155,7 +159,7 @@ class moduleMenu extends wg
return a
(
set('href', $closeLink),
isInModal() ? set('data-load', 'modal') : null,
$titleAttrs ? setData($titleAttrs) : null,
icon('close', setStyle('color', 'var(--color-slate-600)'))
);
}
+9 -3
View File
@@ -64,7 +64,8 @@ class pageBase extends wg
$themeName = $app->cookie->theme;
$zuiPath = $config->zin->zuiPath;
$jsConfig->zin = (isset($config->zin->mode) && $config->zin->mode) ? $config->zin->mode : true;
$zinMode = isset($config->zin->mode) ? $config->zin->mode : '';
$jsConfig->zin = !empty($zinMode) ? $zinMode : true;
$headImports = array();
if($zui)
@@ -74,10 +75,15 @@ class pageBase extends wg
$headImports[] = h::importJs($zuiPath . 'zui.zentao.umd.cjs', setID('zuiJS'));
$headImports[] = h::jsCall('$.setLibRoot', $zuiPath);
if(isset($config->zin->extraCSS) && $config->zin->extraCSS) $headImports[] = h::importCss($webRoot . 'js/zui3/' . $config->zin->extraCSS);
$extraCSS = isset($config->zin->extraCSS) ? $config->zin->extraCSS : '';
if(!empty($extraCSS)) $headImports[] = h::importCss($webRoot . 'js/zui3/' . $extraCSS);
}
$headImports[] = h::jsVar('window.config', $jsConfig, setID('configJS'));
if($zui) $headImports[] = h::importJs($webRoot . 'js/zui3/zin.js', setID('zinJS'));
if($zui)
{
$extraJS = isset($config->zin->extraJS) ? $config->zin->extraJS : 'zin.js';
if(!empty($extraJS)) $headImports[] = h::importJs($webRoot . 'js/zui3/' . $extraJS);
}
if($config->debug)
{
+7
View File
@@ -33,6 +33,7 @@ class searchForm extends wg
'simple?: boolean', // 是否为简单模式,不包含保存搜索条件和已保存的查询条件侧边栏。
'setting?: array', // 默认配置。
'url?: string', // 配置加载地址,默认为 search-buildForm-module。
'searchUrl?: string', // 搜索时提交表单的 URL。
'searchLoader?: string|array' // 搜索时 loadPage 参数。
);
@@ -44,6 +45,12 @@ class searchForm extends wg
*/
protected function build(): wg
{
global $config;
if(isset($config->zin->mode) && $config->zin->mode == 'compatible')
{
if(!$this->hasProp('url')) $this->setProp('url', createLink('search', 'buildZinForm', 'module=' . $this->prop('module')));
if(!$this->hasProp('searchUrl')) $this->setProp('searchUrl', createLink('search', 'buildZinQuery'));
}
return zui::searchForm(inherit($this));
}
}
+14 -5
View File
@@ -7,7 +7,9 @@ class searchToggle extends wg
protected static array $defineProps = array(
'target?: string',
'open?: bool',
'module?: string'
'module?: string',
'url?: string',
'searchUrl?: string'
);
protected function checkErrors()
@@ -17,19 +19,26 @@ class searchToggle extends wg
protected function build(): wg
{
global $lang, $app;
list($target, $module, $open) = $this->prop(array('target', 'module', 'open'));
global $lang, $app, $config;
list($target, $module, $open, $url, $searchUrl) = $this->prop(array('target', 'module', 'open', 'url', 'searchUrl'));
if(is_null($open) && !empty($_GET['browseType'])) $open = $_GET['browseType'] === 'bySearch';
if(is_null($module)) $module = $app->rawModule;
if(isset($config->zin->mode) && $config->zin->mode == 'compatible')
{
if(is_null($url)) $url = createLink('search', 'buildZinForm', 'module=' . $this->prop('module'));
if(is_null($searchUrl)) $searchUrl = createLink('search', 'buildZinQuery');
}
return btn
(
set::className('ghost search-form-toggle'),
set::icon('search'),
set::active($open),
set::text($lang->searchAB),
toggle::searchform(array('module' => $module, 'target' => $target)),
$open ? h::jsCall('~zui.toggleSearchForm', array('module' => $module, 'target' => $target, 'show' => true)) : null
toggle::searchform(array('module' => $module, 'target' => $target, 'url' => $url, 'searchUrl' => $searchUrl)),
$open ? h::jsCall('~zui.toggleSearchForm', array('module' => $module, 'target' => $target, 'show' => true, 'url' => $url, 'searchUrl' => $searchUrl)) : null
);
}
}
+1 -1
View File
@@ -194,7 +194,7 @@ class StepsEditor extends zui.Component
reset(data, skipRender)
{
this._items = array();
this._items = [];
this.update(data, skipRender);
}
+3 -6
View File
@@ -638,7 +638,7 @@ class actionModel extends model
* @access public
* @return string
*/
public function formatActionComment($comment): string
public function formatActionComment(string $comment): string
{
if(str_contains($comment, '<pre class="prettyprint lang-html">'))
{
@@ -704,10 +704,7 @@ class actionModel extends model
public function printAction(object $action, string $desc = '')
{
$content = $this->renderAction($action, $desc);
if(is_string($content))
{
echo $content;
}
if(is_string($content)) echo $content;
return;
}
@@ -1551,7 +1548,7 @@ class actionModel extends model
* @access public
* @return array
*/
public function processDynamicForAPI($dynamics): array
public function processDynamicForAPI(array $dynamics): array
{
/* 获取用户列表。 */
/* Get user list. */
+1 -6
View File
@@ -76,7 +76,7 @@ class productEntry extends entry
$product->actions = $this->loadModel('action')->processActionForAPI($actions, $users, $this->lang->product);
break;
case 'lastexecution':
$execution = $this->dao->select('t2.id,t2.name,t2.type')->from(TABLE_PROJECTPRODUCT)->alias('t1')
$execution = $this->dao->select('t2.id,t2.name,t2.type,t2.progress')->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)
@@ -84,11 +84,6 @@ class productEntry extends entry
->orderBy('t2.id desc')
->limit(1)
->fetch();
if($execution)
{
$workhour = $this->loadModel('project')->computeProgress(array($execution->id => $execution));
if(isset($workhour[$execution->id])) $execution->progress = $workhour[$execution->id]->progress;
}
$product->lastExecution = $execution;
break;
+4 -4
View File
@@ -15,10 +15,10 @@ jsVar('delayInfo', $lang->project->delayInfo);
foreach($executionStats as $scrum)
{
$scrum->totalEstimate = zget($scrum->hours, 'totalEstimate', 0) . $lang->execution->workHourUnit;
$scrum->totalConsumed = zget($scrum->hours, 'totalConsumed', 0) . $lang->execution->workHourUnit;
$scrum->totalLeft = zget($scrum->hours, 'totalLeft', 0) . $lang->execution->workHourUnit;
$scrum->progress = zget($scrum->hours, 'progress', 0);
$scrum->totalEstimate = zget($scrum, 'estimate', 0) . $lang->execution->workHourUnit;
$scrum->totalConsumed = zget($scrum, 'consumed', 0) . $lang->execution->workHourUnit;
$scrum->totalLeft = zget($scrum, 'left', 0) . $lang->execution->workHourUnit;
$scrum->progress = zget($scrum, 'progress', 0);
}
if(!$longBlock)
+1 -15
View File
@@ -755,21 +755,7 @@ class blockZen extends block
$type = isset($block->params->type) ? $block->params->type : 'all';
$orderBy = isset($block->params->orderBy) ? $block->params->orderBy : 'id_desc';
$projects = $this->loadModel('project')->getOverviewList($type, 0, $orderBy, $count);
/* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
$tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('project')->in(array_keys($projects))
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
$hours = $this->loadModel('program')->computeProjectHours($tasks);
$projects = $this->program->appendStatToProjects($projects, 'hours', array('hours' => $hours));
foreach($projects as $project) $project->progress = $project->hours->progress;
$this->view->projects = $projects;
$this->view->projects = $this->loadModel('project')->getOverviewList($type, 0, $orderBy, $count);
$this->view->users = $this->loadModel('user')->getPairs('noletter', '', 0, array_unique(array_column($this->view->projects, 'PM')));
}
+1
View File
@@ -335,6 +335,7 @@ formPanel
formGroup
(
set::label($lang->bug->steps),
set::required(strpos(",{$this->config->bug->create->requiredFields},", ",steps,") !== false),
editor
(
set::name('steps'),
+1
View File
@@ -87,6 +87,7 @@ class caselibModel extends model
$oldLib = $this->dao->select('*')->from(TABLE_TESTSUITE)->where('id')->eq($lib->id)->fetch();
$this->lang->testsuite->name = $this->lang->caselib->name;
$this->lang->testsuite->desc = $this->lang->caselib->desc;
$this->dao->update(TABLE_TESTSUITE)->data($lib, 'uid')
->autoCheck()
+2 -4
View File
@@ -31,10 +31,8 @@ formPanel
formGroup
(
set::label($lang->caselib->desc),
editor
(
set::name('desc')
)
set::control('editor'),
set::name('desc')
)
);
+1
View File
@@ -24,6 +24,7 @@ formPanel
formGroup
(
set::label($lang->caselib->desc),
set::required(strpos(",{$this->config->caselib->edit->requiredFields},", ",desc,") !== false),
editor
(
set::name('desc'),
+3 -2
View File
@@ -5,8 +5,9 @@ $config->cron->edit = new stdclass();
$config->cron->create->requiredFields = 'm,h,dom,mon,dow,command';
$config->cron->edit->requiredFields = 'm,h,dom,mon,dow,command';
$config->cron->maxRunDays = 8;
$config->cron->maxRunTime = 65;
$config->cron->maxRunDays = 8;
$config->cron->maxRunTime = 65;
$config->cron->maxConsumer = 2;
global $lang;
$config->cron->dtable = new stdclass();
+2 -60
View File
@@ -115,64 +115,6 @@ class cron extends control
return $this->sendSuccess(array('load' => true));
}
/**
* Schedule cron task by RoadRunner.
*
* @access public
* @return void
*/
public function rrSchedule()
{
if('cli' !== PHP_SAPI) return;
set_time_limit(0);
session_write_close();
$this->loadModel('common');
$execId = mt_rand();
while(true)
{
dao::$cache = array();
if(empty($this->config->global->cron) || !$this->canSchedule($execId))
{
sleep(60);
continue;
}
$this->schedule($execId);
sleep(30);
}
}
/**
* Consume cron task by RoadRunner.
*
* @access public
* @return void
*/
public function rrConsume()
{
if('cli' !== PHP_SAPI) return;
set_time_limit(0);
session_write_close();
$this->loadModel('common');
while(true)
{
if(empty($this->config->global->cron))
{
sleep(60);
continue;
}
$this->execTasks(mt_rand());
sleep(10);
}
}
/**
* 使用Ajax请求执行定时任务.
* Ajax execute cron.
@@ -315,8 +257,8 @@ class cron extends control
{
if($setting->value > $expirDate)
{
if($setting->key == strval($execId)) $roles[] = 'consumer';
$consumerCount ++;
if($consumer < $this->config->cron->maxConsumer && $setting->key == strval($execId)) $roles[] = 'consumer';
}
else
{
@@ -326,7 +268,7 @@ class cron extends control
}
if(in_array($scheduler['execId'], array(0, $execId)) || $scheduler['lastTime'] < $expirDate) $roles[] = 'scheduler';
if($consumerCount < 4) $roles[] = 'consumer';
if($consumerCount < $this->config->cron->maxConsumer) $roles[] = 'consumer';
return $roles;
}
+21 -262
View File
@@ -12,277 +12,52 @@
class custom extends control
{
/**
* Index
* Index.
*
* @access public
* @return void
*/
public function index()
{
if($this->config->vision == 'lite') return print(js::locate(inlink('execution')));
if($this->config->vision == 'lite') return $this->locate(inlink('execution'));
if(common::hasPriv('custom', 'set')) return print(js::locate(inlink('set', "module=project&field=" . key($this->lang->custom->project->fields))));
if(common::hasPriv('custom', 'product')) return print(js::locate(inlink('product')));
if(common::hasPriv('custom', 'execution')) return print(js::locate(inlink('execution')));
if(common::hasPriv('custom', 'set')) return $this->locate(inlink('set', "module=project&field=" . key($this->lang->custom->project->fields)));
if(common::hasPriv('custom', 'product')) return $this->locate(inlink('product'));
if(common::hasPriv('custom', 'execution')) return $this->locate(inlink('execution'));
foreach($this->lang->custom->system as $sysObject)
{
if(common::hasPriv('custom', $sysObject)) return print(js::locate(inlink($sysObject)));
if(common::hasPriv('custom', $sysObject)) return $this->locate(inlink($sysObject));
}
}
/**
* Custom
* 设置对象字段的语言项。
* Set the language items of the object fields.
*
* @param string $module
* @param string $field
* @param string $lang
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @param string $lang all|zh-cn|zh-tw|en|de|fr
* @access public
* @return void
*/
public function set(string $module = 'story', string $field = 'priList', string $lang = '')
{
if(empty($lang)) $lang = $this->app->getClientLang();
if($module == 'user' and $field == 'priList') $field = 'statusList';
if($module == 'block' and $field == 'priList')$field = 'closed';
if($module == 'user' && $field == 'priList') $field = 'statusList';
if($module == 'block' && $field == 'priList')$field = 'closed';
$currentLang = $this->app->getClientLang();
$this->app->loadLang($module);
if($lang == 'all')
{
$fieldList = array();
$items = $this->custom->getItems("lang=all&module=$module&section=$field&vision={$this->config->vision}");
foreach($items as $key => $item)
{
$fieldList[$key] = $item->value;
}
}
else
{
$fieldList = zget($this->lang->$module, $field, '');
}
if($module == 'project' and $field == 'unitList')
{
$this->app->loadConfig($module);
$unitList = zget($this->config->$module, 'unitList', '');
$this->view->unitList = explode(',', $unitList);
$this->view->defaultCurrency = zget($this->config->$module, 'defaultCurrency', 'CNY');
}
if(($module == 'story' or $module == 'demand') and $field == 'reviewRules')
{
$this->app->loadConfig($module);
$this->view->reviewRule = zget($this->config->$module, 'reviewRules', 'allpass');
$this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
$this->view->superReviewers = zget($this->config->$module, 'superReviewers', '');
}
if(($module == 'story' or $module == 'testcase' or $module == 'demand') and $field == 'review')
{
$this->app->loadConfig($module);
$this->loadModel('user');
if($module == 'story' or $module == 'demand')
{
$this->view->depts = $this->loadModel('dept')->getDeptPairs();
$this->view->forceReviewRoles = zget($this->config->$module, 'forceReviewRoles', '');
$this->view->forceReviewDepts = zget($this->config->$module, 'forceReviewDepts', '');
$this->view->forceNotReviewRoles = zget($this->config->$module, 'forceNotReviewRoles', '');
$this->view->forceNotReviewDepts = zget($this->config->$module, 'forceNotReviewDepts', '');
}
$this->view->users = $module == 'story' ? $this->user->getCanCreateStoryUsers() : $this->user->getPairs('noclosed|nodeleted');
$this->view->needReview = zget($this->config->$module, 'needReview', 1);
$this->view->forceReview = zget($this->config->$module, 'forceReview', '');
$this->view->forceNotReview = zget($this->config->$module, 'forceNotReview', '');
}
if($module == 'bug' and $field == 'longlife')
{
$this->app->loadConfig('bug');
$this->view->longlife = $this->config->bug->longlife;
}
if($module == 'block' and $field == 'closed')
{
$this->loadModel('block');
$closedBlock = isset($this->config->block->closed) ? $this->config->block->closed : '';
$this->view->blockPairs = $this->block->getClosedBlockPairs($closedBlock);
$this->view->closedBlock = $closedBlock;
}
if($module == 'user' and $field == 'deleted')
{
$this->app->loadConfig('user');
$this->view->showDeleted = isset($this->config->user->showDeleted) ? $this->config->user->showDeleted : '0';
}
$this->customZen->assignVarsForSet($module, $field, $lang, $currentLang);
if(strtolower($this->server->request_method) == "post")
{
$postArray = fixer::input('post');
$keys = array();
if(isset($postArray->data->keys))
{
foreach($postArray->data->keys as $key)
{
if($module == 'testtask' and $field == 'typeList' and empty($key)) continue;
if($key && in_array($key, $keys)) return $this->send(array('result' => 'fail', 'message' => sprintf($this->lang->custom->notice->repeatKey, $key)));
$keys[] = $key;
}
}
if($module == 'project' and $field == 'unitList')
{
$data = fixer::input('post')->join('unitList', ',')->get();
if(empty($data->unitList)) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->currencyNotEmpty));
if(empty($data->defaultCurrency)) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->defaultNotEmpty));
$this->loadModel('setting')->setItems("system.$module", $data);
}
elseif(($module == 'story' or $module == 'demand') and $field == 'review')
{
$data = fixer::input('post')
->setDefault('forceReview', '')
->setDefault('forceNotReview', '')
->setDefault('forceReviewRoles', '')
->setDefault('forceNotReviewRoles', '')
->setDefault('forceReviewDepts', '')
->setDefault('forceNotReviewDepts', '')
->join('forceReview', ',')
->join('forceReviewRoles', ',')
->join('forceReviewDepts', ',')
->join('forceNotReview', ',')
->join('forceNotReviewRoles', ',')
->join('forceNotReviewDepts', ',')
->get();
foreach($data as $key => $value)
{
if($key == 'needReview') continue;
if(strpos($key, 'Not') and $data->needReview == 0) $data->$key = '';
if(!strpos($key, 'Not') and $data->needReview == 1) $data->$key = '';
}
$this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
}
elseif(($module == 'story' or $module == 'demand') and $field == 'reviewRules')
{
$data = fixer::input('post')->setDefault('superReviewers', '')->join('superReviewers', ',')->get();
$this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
}
elseif($module == 'testcase' and $field == 'review')
{
$review = fixer::input('post')->get();
if($review->needReview) $data = fixer::input('post')->setDefault('forceNotReview', '')->join('forceNotReview', ',')->remove('forceReview')->get();
if(!$review->needReview) $data = fixer::input('post')->setDefault('forceReview', '')->join('forceReview', ',')->remove('forceNotReview')->get();
$this->loadModel('setting')->setItems("system.$module", $data);
$reviewCase = isset($review->reviewCase) ? $review->reviewCase : 0;
if($review->needReview == 0 and $reviewCase)
{
$waitCases = $this->loadModel('testcase')->getByStatus(0, 0, 'all', 'wait');
$this->testcase->batchReview(array_keys($waitCases), 'pass');
}
}
elseif($module == 'bug' and $field == 'longlife')
{
$this->loadModel('setting')->setItems('system.bug', fixer::input('post')->get());
}
elseif($module == 'block' and $field == 'closed')
{
$data = fixer::input('post')->join('closed', ',')->get();
$this->loadModel('setting')->setItem('system.block.closed', zget($data, 'closed', ''));
}
elseif($module == 'user' and $field == 'contactField')
{
$data = fixer::input('post')->join('contactField', ',')->get();
if(!isset($data->contactField)) $data->contactField = '';
$this->loadModel('setting')->setItem('system.user.contactField', $data->contactField);
}
elseif($module == 'user' and $field == 'deleted')
{
$data = fixer::input('post')->get();
$this->loadModel('setting')->setItem('system.user.showDeleted', $data->showDeleted);
}
else
{
if(!$this->post->keys) return $this->sendError(sprintf($this->lang->error->notempty, $this->lang->custom->key));
$lang = $_POST['lang'];
$oldCustoms = $this->custom->getItems("lang=$lang&module=$module&section=$field");
foreach($_POST['keys'] as $index => $key)
{
if(!empty($key)) $key = trim($key);
/* Invalid key. It should be numbers. (It includes severityList in bug module and priList in story, task, bug, testcasea, testtask and todo module.) */
if($field == 'priList' or $field == 'severityList')
{
if(!is_numeric($key) or $key > 255) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidNumberKey));
}
if(!empty($key) and !isset($oldCustoms[$key]) and $key != 'n/a' and !validater::checkREG($key, '/^[a-z_A-Z_0-9]+$/')) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStringKey));
/* The length of roleList in user module is less than 10. check it when saved. */
if($module == 'user' and $field == 'roleList' and strlen($key) > 10) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['ten']));
/* The length of typeList in todo module is less than 15. check it when saved. */
if($module == 'todo' and $field == 'typeList' and strlen($key) > 15) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['fifteen']));
/* The length of sourceList in story module and typeList in task module is less than 20, check it when saved. */
if(($module == 'story' and $field == 'sourceList') or ($module == 'task' and $field == 'typeList'))
{
if(strlen($key) > 20) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['twenty']));
}
/* The length of stageList in testcase module is less than 255, check it when saved. */
if($module == 'testcase' and $field == 'stageList' and strlen($key) > 255) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['twoHundred']));
/* The length of field that in bug and testcase module and reasonList in story and task module is less than 30, check it when saved. */
if(in_array($module, array('bug', 'testcase')) or (in_array($module, array('story', 'task')) and $field == 'reasonList'))
{
if(strlen($key) > 30) return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->invalidStrlen['thirty']));
}
}
$this->custom->deleteItems("lang=$lang&module=$module&section=$field&vision={$this->config->vision}");
if($lang == 'all') $this->custom->deleteItems("lang=$currentLang&module=$module&section=$field&vision={$this->config->vision}");
$data = fixer::input('post')->get();
$emptyKey = false;
foreach($data->keys as $index => $key)
{
if(!$key && $emptyKey) continue;
//if(!$system and (!$value or !$key)) continue; //Fix bug #951.
$value = $data->values[$index];
$system = $data->systems[$index];
if($key and trim($value) === '') return $this->send(array('result' => 'fail', 'message' => $this->lang->custom->notice->valueEmpty)); // Fix bug #23538.
$this->custom->setItem("{$lang}.{$module}.{$field}.{$key}.{$system}", $value);
if(!$key) $emptyKey = true;
}
}
$this->customZen->setFieldListForSet($module, $field);
if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
return $this->sendSuccess(array('load' => $this->createLink('custom', 'set', "module=$module&field=$field&lang=" . ($lang == 'all' ? $lang : ''))));
}
/* Check whether the current language has been customized. */
$lang = str_replace('_', '-', $lang);
$dbFields = $this->custom->getItems("lang=$lang&module=$module&section=$field&vision={$this->config->vision}");
if(empty($dbFields)) $dbFields = $this->custom->getItems("lang=" . ($lang == $currentLang ? 'all' : $currentLang) . "&module=$module&section=$field");
if($dbFields)
{
$dbField = reset($dbFields);
if($lang != $dbField->lang)
{
$lang = str_replace('-', "_", $dbField->lang);
foreach($fieldList as $key => $value)
{
if(isset($dbFields[$key]) and $value != $dbFields[$key]->value) $fieldList[$key] = $dbFields[$key]->value;
}
}
}
$this->view->title = $this->lang->custom->common . $this->lang->colon . $this->lang->$module->common;
$this->view->fieldList = $fieldList;
$this->view->dbFields = $dbFields;
$this->view->field = $field;
$this->view->lang2Set = str_replace('_', '-', $lang);
$this->view->module = $module;
@@ -293,6 +68,7 @@ class custom extends control
}
/**
* 还原默认语言项。删除相关项。
* Restore the default lang. Delete the related items.
*
* @param string $module
@@ -302,38 +78,21 @@ class custom extends control
*/
public function restore(string $module, string $field)
{
if($module == 'user' and $field == 'contactField')
if($module == 'user' && $field == 'contactField')
{
$this->loadModel('setting')->deleteItems("module=$module&key=$field");
$this->loadModel('setting')->deleteItems("module={$module}&key={$field}");
}
else
{
$this->custom->deleteItems("module=$module&section=$field");
$this->custom->deleteItems("module={$module}&section={$field}");
}
return $this->sendSuccess(array('load' => true));
}
/**
* Set working mode function.
*
* @access public
* @return void
*/
public function working()
{
if($_POST)
{
$this->loadModel('setting')->setItem('system.common.global.flow', $this->post->flow);
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'reload'));
}
$this->view->title = $this->lang->custom->working;
$this->display();
}
/**
* Set Required.
* 设置表单必填字段。
* Set the required fields.
*
* @param string $moduleName
* @access public
+10 -5
View File
@@ -30,8 +30,9 @@ window.changeReview = function(e)
window.changeUnit = function()
{
const defaultCurrency = $('#defaultCurrency').val();
$('#defaultCurrency').empty().append('<option></option>');
const defaultCurrency = $('[name=defaultCurrency]').val();
let defaultCurrencyItems = new Array();
let index = 0;
$('[name^=unitList]').each(function()
{
if($(this).prop('checked'))
@@ -40,11 +41,15 @@ window.changeUnit = function()
const firstStr = $(this).val() + '">';
text = text.substring(text.lastIndexOf(firstStr) + firstStr.length, text.lastIndexOf('<'));
$('#defaultCurrency').append("<option value='" + $(this).val() + "'>" + text + '</option>');
}
defaultCurrencyItems[index] = {'value': $(this).val(), 'text': text};
index ++;
}
});
$('#defaultCurrency').val(defaultCurrency);
let $defaultCurrencyPicker = $('[name=defaultCurrency]').zui('picker');
$defaultCurrencyPicker.render({items: defaultCurrencyItems});
$defaultCurrencyPicker.$.setValue(defaultCurrency);
}
window.savaTestcaseReview = function()
+17 -9
View File
@@ -124,25 +124,28 @@ class customModel extends model
}
/**
* 删除自定义项。
* Delete items.
*
* @param string $paramString see parseItemParam();
* @param string $paramString see parseItemParam();
* @access public
* @return void
* @return bool
*/
public function deleteItems($paramString)
public function deleteItems($paramString): bool
{
$this->prepareSQL($this->parseItemParam($paramString), 'delete')->exec();
return !dao::isError();
}
/**
* 解析选择或删除项的参数字符串。
* Parse the param string for select or delete items.
*
* @param string $paramString lang=xxx&module=story&section=sourceList&key=customer and so on.
* @param string $paramString lang=xxx&module=story&section=sourceList&key=customer and so on.
* @access public
* @return array
*/
public function parseItemParam($paramString)
public function parseItemParam(string $paramString): array
{
/* Parse the param string into array. */
parse_str($paramString, $params);
@@ -150,20 +153,25 @@ class customModel extends model
/* Init fields not set in the param string. */
$fields = 'lang,module,section,key,vision';
$fields = explode(',', $fields);
foreach($fields as $field) if(!isset($params[$field])) $params[$field] = '';
foreach($fields as $field)
{
if(isset($params[$field])) continue;
$params[$field] = '';
}
return $params;
}
/**
* 创建一个DAO对象来选择或删除一条或多条记录。
* Create a DAO object to select or delete one or more records.
*
* @param array $params the params parsed by parseItemParam() method.
* @param string $method select|delete.
* @param array $params the params parsed by parseItemParam() method.
* @param string $method select|delete.
* @access public
* @return object
*/
public function prepareSQL($params, $method = 'select')
public function prepareSQL(array $params, string $method = 'select'): object
{
return $this->dao->$method('*')->from(TABLE_LANG)->where('1 = 1')
->beginIF($params['lang'])->andWhere('lang')->in($params['lang'])->fi()
+14 -13
View File
@@ -68,46 +68,47 @@ class customTest
}
/**
* 删除自定义项。
* Test delete items.
*
* @param string $paramString
* @param string $paramString
* @access public
* @return int
* @return array|bool
*/
public function deleteItemsTest($paramString)
public function deleteItemsTest($paramString): array|bool
{
$objects = $this->objectModel->deleteItems($paramString);
$result = $this->objectModel->deleteItems($paramString);
if(dao::isError()) return dao::getError();
return $objects;
return $result;
}
/**
* 解析选择或删除项的参数字符串。
* Test parse the param string for select or delete items.
*
* @param string $paramString
* @param string $paramString lang=xxx&module=story&section=sourceList&key=customer and so on.
* @access public
* @return array
*/
public function parseItemParamTest($paramString)
public function parseItemParamTest(string $paramString): array
{
$objects = $this->objectModel->parseItemParam($paramString);
$params = $this->objectModel->parseItemParam($paramString);
if(dao::isError()) return dao::getError();
return $objects;
return $params;
}
/**
* 创建一个DAO对象来选择或删除一条或多条记录。
* Test create a DAO object to select or delete one or more records.
*
* @param string $paramString
* @param string $paramString
* @param string $method
* @access public
* @return array|int
*/
public function prepareSQLTest($paramString, $method = 'select')
public function prepareSQLTest(string $paramString, string $method = 'select'): array|int
{
$params = $this->objectModel->parseItemParam($paramString);
if($method == 'delete')
+8 -15
View File
@@ -7,15 +7,8 @@ su('admin');
/**
title=测试 customModel->deleteItems();
timeout=0
cid=1
pid=1
测试参数为空 >> 0
测试参数为lang >> 0
测试参数为lang,module >> 0
测试参数为lang,key,section >> 0
测试参数为lang,key,section,module >> 0
测试参数为lang,key,section,module,vision >> 0
*/
@@ -26,11 +19,11 @@ $key = 'key=1';
$vision = 'vision=rnd';
$paramString = array('', $lang, $lang.'&'.$module, $lang.'&'.$key.'&'.$section, $lang.'&'.$key.'&'.$section.'&'.$module, $lang.'&'.$key.'&'.$section.'&'.$module.'&'.$vision);
$custom = new customTest();
$customTester = new customTest();
r($custom->deleteItemsTest($paramString[0])) && p() && e('0'); //测试参数为空
r($custom->deleteItemsTest($paramString[1])) && p() && e('0'); //测试参数为lang
r($custom->deleteItemsTest($paramString[2])) && p() && e('0'); //测试参数为lang,module
r($custom->deleteItemsTest($paramString[3])) && p() && e('0'); //测试参数为lang,key,section
r($custom->deleteItemsTest($paramString[4])) && p() && e('0'); //测试参数为lang,key,section,module
r($custom->deleteItemsTest($paramString[5])) && p() && e('0'); //测试参数为lang,key,section,module,vision
r($customTester->deleteItemsTest($paramString[0])) && p() && e('1'); //测试参数为空
r($customTester->deleteItemsTest($paramString[1])) && p() && e('1'); //测试参数为lang
r($customTester->deleteItemsTest($paramString[2])) && p() && e('1'); //测试参数为lang,module
r($customTester->deleteItemsTest($paramString[3])) && p() && e('1'); //测试参数为lang,key,section
r($customTester->deleteItemsTest($paramString[4])) && p() && e('1'); //测试参数为lang,key,section,module
r($customTester->deleteItemsTest($paramString[5])) && p() && e('1'); //测试参数为lang,key,section,module,vision
+8 -16
View File
@@ -7,15 +7,8 @@ su('admin');
/**
title=测试 customModel->parseItemParam();
timeout=0
cid=1
pid=1
测试参数为空 >> ,,,,
测试参数为lang >> zh-cn,,,,
测试参数为lang,module >> zh-cn,custom,,,
测试参数为lang,key,section >> zh-cn,,URSRList,1,
测试参数为lang,key,section,module >> zh-cn,custom,URSRList,1,
测试参数为lang,key,section,module,vision >> zh-cn,custom,URSRList,1,rnd
*/
@@ -26,11 +19,10 @@ $key = 'key=1';
$vision = 'vision=rnd';
$paramString = array('', $lang, $lang.'&'.$module, $lang.'&'.$key.'&'.$section, $lang.'&'.$key.'&'.$section.'&'.$module, $lang.'&'.$key.'&'.$section.'&'.$module.'&'.$vision);
$custom = new customTest();
r($custom->parseItemParamTest($paramString[0])) && p('lang,module,section,key,vision') && e(',,,,'); //测试参数为空
r($custom->parseItemParamTest($paramString[1])) && p('lang,module,section,key,vision') && e('zh-cn,,,,'); //测试参数为lang
r($custom->parseItemParamTest($paramString[2])) && p('lang,module,section,key,vision') && e('zh-cn,custom,,,'); //测试参数为lang,module
r($custom->parseItemParamTest($paramString[3])) && p('lang,module,section,key,vision') && e('zh-cn,,URSRList,1,'); //测试参数为lang,key,section
r($custom->parseItemParamTest($paramString[4])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,'); //测试参数为lang,key,section,module
r($custom->parseItemParamTest($paramString[5])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,rnd'); //测试参数为lang,key,section,module,vision
$customTester = new customTest();
r($customTester->parseItemParamTest($paramString[5])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,rnd'); // 测试参数为lang,key,section,module,vision
r($customTester->parseItemParamTest($paramString[4])) && p('lang,module,section,key,vision') && e('zh-cn,custom,URSRList,1,~~'); // 测试参数为lang,key,section,module
r($customTester->parseItemParamTest($paramString[3])) && p('lang,module,section,key,vision') && e('zh-cn,~~,URSRList,1,~~'); // 测试参数为lang,key,section
r($customTester->parseItemParamTest($paramString[2])) && p('lang,module,section,key,vision') && e('zh-cn,custom,~~,~~,~~'); // 测试参数为lang,module
r($customTester->parseItemParamTest($paramString[1])) && p('lang,module,section,key,vision') && e('zh-cn,N/A,N/A,N/A,N/A'); // 测试参数为lang
r($customTester->parseItemParamTest($paramString[0])) && p('lang,module,section,key,vision') && e('N/A,N/A,N/A,N/A,N/A'); // 测试参数为空
+22 -30
View File
@@ -7,43 +7,35 @@ su('admin');
/**
title=测试 customModel->prepareSQL();
timeout=0
cid=1
pid=1
测试method为select,paramString参数为空 >> zh-cn,custom
测试method为select,paramString参数为lang >> zh-cn,custom
测试method为select,paramString参数为lang,module >> zh-cn,custom
测试method为select,paramString参数为lang,key,section >> zh-cn,custom
测试method为select,paramString参数为lang,key,section,module >> zh-cn,custom
测试method为select,paramString参数为lang,key,section,module,vision >> zh-cn,custom
测试method为delete,paramString参数为空 >> 14
测试method为delete,paramString参数为lang >> 0
测试method为delete,paramString参数为lang,module >> 0
测试method为delete,paramString参数为lang,key,section >> 0
测试method为delete,paramString参数为lang,key,section,module >> 0
测试method为delete,paramString参数为lang,key,section,module,vision >> 0
*/
$lang = zdTable('lang');
$lang->lang->range('zh-cn');
$lang->module->range('custom');
$lang->vision->range('rnd');
$lang->gen(1, true);
$lang = 'lang=zh-cn';
$module = 'module=custom';
$section = 'section=URSRList';
$key = 'key=1';
$section = 'section=section1';
$key = 'key=key1';
$vision = 'vision=rnd';
$paramString = array('', $lang, $lang.'&'.$module, $lang.'&'.$key.'&'.$section, $lang.'&'.$key.'&'.$section.'&'.$module, $lang.'&'.$key.'&'.$section.'&'.$module.'&'.$vision);
$method = array('select', 'delete');
$custom = new customTest();
r($custom->prepareSQLTest($paramString[0], $method[0])) && p('1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为空
r($custom->prepareSQLTest($paramString[1], $method[0])) && p('1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang
r($custom->prepareSQLTest($paramString[2], $method[0])) && p('1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,module
r($custom->prepareSQLTest($paramString[3], $method[0])) && p('1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,key,section
r($custom->prepareSQLTest($paramString[4], $method[0])) && p('1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,key,section,module
r($custom->prepareSQLTest($paramString[5], $method[0])) && p('1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,key,section,module,vision
r($custom->prepareSQLTest($paramString[0], $method[1])) && p() && e('14'); //测试method为delete,paramString参数为空
r($custom->prepareSQLTest($paramString[1], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang
r($custom->prepareSQLTest($paramString[2], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,module
r($custom->prepareSQLTest($paramString[3], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,key,section
r($custom->prepareSQLTest($paramString[4], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,key,section,module
r($custom->prepareSQLTest($paramString[5], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,key,section,module,vision
$customTester = new customTest();
r($customTester->prepareSQLTest($paramString[0], $method[0])) && p('key1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为空
r($customTester->prepareSQLTest($paramString[1], $method[0])) && p('key1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang
r($customTester->prepareSQLTest($paramString[2], $method[0])) && p('key1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,module
r($customTester->prepareSQLTest($paramString[3], $method[0])) && p('key1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,key,section
r($customTester->prepareSQLTest($paramString[4], $method[0])) && p('key1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,key,section,module
r($customTester->prepareSQLTest($paramString[5], $method[0])) && p('key1:lang,module') && e('zh-cn,custom'); //测试method为select,paramString参数为lang,key,section,module,vision
r($customTester->prepareSQLTest($paramString[0], $method[1])) && p() && e('1'); //测试method为delete,paramString参数为空
r($customTester->prepareSQLTest($paramString[1], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang
r($customTester->prepareSQLTest($paramString[2], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,module
r($customTester->prepareSQLTest($paramString[3], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,key,section
r($customTester->prepareSQLTest($paramString[4], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,key,section,module
r($customTester->prepareSQLTest($paramString[5], $method[1])) && p() && e('0'); //测试method为delete,paramString参数为lang,key,section,module,vision
+4 -2
View File
@@ -407,7 +407,9 @@ else
foreach($fieldList as $key => $value)
{
$system = isset($dbFields[$key]) ? $dbFields[$key]->system : 1;
$system = isset($dbFields[$key]) ? $dbFields[$key]->system : 1;
$keyLabel = $key === '' ? 'NULL' : $key;
if($key == 0) $keyLabel = '0';
$formItems[] = formRow
(
@@ -426,7 +428,7 @@ else
formGroup
(
set::width('1/2'),
set::label($key === '' ? 'NULL' : $key),
set::label($keyLabel),
set::name('values[]'),
set::value(isset($dbFields[$key]) ? $dbFields[$key]->value : $value),
set::readonly(empty($key))
+364
View File
@@ -0,0 +1,364 @@
<?php
declare(strict_types=1);
/**
* The zen file of custom 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 Shujie Tian <tianshujie@easycorp.ltd>
* @package custom
* @link https://www.zentao.net
*/
class customZen extends custom
{
/**
* 设置自定义字段列表。
* Set the list of custom fields.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @param string $lang all|zh-cn|zh-tw|en|de|fr
* @param string $currentLang all|zh-cn|zh-tw|en|de|fr
* @access protected
* @return void
*/
protected function assignFieldListForSet(string $module = 'story', string $field = 'priList', string $lang = '', string $currentLang = ''): void
{
$this->app->loadLang($module);
if($lang == 'all')
{
$fieldList = array();
$items = $this->custom->getItems("lang=all&module={$module}&section={$field}&vision={$this->config->vision}");
foreach($items as $key => $item)
{
$fieldList[$key] = $item->value;
}
}
else
{
$fieldList = zget($this->lang->$module, $field, array());
}
/* Check whether the current language has been customized. */
$lang = str_replace('_', '-', $lang);
$dbFields = $this->custom->getItems("lang={$lang}&module={$module}&section={$field}&vision={$this->config->vision}");
if(empty($dbFields)) $dbFields = $this->custom->getItems("lang=" . ($lang == $currentLang ? 'all' : $currentLang) . "&module={$module}&section={$field}");
if($dbFields)
{
$dbField = reset($dbFields);
if($lang != $dbField->lang)
{
$lang = str_replace('-', "_", $dbField->lang);
foreach($fieldList as $key => $value)
{
if(isset($dbFields[$key]) && $value != $dbFields[$key]->value) $fieldList[$key] = $dbFields[$key]->value;
}
}
}
$this->view->fieldList = $fieldList;
$this->view->dbFields = $dbFields;
}
/**
* 设置自定义列表变量。
* Set the list of custom variables.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @param string $lang all|zh-cn|zh-tw|en|de|fr
* @param string $currentLang all|zh-cn|zh-tw|en|de|fr
* @access protected
* @return void
*/
protected function assignVarsForSet(string $module = 'story', string $field = 'priList', string $lang = '', string $currentLang = ''): void
{
$this->assignFieldListForSet($module, $field, $lang, $currentLang);
if($module == 'project' && $field == 'unitList')
{
$this->app->loadConfig($module);
$unitList = zget($this->config->$module, 'unitList', '');
$this->view->unitList = explode(',', $unitList);
$this->view->defaultCurrency = zget($this->config->$module, 'defaultCurrency', 'CNY');
}
if(in_array($module, array('story', 'demand')) && $field == 'reviewRules')
{
$this->app->loadConfig($module);
$this->view->reviewRule = zget($this->config->$module, 'reviewRules', 'allpass');
$this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted');
$this->view->superReviewers = zget($this->config->$module, 'superReviewers', '');
}
if(in_array($module, array('story', 'testcase', 'demand')) && $field == 'review')
{
$this->app->loadConfig($module);
$this->loadModel('user');
if(in_array($module, array('story', 'demand')))
{
$this->view->depts = $this->loadModel('dept')->getDeptPairs();
$this->view->forceReviewRoles = zget($this->config->$module, 'forceReviewRoles', '');
$this->view->forceReviewDepts = zget($this->config->$module, 'forceReviewDepts', '');
$this->view->forceNotReviewRoles = zget($this->config->$module, 'forceNotReviewRoles', '');
$this->view->forceNotReviewDepts = zget($this->config->$module, 'forceNotReviewDepts', '');
}
$this->view->users = $module == 'story' ? $this->user->getCanCreateStoryUsers() : $this->user->getPairs('noclosed|nodeleted');
$this->view->needReview = zget($this->config->$module, 'needReview', 1);
$this->view->forceReview = zget($this->config->$module, 'forceReview', '');
$this->view->forceNotReview = zget($this->config->$module, 'forceNotReview', '');
}
if($module == 'bug' && $field == 'longlife')
{
$this->app->loadConfig('bug');
$this->view->longlife = $this->config->bug->longlife;
}
if($module == 'block' && $field == 'closed')
{
$this->loadModel('block');
$closedBlock = isset($this->config->block->closed) ? $this->config->block->closed : '';
$this->view->blockPairs = $this->block->getClosedBlockPairs($closedBlock);
$this->view->closedBlock = $closedBlock;
}
if($module == 'user' && $field == 'deleted')
{
$this->app->loadConfig('user');
$this->view->showDeleted = isset($this->config->user->showDeleted) ? $this->config->user->showDeleted : '0';
}
}
/**
* 设置自定义字段的值。
* Set the value of the custom field.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @access protected
* @return string|bool
*/
protected function setFieldListForSet(string $module = 'story', string $field = 'priList'): string|bool
{
$data = $_POST;
if($module == 'project' && $field == 'unitList')
{
if(empty($data['unitList'])) return dao::$errors['message'] = $this->lang->custom->currencyNotEmpty;
if(empty($data['defaultCurrency'])) return dao::$errors['message'] = $this->lang->custom->defaultNotEmpty;
$this->loadModel('setting')->setItems("system.$module", $data);
}
elseif(($module == 'story' || $module == 'demand') && $field == 'review')
{
$this->setStoryReview($data);
}
elseif(($module == 'story' || $module == 'demand') && $field == 'reviewRules')
{
if(!isset($data['superReviewers'])) $data['superReviewers'] = array();
$data['superReviewers'] = implode(',', $data['superReviewers']);
$this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
}
elseif($module == 'testcase' && $field == 'review')
{
$this->setTestcaseReview($data);
}
elseif($module == 'bug' && $field == 'longlife')
{
$this->loadModel('setting')->setItems('system.bug', $data);
}
elseif($module == 'block' && $field == 'closed')
{
$data['closed'] = implode(',', $data['closed']);
$this->loadModel('setting')->setItem('system.block.closed', zget($data, 'closed', ''));
}
elseif($module == 'user' && $field == 'contactField')
{
if(!isset($data['contactField'])) $data['contactField']= array();
$data['contactField'] = implode(',', $data['contactField']);
$this->loadModel('setting')->setItem('system.user.contactField', $data['contactField']);
}
elseif($module == 'user' && $field == 'deleted')
{
$this->loadModel('setting')->setItem('system.user.showDeleted', $data['showDeleted']);
}
else
{
if(!$this->post->keys) return dao::$errors['message'] = sprintf($this->lang->error->notempty, $this->lang->custom->key);
$this->checkKeysForSet($module, $field);
}
return !dao::isError();
}
/**
* 检查自定义key值是否合法。
* Check whether the key of custom is valid.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @access protected
* @return bool
*/
protected function checkKeysForSet(string $module = 'story', string $field = 'priList'): bool
{
$this->checkDuplicateKeys($module, $field);
if(dao::$errors) return false;
$this->checkInvalidKeys($module, $field);
if(dao::$errors) return false;
$lang = $_POST['lang'];
$this->custom->deleteItems("lang={$lang}&module={$module}&section={$field}&vision={$this->config->vision}");
if($lang == 'all') $this->custom->deleteItems("lang={$currentLang}&module={$module}&section={$field}&vision={$this->config->vision}");
$this->checkEmptyKeys($module, $field);
return !dao::isError();
}
/**
* 检查自定义key值是否重复。
* Check whether the key of custom is duplicate.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @access protected
* @return bool|string
*/
protected function checkDuplicateKeys(string $module = 'story', string $field = 'priList'): bool|string
{
$keys = array();
if(isset($_POST['keys']))
{
foreach($_POST['keys'] as $key)
{
if($module == 'testtask' && $field == 'typeList' && empty($key)) continue;
if($key && in_array($key, $keys)) return dao::$errors['message'] = sprintf($this->lang->custom->notice->repeatKey, $key);
$keys[] = $key;
}
}
return true;
}
/**
* 检查自定义key值是否正确。
* Check whether the key of custom is valid.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @access protected
* @return bool|string
*/
protected function checkInvalidKeys(string $module = 'story', string $field = 'priList'): bool|string
{
$lang = $_POST['lang'];
$oldCustoms = $this->custom->getItems("lang={$lang}&module={$module}&section={$field}");
foreach($_POST['keys'] as $index => $key)
{
if(!empty($key)) $key = trim($key);
/* Invalid key. It should be numbers. (It includes severityList in bug module && priList in story, task, bug, testcasea, testtask && todo module.) */
if(($field == 'priList' || $field == 'severityList') && (!is_numeric($key) || $key > 255)) return dao::$errors['message'] = $this->lang->custom->notice->invalidNumberKey;
if(!empty($key) && !isset($oldCustoms[$key]) && $key != 'n/a' && !validater::checkREG($key, '/^[a-z_A-Z_0-9]+$/')) return dao::$errors['message'] = $this->lang->custom->notice->invalidStringKey;
/* The length of roleList in user module is less than 10. check it when saved. */
if($module == 'user' && $field == 'roleList' && strlen($key) > 10) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['ten'];
/* The length of typeList in todo module is less than 15. check it when saved. */
if($module == 'todo' && $field == 'typeList' && strlen($key) > 15) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['fifteen'];
/* The length of sourceList in story module && typeList in task module is less than 20, check it when saved. */
if((($module == 'story' && $field == 'sourceList') || ($module == 'task' && $field == 'typeList')) && strlen($key) > 20) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['twenty'];
/* The length of field that in bug && testcase module && reasonList in story && task module is less than 30, check it when saved. */
if((in_array($module, array('bug', 'testcase')) || (in_array($module, array('story', 'task')) && $field == 'reasonList')) && strlen($key) > 30) return dao::$errors['message'] = $this->lang->custom->notice->invalidStrlen['thirty'];
}
return true;
}
/**
* 检查自定义key值是否为空。
* Check whether the key of custom is empty.
*
* @param string $module todo|story|task|bug|testcase|testtask|user|project
* @param string $field priList|typeList|statusList|sourceList|reasonList|stageList|reviewRules|reviewResultList|review|severityList|osList|browserList|resolutionList|longlife|resultList|roleList|contactField|deleted|unitList
* @access protected
* @return bool|string
*/
protected function checkEmptyKeys(string $module = 'story', string $field = 'priList'): bool|string
{
$lang = $_POST['lang'];
$emptyKey = false;
$keys = array();
foreach($_POST['keys'] as $index => $key)
{
if(!$key && $emptyKey) continue;
$value = $_POST['values'][$index];
$system = $_POST['systems'][$index];
if($key && trim($value) === '') return dao::$errors['message'] = $this->lang->custom->notice->valueEmpty; // Fix bug #23538.
$this->custom->setItem("{$lang}.{$module}.{$field}.{$key}.{$system}", $value);
if(!$key) $emptyKey = true;
}
return true;
}
/**
* 设置需求评审规则。
* Set the review rules of story.
*
* @param array $data
* @access protected
* @return bool
*/
protected function setStoryReview(array $data): bool
{
$forceFields = array('forceReview', 'forceNotReview', 'forceReviewRoles', 'forceNotReviewRoles', 'forceReviewDepts', 'forceNotReviewDepts');
foreach($forceFields as $forceField)
{
if(!isset($data[$forceField])) $data[$forceField] = array();
$data[$forceField] = implode(',', $data[$forceField]);
}
foreach($data as $key => $value)
{
if($key == 'needReview') continue;
if(strpos($key, 'Not') && $data['needReview'] == 0) $data[$key] = '';
if(!strpos($key, 'Not') && $data['needReview'] == 1) $data[$key] = '';
}
$this->loadModel('setting')->setItems("system.$module@{$this->config->vision}", $data);
return !dao::isError();
}
/**
* 设置用例评审规则。
* Set testcase review rules.
*
* @param array $data
* @access protected
* @return bool
*/
protected function setTestcaseReview(array $data): bool
{
if($data['needReview'])
{
unset($data['forceReview']);
if(!isset($data['forceNotReview'])) $data['forceNotReview'] = array();
$data['forceNotReview'] = implode(',', $data['forceNotReview']);
}
else
{
unset($data['forceNotReview']);
if(!isset($data['forceReview'])) $data['forceReview'] = array();
$data['forceReview'] = implode(',', $data['forceReview']);
}
$this->loadModel('setting')->setItems("system.testcase", $data);
$reviewCase = isset($data['reviewCase']) ? $data['reviewCase'] : 0;
if($data['needReview'] == 0 && $reviewCase)
{
$waitCases = $this->loadModel('testcase')->getByStatus(0, 0, 'all', 'wait');
$this->testcase->batchReview(array_keys($waitCases), 'pass');
}
return !dao::isError();
}
}
+2
View File
@@ -0,0 +1,2 @@
<?php
$config->dept->newChildCount = 10;
+12 -24
View File
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* The control file of dept module of ZenTaoPMS.
*
@@ -11,41 +12,28 @@
*/
class dept extends control
{
const NEW_CHILD_COUNT = 10;
/**
* Construct function, set menu.
*
* @access public
* @return void
*/
public function __construct($moduleName = '', $methodName = '')
{
parent::__construct($moduleName, $methodName);
$this->loadModel('company')->setMenu();
}
/**
* Browse a department.
* 部门结构维护页面。
* Department management page.
*
* @param int $deptID
* @access public
* @return void
*/
public function browse($deptID = 0)
public function browse(int $deptID = 0)
{
$parentDepts = $this->dept->getParents($deptID);
$this->view->title = $this->lang->dept->manage . $this->lang->colon . $this->app->company->name;
$this->view->deptID = $deptID;
$this->view->depts = $this->dept->getTreeMenu($rootDeptID = 0, array('deptmodel', 'createManageLink'));
$this->view->parentDepts = $parentDepts;
$this->view->depts = $this->dept->getTreeMenu(0, array('deptmodel', 'createManageLink'));
$this->view->sons = $this->dept->getSons($deptID);
$this->view->tree = $this->dept->getDataStructure();
$this->view->parentDepts = $this->dept->getParents($deptID);
$this->display();
}
/**
* Update the departments order.
* 部门结构排序。
* Sort of departments.
*
* @access public
* @return void
@@ -55,12 +43,13 @@ class dept extends control
if(!empty($_POST))
{
$this->dept->updateOrder($_POST['orders']);
return print(js::reload('parent'));
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
}
}
/**
* Manage childs.
* 修改子部门结构。
* Update child departments.
*
* @access public
* @return void
@@ -70,8 +59,7 @@ class dept extends control
if(!empty($_POST))
{
$deptIDList = $this->dept->manageChild($_POST['parentDeptID'], $_POST['depts']);
if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $deptIDList));
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true));
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true, 'idList' => $deptIDList));
}
}
+1 -2
View File
@@ -471,8 +471,7 @@ class deptModel extends model
$node->text = $node->name;
if(isset($tree[$deptID]))
{
$node->items = $tree[$deptID];
$node->actions = array('delete' => false);
$node->items = $tree[$deptID];
unset($tree[$deptID]);
}
$tree[$node->parent][] = $node;
+37 -41
View File
@@ -10,48 +10,23 @@ declare(strict_types=1);
*/
namespace zin;
jsVar('editLinkTemp', createLink('dept', 'edit', "deptID={id}"));
jsVar('deleteLinkTemp', createLink('dept', 'delete', "deptID={id}"));
jsVar('deleteTip', $lang->dept->confirmDelete);
$deptActions = array();
if(hasPriv('dept', 'edit'))
$processTreeAction = function($tree) use (&$processTreeAction)
{
$deptAction = array();
$deptAction['key'] = 'edit';
$deptAction['icon'] = 'edit';
$deptAction['hint'] = $lang->dept->edit;
$deptAction['onClick'] = jsRaw('window.operateDept');
$canEditDept = hasPriv('dept', 'edit');
$canDeleteDept = hasPriv('dept', 'delete');
foreach($tree as $node)
{
$actions = array();
if($canEditDept) $actions[] = array('key' => 'edit', 'icon' => 'edit', 'hint' => $lang->dept->edit, 'onClick' => jsRaw('window.operateDept'));
if(empty($node->items) && $canDeleteDept) $actions[] = array('key' => 'delete', 'icon' => 'trash', 'hint' => $lang->dept->delete, 'onClick' => jsRaw('window.operateDept'));
if(!empty($node->items)) $node->items = $processTreeAction($node->items);
$node->actions = $actions;
}
$deptActions[] = $deptAction;
}
if(hasPriv('dept', 'delete'))
{
$deptAction = array();
$deptAction['key'] = 'delete';
$deptAction['icon'] = 'trash';
$deptAction['hint'] = $lang->dept->delete;
$deptAction['onClick'] = jsRaw('window.operateDept');
return $tree;
};
$deptActions[] = $deptAction;
}
sidebar
(
width('400px'),
set::showToggle(false),
panel
(
set::title($title),
tree
(
set::id('deptTree'),
set::items($tree),
set::hover(true),
set::itemActions($deptActions)
)
)
);
$tree = $processTreeAction($tree);
$parentNames = array();
foreach($parentDepts as $dept)
@@ -86,8 +61,9 @@ foreach($sons as $dept)
set::value($dept->name)
);
}
$emptyInputs = array();
for($i = 0; $i < \DEPT::NEW_CHILD_COUNT ; $i ++)
for($i = 0; $i < $config->dept->newChildCount; $i ++)
{
$emptyInputs[] = formGroup
(
@@ -97,6 +73,27 @@ for($i = 0; $i < \DEPT::NEW_CHILD_COUNT ; $i ++)
);
}
jsVar('editLinkTemp', createLink('dept', 'edit', "deptID={id}"));
jsVar('deleteLinkTemp', createLink('dept', 'delete', "deptID={id}"));
jsVar('deleteTip', $lang->dept->confirmDelete);
sidebar
(
width('400px'),
set::showToggle(false),
panel
(
set::title($title),
tree
(
set::id('deptTree'),
set::items($tree),
set::hover(true),
set::itemActions($deptActions)
)
)
);
panel
(
set::title($lang->dept->manageChild),
@@ -147,4 +144,3 @@ panel
);
render();
+1 -1
View File
@@ -53,7 +53,7 @@
if($sonDept->order > $maxOrder) $maxOrder = $sonDept->order;
echo html::input("depts[id$sonDept->id]", $sonDept->name, "class='form-control'");
}
for($i = 0; $i < DEPT::NEW_CHILD_COUNT ; $i ++) echo html::input("depts[]", '', "class='form-control'");
for($i = 0; $i < $config->dept->newChildCount; $i ++) echo html::input("depts[]", '', "class='form-control'");
?>
</td>
<td></td>
+1 -1
View File
@@ -10,7 +10,7 @@ $(function()
if(requiredFields.indexOf('content') >= 0)
{
if($('[name="content"]').val() == '')
if($('input[name="content"]').val() == '')
{
zui.Modal.alert(contentNotEmpty);
return false;
+2 -4
View File
@@ -183,10 +183,8 @@ else
formGroup
(
set::label($lang->doc->keywords),
input
(
set::name('keywords')
)
set::control('input'),
set::name('keywords')
),
formGroup
(
+3 -5
View File
@@ -136,11 +136,9 @@ form
(
strpos($config->doc->officeTypes, $doc->type) === false ? setClass('hidden') : null,
set::label($lang->doc->keywords),
input
(
set::name('keywords'),
set::value($doc->keywords)
)
set::control('input'),
set::name('keywords'),
set::value($doc->keywords)
),
formGroup
(
+1 -1
View File
@@ -81,7 +81,7 @@ if($doc->keywords)
{
$keywordsLabel[] = span
(
setClass('label secondary-outline'),
setClass('label secondary-outline ml-2'),
$keywords
);
}
+1 -1
View File
@@ -73,7 +73,7 @@ class editor extends control
$extension = 'php';
if($filePath)
{
$filePath = helper::safe64Decode($filePath);
$filePath = realpath($filePath);
if(strpos(strtolower($filePath), strtolower($this->app->getBasePath())) !== 0) return print($this->lang->editor->editFileError);
if($action == 'extendOther' and file_exists($filePath)) $this->view->showContent = file_get_contents($filePath);
+1 -1
View File
@@ -209,7 +209,7 @@ $config->execution->importBug->dtable->fieldList['taskPri']['type'] = 'h
$config->execution->importBug->dtable->fieldList['taskPri']['width'] = 68;
$config->execution->importBug->dtable->fieldList['taskPri']['sortType'] = false;
$config->execution->importBug->dtable->fieldList['taskPri']['type'] = 'control';
$config->execution->importBug->dtable->fieldList['taskPri']['control'] = 'select';
$config->execution->importBug->dtable->fieldList['taskPri']['control'] = 'picker';
$config->execution->importBug->dtable->fieldList['taskPri']['controlItems'] = $lang->task->priList;
$config->execution->importBug->dtable->fieldList['assignedTo']['title'] = $lang->bug->assignedTo;
-7
View File
@@ -1635,14 +1635,11 @@ class executionModel extends model
$parentList = array();
$emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
$today = helper::today();
$hours = $this->loadModel('project')->computeProgress($executions);
$burns = $this->getBurnData($executions);
foreach($executions as $execution)
{
$execution->productName = isset($productList[$execution->id]) ? trim($productList[$execution->id]->productName, ',') : '';
$execution->end = date(DT_DATE1, strtotime($execution->end));
$execution->hours = isset($hours[$execution->id]) ? $hours[$execution->id] : (object)$emptyHour;
$execution->teamCount = isset($memberGroup[$execution->id]) ? $memberGroup[$execution->id]->teams : 0;
if(isset($executions[$execution->parent])) $executions[$execution->parent]->isParent = 1;
if(empty($productID) && !empty($productList[$execution->id])) $execution->product = trim($productList[$execution->id]->product, ',');
@@ -4684,10 +4681,6 @@ class executionModel extends model
$execution->project = $execution->projectName;
$execution->parent = ($execution->parent && $execution->grade > 1) ? 'pid' . (string)$execution->parent : '';
$execution->isParent = !empty($execution->isParent) or !empty($execution->tasks);
$execution->progress = $execution->hours->progress;
$execution->totalEstimate = $execution->hours->totalEstimate;
$execution->totalConsumed = $execution->hours->totalConsumed;
$execution->totalLeft = $execution->hours->totalLeft;
$execution->actions = array();
foreach($this->config->projectExecution->dtable->fieldList['actions'][$execution->projectModel] as $actionKey)
{
+1 -1
View File
@@ -225,7 +225,7 @@ modal
set::text($lang->execution->next),
set::btnType('submit'),
set::type('primary'),
setData(array('dismiss' => 'primary'))
setData(array('dismiss' => 'modal'))
)
)
);
+6 -2
View File
@@ -13,11 +13,13 @@ function changeEngine(event)
var repos = engine == 'gitlab' ? gitlabRepos : repoPairs;
var repoItems = [];
var repoID = 0;
for(i in repos)
{
if(repoID == 0) repoID = i;
repoItems.push({'text': repos[i], 'value': i});
}
zui.Picker.query('#repo').render({items: repoItems});
zui.Picker.query('#repo').render({items: repoItems, value: repoID});
if(engine == 'gitlab')
{
@@ -36,6 +38,8 @@ function changeEngine(event)
if(engine == 'jenkins' || frame != 'sonarqube') items.push({'text': frameList[frame], 'value': frame});
}
zui.Picker.query('#frame').render({items: items});
changeRepo();
}
function changeFrame(event)
@@ -56,7 +60,7 @@ function changeFrame(event)
function changeRepo(event)
{
const repoID = $(event.target).val();
const repoID = $('input[name="repo"]').val();
if(repoID <= 0) return;
var link = $.createLink('repo', 'ajaxLoadProducts', 'repoID=' + repoID);
+2 -6
View File
@@ -1444,10 +1444,6 @@ class kanbanModel extends model
->andWhere('action')->eq('opened')
->fetchPairs();
}
elseif($fromType == 'execution')
{
$executionProgress = $this->loadModel('project')->computeProgress($objects);
}
/* Data for constructing the card. */
foreach($objectCards as $objectID => $cardsInfo)
@@ -1473,7 +1469,7 @@ class kanbanModel extends model
if($delay > 0) $objectCard->delay = $delay;
}
$objectCard->execType = $object->type;
$objectCard->progress = isset($executionProgress[$objectID]->progress) ? $executionProgress[$objectID]->progress : 0;
$objectCard->progress = $object->progress;
$parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($object->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs();
$objectCard->title = implode('/', $parentExecutions);
@@ -1743,7 +1739,7 @@ class kanbanModel extends model
if($cardIdList)
{
$cardsData = $this->buildExecutionCards($cardsData, $column, $lane->type, $cardIdList, $objectGroup, $searchValue, $menus);
$columnsData[$column->id]['cards'] = count($cardsData[$column->lane][$column->id]);
$columnsData[$column->id]['cards'] = empty($cardsData[$column->lane][$column->id]) ? 0 : count($cardsData[$column->lane][$column->id]);
}
}
+37 -41
View File
@@ -27,8 +27,7 @@ formPanel
setClass('primary-pale'),
set::icon('copy'),
set::url('#copyKanbanModal'),
set('data-destoryOnHide', true),
set('data-toggle', 'modal'),
setData(array('destoryOnHide' => true, 'toggle' => 'modal')),
$lang->kanban->copy . $lang->kanban->common
)
),
@@ -48,7 +47,7 @@ formPanel
),
formRow
(
set::id('WIPCountBox'),
setID('WIPCountBox'),
formGroup
(
set::label($lang->kanban->WIPCount),
@@ -63,7 +62,7 @@ formPanel
),
formRow
(
set::id('spaceBox'),
setID('spaceBox'),
formGroup
(
set::width('1/2'),
@@ -79,7 +78,7 @@ formPanel
),
formRow
(
set::id('nameBox'),
setID('nameBox'),
formGroup
(
set::width('1/2'),
@@ -90,8 +89,8 @@ formPanel
),
formRow
(
set::id('ownerBox'),
set::className($type == 'private' ? 'hidden' : ''),
setID('ownerBox'),
setClass($type == 'private' ? 'hidden' : ''),
formGroup
(
set::width('1/2'),
@@ -106,10 +105,10 @@ formPanel
),
span
(
set('class', 'input-group-addon'),
setClass('input-group-addon'),
a
(
set('id', 'allUsers'),
setID('allUsers'),
set('href', 'javascript:;'),
$lang->kanban->allUsers
)
@@ -119,8 +118,8 @@ formPanel
),
formRow
(
set::id('teamBox'),
set::className($type == 'private' ? 'hidden' : ''),
setID('teamBox'),
setClass($type == 'private' ? 'hidden' : ''),
formGroup
(
set::label($lang->kanban->team),
@@ -135,7 +134,7 @@ formPanel
),
formRow
(
set::id('fixedColBox'),
setID('fixedColBox'),
formGroup
(
setClass('items-center'),
@@ -149,10 +148,10 @@ formPanel
),
div
(
set::className('flex items-center ml-8 py-1'),
setClass('flex items-center ml-8 py-1'),
set::style(array('padding-left' => '1px')),
span($lang->kanban->colWidth),
input(set::type('number'), set::min($config->colWidth), set::name('colWidth'), set::className('w-16 size-sm mx-1'), set::value(isset($copyKanban->colWidth) ? $copyKanban->colWidth : $config->colWidth)),
input(set::type('number'), set::min($config->colWidth), set::name('colWidth'), setClass('w-16 size-sm mx-1'), set::value(isset($copyKanban->colWidth) ? $copyKanban->colWidth : $config->colWidth)),
span('px')
),
div
@@ -165,7 +164,7 @@ formPanel
),
formRow
(
set::id('autoColBox'),
setID('autoColBox'),
set::style(array('margin-top' => '0px')),
formGroup
(
@@ -180,12 +179,12 @@ formPanel
),
div
(
set::className('flex items-center ml-5 py-1'),
setClass('flex items-center ml-5 py-1'),
span($lang->kanban->colWidth),
input(set::type('number'), set::min($config->minColWidth), set::name('minColWidth'), set::className('w-16 size-sm mx-1'), set::value(isset($copyKanban->minColWidth) ? $copyKanban->minColWidth : $config->minColWidth)),
input(set::type('number'), set::min($config->minColWidth), set::name('minColWidth'), setClass('w-16 size-sm mx-1'), set::value(isset($copyKanban->minColWidth) ? $copyKanban->minColWidth : $config->minColWidth)),
span('px'),
span('~', set::className('mx-1')),
input(set::type('number'), set::min($config->maxColWidth), set::name('maxColWidth'), set::className('w-16 size-sm mx-1'), set::value(isset($copyKanban->maxColWidth) ? $copyKanban->maxColWidth : $config->maxColWidth)),
span('~', setClass('mx-1')),
input(set::type('number'), set::min($config->maxColWidth), set::name('maxColWidth'), setClass('w-16 size-sm mx-1'), set::value(isset($copyKanban->maxColWidth) ? $copyKanban->maxColWidth : $config->maxColWidth)),
span('px')
),
div
@@ -228,7 +227,7 @@ formPanel
),
formRow
(
set::id('archiveBox'),
setID('archiveBox'),
formGroup
(
set::label($lang->kanban->archive),
@@ -243,7 +242,7 @@ formPanel
),
formRow
(
set::id('manageProgressBox'),
setID('manageProgressBox'),
formGroup
(
set::label($lang->kanban->manageProgress),
@@ -258,7 +257,7 @@ formPanel
),
formRow
(
set::id('alignmentBox'),
setID('alignmentBox'),
formGroup
(
set::label($lang->kanban->alignment),
@@ -273,7 +272,7 @@ formPanel
),
formRow
(
set::id('descBox'),
setID('descBox'),
formGroup
(
set::label($lang->kanban->desc),
@@ -286,8 +285,8 @@ formPanel
),
formRow
(
set::id('whitelistBox'),
set::className($type != 'private' ? 'hidden' : ''),
setID('whitelistBox'),
setClass($type != 'private' ? 'hidden' : ''),
formGroup
(
set::label($lang->whitelist),
@@ -299,8 +298,8 @@ formPanel
set::value(isset($copyKanban->whitelist) ? $copyKanban->whitelist : '')
)
),
input(set::className('hidden'), set::name('copyKanbanID'), set::value($copyKanbanID)),
input(set::className('hidden'), set::name('copyRegion'), set::value($copyRegion))
input(setClass('hidden'), set::name('copyKanbanID'), set::value($copyKanbanID)),
input(setClass('hidden'), set::name('copyRegion'), set::value($copyRegion))
)
);
@@ -310,11 +309,11 @@ if($copyKanbanID != 0)
$kanbanList[] = cell
(
set::width('1/3'),
set::className('p-2'),
setClass('p-2'),
div
(
set::className('copy-card p-2 border rounded-md text-danger'),
icon('cancel', set::className('pr-2')),
setClass('copy-card p-2 border rounded-md text-danger'),
icon('cancel', setClass('pr-2')),
$lang->kanban->cancelCopy
)
);
@@ -327,15 +326,12 @@ if(!empty($kanbans))
$kanbanList[] = cell
(
set::width('1/3'),
set::className('p-2'),
setClass('p-2'),
div
(
set('data-on', 'click'),
set('data-call', 'clickCopyCard'),
set('data-params', 'event'),
set('data-id', $id),
set::className('copy-card p-2 border rounded-md'),
icon('kanban', set::className('pr-2')),
setData(array('on' => 'click', 'call' => 'clickCopyCard', 'params' => 'event', 'id' => $id)),
setClass('copy-card p-2 border rounded-md'),
icon('kanban', setClass('pr-2')),
$name
)
);
@@ -349,7 +345,7 @@ else
icon('exclamation-sign icon-2x pl-2 text-warning'),
span
(
set::className('font-bold ml-2'),
setClass('font-bold ml-2'),
$lang->kanban->copyNoKanban
)
);
@@ -359,12 +355,12 @@ modalTrigger
(
modal
(
set::id('copyKanbanModal'),
setID('copyKanbanModal'),
to::header
(
span
(
set::className('copy-title article-h1'),
setClass('copy-title article-h1'),
$lang->kanban->copyTitle,
),
span
@@ -381,7 +377,7 @@ modalTrigger
),
div
(
set::id('copyKanbans'),
setID('copyKanbans'),
setClass('flex flex-wrap'),
$kanbanList
)
+1 -1
View File
@@ -39,7 +39,7 @@ formPanel
),
formRow
(
set::id('otherLaneBox'),
setID('otherLaneBox'),
formGroup
(
set::label($lang->kanbanlane->otherlane),
+4 -4
View File
@@ -26,9 +26,9 @@ featureBar
(
inputGroup
(
span(set::className('input-group-addon'), $lang->kanban->selectedProduct),
span(setClass('input-group-addon'), $lang->kanban->selectedProduct),
picker(set::name('project'), set::items($projects), set::value($selectedProjectID), set::style(array('width' => '200px')), set('data-on', 'change'), set('data-call', 'changeProject'), set::required(true)),
span(set::className('input-group-addon'), $lang->kanban->selectedLane),
span(setClass('input-group-addon'), $lang->kanban->selectedLane),
picker(set::name('lane'), set::items($lanePairs), set::style(array('width' => '200px')), set::required(true))
)
);
@@ -50,9 +50,9 @@ unset($config->execution->dtable->fieldList['actions']);
formBase
(
set::id('linkForm'),
setID('linkForm'),
set::actions(''),
set::className('mt-2'),
setClass('mt-2'),
dtable
(
set::fixedLeftWidth('0.33'),
+4 -4
View File
@@ -35,9 +35,9 @@ featureBar
(
inputGroup
(
span(set::className('input-group-addon'), $lang->kanban->selectedProduct),
span(setClass('input-group-addon'), $lang->kanban->selectedProduct),
picker(set::name('product'), set::items($products), set::value($selectedProductID), set::style(array('width' => '200px')), set('data-on', 'change'), set('data-call', 'changeProduct'), set::required(true)),
span(set::className('input-group-addon'), $lang->kanban->selectedLane),
span(setClass('input-group-addon'), $lang->kanban->selectedLane),
picker(set::name('lane'), set::items($lanePairs), set::style(array('width' => '200px')), set::required(true))
)
);
@@ -49,9 +49,9 @@ unset($config->release->dtable->fieldList['actions']);
formBase
(
set::id('linkForm'),
setID('linkForm'),
set::actions(''),
set::className('mt-2'),
setClass('mt-2'),
dtable
(
set::fixedLeftWidth('0.33'),
+2 -1
View File
@@ -67,7 +67,7 @@ class metric extends control
$resultHeader = $this->metricZen->getViewTableHeader($metric);
$resultData = $this->metricZen->getViewTableData($metric, $result);
}
$this->view->metrics = $metrics;
$this->view->groupMetrics = $groupMetrics;
$this->view->current = $current;
@@ -80,6 +80,7 @@ class metric extends control
$this->view->filtersBase64 = $filtersBase64;
$this->view->resultHeader = $resultHeader;
$this->view->resultData = $resultData;
$this->view->tableWidth = $this->metricZen->getViewTableWidth($resultHeader);
$this->view->chartTypeList = $this->metric->getChartTypeList($resultHeader);
$this->view->echartOptions = $this->metric->getEchartsOptions($resultHeader, $resultData);
$this->display();
+2 -2
View File
@@ -48,8 +48,8 @@
i.star-empty {color: var(--color-slate-400);}
i.star {color: var(--color-warning-300);}
.table-and-chart .table-side {flex: 0 0 480px;}
.table-and-chart .chart-side {flex: 0 0 calc(100% - 480px);}
.table-and-chart .table-side {flex-shrink: 1; max-width: 50%;}
.table-and-chart .chart-side {flex-grow: 1;}
.table-and-chart .chart-center {display:flex; justify-content: center; align-items: center;}
.icon-18 {font-size: 18px;}
+2 -1
View File
@@ -21,7 +21,7 @@ $fnGenerateSide = function() use($groupMetrics, $current, $viewType, $scope, $la
$metricList[] = li
(
set::class('metric-group'),
set::className('metric-group'),
$lang->metric->objectList[$key]
);
@@ -350,6 +350,7 @@ div
div
(
setClass('table-side'),
setStyle(array('flex-basis' => $tableWidth . 'px')),
div
(
$resultData ? dtable
+12 -1
View File
@@ -397,7 +397,7 @@ class metricZen extends metric
if(!$result) return array
(
array('name' => 'value', 'title' => $this->lang->metric->value),
array('name' => 'value', 'title' => $this->lang->metric->value, 'width' => 96),
array('name' => 'calcTime', 'title' => $this->lang->metric->calcTime, 'width' => 150)
);
@@ -415,6 +415,17 @@ class metricZen extends metric
return $header;
}
protected function getViewTableWidth($headers)
{
$width = 0;
foreach($headers as $header)
{
$width += isset($header['width']) ? $header['width'] : 160;
}
return $width;
}
/**
* 获取度量数据表的数据。
* Get data of result table.
+1 -4
View File
@@ -45,7 +45,4 @@ formPanel
set::rows('6')
)
);
history();
render();
history(set::objectID($MR->id));
+2 -2
View File
@@ -100,7 +100,7 @@ $lang->pivot->singleColor[] = 'F6BD0F';
$lang->pivot->projectDeviation = "{$lang->execution->common}偏差报表";
$lang->pivot->productSummary = $lang->productCommon . '汇总表';
$lang->pivot->bugCreate = 'Bug创建表';
$lang->pivot->bugAssign = 'Bug指派表';
$lang->pivot->bugAssign = '未解决Bug指派表';
$lang->pivot->workload = '员工负载表';
$lang->pivot->workloadAB = '工作负载';
$lang->pivot->bugOpenedDate = 'Bug创建时间';
@@ -119,7 +119,7 @@ $lang->pivotList->staff = new stdclass();
$lang->pivotList->product->lists[10] = $lang->productCommon . '汇总表|pivot|productsummary';
$lang->pivotList->project->lists[10] = "{$lang->execution->common}偏差报表|pivot|projectdeviation";
$lang->pivotList->test->lists[10] = 'Bug创建表|pivot|bugcreate';
$lang->pivotList->test->lists[13] = 'Bug指派表|pivot|bugassign';
$lang->pivotList->test->lists[13] = '未解决Bug指派表|pivot|bugassign';
$lang->pivotList->staff->lists[10] = '员工负载表|pivot|workload';
$lang->pivot->id = '编号';
+2 -2
View File
@@ -41,7 +41,7 @@ $lang->pivot->singleColor[] = 'F6BD0F';
$lang->pivot->projectDeviation = "{$lang->execution->common}偏差報表";
$lang->pivot->productSummary = $lang->productCommon . '彙總表';
$lang->pivot->bugCreate = 'Bug創建表';
$lang->pivot->bugAssign = 'Bug指派表';
$lang->pivot->bugAssign = '未解决Bug指派表';
$lang->pivot->workload = '員工負載表';
$lang->pivot->workloadAB = '工作負載';
$lang->pivot->bugOpenedDate = 'Bug創建時間';
@@ -60,7 +60,7 @@ $lang->pivotList->staff = new stdclass();
$lang->pivotList->product->lists[10] = $lang->productCommon . '彙總表|pivot|productsummary';
$lang->pivotList->project->lists[10] = "{$lang->execution->common}偏差報表|pivot|projectdeviation";
$lang->pivotList->test->lists[10] = 'Bug創建表|pivot|bugcreate';
$lang->pivotList->test->lists[13] = 'Bug指派表|pivot|bugassign';
$lang->pivotList->test->lists[13] = '未解决Bug指派表|pivot|bugassign';
$lang->pivotList->staff->lists[10] = '員工負載表|pivot|workload';
$lang->pivot->id = '編號';
+1 -1
View File
@@ -2112,7 +2112,7 @@ class pivotModel extends model
switch($type)
{
case 'user':
$options = $this->loadModel('user')->getPairs();
$options = $this->loadModel('user')->getPairs('noletter');
break;
case 'product':
$options = $this->loadModel('product')->getPairs();
+4 -5
View File
@@ -46,11 +46,10 @@ $config->product->report->planLabels = array();
$config->product->report->planLabels[] = '';
$config->product->statisticFields = array();
$config->product->statisticFields['requirements'] = array('draftRequirements', 'activeRequirements', 'changingRequirements', 'reviewingRequirements', 'closedRequirements');
$config->product->statisticFields['stories'] = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishClosedStories', 'unclosedStories');
$config->product->statisticFields['bugs'] = array('unResolvedBugs', 'closedBugs', 'fixedBugs');
$config->product->statisticFields['plans'] = array('plans');
$config->product->statisticFields['releases'] = array('releases');
$config->product->statisticFields['stories'] = array('draftStories', 'activeStories', 'changingStories', 'reviewingStories', 'closedStories', 'finishedStories', 'totalStories');
$config->product->statisticFields['bugs'] = array('unresolvedBugs', 'closedBugs', 'fixedBugs');
$config->product->statisticFields['plans'] = array('plans');
$config->product->statisticFields['releases'] = array('releases');
$config->product->skipRedirectMethod = ',create,index,showerrornone,ajaxgetdropmenu,kanban,all,manageline,export,ajaxgetplans,';
$config->product->memberFields = array('PO', 'QD', 'RD', 'feedback', 'ticket', 'createdBy;');
+21 -21
View File
@@ -98,12 +98,12 @@ $config->product->dtable->fieldList['testCaseCoverage']['type'] = 'progress'
$config->product->dtable->fieldList['testCaseCoverage']['sortType'] = false;
$config->product->dtable->fieldList['testCaseCoverage']['border'] = 'right';
$config->product->dtable->fieldList['unResolvedBugs']['name'] = 'unResolvedBugs';
$config->product->dtable->fieldList['unResolvedBugs']['title'] = $lang->product->activatedBug;
$config->product->dtable->fieldList['unResolvedBugs']['minWidth'] = 64;
$config->product->dtable->fieldList['unResolvedBugs']['type'] = 'number';
$config->product->dtable->fieldList['unResolvedBugs']['sortType'] = false;
$config->product->dtable->fieldList['unResolvedBugs']['align'] = 'center';
$config->product->dtable->fieldList['unresolvedBugs']['name'] = 'unresolvedBugs';
$config->product->dtable->fieldList['unresolvedBugs']['title'] = $lang->product->activatedBug;
$config->product->dtable->fieldList['unresolvedBugs']['minWidth'] = 64;
$config->product->dtable->fieldList['unresolvedBugs']['type'] = 'number';
$config->product->dtable->fieldList['unresolvedBugs']['sortType'] = false;
$config->product->dtable->fieldList['unresolvedBugs']['align'] = 'center';
$config->product->dtable->fieldList['bugFixedRate']['name'] = 'bugFixedRate';
$config->product->dtable->fieldList['bugFixedRate']['title'] = $lang->product->bugFixedRate;
@@ -240,13 +240,13 @@ $config->product->all->dtable->fieldList['plans']['show'] = true;
$config->product->all->dtable->fieldList['plans']['sortType'] = false;
$config->product->all->dtable->fieldList['plans']['group'] = 'g5';
$config->product->all->dtable->fieldList['execution']['name'] = 'execution';
$config->product->all->dtable->fieldList['execution']['title'] = $lang->execution->common;
$config->product->all->dtable->fieldList['execution']['width'] = 64;
$config->product->all->dtable->fieldList['execution']['type'] = 'number';
$config->product->all->dtable->fieldList['execution']['show'] = true;
$config->product->all->dtable->fieldList['execution']['sortType'] = false;
$config->product->all->dtable->fieldList['execution']['group'] = 'g6';
$config->product->all->dtable->fieldList['executions']['name'] = 'executions';
$config->product->all->dtable->fieldList['executions']['title'] = $lang->execution->common;
$config->product->all->dtable->fieldList['executions']['width'] = 64;
$config->product->all->dtable->fieldList['executions']['type'] = 'number';
$config->product->all->dtable->fieldList['executions']['show'] = true;
$config->product->all->dtable->fieldList['executions']['sortType'] = false;
$config->product->all->dtable->fieldList['executions']['group'] = 'g6';
$config->product->all->dtable->fieldList['testCaseCoverage']['name'] = 'testCaseCoverage';
$config->product->all->dtable->fieldList['testCaseCoverage']['title'] = $lang->product->testCaseCoverage;
@@ -257,14 +257,14 @@ $config->product->all->dtable->fieldList['testCaseCoverage']['show'] = true;
$config->product->all->dtable->fieldList['testCaseCoverage']['sortType'] = false;
$config->product->all->dtable->fieldList['testCaseCoverage']['group'] = 'g7';
$config->product->all->dtable->fieldList['totalActivatedBugs']['name'] = 'totalActivatedBugs';
$config->product->all->dtable->fieldList['totalActivatedBugs']['title'] = $lang->product->activatedBug;
$config->product->all->dtable->fieldList['totalActivatedBugs']['width'] = 64;
$config->product->all->dtable->fieldList['totalActivatedBugs']['minWidth'] = 86;
$config->product->all->dtable->fieldList['totalActivatedBugs']['type'] = 'number';
$config->product->all->dtable->fieldList['totalActivatedBugs']['show'] = true;
$config->product->all->dtable->fieldList['totalActivatedBugs']['sortType'] = false;
$config->product->all->dtable->fieldList['totalActivatedBugs']['group'] = 'g8';
$config->product->all->dtable->fieldList['unresolvedBugs']['name'] = 'unresolvedBugs';
$config->product->all->dtable->fieldList['unresolvedBugs']['title'] = $lang->product->activatedBug;
$config->product->all->dtable->fieldList['unresolvedBugs']['width'] = 64;
$config->product->all->dtable->fieldList['unresolvedBugs']['minWidth'] = 86;
$config->product->all->dtable->fieldList['unresolvedBugs']['type'] = 'number';
$config->product->all->dtable->fieldList['unresolvedBugs']['show'] = true;
$config->product->all->dtable->fieldList['unresolvedBugs']['sortType'] = false;
$config->product->all->dtable->fieldList['unresolvedBugs']['group'] = 'g8';
$config->product->all->dtable->fieldList['totalBugs']['name'] = 'totalBugs';
$config->product->all->dtable->fieldList['totalBugs']['title'] = $lang->product->totalBugs;
+3
View File
@@ -589,6 +589,9 @@ class product extends control
$this->app->loadClass('pager', true);
$pager = new pager($recTotal, $recPerPage, $pageID);
$this->product->refreshStats(); // Refresh stats fields of products.
$productStatList = $this->product->getStats(array_keys($products), $orderBy, $pager, 'story', $programID);
/* Generate root program list. */
+15 -78
View File
@@ -879,26 +879,7 @@ class productModel extends model
$projects = $this->getProjectListByProduct($productID, $browseType, $branch, $involved, $orderBy, $pager);
if(empty($projects)) return array();
$projectKeys = array_keys($projects);
/* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
$tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason')
->from(TABLE_TASK)
->where('project')->in($projectKeys)
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
$hours = $this->loadModel('program')->computeProjectHours($tasks);
/* Get the number of project teams. */
$teams = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
->where('t1.root')->in($projectKeys)
->andWhere('t1.type')->eq('project')
->andWhere('t2.deleted')->eq(0)
->fetchGroup('root', 'account');
return $this->program->appendStatToProjects($projects, 'hours,teamCount', array('hours' => $hours, 'teams' => $teams));
return $this->program->appendStatToProjects($projects);
}
/**
@@ -1065,44 +1046,22 @@ class productModel extends model
if(empty($productIdList)) return array();
/* Get stats data. */
$products = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager);
$unclosedStory = $this->loadModel('story')->getUnClosedTotal();
$finishClosedStory = $this->story->getFinishClosedTotal();
$stats = $this->getStatsProducts($productIdList, $programID == 0, $orderBy, $pager);
$modules = array('plans', 'releases', 'latestReleases', 'bugs', 'unResolved', 'activeBugs', 'fixedBugs', 'closedBugs', 'thisWeekBugs', 'assignToNull');
foreach($modules as $module) $$module = $this->productTao->getStatisticByType($productIdList, $module);
list($stories, $requirements) = $this->getStatsStoriesAndRequirements($productIdList, $storyType);
$latestReleases = $this->productTao->getStatisticByType($productIdList, 'latestReleases');
$projectCountPairs = $this->productTao->getProjectCountPairs($productIdList);
$executionCountPairs = $this->productTao->getExecutionCountPairs($productIdList);
$coveragePairs = $this->getCaseCoveragePairs($productIdList);
$projectsPairs = $this->productTao->getProjectCountPairs($productIdList);
/* Render statistic result to each product. */
$stats = array();
foreach($products as $productID => $product)
foreach($stats as $product)
{
foreach($modules as $field) $product->$field = zget($$field, $productID, 0);
$product->stories = zget($stories, $product->id, array());
$product->stories['finishClosed'] = zget($finishClosedStory, $product->id, 0);
$product->stories['unclosed'] = zget($unclosedStory, $product->id, 0);
$product->activeStories = zget($product->stories, 'active', 0);
$product->requirements = zget($requirements, $product->id, array());
$product->executions = zget($executionCountPairs, $product->id, 0);
$product->coverage = zget($coveragePairs, $product->id, 0);
$product->projects = zget($projectsPairs, $product->id, 0);
$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 : '';
/* Calculate product progress. */
$closedTotal = $product->stories['closed'] + $product->requirements['closed'];
$allTotal = array_sum($product->stories) + array_sum($product->requirements);
$product->progress = empty($closedTotal) ? 0 : round($closedTotal / $allTotal * 100, 1);
$stats[$productID] = $product;
}
return $stats;
@@ -1493,35 +1452,14 @@ class productModel extends model
*/
public function formatDataForList(object $product, array $users): object
{
$totalStories = $product->stories['closed'] + $product->stories['unclosed'];
$totalBugs = $product->unResolved + $product->fixedBugs;
$product->type = 'product';
$product->productLine = $product->lineName;
$product->PO = !empty($product->PO) ? zget($users, $product->PO) : '';
$product->testCaseCoverage = $product->coverage;
$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;
$item = new stdClass();
$item->type = 'product';
$item->id = $product->id;
$item->name = $product->name;
$item->productLine = $product->lineName;
$item->PO = !empty($product->PO) ? zget($users, $product->PO) : '';
$item->createdDate = $product->createdDate;
$item->createdBy = $product->createdBy;
$item->draftStories = $product->stories['draft'];
$item->activeStories = $product->stories['active'];
$item->changingStories = $product->stories['changing'];
$item->reviewingStories = $product->stories['reviewing'];
$item->totalStories = $totalStories;
$item->storyCompleteRate = ($totalStories == 0 ? 0 : round($product->stories['finishClosed'] / $totalStories, 3) * 100);
$item->plans = $product->plans;
$item->execution = $product->executions;
$item->testCaseCoverage = $product->coverage;
$item->totalActivatedBugs= $product->activeBugs;
$item->totalBugs = $product->bugs;
$item->bugFixedRate = ($totalBugs == 0 ? 0 : round($product->fixedBugs / $totalBugs, 3) * 100);
$item->releases = $product->releases;
$item->latestReleaseDate = $product->latestReleaseDate;
$item->latestRelease = $product->latestRelease;
if(isset($product->actions)) $item->actions = $product->actions;
return $item;
return $product;
}
/**
@@ -2175,5 +2113,4 @@ class productModel extends model
/* 4. Clear actions older than 30 days. */
$this->loadModel('action')->cleanActions();
}
}
+1 -32
View File
@@ -804,41 +804,10 @@ class productTao extends productModel
$data = $type == 'program' ? $programStructure[$product->program] : $programStructure[$product->program][$product->line];
foreach($this->config->product->statisticFields as $key => $fields)
{
/* Get the total number of requirements and stories. */
if(strpos('stories|requirements', $key) !== false)
{
$totalObjects = 0;
foreach($product->$key as $status => $number) if(isset($this->lang->story->statusList[$status])) $totalObjects += $number;
$fieldType = $key == 'stories' ? 'Stories' : 'Requirements';
if(!isset($data['total' . $fieldType])) $data['total' . $fieldType] = 0;
$data['total' . $fieldType] += $totalObjects;
}
elseif($key == 'bugs')
{
$fieldType = 'Bugs';
}
foreach($fields as $field)
{
if(!isset($data[$field])) $data[$field] = 0;
$status = $field;
if(strpos($field, 'Requirements') !== false or strpos($field, 'Stories') !== false or $field == 'unResolvedBugs')
{
$length = strpos($field, $fieldType);
$status = substr($field, 0, $length);
}
if(strpos('requirements|stories', $key) !== false)
{
$objects = $product->$key;
$data[$field] += $objects[$status];
}
else
{
$data[$field] += $product->$status;
}
$data[$field] += $product->$field;
}
}
-8
View File
@@ -434,15 +434,7 @@ class productZen extends product
{
$product->line = zget($lines, $product->line, '');
$product->manager = zget($users, $product->PO, '');
$product->draftStories = (int)$product->stories['draft'];
$product->activeStories = (int)$product->stories['active'];
$product->changedStories = (int)$product->stories['changing'];
$product->reviewingStories = (int)$product->stories['reviewing'];
$product->closedStories = (int)$product->stories['closed'];
$product->totalStories = $product->activeStories + $product->changedStories + $product->draftStories + $product->closedStories + $product->reviewingStories;
$product->storyCompleteRate = ($product->totalStories == 0 ? 0 : round($product->closedStories / $product->totalStories, 3) * 100) . '%';
$product->unResolvedBugs = (int)$product->unResolved;
$product->assignToNullBugs = (int)$product->assignToNull;
$product->bugFixedRate = (($product->unResolved + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unResolved + $product->fixedBugs), 3) * 100) . '%';
$product->program = $product->programName;
}
+2 -2
View File
@@ -66,8 +66,8 @@ if($canBatchAction)
menu
(
set::id('navStatus'),
set::className('dropdown-menu'),
setID('navStatus'),
setClass('dropdown-menu'),
set::items(array_values($items))
);
+1 -1
View File
@@ -24,7 +24,7 @@ formPanel
set::required(true),
select
(
set::id('closedReasonBox'),
setID('closedReasonBox'),
set::name('closedReason'),
set::strong(false),
set::items($lang->productplan->closedReasonList),
+1 -1
View File
@@ -25,7 +25,7 @@ searchForm
dtable
(
set::id('unlinkBugList'),
setID('unlinkBugList'),
set::userMap($users),
set::checkable(true),
set::cols($cols),
+1 -1
View File
@@ -32,7 +32,7 @@ searchForm
dtable
(
set::id('unlinkStoryList'),
setID('unlinkStoryList'),
set::userMap($users),
set::cols($cols),
set::data(array_values($allStories)),
+2 -2
View File
@@ -203,7 +203,7 @@ detailBody
),
dtable
(
set::id('storyDTable'),
setID('storyDTable'),
set::userMap($users),
set::bordered(true),
set::cols($storyCols),
@@ -245,7 +245,7 @@ detailBody
),
dtable
(
set::id('bugDTable'),
setID('bugDTable'),
set::userMap($users),
set::bordered(true),
set::cols($bugCols),
+9 -9
View File
@@ -55,15 +55,15 @@ class program extends control
$this->config->program->search['actionURL'] = $actionURL;
$this->loadModel('search')->setSearchParams($this->config->program->search);
$this->view->title = $this->lang->program->browse;
$this->view->programs = $programs;
$this->view->status = $status;
$this->view->orderBy = $orderBy;
$this->view->pager = $pager;
$this->view->users = $this->loadModel('user')->getPairs('noletter');
$this->view->usersAvatar = $this->user->getAvatarPairs('');
$this->view->PMList = $PMList;
$this->view->param = $param;
$this->view->title = $this->lang->program->browse;
$this->view->programs = $programs;
$this->view->status = $status;
$this->view->orderBy = $orderBy;
$this->view->pager = $pager;
$this->view->users = $this->loadModel('user')->getPairs('noletter');
$this->view->usersAvatar = $this->user->getAvatarPairs('');
$this->view->PMList = $PMList;
$this->view->param = $param;
$this->display();
}
+51 -208
View File
@@ -287,8 +287,8 @@ class programModel extends model
$programs = $this->getTopPairs('noclosed');
/* Group data by product. */
list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours) = $this->getKanbanStatisticData($programs);
$productGroup = $this->processProductsForKanban($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours);
list($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions) = $this->getKanbanStatisticData($programs);
$productGroup = $this->processProductsForKanban($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions);
/* Group data by program. */
$kanbanGroup = array();
@@ -359,20 +359,7 @@ class programModel extends model
$executionPairs = array();
foreach($doingExecutions as $execution) $executionPairs[$execution->id] = $execution->id;
/* Compute executions and projects progress. */
$tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('(execution')->in($executionPairs)
->orWhere('project')->in($projectIdList)
->markRight(1)
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('execution', 'id');
$hours = $this->computeProjectHours($tasks);
$projectHours = $this->getProgressList();
return array($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions, $hours, $projectHours);
return array($productGroup, $planGroup, $releaseGroup, $projectGroup, $doingExecutions);
}
/**
@@ -384,12 +371,10 @@ class programModel extends model
* @param array $releaseGroup
* @param array $projectGroup
* @param array $doingExecutions
* @param array $hours
* @param array $projectHours
* @access public
* @return array
*/
public function processProductsForKanban(array $productGroup, array $planGroup, array $releaseGroup, array $projectGroup, array $doingExecutions, array $hours, array $projectHours): array
public function processProductsForKanban(array $productGroup, array $planGroup, array $releaseGroup, array $projectGroup, array $doingExecutions): array
{
if(empty($productGroup)) return $productGroup;
@@ -414,14 +399,10 @@ class programModel extends model
$status = $project->status == 'wait' ? 'wait' : 'doing';
$execution = zget($doingExecutions, $project->id, array());
if(!empty($execution))
{
$execution->hours = zget($hours, $execution->id, array());
if(helper::diffDate($today, $execution->end) > 0) $execution->delay = 1;
}
if(!empty($execution) && helper::diffDate($today, $execution->end) > 0) $execution->delay = 1;
$project->execution = $execution;
$project->hours['progress'] = zget($projectHours, $project->id, array());
$project->hours = array('progress' => $project->progress);
/* Convert predefined HTML entities to characters. */
$project->name = htmlspecialchars_decode($project->name, ENT_QUOTES);
@@ -527,67 +508,18 @@ class programModel extends model
}
/**
* 根据按照项目分组的任务,统计各个项目的工时和进度
* Compute hours and progress for project or execution.
*
* @param array $tasks
* @access public
* @return array
*/
public function computeProjectHours(array $tasks): array
{
if(empty($tasks)) return array();
$hours = array();
foreach($tasks as $projectID => $projectTasks)
{
/* Init hour. */
$hour = new stdclass();
$hour->totalConsumed = 0;
$hour->totalEstimate = 0;
$hour->totalLeft = 0;
/* Compute totalEstimate, totalConsumed, totalLeft to them. */
foreach($projectTasks as $task)
{
$hour->totalConsumed += $task->consumed;
$hour->totalEstimate += $task->estimate;
if(strpos('|cancel|closed|done|', "|{$task->status}|") === false) $hour->totalLeft += (float)$task->left;
}
$hours[$projectID] = $hour;
}
/* 计算进度。四舍五入totalEstimate, totalConsumed, totalLeft. */
$progressList = $this->loadModel('project')->getWaterfallProgress(array_keys($hours));
foreach($hours as $projectID => $hour)
{
$hour->totalEstimate = round($hour->totalEstimate, 1) ;
$hour->totalConsumed = round($hour->totalConsumed, 1);
$hour->totalLeft = round($hour->totalLeft, 1);
$hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
$hour->progress = zget($progressList, $projectID, ($hour->totalReal ? round($hour->totalConsumed / $hour->totalReal, 2) * 100 : 0)) ;
}
return $hours;
}
/**
* 将工时、团队人数、剩余任务数、团队成员等统计信息,追加到对应的项目中。
* 将团队成员等统计信息,追加到对应的项目中。
* Append statistics fields to projects.
*
* @param array $projects
* @param string $appendFields hours,teamCount,leftTasks,teamMembers
* @param array $data array keys are hours, teams and leftTasks.
* @param array $teams array keys are hours, teams and leftTasks.
* @access public
* @return array
*/
public function appendStatToProjects(array $projects, string $appendFields = '', array $data = array()): array
public function appendStatToProjects(array $projects, array $teams = array()): array
{
if(empty($projects)) return array();
if(empty($appendFields) || empty($data)) return $projects;
$appendFields = explode(',', $appendFields);
$emptyHour = json_decode(json_encode(array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0)));
/* Process projects. */
$stats = array();
foreach($projects as $projectID => $project)
@@ -601,20 +533,16 @@ class programModel extends model
if($delay > 0) $project->delay = $delay;
}
/* Merge the hours. */
if(in_array('hours', $appendFields))
{
$project->hours = $emptyHour;
if(isset($data['hours'])) $project->hours = zget($data['hours'], $project->id, $emptyHour);
}
/* Merge the team and left tasks. */
if(in_array('teamCount', $appendFields)) $project->teamCount = isset($data['teams'][$project->id]) ? count($data['teams'][$project->id]) : 0;
if(in_array('teamMembers', $appendFields)) $project->teamMembers = isset($data['teams'][$project->id]) ? array_keys($data['teams'][$project->id]) : array();
if(in_array('leftTasks', $appendFields)) $project->leftTasks = isset($data['leftTasks'][$project->id]) ? $data['leftTasks'][$project->id]->tasks : '—';
/* Merge project team. */
if(!empty($teams))
{
$project->teamCount = isset($teams[$project->id]) ? count($teams[$project->id]) : 0;
$project->teamMembers = isset($teams[$project->id]) ? array_keys($teams[$project->id]) : array();
}
$stats[$projectID] = $project;
}
return $stats;
}
@@ -728,59 +656,6 @@ class programModel extends model
->fetchAll();
}
/**
* 计算项目和项目集的进度。
* Get program and project progress list.
*
* @access public
* @return array
*/
public function getProgressList(): array
{
$totalProgress = array();
$projectCount = array();
$userPRJCount = array();
$progressList = array();
$programPairs = $this->getPairs();
$projectStats = $this->getProjectStats(0, 'all', 0, 'id_desc', '', true);
/* Add program progress. */
foreach(array_keys($programPairs) as $programID)
{
$totalProgress[$programID] = 0;
$projectCount[$programID] = 0;
$userPRJCount[$programID] = 0;
$progressList[$programID] = 0;
foreach($projectStats as $project)
{
if(strpos($project->path, ',' . $programID . ',') === false) continue;
/* The number of projects under this program that the user can view. */
if(strpos(',' . $this->app->user->view->projects . ',', ',' . $project->id . ',') !== false) $userPRJCount[$programID] ++;
$totalProgress[$programID] += $project->hours->progress;
$projectCount[$programID] ++;
}
if(empty($projectCount[$programID])) continue;
/* Program progress can't see when this user don't have all projects priv. */
if(!$this->app->user->admin && $userPRJCount[$programID] != $projectCount[$programID])
{
unset($progressList[$programID]);
continue;
}
$progressList[$programID] = round($totalProgress[$programID] / $projectCount[$programID]);
}
/* Add project progress. */
foreach($projectStats as $project) $progressList[$project->id] = $project->hours->progress;
return $progressList;
}
/**
* 创建项目集。
* Create a program.
@@ -1324,44 +1199,10 @@ class programModel extends model
{
if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getProjectStats($browseType);
/* Init vars. */
$projects = $this->getProjectList($programID, $browseType, $queryID, $orderBy, $programTitle, $queryAll, $pager);
if(empty($projects)) return array();
$projectKeys = array_keys($projects);
$executions = $this->loadModel('project')->getExecutionList($projectKeys);
/* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
$tasks = $this->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('project')->in($projectKeys)
->andWhere('execution')->in(array_keys($executions))
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
$hours = $this->computeProjectHours($tasks);
/* Get the number of left tasks. */
$leftTasks = array();
if($this->cookie->projectType && $this->cookie->projectType == 'bycard')
{
$leftTasks = $this->dao->select('t2.parent as project, count(*) as tasks')->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.execution = t2.id')
->where('t1.execution')->in(array_keys($executions))
->andWhere('t1.status')->notIn('cancel,closed')
->groupBy('t2.parent')
->fetchAll('project');
}
/* Get the members of project teams. */
$teamMembers = $this->dao->select('t1.root,t1.account')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
->where('t1.root')->in($projectKeys)
->andWhere('t1.type')->eq('project')
->andWhere('t2.deleted')->eq(0)
->fetchGroup('root', 'account');
$stats = $this->appendStatToProjects($projects, 'hours,teamCount,teamMembers,leftTasks', array('hours' => $hours, 'teams' => $teamMembers, 'leftTasks' => $leftTasks));
$stats = $this->appendStatToProjects($projects);
foreach($stats as $project) $project->name = htmlspecialchars_decode($project->name, ENT_QUOTES); // Convert predefined HTML entities to characters.
return $stats;
@@ -1570,13 +1411,30 @@ class programModel extends model
}
/* 1. Get summary and members of executions to be refreshed. */
$summary = $this->dao->select('execution, SUM(t1.estimate) AS totalEstimate, SUM(t1.consumed) AS totalConsumed, SUM(t1.`left`) AS totalLeft')->from(TABLE_TASK)->alias('t1')
$tasks = $this->dao->select('t1.id, execution, t1.estimate, t1.consumed, t1.`left`, t1.status')->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->where('t1.deleted')->eq(0)
->andWhere('t1.parent')->le(0) // Ignore child task.
->andWhere('t1.parent')->ge(0) // Ignore parent task.
->beginIF(!empty($projects))->andWhere('t1.project')->in(array_keys($projects))->fi()
->groupBy('execution')
->fetchAll();
->fetchAll('id');
$summary = array();
foreach($tasks as $task)
{
if(empty($task->execution)) continue;
if(!isset($summary[$task->execution]))
{
$summary[$task->execution] = new stdclass();
$summary[$task->execution]->totalEstimate = 0;
$summary[$task->execution]->totalConsumed = 0;
$summary[$task->execution]->totalLeft = 0;
}
$summary[$task->execution]->execution = $task->execution;
$summary[$task->execution]->totalEstimate += $task->estimate;
$summary[$task->execution]->totalConsumed += $task->consumed;
$summary[$task->execution]->totalLeft += ($task->status == 'closed' or $task->status == 'cancel') ? 0 : $task->left;
}
$teamMembers = $this->dao->select('t1.root, COUNT(1) AS members')->from(TABLE_TEAM)->alias('t1')
->leftJoin(TABLE_USER)->alias('t2')->on('t1.account=t2.account')
@@ -1606,6 +1464,8 @@ class programModel extends model
$stats[$nodeID]['totalEstimate'] += $execution->totalEstimate;
$stats[$nodeID]['totalConsumed'] += $execution->totalConsumed;
$stats[$nodeID]['totalLeft'] += $execution->totalLeft;
// Check $execution->execution and $nodeID(path) is not deleted.
if(empty($projectsPairs[$execution->execution]) && empty($projectsPairs[$nodeID]))
{
$stats[$nodeID]['totalConsumedNotDel'] += $execution->totalConsumed;
@@ -1614,22 +1474,6 @@ class programModel extends model
}
}
$this->loadModel('project');
foreach($projects as $projectID => $project)
{
if($project->model != 'waterfall') continue;
$projectStats = $this->project->getWaterfallPVEVAC($projectID);
$stats[$projectID]['totalEstimate'] = $projectStats['PV'];
$stats[$projectID]['totalConsumed'] = $projectStats['AC'];
$stats[$projectID]['totalLeft'] = $projectStats['left'];
if(empty($project->deleted))
{
$stats[$projectID]['totalConsumedNotDel'] = $projectStats['AC'];
$stats[$projectID]['totalLeftNotDel'] = $projectStats['left'];
}
}
foreach($teamMembers as $projectID => $teamCount)
{
if(!isset($stats[$projectID])) $stats[$projectID] = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'teamCount' => 0, 'totalConsumedNotDel' => 0, 'totalLeftNotDel' => 0);
@@ -1645,14 +1489,14 @@ class programModel extends model
->set('progress')->eq($progress)
->set('teamCount')->eq($project['teamCount'])
->set('estimate')->eq($project['totalEstimate'])
->set('consumed')->eq($project['totalConsumed'])
->set('left')->eq($project['totalLeft'])
->set('consumed')->eq($project['totalConsumedNotDel'])
->set('left')->eq($project['totalLeftNotDel'])
->where('id')->eq($projectID)
->exec();
}
/* 5. Update programStatsTime. */
$projectList = $this->dao->select('id,progress,path')->from(TABLE_PROJECT)
$projectList = $this->dao->select('id,progress,path,consumed,`left`')->from(TABLE_PROJECT)
->where('type')->eq('project')
->andWhere('parent')->ne(0)
->andWhere('deleted')->eq(0)
@@ -1665,19 +1509,18 @@ class programModel extends model
foreach($path as $programID)
{
if($programID == $projectID) continue;
$programProgress[$programID][] = $project->progress;
if(!isset($programProgress[$programID])) $programProgress[$programID] = array('consumed' => 0, 'left' => 0);
$programProgress[$programID]['consumed'] += $project->consumed;
$programProgress[$programID]['left'] += $project->left;
}
}
foreach($programProgress as $programID => $progress)
foreach($programProgress as $programID => $hours)
{
$count = count($progress);
$sum = array_sum($progress);
$average = round($sum / $count, 2);
$this->dao->update(TABLE_PROJECT)
->set('progress')->eq($average)
->where('id')->eq($programID)
->exec();
$progress = ($hours['consumed'] + $hours['left']) ? floor($hours['consumed'] / ($hours['consumed'] + $hours['left']) * 1000) / 1000 * 100 : 0;
$this->dao->update(TABLE_PROJECT)->set('progress')->eq($progress)->where('id')->eq($programID)->exec();
}
/* 6. Update projectStatsTime in config. */
@@ -1,32 +0,0 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
zdTable('user')->gen(5);
su('admin');
zdTable('project')->gen(50);
$task = zdTable('task');
$task->project->range('1-20');
$task->gen(100);
/**
title=测试 programModel::computeProjectHours();
timeout=0
cid=1
*/
global $tester;
$tester->loadModel('program');
$tasks = $tester->program->dao->select('id, project, estimate, consumed, `left`, status, closedReason, execution')
->from(TABLE_TASK)
->where('project')->eq('11')
->andWhere('parent')->lt(1)
->andWhere('deleted')->eq(0)
->fetchGroup('project', 'id');
r($tester->program->computeProjectHours(array())) && p() && e('0');
r($tester->program->computeProjectHours($tasks)) && p('11:totalConsumed,totalEstimate,totalLeft,progress') && e('15,30,10,60');
@@ -1,49 +0,0 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/program.class.php';
su('admin');
$program = zdTable('project');
$program->id->range('1-8');
$program->name->setFields(array(
array('field' => 'name1', 'range' => '项目集{2},项目{3},迭代{3}'),
array('field' => 'name2', 'range' => '1-8')
));
$program->type->range('program{2},project{3},sprint{3}');
$program->status->range('doing');
$program->parent->range('0,0,1,1,2,3,4,5');
$program->grade->range('1{2},2{3},1{3}');
$program->project->range('0{5},3-5');
$program->path->range('1,2,`1,3`,`1,4`,`2,5`,`3,6`,`4,7`,`5,8`')->prefix(',')->postfix(',');
$program->begin->range('20220112 000000:0')->type('timestamp')->format('YYYY-MM-DD');
$program->end->range('20230212 000000:0')->type('timestamp')->format('YYYY-MM-DD');
$program->gen(8);
$task = zdTable('task');
$task->name->prefix('任务')->range('1-3');
$task->project->range('3-5');
$task->execution->range('6-8');
$task->estimate->range('6-8');
$task->consumed->range('1,3,2');
$task->left->range('5,4,6');
$task->status->range('doing');
$task->assignedTo->range('admin');
$task->gen(3);
zdTable('team')->gen(0);
/**
title=测试 programModee::getProgressList();
timeout=0
cid=1
*/
$programTester = new programTest();
$progressList = $programTester->getProgressListTest();
r(count($progressList)) && p() && e('5'); // 获取项目和项目集的个数
r($progressList) && p('2') && e('25'); // 获取id=2的项目的进度
r($progressList) && p('4') && e('43'); // 获取id=4的项目集的进度
-11
View File
@@ -293,17 +293,6 @@ class programTest
return $this->program->getTeamMemberPairs($programID);
}
/**
* Get progress list.
*
* @access public
* @return array
*/
public function getProgressListTest()
{
return $this->program->getProgressList();
}
/**
* Test save state.
*
+3 -4
View File
@@ -51,9 +51,7 @@ foreach($programs as $program)
$program->budget = !empty($program->budget) ? zget($lang->project->currencySymbol, $program->budgetUnit) . ' ' . $programBudget : $lang->project->future;
$parents[$program->parent][] = $program->id;
/* Progress. */
if(isset($progressList[$program->id])) $program->progress = round($progressList[$program->id]);
$program->progress = round($program->progress);
$program->isParent = false;
if($program->parent > 0 and isset($programs[$program->parent])) $programs[$program->parent]->isParent = true;
if($program->parent > 0 and !isset($programs[$program->parent]))
@@ -78,7 +76,8 @@ foreach($programs as $program)
}
/* Set invested hours. */
if(!isset($program->invested)) $program->invested = 0;
$program->invested = !empty($this->config->execution->defaultWorkhours) ? round($program->consumed / $this->config->execution->defaultWorkhours, 2) : 0;
if(!is_null($program->end) && str_contains($program->end, LONG_TIME)) $program->end = $lang->program->longTime;
/* Actions. */
+8 -10
View File
@@ -52,7 +52,7 @@ $fnGenerateProgramRowData = function($programID, $program) use ($config, $users)
if(!isset($program['programName']) || $config->systemMode != 'ALM') return null;
/* ALM mode with more data. */
$totalStories = $program['finishClosedStories'] + $program['unclosedStories'];
$totalStories = $program['totalStories'];
$pmName = '';
if(!empty($program['programPM']))
{
@@ -70,9 +70,9 @@ $fnGenerateProgramRowData = function($programID, $program) use ($config, $users)
$item->PM = $pmName;
$item->createdDate = '';
$item->createdBy = '';
$item->totalUnclosedStories = $program['unclosedStories'];
$item->totalUnclosedStories = $totalStories - $program['closedStories'];
$item->totalStories = $totalStories;
$item->closedStoryRate = ($totalStories == 0 ? 0 : round($program['finishClosedStories'] / $totalStories, 3) * 100);
$item->closedStoryRate = ($totalStories == 0 ? 0 : round($program['closedStories'] / $totalStories, 3) * 100);
$item->totalPlans = $program['plans'];
$item->totalProjects = 0;
$item->totalExecutions = 0;
@@ -125,8 +125,6 @@ $fnGenerateLineRowData = function($programID, $lineID, $line) use ($config, &$li
/* Closure for generating product row data. */
$fnGenerateProductRowData = function($lineID, $product) use ($users)
{
$totalStories = $product->stories['finishClosed'] + $product->stories['unclosed'];
$item = new stdClass();
$item->type = 'product';
$item->id = $product->id;
@@ -135,15 +133,15 @@ $fnGenerateProductRowData = function($lineID, $product) use ($users)
$item->PM = !empty($product->PO) ? zget($users, $product->PO) : '';
$item->createdDate = $product->createdDate;
$item->createdBy = $product->createdBy;
$item->totalUnclosedStories = $product->stories['unclosed'];
$item->totalStories = $totalStories;
$item->closedStoryRate = empty($totalStories) ? 0 : round($product->stories['finishClosed'] / $totalStories, 3) * 100;
$item->totalUnclosedStories = $product->totalStories - $product->closedStories;
$item->totalStories = $product->totalStories;
$item->closedStoryRate = empty($product->totalStories) ? 0 : round($product->closedStories / $product->totalStories, 3) * 100;
$item->totalPlans = $product->plans;
$item->totalProjects = $product->projects;
$item->totalExecutions = $product->executions;
$item->testCaseCoverage = $product->coverage;
$item->totalActivatedBugs = $product->activeBugs;
$item->totalBugs = $product->bugs;
$item->totalActivatedBugs = $product->unresolvedBugs;
$item->totalBugs = $product->totalBugs;
$item->fixedRate = empty($item->totalBugs) ? 0 : round($product->fixedBugs / $item->totalBugs, 3) * 100;
$item->totalReleases = $product->releases;
$item->latestReleaseDate = $product->latestReleaseDate;
+3 -1
View File
@@ -325,6 +325,8 @@ class project extends control
$actionURL = $this->createLink('project', 'browse', "&programID=$programID&browseType=bySearch&queryID=myQueryID");
$this->project->buildSearchForm($queryID, $actionURL);
$this->loadModel('program')->refreshStats(); // Refresh stats fields of projects.
$programTitle = $this->loadModel('setting')->getItem("owner={$this->app->user->account}&module=project&key=programTitle");
$projectStats = $this->program->getProjectStats($programID, $browseType, $queryID, $orderBy, $programTitle, false, $pager);
@@ -1311,7 +1313,7 @@ class project extends control
*/
public function activate(int $projectID)
{
$project = $this->project->getByID($projectID);
$project = $this->project->getByID($projectID);
if(!empty($_POST))
{
+1 -1
View File
@@ -3,7 +3,7 @@ window.programMenuOnClick = function(data, url)
location.href = url.replace('%d', data.item.key);
}
window.renderReleaseCountCell = function(result, {col, row})
window.renderCell = function(result, {col, row})
{
if(col.name === 'name')
{
+10 -83
View File
@@ -242,7 +242,7 @@ class projectModel extends model
* @access public
* @return object[]
*/
public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 15, string $excludedModel = ''): array
public function getOverviewList(string $status = '', int $projectID = 0, string $orderBy = 'id_desc', int $limit = 10, string $excludedModel = ''): array
{
/* Get project list by query. */
$projects = $this->projectTao->fetchProjectListByQuery($status, $projectID, $orderBy, $limit, $excludedModel);
@@ -254,24 +254,15 @@ class projectModel extends model
$projects = array($projectID => $projects[$projectID]);
}
/* Get team members under the project. */
$projectIdList = array_keys($projects);
$teamCount = $this->projectTao->fetchMemberCountByIdList($projectIdList);
/* Get all consumed and all estimate under the project. */
$hours = $this->projectTao->fetchTaskEstimateByIdList($projectIdList, 'consumed,estimate');
/* Get bug, task and story summary under the project. */
$bugSummary = $this->projectTao->getTotalBugByProject($projectIdList);
$taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList);
$storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList);
$projectIdList = array_keys($projects);
$bugSummary = $this->projectTao->getTotalBugByProject($projectIdList);
$taskSummary = $this->projectTao->getTotalTaskByProject($projectIdList);
$storySummary = $this->projectTao->getTotalStoriesByProject($projectIdList);
/* Set project attribute. */
foreach($projects as $projectID => $project)
{
$project->teamCount = zget($teamCount, $projectID, 0);
$project->consumed = isset($hours[$projectID]) ? round((float)$hours[$projectID]->consumed, 1) : 0;
$project->estimate = isset($hours[$projectID]) ? round((float)$hours[$projectID]->estimate, 1) : 0;
$project->leftBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->leftBugs : 0;
$project->allBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->allBugs : 0;
$project->doneBugs = isset($bugSummary[$projectID]) ? $bugSummary[$projectID]->doneBugs : 0;
@@ -2029,70 +2020,6 @@ class projectModel extends model
return array($projectsStats, $ongoingExecutions);
}
/**
* Computer execution progress.
*
* @param array $executions
* @access public
* @return array
*/
public function computeProgress($executions)
{
$hours = array();
$emptyHour = array('totalEstimate' => 0, 'totalConsumed' => 0, 'totalLeft' => 0, 'progress' => 0);
$executionIdList = array_keys($executions);
/* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
$tasks = $this->loadModel('execution')->getTaskGroupByExecution($executionIdList);
$projects = $this->dao->select('t1.id,t2.model')
->from(TABLE_PROJECT)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->where('t1.deleted')->eq('0')
->andWhere('t1.id')->in($executionIdList)
->fetchPairs();
/* Compute totalEstimate, totalConsumed, totalLeft. */
foreach($tasks as $executionID => $executionTasks)
{
$hour = (object)$emptyHour;
foreach($executionTasks as $task)
{
$hour->totalEstimate += $task->estimate;
$hour->totalConsumed += $task->consumed;
if($task->status != 'cancel' and $task->status != 'closed') $hour->totalLeft += (float)$task->left;
}
$hours[$executionID] = $hour;
if(isset($executions[$executionID]) && $executions[$executionID]->grade > 1 && isset($projects[$executionID]) && in_array($projects[$executionID], array('waterfall', 'waterfallplus')))
{
$stageParents = $this->dao->select('id')->from(TABLE_EXECUTION)->where('id')->in(trim($executions[$executionID]->path, ','))->andWhere('type')->eq('stage')->andWhere('id')->ne($executions[$executionID]->id)->orderBy('grade')->fetchPairs();
foreach($stageParents as $stageParent)
{
if(!isset($hours[$stageParent]))
{
$hours[$stageParent] = clone $hour;
continue;
}
$hours[$stageParent]->totalEstimate += $hour->totalEstimate;
$hours[$stageParent]->totalConsumed += $hour->totalConsumed;
$hours[$stageParent]->totalLeft += $hour->totalLeft;
}
}
}
/* Compute totalReal and progress. */
foreach($hours as $hour)
{
$hour->totalEstimate = round($hour->totalEstimate, 1) ;
$hour->totalConsumed = round($hour->totalConsumed, 1);
$hour->totalLeft = round($hour->totalLeft, 1);
$hour->totalReal = $hour->totalConsumed + $hour->totalLeft;
$hour->progress = $hour->totalReal ? round($hour->totalConsumed / $hour->totalReal * 100, 2) : 0;
}
return $hours;
}
/**
* 设置项目的导航菜单。
* Set menu of project module.
@@ -2493,13 +2420,13 @@ class projectModel extends model
$project->budget = $project->budget != 0 ? zget($this->lang->project->currencySymbol, $project->budgetUnit) . ' ' . $projectBudget : $this->lang->project->future;
$project->statusTitle = $this->processStatus('project', $project);
$project->estimate = $project->hours->totalEstimate . $this->lang->project->workHourUnit;
$project->consume = $project->hours->totalConsumed . $this->lang->project->workHourUnit;
$project->surplus = $project->hours->totalLeft . $this->lang->project->workHourUnit;
$project->progress = $project->hours->progress;
$project->estimate = $project->estimate . $this->lang->project->workHourUnit;
$project->consume = $project->consumed . $this->lang->project->workHourUnit;
$project->surplus = $project->left . $this->lang->project->workHourUnit;
$project->progress = $project->progress;
$project->end = $project->end == LONG_TIME ? $this->lang->project->longTime : $project->end;
$project->hasProduct = zget($this->lang->project->projectTypeList, $project->hasProduct);
$project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->hours->totalConsumed / $this->config->execution->defaultWorkhours, 2) : 0;
$project->invested = !empty($this->config->execution->defaultWorkhours) ? round($project->consumed / $this->config->execution->defaultWorkhours, 2) : 0;
if($project->PM)
{
@@ -1,26 +0,0 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
zdTable('project')->config('execution')->gen(10);
zdTable('task')->config('task')->gen(20);
zdTable('taskteam')->gen(20);
su('admin');
/**
title=测试 projectModel->computeProgress();
timeout=0
cid=1
*/
global $tester;
$tester->loadModel('project');
$tester->loadModel('execution');
$executions = $tester->execution->getList(11);
$executions = $tester->project->computeProgress($executions);
r(count($executions)) && p('') && e('1'); // 查看项目11下的所有执行的数量
r($executions) && p('101:totalEstimate,totalConsumed,totalLeft,progress,totalReal') && e('44,9,44,16.98,53'); // 查看计算后的执行工时汇总信息
+1 -1
View File
@@ -13,7 +13,7 @@ namespace zin;
$programAclList = array();
$projectAclList = array();
foreach($lang->program->subAcls as $acl => $label) $programAclList[] = array('text' => $label, 'value' => $acl);
foreach($lang->project->acls as $acl => $label) $projectAclList[] = array('text' => $label, 'value' => $acl);
foreach($lang->project->acls as $acl => $label) $projectAclList[] = array('text' => $label, 'value' => $acl);
jsVar('LONG_TIME', LONG_TIME);
jsVar('weekend', $config->execution->weekend);
+4
View File
@@ -10,6 +10,9 @@ declare(strict_types=1);
*/
namespace zin;
jsVar('langManDay', $lang->program->manDay);
jsVar('langPostponed', $lang->project->statusList['delay']);
/* zin: Define the feature bar on main menu. */
featureBar
(
@@ -108,6 +111,7 @@ dtable
set::sortLink(createLink('project', 'browse', "programID=$programID&browseType=$browseType&param=$param&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")),
set::footPager(usePager()),
set::customCols(true),
set::onRenderCell(jsRaw('window.renderCell')),
set::emptyTip($lang->project->empty),
set::createTip($lang->project->create),
set::createLink(hasPriv('project', 'create') ? createLink('project', 'createGuide') : ''),
+11 -9
View File
@@ -10,7 +10,7 @@ declare(strict_types=1);
*/
namespace zin;
$linkParams = "projectID={$project->id}&productID={$productID}&branch=$branchID&orderBy=status,id_desc&build=$buildID&type={key}" . ($type == 'bysearch' ? '' : "&param=$param");
$linkParams = "projectID={$project->id}&productID={$productID}&branch=$branchID&orderBy=status,id_desc&build=$buildID&type={key}" . ($type == 'bysearch' ? '' : "&param=$param") . "&recTotal={$pager->recTotal}&recPerpage={$pager->recPerPage}";
featureBar
(
set::current($type),
@@ -29,14 +29,16 @@ toolbar
'url' => createLink('bug', 'export', "productID={$productID}&orderBy=$orderBy&browseType=&projectID={$project->id}"),
'data-toggle' => 'modal'
))) : null,
$canCreate ? item(set(array
$canCreate ? item
(
'text' => $lang->bug->create,
'icon' => 'plus',
'type' => 'primary',
'url' => createLink('bug', 'create', "productID={$productID}&branch={$branchID}&extras=projectID={$project->id}")
)),
setData('app', 'project')
set(array
(
'text' => $lang->bug->create,
'icon' => 'plus',
'type' => 'primary',
'url' => createLink('bug', 'create', "productID={$productID}&branch={$branchID}&extras=projectID={$project->id}")
)),
setData('app', 'project')
) : null
);
@@ -51,7 +53,7 @@ sidebar
)))
);
$canBatchAssignTo = common::hasPriv('bug', 'batchAssignTo');
$canBatchAssignTo = hasPriv('bug', 'batchAssignTo');
$config->bug->dtable->fieldList['module']['map'] = $modulePairs;
$config->bug->dtable->fieldList['story']['map'] = $stories;
+4 -12
View File
@@ -37,14 +37,11 @@ featureBar
/* zin: Define the toolbar on main menu. */
$canCreateBuild = hasPriv('projectbuild', 'create') && common::canModify('project', $project);
if($canCreateBuild) $createItem = array('icon' => 'plus', 'class' => 'primary', 'text' => $lang->build->create, 'url' => $this->createLink('projectbuild', 'create', "projectID={$project->id}"));
toolbar
(
!empty($createItem) ? item(set($createItem)) : null
);
if($canCreateBuild) toolbar(item(set(array('icon' => 'plus', 'class' => 'primary', 'text' => $lang->build->create, 'url' => createLink('projectbuild', 'create', "projectID={$project->id}")))));
jsVar('projectID', $project->id);
jsVar('changeProductLink', helper::createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type=product&param={productID}&orderBy={$orderBy}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}"));
jsVar('changeProductLink', createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type=product&param={productID}&orderBy={$orderBy}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}"));
jsVar('scmPathTip', $lang->build->scmPath);
jsVar('filePathTip', $lang->build->filePath);
jsVar('integratedTip', $lang->build->integrated);
@@ -59,12 +56,7 @@ dtable
set::sortLink(createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type={$type}&param={$param}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")),
set::onRenderCell(jsRaw('window.renderCell')),
set::getCellSpan(jsRaw('window.getCellSpan')),
set::footPager(
usePager
(
array('linkCreator' => helper::createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type={$type}&param={$param}&orderBy={$orderBy}&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}"), 'recTotal' => $pager->recTotal, 'recPerPage' => $pager->recPerPage)
)
),
set::footPager(usePager(array('linkCreator' => createLink($app->rawModule, $app->rawMethod, "projectID={$project->id}&type={$type}&param={$param}&orderBy={$orderBy}&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}"), 'recTotal' => $pager->recTotal, 'recPerPage' => $pager->recPerPage))),
set::emptyTip($lang->build->noBuild),
set::createTip($lang->build->create),
set::createLink($canCreateBuild ? createLink('projectbuild', 'create', "projectID={$project->id}") : '')
+13 -10
View File
@@ -22,15 +22,17 @@ jsVar('pageExecSummary', $lang->execution->pageExecSummary);
jsVar('checkedExecSummary', $lang->execution->checkedExecSummary);
$footToolbar = array();
$canBatchEdit = common::hasPriv('execution', 'batchEdit');
$canBatchChangeStatus = common::hasPriv('execution', 'batchChangeStatus');
$canBatchEdit = hasPriv('execution', 'batchEdit');
$canBatchChangeStatus = hasPriv('execution', 'batchChangeStatus');
$canBatchAction = $canBatchEdit || $canBatchChangeStatus;
if($canBatchAction)
{
$editClass = $canBatchEdit ? 'batch-btn' : 'disabled';
$footToolbar['items'][] = array(
$footToolbar['items'][] = array
(
'type' => 'btn-group',
'items' => array(
'items' => array
(
array('text' => $lang->edit, 'className' => "secondary size-sm {$editClass}", 'btnType' => 'primary', 'data-url' => createLink('execution', 'batchEdit')),
)
);
@@ -58,6 +60,12 @@ $fnGenerateCols = function() use ($config, $project)
$fieldList['actions']['actionsMap']['edit']['url'] = createLink('programplan', 'edit', "stageID={rawID}&projectID={projectID}");
}
if(!$this->cookie->showTask)
{
$fieldList['name']['type'] = 'title';
unset($fieldList['name']['nestedToggle']);
}
return array_values($fieldList);
};
@@ -126,12 +134,7 @@ dtable
set::canRowCheckable(jsRaw("function(rowID){return this.getRowInfo(rowID).data.id.indexOf('pid') > -1;}")),
set::checkInfo(jsRaw("function(checkedIDList){ return window.footerSummary(this, checkedIDList);}")),
set::footToolbar($footToolbar),
set::footPager(
usePager
(
array('linkCreator' => helper::createLink('project', 'execution', "status={$status}&projectID=$projectID&orderBy={$orderBy}&productID={$productID}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}"))
)
),
set::footPager(usePager(array('linkCreator' => createLink('project', 'execution', "status={$status}&projectID=$projectID&orderBy={$orderBy}&productID={$productID}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}")))),
set::emptyTip($lang->execution->noExecution),
set::createTip($isStage ? $lang->programplan->create : $lang->execution->create),
set::createLink(hasPriv('execution', 'create') ? $createLink : '')
+2 -1
View File
@@ -17,7 +17,8 @@ jsVar('confirmDelete', $lang->group->confirmDelete);
/* zin: Define the feature bar on main menu. */
featureBar
(
set::current('all'),
set::linkParams("projectID={$projectID}&programID={$programID}"),
set::current('all')
);
/* zin: Define the toolbar on main menu. */
-2
View File
@@ -161,11 +161,9 @@ $config->repo->fileExt["yaml"] = array('.yaml', '.yml');
$config->repo->search['module'] = 'repo';
$config->repo->search['fields']['name'] = $lang->repo->name;
$config->repo->search['fields']['id'] = $lang->repo->id;
$config->repo->search['fields']['product'] = $lang->repo->product;
$config->repo->search['fields']['projects'] = $lang->repo->projects;
$config->repo->search['fields']['SCM'] = $lang->repo->SCM;
$config->repo->search['fields']['desc'] = $lang->repo->desc;
$config->repo->search['params']['name'] = array('operator' => 'include', 'control' => 'input', 'values' => '');
$config->repo->search['params']['product'] = array('operator' => 'include', 'control' => 'select', 'values' => '');
+62 -229
View File
@@ -322,6 +322,7 @@ class repo extends control
$entry = urldecode($entry);
$pathInfo = helper::mbPathinfo($entry);
if($repo->SCM == 'Gitlab') $repo = $this->repo->processGitService($repo, true);
if($this->app->tab == 'execution') $this->view->executionID = $objectID;
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->view;
$this->view->dropMenus = $this->repoZen->getBranchAndTagItems($repo, $this->cookie->repoBranch);
@@ -539,32 +540,33 @@ class repo extends control
$this->scm->setEngine($repo);
$log = $this->scm->log('', $revision, $revision);
$history = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('revision')->eq($log[0]->revision)->andWhere('repo')->eq($repoID)->fetch();
if($history)
$revision = $this->repo->getHistoryRevision($repoID, $log[0]->revision);
if($revision)
{
if(in_array($repo->SCM, $this->config->repo->gitTypeList))
{
$thisAndPrevRevisions = $this->scm->exec("rev-list -n 2 {$history->revision} --");
$thisAndPrevRevisions = $this->scm->exec("rev-list -n 2 {$revision} --");
array_shift($thisAndPrevRevisions);
if($thisAndPrevRevisions) $oldRevision = array_shift($thisAndPrevRevisions);
}
else
{
$oldRevision = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('revision')->lt($history->revision)->andWhere('repo')->eq($repoID)->orderBy('revision_desc')->limit(1)->fetch('revision');
$oldRevision = $this->repo->getHistoryRevision($repoID, $revision, false, 'lt');
}
}
if(empty($oldRevision))
{
$oldRevision = '^';
if($history and in_array($repo->SCM, $this->config->repo->gitTypeList)) $oldRevision = "{$history->revision}^";
if($revision and in_array($repo->SCM, $this->config->repo->gitTypeList)) $oldRevision = "{$revision}^";
}
$this->locate($this->repo->createLink('diff', "repoID=$repoID&objectID=$objectID&entry=&oldrevision=$oldRevision&newRevision={$log[0]->revision}"));
}
/**
* 代码blame信息。
* Blame repo file.
*
* @param int $repoID
@@ -575,13 +577,14 @@ class repo extends control
* @access public
* @return void
*/
public function blame($repoID, $objectID = 0, $entry = '', $revision = 'HEAD', $encoding = '')
public function blame(int $repoID, int $objectID = 0, string $entry = '', string $revision = 'HEAD', string $encoding = '')
{
$this->commonAction($repoID, $objectID);
if($this->get->repoPath) $entry = $this->get->repoPath;
if($repoID == 0) $repoID = $this->session->repoID;
$repo = $this->repo->getByID($repoID);
$file = $entry;
$entry = $this->repo->decodePath($entry);
@@ -596,7 +599,7 @@ class repo extends control
if($encoding != 'utf-8') $blames[$i]['content'] = helper::convertEncoding($blame['content'], $encoding);
}
$log = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->dao->select('revision,commit')->from(TABLE_REPOHISTORY)->where('revision')->eq($revision)->andWhere('repo')->eq($repo->id)->fetch() : '';
$log = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->repo->getHistoryRevision($repo->id, $revision, true) : '';
$this->view->title = $this->lang->repo->common;
$this->view->repoID = $repoID;
@@ -607,14 +610,14 @@ class repo extends control
$this->view->entry = $entry;
$this->view->file = $file;
$this->view->encoding = str_replace('-', '_', $encoding);
$this->view->historys = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->dao->select('revision,commit')->from(TABLE_REPOHISTORY)->where('revision')->in($revisions)->andWhere('repo')->eq($repo->id)->fetchPairs() : '';
$this->view->revisionName = ($log and in_array($repo->SCM, $this->config->repo->gitTypeList)) ? $this->repo->getGitRevisionName($log->revision, $log->commit) : $revision;
$this->view->revisionName = ($log && in_array($repo->SCM, $this->config->repo->gitTypeList)) ? $this->repo->getGitRevisionName($log->revision, $log->commit) : $revision;
$this->view->blames = $blames;
$this->display();
}
/**
* Show diff.
* 代码diff信息。
* Show repo diff.
*
* @param int $repoID
* @param int $objectID
@@ -627,125 +630,51 @@ class repo extends control
* @access public
* @return void
*/
public function diff($repoID, $objectID = 0, $entry = '', $oldRevision = '', $newRevision = '', $showBug = 'false', $encoding = '', $isBranchOrTag = false)
public function diff(int $repoID, int $objectID = 0, string $entry = '', string $oldRevision = '', string $newRevision = '', string $showBug = 'false', string $encoding = '', bool $isBranchOrTag = false)
{
$newRevision = strtr($newRevision, '*', '-');
$oldRevision = strtr($oldRevision, '*', '-');
$oldRevision = urldecode(urldecode($oldRevision)); //Fix error.
if($isBranchOrTag)
{
$oldRevision = strtr($oldRevision, '*', '-');
$newRevision = strtr($newRevision, '*', '-');
if($isBranchOrTag)
{
$oldRevision = urldecode(helper::safe64Decode($oldRevision));
$newRevision = urldecode(helper::safe64Decode($newRevision));
}
}
else
{
$oldRevision = urldecode(urldecode($oldRevision)); //Fix error.
$oldRevision = urldecode(helper::safe64Decode($oldRevision));
$newRevision = urldecode(helper::safe64Decode($newRevision));
}
$this->commonAction($repoID, $objectID);
$repo = $this->repo->getByID($repoID);
if($this->get->repoPath) $entry = $this->get->repoPath;
$file = $entry;
$repo = $this->repo->getByID($repoID);
$entry = $this->repo->decodePath($entry);
if($repo->SCM == 'Git' and !is_dir($repo->path))
{
$error = sprintf($this->lang->repo->error->notFound, $repo->name, $repo->path);
return print(js::error($error) . js::locate($this->repo->createLink('maintain')));
}
$pathInfo = pathinfo($entry);
$suffix = '';
if(isset($pathInfo["extension"])) $suffix = strtolower($pathInfo["extension"]);
if($repo->SCM == 'Git' && !is_dir($repo->path)) return $this->sendError(sprintf($this->lang->repo->error->notFound, $repo->name, $repo->path), $this->repo->createLink('maintain'));
$arrange = $this->cookie->arrange ? $this->cookie->arrange : 'inline';
if($this->server->request_method == 'POST')
{
$oldRevision = isset($this->post->revision[1]) ? $this->post->revision[1] : '';
$newRevision = isset($this->post->revision[0]) ? $this->post->revision[0] : '';
if($this->server->request_method == 'POST') return $this->repoZen->locateDiffPage($repoID, $objectID, $arrange, $isBranchOrTag, $file);
if($this->post->arrange)
{
$arrange = $this->post->arrange;
helper::setcookie('arrange', $arrange);
}
if($this->post->encoding) $encoding = $this->post->encoding;
if($this->post->isBranchOrTag) $isBranchOrTag = $this->post->isBranchOrTag;
$this->locate($this->repo->createLink('diff', "repoID=$repoID&objectID=$objectID&entry=" . $this->repo->encodePath($entry) . "&oldrevision=$oldRevision&newRevision=$newRevision&showBug=&encoding=$encoding&isBranchOrTag=$isBranchOrTag"));
}
$info = new stdClass();
$diffs = array();
$encoding = empty($encoding) ? $repo->encoding : $encoding;
$encoding = strtolower(str_replace('_', '-', $encoding));
if($oldRevision !== '')
{
$this->scm->setEngine($repo);
$info = $this->scm->info($entry, $newRevision);
$diffs = $this->scm->diff($entry, $oldRevision, $newRevision, 'yes', $isBranchOrTag ? 'isBranchOrTag': '');
}
foreach($diffs as $diff)
{
if($encoding != 'utf-8')
{
$diff->fileName = helper::convertEncoding($diff->fileName, $encoding);
if(empty($diff->contents)) continue;
foreach($diff->contents as $content)
{
if(empty($content->lines)) continue;
foreach($content->lines as $lines)
{
if(empty($lines->line)) continue;
$lines->line = helper::convertEncoding($lines->line, $encoding);
}
}
}
}
/* When arrange is appose then adjust data for show them easy.*/
if($arrange == 'appose')
{
foreach($diffs as $diffFile)
{
if(empty($diffFile->contents)) continue;
foreach($diffFile->contents as $content)
{
$old = array();
$new = array();
foreach($content->lines as $line)
{
if($line->type != 'new') $old[$line->oldlc] = $line->line;
if($line->type != 'old') $new[$line->newlc] = $line->line;
}
$content->old = $old;
$content->new = $new;
}
}
}
$this->view->type = 'diff';
$this->view->showBug = $showBug;
$this->view->entry = urldecode($entry);
$this->view->suffix = $suffix;
$this->view->file = $file;
$this->view->repoID = $repoID;
$this->view->branchID = (string) $this->cookie->repoBranch;
$this->view->objectID = $objectID;
$this->view->repo = $repo;
$this->view->encoding = str_replace('-', '_', $encoding);
$this->view->arrange = $arrange;
$this->view->diffs = $diffs;
$this->view->newRevision = $newRevision;
$this->view->oldRevision = $oldRevision;
$this->view->revision = $newRevision;
$this->view->historys = in_array($repo->SCM, $this->config->repo->gitTypeList) ? $this->dao->select('revision,commit')->from(TABLE_REPOHISTORY)->where('revision')->in("$oldRevision,$newRevision")->andWhere('repo')->eq($repo->id)->fetchPairs() : '';
$this->view->info = $info;
if($encoding != 'utf-8') $diffs = $this->repoZen->encodingDiff($diffs, $encoding);
if($arrange == 'appose') $diffs = $this->repoZen->getApposeDiff($diffs);
$this->view->entry = urldecode($entry);
$this->view->encoding = str_replace('-', '_', $encoding);
$this->view->file = $file;
$this->view->repoID = $repoID;
$this->view->branchID = (string) $this->cookie->repoBranch;
$this->view->objectID = $objectID;
$this->view->repo = $repo;
$this->view->diffs = $diffs;
$this->view->newRevision = $newRevision;
$this->view->oldRevision = $oldRevision;
$this->view->isBranchOrTag = $isBranchOrTag;
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->diff;
@@ -753,6 +682,7 @@ class repo extends control
}
/**
* 代码下载。
* Download repo file.
*
* @param int $repoID
@@ -764,7 +694,7 @@ class repo extends control
* @access public
* @return void
*/
public function download($repoID, $path, $fromRevision = 'HEAD', $toRevision = '', $type = 'file', $isBranchOrTag = false)
public function download(int $repoID, string $path, string $fromRevision = 'HEAD', string $toRevision = '', string $type = 'file', bool $isBranchOrTag = false)
{
if($this->get->repoPath) $path = $this->get->repoPath;
$entry = $this->repo->decodePath($path);
@@ -790,8 +720,9 @@ class repo extends control
$fileName = basename(urldecode($entry));
if($type != 'file') $fileName .= "r$fromRevision--r$toRevision.patch";
$extension = ltrim(strrchr($fileName, '.'), '.');
$this->fetch('file', 'sendDownHeader', array("fileName" => $fileName, "fileType" => $extension, "content" => $content));
$this->fetch('file', 'sendDownHeader', array("fileName" => $fileName, "fileType" => $extension, "content" => $content));
}
/**
@@ -823,7 +754,8 @@ class repo extends control
}
/**
* Show sync comment.
* 显示提交同步进度。
* Show sync commit.
*
* @param int $repoID
* @param int $objectID projectID|executionID
@@ -831,16 +763,15 @@ class repo extends control
* @access public
* @return void
*/
public function showSyncCommit($repoID = 0, $objectID = 0, $branch = '')
public function showSyncCommit(int $repoID = 0, int $objectID = 0, string $branch = '')
{
$this->commonAction($repoID, $objectID);
if($repoID == 0) $repoID = $this->session->repoID;
if($branch) $branch = base64_decode(helper::safe64Decode($branch));
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->showSyncCommit;
$latestInDB = $this->repo->getLatestCommit($repoID);
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->showSyncCommit;
$this->view->version = $latestInDB ? (int)$latestInDB->commit : 1;
$this->view->repoID = $repoID;
$this->view->repo = $this->repo->getByID($repoID);
@@ -851,6 +782,7 @@ class repo extends control
}
/**
* 根据提交信息关联需求。
* Link story to commit.
*
* @param int $repoID
@@ -898,6 +830,7 @@ class repo extends control
}
/**
* 根据提交信息关联Bug。
* Link bug to commit.
*
* @param int $repoID
@@ -945,6 +878,7 @@ class repo extends control
}
/**
* 根据提交信息关联任务。
* Link task to commit.
*
* @param int $repoID
@@ -999,6 +933,7 @@ class repo extends control
}
/**
* 取消提交信息的关联记录。
* Unlink object and commit revision.
*
* @param int $repoID
@@ -1008,7 +943,7 @@ class repo extends control
* @access public
* @return void
*/
public function unlink($repoID, $revision, $objectType, $objectID)
public function unlink(int $repoID, string $revision, string $objectType, int $objectID)
{
$this->repo->unlink($repoID, $revision, $objectType, $objectID);
@@ -1042,10 +977,8 @@ class repo extends control
$gitlab = empty($server) ? array_shift($gitlabList) : $this->gitlab->getById($server);
$repoList = $gitlab ? $this->repoZen->getGitlabNotExistRepos($gitlab) : array();
$products = $this->loadModel('product')->getPairs('', 0, '', 'all');
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->importAction;
$this->view->gitlabPairs = $this->gitlab->getPairs();
$this->view->products = $products;
@@ -1056,6 +989,7 @@ class repo extends control
}
/**
* 获取代码对比编辑器内容。
* Get diff editor content by ajax.
*
* @param int $repoID
@@ -1063,12 +997,12 @@ class repo extends control
* @param string $entry
* @param string $oldRevision
* @param string $newRevision
* @param string $showBug
* @param string $showBug // Used for biz.
* @param string $encoding
* @access public
* @return void
*/
public function ajaxGetDiffEditorContent($repoID, $objectID = 0, $entry = '', $oldRevision = '', $newRevision = '', $showBug = 'false', $encoding = '')
public function ajaxGetDiffEditorContent(int $repoID, int $objectID = 0, string $entry = '', string $oldRevision = '', string $newRevision = '', string $showBug = 'false', string $encoding = '')
{
if(!$entry) $entry = (string) $this->cookie->repoCodePath;
@@ -1106,6 +1040,7 @@ class repo extends control
}
/**
* 获取代码详情的编辑器内容。
* Get editor content by ajax.
*
* @param int $repoID
@@ -1117,7 +1052,7 @@ class repo extends control
* @access public
* @return void
*/
public function ajaxGetEditorContent($repoID, $objectID = 0, $entry = '', $revision = 'HEAD', $showBug = 'false', $encoding = '')
public function ajaxGetEditorContent(int $repoID, int $objectID = 0, string $entry = '', string $revision = 'HEAD', string $showBug = 'false', string $encoding = '')
{
if(!$entry) $entry = (string) $this->cookie->repoCodePath;
@@ -1130,6 +1065,7 @@ class repo extends control
$info = $this->scm->info($entry, $revision);
$path = $entry ? $info->path : '';
if($info->kind == 'dir') $this->locate($this->repo->createLink('browse', "repoID=$repoID&branchID=&objectID=$objectID&path=" . $this->repo->encodePath($path) . "&revision=$revision"));
$content = $this->scm->cat($entry, $revision);
$entry = urldecode($entry);
$pathInfo = pathinfo($entry);
@@ -1152,17 +1088,11 @@ class repo extends control
$this->view->title = $this->lang->repo->common . $this->lang->colon . $this->lang->repo->view;
$this->view->type = 'view';
$this->view->showBug = $showBug;
$this->view->encoding = str_replace('-', '_', $encoding);
$this->view->repoID = $repoID;
$this->view->branchID = $this->cookie->repoBranch;
$this->view->objectID = $objectID;
$this->view->repo = $repo;
$this->view->revision = $revision;
$this->view->oldRevision = '';
$this->view->file = $file;
$this->view->entry = $entry;
$this->view->path = $entry;
$this->view->info = $info;
$this->view->suffix = $suffix;
$this->view->content = $content ? $content : '';
$this->view->pathInfo = $pathInfo;
@@ -1171,6 +1101,7 @@ class repo extends control
}
/**
* 异步同步代码提交记录。
* Ajax sync comment.
*
* @param int $repoID
@@ -1178,136 +1109,38 @@ class repo extends control
* @access public
* @return void
*/
public function ajaxSyncCommit($repoID = 0, $type = 'batch')
public function ajaxSyncCommit(int $repoID = 0, string $type = 'batch')
{
set_time_limit(0);
$repo = $this->repo->getByID($repoID);
if(empty($repo)) return;
if(empty($repo)) return print($this->config->repo->repoSyncLog->finish);
if($repo->synced) return print($this->config->repo->repoSyncLog->finish);
if(in_array($repo->SCM, array('Gitea', 'Gogs')))
{
$logFile = realPath($this->app->getTmpRoot() . $this->config->repo->repoSyncLog->logFilePrefix . strtolower($repo->SCM) . ".{$repo->name}.log");
if($logFile)
{
$content = file($logFile);
foreach($content as $line)
{
if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->fatal) !== false) return print($line);
if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->failed) !== false) return print($line);
}
$lastLine = $content[count($content) - 1];
if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->done) === false)
{
if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->emptyRepo) !== false)
{
@unlink($logFile);
}
elseif($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->total) !== false)
{
$logContent = file_get_contents($logFile);
if($this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCount) !== false and $this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCompress) !== false)
{
@unlink($logFile);
}
else
{
return print($this->config->repo->repoSyncLog->one);
}
}
else
{
return print($this->config->repo->repoSyncLog->one);
}
}
else
{
@unlink($logFile);
}
}
$syncLog = $this->repoZen->syncLocalCommit($repo);
if($syncLog) return print($syncLog);
}
$this->commonAction($repoID);
$this->scm->setEngine($repo);
$branchID = '';
if(in_array($repo->SCM, $this->config->repo->gitTypeList) and empty($branchID))
{
$branches = $this->scm->branch();
if($branches)
{
/* Init branchID. */
if($this->cookie->syncBranch) $branchID = $this->cookie->syncBranch;
if(!isset($branches[$branchID])) $branchID = '';
if(empty($branchID)) $branchID = 'master';
/* Get unsynced branches. */
unset($branches['master']);
if($branchID != 'master')
{
foreach($branches as $branch)
{
unset($branches[$branch]);
if($branch == $branchID) break;
}
}
$this->repo->setRepoBranch($branchID);
helper::setcookie("syncBranch", $branchID, 0, $this->config->webRoot, '', $this->config->cookieSecure, true);
}
}
$branches = $this->repoZen->getSyncBranches($repo, $branchID);
$logs = array();
$version = 1;
if($repo->SCM != 'Gitlab')
{
$latestInDB = $this->dao->select('t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
->where('t1.repo')->eq($repoID)
->beginIF($repo->SCM == 'Git' and $this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
->beginIF($repo->SCM == 'Gitlab' and $this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
->orderBy('t1.time')
->limit(1)
->fetch();
$latestInDB = $this->repo->getLatestCommit($repoID, false);
$version = empty($latestInDB) ? 1 : $latestInDB->commit + 1;
$revision = $version == 1 ? 'HEAD' : (in_array($repo->SCM, array('Git', 'Gitea', 'Gogs')) ? $latestInDB->commit : $latestInDB->revision);
if($type == 'batch')
{
$logs = $this->scm->getCommits($revision, $this->config->repo->batchNum, $branchID);
}
else
{
$logs = $this->scm->getCommits($revision, 0, $branchID);
}
$logs = $this->scm->getCommits($revision, $type == 'batch' ? $this->config->repo->batchNum : 0, $branchID);
}
$commitCount = $this->repo->saveCommit($repoID, $logs, $version, $branchID);
if(empty($commitCount))
{
if(!$repo->synced)
{
if(in_array($repo->SCM, $this->config->repo->gitTypeList))
{
if($branchID) $this->repo->saveExistCommits4Branch($repo->id, $branchID);
$branchID = reset($branches);
setcookie("syncBranch", $branchID, 0, $this->config->webRoot, '', $this->config->cookieSecure, true);
if($branchID) $this->repo->fixCommit($repoID);
}
if(empty($branchID))
{
$this->repo->markSynced($repoID);
return print($this->config->repo->repoSyncLog->finish);
}
}
}
$this->dao->update(TABLE_REPO)->set('commits=commits + ' . $commitCount)->where('id')->eq($repoID)->exec();
echo $type == 'batch' ? $commitCount : $this->config->repo->repoSyncLog->finish;
echo $this->repoZen->checkSyncResult($repo, $branches, $branchID, $commitCount);
}
/**
+35 -12
View File
@@ -983,28 +983,30 @@ class repoModel extends model
* Get latest commit.
*
* @param int $repoID
* @param bool $checkCount
* @access public
* @return object
*/
public function getLatestCommit($repoID)
public function getLatestCommit($repoID, $checkCount = true)
{
$count = $this->dao->select('count(DISTINCT t1.id) as count')->from(TABLE_REPOHISTORY)->alias('t1')
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
->where('t1.repo')->eq($repoID)
->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
->fetch('count');
$repo = $this->fetchByID($repoID);
$branchID = (string)$this->cookie->repoBranch;
$lastComment = $this->dao->select('t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
->where('t1.repo')->eq($repoID)
->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
->orderBy('t1.time desc')
->limit(1)
->fetch();
if(empty($lastComment)) return null;
if(empty($lastComment)) return new stdclass();
if(!$checkCount) return $lastComment;
$repo = $this->getByID($repoID);
if($repo->SCM == 'Git' and $lastComment->commit != $count)
$count = $this->dao->select('count(DISTINCT t1.id) as count')->from(TABLE_REPOHISTORY)->alias('t1')
->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
->where('t1.repo')->eq($repoID)
->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
->fetch('count');
if($repo->SCM == 'Git' && $lastComment->commit != $count)
{
$this->fixCommit($repo->id);
$lastComment->commit = $count;
@@ -3289,4 +3291,25 @@ class repoModel extends model
return array_merge($folders, $files);
}
/**
* 查询提交记录的版本号。
* Get history revision.
*
* @param int $repoID
* @param string $revision
* @param bool $withCommit
* @param string $condition
* @access public
* @return string|false
*/
public function getHistoryRevision(int $repoID, string $revision, bool $withCommit = false, string $condition = 'eq'): string|false
{
$field = $withCommit ? 'revision, commit' : 'revision';
return $this->dao->select($field)->from(TABLE_REPOHISTORY)
->where('repo')->eq($repoID)
->beginIF($condition != 'lt')->andWhere('revision')->eq($revision)->fi()
->beginIF($condition == 'lt')->andWhere('revision')->lt($revision)->fi()
->fetch($withCommit ? '' : 'revision');
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ $queryMenuLink = createLink('repo', 'maintain', "objectID=$objectID&orderBy=&rec
(
set::current('all'),
set::queryMenuLinkCallback(fn($key) => str_replace('{queryID}', (string)$key, $queryMenuLink)),
li(searchToggle(set::open($type == 'bySearch')))
li(searchToggle(set::module('repo'), set::open($type == 'bySearch')))
);
toolBar
+216
View File
@@ -1210,4 +1210,220 @@ class repoZen extends repo
return $error;
}
/**
* 跳转到版本库的diff页面。
* Redirect to diff page.
*
* @param int $repoID
* @param int $objectID
* @param string $arrange
* @param bool $isBranchOrTag
* @param string $file
* @access protected
* @return void
*/
protected function locateDiffPage(int $repoID, int $objectID, string $arrange, bool $isBranchOrTag, string $file)
{
$oldRevision = isset($this->post->revision[1]) ? $this->post->revision[1] : '';
$newRevision = isset($this->post->revision[0]) ? $this->post->revision[0] : '';
if($this->post->arrange)
{
$arrange = $this->post->arrange;
helper::setcookie('arrange', $arrange);
}
if($this->post->encoding) $encoding = $this->post->encoding;
if($this->post->isBranchOrTag) $isBranchOrTag = $this->post->isBranchOrTag;
return $this->locate($this->repo->createLink('diff', "repoID={$repoID}&objectID={$objectID}&entry={$file}&oldrevision={$oldRevision}&newRevision={$newRevision}&showBug=&encoding={$encoding}&isBranchOrTag={$isBranchOrTag}"));
}
/**
* 设置对比信息的编码格式。
* Set encoding for diff.
*
* @param array $diffs
* @param string $encoding
* @access protected
* @return array
*/
protected function encodingDiff(array $diffs, string $encoding): array
{
foreach($diffs as $diff)
{
$diff->fileName = helper::convertEncoding($diff->fileName, $encoding);
if(empty($diff->contents)) continue;
foreach($diff->contents as $content)
{
if(empty($content->lines)) continue;
foreach($content->lines as $lines)
{
if(empty($lines->line)) continue;
$lines->line = helper::convertEncoding($lines->line, $encoding);
}
}
}
return $diffs;
}
/**
* 获取并列展示的对比信息。
* Get appose diff.
*
* @param array $diffs
* @access protected
* @return array
*/
protected function getApposeDiff(array $diffs): array
{
foreach($diffs as $diffFile)
{
if(empty($diffFile->contents)) continue;
foreach($diffFile->contents as $content)
{
$old = array();
$new = array();
foreach($content->lines as $line)
{
if($line->type != 'new') $old[$line->oldlc] = $line->line;
if($line->type != 'old') $new[$line->newlc] = $line->line;
}
$content->old = $old;
$content->new = $new;
}
}
return $diffs;
}
/**
* 获取代码同步本地的日志。
* Get sync log.
*
* @param object $repo
* @access protected
* @return string
*/
protected function syncLocalCommit(object $repo): string
{
$logFile = realPath($this->app->getTmpRoot() . $this->config->repo->repoSyncLog->logFilePrefix . strtolower($repo->SCM) . ".{$repo->name}.log");
if($logFile)
{
$content = file($logFile);
foreach($content as $line)
{
if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->fatal) !== false) return $line;
if($this->repo->strposAry($line, $this->config->repo->repoSyncLog->failed) !== false) return $line;
}
$lastLine = $content[count($content) - 1];
if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->done) === false)
{
if($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->emptyRepo) !== false)
{
@unlink($logFile);
}
elseif($this->repo->strposAry($lastLine, $this->config->repo->repoSyncLog->total) !== false)
{
$logContent = file_get_contents($logFile);
if($this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCount) !== false and $this->repo->strposAry($logContent, $this->config->repo->repoSyncLog->finishCompress) !== false)
{
@unlink($logFile);
}
else
{
return $this->config->repo->repoSyncLog->one;
}
}
else
{
return $this->config->repo->repoSyncLog->one;
}
}
else
{
@unlink($logFile);
}
}
return '';
}
/**
* 获取需要同步的分支。
* Get sync branches.
*
* @param object $repo
* @param string $branchID
* @access protected
* @return array
*/
protected function getSyncBranches(object $repo, string &$branchID = ''): array
{
$branches = array();
if(in_array($repo->SCM, $this->config->repo->gitTypeList))
{
$branches = $this->scm->branch();
if($branches)
{
/* Init branchID. */
if($this->cookie->syncBranch) $branchID = $this->cookie->syncBranch;
if(!isset($branches[$branchID])) $branchID = '';
if(empty($branchID)) $branchID = 'master';
/* Get unsynced branches. */
unset($branches['master']);
if($branchID != 'master')
{
foreach($branches as $branch)
{
unset($branches[$branch]);
if($branch == $branchID) break;
}
}
$this->repo->setRepoBranch($branchID);
helper::setcookie("syncBranch", $branchID, 0, $this->config->webRoot, '', $this->config->cookieSecure, true);
}
}
return $branches;
}
/**
* 获取同步结果。
* Get sync result.
*
* @param object $repo
* @param array $branches
* @param string $branchID
* @param int $commitCount
* @access protected
* @return string
*/
protected function checkSyncResult(object $repo, array $branches, string $branchID, int $commitCount): string
{
if(empty($commitCount) && !$repo->synced)
{
if(in_array($repo->SCM, $this->config->repo->gitTypeList))
{
if($branchID) $this->repo->saveExistCommits4Branch($repo->id, $branchID);
helper::setcookie("syncBranch", reset($branches));
if($branchID) $this->repo->fixCommit($repo->id);
}
if(empty($branchID))
{
$this->repo->markSynced($repo->id);
return $this->config->repo->repoSyncLog->finish;
}
}
$this->dao->update(TABLE_REPO)->set('commits=commits + ' . $commitCount)->where('id')->eq($repo->id)->exec();
return $type == 'batch' ? $commitCount : $this->config->repo->repoSyncLog->finish;
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ $lang->report->singleColor[] = 'F6BD0F';
$lang->report->projectDeviation = "{$lang->execution->common}偏差报表";
$lang->report->productSummary = $lang->productCommon . '汇总表';
$lang->report->bugCreate = 'Bug创建表';
$lang->report->bugAssign = 'Bug指派表';
$lang->report->bugAssign = '未解决Bug指派表';
$lang->report->workload = '员工负载表';
$lang->report->workloadAB = '工作负载';
$lang->report->bugOpenedDate = 'Bug创建时间';
+1 -1
View File
@@ -39,7 +39,7 @@ $lang->report->singleColor[] = 'F6BD0F';
$lang->report->projectDeviation = "{$lang->execution->common}偏差報表";
$lang->report->productSummary = $lang->productCommon . '彙總表';
$lang->report->bugCreate = 'Bug創建表';
$lang->report->bugAssign = 'Bug指派表';
$lang->report->bugAssign = '未解决Bug指派表';
$lang->report->workload = '員工負載表';
$lang->report->workloadAB = '工作負載';
$lang->report->bugOpenedDate = 'Bug創建時間';
+41 -11
View File
@@ -237,7 +237,7 @@ class screenModel extends model
list($component, $typeChanged) = $this->initComponent($chart, $type, $component);
if(empty($chart) || ($chart->stage == 'draft' || $chart->deleted === '1'))
if(empty($chart) || ($chart->stage == 'draft' || $chart->deleted == '1'))
{
$component->option = new stdclass();
if($type == 'chart')
@@ -357,10 +357,12 @@ class screenModel extends model
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->loadModel('chart')->getMultiData($settings, $chart->sql, $filters);
$fields = json_decode($chart->fields);
$dimensions = array($settings['xaxis'][0]['field']);
$sourceData = array();
$clientLang = $this->app->getClientLang();
$fields = json_decode($chart->fields, true);
$dimensions = array($settings['xaxis'][0]['field']);
$sourceData = array();
$clientLang = $this->app->getClientLang();
$xLabelValues = $this->processXLabel($xLabels, $fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
foreach($yStats as $index => $dataList)
{
$field = zget($fields, $metrics[$index]);
@@ -371,6 +373,7 @@ class screenModel extends model
foreach($dataList as $valueField => $value)
{
$valueField = $xLabelValues[$valueField];
if(empty($sourceData[$valueField]))
{
$sourceData[$valueField] = new stdclass();
@@ -406,10 +409,12 @@ class screenModel extends model
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->loadModel('chart')->getMultiData($settings, $chart->sql, $filters);
$fields = json_decode($chart->fields);
$dimensions = array($settings['xaxis'][0]['field']);
$sourceData = array();
$clientLang = $this->app->getClientLang();
$fields = json_decode($chart->fields, true);
$dimensions = array($settings['xaxis'][0]['field']);
$sourceData = array();
$clientLang = $this->app->getClientLang();
$xLabelValues = $this->processXLabel($xLabels, $fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
foreach($yStats as $index => $dataList)
{
$field = zget($fields, $metrics[$index]);
@@ -420,6 +425,7 @@ class screenModel extends model
foreach($dataList as $valueField => $value)
{
$valueField = $xLabelValues[$valueField];
if(empty($sourceData[$valueField]))
{
$sourceData[$valueField] = new stdclass();
@@ -509,11 +515,13 @@ class screenModel extends model
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->loadModel('chart')->getMultiData($settings, $chart->sql, $filters);
$fields = json_decode($chart->fields);
$fields = json_decode($chart->fields, true);
$radarIndicator = array();
$seriesData = array();
$max = 0;
$clientLang = $this->app->getClientLang();
$xLabelValues = $this->processXLabel($xLabels, $fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
foreach($yStats as $index => $dataList)
{
$field = zget($fields, $metrics[$index]);
@@ -539,7 +547,7 @@ class screenModel extends model
foreach($dataList as $valueField => $value)
{
$indicator = new stdclass();
$indicator->name = $valueField;
$indicator->name = $xLabelValues[$valueField];
$indicator->max = $max;
$radarIndicator[] = $indicator;;
}
@@ -722,6 +730,28 @@ class screenModel extends model
return $screenFilters;
}
/**
* Process xLabel with lang
*
* @param array $xLabel
* @param string $type
* @param string $object
* @param string $field
* @access public
* @return array
*/
public function processXLabel($xLabels, $type, $object, $field)
{
$options = $this->getSysOptions($type, $object, $field);
$xLabelValues = array();
foreach($xLabels as $index => $label)
{
$xLabelValues[$label] = isset($options[$label]) ? $options[$label] : $label;
}
return $xLabelValues;
}
/**
* Get system options.
*
+2 -2
View File
@@ -128,8 +128,8 @@ class space extends control
$defaultApp = '';
foreach($pagedApps->apps as $app)
{
if(in_array($app->id, array(29,51,52,53,54,142))) continue;
if(!$appID and $app->alias == 'GitLab') $defaultApp = $app->id;
if(strpos($app->name, 'zentao') === 0 || strpos($app->name, 'zdoo') === 0 || strpos($app->name, 'xuanxuan') === 0) continue;
if(!$appID && $app->alias == 'GitLab') $defaultApp = $app->id;
$apps[$app->id] = $app->alias;
}
+2 -2
View File
@@ -9,12 +9,12 @@ window.treeClick = function(info)
if (info.item.items && info.item.items.length > 0) return;
if(checkedCategories[info.item.id] != undefined)
{
$('#' + info.item.id).parent().removeClass('selected');
$('.store-tree-' + info.item.id + ' > .listitem').removeClass('active');
delete checkedCategories[info.item.id];
}
else
{
$('#' + info.item.id).parent().addClass('selected');
$('.store-tree-' + info.item.id + ' > .listitem').addClass('active');
checkedCategories[info.item.id] = true;
}
+5 -4
View File
@@ -16,10 +16,11 @@ $tree = array();
foreach($categories as $id => $category)
{
$tree[] = array(
'id' => $id,
'parent' => 0,
'name' => $category,
'key' => 'storeTree-' . $id
'id' => $id,
'parent' => 0,
'name' => $category,
'key' => "storeTree-{$id}",
'className' => "store-tree-{$id}"
);
}
jsVar('tree', $tree);
+5 -5
View File
@@ -270,11 +270,11 @@ $lang->story->sourceList['bug'] = 'Bug';
$lang->story->sourceList['forum'] = 'Forum';
$lang->story->sourceList['other'] = 'Andere';
$lang->story->priList[''] = '';
$lang->story->priList[1] = '1';
$lang->story->priList[2] = '2';
$lang->story->priList[3] = '3';
$lang->story->priList[4] = '4';
$lang->story->priList[0] = '';
$lang->story->priList[1] = '1';
$lang->story->priList[2] = '2';
$lang->story->priList[3] = '3';
$lang->story->priList[4] = '4';
$lang->story->changeList = array();
$lang->story->changeList['no'] = 'Cancel';
+5 -5
View File
@@ -270,11 +270,11 @@ $lang->story->sourceList['bug'] = 'Bug';
$lang->story->sourceList['forum'] = 'Forum';
$lang->story->sourceList['other'] = 'Others';
$lang->story->priList[''] = '';
$lang->story->priList[1] = '1';
$lang->story->priList[2] = '2';
$lang->story->priList[3] = '3';
$lang->story->priList[4] = '4';
$lang->story->priList[0] = '';
$lang->story->priList[1] = '1';
$lang->story->priList[2] = '2';
$lang->story->priList[3] = '3';
$lang->story->priList[4] = '4';
$lang->story->changeList = array();
$lang->story->changeList['no'] = 'Cancel';

Some files were not shown because too many files have changed in this diff Show More