Files
EasySoft-ZenTaoPMS/module/upgrade/model.php
T

13273 lines
517 KiB
PHP

<?php
declare(strict_types=1);
/**
* The model file of upgrade module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
* @package upgrade
* @version $Id: model.php 5019 2013-07-05 02:02:31Z wyd621@gmail.com $
* @link https://www.zentao.net
*/
?>
<?php
class upgradeModel extends model
{
static $errors = array();
/**
* 升级前的版本号。
* The version before upgrade.
*
* @var string
* access public
*/
public $fromVersion = '';
/**
* 升级前的版本类型。
* The edition before upgrade.
*
* @var string
* access public
*/
public $fromEdition = '';
/**
* 已执行的升级内容。
* Executed Changes.
*
* @var array
* access private
*/
private $executedChanges = [];
/**
* 构造函数。
* Construct function.
*
* @access public
* @return void
*/
public function __construct()
{
parent::__construct();
$this->loadModel('setting');
}
/**
* 获取升级的版本。
* Get versions to update.
*
* @param mixed $openVersion
* @param string $fromEdition open|pro|biz|max|ipd
* @access public
* @return array
*/
public function getVersionsToUpdate(string $openVersion, string $fromEdition): array
{
$versions = array();
/* pms20beta2 is a version released between pms18.11 and pms18.12, which was upgraded to pms20 and lost 18.11 upgrade sql. */
if($openVersion == '20_0_beta2') $versions['18_11'] = array('pro' => array(), 'biz' => array(), 'max' => array(), 'ipd' => array());
/* Always update open sql. */
foreach($this->lang->upgrade->fromVersions as $version => $versionName)
{
if(!is_numeric($version[0])) continue;
if(version_compare(str_replace('_', '.', $version), str_replace('_', '.', $openVersion)) < 0) continue;
$versions[$version] = array('pro' => array(), 'biz' => array(), 'max' => array(), 'ipd' => array());
}
if($fromEdition == 'open') return $versions;
/* Update pro sql from pro|biz|max|ipd. */
foreach($this->config->upgrade->proVersion as $pro => $open)
{
if(isset($versions[$open])) $versions[$open]['pro'][] = $pro;
}
if($fromEdition == 'pro') return $versions;
/* Update biz sql from biz|max|ipd. */
foreach($this->config->upgrade->bizVersion as $biz => $open)
{
if(isset($versions[$open])) $versions[$open]['biz'][] = $biz;
}
if($fromEdition == 'biz') return $versions;
/* Update max sql from max|ipd. */
foreach($this->config->upgrade->maxVersion as $max => $open)
{
if(isset($versions[$open])) $versions[$open]['max'][] = $max;
}
if($fromEdition == 'max') return $versions;
/* Update ipd sql only from ipd. */
foreach($this->config->upgrade->ipdVersion as $ipd => $open)
{
if(isset($versions[$open])) $versions[$open]['ipd'][] = $ipd;
}
return $versions;
}
/**
* 获取额外执行的方法。
* Get other methods to execute.
*
* @param string $fromEdition
* @access public
* @return array
*/
public function getOtherMethods(string $fromEdition): array
{
$methods = [];
/* Means open source/pro upgrade to biz or max. */
if($this->config->edition != 'open' && ($fromEdition == 'open' || $fromEdition == 'pro'))
{
$methods['importBuildinModules'] = [];
$methods['importLiteModules'] = [];
$methods['addSubStatus'] = [];
$methods['processDataset'] = [];
$methods['upgradeScreenAndMetricData'] = [];
$methods['upgradeBIData'] = [];
}
$methods['convertCharset'] = [];
$methods['program-refreshStats'] = [true];
$methods['product-refreshStats'] = [true];
$methods['deletePatch'] = [];
return $methods;
}
/**
* 记录已升级的 SQL 和方法。
* Record executed SQLs and methods.
*
* @access public
* @return bool
*/
public function recordExecutedChanges(): bool
{
$this->loadModel('setting')->setItem('system.upgrade.executedChanges', json_encode($this->executedChanges));
return !dao::isError();
}
/**
* 执行升级 sql 文件。
* The execute method. According to the $fromVersion call related methods.
*
* @param string $fromVersion
* @param string $toVersion
* @access public
* @return bool
*/
public function execute(string $fromVersion, string $toVersion): bool
{
set_time_limit(0);
if(!isset($this->app->user)) $this->loadModel('user')->su();
$this->dao->exec("SET @@sql_mode=''");
/* Set real time log file. */
dao::$realTimeFile = $this->getLogFile();
if(file_exists(dao::$realTimeFile)) unlink(dao::$realTimeFile);
if(strpos($fromVersion, 'lite') !== false) $fromVersion = $this->config->upgrade->liteVersion[$fromVersion];
if(empty($this->config->global->hideUpgradeGuide))
{
if(strpos($fromVersion, 'ipd') !== false)
{
if(version_compare($fromVersion, 'ipd2.0.0', '>=')) $this->loadModel('setting')->setItem("system.common.global.hideUpgradeGuide", 1);
}
elseif(strpos($fromVersion, 'max') !== false)
{
if(version_compare($fromVersion, 'max5.0.0', '>=')) $this->loadModel('setting')->setItem("system.common.global.hideUpgradeGuide", 1);
}
elseif(strpos($fromVersion, 'pro') !== false || strpos($fromVersion, 'biz') !== false)
{
if(version_compare($fromVersion, 'biz10.0.0', '>=')) $this->loadModel('setting')->setItem("system.common.global.hideUpgradeGuide", 1);
}
else
{
if(version_compare($fromVersion, '20.0.beta1', '>=')) $this->loadModel('setting')->setItem("system.common.global.hideUpgradeGuide", 1);
}
}
$this->fromVersion = $fromVersion;
$this->fromEdition = $this->getEditionByVersion($fromVersion);
$this->executedChanges = empty($this->config->upgrade->executedChanges) ? [] : json_decode($this->config->upgrade->executedChanges, true);
/* Execute. */
$fromOpenVersion = $this->getOpenVersion(str_replace('.', '_', $this->config->installedVersion));
$toOpenVersion = $this->getOpenVersion($toVersion);
$upgradeVersions = $this->getVersionsToUpdate($fromOpenVersion, $this->fromEdition);
/* Execute charge edition. */
foreach($upgradeVersions as $openVersion => $chargedVersions)
{
if(version_compare($openVersion, $fromOpenVersion, '<')) continue;
if(version_compare($openVersion, $toOpenVersion, '>=')) continue;
/* Execute open edition. */
$this->saveLogs("Execute $openVersion");
$result = $this->execSQL($this->getUpgradeFile(str_replace('_', '.', $openVersion)), $openVersion);
if(!$result) return $this->recordExecutedChanges();
$result = $this->executeByConfig($openVersion);
if(!$result) return $this->recordExecutedChanges();
foreach($chargedVersions as $chargedVersion)
{
foreach($chargedVersion as $version)
{
$this->saveLogs("Execute $version");
$result = $this->execSQL($this->getUpgradeFile(str_replace('_', '.', $version)),$version);
if(!$result) return $this->recordExecutedChanges();
$result = $this->executeByConfig($version);
if(!$result) return $this->recordExecutedChanges();
}
}
}
/* 如果此次升级到最终版本,则执行额外的数据处理流程。*/
if(version_compare($toVersion, $this->config->version, '='))
{
$result = $this->executeOthers($this->fromEdition);
if(!$result) return $this->recordExecutedChanges();
}
else
{
/**
* 升级到最终版本之前,每升级完一个版本,更新一次版本号,防止中途失败重复升级。
* 升级到最终版本之后,则在执行完所有后续的数据处理流程后再更新版本号。
*/
$this->loadModel('setting')->updateVersion($toVersion);
}
return !$this->isError() && !dao::isError();
}
/**
* 执行导入模块、刷新项目集和产品信息并且删除补丁。
* Import buildin modules, and refresh stats of programs and products, delete path.
*
* @param string $fromEdition
* @access public
* @return bool
*/
public function executeOthers(string $fromEdition): bool
{
$version = str_replace('.', '_', $this->config->installedVersion);
$methods = $this->getOtherMethods($fromEdition);
foreach($methods as $method => $params)
{
$result = $this->executeUpgradeMethod($version, $method, $params);
if(!$result) return false;
}
return true;
}
/**
* 通过配置执行升级方法。
* Execute upgrade methods by config.
*
* @param string $version
* @access public
* @return bool
*/
public function executeByConfig(string $version): bool
{
$execConfig = zget($this->config->upgrade->execFlow, $version, array());
$functions = zget($execConfig, 'functions', '');
$params = zget($execConfig, 'params', array());
$xxsqls = zget($execConfig, 'xxsqls', '');
$xxfunctions = zget($execConfig, 'xxfunctions', '');
foreach(array_filter(explode(',', $functions)) as $function)
{
$result = $this->executeUpgradeMethod($version, $function, zget($params, $function, array()));
if(!$result) return false;
}
if($version == 'pro1_1_1')
{
$result = $this->execSQL($this->getUpgradeFile('pro1.1'), $version);
if(!$result) return false;
}
if($version == 'pro8_3')
{
$result = $this->execSQL($this->getUpgradeFile('pro8.2'), $version);
if(!$result) return false;
}
if(!empty($xxsqls))
{
foreach(array_filter(explode(',', $xxsqls)) as $sqlFile)
{
$result = $this->execSQL($sqlFile, $version);
if(!$result) return false;
}
}
if(!empty($xxfunctions))
{
foreach(array_filter(explode(',', $xxfunctions)) as $function)
{
$result = $this->executeUpgradeMethod($version, $function, zget($params, $function, array()));
if(!$result) return false;
}
}
return true;
}
/**
* 执行单个升级方法。
* Execute single upgrade method.
*
* @param string $version
* @param string $rawMethod
* @param array $params
* @access public
* @return bool
*/
public function executeUpgradeMethod(string $version, string $rawMethod, array $params = []): bool
{
if(isset($this->executedChanges[$version]['methods'][$rawMethod])) return true;
$this->saveLogs("Run Method {$rawMethod}");
$model = $this;
$method = $rawMethod;
if(str_contains($rawMethod, '-'))
{
list($module, $method) = explode('-', $rawMethod);
$model = $this->loadModel($module);
}
dao::$realTimeLog = true;
call_user_func_array(array($model, $method), $params);
dao::$realTimeLog = false;
if($this->isError()) return false;
if(dao::isError())
{
static::$errors = array_merge(static::$errors, dao::getError());
return false;
}
$this->executedChanges[$version]['methods'][$rawMethod] = true;
$this->recordExecutedChanges();
return true;
}
/**
* 获取确认 SQL 内容。
* Get the confirm contents.
*
* @param string $fromVersion
* @access public
* @return string
*/
public function getConfirm(string $fromVersion): string
{
$confirmContent = '';
$fromEdition = $this->getEditionByVersion($fromVersion);
$fromOpenVersion = $this->getOpenVersion($fromVersion);
$versions = $this->getVersionsToUpdate($fromOpenVersion, $fromEdition);
foreach($versions as $openVersion => $chargedVersions)
{
/* Get open contents. */
$sqlFile = $this->getUpgradeFile(str_replace('_', '.', $openVersion));
if(file_exists($sqlFile)) $confirmContent .= file_get_contents($sqlFile);
/* Get xuanxuan contents. */
if(isset($this->config->upgrade->execFlow[$openVersion]['xxsqls']))
{
foreach(array_filter(explode(',', $this->config->upgrade->execFlow[$openVersion]['xxsqls'])) as $sqlFile)
{
if(file_exists($sqlFile)) $confirmContent .= file_get_contents($sqlFile);
}
}
/* Get charge contents. */
foreach($chargedVersions as $chargedVersion)
{
foreach($chargedVersion as $version)
{
$sqlFile = $this->getUpgradeFile(str_replace('_', '.', $version));
if(file_exists($sqlFile)) $confirmContent .= file_get_contents($sqlFile);
/* Get xuanxuan contents. */
if(!isset($this->config->upgrade->execFlow[$version]['xxsqls'])) continue;
foreach(array_filter(explode(',', $this->config->upgrade->execFlow[$version]['xxsqls'])) as $sqlFile)
{
if(file_exists($sqlFile)) $confirmContent .= file_get_contents($sqlFile);
}
}
}
}
return str_replace('zt_', $this->config->db->prefix, $confirmContent);
}
/**
* 通过版本号获取产品版本类型。
* Get edition by version.
*
* @param string $version
* @access public
* @return string
*/
public function getEditionByVersion(string $version): string
{
$editions = array('p' => 'pro', 'b' => 'biz', 'm' => 'max', 'i' => 'ipd');
return is_numeric($version[0]) ? 'open' : $editions[$version[0]];
}
/**
* 获取开源版版本。
* Get open version.
*
* @param string $version.
* @access public
* @return string
*/
public function getOpenVersion(string $version): string
{
$edition = $this->getEditionByVersion($version);
return is_numeric($version[0]) ? $version : zget($this->config->upgrade->{$edition . 'Version'}, $version);
}
/**
* Update company field.
*
* This method is used to update since 1.0 beta. Any new tables added after 1.0 beta should skip.
*
* @access public
* @return void
*/
public function updateCompany()
{
/* Get user defined constants. */
$constants = get_defined_constants(true);
$userConstants = $constants['user'];
/* Update tables. */
foreach($userConstants as $key => $value)
{
if(strpos($key, 'TABLE') === false) continue;
if($key == 'TABLE_COMPANY') continue;
$table = $value;
$result = $this->dbh->query("SHOW TABLES LIKE '$table'");
if($result->rowCount() > 0)
{
$this->dbh->exec("UPDATE $table SET company = '{$this->app->company->id}'");
}
}
}
/**
* Check table consistency.
*
* @param string $sql
* @access private
* @return array
*/
private function checkTableConsistency($sql)
{
$changes = array();
$lines = explode("\n", $sql);
$createHead = array_shift($lines);
preg_match_all('/CREATE TABLE [^`]*`([^`]*)`/', $createHead, $out);
if(!isset($out[1][0])) return $changes;
$table = str_replace('zt_', $this->config->db->prefix, $out[1][0]);
if($table == $this->config->db->prefix . 'metriclib') return $changes; // 度量库表数据量过大,检查到表结构不一致执行升级会导致长时间卡死,跳过检查。Skip checking metriclib table because it has too much data and checking it will cause long time stuck.
$fields = array();
try
{
$dbCreateSQL = $this->dbh->query("SHOW CREATE TABLE `$table`")->fetch(PDO::FETCH_ASSOC);
$dbSQLLines = explode("\n", $dbCreateSQL['Create Table']);
foreach($dbSQLLines as $dbSQLLine)
{
$dbSQLLine = trim($dbSQLLine);
if(!preg_match('/^`([^`]*)` /', $dbSQLLine)) continue; // Skip no describe field line.
$dbSQLLine = rtrim($dbSQLLine, ',');
$dbSQLLine = str_replace('utf8 COLLATE utf8_general_ci', 'utf8', $dbSQLLine);
$dbSQLLine = preg_replace('/ DEFAULT (\-?\d+\.?\d*)$/i', " DEFAULT '$1'", $dbSQLLine);
list($field) = explode(' ', $dbSQLLine);
$fields[$field] = rtrim($dbSQLLine, ',');
}
}
catch(PDOException $e)
{
$message = $e->getMessage();
if($message && strpos($message, '1146') !== false) return array($sql);
}
if(empty($fields)) return $changes;
foreach($lines as $line)
{
$change = $this->checkFieldConsistency($table, $line, $fields);
if($change) $changes[] = $change;
}
return $changes;
}
/**
* Check field consistency.
*
* @param string $table
* @param string $line
* @param string $fields
* @access private
* @return array
*/
private function checkFieldConsistency($table, $line, $fields)
{
$line = trim($line);
if(!preg_match('/^`([^`]*)` /', $line)) return ''; // Skip no describe field line.
$line = rtrim($line, ',');
$line = str_replace('utf8 COLLATE utf8_general_ci', 'utf8', $line);
$line = preg_replace('/ DEFAULT (\-?\d+\.?\d*)$/i', " DEFAULT '$1'", $line);
list($field) = explode(' ', $line);
if(isset($fields[$field]) and strpos($fields[$field], ' NULL') === false and strpos($line, ' DEFAULT NULL') !== false) $line = str_replace(' DEFAULT NULL', '', $line); // e.g. standard sql like [ `content` text DEFAULT NULL ] , but current db sql like [ `content` text ].
/* Dont change if matched. */
if(isset($fields[$field]) && $line == $fields[$field]) return '';
/* Add field. */
if(!isset($fields[$field]))
{
$execSQL = "ALTER TABLE `$table` ADD $line";
if(stripos($execSQL, 'auto_increment') !== false && stripos($execSQL, ' PRIMARY KEY ') === false) $execSQL .= ' PRIMARY KEY';
}
else
{
/* Modify field. */
$execSQL = $this->checkFieldSQL($table, $line, $fields[$field]);
}
if(stripos($execSQL, 'auto_increment') !== false) $execSQL .= ' FIRST';
return $execSQL;
}
/**
* Check field SQL.
*
* @param string $table
* @param string $stdField
* @param string $dbField
* @access private
* @return string
*/
private function checkFieldSQL($table, $stdField, $dbField)
{
$needModify = false;
if($this->checkFieldTypeDiff($stdField, $dbField)) $needModify = true;
if($this->checkFieldNullDiff($stdField, $dbField)) $needModify = true;
if($this->checkFieldDefaultDiff($stdField, $dbField)) $needModify = true;
$fieldName = explode(' ', $dbField)[0];
return $needModify ? "ALTER TABLE `$table` CHANGE $fieldName $stdField" : '';
}
/**
* Check field null diff.
*
* @param string $stdField
* @param string $dbField
* @access private
* @return bool
*/
private function checkFieldNullDiff($stdField, $dbField)
{
$dbNotNull = stripos($dbField, 'NOT NULL') !== false;
$stdNotNull = stripos($stdField, 'NOT NULL') !== false;
if($dbNotNull && !$stdNotNull) return true;
}
/**
* Check field default diff.
*
* @param string $stdField
* @param string $dbField
* @access private
* @return bool
*/
private function checkFieldDefaultDiff($stdField, $dbField)
{
$defaultPos = stripos($stdField, ' DEFAULT ');
if($defaultPos === false) return false; // No default in std, don't change.
$stdDefault = str_replace("'", '', substr($stdField, $defaultPos + 9));
$commentPos = stripos($stdDefault, ' COMMENT ');
if($commentPos !== false) $stdDefault = substr($stdDefault, 0, $commentPos);
if($stdDefault == 'NULL') return false; // Default is NULL, don't change.
if(strpos($stdField, 'text') !== false && empty($stdDefault)) return false; // Default is '' and text type, don't change.
$defaultPos = stripos($dbField, ' DEFAULT ');
if($defaultPos === false) return true; // No default in db, change it.
$dbDefault = str_replace("'", '', substr($dbField, $defaultPos + 9));
$commentPos = stripos($dbDefault, ' COMMENT ');
if($commentPos !== false) $dbDefault = substr($dbDefault, 0, $commentPos);
return $stdDefault != $dbDefault;
}
/**
* Check field type diff .
*
* @param string $stdField
* @param string $dbField
* @access private
* @return bool
*/
private function checkFieldTypeDiff($stdField, $dbField)
{
$stdConfigs = preg_split('/\s+/', $stdField);
$dbConfigs = preg_split('/\s+/', $dbField);
$stdType = $stdConfigs[1];
$dbType = $dbConfigs[1];
if($stdType == $dbType) return false;
$stdLength = 0;
$dbLength = 0;
preg_match_all('/^(\w+)(\((\d+)\))?$/', $stdConfigs[1], $stdOutput);
if(!empty($stdOutput[1][0])) $stdType = $stdOutput[1][0];
if(!empty($stdOutput[3][0])) $stdLength = $stdOutput[3][0];
preg_match_all('/^(\w+)(\((\d+)\))?$/', $dbConfigs[1], $dbOutput);
if(!empty($dbOutput[1][0])) $dbType = $dbOutput[1][0];
if(!empty($dbOutput[3][0])) $dbLength = $dbOutput[3][0];
$stdIsVarchar = stripos($stdType, 'varchar') !== false;
$stdIsText = stripos($stdType, 'text') !== false;
$stdIsFloat = preg_match('/float|decimal|double/i', $stdType);
$dbIsInt = stripos($dbType, 'int') !== false;
$dbIsVarchar = stripos($dbType, 'varchar') !== false;
if($dbIsInt)
{
if($stdIsFloat || $stdIsText || $stdIsVarchar) return true;
if($dbLength != 0 && $stdLength > $dbLength) return true; // MySQL 8.0 int has no length.
}
elseif($dbIsVarchar)
{
if($stdIsText) return true;
if($dbLength && $stdLength > $dbLength) return true;
}
return false;
}
/**
* 检查一致性。
* Check consistency.
*
* @param string $version
* @access public
* @return string
*/
public function checkConsistency(string $version = ''): string
{
$alterSQL = array();
$sqls = $this->getStandardSQLs($version);
foreach(explode(';', $sqls) as $sql)
{
$sql = trim($sql);
if(strpos($sql, 'CREATE TABLE ') !== 0) continue;
$changes = $this->checkTableConsistency($sql);
if(!empty($changes)) $alterSQL = array_merge($alterSQL, $changes);
}
return implode(";\n", $alterSQL);
}
/**
* 为一致性问题执行修复sql。
* Exec fix sql for consistency.
*
* @param string $version
* @access public
* @return void
*/
public function fixConsistency(string $version): void
{
$logFile = $this->getConsistencyLogFile();
$lockFile = $logFile . '.lock';
if(file_exists($lockFile) && (time() - filemtime($lockFile)) < 60) return;
if(file_exists($logFile)) unlink($logFile);
touch($lockFile);
$hasError = false;
$fixSqls = $this->checkConsistency($version);
if($fixSqls) $fixSqls = "SET @@sql_mode= '';\n" . $fixSqls;
/* 记录执行的sql并且记录执行结果。 */
/* Record the executed sql and the result. */
$sqlLines = explode(';', $fixSqls);
file_put_contents($logFile, count($sqlLines) . "\n");
foreach($sqlLines as $fixSQL)
{
file_put_contents($logFile, $fixSQL, FILE_APPEND);
try
{
$this->dbh->exec($fixSQL);
}
catch(PDOException $e)
{
$hasError = true;
file_put_contents($logFile, $e->getMessage() . "\n", FILE_APPEND);
}
}
$finishTag = "\nFinished";
if($hasError) $finishTag = "\nHasError" . $finishTag;
file_put_contents($logFile, $finishTag, FILE_APPEND);
unlink($lockFile);
}
/**
* 删除临时 model 文件。
* Delete tmp model files.
*
* @access public
* @return void
*/
public function deleteTmpModel(): void
{
$zfile = $this->app->loadClass('zfile');
$tmpModelDir = $this->app->getTmpRoot() . 'model/';
foreach(glob($tmpModelDir . '/*') as $tmpModelFile)
{
if(is_file($tmpModelFile)) unlink($tmpModelFile);
if(is_dir($tmpModelFile)) $zfile->removeDir($tmpModelFile);
}
}
/**
* 删除无用的文件。
* Delete Useless Files.
*
* @param string $script
* @access public
* @return string
*/
public function deleteFiles(string $script): string
{
$dir = dirname($script);
if(!is_writable($dir)) return "chmod 777 {$dir}";
$command = array();
$zfile = $this->app->loadClass('zfile');
foreach($this->config->delete as $deleteFiles)
{
$basePath = $this->app->getBasePath();
foreach($deleteFiles as $file)
{
if(isset($this->config->excludeFiles[$file])) continue;
$fullPath = $basePath . str_replace('/', DS, $file);
if(file_exists($fullPath))
{
$isDir = is_dir($fullPath);
/* Get the command to delete files that could not be deleted. */
if(!is_writable($fullPath) || ($isDir && !$zfile->removeDir($fullPath)) ||
(!$isDir && !$zfile->removeFile($fullPath)))
{
$command[] = 'rm -fr ' . $fullPath;
}
}
}
}
/* Delete all patch files when upgrade zentao. */
$patchPath = $this->app->getTmpRoot() . 'patch';
$isDir = is_dir($patchPath);
if(file_exists($patchPath))
{
/* Get the command to delete files that could not be deleted. */
if(!is_writable($patchPath) || ($isDir && !$zfile->removeDir($patchPath)) ||
(!$isDir && !$zfile->removeDir($patchPath)))
{
$command[] = 'rm -fr ' . $patchPath;
}
}
if(!$command) return '';
asort($command);
$content = "#!/bin/bash\n" . implode("\n", $command);
file_put_contents($script, $content);
return "/bin/bash $script";
}
/**
* Update nl to br from 1.2 version.
*
* @access public
* @return void
*/
public function updateNL1_2()
{
$tasks = $this->dao->select('id, `desc`')->from(TABLE_TASK)->fetchAll();
$stories = $this->dao->select('story, version, spec')->from($this->config->db->prefix . 'storySpec')->fetchAll();
$todos = $this->dao->select('id, `desc`')->from(TABLE_TODO)->fetchAll();
$testTasks = $this->dao->select('id, `desc`')->from($this->config->db->prefix . 'testTask')->fetchAll();
foreach($tasks as $task)
{
$task->desc = nl2br($task->desc);
$this->dao->update(TABLE_TASK)->data($task)->where('id')->eq($task->id)->exec();
}
foreach($stories as $story)
{
$story->spec = nl2br($story->spec);
$this->dao->update($this->config->db->prefix . 'storySpec')->data($story)->where('story')->eq($story->story)->andWhere('version')->eq($story->version)->exec();
}
foreach($todos as $todo)
{
$todo->desc = nl2br($todo->desc);
$this->dao->update(TABLE_TODO)->data($todo)->where('id')->eq($todo->id)->exec();
}
foreach($testTasks as $testtask)
{
$testtask->desc = nl2br($testtask->desc);
$this->dao->update($this->config->db->prefix . 'testTask')->data($testtask)->where('id')->eq($testtask->id)->exec();
}
}
/**
* Update nl to br from 1.3 version.
*
* @access public
* @return void
*/
public function updateNL1_3()
{
$products = $this->dao->select('id, `desc`')->from(TABLE_PRODUCT)->fetchAll();
$plans = $this->dao->select('id, `desc`')->from($this->config->db->prefix . 'productPlan')->fetchAll();
$releases = $this->dao->select('id, `desc`')->from(TABLE_RELEASE)->fetchAll();
$projects = $this->dao->select('id, `desc`, goal')->from(TABLE_PROJECT)->fetchAll();
$builds = $this->dao->select('id, `desc`')->from(TABLE_BUILD)->fetchAll();
$account = isset($this->app->user->account) ? $this->app->user->account : '';
foreach($products as $product)
{
$product->desc = nl2br($product->desc);
$this->dao->update(TABLE_PRODUCT)->data($product)->where('id')->eq($product->id)->exec();
}
foreach($plans as $plan)
{
$plan->desc = nl2br($plan->desc);
$this->dao->update($this->config->db->prefix . 'productPlan')->data($plan)->where('id')->eq($plan->id)->exec();
}
foreach($releases as $release)
{
$release->desc = nl2br($release->desc);
$this->dao->update(TABLE_RELEASE)->data($release)->where('id')->eq($release->id)->exec();
}
foreach($projects as $project)
{
$project->desc = nl2br($project->desc);
$project->goal = nl2br($project->goal);
$project->lastEditedBy = $account;
$project->lastEditedDate = helper::now();
$this->dao->update(TABLE_PROJECT)->data($project)->where('id')->eq($project->id)->exec();
}
foreach($builds as $build)
{
$build->desc = nl2br($build->desc);
$this->dao->update(TABLE_BUILD)->data($build)->where('id')->eq($build->id)->exec();
}
}
/**
* Update task fields.
*
* @access public
* @return void
*/
public function updateTasks()
{
/* Get all actions of tasks. */
$actions = $this->dao->select('*')->from(TABLE_ACTION)
->where('objectType')->eq('task')
->orderBy('id')
->fetchAll('id', false);
/* Get histories about status field. */
$histories = $this->dao->select('*')->from(TABLE_HISTORY)
->where('action')->in(array_keys($actions))
->andWhere('field')->eq('status')
->orderBy('id')
->fetchGroup('action');
$tasks = array();
foreach($actions as $action)
{
if(!isset($tasks[$action->objectID])) $tasks[$action->objectID] = new stdclass;
$task = $tasks[$action->objectID];
$task->id = $action->objectID;
$actionType = strtolower($action->action);
/* Set the openedBy info. */
if($actionType == 'opened')
{
$task->openedBy = $action->actor;
$task->openedDate = $action->date;
}
else
{
if(!isset($histories[$action->id])) continue;
$actionHistories = $histories[$action->id];
foreach($actionHistories as $history)
{
/* Finished by. */
if($history->new == 'done')
{
$task->finishedBy = $action->actor;
$task->finishedDate = $action->date;
$action->action = 'finished';
}
/* Canceled By. */
elseif($history->new == 'cancel')
{
$task->canceledBy = $action->actor;
$task->canceledDate = $action->date;
$action->action = 'canceled';
}
}
/* Last edited by .*/
$task->lastEditedBy = $action->actor;
$task->lastEditedDate = $action->date;
/* Update action type. */
$this->dao->update(TABLE_ACTION)->set('action')->eq($action->action)->where('id')->eq($action->id)->exec();
}
}
/* Update db. */
foreach($tasks as $task) $this->dao->update(TABLE_TASK)->data($task)->where('id')->eq($task->id)->exec();
$this->dao->update(TABLE_TASK)->set('assignedTo=openedBy, assignedDate = finishedDate')->where('status')->eq('done')->exec();
$this->dao->update(TABLE_TASK)->set('assignedTo=openedBy, assignedDate = canceledDate')->where('status')->eq('cancel')->exec();
}
/**
* Update activated count of Bug.
*
* @access public
* @return void
*/
public function updateActivatedCountOfBug()
{
$bugActivatedActions = $this->dao->select('*')->from(TABLE_ACTION)->where('action')->eq('activated')->andWhere('objectType')->eq('bug')->fetchAll('id', false);
if(!empty($bugActivatedActions))
{
foreach($bugActivatedActions as $action)
{
if(!isset($counts[$action->objectID])) $counts[$action->objectID] = 0;
$counts[$action->objectID] ++;
}
foreach($counts as $key => $count) $this->dao->update(TABLE_BUG)->set('activatedCount')->eq($count)->where('id')->eq($key)->exec();
}
}
/**
* Update lastRun and lastResult field in zt_case
*
* @access public
* @return void
*/
public function updateCases()
{
$results = $this->dao->select('`case`, date, caseResult')->from($this->config->db->prefix . 'testResult')->orderBy('id desc')->fetchGroup('case');
foreach($results as $result)
{
$this->dao->update(TABLE_CASE)
->set('lastRun')->eq($result[0]->date)
->set('lastResult')->eq($result[0]->caseResult)
->where('id')->eq($result[0]->case)
->exec();
}
}
/**
* Update type of projects.
*
* @access public
* @return void
*/
public function updateProjectType()
{
$projects = $this->dao->select('root')->from(TABLE_MODULE)->where('type')->eq('task')->fetchPairs('root');
$this->dao->update(TABLE_PROJECT)->set('type')->eq('waterfall')->where('id')->in($projects)->exec();
return true;
}
/**
* Update estimate priv.
*
* @access public
* @return void
*/
public function updateEstimatePriv()
{
$methods = ['recordWorkhour', 'editEffort', 'deleteWorkhour'];
$privTable = $this->config->db->prefix . 'groupPriv';
$groups = $this->dao->select('*')->from($privTable)
->where('module')->eq('task')
->andWhere('method')->eq('edit')
->fetchAll();
$this->dao->begin();
foreach($groups as $group)
{
$this->dao->delete()->from($privTable)
->where('`group`')->eq($group->group)
->andWhere('module')->eq('task')
->andWhere('method')->in($methods)
->exec();
if(dao::isError())
{
$this->dao->rollback();
return false;
}
foreach($methods as $method)
{
$this->dao->insert($privTable)
->set('company')->eq($group->company)
->set('`group`')->eq($group->group)
->set('module')->eq('task')
->set('method')->eq($method)
->exec();
if(dao::isError())
{
$this->dao->rollback();
return false;
}
}
}
return $this->dao->commit();
}
/**
* Update the data of action.
*
* @access public
* @return void
*/
public function updateAction()
{
/* Get projects and tasks from action table. */
$projects = $this->dao->select('id')->from(TABLE_PROJECT)->fetchPairs('id');
$tasks = $this->dao->select('id, project')->from(TABLE_TASK)->fetchPairs('id');
/* Get products of projects and tasks. */
$projectProducts = $this->dao->select('project,product')->from($this->config->db->prefix . 'projectProduct')->where('project')->in(array_keys($projects))->fetchGroup('project', 'product');
$taskProducts = $this->dao->select('t1.id, t2.product')->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
->where('t1.id')->in(array_keys($tasks))
->fetchPairs('id');
/* Process project actions. */
foreach($projects as $projectID)
{
$productList = isset($projectProducts[$projectID]) ? implode(',', array_keys($projectProducts[$projectID])) : '';
$this->dao->update(TABLE_ACTION)->set('product')->eq($productList)->where('objectType')->eq('project')->andWhere('objectID')->eq($projectID)->exec();
}
/* Process task actions. */
foreach($tasks as $taskID => $projectID)
{
$productList = '';
if($taskProducts[$taskID])
{
$productList = $taskProducts[$taskID];
}
else
{
$productList = isset($projectProducts[$projectID]) ? implode(',', array_keys($projectProducts[$projectID])) : '';
}
$this->dao->update(TABLE_ACTION)->set('product')->eq($productList)->where('objectType')->eq('task')->andWhere('objectID')->eq($taskID)->andWhere('project')->eq($projectID)->exec();
}
$this->dao->update(TABLE_ACTION)->set("product = concat(',',product,',')")->exec();
return true;
}
/**
* Init the data of product and project order field.
*
* @access public
* @return void
*/
public function setOrderData()
{
$products = $this->dao->select('*')->from(TABLE_PRODUCT)->where('deleted')->eq(0)->orderBy('code')->fetchAll('id');
foreach(array_keys($products) as $key => $productID)
{
$this->dao->update(TABLE_PRODUCT)->set('`order`')->eq(($key + 1) * 10)->where('id')->eq($productID)->exec();
}
$projects = $this->dao->select('*')->from(TABLE_PROJECT)->where('iscat')->eq(0)->andWhere('deleted')->eq(0)->orderBy('status, id desc')->fetchAll('id');
foreach(array_keys($projects) as $key => $projectID)
{
$this->dao->update(TABLE_PROJECT)->set('`order`')->eq(($key + 1) * 10)->where('id')->eq($projectID)->exec();
}
return true;
}
/**
* Update task assignedTo.
*
* @access public
* @return void
*/
public function updateTaskAssignedTo()
{
$this->dao->update(TABLE_TASK)->set('assignedTo')->eq('closed')
->where('status')->eq('closed')
->andWhere('assignedTo')->eq('')
->exec();
return true;
}
/**
* 删除补丁记录。
* Delete the patch record.
*
* @access public
* @return bool
*/
public function deletePatch(): bool
{
$this->dao->delete()->from(TABLE_EXTENSION)->where('type')->eq('patch')->exec();
$this->dao->delete()->from(TABLE_EXTENSION)->where('code')->in('zentaopatch,patch')->exec();
return !dao::isError();
}
/**
* 获取升级 sql 文件路径。
* Get the upgrade sql file.
*
* @param string $version
* @access public
* @return string
*/
public function getUpgradeFile(string $version): string
{
return $this->app->getAppRoot() . 'db' . DS . 'update' . $version . '.sql';
}
/**
* 获取项目集下的项目。
* Get the project of the program it belongs to.
*
* @param string $programID
* @access public
* @return array
*/
public function getProjectPairsByProgram(int $programID = 0): array
{
return $this->dao->select('id,name')->from(TABLE_PROJECT)->where('deleted')->eq(0)->andWhere('type')->eq('project')->andWhere('parent')->eq($programID)->fetchPairs();
}
/**
* 执行一个 sql 文件。
* Execute a sql.
*
* @param string $sqlFile
* @param string $version
* @access public
* @return bool
*/
public function execSQL(string $sqlFile, string $version = ''): bool
{
$this->saveLogs('Run Method ' . __FUNCTION__);
$ignoreCode = '|1050|1054|1060|1091|1061|';
$sqls = $this->parseToSqls($sqlFile); // Get sqls in the file.
foreach($sqls as $sql)
{
if(empty($sql)) continue;
$sqlMd5 = md5($sql);
$fileMd5 = md5($sqlFile);
if(isset($this->executedChanges[$version]['sqls'][$fileMd5][$sqlMd5])) continue; // Skip the sql that has been executed.
try
{
$this->saveLogs($sql);
$this->dbh->exec($sql);
$this->executedChanges[$version]['sqls'][$fileMd5][$sqlMd5] = true; // Mark the sql has been executed.
$this->recordExecutedChanges();
}
catch(PDOException $e)
{
$errorInfo = $e->errorInfo;
$errorCode = !empty($errorInfo) ? $errorInfo[1] : '';
if(strpos($ignoreCode, "|$errorCode|") !== false)
{
$this->executedChanges[$version]['sqls'][$fileMd5][$sqlMd5] = true; // Mark the sql has been executed.
$this->recordExecutedChanges();
}
else
{
static::$errors[] = $e->getMessage();
return false;
}
}
}
return true;
}
/**
* 将 sql 文件解析为 sql 语句。
* Parse sql file to sqls.
*
* @param string $sqlFile
* @access public
* @return array
*/
public function parseToSqls(string $sqlFile): array
{
if(!file_exists($sqlFile)) return [];
/* Read the sql file to lines, remove the comment lines, then join theme by ';'. */
$sqls = array();
$lines = explode("\n", file_get_contents($sqlFile));
foreach($lines as $line)
{
$line = trim($line);
/* Skip sql that is note and empty sql. */
if(!$line || preg_match('/^--|^#|^\/\*/', $line)) continue;
$sqls[] = $line;
}
$this->loadModel('install');
$sqls = array_filter(explode(';', implode("\n", $sqls)));
foreach($sqls as $key => $sql)
{
$sql = trim($sql); // Trim \n and space.
$sql = $this->install->replaceContantsInSQL($sql);
$sql = $this->install->appendMySQLTableOptions($sql);
$sqls[$key] = $sql;
}
return $sqls;
}
/**
* Add priv for version 4.0.1
*
* @access public
* @return void
*/
public function addPriv4_0_1()
{
$privTable = $this->config->db->prefix . 'groupPriv';
$oldPriv = $this->dao->select('*')->from($privTable)
->where('module')->eq('company')
->andWhere('method')->eq('edit')
->fetchAll();
foreach($oldPriv as $item)
{
$this->dao->replace($privTable)
->set('company')->eq($item->company)
->set('module')->eq('company')
->set('method')->eq('view')
->set('`group`')->eq($item->group)
->exec();
}
$oldPriv = $this->dao->select('*')->from($privTable)
->where('module')->eq('todo')
->andWhere('method')->eq('finish')
->fetchAll();
foreach($oldPriv as $item)
{
$this->dao->replace($privTable)
->set('company')->eq($item->company)
->set('module')->eq('todo')
->set('method')->eq('batchFinish')
->set('`group`')->eq($item->group)
->exec();
}
return true;
}
/**
* Add priv for version 4.1
*
* @access public
* @return bool
*/
public function addPriv4_1()
{
$privTable = $this->config->db->prefix . 'groupPriv';
$oldPriv = $this->dao->select('*')->from($privTable)
->where('module')->eq('tree')
->andWhere('method')->eq('browse')
->fetchAll();
foreach($oldPriv as $item)
{
$this->dao->replace($privTable)
->set('company')->eq($item->company)
->set('module')->eq('tree')
->set('method')->eq('browseTask')
->set('`group`')->eq($item->group)
->exec();
}
return true;
}
/**
* Add priv for version 8.1
*
* @access public
* @return bool
*/
public function addPriv8_1()
{
$methods = [
'bug' => ['linkBugs', 'unlinkBug'],
'story' => ['linkStory', 'unlinkStory'],
'testcase' => ['linkCases', 'unlinkCase']
];
$privTable = $this->config->db->prefix . 'grouppriv';
$oldPrivs = $this->dao->select('*')->from($privTable)
->where('module')->in(array_keys($methods))
->andWhere('method')->eq('edit')
->fetchGroup('module');
$this->dao->begin();
foreach($oldPrivs as $module => $privs)
{
foreach($privs as $item)
{
foreach($methods[$module] as $method)
{
$this->dao->replace($privTable)
->set('module')->eq($module)
->set('method')->eq($method)
->set('`group`')->eq($item->group)
->exec();
if(dao::isError())
{
$this->dao->rollback();
return false;
}
}
}
}
return $this->dao->commit();
}
/**
* Add priv for version 12.3.3
*
* @access public
* @return bool
*/
public function addPriv12_3_3()
{
$privTable = $this->config->db->prefix . 'grouppriv';
$oldPriv = $this->dao->select('*')->from($privTable)
->where('module')->eq('todo')
->andWhere('method')->eq('edit')
->fetchAll();
foreach($oldPriv as $item)
{
$this->dao->replace($privTable)
->set('module')->eq('todo')
->set('method')->eq('start')
->set('`group`')->eq($item->group)
->exec();
}
return true;
}
/**
* Add priv for 8.2.
*
* @access public
* @return bool
*/
public function addPriv8_2_beta()
{
$privTable = $this->config->db->prefix . 'grouppriv';
/* Change product-all priv. */
$groups = $this->dao->select('`group`')->from($privTable)->where('`module`')->eq('product')->andWhere('`method`')->eq('index')->fetchPairs('group', 'group');
foreach($groups as $group)
{
$this->dao->replace($privTable)->set('module')->eq('product')->set('method')->eq('all')->set('`group`')->eq($group)->exec();
}
/* Change project-all priv. */
$groups = $this->dao->select('`group`')->from($privTable)->where('`module`')->eq('project')->andWhere('`method`')->eq('index')->fetchPairs('group', 'group');
foreach($groups as $group)
{
$this->dao->replace($privTable)->set('module')->eq('project')->set('method')->eq('all')->set('`group`')->eq($group)->exec();
}
/* Add kanban and tree priv. */
$groups = $this->dao->select('`group`')->from($privTable)->where('`module`')->eq('project')->andWhere('`method`')->eq('task')->fetchPairs('group', 'group');
foreach($groups as $group)
{
$this->dao->replace($privTable)->set('module')->eq('project')->set('method')->eq('kanban')->set('`group`')->eq($group)->exec();
$this->dao->replace($privTable)->set('module')->eq('project')->set('method')->eq('tree')->set('`group`')->eq($group)->exec();
}
/* Change manageContacts priv. */
$groups = $this->dao->select('`group`')->from($privTable)->where('`module`')->eq('user')->andWhere('`method`')->eq('manageContacts')->fetchPairs('group', 'group');
foreach($groups as $group)
{
$this->dao->replace($privTable)->set('module')->eq('my')->set('method')->eq('manageContacts')->set('`group`')->eq($group)->exec();
}
/* Change deleteContacts priv. */
$groups = $this->dao->select('`group`')->from($privTable)->where('`module`')->eq('user')->andWhere('`method`')->eq('deleteContacts')->fetchPairs('group', 'group');
foreach($groups as $group)
{
$this->dao->replace($privTable)->set('module')->eq('my')->set('method')->eq('deleteContacts')->set('`group`')->eq($group)->exec();
}
/* Change batchChangeModule priv. */
$oldPriv = $this->dao->select('*')->from($privTable)
->where("(`module`='story' and `method`='edit')")
->orWhere("(`module`='task' and `method`='edit')")
->orWhere("(`module`='bug' and `method`='edit')")
->orWhere("(`module`='testcase' and `method`='edit')")
->fetchAll();
foreach($oldPriv as $item)
{
$this->dao->replace($privTable)->set('module')->eq($item->module)->set('method')->eq('batchChangeModule')->set('`group`')->eq($item->group)->exec();
}
return true;
}
/**
* 为系统管理员添加 or 页面的权限。
* Add or view priv for adminer.
*
* @param string $openVersion
* @access public
* @return bool
*/
public function addORPriv($openVersion = ''): bool
{
/* Get admin users. */
$admins = $this->dao->select('admins')->from(TABLE_COMPANY)->where('deleted')->eq(0)->fetchPairs();
$admins = explode(',', implode(',', $admins));
$admins = array_unique($admins);
$users = $this->dao->select('account,visions')->from(TABLE_USER)->where('account')->in($admins)->fetchPairs();
/* Add or to visions. */
foreach($users as $account => $visions)
{
if(strpos($visions, 'or') === false)
{
$visions = 'or,' . $visions;
$this->dao->update(TABLE_USER)->set('visions')->eq($visions)->where('account')->eq($account)->exec();
}
}
include('priv.php');
/* Add or groups. */
$orGroup = $this->dao->select('name')->from(TABLE_GROUP)->where('vision')->eq('or')->fetchAll();
if(empty($orGroup))
{
$group = new stdclass();
$group->vision = 'or';
foreach($orData as $role => $name)
{
$group->name = $name;
$group->role = $role;
$group->desc = $name;
$this->dao->insert(TABLE_GROUP)->data($group)->exec();
if(dao::isError()) continue;
$groupID = (string)$this->dao->lastInsertID();
$sql = 'REPLACE INTO' . TABLE_GROUPPRIV . '(`group`, `module`, `method`) VALUES ' . str_replace('GROUPID', $groupID, ${$role . 'Priv'});
$this->dao->exec($sql);
}
}
return true;
}
/**
* Adjust config section and key.
*
* @access public
* @return bool
*/
public function adjustConfigSectionAndKey()
{
$this->dao->update(TABLE_CONFIG)->set('`key`')->eq('productProject')->where('`key`')->eq('productproject')->andWhere('module')->eq('custom')->exec();
$this->dao->update(TABLE_CONFIG)->set('section')->eq('bugBrowse')->where('section')->eq('bugbrowse')->andWhere('module')->eq('datatable')->exec();
$this->dao->update(TABLE_CONFIG)->set('section')->eq('productBrowse')->where('section')->eq('productbrowse')->andWhere('module')->eq('datatable')->exec();
$this->dao->update(TABLE_CONFIG)->set('section')->eq('projectTask')->where('section')->eq('projecttask')->andWhere('module')->eq('datatable')->exec();
$this->dao->update(TABLE_CONFIG)->set('section')->eq('testcaseBrowse')->where('section')->eq('testcasebrowse')->andWhere('module')->eq('datatable')->exec();
$this->dao->update(TABLE_CONFIG)->set('section')->eq('testtaskCases')->where('section')->eq('testtaskcases')->andWhere('module')->eq('datatable')->exec();
return true;
}
/**
* To lower table.
*
* @param string $build
* @access public
* @return bool
*/
public function toLowerTable($build = 'basic')
{
$results = $this->dao->query("show Variables like '%table_names'")->fetchAll();
$hasLowered = false;
foreach($results as $result)
{
if(strtolower($result->Variable_name) == 'lower_case_table_names' and $result->Value == 1)
{
$hasLowered = true;
break;
}
}
if($hasLowered) return true;
if($build == 'basic') $tables2Rename = $this->config->upgrade->lowerTables;
if($build == 'pro') $tables2Rename = $this->config->upgrade->lowerProTables;
if(!isset($tables2Rename)) return false;
$tablesExists = $this->dao->query('SHOW TABLES')->fetchAll();
foreach($tablesExists as $key => $table) $tablesExists[$key] = current((array)$table);
$tablesExists = array_flip($tablesExists);
foreach($tables2Rename as $oldTable => $newTable)
{
if(!isset($tablesExists[$oldTable])) continue;
$upgradebak = $newTable . '_othertablebak';
if(isset($tablesExists[$upgradebak]))
{
$this->dao->query("DROP TABLE `$upgradebak`");
}
if(isset($tablesExists[$newTable]))
{
$this->dao->query("RENAME TABLE `$newTable` TO `$upgradebak`");
}
$tempTable = $oldTable . '_zentaotmp';
$this->dao->query("RENAME TABLE `$oldTable` TO `$tempTable`");
$this->dao->query("RENAME TABLE `$tempTable` TO `$newTable`");
}
return true;
}
/**
* Process finishedBy and finishedDate of task.
*
* @access public
* @return bool
*/
public function processTaskFinish()
{
$this->dao->update(TABLE_TASK)
->set('finishedBy = lastEditedBy')
->set('finishedDate = lastEditedDate')
->where('status')->in('done,closed')
->andWhere('finishedBy')->eq('')
->exec();
return true;
}
/**
* Process bugs which import to project tasks but canceled.
*
* @access public
* @return void
*/
public function processImport2TaskBugs()
{
$bugs = $this->dao->select('t1.id')->from(TABLE_BUG)->alias('t1')
->leftJoin(TABLE_TASK)->alias('t2')->on('t1.toTask = t2.id')
->where('t1.toTask')->ne(0)
->andWhere('t1.status')->eq('active')
->andWhere('t2.canceledBy')->ne('')
->fetchPairs();
$this->dao->update(TABLE_BUG)->set('toTask')->eq(0)->where('id')->in($bugs)->exec();
return true;
}
/**
* Delete company field for the table of zt_config and zt_groupPriv.
*
* @access public
* @return void
*/
public function deleteCompany()
{
$privTable = $this->config->db->prefix . 'groupPriv';
/* Delete priv that is not in this company. Prevent conflict when delete company's field.*/
$this->dao->delete()->from($privTable)->where('company')->ne($this->app->company->id)->exec();
$this->dao->exec("ALTER TABLE " . $privTable . " DROP `company`;");
/* Delete config that don't conform to the rules. Prevent conflict when delete company's field.*/
$rows = $this->dao->select('*')->from(TABLE_CONFIG)->orderBy('id desc')->fetchAll('id', false);
$items = array();
$delList = array();
foreach($rows as $config)
{
if(isset($items[$config->owner][$config->module][$config->section][$config->key]))
{
$delList[] = $config->id;
continue;
}
$items[$config->owner][$config->module][$config->section][$config->key] = $config->id;
}
if($delList) $this->dao->delete()->from(TABLE_CONFIG)->where('id')->in($delList)->exec();
$this->dao->exec("ALTER TABLE " . TABLE_CONFIG . " DROP `company`;");
return true;
}
/**
* Merge the goal and desc of project.
*
* @access public
* @return void
*/
public function mergeProjectGoalAndDesc()
{
$projects = $this->dao->select('*')->from(TABLE_PROJECT)->fetchAll('id', false);
foreach($projects as $id => $project)
{
if(!isset($project->goal)) continue;
$this->dao->update(TABLE_PROJECT)
->set('`desc`')->eq($project->desc . '<br />' . $project->goal)
->where('id')->eq($id)
->exec();
}
return true;
}
/**
* Fix OS info of bugs.
*
* @access public
* @return void
*/
public function fixBugOSInfo()
{
$this->dao->update(TABLE_BUG)->set('os')->eq('android')->where('os')->eq('andriod')->exec();
$this->dao->update(TABLE_BUG)->set('os')->eq('osx')->where('os')->eq('mac')->exec();
}
/**
* Fix finishedBy of task.
*
* @access public
* @return void
*/
public function fixTaskFinishedBy()
{
$tasks = $this->dao->select('t1.id,t2.actor,t2.date')->from(TABLE_TASK)->alias('t1')
->leftJoin(TABLE_ACTION)->alias('t2')
->on('t1.id = t2.objectID')
->leftJoin(TABLE_HISTORY)->alias('t3')
->on('t2.id = t3.action')
->where('t3.new')->eq(0)
->andWhere('t3.field')->eq('left')
->andWhere('t2.objectType')->eq('task')
->andWhere('t1.finishedBy')->eq('')
->andWhere('t1.status')->in('done,closed')
->andWhere('t1.deleted')->eq(0)
->fetchAll('id');
foreach($tasks as $taskID => $task)
{
$this->dao->update(TABLE_TASK)
->set('finishedBy')->eq($task->actor)
->set('finishedDate')->eq($task->date)
->where('id')->eq($taskID)
->exec();
}
}
/**
* Touch index.html for upload when has not it.
*
* @access public
* @return bool
*/
public function fixDataIndex()
{
$savePath = $this->loadModel('file')->savePath;
foreach(glob($savePath . '*') as $childDir)
{
if(is_dir($childDir) and !is_file($childDir . '/index.html')) @touch($childDir . '/index.html');
}
return true;
}
/**
* Init order.
*
* @access public
* @return bool
*/
public function initOrder()
{
$dataList = $this->dao->select('id')->from(TABLE_PRODUCT)->orderBy('code_desc')->fetchAll();
$i = 1;
foreach($dataList as $data)
{
$this->dao->update(TABLE_PRODUCT)->set('`order`')->eq($i++)->where('id')->eq($data->id)->exec();
}
$dataList = $this->dao->select('id')->from(TABLE_PROJECT)->orderBy('code_desc')->fetchAll();
$i = 1;
foreach($dataList as $data)
{
$this->dao->update(TABLE_PROJECT)->set('`order`')->eq($i++)->where('id')->eq($data->id)->exec();
}
return true;
}
/**
* Adjust order for 7.3
*
* @access public
* @return void
*/
public function adjustOrder7_3()
{
$this->loadModel('product')->fixOrder();
$this->loadModel('execution')->fixOrder();
return true;
}
/**
* Adjust priv for 7.4.beta
*
* @access public
* @return void
*/
public function adjustPriv7_4_beta()
{
$groups = $this->dao->select('id')->from(TABLE_GROUP)->where('name')->ne('guest')->fetchPairs('id', 'id');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'my';
$groupPriv->method = 'unbind';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Adjust doc module.
*
* @access public
* @return bool
*/
public function adjustDocModule()
{
$this->app->loadLang('doc');
$productDocModules = $this->dao->select('*')->from(TABLE_MODULE)->where('type')->eq('productdoc')->orderBy('grade,id')->fetchAll('id');
$allProductIdList = $this->dao->select('id,name,acl,whitelist,createdBy')->from(TABLE_PRODUCT)->where('deleted')->eq('0')->fetchAll('id');
foreach($allProductIdList as $productID => $product)
{
$this->dao->delete()->from(TABLE_DOCLIB)->where('product')->eq($productID)->exec();
$lib = new stdclass();
$lib->product = $productID;
$lib->name = $this->lang->doclib->main['product'];
$lib->main = 1;
$lib->acl = $product->acl == 'open' ? 'open' : 'custom';
$lib->users = $product->createdBy;
if($product->acl == 'custom') $lib->groups = $product->whitelist;
$this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
$libID = $this->dao->lastInsertID();
$relation = array();
foreach($productDocModules as $moduleID => $module)
{
unset($module->id);
$module->root = $libID;
$module->type = 'doc';
$this->dao->insert(TABLE_MODULE)->data($module)->exec();
$newModuleID = $this->dao->lastInsertID();
$relation[$moduleID] = $newModuleID;
$newPaths = array();
foreach(explode(',', trim($module->path, ',')) as $path)
{
if(isset($relation[$path])) $newPaths[] = $relation[$path];
}
$newPaths = implode(',', $newPaths);
$this->dao->update(TABLE_MODULE)->set('path')->eq($newPaths)->set('parent')->eq($relation[$module->parent])->where('id')->eq($newModuleID)->exec();
$this->dao->update(TABLE_DOC)->set('module')->eq($newModuleID)->where('product')->eq($productID)->andWhere('module')->eq($moduleID)->andWhere('lib')->eq('product')->exec();
}
$this->dao->update(TABLE_DOC)->set('lib')->eq($libID)->where('product')->eq($productID)->exec();
}
$this->dao->delete()->from(TABLE_MODULE)->where('id')->in(array_keys($productDocModules))->exec();
$projectDocModules = $this->dao->select('*')->from(TABLE_MODULE)->where('type')->eq('projectdoc')->orderBy('grade,id')->fetchAll('id');
$allProjectIdList = $this->dao->select('id,name,acl,whitelist')->from(TABLE_PROJECT)->where('deleted')->eq('0')->fetchAll('id');
foreach($allProjectIdList as $projectID => $project)
{
$this->dao->delete()->from(TABLE_DOCLIB)->where('project')->eq($projectID)->exec();
$lib = new stdclass();
$lib->project = $projectID;
$lib->name = $this->lang->doclib->main['project'];
$lib->main = 1;
$lib->acl = $project->acl == 'open' ? 'open' : 'custom';
$teams = $this->dao->select('project, account')->from(TABLE_TEAM)->where('project')->eq($projectID)->fetchPairs('account', 'account');
$lib->users = implode(',', $teams);
if($project->acl == 'custom') $lib->groups = $project->whitelist;
$this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
$libID = $this->dao->lastInsertID();
$docLibs = $this->dao->select('id,users')->from(TABLE_DOCLIB)->alias('t1')
->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.product=t2.product')
->where('t2.project')->eq($projectID)
->andWhere('t1.acl')->eq('custom')
->fetchAll('id');
foreach($docLibs as $lib)
{
$docUsers = $teams + explode(',', $lib->users);
$docUsers = array_unique($docUsers);
$this->dao->update(TABLE_DOCLIB)->set('users')->eq(implode(',', $docUsers))->where('id')->eq($lib->id)->exec();
}
$relation = array();
foreach($projectDocModules as $moduleID => $module)
{
unset($module->id);
$module->root = $libID;
$module->type = 'doc';
$this->dao->insert(TABLE_MODULE)->data($module)->exec();
$newModuleID = $this->dao->lastInsertID();
$relation[$moduleID] = $newModuleID;
$newPaths = array();
foreach(explode(',', trim($module->path, ',')) as $path)
{
if(isset($relation[$path])) $newPaths[] = $relation[$path];
}
$newPaths = implode(',', $newPaths);
$newPaths = ",$newPaths,";
$this->dao->update(TABLE_MODULE)->set('path')->eq($newPaths)->where('id')->eq($newModuleID)->exec();
$this->dao->update(TABLE_DOC)->set('module')->eq($newModuleID)->where('project')->eq($projectID)->andWhere('module')->eq($moduleID)->exec();
}
$this->dao->update(TABLE_DOC)->set('lib')->eq($libID)->where('project')->eq($projectID)->exec();
}
$this->dao->delete()->from(TABLE_MODULE)->where('id')->in(array_keys($projectDocModules))->exec();
return true;
}
/**
* 更新在编辑器中的文件 objectID。
* Update file objectID in editor.
*
* @param string $type
* @param int $lastID
* @access public
* @return array
*/
public function updateFileObjectID(string $type = '', int $lastID = 0): array
{
$limit = 100;
if(empty($type)) $type = 'comment';
if($type == 'comment')
{
$actions = $this->dao->select('id,objectType,objectID,comment')->from(TABLE_ACTION)->where('comment')->like('%data/upload/%')->andWhere('id')->gt($lastID)->orderBy('id')->limit($limit)->fetchAll('id');
foreach($actions as $action)
{
$files = array();
preg_match_all('/"data\/upload\/.*1\/([0-9]{6}\/[^"]+)"/', $action->comment, $output);
foreach($output[1] as $path) $files[$path] = $path;
$this->dao->update(TABLE_FILE)->set('objectType')->eq($action->objectType)->set('objectID')->eq($action->objectID)->set('extra')->eq('editor')->where('pathname')->in($files)->exec();
}
$result['type'] = 'comment';
$result['count'] = count($actions);
$result['lastID'] = count($actions) < $limit ? 0 : $action->id;
return $result;
}
list($objectCount, $lastID) = $this->upgradeTao->updateFileObjects($type, $lastID, $limit);
if($objectCount < $limit)
{
$editorKeys = array_keys($this->config->upgrade->editors);
foreach($editorKeys as $i => $objectType)
{
if($type == $objectType)
{
$nextType = isset($editorKeys[$i + 1]) ? $editorKeys[$i + 1] : '';
break;
}
}
$result['type'] = empty($nextType) ? 'finish' : $nextType;
$result['count'] = $objectCount;
$result['lastID'] = 0;
}
else
{
$result['type'] = $type;
$result['count'] = $objectCount;
$result['lastID'] = $lastID;
}
return $result;
}
/**
* Move doc content to table zt_doccontent.
*
* @access public
* @return bool
*/
public function moveDocContent()
{
$fieldDesc = $this->dao->descTable(TABLE_DOC);
$fieldPairs = array_column($fieldDesc, 'field', 'field');
if(!isset($fieldPairs['content']) || !isset($fieldPairs['digest']) || !isset($fieldPairs['url'])) return true;
$this->dao->exec('TRUNCATE TABLE ' . TABLE_DOCCONTENT);
$stmt = $this->dao->select('id,title,digest,content,url')->from(TABLE_DOC)->query();
$fileGroups = $this->dao->select('id,objectID')->from(TABLE_FILE)->where('objectType')->eq('doc')->fetchGroup('objectID', 'id');
while($doc = $stmt->fetch())
{
$url = empty($doc->url) ? '' : urldecode($doc->url);
$docContent = new stdclass();
$docContent->doc = $doc->id;
$docContent->title = $doc->title;
$docContent->digest = $doc->digest;
$docContent->content = $doc->content;
$docContent->content .= empty($url) ? '' : $url;
$docContent->version = 1;
$docContent->type = 'html';
if(isset($fileGroups[$doc->id])) $docContent->files = implode(',', array_keys($fileGroups[$doc->id]));
$this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
}
$this->dao->exec('ALTER TABLE ' . TABLE_DOC . ' DROP `digest`');
$this->dao->exec('ALTER TABLE ' . TABLE_DOC . ' DROP `content`');
$this->dao->exec('ALTER TABLE ' . TABLE_DOC . ' DROP `url`');
return true;
}
/**
* Adjust priv 8.3
*
* @access public
* @return bool
*/
public function adjustPriv8_3()
{
$docPrivGroups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('doc')->andWhere('method')->eq('index')->fetchPairs('group', 'group');
foreach($docPrivGroups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'doc';
$data->method = 'allLibs';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
$data->method = 'showFiles';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
$data->method = 'objectLibs';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Rename main lib.
*
* @access public
* @return bool
*/
public function renameMainLib()
{
$this->app->loadLang('doc');
$this->dao->update(TABLE_DOCLIB)->set('name')->eq($this->lang->doclib->main['product'])->where('product')->gt(0)->andWhere('main')->eq(1)->exec();
$this->dao->update(TABLE_DOCLIB)->set('name')->eq($this->lang->doclib->main['project'])->where('project')->gt(0)->andWhere('main')->eq(1)->exec();
return true;
}
/**
* Adjust priv for 8.4.
*
* @access public
* @return bool
*/
public function adjustPriv8_4()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('branch')->andWhere('method')->eq('manage')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'branch';
$data->method = 'sort';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('story')->andWhere('method')->eq('tasks')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'story';
$data->method = 'bugs';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
$data->method = 'cases';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Adjust priv for 9.0
*
* @access public
* @return void
*/
public function adjustPriv9_0()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('testtask')->andWhere('method')->eq('results')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'testcase';
$data->method = 'bugs';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('mail')->andWhere('method')->eq('delete')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'mail';
$data->method = 'resend';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Fix projectproduct data.
*
* @access public
* @return bool
*/
public function fixProjectProductData()
{
$this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('product')->eq(0)->exec();
return true;
}
/**
* Add bug deadline for custom fields.
*
* @access public
* @return bool
*/
public function addBugDeadlineToCustomFields()
{
$createFieldsItems = $this->dao->select('id, value')->from(TABLE_CONFIG)
->where('module')->eq('bug')
->andWhere('section')->eq('custom')
->andWhere('`key`')->eq('createFields')
->fetchAll();
$batchEditFieldsItems = $this->dao->select('id, value')->from(TABLE_CONFIG)
->where('module')->eq('bug')
->andWhere('section')->eq('custom')
->andWhere('`key`')->eq('batchEditFields')
->fetchAll();
foreach($createFieldsItems as $createFieldsItem)
{
$value = empty($createFieldsItem->value) ? 'deadline' : $createFieldsItem->value . ",deadline";
$this->dao->update(TABLE_CONFIG)->set('value')->eq($value)->where('id')->eq($createFieldsItem->id)->exec();
}
foreach($batchEditFieldsItems as $batchEditFieldsItem)
{
$value = empty($batchEditFieldsItem->value) ? 'deadline' : $batchEditFieldsItem->value . ",deadline";
$this->dao->update(TABLE_CONFIG)->set('value')->eq($value)->where('id')->eq($batchEditFieldsItem->id)->exec();
}
return true;
}
/**
* Adjust priv for 9.0.1.
*
* @access public
* @return bool
*/
public function adjustPriv9_0_1()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('testcase')->andWhere('method')->eq('edit')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'testcase';
$newMethods = array('review', 'batchReview', 'batchChangeType', 'batchConfirmStoryChange');
foreach($newMethods as $method)
{
$data->method = $method;
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
$data->module = 'testsuite';
$newMethods = array('create', 'edit', 'delete', 'linkCase', 'unlinkCase', 'batchUnlinkCases');
foreach($newMethods as $method)
{
$data->method = $method;
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
}
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('testtask')->andWhere('method')->eq('start')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'testtask';
$newMethods = array('activate', 'block', 'report');
foreach($newMethods as $method)
{
$data->method = $method;
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
}
$groups = $this->dao->select('distinct `group`')->from(TABLE_GROUPPRIV)->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'testsuite';
$newMethods = array('index', 'browse', 'view');
foreach($newMethods as $method)
{
$data->method = $method;
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
}
return true;
}
/**
* Adjust priv for 9.2.
*
* @access public
* @return void
*/
public function adjustPriv9_2()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('testsuite')->andWhere('method')->eq('createCase')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'testsuite';
$newMethods = array('batchCreateCase', 'exportTemplate', 'import', 'showImport');
foreach($newMethods as $method)
{
$data->method = $method;
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
}
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('product')->andWhere('method')->eq('index')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'product';
$data->method = 'build';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('custom')->andWhere('method')->eq('flow')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'custom';
$data->method = 'working';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Adjust priv for 9.4.
*
* @access public
* @return bool
*/
public function adjustPriv9_4()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('bug')->andWhere('method')->eq('activate')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'bug';
$data->method = 'batchActivate';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Adjust priv for 11.4.
*
* @access public
* @return bool
*/
public function addPriv11_4()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('story')->andWhere('method')->eq('edit')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'story';
$data->method = 'assignTo';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Add Priv for 11.5
*
* @access public
* @return bool
*/
public function addPriv11_5()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('bug')->andWhere('method')->eq('setPublic')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'user';
$data->method = 'setPublicTemplate';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Add unique key for stage.
*
* @access public
* @return bool
*/
public function addUniqueKey4Stage()
{
$stmt = $this->dao->select('story,branch')->from(TABLE_STORYSTAGE)->orderBy('story,branch')->query();
$preStage = '';
while($stage = $stmt->fetch())
{
if($preStage == "{$stage->story}_{$stage->branch}") $this->dao->delete()->from(TABLE_STORYSTAGE)->where('story')->eq($stage->story)->andWhere('branch')->eq($stage->branch)->exec();
$preStage = "{$stage->story}_{$stage->branch}";
}
$this->dao->exec("ALTER TABLE " . TABLE_STORYSTAGE . " ADD UNIQUE `story_branch` (`story`, `branch`)");
return true;
}
/**
* 判断是否出现错误。
* Judge any error occurs.
*
* @access public
* @return bool
*/
public function isError(): bool
{
return !empty(static::$errors);
}
/**
* 获取升级时的错误。
* Get errors during the upgrading.
*
* @access public
* @return array
*/
public function getError(): array
{
$errors = static::$errors;
static::$errors = array();
return $errors;
}
/**
* 获取一致性检查的日志文件。
* Get upgrade log file.
*
* @access public
* @return string
*/
public function getConsistencyLogFile(): string
{
return $this->app->getTmpRoot() . 'log/consistency.' . date('Ymd') . '.log.php';
}
/**
* 获取升级日志文件。
* Get upgrade log file.
*
* @access public
* @return string
*/
public function getLogFile(): string
{
return $this->app->getTmpRoot() . 'log/upgrade.' . date('Ymd') . '.log.php';
}
/**
* 检查流程。
* Check weither process or not.
*
* @access public
* @return array
*/
public function checkProcess(): array
{
$fromVersion = $this->config->installedVersion;
$needProcess = array();
if(strpos($fromVersion, 'ipd') !== false) return $needProcess;
if(strpos($fromVersion, 'max') === false && strpos($fromVersion, 'biz') === false && (strpos($fromVersion, 'pro') === false ? version_compare($fromVersion, '8.3', '<') : version_compare($fromVersion, 'pro5.4', '<'))) $needProcess['updateFile'] = 'process';
if(strpos($fromVersion, 'max') === false && $this->config->systemMode == 'new')
{
if(strpos($fromVersion, 'pro') !== false)
{
if(version_compare($fromVersion, 'pro10.0', '<')) $needProcess['search'] = 'notice';
}
elseif(strpos($fromVersion, 'biz') !== false)
{
if(version_compare($fromVersion, 'biz5.0', '<')) $needProcess['search'] = 'notice';
}
elseif(version_compare($fromVersion, '15.0.rc1', '<'))
{
$needProcess['search'] = 'notice';
}
}
$openVersion = $this->getOpenVersion(str_replace('.', '_', $fromVersion));
if(version_compare($openVersion, '17_4', '<=')) $needProcess['changeEngine'] = 'notice';
return $needProcess;
}
/**
* Process customMenus for different working.
*
* @access public
* @return void
*/
public function processCustomMenus()
{
$this->loadModel('setting')->setItem('system.common.global.flow', 'full');
$customMenus = $this->dao->select('*')->from(TABLE_CONFIG)->where('section')->eq('customMenu')->fetchAll();
foreach($customMenus as $customMenu)
{
$this->dao->update(TABLE_CONFIG)->set('`key`')->eq("full_{$customMenu->key}")->where('id')->eq($customMenu->id)->exec();
}
return !dao::isError();
}
/**
* Init project story order.
*
* @access public
* @return bool
*/
public function initProjectStoryOrder()
{
$storyGroup = $this->dao->select('t1.*')->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id')
->orderBy('t2.pri_desc,t1.story_asc')
->fetchGroup('project', 'story');
foreach($storyGroup as $projectID => $stories)
{
$order = 1;
foreach($stories as $storyID => $projectStory)
{
$this->dao->update(TABLE_PROJECTSTORY)->set('`order`')->eq($order)->where('project')->eq($projectID)->andWhere('story')->eq($storyID)->exec();
$order++;
}
}
return true;
}
/**
* Fix datatable cols config.
*
* @access public
* @return bool
*/
public function fixDatatableColsConfig()
{
$config = $this->dao->select('*')->from(TABLE_CONFIG)
->where('module')->eq('datatable')
->andWhere('section')->eq('projectTask')
->andWhere('`key`')->eq('cols')
->fetchAll('id', false);
foreach($config as $datatableCols)
{
$cols = json_decode($datatableCols->value);
foreach($cols as $i => $col)
{
if($col->id == 'progess') $col->id = 'progress';
if($col->id == 'actions' and $col->width == 'auto') $col->width = '180px';
}
$this->dao->update(TABLE_CONFIG)->set('value')->eq(json_encode($cols))->where('id')->eq($datatableCols->id)->exec();
}
return true;
}
/**
* Add limited group.
*
* @access public
* @return bool
*/
public function addLimitedGroup()
{
$limitedGroup = $this->dao->select('*')->from(TABLE_GROUP)->where('`role`')->eq('limited')->fetch();
if(empty($limitedGroup))
{
$group = new stdclass();
$group->name = 'limited';
$group->role = 'limited';
$group->desc = 'For limited user';
$this->dao->insert(TABLE_GROUP)->data($group)->exec();
$groupID = $this->dao->lastInsertID();
}
else
{
$groupID = $limitedGroup->id;
}
$limitedGroups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('my')
->andWhere('method')->eq('limited')
->fetchPairs('group', 'group');
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('module')->eq('my')->andWhere('method')->eq('limited')->exec();
$limitedUsers = $this->dao->select('account')->from(TABLE_USERGROUP)->where('`group`')->in($limitedGroups)->fetchPairs('account', 'account');
foreach($limitedUsers as $limitedUser)
{
$this->dao->replace(TABLE_USERGROUP)->set('account')->eq($limitedUser)->set('`group`')->eq($groupID)->exec();
}
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'my';
$groupPriv->method = 'limited';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
return true;
}
/**
* Change limited name.
*
* @access public
* @return bool
*/
public function changeLimitedName()
{
$this->app->loadLang('install');
$this->dao->update(TABLE_GROUP)->set('name')->eq($this->lang->install->groupList['LIMITED']['name'])
->set('desc')->eq($this->lang->install->groupList['LIMITED']['desc'])
->where('role')->eq('limited')
->exec();
return true;
}
/**
* Adjust Priv for 9.7
*
* @access public
* @return bool
*/
public function adjustPriv9_7()
{
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('method')->eq('edit')->andWhere('module')->in('story,task,bug,testcase')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'action';
$groupPriv->method = 'comment';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Change story field width.
*
* @access public
* @return bool
*/
public function changeStoryWidth()
{
$projectCustom = $this->dao->select('*')->from(TABLE_CONFIG)->where('section')->eq('projectTask')->andWhere('`key`')->in('cols,tablecols')->fetchAll('id', false);
foreach($projectCustom as $configID => $projectTask)
{
$fields = json_decode($projectTask->value);
foreach($fields as $i => $field)
{
if($field->id == 'story') $field->width = '40px';
}
$this->dao->update(TABLE_CONFIG)->set('value')->eq(json_encode($fields))->where('id')->eq($configID)->exec();
}
return true;
}
/**
* Change team field for 9.8.
*
* @access public
* @return bool
*/
public function changeTeamFields()
{
$fields = $this->dao->descTable(TABLE_TEAM);
foreach($fields as $field)
{
if($field->field == 'root') return true;
}
$this->dao->exec("ALTER TABLE " . TABLE_TEAM . " CHANGE `project` `root` MEDIUMINT(8) UNSIGNED NOT NULL DEFAULT '0'");
$this->dao->exec("ALTER TABLE " . TABLE_TEAM . " ADD `type` ENUM('project', 'task') NOT NULL DEFAULT 'project' AFTER `root`");
$this->dao->exec("UPDATE " . TABLE_TEAM . " SET `root` = `task`, `type` = 'task' WHERE `task` > '0'");
$this->dao->exec("ALTER TABLE " . TABLE_TEAM . " DROP PRIMARY KEY");
$this->dao->exec("ALTER TABLE " . TABLE_TEAM . " DROP `task`");
return true;
}
/**
* Move data to notify.
*
* @access public
* @return bool
*/
public function moveData2Notify()
{
$mailQueueTable = '`' . $this->config->db->prefix . 'mailqueue`';
$syncBeginDate = date('Y-m-d', time() - 15 * 24 * 3600);
$stmt = $this->dao->select('*')->from($mailQueueTable)->where('addedDate')->ge($syncBeginDate)->orderBy('id')->query();
while($mailQueue = $stmt->fetch())
{
$notify = new stdclass();
$notify->objectType = 'mail';
$notify->toList = $mailQueue->toList;
$notify->ccList = $mailQueue->ccList;
$notify->subject = $mailQueue->subject;
$notify->data = $mailQueue->body;
$notify->createdBy = $mailQueue->addedBy;
$notify->createdDate = $mailQueue->addedDate;
$notify->sendTime = $mailQueue->sendTime;
$notify->status = $mailQueue->status;
$notify->failReason = $mailQueue->failReason;
$this->dao->insert(TABLE_NOTIFY)->data($notify)->exec();
}
$webhookDataTable = '`' . $this->config->db->prefix . 'webhookdatas`';
$stmt = $this->dao->select('*')->from($webhookDataTable)->orderBy('id')->limit($offset, $rows)->query();
while($webhookData = $stmt->fetch())
{
$notify = new stdclass();
$notify->objectType = 'webhook';
$notify->objectID = $webhookData->webhook;
$notify->action = $webhookData->action;
$notify->data = $webhookData->data;
$notify->createdBy = $webhookData->createdBy;
$notify->createdDate = $webhookData->createdDate;
$notify->status = $webhookData->status;
$this->dao->insert(TABLE_NOTIFY)->data($notify)->exec();
}
return true;
}
/**
* Adjust priv 9.8.
*
* @access public
* @return bool
*/
public function adjustPriv9_8()
{
$groups = $this->dao->select('id')->from(TABLE_GROUP)->fetchPairs('id', 'id');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'todo';
$groupPriv->method = 'createcycle';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('mail')->orWhere('module')->eq('webhook')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'message';
$groupPriv->method = 'index';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('project')->andWhere('method')->eq('linkStory')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'project';
$groupPriv->method = 'importPlanStories';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Fix task finishedBy.
*
* @access public
* @return bool
*/
public function fixTaskFinishedInfo()
{
$stmt = $this->dao->select('t1.id as historID,t2.objectType,t2.objectID,t2.actor')->from(TABLE_HISTORY)->alias('t1')
->leftJoin(TABLE_ACTION)->alias('t2')->on('t1.action=t2.id')
->where('t1.field')->eq('finishedBy')
->andWhere('t2.objectType')->eq('task')
->andWhere('t2.action')->eq('finished')
->andWhere('t2.actor != t1.`new`')
->query();
while($action = $stmt->fetch())
{
$this->dao->update(TABLE_HISTORY)->set('`new`')->eq($action->actor)->where('id')->eq($action->historID)->exec();
$this->dao->update(TABLE_TASK)->set('`finishedBy`')->eq($action->actor)->where('id')->eq($action->objectID)->exec();
}
return true;
}
/**
* Fix assignedTo for closed tasks, but assignedTo is not closed.
*
* @access public
* @return bool
*/
public function fixTaskAssignedTo()
{
$minParent = $this->dao->select('parent')->from(TABLE_TASK)->where('parent')->ne(0)->orderBy('parent')->limit(1)->fetch('parent');
if(empty($minParent)) return true;
$needUpdateTasks = $this->dao->select('id,parent,closedBy')->from(TABLE_TASK)
->where('status')->eq('closed')
->andWhere('assignedTo')->ne('closed')
->andWhere('id')->ge($minParent)
->fetchAll('id');
if(empty($needUpdateTasks)) return true;
$needUpdateParentTasks = array();
$needUpdateChildTasks = array();
foreach($needUpdateTasks as $taskID => $task)
{
if(!$task->parent)
{
$needUpdateParentTasks[$taskID] = $task;
}
else
{
if(!isset($needUpdateChildTasks[$task->parent])) $needUpdateChildTasks[$task->parent] = array();
$needUpdateChildTasks[$task->parent][$taskID] = $task;
}
}
/* Update parent task.*/
$childTasks = $this->dao->select('id,parent,assignedDate,closedBy,closedDate,closedReason')->from(TABLE_TASK)->where('parent')->in(array_keys($needUpdateParentTasks))->fetchGroup('parent');
$lastChildTasks = array();
foreach($childTasks as $parentID => $tasks)
{
foreach($tasks as $task)
{
if(isset($lastChildTasks[$parentID]))
{
if($lastChildTasks[$parentID]->closedDate < $task->closedDate) $lastChildTasks[$parentID] = $task;
}
else
{
$lastChildTasks[$parentID] = $task;
}
}
}
foreach($needUpdateParentTasks as $parentTask)
{
$lastChild = isset($lastChildTasks[$parentTask->id]) ? $lastChildTasks[$parentTask->id] : '';
$stmt = $this->dao->update(TABLE_TASK)->set('assignedTo')->eq('closed');
if($lastChild) $stmt->set('assignedDate')->eq($lastChild->assignedDate);
$stmt->where('id')->eq($parentTask->id)->exec();
if(empty($parentTask->closedBy) && !empty($lastChild->closedBy))
{
$this->dao->update(TABLE_TASK)->set('closedBy')->eq($lastChild->closedBy)->set('closedDate')->eq($lastChild->closedDate)->set('closedReason')->eq($lastChild->closedReason)->where('id')->eq($parentTask->id)->exec();
}
}
/* Update children task.*/
$parentTasks = $this->dao->select('id,assignedDate,closedBy,closedDate,closedReason')->from(TABLE_TASK)
->where('parent')->eq(0)
->andWhere('id')->in(array_keys($needUpdateChildTasks))
->fetchAll('id');
foreach($needUpdateChildTasks as $parentID => $childTasks)
{
$parent = isset($parentTasks[$parentID]) ? $parentTasks[$parentID] : '';
foreach($childTasks as $childTask)
{
$stmt = $this->dao->update(TABLE_TASK)->set('assignedTo')->eq('closed');
if(!empty($parent)) $stmt->set('assignedDate')->eq($parent->assignedDate);
$stmt->where('id')->eq($childTask->id)->exec();
if(empty($childTask->closedBy) && !empty($parent->closedBy))
{
$this->dao->update(TABLE_TASK)->set('closedBy')->eq($parent->closedBy)->set('closedDate')->eq($parent->closedDate)->set('closedReason')->eq($parent->closedReason)->where('id')->eq($childTask->id)->exec();
}
}
}
return dao::isError();
}
/**
* Fix project closedBy and closedDate.
*
* @access public
* @return bool
*/
public function fixProjectClosedInfo()
{
$stmt = $this->dao->select('t1.id as historID, t2.id, t2.objectType,t2.objectID,t2.actor,t2.date')->from(TABLE_HISTORY)->alias('t1')
->leftJoin(TABLE_ACTION)->alias('t2')->on('t1.action=t2.id')
->where('t1.field')->eq('status')
->andWhere('t2.objectType')->eq('project')
->andWhere('t2.action')->eq('closed')
->query();
while($action = $stmt->fetch())
{
$this->dao->insert(TABLE_HISTORY)->set('`new`')->eq($action->actor)->set('`field`')->eq('closedBy')->set('`action`')->eq($action->id)->exec();
$this->dao->insert(TABLE_HISTORY)->set('`new`')->eq($action->date)->set('`old`')->eq('0000-00-00 00:00:00')->set('`field`')->eq('closedDate')->set('`action`')->eq($action->id)->exec();
$this->dao->update(TABLE_HISTORY)->set('`new`')->eq('closed')->where('`action`')->eq($action->id)->andWhere('field')->eq('status')->exec();
$this->dao->update(TABLE_PROJECT)
->set('`status`')->eq('closed')
->set('`closedBy`')->eq($action->actor)
->set('`closedDate`')->eq($action->date)
->where('id')->eq($action->objectID)
->andWhere('status')->eq('done')
->exec();
}
return !dao::isError();
}
/**
* Set the value of deleted product line to 0.
*
* @access public
* @return bool
*/
public function resetProductLine()
{
$deletedLines = $this->dao->select('id')->from(TABLE_MODULE)->where('type')->eq('line')->andWhere('deleted')->eq('1')->fetchPairs('id', 'id');
$this->dao->update(TABLE_PRODUCT)->set('line')->eq(0)->where('line')->in($deletedLines)->exec();
return !dao::isError();
}
/**
* Add unique key to team table.
*
* @access public
* @return bool
*/
public function addUniqueKeyToTeam()
{
$members = $this->dao->select('root, type, account')->from(TABLE_TEAM)->groupBy('root, type, account')->having('count(1)')->gt(1)->fetchAll();
foreach($members as $member)
{
$maxID = $this->dao->select('MAX(id) id')
->from(TABLE_TEAM)
->where('root')->eq($member->root)
->andWhere('`type`')->eq($member->type)
->andWhere('account')->eq($member->account)
->fetch('id');
$this->dao->delete()->from(TABLE_TEAM)
->where('root')->eq($member->root)
->andWhere('`type`')->eq($member->type)
->andWhere('account')->eq($member->account)
->andWhere('id')->ne($maxID)
->exec();
}
$this->dao->exec("ALTER TABLE " . TABLE_TEAM . " ADD UNIQUE `team` (`root`, `type`, `account`)");
return !dao::isError();
}
/**
* Adjust priv for 10_0_alpha.
*
* @access public
* @return bool
*/
public function adjustPriv10_0_alpha()
{
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('my')->andWhere('method')->eq('todo')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'my';
$groupPriv->method = 'calendar';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Fix project statistic block.
*
* @access public
* @return void
*/
public function fixProjectStatisticBlock()
{
$block = $this->dao->select('*')->from(TABLE_BLOCK)->where('module')->eq('my')->andWhere('source')->eq('project')->andWhere('block')->eq('statistic')->fetch();
if($block)
{
$blockParams = json_decode($block->params);
if($blockParams->type == 'noclosed')
{
$blockParams->type = 'undone';
$this->dao->update(TABLE_BLOCK)->set('params')->eq(helper::jsonEncode($blockParams))->where('id')->eq($block->id)->exec();
return !dao::isError();
}
}
return true;
}
/**
* Fix story spec title.
*
* @access public
* @return bool
*/
public function fixStorySpecTitle()
{
$stories = $this->dao->select('t1.id, t1.title')->from(TABLE_STORY)->alias('t1')
->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story && t1.title != t2.title && t1.version = t2.version')
->where('t2.version')->eq(1)
->fetchPairs('id', 'title');
foreach($stories as $story => $title)
{
$this->dao->update(TABLE_STORYSPEC)->set('title')->eq($title)->where('story')->eq($story)->andWhere('version')->eq(1)->exec();
}
return !dao::isError();
}
/**
* Remove unlink privilege for story, bug and testcase module.
*
* @access public
* @return bool
*/
public function removeUnlinkPriv()
{
$this->dao->delete()->from(TABLE_GROUPPRIV)
->where('((module')->eq('story')
->andWhere('method')->eq('unlinkStory')
->markRight(1)
->orWhere('(module')->eq('bug')
->andWhere('method')->eq('unlinkBug')
->markRight(1)
->orWhere('(module')->eq('testcase')
->andWhere('method')->eq('unlinkCase')
->markRight(2)
->exec();
return !dao::isError();
}
/**
* Change task parent to -1 for 10.4 .
* @return bool
*/
public function changeTaskParentValue()
{
$tasks = $this->dao->select('*')->from(TABLE_TASK)->where('parent')->gt(0)->fetchGroup('parent');
if($tasks)
{
$this->dao->update(TABLE_TASK)->set('parent')->eq('-1')->where('id')->in(array_keys($tasks))->exec();
}
return !dao::isError();
}
/**
* Remove custom menu.
*
* @access public
* @return bool
*/
public function removeCustomMenu()
{
$customMenuMain = $this->dao->select('*')->from(TABLE_CONFIG)->where('module')->eq('common')->andWhere('section')->eq('customMenu')->andWhere("(`key`='full_main' OR `key`='onlyTask_main' OR `key`='onlyStory_main' OR `key`='onlyTest_main')")->fetchAll('id', false);
foreach($customMenuMain as $mainMenu)
{
$mainMenuValue = json_decode($mainMenu->value);
foreach($mainMenuValue as $menu)
{
/* If has admin in custom value, then delete old custom menu config. */
if($menu->name == 'admin')
{
$this->dao->delete()->from(TABLE_CONFIG)->where('module')->eq('common')
->andWhere('section')->eq('customMenu')
->andWhere('owner')->eq($mainMenu->owner)
->exec();
}
}
}
$this->dao->delete()->from(TABLE_CONFIG)->where('module')->eq('common')->andWhere('section')->eq('customMenu')->andWhere('`key`')->eq('full_project')->exec();
$this->dao->delete()->from(TABLE_CONFIG)->where('module')->eq('common')->andWhere('section')->eq('customMenu')->andWhere('`key`')->eq('onlyTask_project')->exec();
return !dao::isError();
}
/**
* 初始化用户视图.
* Initialize user view.
*
* @access public
* @return bool
*/
public function initUserView(): bool
{
$this->dao->delete()->from(TABLE_USERVIEW)->exec();
return true;
}
/**
* Init Xuanxuan.
*
* @access public
* @return bool
*/
public function initXuanxuan()
{
$this->loadModel('setting');
$keyID = $this->dao->select('id')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('xuanxuan')->andWhere('`key`')->eq('key')->fetch('id');
if($keyID)
{
$existKey = $this->dao->select('id')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('common')->andWhere('section')->eq('xuanxuan')->andWhere('`key`')->eq('key')->fetch('id');
if($existKey) $this->dao->delete()->from(TABLE_CONFIG)->where('id')->eq($existKey)->exec();
$this->dao->update(TABLE_CONFIG)->set('module')->eq('common')->set('section')->eq('xuanxuan')->where('id')->eq($keyID)->exec();
$this->setting->setItem('system.common.xuanxuan.turnon', '0');
$this->setting->setItem('system.common.xxserver.noticed', '1');
}
return true;
}
/**
* Process doc lib acl.
*
* @access public
* @return void
*/
public function processDocLibAcl()
{
$this->dao->update(TABLE_DOCLIB)->set('acl')->eq('default')->where('type')->in('product,project')->andWhere('acl')->in('open,private')->exec();
return !dao::isError();
}
/**
* Update xuanxuan for 11_5.
*
* @access public
* @return bool
*/
public function updateXX_11_5()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('admin')->andWhere('method')->eq('xuanxuan')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'setting';
$groupPriv->method = 'xuanxuan';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
try
{
$this->dao->update(TABLE_GROUPPRIV)->set('module')->eq('setting')->where('module')->eq('admin')->andWhere('method')->eq('downloadxxd')->exec();
}
catch(PDOException $e){}
return true;
}
/**
* Adjust webhook type list when webhook use bearychat.
*
* @access public
* @return void
*/
public function adjustWebhookType()
{
$bearychatCount = $this->dao->select('COUNT(1) AS count')->from(TABLE_WEBHOOK)->where('type')->eq('bearychat')->fetch('count');
if($bearychatCount)
{
$item = new stdclass();
$item->module = 'webhook';
$item->section = 'typeList';
foreach(array('zh-cn', 'zh-tw', 'en', 'de') as $currentLang)
{
$langFile = $this->app->getModuleRoot() . 'webhook' . DS . 'lang' . DS . $currentLang . '.php';
if(!file_exists($langFile)) continue;
$lang = new stdclass();
$lang->webhook = new stdclass();
$lang->productCommon = $this->config->productCommonList[$currentLang][0];
$lang->executionCommon = $this->config->executionCommonList[$currentLang][0];
include $langFile;
if(!isset($lang->webhook->typeList)) continue;
$item->lang = $currentLang;
$item->key = 'bearychat';
$item->value = $this->config->upgrade->bearychat[$currentLang];
$this->dao->replace(TABLE_LANG)->data($item)->exec();
foreach($lang->webhook->typeList as $typeKey => $typeName)
{
if(empty($typeKey)) continue;
$item->key = $typeKey;
$item->value = $typeName;
$this->dao->replace(TABLE_LANG)->data($item)->exec();
}
}
}
return true;
}
/**
* Adjust priv for 11.6.2.
*
* @access public
* @return bool
*/
public function adjustPriv11_6_2()
{
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('method')->eq('index')->andWhere('module')->in('message')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'message';
$groupPriv->method = 'browser';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Adjust priv for 11.6.4.
*
* @access public
* @return void
*/
public function adjustPriv11_6_4()
{
$this->dao->update(TABLE_GROUPPRIV)->set('module')->eq('caselib')->set('method')->eq('browse')->where('module')->eq('testsuite')->andWhere('method')->eq('library')->exec();
$this->dao->update(TABLE_GROUPPRIV)->set('module')->eq('caselib')->set('method')->eq('create')->where('module')->eq('testsuite')->andWhere('method')->eq('createLib')->exec();
$this->dao->update(TABLE_GROUPPRIV)->set('module')->eq('caselib')->set('method')->eq('view')->where('module')->eq('testsuite')->andWhere('method')->eq('libView')->exec();
$this->dao->update(TABLE_GROUPPRIV)->set('module')->eq('caselib')->where('module')->eq('testsuite')->andWhere('method')->in('exportTemplate,import,showImport,batchCreateCase,createCase')->exec();
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('testsuite')->andWhere('method')->eq('edit')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'caselib';
$groupPriv->method = 'edit';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('testsuite')->andWhere('method')->eq('delete')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'caselib';
$groupPriv->method = 'delete';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Fix group acl.
*
* @access public
* @return bool
*/
public function fixGroupAcl()
{
$groups = $this->dao->select('*')->from(TABLE_GROUP)->fetchAll('id', false);
foreach($groups as $group)
{
if(empty($group->acl)) continue;
$acl = json_decode($group->acl, true);
if(isset($acl['products']))
{
$isEmpty = true;
foreach($acl['products'] as $productID)
{
if(!empty($productID)) $isEmpty = false;
}
if($isEmpty) unset($acl['products']);
}
if(isset($acl['projects']))
{
$isEmpty = true;
foreach($acl['projects'] as $projectID)
{
if(!empty($projectID)) $isEmpty = false;
}
if($isEmpty) unset($acl['projects']);
}
$acl = json_encode($acl);
$this->dao->update(TABLE_GROUP)->set('acl')->eq($acl)->where('id')->eq($group->id)->exec();
}
return true;
}
/**
* Adjust 11.7 priv.
*
* @access public
* @return void
*/
public function adjustPriv11_7()
{
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('editor')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'dev';
$groupPriv->method = 'editor';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('translate')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'dev';
$groupPriv->method = 'translate';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('module')->eq('translate')->exec();
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('module')->eq('editor')->exec();
return true;
}
/**
* Fix bug typeList.
*
* @access public
* @return bool
*/
public function fixBugTypeList()
{
$customedTypeList4All = $this->dao->select('*')->from(TABLE_LANG)
->where('lang')->eq("all")
->andWhere('module')->eq('bug')
->andWhere('section')->eq('typeList')
->fetchPairs('`key`', 'value');
foreach($this->config->upgrade->discardedBugTypes as $langCode => $types)
{
$bugs = $this->dao->select('type')->from(TABLE_BUG)->where('type')->in(array_keys($types))->fetchPairs('type', 'type');
if(empty($bugs)) return true;
$usedTypes = array_keys($bugs);
$customedTypeList = $this->dao->select('*')->from(TABLE_LANG)
->where('lang')->eq($langCode)
->andWhere('module')->eq('bug')
->andWhere('section')->eq('typeList')
->fetchPairs('`key`', 'value');
$typesToSave = array_diff($usedTypes, empty($customedTypeList) ? $customedTypeList4All : $customedTypeList);
if(empty($typesToSave)) continue;
$langs = array();
foreach($typesToSave as $type) $langs[$type] = $types[$type];
if(empty($customedTypeList) and empty($customedTypeList4All))
{
$lang = new stdclass;
$lang->bug = new stdclass;
$lang->productCommon = '';
$lang->executionCommon = '';
$lang->more = '';
$langFile = $this->app->getModuleRoot() . DS . 'bug' . DS . 'lang' . DS . $langCode . '.php';
if(is_file($langFile)) include $langFile;
$langs = array_merge($lang->bug->typeList, $langs);
}
elseif(empty($customedTypeList))
{
$langs = array_merge($customedTypeList4All, $langs);
}
$this->loadModel('custom');
foreach($langs as $type => $typeName) $this->custom->setItem("{$langCode}.bug.typeList.{$type}.1", $typeName);
}
return true;
}
/**
* Remove editor and translate.
*
* @access public
* @return bool
*/
public function rmEditorAndTranslateDir()
{
$zfile = $this->app->loadClass('zfile');
$moduleRoot = $this->app->getModuleRoot();
$editorDir = $moduleRoot . 'editor';
if(is_dir($editorDir)) $zfile->removeDir($editorDir);
$translateDir = $moduleRoot . 'translate';
if(is_dir($translateDir)) $zfile->removeDir($translateDir);
return true;
}
/**
* Set concept setted.
*
* @access public
* @return bool
*/
public function setConceptSetted()
{
$conceptSetted = $this->dao->select('*')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('common')->andWhere('`key`')->eq('conceptSetted')->fetchAll();
if(empty($conceptSetted))
{
$setting = new stdclass();
$setting->owner = 'system';
$setting->module = 'custom';
$setting->key = 'storyRequirement';
$setting->value = '0';
$this->dao->insert(TABLE_CONFIG)->data($setting)->exec();
$setting->key = 'hourPoint';
$setting->value = '0';
$this->dao->insert(TABLE_CONFIG)->data($setting)->exec();
$setting->module = 'common';
$setting->key = 'conceptSetted';
$setting->value = '1';
$this->dao->insert(TABLE_CONFIG)->data($setting)->exec();
}
return true;
}
/**
* Adjust priv 12.0.
*
* @access public
* @return bool
*/
public function adjustPriv12_0()
{
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('file')->andWhere('method')->eq('delete')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'doc';
$groupPriv->method = 'deleteFile';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Save repo from svn and git config.
*
* @access public
* @return bool
*/
public function importRepoFromConfig()
{
$this->app->loadConfig('svn');
if(isset($this->config->svn->repos))
{
$scm = $this->app->loadClass('scm');
foreach($this->config->svn->repos as $i => $repo)
{
$repoPath = $repo['path'];
if(empty($repoPath)) continue;
$existRepo = $this->dao->select('*')->from(TABLE_REPO)->where('path')->eq($repoPath)->andWhere('SCM')->eq('Subversion')->fetch();
if($existRepo) continue;
$svnRepo = new stdclass();
$svnRepo->client = $this->config->svn->client;
$svnRepo->name = basename($repoPath);
$svnRepo->path = $repoPath;
$svnRepo->SCM = 'Subversion';
$svnRepo->account = $repo['username'];
$svnRepo->password = $repo['password'];
$svnRepo->encrypt = 'base64';
$svnRepo->encoding = zget($repo, 'encoding', $this->config->svn->encoding);
$scm->setEngine($svnRepo);
$info = $scm->info('');
$svnRepo->prefix = empty($info->root) ? '' : trim(str_ireplace($info->root, '', str_replace('\\', '/', $svnRepo->path)), '/');
if($svnRepo->prefix) $svnRepo->prefix = '/' . $svnRepo->prefix;
$svnRepo->password = base64_encode($repo['password']);
$this->dao->insert(TABLE_REPO)->data($svnRepo)->exec();
}
}
$this->app->loadConfig('git');
if(isset($this->config->git->repos))
{
foreach($this->config->git->repos as $i => $repo)
{
$repoPath = $repo['path'];
if(empty($repoPath)) continue;
$existRepo = $this->dao->select('*')->from(TABLE_REPO)->where('path')->eq($repoPath)->andWhere('SCM')->eq('Git')->fetch();
if($existRepo) continue;
$gitRepo = new stdclass();
$gitRepo->client = $this->config->git->client;
$gitRepo->name = basename($repoPath);
$gitRepo->path = $repoPath;
$gitRepo->prefix = '';
$gitRepo->SCM = 'Git';
$gitRepo->account = '';
$gitRepo->password = '';
$gitRepo->encrypt = 'base64';
$gitRepo->encoding = zget($repo, 'encoding', $this->config->git->encoding);
$this->dao->insert(TABLE_REPO)->data($gitRepo)->exec();
}
}
return true;
}
/**
* Fix fromCaseVersion field for zt_case table.
*
* @access public
* @return bool
*/
public function fixFromCaseVersion()
{
/* Get imported cases and cases version is null. */
$errorCasePairs = $this->dao->select('id,fromCaseID,fromCaseVersion')->from(TABLE_CASE)->where('fromCaseID')->ne(0)->andWhere('fromCaseVersion')->eq(0)->fetchPairs('id', 'fromCaseID');
if(empty($errorCasePairs)) return true;
/* Get from case versions by from cases. */
$fromCaseIdList = array_unique(array_values($errorCasePairs));
$fromCaseVersions = $this->dao->select('id,version')->from(TABLE_CASE)->where('id')->in($fromCaseIdList)->fetchPairs('id', 'version');
/* Fix fromCaseVersion field. */
foreach($errorCasePairs as $caseID => $fromCaseID)
{
$fromCaseVersion = zget($fromCaseVersions, $fromCaseID, 1);
$this->dao->update(TABLE_CASE)->set('fromCaseVersion')->eq($fromCaseVersion)->where('id')->eq($caseID)->exec();
}
return true;
}
/**
* Adjust priv 12.5.
*
* @access public
* @return bool
*/
public function adjustPriv12_5()
{
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('custom')->andWhere('method')->eq('set')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'custom';
$groupPriv->method = 'product';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
$groupPriv->method = 'project';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$groups = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('task')->andWhere('method')->eq('create')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'story';
$groupPriv->method = 'batchToTask';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Adjust priv 15.0.
*
* @access public
* @return true
*/
public function adjustPriv15_0()
{
$executionPriv = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('execution')->limit(1)->fetch();
if(empty($executionPriv))
{
$projectPrivList = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('project')->fetchAll();
$this->dao->update(TABLE_GROUPPRIV)->set('module')->eq('execution')->where('module')->eq('project')->exec();
foreach($projectPrivList as $projectPriv)
{
if(!in_array($projectPriv->method, array('browse', 'story', 'bug', 'testtask', 'doc', 'build', 'index', 'create', 'edit', 'batchedit', 'start', 'activate', 'suspend', 'close', 'delete', 'export', 'manageProducts', 'manageMembers', 'team', 'unlinkMember', 'unlinkStory', 'view'))) continue;
$this->dao->replace(TABLE_GROUPPRIV)->data($projectPriv)->exec();
}
}
$groups = $this->dao->select('id')->from(TABLE_GROUP)->fetchPairs('id', 'id');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'my';
$groupPriv->method = 'work';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
$groupPriv->method = 'contribute';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
$groupPriv->method = 'team';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('my')->andWhere('method')->eq('project')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->method = 'execution';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('program')->andWhere('method')->like('PGM%')->query();
while($grouppriv = $stmt->fetch())
{
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('module')->eq($grouppriv->module)->andWhere('method')->eq($grouppriv->method)->exec();
$grouppriv->method = strtolower(str_ireplace('PGM', '', $grouppriv->method));
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
$grouppriv->method = 'index';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('program')->andWhere('method')->like('PRJ%')->query();
while($grouppriv = $stmt->fetch())
{
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('module')->eq($grouppriv->module)->andWhere('method')->eq($grouppriv->method)->exec();
$grouppriv->module = 'project';
$grouppriv->method = strtolower(str_ireplace('PRJ', '', $grouppriv->method));
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
$grouppriv->method = 'index';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('project')->andWhere('method')->eq('story')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->module = 'projectstory';
$grouppriv->method = 'story';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('story')->andWhere('method')->eq('view')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->module = 'projectstory';
$grouppriv->method = 'view';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('project')->andWhere('method')->eq('linkstory')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->module = 'projectstory';
$grouppriv->method = 'linkstory';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('project')->andWhere('method')->eq('unlinkstory')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->module = 'projectstory';
$grouppriv->method = 'unlinkstory';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('execution')->andWhere('method')->eq('all')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->module = 'project';
$grouppriv->method = 'execution';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
$grouppriv->module = 'project';
$grouppriv->method = 'browse';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
$grouppriv->module = 'project';
$grouppriv->method = 'index';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('doc')->andWhere('method')->eq('createlib')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->method = 'createLib';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
$stmt = $this->dao->select('`group`,module,method')->from(TABLE_GROUPPRIV)->where('module')->eq('doc')->andWhere('method')->eq('editlib')->query();
while($grouppriv = $stmt->fetch())
{
$grouppriv->method = 'editLib';
$this->dao->replace(TABLE_GROUPPRIV)->data($grouppriv)->exec();
}
return true;
}
/**
* Adjust userview.
*
* @access public
* @return bool
*/
public function adjustUserView()
{
$userViews = $this->dao->select('`account`,`sprints`,`projects`')->from(TABLE_USERVIEW)->where('projects')->ne('')->fetchAll('account');
$projectIdList = array();
$accountProjects = array();
$accountExecutions = array();
foreach($userViews as $account => $userView)
{
$projects = explode(',', trim($userView->projects, ','));
foreach($projects as $projectID)
{
if(empty($projectID)) continue;
$accountProjects[$account][$projectID] = $projectID;
if(isset($projectIdList[$projectID])) continue;
$projectIdList[$projectID] = $projectID;
}
$executions = explode(',', trim($userView->sprints, ','));
foreach($executions as $executionID)
{
if(empty($executionID)) continue;
$accountExecutions[$account][$executionID] = $executionID;
}
}
$executionPairs = $this->dao->select('id')->from(TABLE_PROJECT)->where('id')->in($projectIdList)->andWhere('type')->in('sprint,stage,kanban')->fetchPairs('id', 'id');
foreach($userViews as $account => $userView)
{
$projects = zget($accountProjects, $account, array());
if(empty($projects)) continue;
$executions = zget($accountExecutions, $account, array());
foreach($projects as $projectID)
{
if(isset($executionPairs[$projectID]))
{
$executions[$projectID] = $projectID;
unset($projects[$projectID]);
}
}
$this->dao->update(TABLE_USERVIEW)->set('sprints')->eq(implode(',', $executions))->set('projects')->eq(implode(',', $projects))->where('account')->eq($account)->exec();
}
return true;
}
/**
* 存储日志。
* Save logs.
*
* @param string $log
* @access public
* @return void
*/
public function saveLogs(string $log): void
{
$logFile = $this->getLogFile();
$log = date('Y-m-d H:i:s') . ' ' . trim($log) . "\n";
if(!file_exists($logFile)) $log = "<?php\ndie();\n?" . ">\n" . $log;
file_put_contents($logFile, $log, FILE_APPEND);
}
/**
* 手动归并数据。
* Manually merge data.
*
* @param array $projectIdList
* @access public
* @return int
*/
public function createProgram(array $projectIdList = array()): bool|array
{
$data = fixer::input('post')->get();
/* 新增项目集或者更新项目集状态。*/
/* Create program or update program status. */
if(isset($data->newProgram) && $this->config->systemMode != 'light')
{
$result = $this->createNewProgram($data, $projectIdList);
if(dao::isError()) return false;
if(isset($result['result']) && $result['result'] == 'fail') return $result;
$programID = $result;
}
else
{
$programID = $data->programID ? $data->programID : (int)$data->programs;
$this->dao->update(TABLE_PROGRAM)->set('status')->eq($data->programStatus)->where('id')->eq($programID)->exec();
if($data->programStatus == 'closed') $this->loadModel('action')->create('program', $programID, 'closedbysystem');
}
/* 新增产品线,返回产品线 ID。*/
/* Create product line, and return product line ID. */
$lineID = 0;
if(isset($data->newLine))
{
if(!empty($data->lineName))
{
$lineID = $this->createNewLine($data->lineName, $programID);
if(dao::isError()) return false;
}
}
else
{
if(!empty($data->lines)) $lineID = (int)$data->lines;
}
if(!isset($data->sprints)) return array($programID, 0, $lineID);
/* 新增项目或者更新项目状态。*/
$projectList = array();
if(isset($data->newProject))
{
$result = $this->createNewProject($data, $programID, $projectIdList);
if(dao::isError()) return false;
if(isset($result['result']) && $result['result'] == 'fail') return $result;
$projectList = $result;
}
elseif(!empty($data->projects))
{
$projectList = (int)$data->projects;
$this->dao->update(TABLE_PROJECT)->set('status')->eq($data->projectStatus)->where('id')->eq($projectList)->exec();
if($data->projectStatus == 'closed') $this->loadModel('action')->create('project', $projectList, 'closedbysystem');
}
return array($programID, $projectList, $lineID);
}
/**
* 创建新的项目集。
* Create new program.
*
* @param object $data
* @param array $projectIdList
* @access public
* @return bool|int|array
*/
public function createNewProgram(object $data, array $projectIdList): bool|int|array
{
$projectType = zget($data, 'projectType', 'project');
$this->upgradeTao->checkProgramRequired($data, $projectType);
if(dao::isError()) return false;
if($projectType == 'project')
{
$duplicateList = $this->upgradeTao->getDuplicateProjectName($projectIdList);
if($duplicateList) return array('result' => 'fail', 'callback' => "loadModal('" . helper::createLink('upgrade', 'renameObject', "type=project&duplicateList={$duplicateList}") . "')");
}
/* Insert program. */
$program = new stdclass();
$program->name = $data->programName;
$program->type = 'program';
$program->status = $data->programStatus;
$program->begin = !empty($data->begin) ? $data->begin : helper::now();
$program->end = !empty($data->end) ? $data->end : LONG_TIME;
$program->openedBy = isset($this->app->user->account) ? $this->app->user->account : '';
$program->openedDate = helper::now();
$program->openedVersion = $this->config->version;
$program->acl = isset($data->programAcl) ? $data->programAcl : 'open';
$program->days = $this->computeDaysDelta($program->begin, $program->end);
$program->PM = $projectType == 'project' ? zget($data, 'PM', '') : '';
$program->vision = 'rnd';
$this->app->loadLang('program');
$this->app->loadLang('project');
$this->lang->project->name = $this->lang->program->name;
$this->dao->insert(TABLE_PROJECT)->data($program)
->batchcheck('name,begin', 'notempty')
->checkIF($program->end != '', 'end', 'gt', $program->begin)
->check('name', 'unique', "deleted='0' and type= 'program'")
->exec();
if(dao::isError())
{
$errors = dao::getError();
if(isset($errors['name']))
{
$errors['programName'] = $errors['name'];
unset($errors['name']);
}
dao::$errors = $errors;
return false;
}
$programID = $this->dao->lastInsertId();
$this->dao->update(TABLE_PROGRAM)->set('grade')->eq(1)->set('path')->eq(",{$programID},")->set('`order`')->eq($programID * 5)->where('id')->eq($programID)->exec();
$this->loadModel('action')->create('program', $programID, 'openedbysystem');
if($data->programStatus == 'closed') $this->loadModel('action')->create('program', $programID, 'closedbysystem');
return $programID;
}
/**
* 创建新的产品线。
* Create new product line.
*
* @param sting $lineName
* @param int $programID
* @access public
* @return bool|int
*/
public function createNewLine(string $lineName, int $programID): bool|int
{
/* Insert product line. */
$maxOrder = $this->dao->select("MAX(`order`) AS maxOrder")->from(TABLE_MODULE)->where('type')->eq('line')->fetch('maxOrder');
$maxOrder = $maxOrder ? $maxOrder + 10 : 0;
$line = new stdClass();
$line->type = 'line';
$line->parent = 0;
$line->grade = 1;
$line->name = $lineName;
$line->root = $programID;
$line->order = $maxOrder;
$this->dao->insert(TABLE_MODULE)->data($line)->exec();
$lineID = $this->dao->lastInsertID();
$path = ",$lineID,";
$this->dao->update(TABLE_MODULE)->set('path')->eq($path)->where('id')->eq($lineID)->exec();
if(dao::isError()) return false;
return $lineID;
}
/**
* 创建新的项目。
* Create new project.
*
* @param object $data
* @param int $programID
* @param array $projectIdList
* @access public
* @return bool|int|array
*/
public function createNewProject(object $data, int $programID, array $projectIdList): bool|int|array
{
if(!isset($data->longTime) && !$data->end)
{
dao::$errors['end'][] = sprintf($this->lang->error->notempty, $this->lang->upgrade->end);
return false;
}
/* Create a project. */
$this->lang->project->name = $this->lang->upgrade->projectName;
/* Use historical projects as execution upgrades. */
$projectType = zget($data, 'projectType', 'project');
if($projectType == 'execution') return $this->createProject($programID, $data);
/* Use historical projects as project upgrades. */
$projects = $this->dao->select('id,name,begin,end,status,PM,acl,team')->from(TABLE_PROJECT)->where('id')->in($projectIdList)->fetchAll('id');
$projectPairs = $this->dao->select('name,id')->from(TABLE_PROJECT)->where('deleted')->eq('0')->andWhere('type')->eq('project')->andWhere('parent')->eq($programID)->fetchPairs();
$duplicateList = '';
foreach($projects as $projectID => $project)
{
if(isset($projectPairs[$project->name]))
{
$duplicateList .= "$projectID,";
$duplicateList .= "{$projectPairs[$project->name]},";
}
}
if($duplicateList) return array('result' => 'fail', 'callback' => "loadModal('" . helper::createLink('upgrade', 'renameObject', "type=project&duplicateList={$duplicateList}") . "')");
$projectList = array();
foreach($projectIdList as $projectID)
{
if(!isset($projects[$projectID])) continue;
$data->projectName = $projects[$projectID]->name;
$data->begin = $projects[$projectID]->begin;
$data->end = $projects[$projectID]->end;
$data->projectStatus = $projects[$projectID]->status;
$data->team = empty($projects[$projectID]->team) ? $projects[$projectID]->name : $projects[$projectID]->team;
$data->PM = $projects[$projectID]->PM;
$data->projectAcl = $projects[$projectID]->acl == 'custom' ? 'private' : $projects[$projectID]->acl;
$projectList[$projectID] = $this->createProject($programID, $data);
}
return $projectList;
}
/**
* 创建一个项目。
* Create a project.
*
* @param int $programID
* @param object $data
* @access public
* @return int|bool
*/
public function createProject(int $programID = 0, ?object $data = null): int|bool
{
$now = helper::now();
$account = isset($this->app->user->account) ? $this->app->user->account : '';
/* Insert project. */
$project = new stdclass();
$project->name = $data->projectName;
$project->type = 'project';
$project->model = 'scrum';
$project->parent = $programID;
$project->status = $data->projectStatus;
$project->team = !empty($data->team) ? $data->team : $data->projectName;
$project->begin = $data->begin;
$project->end = !empty($data->end) ? $data->end : LONG_TIME;
$project->days = $this->computeDaysDelta($project->begin, $project->end);
$project->PM = $data->PM;
$project->auth = 'extend';
$project->openedBy = $account;
$project->openedDate = $now;
$project->openedVersion = $this->config->version;
$project->lastEditedBy = $account;
$project->lastEditedDate = $now;
$project->acl = isset($data->projectAcl) ? $data->projectAcl : 'open';
$project->storyType = 'story';
$this->dao->insert(TABLE_PROJECT)->data($project)
->batchcheck('name', 'notempty')
->check('name', 'unique', "type='project' AND parent='{$programID}' AND deleted='0'")
->exec();
if(dao::isError()) return false;
$projectID = $this->dao->lastInsertID();
$this->dao->update(TABLE_PROJECT)->set('grade')->eq(2)->set('path')->eq(",{$programID},{$projectID},")->set('`order`')->eq($projectID * 5)->where('id')->eq($projectID)->exec();
/* Modify the begin and end of the program to match the begin and end of the project. */
$programDate = $this->dao->select('begin,end')->from(TABLE_PROGRAM)->where('id')->eq($programID)->fetch();
if($data->begin < $programDate->begin) $this->dao->update(TABLE_PROGRAM)->set('begin')->eq($data->begin)->where('id')->eq($programID)->exec();
if(isset($data->end) && $data->end > $programDate->end) $this->dao->update(TABLE_PROGRAM)->set('end')->eq($data->end)->where('id')->eq($programID)->exec();
$this->loadModel('action')->create('project', $projectID, 'openedbysystem');
if($data->projectStatus == 'closed') $this->action->create('project', $projectID, 'closedbysystem');
/* Create doc lib. */
$this->app->loadLang('doc');
$lib = new stdclass();
$lib->project = $projectID;
$lib->name = $this->lang->doclib->main['project'];
$lib->type = 'project';
$lib->main = '1';
$lib->acl = $project->acl != 'program' ? $project->acl : 'custom';
$this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
return $projectID;
}
/**
* 计算开始和结束日期中间的工作日数。
* Compute delta between the begin and end date.
*
* @param string begin
* @param string end
* @access public
* @return int
*/
public function computeDaysDelta(string $begin, string $end): int
{
if($end == LONG_TIME) return 0;
$delta = helper::diffDate($end, $begin);
$week = date('w', strtotime($begin));
$weekend = 0;
for($i = 0; $i < $delta; $i++)
{
$week = $week % 7;
if($week == 0 || $week == 6) $weekend ++;
$week ++;
}
return $delta - $weekend;
}
/**
* 替换产品和项目相关联对象的项目集或者产品id。
* Replace program or project id for product and project linked objects.
*
* @param int $programID
* @param int $projectID
* @param int $lineID
* @param array $productIdList
* @param array $projectIdList
* @access public
* @return bool
*/
public function processMergedData(int $programID, int $projectID, int $lineID = 0, array $productIdList = array(), array $sprintIdList = array()): bool
{
if(!$projectID || !$sprintIdList) return false;
if($lineID) $this->dao->update(TABLE_MODULE)->set('root')->eq($programID)->where('id')->eq($lineID)->andWhere('root')->eq('0')->exec();
if(empty($productIdList)) $productIdList = $this->dao->select('*')->from(TABLE_PROJECTPRODUCT)->where('project')->in($sprintIdList)->fetchPairs('product', 'product');
$this->computeProductAcl($productIdList, $programID, $lineID);
$this->upgradeTao->updateProjectByProduct($projectID, $productIdList);
$this->upgradeTao->updateProjectByExecution($projectID, $sprintIdList);
$this->upgradeTao->moveSprintStoryToProject($projectID, $sprintIdList);
$this->upgradeTao->moveSprintCaseToProject($projectID, $sprintIdList);
$this->upgradeTao->moveAllTestTaskToProject($projectID, $sprintIdList);
$this->upgradeTao->syncProjectInfoForSprints($projectID, $sprintIdList, $programID, isset($_POST['projectType']) && $_POST['projectType'] == 'execution');
$this->upgradeTao->setProjectProductsRelation($projectID, $productIdList, $sprintIdList);
return true;
}
/**
* 计算产品的 acl。
* Compute product acl.
*
* @param array $productIdList
* @param int $programID
* @param int|object $lineID
* @access public
* @return void
*/
public function computeProductAcl(array $productIdList = array(), int $programID = 0, mixed $lineID = null): void
{
/* Compute product acl. */
$products = $this->dao->select('id,program,acl')->from(TABLE_PRODUCT)->where('id')->in($productIdList)->fetchAll();
foreach($products as $product)
{
if($product->program) continue; // Skip the product that belongs to a program.
$data = new stdclass();
$data->program = $programID;
$data->acl = $product->acl == 'custom' ? 'private' : $product->acl;
if($lineID !== null) $data->line = $lineID;
$this->dao->update(TABLE_PRODUCT)->data($data)->where('id')->eq($product->id)->exec();
}
}
/**
* 计算项目、产品、迭代的成员。
* Compute project, product and sprint members.
*
* @access public
* @return void
*/
public function computeObjectMembers()
{
/* Update the project members. */
$this->updateProjectMembers();
/* Update the product members. */
$this->loadModel('personnel');
$customProducts = $this->dao->select('id,whitelist,acl')->from(TABLE_PRODUCT)->where('whitelist')->ne('')->fetchAll('id');
$whitelistACL = $this->dao->select('objectID,account')->from(TABLE_ACL)->where('objectID')->in(array_keys($customProducts))->andWhere('objectType')->eq('product')->andWhere('type')->eq('whitelist')->fetchGroup('objectID', 'account');
$groupAccounts = $this->dao->select('`group`,account')->from(TABLE_USERGROUP)->fetchGroup('group', 'account');
foreach($customProducts as $product)
{
if($product->acl != 'private') continue;
$whitelist = array();
foreach(explode(',', $product->whitelist) as $group)
{
foreach(zget($groupAccounts, $group, array()) as $account => $userGroup) $whitelist[$account] = $account;
}
$whitelist += zget($whitelistACL, $product->id, array());
$this->personnel->updateWhitelist(array_keys($whitelist), 'product', $product->id, 'whitelist', 'upgrade', 'increase');
}
/* Update the sprint members. */
$customSprints = $this->dao->select('id,whitelist,acl')->from(TABLE_PROJECT)->where('whitelist')->ne('')->andWhere('type')->in('sprint,stage,kanban')->fetchAll('id');
$whitelistACL = $this->dao->select('objectID,account')->from(TABLE_ACL)->where('objectID')->in(array_keys($customSprints))->andWhere('objectType')->eq('sprint')->andWhere('type')->eq('whitelist')->fetchGroup('objectID', 'account');
foreach($customSprints as $sprint)
{
if($sprint->acl != 'private') continue;
$whitelist = array();
foreach(explode(',', $sprint->whitelist) as $group)
{
foreach(zget($groupAccounts, $group, array()) as $account => $userGroup) $whitelist[$account] = $account;
}
$whitelist += zget($whitelistACL, $sprint->id, array());
$this->personnel->updateWhitelist(array_keys($whitelist), 'sprint', $sprint->id, 'whitelist', 'upgrade', 'increase');
}
}
/**
* 更新项目成员。
* Update project members.
*
* @access protected
* @return void
*/
protected function updateProjectMembers(): void
{
/* Get projects and project teams. */
$sprints = $this->dao->select('id,project,days,PM,multiple')->from(TABLE_PROJECT)->where('type')->eq('sprint')->andWhere('multiple')->eq('0')->fetchAll('project');
$projects = $this->dao->select('id,project,days,PM,multiple')->from(TABLE_PROJECT)->where('type')->eq('project')->fetchAll('id');
$projectTeams = $this->getProjectTeams(array_keys($projects));
$this->app->loadLang('user');
/* Insert product and sprint team into project team. */
$today = helper::today();
$users = $this->dao->select('*')->from(TABLE_USER)->where('deleted')->eq('0')->fetchAll('account');
foreach($projectTeams as $projectID => $projectMember)
{
if(empty($projectMember)) continue;
/* Get project and project members in the project. */
$project = zget($projects, $projectID, '');
$projectMember = array_filter($projectMember);
if(!empty($project) && !isset($projectMember[$project->PM])) $projectMember[$project->PM] = $project->PM; // Append PM to project members if PM isn't among the project members.
/* Update the users in the project's main doc lib. */
$this->dao->update(TABLE_DOCLIB)
->set('users')->eq(trim(implode(',', $projectMember), ','))
->where('project')->eq($projectID)
->andWhere('main')->eq(1)
->exec();
/* Replace the members to project team. */
foreach($projectMember as $account)
{
if(!isset($users[$account])) continue;
$user = $users[$account];
$team = new stdclass();
$team->root = $projectID;
$team->type = 'project';
$team->account = $account;
$team->role = zget($this->lang->user->roleList, $user->role);
$team->join = $today;
$team->days = $project->days;
$team->hours = '7.0';
$this->dao->replace(TABLE_TEAM)->data($team)->exec();
}
if(empty($project->multiple) && !empty($sprints[$project->id]))
{
$sprint = $sprints[$project->id];
foreach($projectMember as $account)
{
if(!isset($users[$account])) continue;
$user = $users[$account];
$team = new stdclass();
$team->root = $sprint->id;
$team->type = 'execution';
$team->account = $account;
$team->role = zget($this->lang->user->roleList, $user->role);
$team->join = $today;
$team->days = $sprint->days;
$team->hours = '7.0';
$this->dao->replace(TABLE_TEAM)->data($team)->exec();
}
}
}
}
/**
* 获取项目的团队。
* Get project teams.
*
* @param array $projectIdList
* @access protected
* @return array
*/
protected function getProjectTeams(array $projectIdList): array
{
$productGroups = $this->dao->select('t1.project,t1.product,t3.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id')
->where('t2.id')->in($projectIdList)
->fetchGroup('project', 'product');
$sprintGroups = $this->dao->select('*')->from(TABLE_PROJECT)->where('project')->in($projectIdList)->fetchGroup('project', 'id');
$teamGroups = $this->dao->select('root,account')->from(TABLE_TEAM)->where('type')->eq('execution')->fetchGroup('root', 'account');
$projectTeams = array();
foreach($projectIdList as $projectID)
{
$teams = array();
$products = zget($productGroups, $projectID, array());
foreach($products as $product)
{
$teams[$product->PO] = $product->PO;
$teams[$product->QD] = $product->QD;
$teams[$product->RD] = $product->RD;
if(isset($product->feedback)) $teams[$product->feedback] = $product->feedback;
}
$sprints = zget($sprintGroups, $projectID, array());
foreach($sprints as $sprint)
{
$teams[$sprint->PO] = $sprint->PO;
$teams[$sprint->PM] = $sprint->PM;
$teams[$sprint->QD] = $sprint->QD;
$teams[$sprint->RD] = $sprint->RD;
if(isset($sprint->feedback)) $teams[$sprint->feedback] = $sprint->feedback;
$sprintTeams = zget($teamGroups, $sprint->id, array());
foreach($sprintTeams as $account => $team) $teams[$account] = $account;
}
$projectTeams[$projectID] = $teams;
}
return $projectTeams;
}
/**
* 合并代码库。
* Merge repo.
*
* @param array $repoes
* @param string $products
* @access public
* @return void
*/
public function mergeRepo(array $repoes, string $products): void
{
foreach($repoes as $repoID) $this->dao->update(TABLE_REPO)->set('product')->eq($products)->where('id')->eq($repoID)->exec();
}
/**
* 设置项目集默认权限。
* Set program default priv.
*
* @access public
* @return void
*/
public function setDefaultPriv(): void
{
$groups = $this->dao->select('id')->from(TABLE_GROUP)->where('role')->ne('limited')->andWhere('role')->ne('projectAdmin')->fetchPairs();
foreach($groups as $groupID)
{
$data = new stdclass();
$data->group = $groupID;
$data->module = 'program';
$data->method = 'pgmindex';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
$data->method = 'prjbrowse';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
$data->method = 'index';
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
/* If is project admin, have all project priv. */
$projectAdminGroupID = $this->dao->select('id')->from(TABLE_GROUP)->where('role')->eq('projectAdmin')->fetch('id');
if(empty($projectAdminGroupID)) return;
$this->app->loadLang('group');
foreach($this->lang->resource->program as $method => $methodLang)
{
$data = new stdclass();
$data->group = $projectAdminGroupID;
$data->module = 'program';
$data->method = $method;
$this->dao->replace(TABLE_GROUPPRIV)->data($data)->exec();
}
}
/**
* Set work to full.
*
* @access public
* @return bool
*/
public function setWork2Full()
{
$this->loadModel('setting')->setItem('system.common.global.flow', 'full');
return true;
}
/**
* Init story sort of plan.
*
* @access public
* @return bool
*/
public function initStoryOfPlan()
{
/* Get all the planned stories and story sort. */
$stories = $this->dao->select('id, plan')->from(TABLE_STORY)->where('plan')->ne(0)->andWhere('plan')->ne('')->orderBy('id_desc')->fetchAll('id');
$planOrder = $this->dao->select('id, `order`')->from(TABLE_PRODUCTPLAN)->where('`order`')->ne('')->fetchAll('id');
/* Organize the stories according to the plan. */
$plans = array();
foreach($stories as $storyID => $story)
{
$planIDList = explode(',', trim($story->plan, ','));
foreach($planIDList as $planID) $plans[$planID][$storyID] = $storyID;
}
foreach($plans as $planID => $storyIDList)
{
/* Order the story according to the plan. */
if(!empty($planOrder[$planID]))
{
$sortIDList = array();
$storySort = explode(',', $planOrder[$planID]->order);
/* Reorder story id list by story order of plan. */
foreach($storySort as $storyID)
{
if(empty($storyID)) continue;
if(!isset($storyIDList[$storyID])) continue;
$sortIDList[$storyID] = $storyID;
unset($storyIDList[$storyID]);
}
if($storyIDList) $sortIDList += $storyIDList;
$storyIDList = $sortIDList;
unset($sortIDList);
}
/* Loop insert sort data by plan. */
$order = 1;
foreach($storyIDList as $storyID)
{
$this->dao->replace(TABLE_PLANSTORY)
->set('plan')->eq($planID)
->set('story')->eq($storyID)
->set('`order`')->eq($order)
->exec();
$order++;
}
}
return true;
}
/**
* Unify the format of the stories and bugs fields in the zt_build table.
*
* @access public
* @return bool
*/
public function processBuildTable()
{
$builds = $this->dao->select('*')->from(TABLE_BUILD)->fetchAll('id', false);
foreach($builds as $build)
{
$data = array();
if(!empty($build->stories) and $build->stories[0] != ',') $data['stories'] = ',' . $build->stories;
if(!empty($build->bugs) and $build->bugs[0] != ',') $data['bugs'] = ',' . $build->bugs;
if($data) $this->dao->update(TABLE_BUILD)->data($data)->where('id')->eq($build->id)->exec();
}
return true;
}
/**
* Adjust the project field of the zt_bug table.
*
* @access public
* @return bool
*/
public function adjustBugOfProject()
{
if($this->config->systemMode != 'new') return true;
$bugs = $this->dao->select('id,execution')->from(TABLE_BUG)->where('execution')->ne('0')->andWhere('project')->eq(0)->fetchPairs('id', 'execution');
$executions = $this->dao->select('id,project')->from(TABLE_EXECUTION)->where('id')->in(array_unique(array_values($bugs)))->fetchPairs('id', 'project');
foreach($bugs as $id => $executionID)
{
if(isset($executions[$executionID])) $this->dao->update(TABLE_BUG)->set('project')->eq($executions[$executionID])->where('id')->eq($id)->exec();
}
return true;
}
/**
* Adjust the whitelist of projects.
*
* @access public
* @return bool
*/
public function adjustWhitelistOfProject()
{
$projects = $this->dao->select('*')->from(TABLE_PROJECT)->where('acl')->eq('custom')->andWhere('type')->eq('sprint')->fetchAll('id', false);
foreach($projects as $project)
{
$groups = explode(',', $project->whitelist);
$accounts = $this->dao->select('account')->from(TABLE_USERGROUP)->where('`group`')->in($groups)->fetchPairs('account');
$whitelist = '';
foreach($accounts as $account)
{
$acl = new stdclass();
$acl->account = $account;
$acl->objectType = $project->type;
$acl->objectID = $project->id;
$acl->type = 'whitelist';
$acl->source = 'upgrade';
$this->dao->insert(TABLE_ACL)->data($acl)->exec();
$whitelist .= ',' . $account;
}
$this->dao->update(TABLE_PROJECT)->set('acl')->eq('private')->set('whitelist')->eq($whitelist)->where('id')->eq($project->id)->exec();
}
return true;
}
/**
* Adjust the whitelist of projects.
*
* @access public
* @return bool
*/
public function adjustWhitelistOfProduct()
{
$products = $this->dao->select('*')->from(TABLE_PRODUCT)->where('acl')->eq('custom')->fetchAll('id', false);
foreach($products as $product)
{
$groups = explode(',', $product->whitelist);
$accounts = $this->dao->select('account')->from(TABLE_USERGROUP)->where('`group`')->in($groups)->fetchPairs('account');
$whitelist = '';
foreach($accounts as $account)
{
$acl = new stdclass();
$acl->account = $account;
$acl->objectType = 'product';
$acl->objectID = $product->id;
$acl->type = 'whitelist';
$acl->source = 'upgrade';
$this->dao->insert(TABLE_ACL)->data($acl)->exec();
$whitelist .= ',' . $account;
}
$this->dao->update(TABLE_PRODUCT)->set('acl')->eq('private')->set('whitelist')->eq($whitelist)->where('id')->eq($product->id)->exec();
}
return true;
}
/**
* Update execution main doclib type.
*
* @access public
* @return bool
*/
public function updateLibType()
{
$executionList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('type')->eq('sprint')->fetchAll('id');
if(empty($executionList)) return true;
$this->dao->update(TABLE_DOCLIB)->set('type')->eq('execution')->where('execution')->in(array_keys($executionList))->exec();
return true;
}
/**
* Update the testtask related cases status.
*
* @access public
* @return bool
*/
public function updateRunCaseStatus()
{
$this->dao->update(TABLE_TESTRUN)->set('status')->eq('normal')->where('status')->in('wait,done')->exec();
return true;
}
/**
* Fix for task link project.
*
* @access public
* @return bool
*/
public function fix4TaskLinkProject()
{
if($this->config->systemMode != 'new') return true;
$executionIdList = $this->dao->select('distinct execution')->from(TABLE_TASK)->where('project')->eq(0)->fetchPairs('execution', 'execution');
$executionPairs = $this->dao->select('id,project')->from(TABLE_PROJECT)->where('id')->in($executionIdList)->andWhere('project')->ne('0')->fetchPairs('id', 'project');
foreach($executionPairs as $executionID => $projectID) $this->dao->update(TABLE_TASK)->set('project')->eq($projectID)->where('execution')->eq($executionID)->exec();
return true;
}
/**
* Fix execution team.
*
* @access public
* @return bool
*/
public function fixExecutionTeam()
{
$errorTeams = $this->dao->select('id,root,account')->from(TABLE_TEAM)->where('type')->eq('')->fetchGroup('root', 'id');
$duplicateTeams = $this->dao->select('root,account')->from(TABLE_TEAM)->where('root')->in(array_keys($errorTeams))->andWhere('type')->ne('')->fetchGroup('root', 'account');
foreach($errorTeams as $root => $teams)
{
if(!isset($duplicateTeams[$root]))
{
$this->dao->update(TABLE_TASK)->set('type')->eq('execution')->where('id')->in(array_keys($teams))->exec();
}
else
{
$existsTeams = $duplicateTeams[$root];
foreach($teams as $team)
{
if(isset($existsTeams[$team->account]))
{
$this->dao->delete()->from(TABLE_TEAM)->where('id')->eq($team->id);
}
else
{
$this->dao->update(TABLE_TASK)->set('type')->eq('execution')->where('id')->in($team->id)->exec();
}
}
}
}
return true;
}
/**
* Update the createdVersion field of the zt_product table.
*
* @access public
* @return void
*/
public function updateProductVersion()
{
$this->dao->update(TABLE_PRODUCT)->set('createdVersion')->eq($this->config->version)->where('createdVersion')->eq('')->andWhere('createdDate')->gt('2020-01-01')->exec();
return true;
}
/**
* Unique projectAdmin group.
*
* @access public
* @return void
*/
public function uniqueProjectAdmin()
{
$projectAdmins = $this->dao->select('*')->from(TABLE_GROUP)->where('role')->eq('projectAdmin')->orderBy('id')->fetchAll('id');
if(count($projectAdmins) <= 1) return true;
$holdGroup = reset($projectAdmins);
unset($projectAdmins[$holdGroup->id]);
$userGroups = $this->dao->select('*')->from(TABLE_USERGROUP)->where('`group`')->in(array_keys($projectAdmins))->fetchGroup('group', 'account');
foreach($userGroups as $groupID => $groups)
{
foreach($groups as $account => $userGroup)
{
$this->dao->delete()->from(TABLE_USERGROUP)->where('`group`')->eq($userGroup->group)->andWhere('account')->eq($userGroup->account)->exec();
$newUserGroup = new stdclass();
$newUserGroup->account = $account;
$newUserGroup->project = $userGroup->project;
$newUserGroup->group = $holdGroup->id;
$this->dao->replace(TABLE_USERGROUP)->data($newUserGroup)->exec();
}
}
$this->dao->delete()->from(TABLE_GROUP)->where('id')->in(array_keys($projectAdmins))->exec();
return true;
}
/**
* Process gitlab repo data.
*
* @access public
* @return bool
*/
public function processGitlabRepo()
{
$repoList = $this->dao->select('*')->from(TABLE_REPO)->where('SCM')->eq('Gitlab')->fetchAll();
foreach($repoList as $repo)
{
if(is_numeric($repo->path)) continue;
/* Create gitlab from repo. */
$gitlab = new stdclass;
$gitlab->type = 'gitlab';
$gitlab->name = $repo->client;
$gitlab->url = $repo->client;
$gitlab->token = $repo->encrypt == 'base64' ? base64_decode($repo->password) : $repo->password;
$gitlab->private = md5(uniqid());
$this->dao->insert(TABLE_PIPELINE)->data($gitlab)->exec();
$gitlabID = $this->dao->lastInsertID();
$this->dao->update(TABLE_REPO)->set('client')->eq($gitlabID)->set('path')->eq($repo->extra)->where('id')->eq($repo->id)->exec();
}
$this->dao->update(TABLE_REPO)->set('prefix')->eq('')->where('SCM')->eq('Gitlab')->exec();
return true;
}
/**
* Process story file type to requirement.
*
* @access public
* @return bool
*/
public function processStoryFileType()
{
$requirementList = $this->dao->select('id')->from(TABLE_STORY)->where('type')->eq('requirement')->fetchPairs('id');
$this->dao->update(TABLE_FILE)->set('objectType')->eq('requirement')
->where('objectID')->in($requirementList)
->andWhere('objectType')->eq('story')
->exec();
return true;
}
/**
* Leave the project field of the product document blank.
*
* @access public
* @return bool
*/
public function processProductDoc()
{
$this->dao->update(TABLE_DOC)->set('project')->eq(0)
->where('product')->ne(0)
->andWhere('project')->ne(0)
->exec();
return true;
}
/**
* Adjust priv 15.3
*
* @access public
* @return bool
*/
public function adjustPriv15_3()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('doc')->andWhere('method')->in('view,objectLibs')->fetchPairs('group', 'group');
foreach($groups as $groupID)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'doc';
$groupPriv->method = 'objectLibs';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
$groupPriv->method = 'tableContents';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
$groupPriv->method = 'showFiles';
$this->dao->replace(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
return true;
}
/**
* Actual finished date of processing testtask.
*
* @access public
* @return bool
*/
public function processTesttaskDate()
{
$this->dao->update(TABLE_TESTTASK)->set("realFinishedDate = end")
->where('status')->eq('done')
->andWhere('realFinishedDate')->eq('0000-00-00 00:00:00')
->exec();
return true;
}
/**
* Store the body of the document in a temporary field.
*
* @access public
* @return bool
*/
public function processDocTempContent()
{
$docContentList = $this->dao->select('doc,content')->from(TABLE_DOCCONTENT)->fetchAll('doc');
foreach($docContentList as $docID => $doc)
{
if(empty($doc->content)) continue;
$this->dao->update(TABLE_DOC)
->set('draft')->eq($doc->content)
->where('id')->eq($docID)
->exec();
}
return true;
}
/**
* Move kanban card data to kanbancell table.
*
* @access public
* @return void
*
*/
public function moveKanbanData()
{
/* Move common kanban data. */
$cards = $this->dao->select('id,kanban,`column`,lane')->from(TABLE_KANBANCARD)->fetchAll('id');
$cellGroup = array();
foreach($cards as $cardID => $card)
{
if(!$card->lane or !$card->column) continue;
$key = $card->kanban . '-' . $card->lane . '-' . $card->column;
$cards = isset($cellGroup[$key]) ? $cellGroup[$key] . "$cardID," : ",$cardID,";
$cellGroup[$key] = $cards;
}
foreach($cellGroup as $key => $cards)
{
$key = explode('-', $key);
if(!is_array($key)) continue;
$cell = new stdclass();
$cell->kanban = $key[0];
$cell->lane = $key[1];
$cell->column = $key[2];
$cell->type = 'common';
$cell->cards = $cards;
$this->dao->insert(TABLE_KANBANCELL)->data($cell)->exec();
}
/* Drop group kanban data. */
$groupLanePairs = $this->dao->select('id')->from(TABLE_KANBANLANE)->where('`groupby`')->ne('')->fetchPairs();
if(!empty($groupLanePairs))
{
$this->dao->delete()->from(TABLE_KANBANLANE)->where('id')->in($groupLanePairs)->exec();
$this->dao->delete()->from(TABLE_KANBANCOLUMN)->where('lane')->in($groupLanePairs)->exec();
}
/* Move execution kanban data. */
$executionKanban = $this->dao->select('t1.id as `lane`, t1.execution, t1.type, t2.id as `column`, t2.cards')->from(TABLE_KANBANLANE)->alias('t1')
->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.id = t2.lane')
->where('t1.execution')->gt(0)
->fetchGroup('lane');
foreach($executionKanban as $laneID => $laneGroup)
{
foreach($laneGroup as $colData)
{
if(!$laneID or !$colData->column) continue;
$cell = new stdclass();
$cell->kanban = $colData->execution;
$cell->lane = $laneID;
$cell->column = $colData->column;
$cell->type = $colData->type;
$cell->cards = $colData->cards;
$this->dao->insert(TABLE_KANBANCELL)->data($cell)->exec();
}
}
}
/**
* Update kanban space team.
*
* @access public
* @return void
*/
public function updateSpaceTeam()
{
$kanbanUsers = $this->dao->select("space, CONCAT(owner, ',', team, ',', whitelist) as users") ->from(TABLE_KANBAN)->fetchAll('space');
$spaceUsers = $this->dao->select("id, CONCAT(owner, ',', team, ',', whitelist) as users")->from(TABLE_KANBANSPACE)->fetchAll('id');
foreach($kanbanUsers as $spaceID => $kanban)
{
$team = zget($spaceUsers, $spaceID)->users;
$team = $team . ',' . $kanban->users;
$team = explode(',', $team);
$team = array_filter($team);
$team = array_unique($team);
$team = implode(',', $team);
$team = trim($team, ',');
$this->dao->update(TABLE_KANBANSPACE)->set('`team`')->eq($team)->where('id')->eq($spaceID)->exec();
}
$this->dao->update(TABLE_KANBANSPACE)->set('`whitelist`')->eq('')->exec();
$this->dao->update(TABLE_KANBAN)->set('`whitelist`')->eq('')->exec();
}
/**
* Adjust for bug required field.
*
* @access public
* @return bool
*/
public function adjustBugRequired()
{
$data = $this->dao->select('*')->from(TABLE_CONFIG)
->where('owner')->eq('system')
->andWhere('module')->eq('bug')
->andWhere('section')->eq('create')
->andWhere('`key`')->eq('requiredFields')
->fetch();
if(empty($data)) return true;
$data->value = ',' . $data->value . ',';
$data->value = str_replace(',project,', ',', $data->value);
$this->dao->update(TABLE_CONFIG)->set('`value`')->eq(trim($data->value, ','))->where('id')->eq($data->id)->exec();
return true;
}
/**
* Update branch when object have module.
*
* @access public
* @return bool
*/
public function updateObjectBranch()
{
$moduleBranchPairs = $this->dao->select('id,branch')->from(TABLE_MODULE)->where('branch')->ne(0)->fetchPairs();
if(empty($moduleBranchPairs)) return true;
$storyModulePairs = $this->dao->select('module')->from(TABLE_STORY)->where('module')->in(array_keys($moduleBranchPairs))->andWhere('branch')->eq(0)->fetchPairs();
foreach($storyModulePairs as $moduleID)
{
if(!isset($moduleBranchPairs[$moduleID])) continue;
$this->dao->update(TABLE_STORY)->set('`branch`')->eq($moduleBranchPairs[$moduleID])->where('module')->eq($moduleID)->exec();
}
$bugModulePairs = $this->dao->select('module')->from(TABLE_BUG)->where('module')->in(array_keys($moduleBranchPairs))->andWhere('branch')->eq(0)->fetchPairs();
foreach($bugModulePairs as $moduleID)
{
if(!isset($moduleBranchPairs[$moduleID])) continue;
$this->dao->update(TABLE_BUG)->set('`branch`')->eq($moduleBranchPairs[$moduleID])->where('module')->eq($moduleID)->exec();
}
$caseModulePairs = $this->dao->select('module')->from(TABLE_CASE)->where('module')->in(array_keys($moduleBranchPairs))->andWhere('branch')->eq(0)->fetchPairs();
foreach($caseModulePairs as $moduleID)
{
if(!isset($moduleBranchPairs[$moduleID])) continue;
$this->dao->update(TABLE_CASE)->set('`branch`')->eq($moduleBranchPairs[$moduleID])->where('module')->eq($moduleID)->exec();
}
return true;
}
/**
* Update branch of project linked stories.
*
* @access public
* @return bool
*/
public function updateProjectStories()
{
$storyPairs = $this->dao->select('t1.story, t2.branch')->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id')
->fetchPairs();
foreach($storyPairs as $storyID => $branch)
{
$this->dao->update(TABLE_PROJECTSTORY)->set('branch')->eq($branch)->where('story')->eq($storyID)->exec();
}
return true;
}
/**
* Update project linked branch.
*
* @access public
* @return void
*/
public function updateProjectLinkedBranch()
{
$projectProducts = $this->dao->select('t1.project,t1.story,t2.branch,t2.product')->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id')
->where('t2.branch')->ne(0)
->fetchGroup('project');
$projectBranches = array();
foreach($projectProducts as $projectID => $stories)
{
foreach($stories as $story)
{
if(!isset($projectBranches[$projectID])) $projectBranches[$projectID] = array();
if(!isset($projectBranches[$projectID][$story->product])) $projectBranches[$projectID][$story->product] = array();
$projectBranches[$projectID][$story->product][$story->branch] = $story->branch;
}
}
foreach($projectBranches as $projectID => $products)
{
foreach($products as $productID => $branches)
{
foreach($branches as $branchID)
{
$data = new stdClass();
$data->project = $projectID;
$data->product = $productID;
$data->branch = $branchID;
$this->dao->replace(TABLE_PROJECTPRODUCT)->data($data)->exec();
}
}
}
return true;
}
/**
* Document library for updating documents.
*
* @access public
* @return bool
*/
public function updateDocField()
{
$hasModuleDocs = $this->dao->select('*')->from(TABLE_DOC)->where('module')->ne(0)->fetchAll('id');
$docModules = $this->dao->select('*')->from(TABLE_MODULE)->where('type')->eq('doc')->fetchAll('id');
foreach($hasModuleDocs as $doc)
{
$libID = isset($docModules[$doc->module]->root) ? $docModules[$doc->module]->root : 0;
if(!$libID or $libID == $doc->lib) continue;
$this->dao->update(TABLE_DOC)->set('lib')->eq($libID)->where('id')->eq($doc->id)->exec();
}
return true;
}
/**
* Update activated date by history action.
*
* @access public
* @return bool
*/
public function updateActivatedDate()
{
$actions = $this->dao->select('objectID, objectType, max(date) as date')->from(TABLE_ACTION)->where('action')->eq('activated')->andWhere('objectType')->in('story, task, bug')->groupBy('objectID, objectType')->fetchAll();
foreach($actions as $action)
{
$table = TABLE_BUG;
if($action->objectType == 'story') $table = TABLE_STORY;
if($action->objectType == 'task') $table = TABLE_TASK;
$this->dao->update($table)->set('activatedDate')->eq($action->date)->where('id')->eq($action->objectID)->exec();
}
return true;
}
/**
* Update group date for lite.
*
* @access public
* @return bool
* */
public function updateGroup4Lite()
{
$privTable = $this->config->db->prefix . 'grouppriv';
$adminPrivs = $this->dao->select('module,method')->from($privTable)->where('`group`')->eq('1')->andWhere('module')->notin($this->config->upgrade->unsetModules)->fetchAll();
$pmPrivs = $this->dao->select('module,method')->from($privTable)->where('`group`')->eq('4')->andWhere('module')->notin($this->config->upgrade->unsetModules)->fetchAll();
$topPrivs = $this->dao->select('module,method')->from($privTable)->where('`group`')->eq('9')->andWhere('module')->notin($this->config->upgrade->unsetModules)->fetchAll();
$liteGroup = $this->dao->select('*')->from(TABLE_GROUP)->where('vision')->eq('lite')->fetchAll();
$sql = 'REPLACE INTO ' . TABLE_GROUPPRIV . ' VALUES ';
foreach($liteGroup as $group)
{
if($group->role == 'liteAdmin' and !empty($adminPrivs))
{
foreach($adminPrivs as $priv)
{
$sql .= "($group->id, ";
$sql .= "'$priv->module', ";
$sql .= "'$priv->method'), ";
}
}
if($group->role == 'liteProject' and !empty($pmPrivs))
{
foreach($pmPrivs as $priv)
{
$sql .= "($group->id, ";
$sql .= "'$priv->module', ";
$sql .= "'$priv->method'), ";
}
}
if($group->role == 'liteTeam' and !empty($topPrivs))
{
foreach($topPrivs as $priv)
{
$sql .= "($group->id, ";
$sql .= "'$priv->module', ";
$sql .= "'$priv->method'), ";
}
}
}
$sql = rtrim($sql, ', ') . ';';
$this->dao->exec($sql);
return true;
}
/**
* 获取需要移动的扩展文件。
* Gets the extension file to be moved.
*
* @access public
* @return array
*/
public function getExtFiles(): array
{
/* Get all modules and skip modules. */
$files = array();
$allModules = glob($this->app->moduleRoot . '*');
$skipModules = $this->getEncryptedModules($allModules);
foreach($allModules as $modulePath)
{
$module = basename($modulePath);
if(in_array($module, $skipModules)) continue; // If the module is in the skip modules, skip it.
/* Get directory root path, and directories in the directory root path. */
$dirRoot = in_array($module, $this->config->upgrade->openModules) ? $modulePath . DS . 'ext' : $modulePath;
$dirs = glob($dirRoot . DS . '*');
/* Get plugin files in the directories. */
foreach($dirs as $dirPath)
{
$dir = basename($dirPath);
$realPath = is_file($dirPath) ? $dirRoot : $dirPath;
$path = in_array($module, $this->config->upgrade->openModules) ? $module . DS . 'ext' . DS . $dir : $module . DS . $dir;
if(is_dir($realPath))
{
$files += $this->getPluginFiles($module, $dir, $realPath, $path);
}
}
}
return $files;
}
/**
* 获取非开源版的模块。
* Get modules that are not open source.
*
* @param array $allModules
* @access public
* @return array
*/
public function getEncryptedModules(array $allModules): array
{
$encryptModules = array();
foreach($allModules as $modulePath)
{
$customFiles = array();
$module = basename($modulePath);
if(in_array($module, $this->config->upgrade->openModules))
{
$extRoot = $modulePath . DS . 'ext';
if(!is_dir($extRoot))
{
/* If the ext directory doesn't exist, append it to encryptModules. */
$encryptModules[] = $module;
continue;
}
else
{
/* Get custom files in the control and model directories. */
foreach(array('control', 'model') as $dir)
{
$realPath = $extRoot . DS . $dir;
$path = $module . DS . 'ext' . DS . $dir;
if(!is_dir($realPath)) continue;
$customFiles += $this->getPluginFiles($module, $dir, $realPath, $path);
}
}
}
else
{
/* Check whether control and model are custom files. */
foreach(array('control.php', 'model.php') as $file)
{
$filePath = $modulePath . DS . $file;
if(!is_file($filePath)) continue;
$customFiles += $this->getPluginFiles($module, $file, $modulePath, $module);
}
}
/* If the customFiles is empty or the module is owt, append it to encryptModules. */
if(empty($customFiles) || $module == 'owt') $encryptModules[$module] = $module;
}
return $encryptModules;
}
/**
* 获取插件文件.
* Get plugin files.
*
* @param string $module
* @param string $dir
* @param string $realPath
* @param string $path
* @access public
* @return array
*/
public function getPluginFiles(string $module, string $dir, string $realPath, string $path): array
{
$pluginFiles = array();
$files = is_file($realPath . DS . $dir) ? array($dir) : glob($realPath . DS . '*');
foreach($files as $file)
{
$file = basename($file);
$filePath = $realPath . DS . $file;
$fileName = is_file($realPath . DS . $dir) ? $path : $path . DS . $file;
if(is_dir($filePath))
{
/* If you are currently pointing to a directory, the files in that directory will be traversed. */
$pluginFiles += $this->getPluginFiles($module, $dir, $filePath, $fileName);
}
else
{
/* Get first line in the file. */
$handle = fopen($filePath, 'r');
$line = fgets($handle);
fclose($handle);
/* If the current file isn't encrypted and the file isn't the system file, append it to pluginFiles. */
if(is_string($line) && strpos($line, '<?php //') === false)
{
$systemFiles = file_get_contents('systemfiles.txt');
$systemFiles = str_replace('/', DS, $systemFiles);
if(strpos($systemFiles, ",{$fileName},") !== false) continue;
$pluginFiles[$fileName] = $fileName;
}
}
}
return $pluginFiles;
}
/**
* 获取自定义的模块。
* Get custom modules.
*
* @param array $allModules
* @access public
* @return array
*/
public function getCustomModules(array $allModules): array
{
/* Get system files. */
$systemFiles = file_get_contents($this->app->getModuleRoot() . 'upgrade/systemfiles.txt');
$systemFiles = str_replace('/', DS, $systemFiles);
$customModules = array();
foreach($allModules as $modulePath)
{
$module = basename($modulePath);
/* If the module isn't the open module and doesn't exist in the system files, it's the custom module. */
if(!in_array($module, $this->config->upgrade->openModules) && !preg_match("#{$module}(/[a-z]*)*(/[a-z]+.[a-z]+)+#", $systemFiles)) $customModules[$module] = $module;
}
return $customModules;
}
/**
* 移动扩展文件。
* Move extension files.
*
* @access public
* @return array
*/
public function moveExtFiles(): array
{
/* Get files and custom root directory, set response. */
$data = fixer::input('post')->get();
$customRoot = $this->app->appRoot . 'extension' . DS . 'custom';
$response = array('result' => 'success');
foreach($data->files as $file => $check)
{
if(!preg_match('/[a-z]/', $file[0])) continue;
$fromPath = $this->app->getModuleRoot() . $file;
if(!is_file($fromPath)) continue;
$dirRoot = $customRoot . DS . dirname($file);
$toPath = $dirRoot . DS . basename($file);
/* If the directory doesn't exist and there is no permission to create the directory, set the response and return it. */
if(!is_dir($dirRoot))
{
if(!mkdir($dirRoot, 0777, true))
{
$response['result'] = 'fail';
$response['command'] = 'chmod o=rwx -R '. $this->app->appRoot . 'extension/custom';
return $response;
}
}
/* Copy from-path to to-path, and replace the load path in the to-path files. */
copy($fromPath, $toPath);
$this->replaceIncludePath($toPath);
}
return $response;
}
/**
* 移除收费版本的目录。
* Remove encrypted directories.
*
* @param string $script
* @access public
* @return array
*/
public function removeEncryptedDir(string $script): string
{
$dir = dirname($script);
if(!is_writable($dir)) return "chmod 777 {$dir}";
$command = [];
$zfile = $this->app->loadClass('zfile');
$allModules = glob($this->app->moduleRoot . '*');
$skipModules = $this->getEncryptedModules($allModules);
$customModules = $this->getCustomModules($allModules);
$modules = $skipModules + $customModules;
foreach($modules as $module)
{
if(in_array($module, $this->config->upgrade->openModules)) continue; // If the module is open module, skip it.
$dirPath = $this->app->moduleRoot . $module;
if(!$zfile->removeDir($dirPath)) $command[] = 'rm -f -r ' . $dirPath; // If the directory can't be removed, append the command for deleting a directory.
}
if(!$command) return '';
asort($command);
$content = "#!/bin/bash\n" . implode("\n", $command);
file_put_contents($script, $content);
return "/bin/bash $script";
}
/**
* 替换文件的加载路径。
* Replace the load path of the file.
*
* @param string $filePath
* @access public
* @return void
*/
public function replaceIncludePath(string $filePath): void
{
$content = file_get_contents($filePath); // Get file contents.
if(strpos(basename($filePath), 'html')) // If the file type is html.
{
/**
* Perform a regular expression search and replace.
* eg: `include '../../../common/view/header.lite.html.php'` to `include $app->getModuleRoot() . 'common/view/header.lite.html.php'`
*/
$content = preg_replace('#(include )(\'|")((../){2,})([a-z]+/)(?!ext/)([a-z]+/)#', '$1' . '$app->getModuleRoot() . ' . '$2$5$6', $content);
/* Get system files. */
$systemFiles = file_get_contents('systemfiles.txt');
$systemFiles = str_replace('/', DS, $systemFiles);
/* Perform a global regular expression match. eg: `include '../../../common/ext/view/calendar.html.php'` */
preg_match_all('#(include )(\'|")(../){2,}[a-z]+/ext/[a-z]+/(([a-z]+[.]?)+)#', $content, $matches);
foreach($matches[0] as $fileName)
{
/* Get file name. eg: `include '../../../common/ext/view/calendar.html.php'` to `common/ext/view/calendar.html.php` as file name. */
$fileName = preg_replace("#(include )('|\")((../){2,})#", "", $fileName);
if(strpos($systemFiles, $fileName) !== false)
{
/* If the file is the system file, replace its include path. eg: `include '../../../common/ext/view/calendar.html.php'` to `include $app->appRoot . 'extension/max/common/ext/view/calendar.html.php'`. */
$fileName = basename($fileName);
$content = preg_replace('#(include )(\'|")((../){2,})([a-z]+/ext/view/' . $fileName . ')#', '$1' . '$app->appRoot . ' . '$2extension/max/$5', $content);
}
else
{
/* If the file isn't the system file, replace its include path. eg: `include '../../../common/ext/view/calendar.html.php'` to `include $app->appRoot . 'extension/custom/common/ext/view/calendar.html.php'`. */
$fileName = basename($fileName);
$content = preg_replace('#(include )(\'|")((../){2,})([a-z]+/ext/view/' . $fileName . ')#', '$1' . '$app->appRoot . ' . '$2extension/custom/$5', $content);
}
}
}
else // If the file type is php.
{
/* Get module name. */
$dirPath = dirname($filePath);
$dir = str_replace($this->app->appRoot . 'extension' . DS . 'custom' .DS , '', $dirPath);
$dirList = explode(DS, $dir);
$moduleName = $dirList[0];
/* Replace the include path of the control. */
$content = str_replace("include '../../control.php';", "helper::importControl('$moduleName');", $content);
$content = str_replace("helper::import('../../control.php');", "helper::importControl('$moduleName');", $content);
$content = str_replace('helper::import(dirname(dirname(dirname(__FILE__))) . "/control.php");', "helper::importControl('$moduleName');", $content);
}
file_put_contents($filePath, $content); // Save the file.
}
/**
* Add groups default kanban private
*
* @access public
* @return void
*/
public function addDefaultKanbanPri()
{
/* Fix bug #17954. */
$hasKanbanPri = $this->dao->select('*')->from(TABLE_GROUPPRIV)->where('module')->eq('kanban')->fetch();
if(!$hasKanbanPri)
{
$this->app->loadLang('group');
$groups = $this->dao->select('id')->from(TABLE_GROUP)->where('role')->in('admin,pm,po')->fetchPairs('id');
foreach($groups as $groupID)
{
foreach($this->lang->resource->kanban as $method => $name)
{
if(stripos($method, 'delete') === false)
{
$groupPriv = new stdclass();
$groupPriv->group = $groupID;
$groupPriv->module = 'kanban';
$groupPriv->method = $method;
$this->dao->insert(TABLE_GROUPPRIV)->data($groupPriv)->exec();
}
}
}
}
}
/**
* Update the story reviewer when 12 version to 15.
*
* @access public
* @return void
*/
public function updateStoryReviewer()
{
$isOldVersion = false;
$fromVersion = str_replace('_', '.', $this->fromVersion);
if(is_numeric($fromVersion[0]) and version_compare($fromVersion, '12.0.0', '<='))
{
$isOldVersion = true;
}
elseif($fromVersion[0] == 'p' and version_compare($fromVersion, 'pro9.0.3', '<='))
{
$isOldVersion = true;
}
elseif($fromVersion[0] == 'b' and version_compare($fromVersion, 'biz4.1.3', '<='))
{
$isOldVersion = true;
}
if(!$isOldVersion) return;
$stories = $this->dao->select('t1.*,t2.PO,t2.createdBy')->from(TABLE_STORY)->alias('t1')
->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
->where('t1.deleted')->eq('0')
->andWhere('t2.deleted')->eq('0')
->andWhere('t1.status')->in('draft,changed')
->fetchAll('id');
foreach($stories as $storyID => $story)
{
if(!empty($story->assignedTo))
{
$story->reviewer = $story->assignedTo;
}
elseif(!empty($story->PO))
{
$story->reviewer = $story->PO;
}
else
{
$story->reviewer = $story->createdBy;
}
$data = new stdclass();
$data->story = $storyID;
$data->version = $story->version;
$data->reviewer = $story->reviewer;
$data->result = '';
$data->reviewDate = '';
$this->dao->insert(TABLE_STORYREVIEW)->data($data)->exec();
}
}
/**
* Update the project status.
*
* @access public
* @return void
*/
public function updateProjectStatus()
{
$projects = $this->dao->select('id')->from(TABLE_PROJECT)
->where('deleted')->eq('0')
->andWhere('status')->eq('doing')
->andWhere('type')->eq('project')
->andWhere('realBegan')->eq('0000-00-00')
->fetchAll('id');
if(empty($projects)) return;
$projectIDList = array_keys($projects);
$executions = $this->dao->select('id,project,min(realBegan) as minBegan')->from(TABLE_EXECUTION)
->where('status')->eq('doing')
->andWhere('deleted')->eq('0')
->andWhere('project')->in($projectIDList)
->groupBy('project')
->fetchAll();
if(empty($executions)) return;
foreach ($executions as $execution) $this->dao->update(TABLE_PROJECT)->set('realBegan')->eq($execution->minBegan)->where('id')->eq($execution->project)->exec();
}
/**
* Change story need Review.
*
* @access public
* @return void
*/
public function changeStoryNeedReview()
{
$this->loadModel('story');
$this->loadModel('setting');
$rndNeedReview = $this->setting->getItem('owner=system&vision=rnd&module=story&section=&key=needReview');
$liteNeedReview = $this->setting->getItem('owner=system&vision=lite&module=story&section=&key=needReview');
$data = new stdclass();
$data->forceReview = '';
$data->forceReviewDepts = '';
$data->forceReviewRoles = '';
if(!empty($rndNeedReview)) $this->setting->setItems("system.story@rnd", $data);
if(!empty($liteNeedReview)) $this->setting->setItems("system.story@lite", $data);
$this->setting->deleteItems('owner=system&module=story&section=&key=forceReviewAll');
}
/**
* The setlane permission is deleted. We need to replace setlane with editlanename and editlanecolor.
*
* @access public
* @return void
*/
public function replaceSetLanePriv()
{
$groupIDList = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('kanban')
->andWhere('method')->eq('setLane')
->fetchAll();
if(!empty($groupIDList))
{
$this->dao->delete()->from(TABLE_GROUPPRIV)
->where('module')->eq('kanban')
->andWhere('method')->eq('setLane')
->exec();
}
foreach($groupIDList as $groupID)
{
$data = new stdClass();
$data->group = $groupID->group;
$data->module = 'kanban';
$data->method = 'editLaneName';
$this->dao->insert(TABLE_GROUPPRIV)->data($data)->exec();
$data->method = 'editLaneColor';
$this->dao->insert(TABLE_GROUPPRIV)->data($data)->exec();
}
return true;
}
/**
* Update path and grade of program, project and execution.
*
* @access public
* @return bool
*/
public function updateProjectData()
{
/* Process programs. */
$programs = $this->dao->select('id,parent,grade,path')->from(TABLE_PROJECT)->where('type')->eq('program')->orderBy('parent_asc')->fetchAll('id');
foreach($programs as $program)
{
if(!$program->parent)
{
$program->grade = 1;
$program->path = ",$program->id,";
}
else
{
$program->grade = $programs[$program->parent]->grade + 1;
$program->path = $programs[$program->parent]->path . "$program->id,";
}
$this->dao->update(TABLE_PROGRAM)
->set('path')->eq($program->path)
->set('grade')->eq($program->grade)
->where('id')->eq($program->id)->exec();
}
/* Process projects. */
$projects = $this->dao->select('id,project,parent,grade,path')->from(TABLE_PROJECT)->where('type')->eq('project')->orderBy('parent_asc')->fetchAll('id');
foreach($projects as $project)
{
if(!$project->parent)
{
$project->grade = 1;
$project->path = ",$project->id,";
}
else
{
$project->grade = $programs[$project->parent]->grade + 1;
$project->path = $programs[$project->parent]->path . "$project->id,";
}
$this->dao->update(TABLE_PROJECT)
->set('path')->eq($project->path)
->set('grade')->eq($project->grade)
->where('id')->eq($project->id)->exec();
}
/* Process executions. */
$sprints = $this->dao->select('id,project,parent,grade,path')->from(TABLE_PROJECT)
->where('type')->ne('project')
->andWhere('type')->ne('program')
->orderBy('parent_asc')->fetchAll('id');
foreach($sprints as $sprint)
{
if($sprint->parent == $sprint->project)
{
$sprint->grade = 1;
$sprint->path = ",$sprint->project,$sprint->id,";
}
else
{
$sprint->grade = 2;
$sprint->path = $sprints[$sprint->parent]->path . "$sprint->id,";
}
$this->dao->update(TABLE_EXECUTION)
->set('path')->eq($sprint->path)
->set('grade')->eq($sprint->grade)
->where('id')->eq($sprint->id)->exec();
}
return true;
}
/**
* Move project admins to new table.
*
* @access public
* @return void
*/
public function moveProjectAdmins()
{
$adminGroupID = $this->dao->select('id')->from(TABLE_GROUP)->where('role')->eq('projectAdmin')->fetch('id');
$projectAdmins = $this->dao->select('account, project')->from(TABLE_USERGROUP)->where('`group`')->eq($adminGroupID)->fetchPairs();
$i = 1;
foreach($projectAdmins as $account => $projects)
{
if(!$account or !$projects) continue;
$data = new stdclass();
$data->group = $i;
$data->account = $account;
$data->projects = $projects;
$this->dao->replace(TABLE_PROJECTADMIN)->data($data)->exec();
$i ++;
}
$this->dao->delete()->from(TABLE_USERGROUP)->where('`group`')->eq($adminGroupID)->exec();
}
/*
* Insert story view of execution.
*
* @access public
* @return bool
*/
public function addStoryViewPriv()
{
$groupIdList = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('story')
->andWhere('method')->eq('view')
->fetchPairs('group');
foreach($groupIdList as $groupID)
{
$this->dao->replace(TABLE_GROUPPRIV)
->set('`group`')->eq($groupID)
->set('module')->eq('execution')
->set('method')->eq('storyView')
->exec();
}
return true;
}
/*
* Add review issue approval data.
*
* @access public
* @return bool
*/
public function addReviewIssusApprovalData()
{
$reviewIssues = $this->dao->select('id,review,type')->from(TABLE_REVIEWISSUE)
->where('type')->eq('review')
->andWhere('approval')->eq(0)
->andWhere('deleted')->eq('0')
->fetchAll('review');
if(empty($reviewIssues)) return false;
$reviewIds = array_unique(helper::arrayColumn($reviewIssues, 'review'));
$approvalsPairs = $this->dao->select('objectID, max(id) as approval')->from(TABLE_APPROVAL)
->where('objectID')->in($reviewIds)
->andWhere('objectType')->eq('review')
->andWhere('result')->eq('fail')
->andWhere('deleted')->eq(0)
->groupBy('objectID')
->fetchAll('objectID');
/* Add approval data. */
foreach($reviewIssues as $reviewIssue)
{
if(!isset($approvalsPairs[$reviewIssue->review]->approval)) continue;
$this->dao->update(TABLE_REVIEWISSUE)
->set('approval')->eq($approvalsPairs[$reviewIssue->review]->approval)
->where('review')->eq($reviewIssue->review)
->andWhere('type')->eq('review')
->andWhere('approval')->eq(0)
->andWhere('deleted')->eq('0')
->exec();
}
}
/**
* Xuan: Add `index` column to all message partition tables.
*
* @access public
* @return bool
*/
public function xuanAddMessageIndexColumns()
{
$prefix = $this->config->db->prefix;
$tables = $this->dbh->query("SHOW TABLES LIKE '{$prefix}im_message\_%'")->fetchAll();
$tables = array_filter(array_map(function($table) use ($prefix)
{
$tableName = current(array_values((array)$table));
if(!preg_match("/{$prefix}im_message_[a-z]+/", $tableName)) return $tableName;
},
$tables
));
if(empty($tables)) return true;
$query = '';
foreach($tables as $table) $query .= "ALTER TABLE `$table` ADD `index` int(11) unsigned DEFAULT 0 AFTER `date`;";
$this->dbh->query($query);
return !dao::isError();
}
/**
* Xuan: Re-index messages.
*
* @access public
* @return bool
*/
public function xuanReindexMessages()
{
/** @var array[] $chatTablePairs Associations of chats and partition tables, without main table. */
$chatTablePairs = array();
ini_set('memory_limit', '-1');
set_time_limit(0);
/* Fetch chat and message partition table associations. */
$chatTableData = $this->dao->select('gid,tableName')->from(TABLE_IM_CHAT_MESSAGE_INDEX)->orderBy('id_asc')->fetchAll();
foreach($chatTableData as $chatTable)
{
if(isset($chatTablePairs[$chatTable->gid]))
{
$chatTablePairs[$chatTable->gid][] = $chatTable->tableName;
continue;
}
$chatTablePairs[$chatTable->gid] = array($chatTable->tableName);
}
/* Append all non-partitioned chats. */
$allChats = $this->dao->select('gid')->from(TABLE_IM_CHAT)->fetchPairs();
$nonPartitionedChats = array_diff(array_values($allChats), array_keys($chatTablePairs));
foreach($nonPartitionedChats as $chat) $chatTablePairs[$chat] = array();
/* Do index. */
foreach($chatTablePairs as $chat => $tables)
{
$result = $this->xuanDoIndex($chat, $tables);
if(!$result) return false;
}
return true;
}
/**
* Xuan: Index messages of chat in partition tables and main table.
*
* @param string $chat
* @param array $tables
* @return bool
*/
public function xuanDoIndex($chat, $tables)
{
$messageIndex = 0;
$tables[] = str_replace('`', '', TABLE_IM_MESSAGE);
foreach($tables as $table)
{
$idIndices = array();
$ids = $this->dao->select('id')->from("`$table`")->where('cgid')->eq($chat)->fetchAll('id');
$ids = array_keys($ids);
if(empty($ids)) continue;
for($index = 1; $index <= count($ids); $index++) $idIndices[$ids[$index - 1]] = $index + $messageIndex;
$queryData = array();
foreach($idIndices as $id => $index) $queryData[] = "WHEN $id THEN $index";
$query = "UPDATE `$table` SET `index` = (CASE `id` " . implode(' ', $queryData) . " END) WHERE `id` IN(" . implode(',', $ids) . ");";
$this->dao->query($query);
$messageIndex = max(array_values($idIndices));
}
$this->dao->update(TABLE_IM_CHAT)->set('lastMessageIndex')->eq($messageIndex)->where('gid')->eq($chat)->exec();
return !dao::isError();
}
/**
* Xuan: Set lastReadMessageIndex into table im_chatuser.
*
* @access public
* @return bool
*/
public function xuanUpdateLastReadMessageIndex()
{
$lastReadMessages = $this->dao->select('lastReadMessage')->from(TABLE_IM_CHATUSER)->where('lastReadMessage')->ne(0)->fetchAll('lastReadMessage');
$lastReadMessages = array_keys($lastReadMessages);
if(empty($lastReadMessages)) return true;
ini_set('memory_limit', '1024M');
set_time_limit(0);
$messages = $this->loadModel('im')->messageGetList('', $lastReadMessages, null, '', '', false);
if(empty($messages)) return;
$queryData = array();
foreach($messages as $message) $queryData[] = "WHEN {$message->id} THEN {$message->index}";
$query = "UPDATE " . TABLE_IM_CHATUSER . " SET `lastReadMessageIndex` = (CASE `lastReadMessage` " . implode(' ', $queryData) . " END) WHERE `id` IN(" . implode(',', $lastReadMessages) . ");";
$this->dao->query($query);
return !dao::isError();
}
/**
* Xuan: Fix chats without lastReadMessage.
*
* @access public
* @return bool
*/
public function xuanFixChatsWithoutLastRead()
{
$zeroLastReadChats = $this->dao->select('cgid')->from(TABLE_IM_CHATUSER)->where('lastReadMessage')->eq(0)->fetchAll('cgid');
$zeroLastReadChats = array_keys($zeroLastReadChats);
if(empty($zeroLastReadChats)) return true;
ini_set('memory_limit', '1024M');
set_time_limit(0);
$lastMessages = $this->dao->select('MAX(`index`), cgid')->from(TABLE_IM_MESSAGE)->where('cgid')->in($zeroLastReadChats)->groupBy('cgid')->fetchAll('cgid');
if(empty($lastMessages)) return true;
$maxIndex = 'MAX(`index`)';
$queryData = array();
foreach($lastMessages as $cgid => $lastMessage) $queryData[] = "WHEN '{$cgid}' THEN {$lastMessage->$maxIndex}";
$query = "UPDATE " . TABLE_IM_CHATUSER . " SET `lastReadMessageIndex` = (CASE `cgid` " . implode(' ', $queryData) . " END) WHERE `cgid` IN('" . implode("','", array_keys($lastMessages)) . "');";
$this->dao->query($query);
return !dao::isError();
}
/**
* Process bug link bug.
*
* @access public
* @return void
*/
public function processBugLinkBug()
{
$bugs = $this->dao->select('id,linkBug')->from(TABLE_BUG)->where('linkBug')->ne('')->fetchPairs();
foreach($bugs as $bugID => $linkBugs)
{
$linkBugs = explode(',', $linkBugs);
$this->dao->update(TABLE_BUG)->set("linkBug = TRIM(BOTH ',' from CONCAT(linkBug, ',$bugID'))")->where('id')->in($linkBugs)->andWhere('id')->ne($bugID)->andWhere("CONCAT(',', linkBug, ',')")->notlike("%,$bugID,%")->exec();
}
return !dao::isError();
}
/**
* Process created information.
*
* @access public
* @return void
*/
public function processCreatedInfo()
{
$objectTypes = array('productplan', 'release', 'testtask', 'build');
$actions = $this->dao->select('objectType, objectID, actor, date')->from(TABLE_ACTION)->where('objectType')->in($objectTypes)->andWhere('action')->eq('opened')->fetchGroup('objectType');
foreach($actions as $objectType => $objectActions)
{
foreach($objectActions as $action)
{
$this->dao->update($this->config->objectTables[$objectType])->set('createdBy')->eq($action->actor)->set('createdDate')->eq($action->date)->where('id')->eq($action->objectID)->exec();
}
}
return !dao::isError();
}
/**
* Update approval process in Workflow.
*
* @access public
* @return void
*/
public function updateApproval()
{
/* Judge whether the action has opened the approval process before. */
/* 判断动作 看之前是否开启过审批流 */
$actions = $this->dao->select('id, module, action, createdDate')->from(TABLE_WORKFLOWACTION)
->where('role')->eq('approval')
->andWhere('action')->in('submit,cancel,review')
->fetchAll('id');
foreach($actions as $id => $action)
{
$module = $action->module;
$actionCode = $action->action;
$this->dao->update(TABLE_WORKFLOWACTION)->set('action')->eq('approval' . $action->action)->where('id')->eq($id)->exec();
/* Change the approval action of the module that has already enabled the approval function */
/* 改原来已经开启过审批功能的模块的审批动作 */
if(isset($this->config->upgrade->recoveryActions->{$module}->{$actionCode}))
{
$data = array_merge($this->config->upgrade->defaultActions, $this->config->upgrade->recoveryActions->{$module}->{$actionCode});
if(isset($data['hasLite']) && $data['hasLite'] === true)
{
unset($data['hasLite']);
$liteData = $data;
$liteData['vision'] = 'lite';
$this->dao->insert(TABLE_WORKFLOWACTION)->data($liteData)->exec();
}
$this->dao->insert(TABLE_WORKFLOWACTION)->data($data)->exec();
}
/* Change history */
/* 改历史记录 */
$this->dao->update(TABLE_ACTION)->set('action')->eq('approval' . $action->action)->where('objectType')->eq($module)->andWhere('action')->eq($action->action)->andWhere('date')->gt($action->createdDate)->exec();
/* Change the action field of the workflowlayout table */
/* 改workflowlayout表的action字段 */
$this->dao->update(TABLE_WORKFLOWLAYOUT)->set('action')->eq('approval' . $action->action)->where('module')->eq($module)->andWhere('action')->eq($action->action)->exec();
}
return !dao::isError();
}
/**
* Process createdBy of conditions.
*
* @access public
* @return bool
*/
public function processCreatedBy()
{
$this->app->loadLang('workflow');
$this->app->loadModuleConfig('workflow');
$modules = $this->dao->select('module')->from(TABLE_WORKFLOW)->where('buildin')->eq('1')->andWhere('approval')->eq('enabled')->andWhere('module')->in(array_keys($this->config->workflow->buildin->createdBy))->fetchPairs();
$actions = $this->dao->select('id, module, conditions')->from(TABLE_WORKFLOWACTION)->where('module')->in($modules)->andWhere('action')->in('submit, cancel, edit, delete')->fetchAll('id');
foreach($actions as $id => $action)
{
$conditions = json_decode($action->conditions);
if(empty($conditions)) continue;
foreach($conditions as $index => $condition)
{
foreach($condition->fields as $field)
{
if($field->field == 'createdBy' && zget($this->config->workflow->buildin->createdBy, $action->module, '')) $field->field = zget($this->config->workflow->buildin->createdBy, $action->module);
}
$conditions[$index] = $condition;
}
$this->dao->update(TABLE_WORKFLOWACTION)->set('conditions')->eq(json_encode($conditions))->where('id')->eq($id)->exec();
}
return !dao::isError();
}
/**
* Update story search index.
*
* @access public
* @return void
*/
public function updateSearchIndex()
{
$requirementIds = $this->dao->select('t1.id')->from(TABLE_SEARCHINDEX)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.objectID = t2.id')
->where('t1.objectType')->eq('story')
->andWhere('t2.type')->eq('requirement')
->fetchPairs('id');
$this->dao->update(TABLE_SEARCHINDEX)->set('objectType')->eq('requirement')->where('id')->in($requirementIds)->exec();
}
/**
* Add required rule to the built-in workflow status field.
*
* @access public
* @return void
*/
public function addDefaultRuleToWorkflow()
{
$notemptyRule = $this->dao->select('id')->from(TABLE_WORKFLOWRULE)->where('rule')->eq('notempty')->fetch();
if(empty($notemptyRule)) return false;
$fields = $this->dao->select('*')->from(TABLE_WORKFLOWFIELD)->where('field')->eq('status')->andWhere('buildin')->eq(1)->fetchAll();
foreach($fields as $field)
{
if(strpos(',' . $field->rules . ',', ',' . $notemptyRule->id . ',') !== false) continue;
$rules = $notemptyRule->id;
if(!empty($field->rules)) $rules = $field->rules . ',' . $rules;
$this->dao->update(TABLE_WORKFLOWFIELD)->set('rules')->eq($rules)->where('id')->eq($field->id)->exec();
}
return !dao::isError();
}
/**
* Add workflow actions.
*
* @param int $version
* @access public
* @return void
*/
public function addFlowActions($version)
{
$this->loadModel('workflow');
$this->loadModel('workflowaction');
$upgradeLang = $this->lang->workflowaction->upgrade[$version];
$upgradeConfig = $this->config->workflowaction->upgrade[$version];
if(empty($upgradeLang) || empty($upgradeConfig)) return true;
$this->lang->workflowaction->upgrade = new stdclass();
$this->config->workflowaction->upgrade = new stdclass();
foreach($upgradeLang as $module => $labels)
{
$this->lang->workflowaction->upgrade->actions = $labels;
foreach($upgradeConfig[$module] as $code => $config) $this->config->workflowaction->upgrade->$code = $config;
$flow = $this->dao->select('*')->from(TABLE_WORKFLOW)->where('module')->eq($module)->fetch();
if(!$flow) continue;
$this->workflow->createActions($flow, 'upgrade');
$this->dao->update(TABLE_WORKFLOWACTION)->set('extensionType')->eq('none')->set('role')->eq('buildin')->where('module')->eq($module)->andWhere('action')->in(array_keys($labels))->exec();
foreach($labels as $method => $label)
{
$workflowAction = $this->dao->select('*')->from(TABLE_WORKFLOWACTION)->where('module')->eq($module)->andWhere('action')->eq($method)->fetch();
if(!$workflowAction) continue;
unset($workflowAction->id);
$workflowAction->vision = $workflowAction->vision == 'lite' ? 'rnd' : 'lite';
$this->dao->replace(TABLE_WORKFLOWACTION)->data($workflowAction)->exec();
}
}
return !dao::isError();
}
/**
* Add flow fields.
*
* @param int $version
* @access public
* @return bool
*/
public function addFlowFields($version)
{
if($this->config->edition == 'open') return true;
$this->loadModel('workflowfield');
$upgradeLang = zget($this->lang->workflowfield->upgrade, $version, array());
$upgradeConfig = zget($this->config->workflowfield->upgrade, $version, array());
$workFlowDataSource = $this->dao->select('code,id')->from(TABLE_WORKFLOWDATASOURCE)->where('type')->ne('system')->fetchPairs();
$field = new stdclass();
$field->buildin = '1';
$field->role = 'buildin';
$field->createdBy = $this->app->user->account;
$field->createdDate = helper::now();
foreach($upgradeLang as $module => $fields)
{
$field->module = $module;
foreach($fields as $code => $name)
{
$field->field = $code;
$field->name = $name;
$fieldConfig = isset($upgradeConfig[$module][$code]) ? $upgradeConfig[$module][$code] : array();
if(empty($fieldConfig)) continue;
foreach($fieldConfig as $key => $value) $field->$key = $value;
if($field->control == 'select' && is_string($field->options) && isset($workFlowDataSource[$field->options])) $field->options = $workFlowDataSource[$field->options];
$this->dao->delete()->from(TABLE_WORKFLOWFIELD)->where('module')->eq($module)->andWhere('field')->eq($code)->exec();
$this->dao->insert(TABLE_WORKFLOWFIELD)->data($field)->autoCheck()->exec();
}
}
return !dao::isError();
}
/**
* Process review linkages of approval.
*
* @access public
* @return bool
*/
public function processReviewLinkages()
{
$linkagePairs = $this->dao->select('id, linkages')->from(TABLE_WORKFLOWACTION)->where('action')->eq('approvalreview')->andWhere('role')->eq('approval')->fetchPairs();
$oldLinkages = array();
$oldLinkages[0]['sources'][0]['field'] = 'reviewResult';
$oldLinkages[0]['sources'][0]['operator'] = '==';
$oldLinkages[0]['sources'][0]['value'] = 'reject';
$oldLinkages[0]['targets'][0]['field'] = 'reviewOpinion';
$oldLinkages[0]['targets'][0]['status'] = 'show';
$newLinkages = array();
$newLinkages[0]['sources'][0]['field'] = 'reviewResult';
$newLinkages[0]['sources'][0]['operator'] = '==';
$newLinkages[0]['sources'][0]['value'] = 'pass';
$newLinkages[0]['targets'][0]['field'] = 'reviewOpinion';
$newLinkages[0]['targets'][0]['status'] = 'hide';
foreach($linkagePairs as $id => $linkages)
{
if(helper::jsonEncode($oldLinkages) == $linkages)
{
$this->dao->update(TABLE_WORKFLOWACTION)->set('linkages')->eq(helper::jsonEncode($newLinkages))->where('id')->eq($id)->exec();
}
}
return !dao::isError();
}
/**
* Update story status.
*
* @access public
* @return void
*/
public function updateStoryStatus()
{
/* After cancel the review of changed story, the story status should be "changing". */
$this->dao->update(TABLE_STORY)->set('status')->eq('changing')->where('status')->eq('draft')->andWhere('version')->gt(1)->exec();
/* The draft story with reviewers should be "reviewing". */
$reviewingStories = $this->dao->select('story')->from(TABLE_STORYREVIEW)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id and t1.version = t2.version')
->where('t2.status')->eq('draft')
->andWhere('t2.version')->eq(1)
->fetchPairs();
$this->dao->update(TABLE_STORY)->set('status')->eq('reviewing')->where('id')->in($reviewingStories)->exec();
return !dao::isError();
}
/**
* Change FULLTEXT index for searchindex table.
*
* @access public
* @return void
*/
public function rebuildFULLTEXT()
{
try
{
$table = TABLE_SEARCHINDEX;
$stmt = $this->dao->query("show index from $table");
$indexes = array();
while($index = $stmt->fetch())
{
if($index->Index_type != 'FULLTEXT') continue;
$indexes[$index->Key_name] = $index->Key_name;
}
if(!isset($indexes['title_content'])) $this->dao->exec( "ALTER TABLE {$table} ADD FULLTEXT `title_content` (`title`, `content`)");
if(isset($indexes['title'])) $this->dao->exec( "ALTER TABLE {$table} DROP INDEX `title`");
if(isset($indexes['content'])) $this->dao->exec( "ALTER TABLE {$table} DROP INDEX `content`");
}
catch(PDOException $e){}
return true;
}
/**
* Check whether the field exists.
*
* @param string $table
* @param string $field
* @access public
* @return bool
*/
public function checkFieldsExists($table, $field)
{
$result = $this->dbh->query("show columns from `$table` like '$field'");
return $result->rowCount() > 0;
}
/**
* Update OS and browser of bug.
*
* @access public
* @return bool
*/
public function updateOSAndBrowserOfBug()
{
$existOSList = $this->dao->select('distinct os')->from(TABLE_BUG)->where('os')->ne('')->fetchPairs();
$existBrowserList = $this->dao->select('distinct browser')->from(TABLE_BUG)->where('os')->ne('')->fetchPairs();
$deletedOSList = array('vista', 'win2012', 'win2008', 'win2003', 'win2000', 'wp8', 'wp7', 'symbian', 'freebsd');
$deletedBrowserList = array('ie7', 'ie6', 'firefox4', 'firefox3', 'firefox2', 'opera11', 'opera10', 'opera9', 'maxthon', 'uc');
$existList = array_merge($existOSList, $existBrowserList);
$deletedList = array_merge($deletedOSList, $deletedBrowserList);
foreach($deletedList as $deletedLang)
{
if(in_array($deletedLang, $existList)) continue;
$this->dao->delete()->from(TABLE_LANG)->where('module')->eq('bug')->andWhere('`key`')->eq($deletedLang)->andWhere('`system`')->eq(1)->andWhere('vision')->eq('rnd')->exec();
}
$this->dao->update(TABLE_LANG)->set('value')->eq('Mac OS')->where('module')->eq('bug')->andWhere('`key`')->eq('osx')->andWhere('value')->eq('OS X')->exec();
$this->dao->update(TABLE_LANG)->set('value')->eq('Opera 系列')->where('module')->eq('bug')->andWhere('`key`')->eq('opera')->andWhere('value')->eq('opera 系列')->exec();
return true;
}
/**
* Add user requirement privilege when URAndSR is open.
*
* @access public
* @return bool
*/
public function addURPriv()
{
if(empty($this->config->URAndSR)) return true;
$sql = "REPLACE INTO " . TABLE_GROUPPRIV . " SELECT `group`,'requirement' as 'module',`method` FROM " . TABLE_GROUPPRIV . " WHERE `module` = 'story' AND `method` in ('create', 'batchEdit', 'edit', 'export', 'delete', 'view', 'change', 'review', 'batchReview', 'recall', 'close', 'batchClose', 'assignTo', 'batchAssignTo', 'activate', 'report', 'linkStory', 'batchChangeBranch', 'batchChangeModule', 'linkStories', 'batchEdit', 'import', 'exportTemplate')";
$this->dbh->exec($sql);
return true;
}
/**
* Sync case to project|execution if case create from import.
*
* @access public
* @return void
*/
public function syncCase2Project()
{
if(strpos($this->fromVersion, 'max') === false) return;
$linkStoryCases = $this->dao->select('id, story, version, product')->from(TABLE_CASE)->where('story')->ne('0')->fetchAll('id');
$linkProjectCases = $this->dao->select('`case`, project')->from(TABLE_PROJECTCASE)->where('`case`')->ne('0')->andWhere('project')->ne('0')->fetchGroup('case', 'project');
if(empty($linkStoryCases)) return true;
$projectList = $this->dao->select('t1.project, t1.story, t1.product')->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->where('t2.status')->ne('closed')
->fetchGroup('story', 'project');
if(empty($projectList)) return true;
foreach($linkStoryCases as $caseID => $case)
{
/* If story unlink project continue. */
if(!isset($projectList[$case->story])) continue;
$storyProjects = $projectList[$case->story];
$lastOrder = 1;
foreach($storyProjects as $projectID => $value)
{
/* If case linked project continue.*/
if(isset($linkProjectCases[$caseID][$projectID])) continue;
$data = new stdclass();
$data->project = $projectID;
$data->product = $case->product;
$data->case = $caseID;
$data->version = $case->version;
$data->order = $lastOrder ++;
$this->dao->insert(TABLE_PROJECTCASE)->data($data)->exec();
}
}
}
/**
* Update story file version.
*
* @access public
* @return void
*/
public function updateStoryFile()
{
$storyFileList = $this->dao->select('*')->from(TABLE_FILE)->where('objectType')->in('story,requirement')->andWhere('extra')->ne('editor')->fetchAll('id');
$storyFiles = array();
foreach($storyFileList as $file)
{
if(!is_numeric($file->extra)) continue;
if(!isset($storyFiles[$file->objectID])) $storyFiles[$file->objectID] = '';
$storyFiles[$file->objectID] .= "$file->id,";
}
foreach($storyFiles as $storyID => $files) $this->dao->update(TABLE_STORYSPEC)->set('files')->eq($files)->where('story')->eq($storyID)->exec();
return true;
}
/*
* Convert task team to table: zt_taskteam.
*
* @access public
* @return void
*/
public function convertTaskteam()
{
$oldTeamGroup = $this->dao->select('root as task, account, estimate, consumed, `left`')->from(TABLE_TEAM)->where('type')->eq('task')->fetchGroup('task');
foreach($oldTeamGroup as $taskID => $oldTeams)
{
$order = 0;
foreach($oldTeams as $oldTeam)
{
$oldTeam->order = $order;
$oldTeam->status = 'wait';
if($oldTeam->consumed > 0 and $oldTeam->left > 0) $oldTeam->status = 'doing';
if($oldTeam->consumed > 0 and $oldTeam->left == 0) $oldTeam->status = 'done';
$this->dao->insert(TABLE_TASKTEAM)->data($oldTeam)->exec();
$this->dao->update(TABLE_TASKESTIMATE)->set('`order`')->eq($order)->where('task')->eq($oldTeam->task)->andWhere('account')->eq($oldTeam->account)->exec();
$this->dao->update(TABLE_EFFORT)->set('`order`')->eq($order)->where('objectType')->eq('task')->andWhere('objectID')->eq($oldTeam->task)->andWhere('account')->eq($oldTeam->account)->exec();
$order ++;
}
}
$this->dao->delete()->from(TABLE_TEAM)->where('type')->eq('task')->exec();
}
/**
* Convert estimate to effort.
*
* @access public
* @return void
*/
public function convertEstToEffort()
{
$estimates = $this->dao->select('*')->from(TABLE_TASKESTIMATE)->orderBy('id')->fetchAll('id', false);
$this->app->loadLang('task');
$this->loadModel('action');
foreach($estimates as $estimate)
{
$relation = $this->action->getRelatedFields('task', $estimate->task);
$effort = new stdclass();
$effort->objectType = 'task';
$effort->objectID = $estimate->task;
$effort->product = $relation['product'];
$effort->project = (int)$relation['project'];
$effort->execution = (int)$relation['execution'];
$effort->account = $estimate->account;
$effort->work = empty($estimate->work) ? $this->lang->task->process : $estimate->work;
$effort->date = $estimate->date;
$effort->left = $estimate->left;
$effort->consumed = $estimate->consumed;
$effort->vision = $this->config->vision;
$effort->order = $estimate->order;
$this->dao->insert(TABLE_EFFORT)->data($effort)->exec();
$this->dao->delete()->from(TABLE_TASKESTIMATE)->where('id')->eq($estimate->id)->exec();
}
return true;
}
/**
* Xuan: Set ownedBy for group chats without it.
*
* @access public
* @return bool
*/
public function xuanSetOwnedByForGroups()
{
$this->dao->update(TABLE_IM_CHAT)->set('ownedBy = createdBy')->where('ownedBy')->eq('')->exec();
return !dao::isError();
}
/**
* Xuan: Recover created date for chats.
*
* @access public
* @return bool
*/
public function xuanRecoverCreatedDates()
{
$chats = $this->dao->select('gid, id')->from(TABLE_IM_CHAT)
->where('createdDate')->eq('0000-00-00 00:00:00')
->fetchPairs('gid');
if(empty($chats)) return true;
$createdDateData = array();
/* Try query earliest message date indexed. */
$indexedMinDates = $this->dao->select('gid, MIN(startDate)')->from(TABLE_IM_CHAT_MESSAGE_INDEX)
->where('gid')->in(array_keys($chats))
->groupBy('gid')
->fetchPairs('gid');
/* Then try query earliest message date non-indexed from master table. */
$queryChats = array_diff(array_keys($chats), array_keys($indexedMinDates));
$minDates = $this->dao->select('cgid, MIN(date)')->from(TABLE_IM_MESSAGE)
->where('cgid')->in($queryChats)
->groupBy('cgid')
->fetchPairs('cgid');
$knownMinDates = array_merge($indexedMinDates, $minDates);
$remainingChats = $chats;
foreach($chats as $cgid => $cid)
{
if(isset($knownMinDates[$cgid]))
{
$createdDateData[$cid] = $knownMinDates[$cgid];
unset($remainingChats[$cgid]);
}
}
/* Use other dates for chats without messages. */
$chatDates = $this->dao->select('id, gid, editedDate, lastActiveTime, dismissDate')->from(TABLE_IM_CHAT)
->where('gid')->in(array_keys($remainingChats))
->fetchAll('gid');
$chatDates = array_map(function($chatDate)
{
$dates = array_filter(array($chatDate->editedDate, $chatDate->lastActiveTime, $chatDate->dismissDate), function($date)
{
return $date != '0000-00-00 00:00:00';
});
$minDate = min($dates);
return $minDate;
}, $chatDates);
$knownMinDates = array_merge($knownMinDates, $chatDates);
if(empty($knownMinDates)) return true;
$queryData = array();
foreach($knownMinDates as $gid => $date) $queryData[] = "WHEN {$chats[$gid]} THEN '{$date}'";
if(empty($queryData)) return true;
$query = "UPDATE " . TABLE_IM_CHAT . " SET `createdDate` = (CASE `id` " . implode(' ', $queryData) . " END) WHERE `id` IN(" . implode(",", array_values($chats)) . ");";
$this->dao->query($query);
return !dao::isError();
}
/**
* Xuan: Set index range for chats in chat partition index table.
*
* @access public
* @return bool
*/
public function xuanSetPartitionedMessageIndex()
{
ini_set('memory_limit', '1024M');
set_time_limit(0);
/* Fetch chat and message partition table associations with message range. */
$chatTableData = $this->dao->select('gid, tableName, start, end')->from(TABLE_IM_CHAT_MESSAGE_INDEX)
->where('startIndex')->eq(0)
->orWhere('endIndex')->eq(0)
->orderBy('id_asc')
->fetchAll();
if(empty($chatTableData)) return true;
/* Sort ranges by table. */
$tableRanges = array();
foreach($chatTableData as $chatTable)
{
if(isset($tableRanges[$chatTable->tableName]))
{
$tableRanges[$chatTable->tableName]->start[] = $chatTable->start;
$tableRanges[$chatTable->tableName]->end[] = $chatTable->end;
continue;
}
$tableRanges[$chatTable->tableName] = (object)array('start' => array($chatTable->start), 'end' => array($chatTable->end));
}
/* Query corresponding message indice. */
foreach($tableRanges as $tableName => $tableRange)
{
$ids = array_merge($tableRange->start, $tableRange->end);
$ids = array_unique($ids);
$indexPairs = $this->dao->select('id, `index`')->from("`$tableName`")
->where('id')->in($ids)
->fetchPairs('id');
$tableRanges[$tableName]->indexPairs = $indexPairs;
}
/* Set startIndice and endIndice. */
foreach($tableRanges as $tableRange)
{
$queryData = array();
foreach($tableRange->start as $id) $queryData[] = "WHEN $id THEN {$tableRange->indexPairs[$id]}";
$query = "UPDATE " . TABLE_IM_CHAT_MESSAGE_INDEX . " SET `startIndex` = (CASE `start` " . implode(' ', $queryData) . " END) WHERE `start` IN(" . implode(',', $tableRange->start) . ");";
$this->dao->query($query);
$queryData = array();
foreach($tableRange->end as $id) $queryData[] = "WHEN $id THEN {$tableRange->indexPairs[$id]}";
$query = "UPDATE " . TABLE_IM_CHAT_MESSAGE_INDEX . " SET `endIndex` = (CASE `end` " . implode(' ', $queryData) . " END) WHERE `end` IN(" . implode(',', $tableRange->end) . ");";
$this->dao->query($query);
}
}
/**
* Fix weekly report.
*
* @access public
* @return bool
*/
public function fixWeeklyReport()
{
if(!isset($this->app->user)) $this->app->user = new stdclass();
$this->app->user->admin = true;
$this->loadModel('weekly');
$projects = $this->dao->select('id,begin,end')->from(TABLE_PROJECT)->where('deleted')->eq('0')->andWhere('model')->eq('waterfall')->fetchAll('id');
$today = helper::today();
foreach($projects as $projectID => $project)
{
if(helper::isZeroDate($project->begin) or helper::isZeroDate($project->end)) continue;
$begin = $project->begin;
$end = $today > $project->end ? $project->end : $today;
$beginTimestame = strtotime($begin);
$endTimestame = strtotime($end);
while($beginTimestame <= $endTimestame)
{
$this->weekly->save($projectID, $begin);
$beginTimestame += 7 * 24 * 3600;
$begin = date('Y-m-d', $beginTimestame);
}
}
return true;
}
/**
* 将历史的项目作为项目升级。
* Historical projects are upgraded by project.
*
* @param int $programID
* @param string $fromMode
* @access public
* @return bool
*/
public function upgradeInProjectMode(int $programID, string $fromMode = ''): bool
{
$noMergedSprints = $this->upgradeTao->getNoMergedSprints();
if(!$noMergedSprints) return true;
$this->loadModel('action');
$now = helper::now();
$account = isset($this->app->user->account) ? $this->app->user->account : '';
foreach($noMergedSprints as $sprint)
{
$project = $this->upgradeTao->buildProjectInAutoUpgrade($sprint, $programID, $fromMode, $account, $now);
$project->days = $this->computeDaysDelta($project->begin, $project->end);
$this->dao->insert(TABLE_PROJECT)->data($project)->exec();
if(dao::isError()) return false;
$projectID = $this->dao->lastInsertID();
if($project->status == 'closed') $this->action->create('project', $projectID, 'closedbysystem');
if($fromMode == 'classic')
{
$this->dao->update(TABLE_PROJECT)->set('multiple')->eq('0')->where('id')->eq($sprint->id)->exec();
$this->dao->update(TABLE_DOCLIB)->set('project')->eq($projectID)->set('type')->eq('project')->set('execution')->eq(0)->where('execution')->eq($sprint->id)->andWhere('type')->eq('execution')->exec();
$this->dao->update(TABLE_DOC)->set('project')->eq($projectID)->set('execution')->eq(0)->where('execution')->eq($sprint->id)->exec();
$this->dao->update(TABLE_ACTION)->set('objectType')->eq('project')->set('objectID')->eq($projectID)->set('project')->eq($projectID)->set('execution')->eq('0')->where('objectID')->eq($sprint->id)->andWhere('objectType')->eq('execution')->exec();
$this->dao->update(TABLE_ACTION)->set('project')->eq($projectID)->set('execution')->eq('0')->where('execution')->eq($sprint->id)->exec();
}
else
{
$this->action->create('project', $projectID, 'openedbysystem');
$project->id = $projectID;
$this->upgradeTao->createProjectDocLib($project);
}
$productIdList = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($sprint->id)->fetchPairs();
$this->processMergedData($programID, $projectID, 0, $productIdList, array($sprint->id));
}
/* Fix project path and update the module. */
$this->fixProjectPath($programID);
$this->dao->update(TABLE_MODULE)->set('root')->eq($programID)->where('type')->eq('line')->andWhere('root')->eq('0')->exec();
/* Compute product's acl under the program. */
$productIdList = $this->dao->select('id')->from(TABLE_PRODUCT)->where('program')->eq('0')->fetchPairs();
$this->computeProductAcl($productIdList, $programID, null);
return !dao::isError();
}
/**
* 将历史的项目作为执行升级。
* Historical projects are upgraded by execution.
*
* @param int $programID
* @access public
* @return bool
*/
public function upgradeInExecutionMode(int $programID): bool
{
$this->loadModel('action');
$now = helper::now();
$account = isset($this->app->user->account) ? $this->app->user->account : '';
$noMergedSprints = $this->upgradeTao->getNoMergedSprints();
if(!$noMergedSprints) return true;
$projects = array();
foreach($noMergedSprints as $sprint)
{
$year = date('Y', strtotime($sprint->openedDate));
$projects[$year][$sprint->id] = $sprint;
}
foreach($projects as $year => $sprints)
{
$project = new stdclass();
$project->name = $year > 0 ? $year : $this->lang->upgrade->unknownDate;
$project->type = 'project';
$project->model = 'scrum';
$project->parent = $programID;
$project->team = $project->name;
$project->auth = 'extend';
$project->begin = '';
$project->end = '';
$project->openedBy = $account;
$project->openedDate = $now;
$project->openedVersion = $this->config->version;
$project->lastEditedBy = $account;
$project->lastEditedDate = $now;
$project->grade = 2;
$project->acl = 'open';
$project->storyType = 'story';
$projectStatus = 'closed';
foreach($sprints as $sprint)
{
if(!$project->begin || $sprint->begin < $project->begin) $project->begin = $sprint->begin;
if(!$project->end || $sprint->end > $project->end) $project->end = $sprint->end;
if($sprint->status != 'closed') $projectStatus = 'doing';
}
$project->status = $projectStatus;
$project->days = $this->computeDaysDelta($project->begin, $project->end);
$this->dao->insert(TABLE_PROJECT)->data($project)->exec();
if(dao::isError()) return false;
$projectID = $this->dao->lastInsertId();
$this->action->create('project', $projectID, 'openedbysystem');
if($project->status == 'closed') $this->action->create('project', $projectID, 'closedbysystem');
$project->id = $projectID;
$this->upgradeTao->createProjectDocLib($project);
$productIdList = $this->dao->select('product')->from(TABLE_PROJECTPRODUCT)->where('project')->in(array_keys($sprints))->fetchPairs();
$this->processMergedData($programID, $projectID, 0, $productIdList, array_keys($sprints));
}
$this->fixProjectPath($programID);
$productIdList = $this->dao->select('id')->from(TABLE_PRODUCT)->where('program')->eq('0')->fetchPairs();
$this->dao->update(TABLE_MODULE)->set('root')->eq($programID)->where('type')->eq('line')->andWhere('root')->eq('0')->exec();
$this->computeProductAcl($productIdList, $programID, null);
if(dao::isError()) return false;
return true;
}
/**
* 修复项目集下的项目路径。
* Fix the project path under the program.
*
* @param int $programID
* @access public
* @return void
*/
public function fixProjectPath(int $programID): void
{
$this->dao->update(TABLE_PROJECT)
->set("path = CONCAT(',{$programID},', id, ',')")->set("`order` = `id` * 5")
->where('type')->eq('project')
->andWhere('parent')->eq($programID)
->andWhere('grade')->eq('2')
->exec();
}
/**
* Relate default program.
*
* @param int $programID
* @access public
* @return bool
*/
public function relateDefaultProgram(int $programID): bool
{
$this->dao->update(TABLE_PRODUCT)->set('program')->eq($programID)->where('program')->eq(0)->exec();
$this->dao->update(TABLE_MODULE)->set('root')->eq($programID)->where('type')->eq('line')->andWhere('root')->eq('0')->exec();
$this->dao->update(TABLE_PROJECT)->set('parent')->eq($programID)->set("path = CONCAT(',{$programID}', path)")->set('grade = grade + 1')->where('type')->eq('project')->andWhere('parent')->eq(0)->andWhere('grade')->eq(1)->exec();
return !dao::isError();
}
/*
* Update the owner of the program into the product view.
*
* @access public
* @return bool
*/
public function updateProductView()
{
$programs = $this->dao->select('id,PM')->from(TABLE_PROGRAM)->where('type')->eq('program')->andWhere('PM')->ne('')->fetchPairs('id', 'PM');
if(empty($programs)) return true;
$productGroup = $this->dao->select('id,program')->from(TABLE_PRODUCT)->where('program')->in(array_keys($programs))->andWhere('acl')->ne('open')->fetchGroup('program', 'id');
if(empty($productGroup)) return true;
$userView = $this->dao->select('*')->from(TABLE_USERVIEW)->where('account')->in(array_values($programs))->fetchAll('account');
foreach($programs as $programID => $programPM)
{
if(empty($productGroup[$programID])) continue;
$canViewProducts = zget($productGroup, $programID);
$view = $userView[$programPM]->products;
foreach($canViewProducts as $productID => $product)
{
if(strpos(",$view,", ",$productID,") === false) $view .= ',' . $productID;
}
$this->dao->update(TABLE_USERVIEW)->set('products')->eq($view)->where('account')->eq($programPM)->exec();
}
return true;
}
/**
* Process feedback module
*
* @access public
* @return void
*/
public function processFeedbackModule()
{
$products = $this->dao->select('id, name')->from(TABLE_PRODUCT)->fetchAll();
$modules = $this->dao->select('*')->from(TABLE_MODULE)->where('type')->eq('feedback')->andWhere('root')->eq(0)->fetchAll('id', false);
$feedbacks = $this->dao->select('*')->from(TABLE_FEEDBACK)->fetchAll();
$allProductRelation = array();
foreach($products as $product)
{
$productID = $product->id;
$relation = array();
foreach($modules as $moduleID => $module)
{
unset($module->id);
$module->root = $productID;
$this->dao->insert(TABLE_MODULE)->data($module)->exec();
$newModuleID = $this->dao->lastInsertID();
$relation[$moduleID] = $newModuleID;
$allProductRelation[$productID][$moduleID] = $newModuleID;
$newPaths = array();
foreach(explode(',', trim($module->path, ',')) as $path)
{
if(isset($relation[$path])) $newPaths[] = $relation[$path];
}
$newPaths = implode(',', $newPaths);
$parent = !empty($module->parent) ? $relation[$module->parent] : 0;
$this->dao->update(TABLE_MODULE)->set('path')->eq($newPaths)->set('parent')->eq($parent)->where('id')->eq($newModuleID)->exec();
}
}
/* Update feedback module */
foreach($feedbacks as $feedback)
{
$moduleID = $feedback->module;
$product = $feedback->product;
if(empty($moduleID)) continue;
$newModuleID = isset($allProductRelation[$product][$moduleID]) ? $allProductRelation[$product][$moduleID] : 0;
if(empty($newModuleID)) continue;
$this->dao->update(TABLE_FEEDBACK)->set('module')->eq($newModuleID)->where('id')->eq($feedback->id)->exec();
}
/* Delete history module */
$this->dao->delete()->from(TABLE_MODULE)->where('type')->eq('feedback')->andWhere('root')->eq(0)->exec();
}
/**
* Add default modules for BI.
*
* @param string $type
* @param int $dimension
* @access public
* @return array
*/
public function addDefaultModules4BI($type = 'report', $dimension = 1)
{
$this->app->loadLang('dimension');
$group = new stdclass();
$group->root = $dimension;
$group->grade = 1;
$group->type = $type;
$group->owner = 'system';
$group->order = 0;
$modules = array();
foreach($this->lang->dimension->moduleList as $module => $name)
{
if(!$module || !$name) continue;
$group->order += 10;
$exist = $this->dao->select('id')->from(TABLE_MODULE)
->where('root')->eq($dimension)
->andWhere('collector')->eq($module)
->andWhere('type')->eq($type)
->fetchAll();
if(!empty($exist)) continue;
$group->name = $name;
$group->collector = $module;
$this->dao->insert(TABLE_MODULE)->data($group)->exec();
$modules[$module] = $this->dao->lastInsertID();
}
$this->dao->update(TABLE_MODULE)->set("`path` = CONCAT(',', `id`, ',')")
->where('type')->eq($type)
->andWhere('grade')->eq('1')
->andWhere('path')->eq('')
->exec();
return $modules;
}
/**
* 处理透视表。
* Process dataset.
*
* @access public
* @return bool
*/
public function processDataset(): bool
{
$dataviewData = $this->dao->select('id')->from(TABLE_DATAVIEW)->fetch();
if(!empty($dataviewData)) return true;
$this->loadModel('dataset');
$this->loadModel('dataview');
$this->dao->begin();
$this->upgradeTao->convertBuiltInDataSet();
if(dao::isError())
{
$this->dao->rollback();
return false;
}
$customDataset = $this->dao->select('*')->from(TABLE_DATASET)->fetchAll('id', false);
if(empty($customDataset)) return $this->dao->commit();
$this->upgradeTao->convertCustomDataSet($customDataset);
if(dao::isError())
{
$this->dao->rollback();
return false;
}
return $this->dao->commit();
}
/**
* Process report modules.
*
* @access public
* @return bool
*/
public function createDefaultDimension()
{
/* Create default dimension. */
$this->loadModel('dimension');
foreach($this->config->dimension->defaultDimension as $dimensionName)
{
$dimension = new stdclass();
$dimension->name = $this->lang->dimension->{$dimensionName};
$dimension->code = $dimensionName;
$dimension->desc = '';
$dimension->createdBy = 'system';
$dimension->createdDate = helper::now();
$this->dao->insert(TABLE_DIMENSION)->data($dimension)->exec();
$dimensionID = $this->dao->lastInsertID();
$chartModules = $this->addDefaultModules4BI('chart', $dimensionID);
$this->addSecondModule4BI($dimensionID, $dimensionName, 'chart', $chartModules);
$pivotModules = $this->addDefaultModules4BI('pivot', $dimensionID);
$this->addSecondModule4BI($dimensionID, $dimensionName, 'pivot', $pivotModules);
}
return !dao::isError();
}
/**
* Add second modules for bi, move buildin chart and pivot to second modules.
*
* @access public
* @return bool
*/
public function addSecondModule4BI($dimensionID, $dimensionName, $module, $modules)
{
$this->loadModel('dimension');
$listIndex = $module . 'Upgrade';
$secondModules = $this->config->dimension->secondModuleList[$dimensionName][$module];
$updateInfos = $this->config->dimension->secondModuleList[$dimensionName][$listIndex];
foreach($modules as $moduleName => $parentID)
{
$insertModules = isset($secondModules[$moduleName]) ? explode(',', $secondModules[$moduleName]) : array();
if(empty($insertModules)) continue;
$i = 1;
foreach($insertModules as $moduleCode)
{
$data = new stdclass();
$data->root = $dimensionID;
$data->name = $this->lang->dimension->modules[$moduleCode];
$data->parent = $parentID;
$data->grade = 2;
$data->order = 10 * $i;
$data->type = $module;
$i ++;
$this->dao->insert(TABLE_MODULE)->data($data)->exec();
$lastGroupID = $this->dao->lastInsertID();
$path = ',' . $parentID . ',' . $lastGroupID . ',';
$this->dao->update(TABLE_MODULE)->set("`path`")->eq($path)->where('id')->eq($lastGroupID)->exec();
$updateArr = isset($updateInfos[$moduleName][$moduleCode]) ? $updateInfos[$moduleName][$moduleCode] : array();
if(!empty($updateArr))
{
foreach($updateArr as $type => $idString)
{
$table = $type == 'chart' ? TABLE_CHART : TABLE_PIVOT;
$idList = explode(',', $idString);
$this->dao->update($table)->set('group')->eq($lastGroupID)->set('dimension')->eq($dimensionID)->where('id')->in($idList)->exec();
}
}
}
}
}
/**
* Add update2BI mark to zt_config.
*
* @access public
* @return bool
*/
public function addBIUpdateMark()
{
$this->loadModel('setting')->setItem("system.bi.update2BI", 1);
}
/**
* Xuan: Set mute and freeze for hidden groups.
*
* @access public
* @return bool
*/
public function xuanSetMuteForHiddenGroups()
{
$this->dao->update(TABLE_IM_CHATUSER)->set('hide')->eq('0')->set('mute')->eq('1')->set('freeze')->eq('1')->where('hide')->eq('1')->andWhere('quit')->eq('0000-00-00 00:00:00')->exec();
return !dao::isError();
}
/**
* Xuan: Notify users who have hide the group.
*
* @access public
* @return bool
*/
public function xuanNotifyGroupHiddenUsers()
{
$noticeUsers = $this->dao->select('user')->from(TABLE_IM_CHATUSER)->where('hide')->eq('1')->andWhere('quit')->eq('0000-00-00 00:00:00')->groupBy('user')->fetchAll();
$sender = new stdClass();
$sender->avatar = commonModel::getSysURL() . '/www/favicon.ico';
$sender->id = 'upgradeArchive';
$sender->realname = $this->lang->upgrade->archiveChangeNoticeTitle;
$this->loadModel('im')->messageCreateNotify(array_keys($noticeUsers), '', '', $this->lang->upgrade->archiveChangeNoticeContent, 'text', '', array(), $sender);
return !dao::isError();
}
/**
* Xuan: Notify users for upgrade 9.0 jitsi conference.
*
* @access public
* @return bool
*/
public function xuanNotifyJitsiConference()
{
$users = array_keys($this->dao->select('id')->from(TABLE_USER)->where('deleted')->eq('0')->fetchAll('id'));
$super = array_values(explode(',', $this->dao->select('admins')->from(TABLE_COMPANY)->where('id')->eq($this->app->company->id)->fetch('admins')));
$this->loadModel('im');
$sender = new stdclass();
$sender->id = 0;
$sender->displayName = $this->lang->im->bot->commonName;
$sender->avatar = commonModel::getSysURL() . $this->config->webRoot . 'data/image/xuanbot.png';
$this->im->messageCreateNotify($super, '', '', $this->lang->upgrade->jitsiConferenceNotice->super, 'text', $this->lang->upgrade->jitsiConferenceNotice->setupLink, array(), $sender);
$this->im->messageCreateNotify($users, '', '', $this->lang->upgrade->jitsiConferenceNotice->user, 'text', $this->lang->upgrade->jitsiConferenceNotice->manualLink, array(), $sender);
return !dao::isError();
}
/**
* Init shadow builds.
*
* @access public
* @return bool
*/
public function initShadowBuilds()
{
$releases = $this->dao->select('id,product,shadow,build,name,date,createdBy,createdDate,deleted')->from(TABLE_RELEASE)->where('shadow')->eq(0)->fetchAll();
foreach($releases as $release)
{
$shadowBuild = new stdclass();
$shadowBuild->product = $release->product;
$shadowBuild->builds = $release->build;
$shadowBuild->name = $release->name;
$shadowBuild->date = $release->date;
$shadowBuild->createdBy = $release->createdBy;
$shadowBuild->createdDate = $release->createdDate;
$shadowBuild->deleted = $release->deleted;
$this->dao->insert(TABLE_BUILD)->data($shadowBuild)->exec();
$shadowBuildID = $this->dao->lastInsertID();
$this->dao->update(TABLE_RELEASE)->set('shadow')->eq($shadowBuildID)->where('id')->eq($release->id)->exec();
}
return true;
}
/**
* Init review efforts.
*
* @access public
* @return void
*/
public function initReviewEfforts()
{
$nodes = $this->dao->select('t1.id,t3.id as reviewID,t3.title,t2.id as approvalID,t1.extra as consumed')->from(TABLE_APPROVALNODE)->alias('t1')
->leftJoin(TABLE_APPROVAL)->alias('t2')->on("t1.approval=t2.id")
->leftJoin(TABLE_REVIEW)->alias('t3')->on("t2.objectID=t3.id")
->where('t3.deleted')->eq('0')
->andWhere('t2.deleted')->eq('0')
->andWhere('t2.objectType')->eq('review')
->andWhere('t1.extra')->ne('')
->andWhere('t1.extra')->ne(0)
->orderBy('t1.approval,t1.id')
->fetchAll('id');
$this->loadModel('effort');
foreach($nodes as $node)
{
$this->dao->delete()->from(TABLE_EFFORT)->where('objectType')->eq('review')->andWhere('objectID')->eq($node->reviewID)->exec();
$this->effort->create('review', $node->reviewID, (int)$node->consumed, $node->title, $node->approvalID);
}
return true;
}
/**
* Update my module blocks.
*
* @access public
* @return bool
*/
public function updateMyBlocks()
{
/* Delete flowchart block. */
$this->dao->delete()->from(TABLE_BLOCK)->where('module')->eq('my')->andWhere('block')->eq('flowchart')->exec();
/* Update block order and insert guide block. */
$visionList = array('rnd', 'lite');
/* Set guide block data. */
$guideBlock = new stdclass();
$guideBlock->module = 'my';
$guideBlock->title = common::checkNotCN() ? 'Guides' : '使用帮助';
$guideBlock->block = 'guide';
$guideBlock->order = 3;
$guideBlock->grid = 8;
foreach($visionList as $vision)
{
$guideBlock->vision = $vision;
$this->dao->update(TABLE_BLOCK)
->set('`order` = `order` + 1')
->where('vision')->eq($vision)
->andWhere('module')->eq('my')
->andWhere('`order`')->ge(3)
->orderBy('`order` desc')
->exec();
$accountList = $this->dao->select('account')->from(TABLE_BLOCK)
->where('vision')->eq($vision)
->andWhere('module')->eq('my')
->fetchPairs('account');
foreach($accountList as $account)
{
if(empty($account)) continue;
$guideBlock->account = $account;
$this->dao->insert(TABLE_BLOCK)->data($guideBlock)->exec();
}
}
return true;
}
/**
* Process dashboard to screen.
*
* @access public
* @return void
*/
public function processDashboard()
{
$dashboards = $this->dao->select('*')->from(TABLE_DASHBOARD)->fetchAll('id', false);
$id = 1002;
foreach($dashboards as $dashboard)
{
$screen = new stdclass();
$screen->id = $id;
$screen->name = $dashboard->name;
$screen->dimension = 1;
$screen->desc = $dashboard->desc;
$screen->scheme = $this->processDashboardLayout($dashboard);
$screen->deleted = $dashboard->deleted;
$screen->status = 'published';
$screen->createdBy = $dashboard->createdBy;
$screen->createdDate = $dashboard->createdDate;
$this->dao->insert(TABLE_SCREEN)->data($screen)->exec();
$id++;
}
$this->dao->exec("ALTER TABLE " . TABLE_CHART . " DROP `dataset`");
$this->dao->exec("ALTER TABLE " . TABLE_PIVOT . " DROP `dataset`");
}
/**
* Upgrade dashboard info to screen.
*
* @param object $dashboard
* @access public
* @return void
*/
public function processDashboardLayout($dashboard)
{
$this->loadModel('screen');
$scheme = file_get_contents($this->app->getModuleRoot() . DS . 'screen' . DS . 'json' . DS . 'screen.json');
$scheme = json_decode($scheme);
$layout = json_decode($dashboard->layout);
$canvasHeight = $scheme->editCanvasConfig->height;
$componentList = array();
foreach($layout as $option)
{
$component = new stdclass();
$component = $this->loadModel('screen')->setComponentDefaults($component);
$component->id = $option->i->id;
$component->attr = json_decode('{"offsetX": 0, "offsetY": 0, "lockScale": false, "zIndex": -1}');
$component->attr->x = round($scheme->editCanvasConfig->width * ($option->x / 12));
$component->attr->y = round(54 * $option->y);
$component->attr->w = round($scheme->editCanvasConfig->width * ($option->w / 12));
$component->attr->h = round(54 * $option->h);
$type = !empty($option->i->type) ? $option->i->type : 'chart';
$chart = $this->loadModel($type)->getByID($option->i->id, false, 'published', false);
if($chart)
{
if($chart->sql)
{
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $this->dbh->query($chart->sql);
if(!$stmt) continue;
}
$settings = $chart->settings;
if($type == 'chart') $chartType = $chart->builtin ? $chart->type : $settings[0]['type'];
if($type == 'pivot') $chartType = 'table';
$chartConfig = json_decode(zget($this->config->screen->chartConfig, $chartType));
$chartConfig->fields = $chart->fieldSettings;
$chartConfig->sourceID = $option->i->id;
$component->title = $chart->name;
$component->type = $chartType;
$component->sourceID = $option->i->id;
$component->key = $chartConfig->key;
$component->chartConfig = $chartConfig;
$component->option = json_decode(zget($this->config->screen->chartOption, $chartType));
if(isset($component->option->title->text)) $component->option->title->text = $chart->name;
$chart->fields = json_encode($chart->fieldSettings);
$chart->settings = json_encode($chart->settings);
$chart->langs = json_encode($chart->langs);
$chartFilters = !empty($chart->filters) ? json_decode($chart->filters, true) : array();
}
$componentList[] = $component;
if($canvasHeight < ($component->attr->y + $component->attr->h)) $canvasHeight = $component->attr->y + $component->attr->h;
}
$filters = json_decode($dashboard->filters);
$globalFilters = array();
foreach($filters as $filter)
{
$globalFilter = new stdclass();
$globalFilter->name = $filter->name;
$globalFilter->type = $filter->type;
$globalFilter->field = $filter->field;
$globalFilter->defaultValue = zget($filter, 'defaultValue' , '');
$diagram = array();
foreach($layout as $option)
{
$type = !empty($option->i->type) ? $option->i->type : 'chart';
$chart = $this->loadModel($type)->getByID($option->i->id);
$fields = explode('.', $filter->field);
if($chart->dataset == $fields[0]) $diagram[] = array('id' => "diagram-{$chart->id}", 'field' => $fields[1]);
}
$globalFilter->diagram = $diagram;
$globalFilters[] = $globalFilter;
}
$scheme->editCanvasConfig->height = $canvasHeight;
$scheme->editCanvasConfig->globalFilter = $globalFilters;
$scheme->componentList = $componentList;
$scheme = json_encode($scheme);
return $scheme;
}
/**
* Insert mix stage.
*
* @access public
* @return bool
*/
public function insertMixStage()
{
$typeList = $this->dao->select('*')->from(TABLE_LANG)
->where('module')->eq('stage')
->andWhere('section')->eq('typeList')
->fetchAll('id', false);
$this->dao->delete()->from(TABLE_LANG)
->where('module')->eq('stage')
->andWhere('section')->eq('typeList')
->exec();
$mixInserted = array();
foreach($typeList as $type)
{
if(!isset($mixInserted[$type->lang . '-' . $type->vision]))
{
$langFile = $this->app->getModuleRoot() . DS . 'stage' . DS . 'lang' . DS . ($type->lang == 'all' ? $this->app->clientLang : $type->lang) . '.php';
if(!is_file($langFile)) continue;
$lang = new stdclass();
$lang->stage = new stdclass();
include $langFile;
$this->dao->replace(TABLE_LANG)
->set('module')->eq('stage')
->set('section')->eq('typeList')
->set('lang')->eq($type->lang)
->set('vision')->eq($type->vision)
->set('key')->eq('mix')
->set('value')->eq($lang->stage->typeList['mix'])
->exec();
$mixInserted[$type->lang . '-' . $type->vision] = true;
}
if($type->key == 'mix') continue;
$this->dao->replace(TABLE_LANG)
->set('module')->eq('stage')
->set('section')->eq('typeList')
->set('lang')->eq($type->lang)
->set('vision')->eq($type->vision)
->set('key')->eq($type->key)
->set('value')->eq($type->value)
->exec();
}
return true;
}
/**
* Change book to custom lib.
*
* @access public
* @return void
*/
public function changeBookToCustomLib()
{
$libs = $this->dao->select('id,id')->from(TABLE_DOCLIB)->where('`type`')->eq('book')->fetchPairs();
foreach($libs as $libID)
{
$chapterModulePairs = array();
$chapters = $this->dao->select('*')->from(TABLE_DOC)->where('lib')->eq($libID)->andWhere('`type`')->eq('chapter')->orderBy('`grade` asc, `order` asc')->fetchGroup('grade', 'id');
foreach($chapters as $grade => $gradeChapters)
{
foreach($gradeChapters as $id => $chapter)
{
$module = new stdclass();
$module->root = $chapter->lib;
$module->name = $chapter->title;
$module->parent = zget($chapterModulePairs, $chapter->parent);
$module->grade = $chapter->grade;
$module->order = $chapter->order;
$module->type = 'doc';
$module->deleted = $chapter->deleted;
$this->dao->insert(TABLE_MODULE)->data($module)->exec();
$moduleID = $this->dao->lastInsertID();
$chapterModulePairs[$id] = $moduleID;
$path = explode(',', $chapter->path);
$path = array_filter($path);
foreach($path as $index => $chapterID) $path[$index] = zget($chapterModulePairs, $chapterID);
$path = implode(',', $path);
$this->dao->update(TABLE_MODULE)->set('`path`')->eq(",{$path},")->where('id')->eq($moduleID)->exec();
$this->dao->update(TABLE_DOC)->set('`module`')->eq($moduleID)->set('path')->eq('')->set('`type`')->eq('text')->where('`parent`')->eq($id)->andWhere('`type`')->eq('article')->exec();
$this->dao->update(TABLE_DOC)->set('deleted')->eq(1)->where('id')->eq($id)->exec();
}
}
$this->dao->update(TABLE_DOCLIB)->set('`type`')->eq('custom')->where('id')->eq($libID)->exec();
$this->dao->update(TABLE_DOC)->set('`type`')->eq('text')->where('`lib`')->eq($libID)->andWhere('grade')->eq(1)->andWhere('`type`')->eq('article')->exec();
}
$this->dao->update(TABLE_DOC)->set('`type`')->eq('text')->where('`type`')->eq('book')->andWhere('templateType')->eq('')->exec();
$this->dbh->exec("UPDATE " . TABLE_DOC . " AS t1 LEFT JOIN " . TABLE_EXECUTION . " AS t2 ON t1.`execution` = t2.`id` SET t1.`project` = t2.`project` WHERE t1.`execution` != 0 AND t1.`project` = 0 AND t2.`project` != 0");
return true;
}
/**
* Create default group.
*
* @param string $type
* @access public
* @return void
*/
public function createDefaultGroup($type)
{
$firstGroup = new stdclass();
$firstGroup->root = 1;
$firstGroup->type = $type;
$firstGroup->parent = 0;
$firstGroup->grade = 1;
$firstGroup->name = $this->lang->upgrade->defaultGroup;
$this->dao->insert(TABLE_MODULE)->data($firstGroup)->autoCheck()->exec();
$firstGroupID = $this->dao->lastInsertID();
$this->dao->update(TABLE_MODULE)->set('path')->eq(",{$firstGroupID},")->where('id')->eq($firstGroupID)->exec();
$secondGroup = new stdclass();
$secondGroup->root = 1;
$secondGroup->type = $type;
$secondGroup->parent = $firstGroupID;
$secondGroup->grade = 2;
$secondGroup->name = $this->lang->upgrade->defaultGroup;
$this->dao->insert(TABLE_MODULE)->data($secondGroup)->autoCheck()->exec();
$secondGroupID = $this->dao->lastInsertID();
$this->dao->update(TABLE_MODULE)->set('path')->eq(",{$firstGroupID},{$secondGroupID},")->where('id')->eq($secondGroupID)->exec();
return $secondGroupID;
}
/**
* Process chart.
*
* @access public
* @return bool
*/
public function processChart()
{
$charts = $this->dao->select('*')->from(TABLE_CHART)->where('builtin')->eq(0)->fetchAll('id', false);
$dashboardLayoutPairs = $this->dao->select('id, layout')->from(TABLE_DASHBOARD)->fetchPairs();
$dataviewList = $this->dao->select('t1.code,t1.view,t1.fields')->from(TABLE_DATAVIEW)->alias('t1')
->leftJoin(TABLE_CHART)->alias('t2')->on('t1.code = t2.dataset')
->where('t2.id')->in(array_keys($charts))
->fetchAll('code');
$defaultChartGroupID = $this->createDefaultGroup('chart');
foreach($charts as $chart)
{
if($chart->type == 'table')
{
$pivotID = $this->upgradeToPivotTable($chart, $dataviewList);
}
else
{
$data = new stdclass();
$data->dimension = 1;
$data->group = $defaultChartGroupID;
$data->stage = 'published';
$data->step = 4;
$data->type = $chart->type == 'bar' ? 'cluBarX' : $chart->type;
if(isset($dataviewList[$chart->dataset]))
{
$dataview = $dataviewList[$chart->dataset];
$data->sql = 'SELECT * FROM ' . $dataview->view;
$data->fields = isset($dataview->fields) ? $dataview->fields : '';
}
elseif($chart->sql)
{
$fieldSettings = $this->getFieldSettings($chart->sql);
$data->fields = json_encode($fieldSettings);
}
$settings = json_decode($chart->settings);
if($settings && (!empty($settings->group) || !empty($settings->xaxis)))
{
$settings->type = $data->type;
$filters = array();
if(isset($settings->filter))
{
$filters = $settings->filter;
unset($settings->filter);
}
$isQuoteDataview = isset($dataviewList[$chart->dataset]);
if(isset($settings->xaxis))
{
$xaxisFields = $settings->xaxis;
foreach($xaxisFields as $xaxisIndex => $xaxisField)
{
if(isset($xaxisField->dateGroup)) $xaxisField->group = $xaxisField->dateGroup;
if($isQuoteDataview && strpos($xaxisField->field, '.') !== false) $xaxisField->field = str_replace('.', '_', $xaxisField->field);
$xaxisFields[$xaxisIndex] = $xaxisField;
}
$settings->xaxis = $xaxisFields;
}
if(isset($settings->group))
{
$groupFields = $settings->group;
foreach($groupFields as $groupIndex => $groupField)
{
if(isset($groupField->dateGroup)) $groupField->group = $groupField->dateGroup;
if($isQuoteDataview && strpos($groupField->field, '.') !== false) $groupField->field = str_replace('.', '_', $groupField->field);
$groupFields[$groupIndex] = $groupField;
}
$settings->group = $groupFields;
}
if(isset($settings->yaxis))
{
$yaxisFields = $settings->yaxis;
foreach($yaxisFields as $yaxisIndex => $yaxisField)
{
if($isQuoteDataview && strpos($yaxisField->field, '.') !== false) $yaxisField->field = str_replace('.', '_', $yaxisField->field);
if($yaxisField->valOrAgg == 'value') $yaxisField->valOrAgg = 'sum';
if($yaxisField->valOrAgg == 'count_distinct') $yaxisField->valOrAgg = 'distinct';
if($yaxisField->valOrAgg == 'value_all') $yaxisField->valOrAgg = 'count';
if($yaxisField->valOrAgg == 'value_distinct') $yaxisField->valOrAgg = 'distinct';
$yaxisFields[$yaxisIndex] = $yaxisField;
}
$settings->yaxis = $yaxisFields;
}
if(isset($settings->metric))
{
$metricFields = $settings->metric;
foreach($metricFields as $metricIndex => $metricField)
{
if($isQuoteDataview && strpos($metricField->field, '.') !== false) $metricField->field = str_replace('.', '_', $metricField->field);
if($metricField->valOrAgg == 'value') $metricField->valOrAgg = 'sum';
if($metricField->valOrAgg == 'count_distinct') $metricField->valOrAgg = 'distinct';
if($metricField->valOrAgg == 'value_all') $metricField->valOrAgg = 'count';
if($metricField->valOrAgg == 'value_distinct') $metricField->valOrAgg = 'distinct';
$metricFields[$metricIndex] = $metricField;
}
$settings->metric = $metricFields;
}
$data->settings = json_encode(array($settings));
}
else
{
$data->step = 1;
$data->stage = 'draft';
}
if(!empty($filters))
{
$where = array();
$operatorMap = array('in' => 'IN', 'notin' => 'NOT IN', 'notnull' => 'IS NOT NULL', 'null' => 'IS NULL');
foreach($filters as $filter)
{
$operator = zget($operatorMap, $filter->operator);
$value = '';
if(!in_array($filter->operator, array('null', 'notnull')))
{
if(!is_array($filter->value))
{
$value = "'" . $filter->value . "'";
}
else
{
$values = array();
foreach($filter->value as $v) $values[] = $type == 'number' ? $v : "'" . $v . "'";
$value = '(' . implode(',', $values) . ')';
}
}
$where[] = $filter->field . ' ' . $operator . ' ' . $value;
}
if(stripos($data->sql, 'where') === false)
{
$data->sql .= ' WHERE ' . implode(' AND ', $where);
}
else
{
$data->sql .= ' ' . implode(' AND ', $where);
}
}
$this->dao->update(TABLE_CHART)->data($data)->autoCheck()->where('id')->eq($chart->id)->exec();
}
/* Update chart id in dashboard. */
foreach($dashboardLayoutPairs as $dashboardID => $layout)
{
$layout = json_decode($layout);
foreach($layout as $index => $chartLayout)
{
if($chartLayout->i->id != $chart->id) continue;
$chartLayout->i->type = $chart->type == 'table' ? 'pivot' : 'chart';
if($chart->type == 'table') $chartLayout->i->id = $pivotID;
$layout[$index] = $chartLayout;
}
$dashboardLayoutPairs[$dashboardID] = json_encode($layout);
$this->dao->update(TABLE_DASHBOARD)->set('layout')->eq(json_encode($layout))->where('id')->eq($dashboardID)->exec();
}
}
if(dao::isError()) return false;
return !dao::isError();
}
/**
* upgrade to pivot table.
*
* @param int $table
* @access public
* @return void
*/
public function upgradeToPivotTable($table, $dataviewList)
{
static $defaultPivotGroupID;
if(!$defaultPivotGroupID) $defaultPivotGroupID = $this->createDefaultGroup('pivot');
$pivot = new stdclass();
$pivot->dimension = 1;
$pivot->group = $defaultPivotGroupID;
$pivot->stage = 'published';
$pivot->step = 4;
$pivot->sql = $table->sql;
$pivot->createdBy = $table->createdBy;
$pivot->createdDate = $table->createdDate;
$pivot->dataset = $table->dataset;
$name = array();
$name['zh-cn'] = $table->name;
$pivot->name = json_encode($name);
$desc = array();
$desc['zh-cn'] = $table->desc;
$pivot->desc = json_encode($desc);
$pivotSettings = new stdclass();
$tableSettings = json_decode($table->settings);
$filters = array();
if($tableSettings && !empty($tableSettings->column))
{
$isQuoteDataview = isset($dataviewList[$table->dataset]);
$index = 1;
foreach($tableSettings->group as $group)
{
$groupKey = "group{$index}";
$pivotSettings->$groupKey = ($isQuoteDataview && strpos($group->field, '.') !== false) ? str_replace('.', '_', $group->field) : $group->field;
$index ++;
}
$columns = array();
foreach($tableSettings->column as $tableColumn)
{
$column = new stdclass();
$column->field = ($isQuoteDataview && strpos($tableColumn->field, '.') !== false) ? str_replace('.', '_', $tableColumn->field) : $tableColumn->field;
$column->stat = $tableColumn->valOrAgg;
if($column->stat == 'value')
{
$column->stat = 'sum';
/* 将按值统计的列升级到透视表的分组。*/
$groupKey = "group{$index}";
$pivotSettings->$groupKey = $column->field;
}
if($column->stat == 'value_all') $column->stat = 'count';
if($column->stat == 'count_distinct' || $column->stat == 'value_distinct') $column->stat = 'distinct';
$columns[] = $column;
}
$pivotSettings->columns = $columns;
if(!empty($tableSettings->filter)) $filters = $tableSettings->filter;
}
else
{
$pivot->stage = 'draft';
$pivot->step = 1;
}
$pivot->settings = json_encode($pivotSettings);
if(isset($dataviewList[$table->dataset]))
{
$dataview = $dataviewList[$table->dataset];
$pivot->sql = 'SELECT * FROM ' . $dataview->view;
$pivot->fields = isset($dataview->fields) ? $dataview->fields : '';
}
elseif($pivot->sql)
{
$fieldSettings = $this->getFieldSettings($table->sql);
$pivot->fields = json_encode($fieldSettings);
}
if(!empty($filters))
{
$where = array();
$operatorMap = array('in' => 'IN', 'notin' => 'NOT IN', 'notnull' => 'IS NOT NULL', 'null' => 'IS NULL');
foreach($filters as $filter)
{
$operator = zget($operatorMap, $filter->operator);
$value = '';
if(!in_array($filter->operator, array('null', 'notnull')))
{
if(!is_array($filter->value))
{
$value = "'" . $filter->value . "'";
}
else
{
$values = array();
foreach($filter->value as $v) $values[] = $type == 'number' ? $v : "'" . $v . "'";
$value = '(' . implode(',', $values) . ')';
}
}
$where[] = $filter->field . ' ' . $operator . ' ' . $value;
}
if(stripos($pivot->sql, 'where') === false)
{
$pivot->sql .= ' WHERE ' . implode(' AND ', $where);
}
else
{
$pivot->sql .= ' ' . implode(' AND ', $where);
}
}
/* Process fieldSettings. */
$this->dao->insert(TABLE_PIVOT)->data($pivot)->autoCheck()->exec();
$pivotID = $this->dao->lastInsertID();
if(dao::isError()) return false;
$this->dao->delete()->from(TABLE_CHART)->where('id')->eq($table->id)->exec();
return $pivotID;
}
/**
* Process report data.
*
* @access public
* @return bool
*/
public function processReport()
{
$reports = $this->dao->select('*')->from(TABLE_REPORT)->fetchAll('id', false);
$groupCollectors = $this->dao->select('collector, id')->from(TABLE_MODULE)->where('type')->eq('pivot')->andWhere('root')->eq(1)->andWhere('collector')->ne('')->fetchPairs();
$this->loadModel('pivot');
$this->loadModel('dataview');
foreach($reports as $report)
{
$data = new stdclass();
$data->dimension = 1;
$data->name = $report->name;
$data->sql = $this->pivot->replaceTableNames($report->sql);
$data->vars = $report->vars;
$data->langs = $report->langs;
$data->stage = 'published';
$data->step = 4;
$data->desc = $report->desc;
$data->createdBy = $report->addedBy;
$data->createdDate = $report->addedDate;
/* Process group. */
$modules = explode(',', trim($report->module, ','));
$groups = array();
foreach($modules as $module)
{
if(isset($groupCollectors[$module]))
{
$groups[] = $groupCollectors[$module];
}
else
{
if(!isset($defaultGroupID))
{
$defaultGroupID = $this->dao->select('id')->from(TABLE_MODULE)
->where('type')->eq('pivot')
->andWhere('name')->eq($this->lang->upgrade->defaultGroup)
->andWhere('root')->eq(1)
->andWhere('parent')->ne(0)
->fetch('id');
if(!$defaultGroupID) $defaultGroupID = $this->createDefaultGroup('pivot');
}
$groups[] = $defaultGroupID;
}
}
$data->group = implode(',', $groups);
/* Process vars. */
$vars = json_decode($report->vars, true);
if($vars)
{
$filters = array();
foreach($vars['varName'] as $index => $varName)
{
$filter = new stdclass();
$filter->from = 'query';
$filter->field = $varName;
$filter->name = $vars['showName'][$index];
$filter->type = $vars['requestType'][$index];
$filter->typeOption = $filter->type == 'select' ? zget($vars['selectList'], $index, '') : '';
$filter->default = isset($vars['default']) ? zget($vars['default'], $index, '') : '';
$filters[] = $filter;
}
$data->filters = json_encode($filters);
}
/* Process settings. */
$settings = new stdclass();
$columns = array();
if($report->params)
{
$params = json_decode($report->params);
$settings->group1 = $params->group1;
$settings->group2 = $params->group2;
$settings->columnTotal = 'sum';
foreach($params->reportField as $index => $field)
{
$column = new stdclass();
$column->field = $field;
$column->slice = $field;
$column->stat = isset($params->reportType) ? zget($params->reportType, $index, 'noStat') : 'noStat';
$column->showTotal = (isset($params->reportTotal) && zget($params->reportTotal, $index, '') == '1') ? 'sum' : 'noShow';
$column->showMode = (isset($params->percent) && zget($params->percent, $index, '') == '1' && isset($params->contrast) && zget($params->contrast, $index, '') == 'crystalTotal') ? 'total' : 'default';
$column->monopolize = isset($params->showAlone) ? zget($params->showAlone, $index, '0') : '0';
if($column->stat == 'sum')
{
$sumAppend = isset($params->sumAppend) ? zget($params->sumAppend, $index, $field) : $field;
if($sumAppend == $field) $column->slice = 'noSlice';
if($sumAppend != $field) $column->field = $sumAppend;
}
$columns[] = $column;
}
}
else
{
$column = new stdclass();
$column->field = '';
$column->stat = '';
$column->slice = 'noSlice';
$column->showMode = 'default';
$column->showTotal = 'noShow';
$columns[] = $column;
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $this->dbh->query($data->sql);
if(!$stmt)
{
$data->stage = 'draft';
$data->step = 1;
}
else
{
$settings->summary = 'notuse';
}
}
$settings->columns = $columns;
/* Process fieldSettings. */
$sql = $data->sql;
if(!empty($filters))
{
foreach($filters as $filter)
{
$filterDefault = isset($filter->default) ? $filter->default : '';
if($filter->type == 'date') $filterDefault = $this->pivot->processDateVar($filterDefault);
$filterDefault = $filterDefault === null ? "NULL" : "'{$filterDefault}'";
$sql = str_replace('$' . $filter->field, $filterDefault, $sql);
}
}
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $this->dbh->query($sql);
if(!$stmt) /* Fix bug #35559. */
{
$data->stage = 'draft';
$fieldSettings = array();
}
else
{
$fieldSettings = $this->getFieldSettings($sql);
}
$data->settings = json_encode($settings);
$data->fields = json_encode($fieldSettings);
$this->dao->insert(TABLE_PIVOT)->data($data)->autoCheck()->exec();
}
return !dao::isError();
}
/**
* Get field settings.
*
* @param string $sql
* @access public
* @return array
*/
public function getFieldSettings($sql)
{
if(!$sql) return array();
$this->loadModel('bi');
$this->loadModel('dataview');
$this->loadModel('chart');
$columns = $this->bi->getColumnsType($sql);
$columnFields = array();
foreach($columns as $column => $type) $columnFields[$column] = $column;
$tableAndFields = $this->bi->getTableAndFields($sql);
$tables = $tableAndFields['tables'];
$fields = $tableAndFields['fields'];
$moduleNames = array();
if($tables) $moduleNames = $this->dataview->getModuleNames($tables);
list($fieldPairs, $relatedObject) = $this->dataview->mergeFields($columnFields, $fields, $moduleNames);
$fieldSettings = array();
foreach($fieldPairs as $field => $name)
{
$relatedTable = zget($relatedObject, $field);
$fieldSetting = new stdclass();
$fieldSetting->object = $relatedTable;
$fieldSetting->field = $field;
$fieldSetting->type = 'string';
$fieldSettings[$field] = $fieldSetting;
}
return $fieldSettings;
}
/**
* Convert doc collect.
*
* @access public
* @return bool
*/
public function convertDocCollect()
{
$this->saveLogs('Run Method ' . __FUNCTION__);
$fields = $this->dao->descTable(TABLE_DOC);
foreach($fields as $field)
{
if($field->field == 'collects') return true;
}
$users = $this->dao->select('account')->from(TABLE_USER)->fetchPairs();
$docs = $this->dao->select('id,collector')->from(TABLE_DOC)->where('collector')->ne('')->fetchPairs();
$this->dao->update(TABLE_DOC)->set('collector')->eq(0)->exec();
$this->dao->exec("ALTER TABLE " . TABLE_DOC . " CHANGE `collector` `collects` smallint unsigned NOT NULL DEFAULT '0'");
$data = new stdclass();
$data->action = 'collect';
$data->date = helper::now();
foreach($docs as $id => $collector)
{
$collects = 0;
$data->doc = $id;
foreach(explode(',', $collector) as $account)
{
$account = trim($account);
if(empty($account)) continue;
if(!isset($users[$account])) continue;
$data->actor = $account;
$this->dao->insert(TABLE_DOCACTION)->data($data)->autoCheck()->exec();
if(dao::isError()) continue;
$collects++;
}
if($collects) $this->dao->update(TABLE_DOC)->set('collects')->eq($collects)->where('id')->eq($id)->exec();
}
return true;
}
/**
* 在功能设置中设置UR开关.
* Set UR switch status in feature switch.
*
* @access public
* @return bool
*/
public function setURSwitchStatus(): bool
{
if(is_numeric($this->fromVersion[0]) && version_compare($this->fromVersion, '18.2', '>=')) return true;
if(strpos($this->fromVersion, 'biz') !== false && version_compare($this->fromVersion, 'biz8.2', '>=')) return true;
if(strpos($this->fromVersion, 'max') !== false && version_compare($this->fromVersion, 'max4.2', '>=')) return true;
$URSwitchStatus = $this->loadModel('setting')->getItem("owner=system&module=custom&key=URAndSR");
if(!$URSwitchStatus)
{
$closedFeatures = $this->setting->getItem('owner=system&module=common&key=closedFeatures');
if(strpos($closedFeatures, 'productUR') === false) $closedFeatures .= ',productUR';
$this->setting->setItem('system.common.closedFeatures', trim($closedFeatures, ','));
}
return true;
}
/**
* 处理devOps上线步骤的历史记录。
* Process deploy step action. Delete deploystep action where id not in zt_deploy.
*
* @access public
* @return bool
*/
public function processDeployStepAction()
{
$steps = $this->dao->select('*')->from(TABLE_DEPLOYSTEP)->fetchAll('id');
if($steps) $this->dao->delete()->from(TABLE_ACTION)->where('objectType')->eq('deploystep')->andWhere('objectID')->notIN(array_keys($steps))->exec();
}
/**
* 更新BI相关zt_pivot表的sql字段。
* Update BI SQL for 18.4.stable new function: dataview model.php checkUniColumn().
*
* @access public
* @return void
*/
public function updateBISQL()
{
$alpha1File = $this->getUpgradeFile('18.4.alpha1');
$beta1File = $this->getUpgradeFile('18.4.beta1');
$alpha1SQL = explode(";", file_get_contents($alpha1File));
$beta1SQL = explode(";", file_get_contents($beta1File));
$execSQL = array();
foreach($alpha1SQL as $sql) if(strpos($sql, '`zt_pivot`') !== false) $execSQL[] = $sql;
foreach($beta1SQL as $sql) if(strpos($sql, '`zt_pivot`') !== false) $execSQL[] = $sql;
/* Update stage to published and update sql. */
foreach($execSQL as $sql)
{
$sql = str_replace('zt_', $this->config->db->prefix, $sql);
$sql = trim($sql);
$this->dbh->exec($sql);
}
}
/**
* 更新透视表的阶段。
* Update pivot stage.
*
* @access public
* @return void
*/
public function updatePivotStage()
{
$this->loadModel('pivot');
$this->loadModel('chart');
$this->loadModel('dataview');
$this->loadModel('bi');
$pivotList = $this->dao->select('*')->from(TABLE_PIVOT)->where('deleted')->eq(0)->fetchAll('id', false);
foreach($pivotList as $pivotID => $pivot)
{
if($pivot->stage == 'draft' || empty($pivot->sql)) continue;
$filters = json_decode($pivot->filters, true);
$filters = $this->pivot->setFilterDefault(empty($filters) ? array() : $filters);
$sql = trim(str_replace(';', '', $pivot->sql));
$sql = $this->bi->parseSqlVars($sql, $filters);
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $this->dbh->query($sql);
if(!$stmt || !$this->dataview->checkUniColumn($sql))
{
$this->dao->update(TABLE_PIVOT)->set('stage')->eq('draft')->where('id')->eq($pivotID)->exec();
}
}
}
/**
* 修复丢失的工作流字段。
* Fix missed rnd workflow field.
*
* @access public
* @return void
*/
public function fixMissedFlowField()
{
if($this->config->edition == 'open' || !in_array($this->fromVersion, $this->config->upgrade->missedFlowFieldVersions)) return;
$this->loadModel('workflow');
$this->loadModel('workflowaction');
$this->loadModel('workflowlayout');
$this->upgradeTao->createModules2Workflow();
$this->upgradeTao->createActions2Workflow();
$this->upgradeTao->createLayouts2Workflow();
}
/**
* 更新透视表中字段的类型。
* Update pivot fields type, pivotID maybe is 1008,1012,1013,1015,1020,1027.
*
* @access public
* @return void
*/
public function updatePivotFieldsType()
{
$fieldsPairs = array();
$fieldsPairs[] = array('maybeID' => 1008, 'beforefields' => '"execution":{"object":"project","field":"execution","type":"string"},"type":{"object":"project","field":"type","type":"string"}',
'fields' => '{"id":{"object":"project","field":"id","type":"string"},"project":{"object":"project","field":"project","type":"string"},"execution":{"object":"project","field":"execution","type":"string"},"type":{"object":"task","field":"type","type":"option"},"taskID":{"object":"task","field":"taskID","type":"string"},"projectstatus":{"object":"task","field":"projectstatus","type":"string"}}',
'langs' => '{"project":{"zh-cn":"\\u9879\\u76ee\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"execution":{"zh-cn":"\\u6267\\u884c\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"id":{"zh-cn":"\\u9879\\u76eeID","zh-tw":"","en":"","de":"","fr":""},"type":{"zh-cn":"\\u4efb\\u52a1\\u7c7b\\u578b","zh-tw":"","en":"","de":"","fr":""},"taskID":{"zh-cn":"taskID","zh-tw":"","en":"","de":"","fr":""},"projectstatus":{"zh-cn":"projectstatus","zh-tw":"","en":"","de":"","fr":""}}');
$fieldsPairs[] = array('maybeID' => 1012, 'beforefields' => '"project":{"object":"projectstory","field":"project","type":"string"},"execution":{"object":"project","field":"execution","type":"string"},"status":{"object":"project","field":"status","type":"string"}',
'fields' => '{"id":{"object":"project","field":"id","type":"string"},"project":{"object":"projectstory","field":"project","type":"string"},"execution":{"object":"project","field":"execution","type":"string"},"status":{"object":"story","field":"status","type":"option"}}',
);
$fieldsPairs[] = array('maybeID' => 1013, 'beforefields' => '"execution":{"object":"project","field":"execution","type":"string"},"stage":{"object":"project","field":"stage","type":"string"}}',
'fields' => '{"id":{"object":"project","field":"id","type":"string"},"project":{"object":"projectstory","field":"project","type":"string"},"execution":{"object":"project","field":"execution","type":"string"},"stage":{"object":"story","field":"stage","type":"option"}}',
'langs' => '{"project":{"zh-cn":"\\u9879\\u76ee\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"execution":{"zh-cn":"\\u6267\\u884c\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"id":{"zh-cn":"\\u9879\\u76eeID","zh-tw":"","en":"","de":"","fr":""},"stage":{"zh-cn":"\\u9636\\u6bb5","zh-tw":"","en":"","de":"","fr":""}}');
$fieldsPairs[] = array('maybeID' => 1015, 'beforefields' => '"bugID":{"object":"bug","field":"bugID","type":"string"},"status":{"object":"project","field":"status","type":"string"}',
'fields' => '{"id":{"object":"project","field":"id","type":"string"},"project":{"object":"project","field":"project","type":"string"},"t3id":{"object":"project","field":"t3id","type":"string"},"execution":{"object":"project","field":"execution","type":"string"},"bugID":{"object":"bug","field":"bugID","type":"string"},"status":{"object":"bug","field":"status","type":"option"}}',
);
$fieldsPairs[] = array('maybeID' => 1020, 'beforefields' => '"bugID":{"object":"project","field":"bugID","type":"string"},"type":{"object":"product","field":"type","type":"string"}',
'fields' => '{"id":{"object":"product","field":"id","type":"string"},"name":{"object":"product","field":"name","type":"string"},"bugID":{"object":"project","field":"bugID","type":"string"},"type":{"object":"bug","field":"type","type":"option"}}',
'langs' => '{"count":{"zh-cn":"\\u9700\\u6c42\\u6570","zh-tw":"\\u9700\\u6c42\\u6570","en":"Stories"},"done":{"zh-cn":"\\u5b8c\\u6210\\u6570","zh-tw":"\\u5b8c\\u6210\\u6570","en":"Done"},"id":{"zh-cn":"\\u7f16\\u53f7","zh-tw":"","en":"","de":"","fr":""},"name":{"zh-cn":"\\u4ea7\\u54c1\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"bugID":{"zh-cn":"bugID","zh-tw":"","en":"","de":"","fr":""},"type":{"zh-cn":"Bug\\u7c7b\\u578b","zh-tw":"","en":"","de":"","fr":""}}');
$fieldsPairs[] = array('maybeID' => 1027, 'beforefields' => '"bugID":{"object":"bug","field":"bugID","type":"string"},"type":{"object":"project","field":"type","type":"string"}',
'fields' => '{"id":{"object":"project","field":"id","type":"string"},"project":{"object":"project","field":"project","type":"string"},"t3id":{"object":"project","field":"t3id","type":"string"},"execution":{"object":"project","field":"execution","type":"string"},"bugID":{"object":"bug","field":"bugID","type":"string"},"type":{"object":"bug","field":"type","type":"option"}}',
'langs' => '{"stories":{"zh-cn":"\\u9700\\u6c42\\u6570","zh-tw":"\\u9700\\u6c42\\u6570","en":"Stories"},"tasks":{"zh-cn":"\\u4efb\\u52a1\\u6570","zh-tw":"\\u4efb\\u52a1\\u6570","en":"Tasks"},"undoneStory":{"zh-cn":"\\u5269\\u4f59\\u9700\\u6c42\\u6570","zh-tw":"\\u5269\\u4f59\\u9700\\u6c42\\u6570","en":"Undone Story"},"undoneTask":{"zh-cn":"\\u5269\\u4f59\\u4efb\\u52a1\\u6570","zh-tw":"\\u5269\\u4f59\\u4efb\\u52a1\\u6570","en":"Undone Task"},"consumed":{"zh-cn":"\\u5df2\\u6d88\\u8017\\u5de5\\u65f6","zh-tw":"\\u5df2\\u6d88\\u8017\\u5de5\\u65f6","en":"Cost(h)"},"left":{"zh-cn":"\\u5269\\u4f59\\u5de5\\u65f6","zh-tw":"\\u5269\\u4f59\\u5de5\\u65f6","en":"Left(h)"},"consumedPercent":{"zh-cn":"\\u8fdb\\u5ea6","zh-tw":"\\u8fdb\\u5ea6","en":"Process"},"execution":{"zh-cn":"\\u6267\\u884c\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"id":{"zh-cn":"\\u9879\\u76eeID","zh-tw":"","en":"","de":"","fr":""},"project":{"zh-cn":"\\u9879\\u76ee\\u540d\\u79f0","zh-tw":"","en":"","de":"","fr":""},"bugID":{"zh-cn":"bugID","zh-tw":"","en":"","de":"","fr":""},"type":{"zh-cn":"Bug\\u7c7b\\u578b","zh-tw":"","en":"","de":"","fr":""}}');
foreach($fieldsPairs as $field)
{
$pivotID = $this->dao->select('id')->from(TABLE_PIVOT)->where('fields')->like("%{$field['beforefields']}%")->fetch('id');
if(empty($pivotID)) continue;
$data = new stdclass();
if(isset($field['fields'])) $data->fields = $field['fields'];
if(isset($field['langs'])) $data->langs = $field['langs'];
$this->dao->update(TABLE_PIVOT)->data($data)->where('id')->eq($pivotID)->exec();
}
}
/**
* 为18_5, biz8_5, max4_5这三个版本的用户故事添加创建历史记录。
* Add create action for story when version is 18_5, biz8_5, max4_5.
*
* @access public
* @return void
*/
public function addCreateAction4Story(): void
{
if(!in_array($this->fromVersion, array('18_5', 'biz8_5', 'max4_5'))) return;
$stories = $this->dao->select('id,product,openedBy,openedDate,vision')->from(TABLE_STORY)->where('openedDate')->ge('2023-07-12')->fetchAll('id');
foreach($stories as $story)
{
$firstAction = $this->dao->select('*')->from(TABLE_ACTION)->where('objectType')->eq('story')->andWhere('objectID')->eq($story->id)->orderBy('date,id')->fetch();
if(empty($firstAction) || $firstAction->action != 'opened')
{
$actionDate = $story->openedDate;
if($firstAction->date <= $actionDate) $actionDate = date('Y-m-d H:i:s', strtotime($firstAction->date) - 1);
$action = new stdclass();
$action->objectType = 'story';
$action->objectID = $story->id;
$action->product = ',' . $story->product . ',';
$action->project = 0;
$action->execution = 0;
$action->actor = $story->openedBy;
$action->action = 'opened';
$action->date = $actionDate;
$action->comment = '';
$action->extra = '';
$action->read = 0;
$action->vision = $story->vision;
$action->efforted = 0;
$this->dao->insert(TABLE_ACTION)->data($action)->exec();
}
}
}
/**
* 当从12.x升级到18.x时,删除产品线必填字段。
* Remove product line required fields when 12.x upgrade to 18.x.
*
* @access public
* @return bool
*/
public function removeProductLineRequired(): bool
{
$createRequired = $this->loadModel('setting')->getItem('owner=system&module=product&section=create&key=requiredFields');
$editRequired = $this->setting->getItem('owner=system&module=product&section=edit&key=requiredFields');
$createRequired = str_replace(',line,', ',', ",$createRequired,");
$editRequired = str_replace(',line,', ',', ",$editRequired,");
if(!empty(trim($createRequired, ','))) $this->setting->setItem('system.product.create.requiredFields', trim($createRequired, ','));
if(!empty(trim($editRequired, ','))) $this->setting->setItem('system.product.edit.requiredFields', trim($editRequired, ','));
return true;
}
/**
* 将旧的度量指标转换为新的度量指标。
* Convert old metrics to new metrics.
*
* @access public
* @return bool
*/
public function processOldMetrics(): bool
{
$this->loadModel('metric');
$scopeMap = $this->config->metric->oldScopeMap;
$purposeMap = $this->config->metric->oldPurposeMap;
$objectMap = $this->config->metric->oldObjectMap;
$oldMetrics = $this->dao->select('*')->from(TABLE_BASICMEAS)->where('deleted')->eq('0')->orderBy('order_asc')->fetchAll('id', false);
foreach($oldMetrics as $oldMetric)
{
$metric = new stdclass();
$metric->scope = zget($scopeMap, $oldMetric->scope, 'other');
$metric->purpose = zget($purposeMap, $oldMetric->purpose, 'other');
$metric->object = zget($objectMap, $oldMetric->object, 'other');
$metric->stage = 'wait';
$metric->type = 'sql';
$metric->name = $oldMetric->name;
$metric->code = $oldMetric->code;
$metric->desc = '';
$metric->definition = $oldMetric->definition;
$metric->createdBy = $oldMetric->createdBy;
$metric->createdDate = helper::isZeroDate($oldMetric->createdDate) ? null : $oldMetric->createdDate;
$metric->editedBy = $oldMetric->editedBy;
$metric->editedDate = helper::isZeroDate($oldMetric->editedDate) ? null : $oldMetric->editedDate;
$metric->builtin = '1';
$metric->fromID = $oldMetric->id;
$metric->order = 0;
$metric->deleted = $oldMetric->deleted;
$this->dao->insert(TABLE_METRIC)->data($metric)->exec();
$metricID = $this->dao->lastInsertID();
$this->loadModel('action')->create('metric', $metricID, 'created', '', '', 'system');
}
return !dao::isError();
}
/**
* processHistoryOfStory方法的别名。
* Alias for processHistoryOfStory.
*
* @access public
* @return void
*/
public function processHistoryDataForMetric(): void
{
$this->processHistoryOfStory();
}
/**
* 构建用户需求的历史记录。
* Process history of story.
*
* @access public
* @return void
*/
public function processHistoryOfStory(): void
{
$releasedStorys = $this->upgradeTao->getReleasedStorys();
foreach($releasedStorys as $releasedStory) $this->dao->update(TABLE_STORY)->set('releasedDate')->eq($releasedStory->date)->where('id')->eq($releasedStory->id)->exec();
}
/**
* Upgrade testtask members.
*
* @access public
* @return void
*/
public function upgradeTesttaskMembers(): void
{
$memberGroup = $this->dao->select('task,lastRunner')->from(TABLE_TESTRUN)
->where('lastRunner')->ne('')
->fetchGroup('task', 'lastRunner');
foreach($memberGroup as $taskID => $members)
{
$members = implode(',', array_keys($members));
$this->dao->update(TABLE_TESTTASK)->set('members')->eq($members)->where('id')->eq($taskID)->exec();
}
}
/**
* 删除瀑布流通用报表。
* Delete waterfall general report block.
*
* @access public
* @return true
*/
public function deleteGeneralReportBlock(): bool
{
$closedBlocks = $this->loadModel('setting')->getItem('owner=system&module=block&key=closed');
if(strpos($closedBlocks, 'waterfallgeneralreport') !== false)
{
$closedBlocks = str_replace(',project|waterfallgeneralreport', '', $closedBlocks);
$this->setting->setItem('system.block.closed', $closedBlocks);
}
$fields = $this->dao->descTable(TABLE_BLOCK);
foreach($fields as $field)
{
if($field->field == 'block') $this->dao->delete()->from(TABLE_BLOCK)->where('block')->eq('waterfallgeneralreport')->exec();
}
return true;
}
/**
* Sync xuanxuan https config.
*
* @access public
* @return void
*/
public function syncXXHttpsConfig()
{
$this->dao->update(TABLE_CONFIG)->set('value')->eq('off')->where('`key`')->eq('isHttps')->andWhere('`section`')->eq('xuanxuan')->andWhere('`value`')->eq('0')->exec();
$this->dao->update(TABLE_CONFIG)->set('value')->eq('on')->where('`key`')->eq('isHttps')->andWhere('`section`')->eq('xuanxuan')->andWhere('`value`')->eq('1')->exec();
return true;
}
/**
* Create demo API.
*
* @access public
* @return void
*/
public function createDemoAPI()
{
$this->loadModel('api')->createDemoData($this->lang->api->zentaoAPI, commonModel::getSysURL() . $this->app->config->webRoot . 'api.php/v1', '16.0');
return true;
}
/**
* Complete the database for different edition.
*
* @access public
* @return void
*/
public function completionAllSQL()
{
switch($this->fromEdition)
{
case 'open':
$this->execSQL($this->getUpgradeFile('proinstall'));
case 'pro':
$this->execSQL($this->getUpgradeFile('bizinstall'));
case 'biz':
$this->execSQL($this->getUpgradeFile('maxinstall'));
$this->execSQL($this->getUpgradeFile('functionss'));
}
}
/**
* 为喧喧表添加adminInvite字段。
* Add adminInvite field for xuanxuan.
*
* @access public
* @return void
*/
public function addAdminInviteField(): void
{
$table = $this->config->db->prefix . 'im_chat';
$exists = $this->checkFieldsExists($table, 'adminInvite');
if(!$exists) $this->dbh->exec("ALTER TABLE {$table} ADD `adminInvite` enum('0','1') NOT NULL DEFAULT '0' AFTER `mergedChats`");
}
/**
* 安装IPD。
* Insetall IPD.
*
* @access public
* @return void
*/
public function installIPD(): void
{
$fromVersion = $this->fromVersion;
$ipdinstall = false;
if(is_numeric($fromVersion[0]) && version_compare($fromVersion, '18.5', '<=')) $ipdinstall = true;
if(strpos($fromVersion, 'pro') !== false) $ipdinstall = true;
if(strpos($fromVersion, 'biz') !== false && version_compare($fromVersion, 'biz8.5', '<=')) $ipdinstall = true;
if(strpos($fromVersion, 'max') !== false && version_compare($fromVersion, 'max4.5', '<=')) $ipdinstall = true;
if($ipdinstall) $this->execSQL($this->getUpgradeFile('ipdinstall'));
if($fromVersion == 'ipd1.0.beta1') $this->execSQL($this->getUpgradeFile('ipd1.0.beta1'));
}
/**
* 停止旧的计划任务。
* Stop old cron.
*
* @access public
* @return void
*/
public function stopOldCron(): void
{
touch($this->app->getCacheRoot() . 'restartcron');
}
/**
* 检查是否包含一致性错误。
* Check has consistency error or not.
*
* @access public
* @return bool
*/
public function hasConsistencyError(): bool
{
$logFile = $this->getConsistencyLogFile();
if(!file_exists($logFile)) return false;
$lastTwoLines = array(0 => '', 1 => '');
$fh = fopen($logFile, 'r');
while(!feof($fh))
{
$lastTwoLines[0] = $lastTwoLines[1];
$lastTwoLines[1] = fgets($fh);
}
fclose($fh);
if(trim($lastTwoLines[0]) == 'HasError') return true;
return false;
}
/**
* 获取没有合并的产品数量。
* Get no merged product count.
*
* @access public
* @return int
*/
public function getNoMergedProductCount(): int
{
return (int)$this->dao->select('COUNT(1) AS count')->from(TABLE_PRODUCT)->where('program')->eq(0)->andWhere('vision')->eq('rnd')->fetch('count');
}
/**
* 获取没有合并的迭代数量。
* Get no merged sprint count.
*
* @access public
* @return int
*/
public function getNoMergedSprintCount(): int
{
return (int)$this->dao->select('COUNT(1) AS count')->from(TABLE_PROJECT)->where('vision')->eq('rnd')->andWhere('project')->eq(0)->andWhere('type')->eq('sprint')->andWhere('deleted')->eq('0')->fetch('count');
}
/**
* Add default traincourse priv to each group.
*
* @access public
* @return bool
*/
public function addDefaultTraincoursePriv(): bool
{
$groups = $this->dao->select('id')->from(TABLE_GROUP)->where('role')->ne('limited')->andWhere('vision')->eq('rnd')->fetchPairs();
$sqlTemplate = 'REPLACE INTO ' . TABLE_GROUPPRIV . " (`group`, `module`, `method`) VALUES ('%s', 'traincourse', 'browse'), ('%s', 'traincourse', 'viewCourse'), ('%s', 'traincourse', 'viewChapter'), ('%s', 'traincourse', 'practice'), ('%s', 'traincourse', 'practiceBrowse'), ('%s', 'traincourse', 'practiceView'), ('%s', 'traincourse', 'updatePractice');";
foreach($groups as $groupID)
{
$sql = str_replace('%s', (string)$groupID, $sqlTemplate);
$this->dbh->exec($sql);
}
return true;
}
/**
* Rename BI default module name.
*
* @access public
* @return bool
*/
public function renameBIModule(): bool
{
$moduleNames = array('zh' => '默认分组', 'en' => 'Default');
$replaceNames = array('zh' => '未分组', 'en' => 'Ungrouped');
$BIModules = array('pivot', 'chart');
foreach($BIModules as $BImodule)
{
foreach($moduleNames as $lang => $moduleName)
{
$modules = $this->dao->select('*')->from(TABLE_MODULE)->where('name')->eq($moduleName)->andWhere('type')->eq($BImodule)->fetchAll();
if(count($modules) == 2)
{
$data = new stdclass();
$data->name = $replaceNames[$lang];
$data->order = 1000;
foreach($modules as $module) $this->dao->update(TABLE_MODULE)->data($data)->where('id')->eq($module->id)->exec();
break;
}
}
}
return true;
}
/**
* Migrate xuanxuan client settings, make owners user accounts.
*
* @access public
* @return void
*/
public function migrateXuanClientSettings()
{
$existingSettings = $this->dao->select('`key`, `value`')->from(TABLE_CONFIG)
->where('owner')->eq('system')
->andWhere('module')->eq('chat')
->andWhere('section')->eq('settings')
->fetchAll();
if(empty($existingSettings)) return true;
foreach($existingSettings as $setting)
{
$migratedSetting = array('owner' => $setting->key, 'module' => 'chat', 'section' => 'clientSettings', 'key' => 'settings', 'value' => $setting->value);
$this->dao->insert(TABLE_CONFIG)->data($migratedSetting)->exec();
}
$this->dao->delete()->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('chat')->andWhere('section')->eq('settings')->exec();
return true;
}
/**
* Revert story custom fields.
*
* @access public
* @return void
*/
public function revertStoryCustomFields()
{
$this->dao->delete()->from(TABLE_CONFIG)->where('section')->eq('productBrowse')->andWhere('module')->eq('datatable')->andWhere('key')->eq('cols')->exec();
$this->dao->delete()->from(TABLE_CONFIG)->where('section')->eq('executionStory')->andWhere('module')->eq('datatable')->andWhere('key')->eq('cols')->exec();
return true;
}
/**
* Update metric datetype.
*
* @access public
* @return void
*/
public function updateMetricDateType()
{
$this->loadModel('metric');
foreach($this->config->metric->dateType as $code => $dateType)
{
$this->dao->update(TABLE_METRIC)->set('dateType')->eq($dateType)->where('code')->eq($code)->exec();
}
$this->dao->update(TABLE_METRIC)->set('dateType')->eq('nodate')->where('dateType')->eq('')->exec();
}
/**
* Migrate configured AI model (from pre 18.11) into table.
*
* @access public
* @return void
*/
public function migrateAIModelConfig()
{
$aiSettings = $this->loadModel('setting')->getItems('owner=system&module=ai');
if(empty($aiSettings)) return;
$aiConfig = new stdclass();
foreach($aiSettings as $item) $aiConfig->{$item->key} = $item->value;
$model = new stdclass();
$model->type = $aiConfig->type;
$model->vendor = $aiConfig->vendor ?: '';
$model->enabled = $aiConfig->status == 'on' ? '1' : '0';
$model->desc = $aiConfig->description ?: '';
$model->createdDate = helper::now();
$model->createdBy = 'system';
if(empty($model->type)) return;
$credentials = new stdclass();
$credentials->key = $aiConfig->key ?: '';
if($model->type == 'baidu-ernie')
{
$credentials->secret = $aiConfig->secret ?: '';
}
elseif($aiConfig->vendor == 'azure')
{
$credentials->resource = $aiConfig->resource ?: '';
$credentials->deployment = $aiConfig->deployment ?: '';
}
$model->credentials = json_encode($credentials);
$model->proxy = $aiConfig->proxyType == 'socks5' ? json_encode((object)array('type' => 'socks5', 'addr' => $aiConfig->proxyAddr)) : null;
$this->dao->insert(TABLE_AI_MODEL)->data($model)->exec();
if(!dao::isError()) $this->setting->deleteItems('owner=system&module=ai');
}
/**
* 检查办公应用是否有数据,没有就隐藏。
* Check if the office app has data, if not, hide it.
*
* @access public
* @return void
*/
public function hideOA()
{
$closedFeatures = $this->loadModel('setting')->getItem('owner=system&module=common&section=&key=closedFeatures');
if(strpos($closedFeatures, 'otherOA') !== false) return;
$attends = $this->dao->select('id')->from(TABLE_ATTEND)->count();
if($attends) return;
$leaves = $this->dao->select('id')->from(TABLE_LEAVE)->count();
if($leaves) return;
$lieus = $this->dao->select('id')->from(TABLE_LIEU)->count();
if($lieus) return;
$overtimes = $this->dao->select('id')->from(TABLE_OVERTIME)->count();
if($overtimes) return;
$this->setting->setItem('system.common.closedFeatures', trim($closedFeatures . ',otherOA', ','));
}
/**
* 检查是否添加了18.10.1版本的 AI 表,如果没有就添加。
* Check if the AI table of version 18.10.1 is added, if not, add it.
*
* @access public
* @return void
*/
public function update18101(): void
{
$count = $this->dao->select('COUNT(1) AS count')->from('information_schema.TABLES')->where('TABLE_SCHEMA')->eq($this->config->db->name)->andWhere('TABLE_NAME')->eq(str_replace('`', '', TABLE_AI_MODEL))->fetch('count');
if($count) return;
/* Execute open edition. */
$this->saveLogs('Execute 18_10_1');
$this->execSQL($this->getUpgradeFile('18.10.1'));
}
/**
* 检查是否执行了18.11版本的SQL。
* Exec 18.11 sql file.
*
* @access public
* @return void
*/
public function update1811(): void
{
/* Execute open edition. */
$this->saveLogs('Execute 18_11');
$this->execSQL($this->getUpgradeFile('18.11'));
}
/**
* 更新通过工作流添加的字段的默认值。
* Update the default value of the fields added through the workflow.
*
* @access public
* @return true
*/
public function updateWorkflowFieldDefaultValue(): bool
{
if($this->config->edition == 'open') return false;
$this->loadModel('workflowfield');
$tables = $this->dao->select('module, `table`')->from(TABLE_WORKFLOW)->fetchPairs();
$nullFields = $this->dao->select('module, field, type')->from(TABLE_WORKFLOWFIELD)
->where('buildin')->eq(0)
->andWhere('type')->in('date, datetime, text')
->andWhere('field')->notin('id, subStatus, deleted')
->fetchAll();
$notNullFields = $this->dao->select('module, field, type, length, `default`')->from(TABLE_WORKFLOWFIELD)
->where('buildin')->eq(0)
->andWhere('type')->notin('date, datetime, text')
->andWhere('field')->notin('id, subStatus, deleted')
->fetchAll();
if(!$nullFields && !$notNullFields) return true;
$sqls = array();
foreach($nullFields as $field)
{
if(!isset($tables[$field->module])) continue;
$table = $tables[$field->module];
$sqls[] = "ALTER TABLE `$table` MODIFY `$field->field` $field->type NULL;";
}
foreach($notNullFields as $field)
{
if(!isset($tables[$field->module])) continue;
$table = $tables[$field->module];
$isNumber = in_array($field->type, $this->config->workflowfield->numberTypes);
if($field->length)
{
if($field->type == 'decimal')
{
list($integerDigits, $decimalDigits) = explode(',', $field->length);
$field->length = $integerDigits + $decimalDigits . ',' . $decimalDigits;
}
$field->type .= "($field->length)";
}
$sql = "ALTER TABLE `$table` MODIFY `$field->field` $field->type NOT NULL";
if($field->default)
{
$sql .= ' DEFAULT ' . $this->dbh->quote($field->default) . ';';
}
else
{
$sql .= $isNumber ? ' DEFAULT 0;' : " DEFAULT '';";
}
$sqls[] = $sql;
}
if(!$sqls) return true;
try
{
foreach($sqls as $sql)
{
$this->saveLogs($sql);
$this->dbh->query($sql);
}
}
catch(PDOException $exception)
{
static::$errors[] = $exception->getMessage();
}
return true;
}
/**
* 将值为'0000-00-00'的日期类型字段值更新为NULL。
* Update the default value of the fields.
*
* @access public
* @return true
*/
public function updateZeroDateToNull()
{
set_time_limit(0);
if(in_array($this->config->db->driver, $this->config->mysqlDriverList))
{
try
{
$this->dao->exec("SET @@sql_mode=''");
$tables = $this->dao->select("TABLE_NAME, COLUMN_NAME, DATA_TYPE")->from('information_schema.COLUMNS')
->where("TABLE_SCHEMA")->eq($this->config->db->name)
->andWhere("TABLE_NAME")->like($this->config->db->prefix . '%')
->andWhere("DATA_TYPE")->in('date,datetime')
->fetchAll();
foreach($tables as $table)
{
$tableName = $table->TABLE_NAME;
$columnName = $table->COLUMN_NAME;
$columnType = $table->DATA_TYPE;
if(strpos($tableName, $this->config->db->prefix) !== 0) continue;
if($columnType == 'date')
{
$this->dbh->exec("UPDATE `$tableName` SET `$columnName` = NULL WHERE `$columnName` = '0000-00-00'");
}
elseif($columnType == 'datetime')
{
$this->dbh->exec("UPDATE `$tableName` SET `$columnName` = NULL WHERE `$columnName` = '0000-00-00 00:00:00'");
}
}
}
catch(PDOException $e)
{
$this->saveLogs($e->getMessage());
}
}
}
/**
* 将设置阶段时自定义字段更改为按项目模型保存。
* Set stage custom fields by project model.
*
* @access public
* @return true
*/
public function updateProgramplanCustom()
{
$createFields = $this->dao->select('*')->from(TABLE_CONFIG)
->where('module')->eq('programplan')
->andWhere('section')->eq('custom')
->andWhere('`key`')->eq('createFields')
->fetchAll('id', false);
foreach($createFields as $createField)
{
unset($createField->id);
$createField->key = 'createWaterfallFields'; //瀑布模型
$this->dao->insert(TABLE_CONFIG)->data($createField)->exec();
$createField->key = 'createWaterfallplusFields'; //融合瀑布模型
$this->dao->insert(TABLE_CONFIG)->data($createField)->exec();
}
/* 删除旧的config。*/
$this->dao->delete()->from(TABLE_CONFIG)->where('module')->eq('programplan')->andWhere('section')->eq('custom')->andWhere('`key`')->eq('createFields')->exec();
return true;
}
/**
* 为自定义过的需求阶段增加条目。
* Add items for custom story stage.
*
* @access public
* @return bool
*/
public function changeCustomStoryStage(): bool
{
$clientLang = $this->app->getClientLang();
$customStagePairs = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('story')
->andWhere('section')->eq('stageList')
->andWhere('lang')->eq($clientLang)
->fetchPairs();
if(empty($customStagePairs))
{
$clientLang = 'all';
$customStagePairs = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('story')
->andWhere('section')->eq('stageList')
->andWhere('lang')->eq($clientLang)
->fetchPairs();
}
if(empty($customStagePairs)) return false;
$this->dao->delete()->from(TABLE_LANG)
->where('module')->eq('story')
->andWhere('section')->eq('stageList')
->andWhere('lang')->eq($clientLang)
->exec();
foreach($this->lang->upgrade->storyStageList as $key => $value)
{
$item = new stdclass();
$item->lang = $clientLang;
$item->module = 'story';
$item->section = 'stageList';
$item->key = $key;
$item->value = isset($customStagePairs[$key]) ? $customStagePairs[$key] : $value;
$item->system = '1';
$item->vision = 'rnd';
$this->dao->insert(TABLE_LANG)->data($item)->exec();
}
return true;
}
/**
* 禅道20版本之前,用需和软需是关联关系,多对多,现改为父子关系,一对多。
* Before ZenTao 20, the relationship between use requirements and story is a many-to-many relationship, now changed to a parent-child relationship, a one-to-many relationship.
*
* @access public
* @return void
*/
public function processStoryRelation()
{
$this->loadModel('story');
$relations = $this->dao->select('t1.*, t2.parent as BParent, t2.isParent')->from(TABLE_RELATION)->alias('t1')
->leftJoin(TABLE_STORY)->alias('t2')->on('t1.BID = t2.id')
->where('AType')->eq('requirement')
->andWhere('BType')->eq('story')
->andWhere('relation')->eq('subdivideinto')
->orderBy('AID asc')
->fetchGroup('AID', 'BID');
$requirementRoadmaps = $this->dao->select('id,roadmap')->from(TABLE_STORY)
->where('id')->notin(array_keys($relations))
->andWhere('roadmap')->ne(0)
->andWhere('type')->eq('requirement')
->andWhere('stage')->eq('wait')
->andWhere('linkStories')->eq('')
->andWhere('linkRequirements')->eq('')
->fetchPairs('id');
$processd = array(); // 记录已处理的软件需求,一个软件需求只能有一个父需求。
foreach($relations as $AID => $storyList)
{
$hasChild = false;
$lastChildID = 0;
foreach($storyList as $BID => $story)
{
if($story->BParent > 0) continue; // 如果已经是子需求,无需处理。
if(!isset($processd[$BID])) // 没有被其它用需变为子需求,才处理。
{
$processd[$BID] = $BID;
$hasChild = true;
$lastChildID = $BID;
/* 当软件需求变为子需求后,需要更改这些字段。 */
$child = new stdclass();
$child->parent = $AID;
$child->parentVersion = $story->AVersion;
$child->path = ',' . $AID . ',' . $BID . ',';
$child->root = $AID;
$this->dao->update(TABLE_STORY)->data($child)->where('id')->eq($BID)->exec();
/* 如果该软件需求本身就是父需求,则将root和path同步给它的子需求。 */
if($story->isParent == '1')
{
$this->dao->update(TABLE_STORY)
->set('root')->eq($AID)
->set("path = concat(',', {$AID}, path)")
->where('parent')->eq($BID)
->exec();
}
/* 将用需软需的关联关系去掉,它们现在是父子关系了。 */
$this->dao->delete()->from(TABLE_RELATION)->where('AID')->eq($BID)->andWhere('BID')->eq($AID)->exec();
$this->dao->delete()->from(TABLE_RELATION)->where('AID')->eq($AID)->andWhere('BID')->eq($BID)->exec();
}
}
if($hasChild)
{
$this->dao->update(TABLE_STORY)->set('isParent')->eq('1')->where('id')->eq($AID)->exec();
$status = $this->dao->select('status')->from(TABLE_STORY)->where('id')->eq($AID)->fetch('status');
if($status == 'closed')
{
$this->dao->update(TABLE_STORY)->set('stage')->eq('closed')->where('id')->eq($AID)->exec();
continue;
}
/* 如果用需成为了父需求且状态不是关闭,则需要通过它最后一个子需求计算它的阶段、工时、状态等信息。 */
$this->story->setStage($lastChildID);
$this->story->updateParentStatus($lastChildID, $AID, false);
}
}
$roadmapList = $this->dao->select('id,status')->from(TABLE_ROADMAP)->where('id')->in(array_values($requirementRoadmaps))->fetchPairs('id');
foreach($requirementRoadmaps as $requirementID => $roadmapID)
{
if(!isset($roadmapList[$roadmapID])) continue;
$roadmapStatus = $roadmapList[$roadmapID];
$requirementStage = $roadmapStatus == 'launched' ? 'incharter' : 'inroadmap';
$this->dao->update(TABLE_STORY)->set('stage')->eq($requirementStage)->where('id')->eq($requirementID)->exec();
}
/* 其它没被处理成父子关系的细分关系,变成关联关系。 */
$this->dao->update(TABLE_RELATION)->set('relation')->eq('linkedto')->where('relation')->eq('subdivideinto')->exec();
$this->dao->update(TABLE_RELATION)->set('relation')->eq('linkedfrom')->where('relation')->eq('subdividedfrom')->exec();
}
/**
* 禅道20版本前,软需关联软需,用需关联用需,是以字符串形式记录在story表的linkStories字段中,现迁移到relation表中。
* Before ZenTao 20, the relationship between stories and stories, and the relationship between requirements and requirements, are recorded in the linkStories field of the story table in string form, and now migrated to the relation table.
*
* @access public
* @return void
*/
public function processLinkStories()
{
$doProcess = function($stories, $storyType)
{
$relation = new stdclass();
$relation->product = 0;
foreach($stories as $storyID => $linkStories)
{
$this->dao->delete()->from(TABLE_RELATION)->where('product')->eq(0)->andWhere('relation')->eq('linkedto')->andWhere('AType')->eq($storyType)->andWhere('BType')->eq($storyType)->andWhere('AID')->eq($storyID)->andWhere('BID')->in($linkStories)->exec();
$this->dao->delete()->from(TABLE_RELATION)->where('product')->eq(0)->andWhere('relation')->eq('linkedfrom')->andWhere('AType')->eq($storyType)->andWhere('BType')->eq($storyType)->andWhere('AID')->in($linkStories)->andWhere('BID')->eq($storyID)->exec();
$linkStories = explode(',', trim($linkStories, ','));
foreach($linkStories as $linkStoryID)
{
if(!$linkStoryID) continue;
$relation->AType = $storyType;
$relation->AID = $storyID;
$relation->BType = $storyType;
$relation->BID = $linkStoryID;
$relation->relation = 'linkedto';
$this->dao->insert(TABLE_RELATION)->data($relation)->exec();
$relation->AType = $storyType;
$relation->AID = $linkStoryID;
$relation->BType = $storyType;
$relation->BID = $storyID;
$relation->relation = 'linkedfrom';
$this->dao->insert(TABLE_RELATION)->data($relation)->exec();
}
}
};
$stories = $this->dao->select('id, linkStories')->from(TABLE_STORY)
->where('linkStories')->ne('')
->andWhere('deleted')->eq('0')
->fetchPairs();
$requirements = $this->dao->select('id, linkRequirements')->from(TABLE_STORY)
->where('linkRequirements')->ne('')
->andWhere('deleted')->eq('0')
->fetchPairs();
$doProcess($stories, 'story');
$doProcess($requirements, 'requirement');
}
/**
* 如果用户自定义过需求概念,则追加默认的业需概念。
* If the user has customized the concept of demand, append the default concept of business demand.
*
* @access public
* @return void
*/
public function addERName()
{
/* If the mode is light, disable the epic story. */
/* 如果是轻量模式,禁用业务需求和用户需求。 */
if(zget($this->config->global, 'mode', 'light') == 'light')
{
$this->loadModel('setting')->setItem('system.custom.enableER', '0');
}
$closedFeatures = $this->loadModel('setting')->getItem('owner=system&module=common&key=closedFeatures');
if(strpos($closedFeatures, 'productUR') !== false) $closedFeatures .= ',productER,';
$this->setting->setItem('system.common.closedFeatures', trim($closedFeatures, ','));
$lang = $this->app->getClientLang();
$URSRList = $this->dao->select('*')->from(TABLE_LANG)
->where('lang')->eq($lang)
->andWhere('module')->eq('custom')
->andWhere('section')->eq('URSRList')
->fetchAll('id', false);
foreach($URSRList as $URSR)
{
$value = json_decode($URSR->value);
if(isset($value->ERName)) continue;
$value->ERName = $this->lang->defaultERName;
$this->dao->update(TABLE_LANG)
->set('value')->eq(json_encode($value))
->where('id')->eq($URSR->id)
->exec();
}
}
/**
* 为IPD项目开发阶段追加TR4A评审点
* Add TR4A points to the development phase of IPD project
*
* @access public
* @return bool
*/
public function addTR4APoint(): bool
{
if($this->config->edition != 'ipd') return true;
$this->loadModel('review');
/* 获取所有已经设置过阶段的IPD项目。*/
/* Get all IPD projects that have been set up to phase. */
$projects = $this->dao->select('t1.project')->from(TABLE_OBJECT)->alias('t1')->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')->where('t2.model')->eq('ipd')->fetchPairs('project');
$data = new stdclass();
$data->title = $this->lang->review->reviewPoint->titleList['TR4A'];
$data->category = 'TR4A';
$data->type = 'reviewed';
$data->enabled = 0;
$data->range = 'all';
foreach($projects as $projectID)
{
$data->project = $projectID;
$this->dao->insert(TABLE_OBJECT)->data($data)->exec();
}
return true;
}
/**
* 更新升级大屏和度量项数据。
* Import screen and metric data.
*
* @access public
* @return bool
*/
public function upgradeScreenAndMetricData(): bool
{
$this->saveLogs('Run Method ' . __FUNCTION__);
$this->dao->clearTablesDescCache();
$metricSQLs = $this->loadModel('bi')->prepareBuiltinMetricSQL('update');
$screenSQLs = $this->bi->prepareBuiltinScreenSQL('update');
$upgradeSqls = array_merge($metricSQLs, $screenSQLs);
$this->dbh->beginTransaction();
try
{
foreach($upgradeSqls as $sql)
{
$sql = trim($sql);
if(empty($sql)) continue;
$this->saveLogs($sql);
$prefix = in_array($this->config->db->driver, $this->config->pgsqlDriverList) ? 'public' : $this->config->db->name;
$sql = str_replace('`zt_', $prefix . '.`zt_', $sql);
$sql = str_replace('zt_', $this->config->db->prefix, $sql);
$this->dbh->exec($sql);
}
}
catch(Error $e)
{
static::$errors[] = $e->getMessage();
$this->dbh->rollBack();
return false;
}
return $this->dbh->commit();
}
/**
* 更新升级BI内置数据。
* Import BI data.
*
* @access public
* @return bool
*/
public function upgradeBIData(): bool
{
$this->loadModel('bi');
$this->saveLogs('Run Method ' . __FUNCTION__);
$this->dao->clearTablesDescCache();
/* Prepare built-in sqls of bi. */
$chartSQLs = $this->bi->prepareBuiltinChartSQL('update');
$pivotSQLs = $this->bi->prepareBuiltinPivotSQL('update');
$upgradeSqls = array_merge($chartSQLs, $pivotSQLs);
$this->dbh->beginTransaction();
try
{
foreach($upgradeSqls as $sql)
{
$sql = trim($sql);
if(empty($sql)) continue;
$this->saveLogs($sql);
$prefix = in_array($this->config->db->driver, $this->config->pgsqlDriverList) ? 'public' : $this->config->db->name;
$sql = str_replace('`zt_', $prefix . '.`zt_', $sql);
$sql = str_replace('zt_', $this->config->db->prefix, $sql);
$this->dbh->exec($sql);
}
}
catch(Error $e)
{
static::$errors[] = $e->getMessage();
$this->dbh->rollBack();
return false;
}
return $this->dbh->commit();
}
/**
* 根据是否加载 APCu 扩展,开启缓存。
* Open cache by load APCu or not.
*
* @access public
* @return bool
*/
public function openCacheByAPCu(): bool
{
return $this->loadModel('install')->enableCache();
}
/**
* 补充分类项语言项。
* Complete classify lang.
*
* @access public
* @return bool
*/
public function completeClassifyLang()
{
$this->app->loadLang('install');
$classifyLang = $this->dao->select('*')->from(TABLE_LANG)
->where('lang')->eq('all')
->andWhere('module')->eq('process')
->andWhere('section')->in(array('scrumClassify', 'agileplusClassify', 'waterfallplusClassify'))
->andWhere('`key`')->in(array('support', 'engineering', 'project'))
->andWhere('vision')->eq('rnd')
->fetchGroup('section', 'key');
$classifyData = new stdclass();
$classifyData->lang = 'all';
$classifyData->module = 'process';
$classifyData->vision = 'rnd';
$classifyData->system = 1;
foreach(array('scrumClassify', 'agileplusClassify', 'waterfallplusClassify') as $section)
{
$classifyData->section = $section;
foreach($this->lang->install->langList as $langInfo)
{
if($langInfo['module'] != 'process') continue;
if(isset($classifyLang[$section][$langInfo['key']])) continue;
$classifyData->key = $langInfo['key'];
$classifyData->value = $langInfo['value'];
$this->dao->insert(TABLE_LANG)->data($classifyData)->exec();
}
}
return true;
}
/**
* 新增反馈的数据源,并且更新到工单的由反馈转化字段。
* Add feedback datasource and set options of ticket feedback.
*
* @access public
* @return bool
*/
public function addFeedbackDatasource()
{
$this->loadModel('feedback');
$datasource = new stdclass();
$datasource->type = 'sql';
$datasource->name = $this->lang->feedback->common;
$datasource->code = 'feedbacks';
$datasource->datasource = "select id,title from zt_feedback where deleted = '0'";
$datasource->keyField = 'id';
$datasource->valueField = 'title';
$datasource->buildin = '1';
$datasource->vision = 'rnd';
$datasource->vision = 'rnd';
$datasource->createdBy = 'system';
$datasource->createdDate = helper::now();
$this->dao->insert(TABLE_WORKFLOWDATASOURCE)->data($datasource)->autoCheck()->exec();
$datasourceID = $this->dao->lastInsertID();
$view = "view_datasource_$datasourceID";
$sql = "CREATE OR REPLACE VIEW $view (`id`, `title`) AS $datasource->datasource";
$this->dbh->query($sql);
$this->dao->update(TABLE_WORKFLOWDATASOURCE)->set('view')->eq($view)->where('id')->eq($datasourceID)->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('options')->eq($datasourceID)->where('module')->eq('ticket')->andWhere('field')->eq('feedback')->exec();
return !dao::isError();
}
/**
* 将瀑布项目的活动和文档同步到融合瀑。
* Synchronise the activities and zoutputs of the Waterfall project with the Waterfallplus project.
*
* @access public
* @return bool
*/
public function syncActivityAndOutput()
{
$models = array('waterfall' => 'waterfallplus', 'scrum' => 'agileplus');
$acitivies = $this->dao->select('*')->from(TABLE_ACTIVITY)->where('deleted')->eq(0)->orderBy('order_asc')->fetchGroup('process', 'id');
$zoutputs = $this->dao->select('*')->from(TABLE_ZOUTPUT)->where('deleted')->eq(0)->orderBy('order_asc')->fetchGroup('activity', 'id');
foreach($models as $model => $plusModel)
{
$plusProcesses = $this->dao->select('*')->from(TABLE_PROCESS)->where('deleted')->eq(0)->andWhere('model')->eq($plusModel)->orderBy('order_asc')->fetchAll('id', false);
if(!empty($plusProcesses))
{
$plusActivities = $this->dao->select('*')->from(TABLE_ACTIVITY)->where('deleted')->eq(0)->andWhere('process')->in(array_keys($plusProcesses))->orderBy('order_asc')->fetchAll();
if(!empty($plusActivities)) continue;
}
$processes = $this->dao->select('*')->from(TABLE_PROCESS)->where('deleted')->eq(0)->andWhere('model')->eq($model)->orderBy('order_asc')->fetchAll('id', false);
foreach($processes as $process)
{
if(!isset($acitivies[$process->id])) continue;
foreach($plusProcesses as $plusProcess)
{
if($plusProcess->name != $process->name || $plusProcess->type != $process->type || $plusProcess->abbr != $process->abbr) continue;
foreach($acitivies[$process->id] as $activity)
{
$data = clone $activity;
$data->process = $plusProcess->id;
$data->editedDate = $data->assignedDate = null;
unset($data->id);
$this->dao->insert(TABLE_ACTIVITY)->data($data)->autoCheck()->exec();
$newActivityID = $this->dao->lastInsertID();
if(!isset($zoutputs[$activity->id])) continue;
foreach($zoutputs[$activity->id] as $zoutput)
{
$data = clone $zoutput;
$data->activity = $newActivityID;
$data->editedDate = null;
unset($data->id);
$this->dao->insert(TABLE_ZOUTPUT)->data($data)->autoCheck()->exec();
}
}
}
}
}
return !dao::isError();
}
/**
* 升级推算已关闭需求池需求阶段。
* Process demand stage.
*
* @access public
* @return bool
*/
public function processDemandStage()
{
$demands = $this->dao->select('id')->from(TABLE_DEMAND)->fetchPairs('id');
if(empty($demands)) return true;
$this->dao->update(TABLE_DEMAND)->set('stage')->eq('closed')->where('status')->eq('closed')->exec();
$this->loadModel('demand')->updateDemandStage($demands);
return true;
}
/**
* 导入内置工作流。
* Import buildin workflow.
*
* @param string $vision
* @param string $importModule 指定导入的模块
* @access public
* @return void
*/
public function importBuildinWorkflow($vision = 'all', $importModule = '')
{
$this->loadModel('workflow');
$this->loadModel('workflowaction');
$this->loadModel('workflowfield');
$this->loadModel('workflowlayout');
$modules = $this->config->workflow->buildin->modules;
$visions = $this->config->workflow->buildin->visions;
$actions = $this->config->workflowaction->buildin->actions;
$actionTypes = $this->config->workflowaction->buildin->types;
$actionMethods = $this->config->workflowaction->buildin->methods;
$actionOpens = $this->config->workflowaction->buildin->opens;
$actionLayouts = $this->config->workflowaction->buildin->layouts;
$actionPositions = $this->config->workflowaction->buildin->positions;
$actionShows = $this->config->workflowaction->buildin->shows;
$fields = $this->config->workflowfield->buildin->fields;
$layouts = $this->config->workflowlayout->buildin->layouts;
$account = isset($this->app->user->account) ? $this->app->user->account : 'admin';
$now = helper::now();
/* Insert buildin modules to TABLE_WORKFLOW. */
$data = new stdclass();
$data->buildin = 1;
$data->createdBy = $account;
$data->createdDate = $now;
$data->status = 'normal';
foreach($modules as $app => $appModules)
{
$data->app = $app;
foreach($appModules as $module => $options)
{
if($importModule && strpos(",$importModule,", ",$module,") === false) continue;
$this->app->loadLang($module);
$data->vision = 'rnd';
$data->module = $module;
$data->name = isset($this->lang->$module->common) ? $this->lang->$module->common : $module;
$data->table = str_replace('`', '', zget($options, 'table', ''));
$data->navigator = zget($options, 'navigator', 'secondary');
if(!empty($visions[$module])) $data->vision = $visions[$module];
if($module == 'execution') $data->name = $this->lang->workflow->execution;
if($module == 'story') $data->name = $this->lang->SRCommon;
if($module == 'epic') $data->name = $this->lang->ERCommon;
if($module == 'requirement') $data->name = $this->lang->URCommon;
if($vision != 'all' && $vision != $data->vision) continue;
$this->dao->delete()->from(TABLE_WORKFLOW)->where('app')->eq($app)->andWhere('module')->eq($module)->andWhere('vision')->eq($data->vision)->exec();
$this->dao->insert(TABLE_WORKFLOW)->data($data)->exec();
}
}
/* Insert actions of buildin modules to TABLE_WORKFLOWACTION. */
$data = new stdclass();
$data->buildin = 1;
$data->role = 'buildin';
$data->extensionType = 'none';
$data->createdBy = $account;
$data->createdDate = $now;
foreach($actions as $module => $moduleActions)
{
if($importModule && strpos(",$importModule,", ",$module,") === false) continue;
$data->module = $module;
foreach($moduleActions as $action)
{
$data->action = $action;
/* Use default action name if not set flow action name. */
$name = '';
if(isset($this->lang->$module->$action)) $name = $this->lang->$module->$action;
if(empty($name) && in_array($action, array_keys($this->config->flowAction)))
{
$methodName = $this->config->flowAction[$action];
if(isset($this->lang->$module->$methodName)) $name = $this->lang->$module->$methodName;
}
if(empty($name) && isset($this->lang->workflowaction->default->actions[$action])) $name = $this->lang->workflowaction->default->actions[$action];
if(empty($name)) $name = $action;
$data->name = $name;
$data->method = $data->action;
$data->open = 'normal';
$data->layout = 'normal';
$data->type = 'single';
$data->position = 'browseandview';
$data->show = 'direct';
$data->vision = 'rnd';
if(isset($actionMethods[$module][$action])) $data->method = $actionMethods[$module][$action];
if(isset($actionOpens[$module][$action])) $data->open = $actionOpens[$module][$action];
if(isset($actionLayouts[$module][$action])) $data->layout = $actionLayouts[$module][$action];
if(isset($actionTypes[$module][$action])) $data->type = $actionTypes[$module][$action];
if(isset($actionPositions[$module][$action])) $data->position = $actionPositions[$module][$action];
if(isset($actionShows[$module][$action])) $data->show = $actionShows[$module][$action];
if(!empty($visions[$module])) $data->vision = $visions[$module];
if($vision != 'all' && $vision != $data->vision) continue;
$this->dao->delete()->from(TABLE_WORKFLOWACTION)->where('module')->eq($module)->andWhere('action')->eq($action)->andWhere('vision')->eq($data->vision)->exec();
$this->dao->insert(TABLE_WORKFLOWACTION)->data($data)->exec();
}
}
/* Insert fields of buildin modules to TABLE_WORKFLOWFIELD. */
$data = new stdclass();
$data->role = 'buildin';
$data->createdBy = $account;
$data->createdDate = $now;
foreach($fields as $module => $moduleFields)
{
if($importModule && strpos(",$importModule,", ",$module,") === false) continue;
$order = 1;
$data->module = $module;
$currentVision = 'rnd';
if(!empty($visions[$module])) $currentVision = $visions[$module];
if($vision != 'all' && $vision != $currentVision) continue;
foreach($moduleFields as $field => $options)
{
$data->field = $field;
$data->name = isset($this->lang->$module->$field) ? $this->lang->$module->$field : $field;
$data->type = zget($options, 'type', 'varchar');
$data->length = zget($options, 'length', '');
$data->control = zget($options, 'control', 'input');
$data->options = zget($options, 'options', '[]');
$data->default = zget($options, 'default', '');
$data->buildin = zget($options, 'buildin', 1);
$data->order = $order;
$data->readonly = ($field == 'subStatus') ? '0' : '1';
$data->rules = zget($options, 'rules', '');
$order++;
if($module == 'execution')
{
$execField = 'exec' . ucfirst($field);
if(isset($this->lang->$module->$execField)) $data->name = $this->lang->$module->$execField;
}
if(is_object($data->options) or is_array($data->options)) $data->options = helper::jsonEncode($data->options);
$this->dao->delete()->from(TABLE_WORKFLOWFIELD)->where('module')->eq($module)->andWhere('field')->eq($field)->exec();
$this->dao->insert(TABLE_WORKFLOWFIELD)->data($data)->exec();
}
}
/* Insert layouts of buildin modules to TABLE_WORKFLOWLAYOUT. */
$data = new stdclass();
foreach($layouts as $module => $moduleLayouts)
{
if($importModule && strpos(",$importModule,", ",$module,") === false) continue;
$data->module = $module;
foreach($moduleLayouts as $action => $layoutFields)
{
$order = 1;
$data->action = $action;
foreach($layoutFields as $field => $options)
{
$data->field = $field;
$data->width = zget($options, 'width', 0);
$data->mobileShow = zget($options, 'mobileShow', 0);
$data->order = $order;
$data->vision = 'rnd';
$order++;
if(!empty($visions[$module])) $data->vision = $visions[$module];
if($data->width == 'auto') $data->width = 0;
if($vision != 'all' && $vision != $data->vision) continue;
$this->dao->delete()->from(TABLE_WORKFLOWLAYOUT)->where('module')->eq($module)->andWhere('action')->eq($action)->andWhere('field')->eq($field)->andWhere('vision')->eq($data->vision)->exec();
$this->dao->insert(TABLE_WORKFLOWLAYOUT)->data($data)->exec();
}
}
}
/* Insert labels of buildin modules to TABLE_WORKFLOWLABEL. */
$data = new stdclass();
$data->buildin = 1;
$data->role = 'buildin';
$data->params = '[]';
$data->createdBy = $account;
$data->createdDate = $now;
foreach($modules as $app => $appModules)
{
foreach($appModules as $module => $options)
{
if($importModule && strpos(",$importModule,", ",$module,") === false) continue;
$labels = array();
if($module == 'product')
{
if(isset($this->lang->product->featureBar['all'])) $labels = $this->lang->product->featureBar['all'];
}
elseif($module == 'story')
{
if(isset($this->lang->product->featureBar['browse'])) $labels = $this->lang->product->featureBar['browse'];
}
elseif($module == 'execution')
{
if(isset($this->lang->execution->featureBar['all'])) $labels = $this->lang->execution->featureBar['all'];
}
elseif($module == 'task')
{
if(isset($this->lang->execution->featureBar['task'])) $labels = $this->lang->execution->featureBar['task'];
}
elseif($module == 'bug')
{
if(isset($this->lang->bug->featureBar['browse'])) $labels = $this->lang->bug->featureBar['browse'];
}
elseif($module == 'feedback')
{
if(isset($this->lang->feedback->menu) && (is_object($this->lang->feedback->menu) || is_array($this->lang->feedback->menu)))
{
foreach($this->lang->feedback->menu as $key => $menuItem)
{
$menus = explode('|', zget($menuItem, 'link', $menuItem));
$labels[$key] = zget($menus, 0, $menuItem);
}
}
}
else
{
if(isset($this->lang->$module->featureBar['browse'])) $labels = $this->lang->$module->featureBar['browse'];
}
$order = 1;
$data->module = $module;
$currentVision = 'rnd';
if(!empty($visions[$module])) $currentVision = $visions[$module];
if($vision != 'all' && $vision != $currentVision) continue;
foreach($labels as $key => $label)
{
$data->code = $key;
$data->label = trim(strip_tags($label));
$data->order = $order;
$order++;
$this->dao->delete()->from(TABLE_WORKFLOWLABEL)->where('module')->eq($module)->andWhere('code')->eq($key)->exec();
$this->dao->insert(TABLE_WORKFLOWLABEL)->data($data)->exec();
}
}
}
}
/**
* 将项目关联的Charter的路标放到projectproduct表中。
* Process project roadmap by charter.
*
* @access public
* @return bool
*/
public function processProjectRoadmapsByCharter()
{
$projectCharters = $this->dao->select('id,charter')->from(TABLE_PROJECT)->where('charter')->ne('0')->fetchPairs('id', 'charter');
if(empty($projectCharters)) return true;
$charterGroups = $this->dao->select('t1.*,t2.id as charterID')->from(TABLE_ROADMAP)->alias('t1')
->leftJoin(TABLE_CHARTER)->alias('t2')->on('FIND_IN_SET(t1.id, t2.roadmap)')
->where('t2.id')->in($projectCharters)
->fetchGroup('charterID');
$productRoadmap = new stdclass();
foreach($projectCharters as $projectID => $charter)
{
$productRoadmap->project = $projectID;
foreach($charterGroups[$charter] as $roadmap)
{
$productRoadmap->product = $roadmap->product;
$productRoadmap->branch = $roadmap->branch;
$productRoadmap->roadmap = ",$roadmap->id,";
$this->dao->delete()->from(TABLE_PROJECTPRODUCT)->where('project')->eq($projectID)->andWhere('product')->eq($roadmap->product)->andWhere('branch')->eq($roadmap->branch)->exec();
$this->dao->insert(TABLE_PROJECTPRODUCT)->data($productRoadmap)->exec();
}
}
return true;
}
/**
* 为文档库的团队空间创建默认空间。
* Create default doclib space for team space.
*
* @access public
* @return void
*/
public function createDefaultDoclibSpace()
{
$this->app->loadLang('doc');
$doclib = new stdclass();
$doclib->type = 'custom';
$doclib->name = $this->lang->doclib->defaultSpace;
$doclib->acl = 'open';
$doclib->addedBy = 'admin';
$doclib->addedDate = helper::now();
foreach(array('or', 'lite', 'rnd') as $vision)
{
$hasDocLib = $this->dao->select('id')->from(TABLE_DOCLIB)->where('vision')->eq($vision)->andWhere('type')->eq('custom')->fetch('id');
if(!$hasDocLib) continue;
$doclib->vision = $vision;
$this->dao->insert(TABLE_DOCLIB)->data($doclib)->exec();
$doclibID = $this->dao->lastInsertID();
$this->dao->update(TABLE_DOCLIB)
->set('parent')->eq($doclibID)
->where('vision')->eq($vision)
->andWhere('type')->eq('custom')
->andWhere('id')->ne($doclibID)
->exec();
}
}
/*
* Update the priv of task relation.
*
* @access public
* @return bool
*/
public function updateTaskRelationPriv()
{
$groupIdList = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('execution')
->andWhere('method')->eq('maintainrelation')
->fetchPairs();
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('`group`')->in($groupIdList)->andWhere('module')->eq('execution')->andWhere('method')->in('editrelation,batcheditrelation')->exec();
foreach($groupIdList as $groupID)
{
$this->dao->update(TABLE_GROUPPRIV)
->set('method')->eq('createrelation')
->where('`group`')->eq($groupID)
->andWhere('module')->eq('execution')
->andWhere('method')->eq('maintainrelation')
->exec();
$this->dao->insert(TABLE_GROUPPRIV)->set('group')->eq($groupID)->set('module')->eq('execution')->set('method')->eq('editrelation')->exec();
$this->dao->insert(TABLE_GROUPPRIV)->set('group')->eq($groupID)->set('module')->eq('execution')->set('method')->eq('batcheditrelation')->exec();
}
return true;
}
/**
* 更新需求的验收时间。
* Update verifiedDate for story.
*
* @access public
* @return void
*/
public function updateStoryVerifiedDate()
{
$verifiedDatePairs = $this->dao->select('t1.objectID, t1.date')->from(TABLE_ACTION)->alias('t1')
->leftJoin(TABLE_HISTORY)->alias('t2')->on('t1.id = t2.action')
->where('t1.objectType')->eq('story')
->andWhere('t1.action')->eq('edited')
->andWhere('t2.field')->eq('stage')
->andWhere('t2.new')->eq('verified')
->orderBy('date_desc')
->fetchPairs();
foreach($verifiedDatePairs as $storyID => $verifiedDate)
{
$this->dao->update(TABLE_STORY)->set('verifiedDate')->eq($verifiedDate)->where('id')->eq($storyID)->andWhere('stage')->eq('verified')->exec();
}
return !dao::isError();
}
/**
* 更新审批流的条件分支数据结构。
* Update the condition branch data structure of the approval flow.
*
* @access public
* @return void
*/
public function updateApprovalCondition()
{
$specs = $this->dao->select('id, nodes')->from(TABLE_APPROVALFLOWSPEC)->fetchPairs();
foreach($specs as $specID => $spec)
{
$nodes = json_decode($spec);
$hasBranch = false;
foreach($nodes as $id => $node)
{
if($node->type != 'branch') continue;
$hasBranch = true;
$nodes[$id] = $this->transferCondition($node);
}
if($hasBranch) $this->dao->update(TABLE_APPROVALFLOWSPEC)->set('nodes')->eq(json_encode($nodes))->where('id')->eq($specID)->exec();
}
}
/**
* 递归转换条件分支的数据结构。
* Transfer branch conditions.
*
* @param object $node
* @access public
* @return object
*/
public function transferCondition($node)
{
foreach($node->branches as $index => $branch)
{
if(isset($branch->conditions))
{
$newConditions = array();
$newCondition = new stdClass();
$newCondition->conditionOperator = 'equal';
$newCondition->conditionLogical = 'or';
foreach($branch->conditions as $condition)
{
if($condition->selectType == 'account')
{
foreach($condition->users as $account)
{
$newCondition->conditionField = 'submitUsers';
$newCondition->conditionValue = $account;
$newConditions[] = clone $newCondition;
}
}
elseif($condition->selectType == 'role')
{
foreach($condition->roles as $role)
{
$newCondition->conditionField = 'submitRoles';
$newCondition->conditionValue = $role;
$newConditions[] = clone $newCondition;
}
}
elseif($condition->selectType == 'position')
{
foreach($condition->positions as $position)
{
$newCondition->conditionField = 'submitPositions';
$newCondition->conditionValue = $position;
$newConditions[] = clone $newCondition;
}
}
elseif($condition->selectType == 'dept')
{
foreach($condition->depts as $dept)
{
$newCondition->conditionField = 'submitDepts';
$newCondition->conditionValue = $dept;
$newConditions[] = clone $newCondition;
}
}
}
$node->branches[$index]->conditions = $newConditions;
}
if(isset($branch->nodes))
{
foreach($branch->nodes as $branchID => $branchNode)
{
if($branchNode->type != 'branch') continue;
$node->branches[$index]->nodes[$branchID] = $this->transferCondition($branchNode);
if(isset($branchNode->default->nodes))
{
foreach($branchNode->default->nodes as $defaultID => $defaultNode)
{
if($defaultNode->type != 'branch') continue;
$node->branches[$index]->nodes[$branchID]->default->nodes[$defaultID] = $this->transferCondition($defaultNode);
}
}
}
}
}
return $node;
}
/**
* 修正工作流字段选项。
* Fix workflowfield options.
*
* @access public
* @return bool
*/
public function fixWorkflowFieldOptions(): bool
{
$datasourceID = $this->dao->select('*')->from(TABLE_WORKFLOWDATASOURCE)->where('code')->eq('requirements')->fetch('id');
if($datasourceID != 5) return true;
$fieldOptions = $this->dao->select('*')->from(TABLE_WORKFLOWFIELD)->where('options')->ne('')->fetchPairs('id', 'options');
foreach($fieldOptions as $id => $options)
{
if(!is_numeric($options)) continue;
if($options < 5) continue;
$this->dao->update(TABLE_WORKFLOWFIELD)->set('options')->eq($options + 2)->where('id')->eq($id)->exec();
}
return true;
}
/**
* Append flow fields for belong.
*
* @access public
* @return void
*/
public function appendFlowFieldsForBelong()
{
$flows = $this->dao->select('*')->from(TABLE_WORKFLOW)->where('buildin')->eq('0')->fetchAll('id', false);
if(empty($flows)) return true;
$tableFields = array();
$tables = $this->dao->showTables();
foreach($tables as $table)
{
$table = reset($table);
if(strpos($table, $this->config->db->prefix . 'flow_') !== 0) continue;
$desc = $this->dao->descTable($table);
$tableFields[$table] = array_column($desc, 'field', 'field');
}
$defaultData = array('type' => 'mediumint', 'length' => '8', 'control' => 'select', 'readonly' => 1, 'buildin' => 1, 'role' => 'default');
$orders = array('program' => '1', 'product' => '2', 'project' => '3', 'execution' => '4');
$fields = array();
$fields['execution'] = "ALTER TABLE %table% ADD `execution` mediumint(8) unsigned NOT NULL DEFAULT 0 AFTER `id`";
$fields['project'] = "ALTER TABLE %table% ADD `project` mediumint(8) unsigned NOT NULL DEFAULT 0 AFTER `id`";
$fields['product'] = "ALTER TABLE %table% ADD `product` mediumint(8) unsigned NOT NULL DEFAULT 0 AFTER `id`";
$fields['program'] = "ALTER TABLE %table% ADD `program` mediumint(8) unsigned NOT NULL DEFAULT 0 AFTER `id`";
$oldFields = $this->dao->select('*')->from(TABLE_WORKFLOWFIELD)->fetchGroup('module', 'field');
foreach($flows as $flow)
{
if(!isset($tableFields[$flow->table])) continue;
if($flow->vision == 'lite') continue;
foreach($fields as $field => $addSQL)
{
if($flow->vision == 'or' && $field != 'product') continue;
$searchedField = '';
foreach($tableFields[$flow->table] as $tableField)
{
if($field == strtolower($tableField))
{
$searchedField = $tableField;
break;
}
}
if($searchedField)
{
$newFieldName = "{$searchedField}_1";
$oldField = isset($oldFields[$flow->module][$searchedField]) ? $oldFields[$flow->module][$searchedField] : null;
if(isset($tableFields[$flow->table][$newFieldName]) || ($oldField && ($oldField->buildin || $oldField->readonly)))
{
$this->dao->exec("ALTER TABLE {$flow->table} DROP `{$searchedField}`");
$this->dao->delete()->from(TABLE_WORKFLOWFIELD)->where('module')->eq($flow->module)->andWhere('field')->eq("{$searchedField}")->exec();
}
elseif($oldField)
{
$this->dao->update(TABLE_WORKFLOWFIELD)->set('field')->eq("{$newFieldName}")->where('module')->eq($flow->module)->andWhere('field')->eq($searchedField)->exec();
$this->dao->update(TABLE_WORKFLOWSQL)->set('field')->eq("{$newFieldName}")->where('module')->eq($flow->module)->andWhere('field')->eq($searchedField)->exec();
$newField = clone $oldField;
$newField->field = $newFieldName;
$this->loadModel('workflowfield');
$this->workflowfield->processTable($flow->table, $oldField, $newField);
$this->workflowfield->updateRelated($flow, $oldField, $newFieldName);
}
}
$this->dao->exec(str_replace('%table%', $flow->table, $addSQL));
$data = $defaultData;
$data['module'] = $flow->module;
$data['field'] = $field;
$data['name'] = $this->lang->upgrade->flowFields[$field];
$data['options'] = $field;
$data['order'] = $orders[$field];
$this->dao->insert(TABLE_WORKFLOWFIELD)->data($data)->exec();
}
}
return true;
}
/**
* Process demand files.
*
* @access public
* @return bool
*/
public function processDemandFiles(): bool
{
$demandIdList = $this->dao->select('objectID')->from(TABLE_FILE)->where('extra')->eq('')->andWhere('objectType')->eq('demand')->fetchPairs('objectID', 'objectID');
$latestVersion = $this->dao->select('id,version')->from(TABLE_DEMAND)->where('id')->in($demandIdList)->fetchPairs('id', 'version');
foreach($demandIdList as $demandID)
{
if(!isset($latestVersion[$demandID])) continue;
$this->dao->update(TABLE_FILE)->set('extra')->eq($latestVersion[$demandID])->where('objectID')->eq($demandID)->andWhere('objectType')->eq('demand')->exec();
}
return true;
}
/**
* Process tables of sql builder.
*
* @access public
* @return void
*/
public function processSqlbuilderTables()
{
$prefix = $this->config->db->prefix;
$sqlbuilderSettings = $this->dao->select('id,setting')->from(TABLE_SQLBUILDER)->fetchPairs();
foreach($sqlbuilderSettings as $id => $setting)
{
$setting = json_decode($setting, true);
if($setting['from']['table'] && strpos($setting['from']['table'], 'zt_') === false) $setting['from']['table'] = $prefix . $setting['from']['table'];
foreach($setting['joins'] as $index => $join)
{
if($setting['joins'][$index]['table'] && strpos($setting['joins'][$index]['table'], 'zt_') === false) $setting['joins'][$index]['table'] = $prefix . $join['table'];
}
$this->dao->update(TABLE_SQLBUILDER)->set('setting')->eq(json_encode($setting))->where('id')->eq($id)->exec();
}
return !dao::isError();
}
/**
* Upgrade my doc space.
*
* @access public
* @return bool
*/
public function upgradeMyDocSpace()
{
$this->loadModel('doc');
$this->app->loadLang('doclib');
$spacesGroup = $this->dao->select('*')->from(TABLE_DOCLIB)
->where('deleted')->eq(0)
->andWhere('type')->eq('mine')
->orderBy('`order` asc, id_asc')
->fetchGroup('addedBy', 'id');
foreach($spacesGroup as $account => $allSpaces)
{
$visionSpaces = array('rnd' => array(), 'lite' => array(), 'or' => array());
foreach($allSpaces as $space) $visionSpaces[$space->vision][$space->id] = $space;
foreach($visionSpaces as $vision => $spaces)
{
if(empty($spaces)) continue;
$space = new stdclass();
$space->type = 'mine';
$space->vision = $vision;
$space->parent = 0;
$space->name = $this->lang->doclib->defaultSpace;
$space->main = '1';
$space->acl = 'private';
$space->addedBy = $account;
$space->addedDate = helper::now();
$spaceID = $this->doc->doInsertLib($space);
$this->dao->update(TABLE_DOCLIB)->set('parent')->eq($spaceID)->where('id')->in(array_keys($spaces))->exec();
$this->dao->update(TABLE_DOCLIB)->set('main')->eq(0)->where('id')->in(array_keys($spaces))->exec();
}
}
}
/**
* 历史产品、项目绑定默认工作流模板。
*
* @access public
* @return void
*/
public function processWorkflowGroups()
{
$workflowGroups = $this->dao->select('code, id')->from(TABLE_WORKFLOWGROUP)->where('main')->eq('1')->fetchPairs();
foreach($workflowGroups as $code => $groupID)
{
if($code == 'productproject')
{
$this->dao->update(TABLE_PRODUCT)->set('workflowGroup')->eq($groupID)->exec();
}
else
{
$this->dao->update(TABLE_PROJECT)
->set('workflowGroup')->eq($groupID)
->where('type')->eq('project')
->beginIF($code == 'scrumproduct')->andWhere('model')->in('scrum,agileplus')->andWhere('hasProduct')->eq('1')->fi()
->beginIF($code == 'scrumproject')->andWhere('model')->in('scrum,agileplus')->andWhere('hasProduct')->eq('0')->fi()
->beginIF($code == 'waterfallproduct')->andWhere('model')->in('waterfall,waterfallplus,ipd')->andWhere('hasProduct')->eq('1')->fi()
->beginIF($code == 'waterfallproject')->andWhere('model')->in('waterfall,waterfallplus,ipd')->andWhere('hasProduct')->eq('0')->fi()
->exec();
}
}
}
/**
* Process object relations.
* 将系统内原有关联关系合并到relation表中:需求关联需求、bug关联bug、用例关联用例、问题关联风险、风险关联问题.
*
* @access public
* @return bool
*/
public function processObjectRelation()
{
if($this->config->edition == 'open') return true;
$relation = new stdClass();
$relation->product = 0;
/* Process story link story. */
$relation->relation = 1;
$linkedtoStories = $this->dao->select('*')->from(TABLE_RELATION)
->where('AType')->in('story,requirement,epic')
->andWhere('relation')->eq('linkedto')
->andWhere('BType')->in('story,requirement,epic')
->fetchAll('id', false);
foreach($linkedtoStories as $story)
{
$relation->AType = $story->AType;
$relation->AID = $story->AID;
$relation->BType = $story->BType;
$relation->BID = $story->BID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
/* Process bug link bug. */
$bugList = $this->dao->select('id,story,relatedBug,task,`case`')->from(TABLE_BUG)
->where('relatedBug')->ne('')
->orWhere('story')->ne(0)
->orWhere('task')->ne(0)
->orWhere('case')->ne(0)
->fetchAll('id');
foreach($bugList as $bugID => $bug)
{
if(!empty($bug->relatedBug))
{
foreach(explode(',', ",{$bug->relatedBug},") as $relatedBugID)
{
if(empty($relatedBugID)) continue;
$relation->AType = 'bug';
$relation->AID = $bugID;
$relation->relation = 1;
$relation->BType = 'bug';
$relation->BID = $relatedBugID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
if(!empty($bug->story))
{
$relation->AType = 'story';
$relation->AID = $bug->story;
$relation->relation = 'generated';
$relation->BType = 'bug';
$relation->BID = $bugID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($bug->task))
{
$relation->AType = 'task';
$relation->AID = $bug->task;
$relation->relation = 'generated';
$relation->BType = 'bug';
$relation->BID = $bugID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($bug->case))
{
$relation->AType = 'testcase';
$relation->AID = $bug->case;
$relation->relation = 'generated';
$relation->BType = 'bug';
$relation->BID = $bugID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process case link case. */
$caseList = $this->dao->select('id,story,linkCase,fromBug')->from(TABLE_CASE)->where('linkCase')->ne('')->orWhere('story')->ne(0)->orWhere('fromBug')->ne(0)->fetchAll('id');
foreach($caseList as $caseID => $case)
{
foreach(explode(',', ",{$case->linkCase},") as $relatedCaseID)
{
if(empty($relatedCaseID)) continue;
$relation->AType = 'testcase';
$relation->AID = $caseID;
$relation->relation = 1;
$relation->BType = 'testcase';
$relation->BID = $relatedCaseID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($case->story))
{
$relation->AType = 'story';
$relation->AID = $case->story;
$relation->relation = 'generated';
$relation->BType = 'testcase';
$relation->BID = $caseID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($case->fromBug))
{
$relation->AType = 'bug';
$relation->AID = $case->fromBug;
$relation->relation = 'generated';
$relation->BType = 'testcase';
$relation->BID = $caseID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process bug transferred to task. */
$relation->BType = 'task';
$taskList = $this->dao->select('id,story,fromBug,design')->from(TABLE_TASK)->where('fromBug')->ne(0)->orWhere('story')->ne(0)->orWhere('design')->ne(0)->fetchAll('id');
foreach($taskList as $taskID => $task)
{
$relation->BID = $taskID;
if(!empty($task->fromBug))
{
$relation->relation = 'transferredto';
$relation->AType = 'bug';
$relation->AID = $task->fromBug;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($task->story))
{
$relation->relation = 'generated';
$relation->AType = 'story';
$relation->AID = $task->story;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($task->design))
{
$relation->relation = 'generated';
$relation->AType = 'design';
$relation->AID = $task->design;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process bug transferred to story. */
$relation->AType = 'bug';
$relation->relation = 'transferredto';
$relation->BType = 'story';
$bugTransferredToStory = $this->dao->select('id,fromBug')->from(TABLE_STORY)->where('fromBug')->ne(0)->fetchPairs();
foreach($bugTransferredToStory as $storyID => $bugID)
{
$relation->AID = $bugID;
$relation->BID = $storyID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
/* Process release. */
$relation->AType = 'story';
$relation->relation = 'interrated';
$relation->BType = 'release';
$releaseLinkedStories = $this->dao->select('id,stories')->from(TABLE_RELEASE)->where('stories')->ne('')->fetchPairs();
foreach($releaseLinkedStories as $releaseID => $storyIdList)
{
$relation->BID = $releaseID;
foreach(explode(',', trim($storyIdList, ',')) as $storyID)
{
if(empty($storyID)) continue;
$relation->AID = $storyID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process build. */
$relation->AType = 'story';
$relation->relation = 'interrated';
$relation->BType = 'build';
$buildLinkedStories = $this->dao->select('id,stories')->from(TABLE_BUILD)->where('stories')->ne('')->fetchPairs();
foreach($buildLinkedStories as $buildID => $storyIdList)
{
$relation->BID = $buildID;
foreach(explode(',', trim($storyIdList, ',')) as $storyID)
{
if(empty($storyID)) continue;
$relation->AID = $storyID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process story generated design. */
$relation->relation = 'generated';
$relation->BType = 'design';
$designStories = $this->dao->select('id,story')->from(TABLE_DESIGN)->where('story')->ne(0)->fetchPairs();
$storyTypeList = $this->dao->select('id,type')->from(TABLE_STORY)->where('id')->in(array_values($designStories))->fetchPairs();
foreach($designStories as $designID => $storyID)
{
$relation->AType = $storyTypeList[$storyID];
$relation->AID = $storyID;
$relation->BID = $designID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
/* Process feedback transferred to story. */
$relation->AType = 'feedback';
$relation->relation = 'transferredto';
$feedbackTransferredToStories = $this->dao->select('id,type,feedback')->from(TABLE_STORY)->where('feedback')->ne(0)->fetchAll();
foreach($feedbackTransferredToStories as $story)
{
$relation->AID = $story->feedback;
$relation->BType = $story->type;
$relation->BID = $story->id;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
/* Process feedback transferredto to bug, task, ticket, demand. */
$relation->AType = 'feedback';
$relation->relation = 'transferredto';
foreach(array('bug', 'task', 'ticket', 'demand') as $feedbackTransferredToType)
{
$relation->BType = $feedbackTransferredToType;
$feedbackTransferredTo = $this->dao->select('id,feedback')->from($this->config->objectTables[$feedbackTransferredToType])->where('feedback')->ne(0)->fetchPairs();
foreach($feedbackTransferredTo as $id => $feedbackID)
{
$relation->AID = $feedbackID;
$relation->BID = $id;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process ticket transferred to story and bug. */
$relation->AType = 'ticket';
$relation->relation = 'transferredto';
$ticketTransferred = $this->dao->select('*')->from(TABLE_TICKETRELATION)->fetchAll('id', false);
foreach($ticketTransferred as $transferredObject)
{
$relation->AID = $transferredObject->ticketId;
$relation->BType = $transferredObject->objectType;
$relation->BID = $transferredObject->objectId;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
/* Process twins. */
$relation->AType = 'story';
$relation->relation = 'twin';
$relation->BType = 'story';
$twins = $this->dao->select('id,twins')->from(TABLE_STORY)->where('twins')->ne('')->fetchPairs();
foreach($twins as $storyID => $twinsID)
{
$relation->AID = $storyID;
foreach(explode(',', trim($twinsID, ',')) as $twinID)
{
if(empty($twinID)) continue;
$relation->BID = $twinID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process child story. */
$relation->relation = 'subdivideinto';
$parentStories = array();
$childStories = $this->dao->select('id,type,parent,demand')->from(TABLE_STORY)->where('parent')->gt(0)->orWhere('demand')->gt(0)->fetchAll();
foreach($childStories as $childStory) $parentStories[$childStory->parent] = $childStory->parent;
$parentStoryType = $this->dao->select('id,type')->from(TABLE_STORY)->where('id')->in($parentStories)->fetchPairs();
foreach($childStories as $childStory)
{
$relation->BType = $childStory->type;
$relation->BID = $childStory->id;
if(!empty($childStory->parent))
{
$relation->AType = $parentStoryType[$childStory->parent];
$relation->AID = $childStory->parent;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!empty($childStory->demand))
{
$relation->AType = 'demand';
$relation->AID = $childStory->demand;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
}
/* Process issue link risk. */
$relation->AType = 'issue';
$relation->relation = 1;
$relation->BType = 'risk';
$riskIssues = $this->dao->select('issue,risk')->from(TABLE_RISKISSUE)->fetchAll();
foreach($riskIssues as $riskIssueList)
{
$relation->AID = $riskIssueList->issue;
$relation->BID = $riskIssueList->risk;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
/* Process child demand. */
$relation->AType = 'demand';
$relation->relation = 'subdivideinto';
$relation->BType = 'demand';
$childDemands = $this->dao->select('id,parent')->from(TABLE_DEMAND)->where('parent')->gt(0)->fetchPairs();
foreach($childDemands as $childDemandID => $parentDemandID)
{
$relation->AID = $parentDemandID;
$relation->BID = $childDemandID;
$this->dao->replace(TABLE_RELATION)->data($relation)->exec();
}
if(!dao::isError())
{
$this->dao->delete()->from(TABLE_CRON)
->where('`command`')->eq('moduleName=upgrade&methodName=ajaxProcessObjectRelation')
->andWhere('type')->eq('zentao')
->andWhere('status')->eq('normal')
->exec();
}
return true;
}
/**
* 处理上线步骤数据。
* Process deploy step.
*
* @access public
* @return void
*/
public function processDeployStep()
{
$this->app->loadLang('deploy');
if(empty($this->lang->deploy->stageList)) return;
$deploySteps = $this->dao->select('*')->from(TABLE_DEPLOYSTEP)->fetchGroup('deploy');
$deployData = array();
foreach($deploySteps as $deployID => $steps)
{
foreach($steps as $step)
{
if($step->parent || $step->stage == '') return;
$deployData[$deployID][$step->stage][] = $step->id;
}
}
$this->loadModel('action');
foreach($deployData as $deployID => $steps)
{
foreach($steps as $stage => $childs)
{
$parent = new stdclass();
$parent->title = zget($this->lang->deploy->stageList, $stage, '');
$parent->status = 'wait';
$parent->deploy = $deployID;
$parent->createdBy = $this->app->user->account;
$parent->createdDate = helper::now();
$this->dao->insert(TABLE_DEPLOYSTEP)->data($parent)->autoCheck()->exec();
$parentID = $this->dao->lastInsertID();
$this->action->create('deploystep', $parentID, 'created');
$this->dao->update(TABLE_DEPLOYSTEP)->set('parent')->eq($parentID)->where('id')->in($childs)->exec();
}
}
}
/**
* 根据旧的缓存配置生成新的缓存配置。
* Generate new cache config.
*
* @access public
* @return void
*/
public function processCacheConfig()
{
$cache = $this->loadModel('setting')->getItem('owner=system&module=common&section=global&key=cache');
if($cache == '{"dao":{"enable":"1"}}') $this->loadModel('install')->enableCache();
$this->setting->deleteItems('owner=system&module=common&section=global&key=cache');
}
/**
* 初始化任务关联。
* Init task relation.
*
* @access public
* @return bool
*/
public function initTaskRelation(): bool
{
$childTasks = $this->dao->select('id,parent')->from(TABLE_TASK)->where('parent')->gt(0)->fetchPairs('id', 'parent');
if(empty($childTasks)) return true;
$this->dao->begin();
$childIdList = array_keys($childTasks);
$this->dao->delete()->from(TABLE_RELATION)->where('BID')->in($childIdList)->andWhere('relation')->eq('subdivideinto')->andWhere('AType')->eq('task')->andWhere('BType')->eq('task')->exec();
$this->dao->delete()->from(TABLE_RELATION)->where('AID')->in($childIdList)->andWhere('relation')->eq('subdividefrom')->andWhere('AType')->eq('task')->andWhere('BType')->eq('task')->exec();
$data = new stdclass();
$data->AType = 'task';
$data->BType = 'task';
foreach($childTasks as $taskID => $parentID)
{
$data->AID = $taskID;
$data->BID = $parentID;
$data->relation = 'subdividefrom';
$this->dao->insert(TABLE_RELATION)->data($data)->exec();
$data->AID = $parentID;
$data->BID = $taskID;
$data->relation = 'subdivideinto';
$this->dao->insert(TABLE_RELATION)->data($data)->exec();
}
$this->dao->delete()->from(TABLE_CRON)
->where('`command`')->eq('moduleName=upgrade&methodName=ajaxInitTaskRelation')
->andWhere('type')->eq('zentao')
->andWhere('status')->eq('normal')
->exec();
if(dao::isError())
{
$this->dao->rollBack();
return false;
}
return $this->dao->commit();
}
/**
* 创建内置立项审批流。
* Add default charter approval flow.
*
* @access public
* @return bool
*/
public function addCharterApprovalFlow()
{
foreach($this->lang->upgrade->defaultCharterApprovalFlow as $approvalType => $approval)
{
$approvalflow = new stdclass();
$approvalflow->name = $approval->title;
$approvalflow->code = $approvalType;
$approvalflow->desc = $approval->desc;
$approvalflow->version = 1;
$approvalflow->createdBy = 'system';
$approvalflow->createdDate = helper::now();
$approvalflow->workflow = 'charter';
$approvalflow->deleted = 0;
$this->dao->insert(TABLE_APPROVALFLOW)->data($approvalflow)->exec();
if(dao::isError()) return false;
$approvalflowID = $this->dao->lastInsertId();
$approvalflowSpec = new stdclass();
$approvalflowSpec->flow = $approvalflowID;
$approvalflowSpec->version = 1;
$approvalflowSpec->nodes = '[{"type":"start","ccs":[]},{"id":"3ewcj92p55e","type":"approval","title":"审批","reviewType":"manual","multiple":"and","percent":"50","commentType":"noRequired","agentType":"pass","selfType":"selfReview","deletedType":"setAdmin","reviewers":[{"type":"select","users":[],"roles":[""],"depts":[""],"positions":[""],"userRange":"all","required":"yes"}],"ccs":[{"type":"select","users":[],"roles":[""],"depts":[""],"positions":[""],"userRange":"all"}]},{"type":"end","ccs":[]}]';
$approvalflowSpec->createdBy = 'system';
$approvalflowSpec->createdDate = helper::now();
$this->dao->insert(TABLE_APPROVALFLOWSPEC)->data($approvalflowSpec)->exec();
if(dao::isError()) return false;
$approvalflowobject = new stdclass();
$approvalflowobject->flow = $approvalflowID;
$approvalflowobject->objectType = 'charter';
$approvalflowobject->extra = $approvalType;
$this->dao->insert(TABLE_APPROVALFLOWOBJECT)->data($approvalflowobject)->exec();
if(dao::isError()) return false;
}
return true;
}
/**
* 对立项附件配置做升级处理。
* Process charter file config.
*
* @access public
* @return bool
*/
public function processCharterFileConfig()
{
$oldCharterFileConfig = $this->loadModel('custom')->getItems('owner=system&module=charter&section=charterList&vision=or');
if(!$oldCharterFileConfig) return true;
$projectApprovalFiles = array();
foreach($oldCharterFileConfig as $oldCharterFile) $projectApprovalFiles[] = array('index' => $oldCharterFile->key, 'name' => $oldCharterFile->value);
$charterFiles = json_decode($this->config->custom->charterFiles, true);
foreach($charterFiles as $index => $charterFile)
{
$charterFiles[$index]['type'] = 'roadmap';
$charterFiles[$index]['projectApproval'] = $projectApprovalFiles;
}
$this->loadModel('setting')->setItem('system.custom.charterFiles', json_encode($charterFiles));
return true;
}
/**
* 处理立项状态及评审状态。
* Process charter status.
*
* @access public
* @return bool
*/
public function processCharterStatus()
{
/* Update roadmap status. */
$doneCharterRoadmaps = $this->dao->select('id,roadmap')->from(TABLE_CHARTER)->where('status')->eq('closed')->andWhere('closedReason')->eq('done')->fetchPairs();
$canceledCharterRoadmaps = $this->dao->select('id,roadmap')->from(TABLE_CHARTER)->where('status')->eq('closed')->andWhere('closedReason')->eq('canceled')->fetchPairs();
$this->dao->update(TABLE_ROADMAP)->set('status')->eq('launched')->where('id')->in(array_filter(explode(',', implode(',', $doneCharterRoadmaps))))->exec();
$this->dao->update(TABLE_ROADMAP)->set('status')->eq('wait')->where('id')->in(array_filter(explode(',', implode(',', $canceledCharterRoadmaps))))->exec();
$this->dao->update(TABLE_CHARTER)
->set('status')->eq('wait')
->set('reviewStatus')->eq('projectReject')
->where('status')->eq('failed')
->andWhere('reviewedResult')->eq('failed')
->exec();
$this->dao->update(TABLE_CHARTER)
->set('reviewStatus')->eq('projectPass')
->where('status')->eq('launched')
->exec();
$this->dao->update(TABLE_CHARTER)
->set('reviewStatus')->eq('completionPass')
->where('status')->eq('closed')
->andWhere('closedReason')->eq('done')
->exec();
$this->dao->update(TABLE_CHARTER)
->set('prevCanceledStatus')->eq('wait')
->set('reviewStatus')->eq('cancelPass')
->where('status')->eq('closed')
->andWhere('closedReason')->eq('canceled')
->exec();
return true;
}
/**
* 处理devops的图表模块数据。
* Process devops chart modules.
*
* @access public
* @return bool
*/
public function createDevOpsChartModule()
{
$chartModules = $this->addDefaultModules4BI('chart');
$this->addSecondModule4BI(1, 'macro', 'chart', $chartModules);
return !dao::isError();
}
/**
* 获取需要升级的文档。
* Get docs need to upgrade.
*
* @access public
* @return array
*/
public function getUpgradeDocs(): array
{
$docs = $this->dao->select('t1.*,t2.title,t2.content,t2.type AS contentType,t2.rawContent,t1.version')->from(TABLE_DOC)->alias('t1')
->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id=t2.doc && t1.version=t2.version')
->where('t2.type')->in(array('doc', 'html'))
->andWhere('t1.status')->ne('draft')
->andWhere('t2.rawContent')->in(null)
->andWhere('t1.templateType')->eq('')
->andWhere('t1.template')->eq('')
->andWhere('t2.fromVersion')->eq(0)
->fetchAll('id', false);
$newDocs = array();
$oldDocs = array();
foreach($docs as $doc)
{
if($doc->contentType == 'doc' && empty($doc->rawContent) && !empty($doc->content)) $newDocs[] = $doc->id;
if(($doc->contentType == 'html') && !empty($doc->content)) $oldDocs[] = $doc->id;
}
$wikis = $this->dao->select('*')->from(TABLE_DOCLIB)
->where('type')->eq('book')
->andWhere('parent')->eq(0)
->fetchAll('id', false);
if(empty($newDocs) && empty($oldDocs) && empty($wikis)) return array();
return array('doc' => $newDocs, 'html' => $oldDocs, 'wiki' => array_keys($wikis));
}
/**
* 升级文档,设置新编辑器文档的 html,以及转换老文档为新文档。
* Upgrade doc, set doc html generated by new editor, and convert old doc to new doc.
*
* @param int $docID
* @param int $version
* @param string $content
* @return bool
*/
public function upgradeDoc(int $docID, int $version, ?string $content)
{
$docContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($docID)->andWhere('version')->eq($version)->fetch();
if(empty($docContent)) return false;
$this->app->loadLang('doc');
if($docContent->type == 'html')
{
$newDocContent = clone $docContent;
$newDocContent->version = $docContent->version + 1;
$newDocContent->type = 'doc';
$newDocContent->rawContent = empty($content) ? json_encode(array('$migrate' => 'html', '$data' => $docContent->content)) : $content;
unset($newDocContent->id);
$newDocContent->addedBy = 'system';
$newDocContent->addedDate = helper::now();
$newDocContent->editedBy = 'system';
$newDocContent->editedDate = helper::now();
$this->dao->insert(TABLE_DOCCONTENT)->data($newDocContent)->exec();
$this->dao->update(TABLE_DOC)->set('version')->eq($newDocContent->version)->where('id')->eq($docID)->exec();
$this->loadModel('action')->create('doc', $docID, 'convertDoc', sprintf($this->lang->doc->docConvertComment, "#$docContent->version", ''), '', 'system');
}
elseif($docContent->type == 'doc')
{
if(empty($content))
{
$link = helper::createLink('doc', 'view', "docID=$docContent->doc");
$content = sprintf($this->lang->doc->previewNotAvailable, "<a href='$link' target='_blank'>#$docContent->doc $docContent->title</a>");
}
$rawContent = empty($docContent->rawContent) ? $docContent->content : $docContent->rawContent;
$this->dao->update(TABLE_DOCCONTENT)
->set('rawContent')->eq($rawContent)
->set('content')->eq($content)
->where('id')->eq($docContent->id)
->exec();
}
if(dao::isError()) return false;
return true;
}
/**
* 升级老版手册
* Migrate old wiki books.
*
* @param array $wikis
* @access public
* @return bool
*/
public function upgradeWikis(array $wikis)
{
if(empty($wikis)) return true;
$this->app->loadLang('doc');
$wikiSpace = $this->dao->select('*')->from(TABLE_DOCLIB)
->where('type')->eq('custom')
->andWhere('name')->eq($this->lang->doclib->migratedWiki)
->andWhere('parent')->eq(0)
->andWhere('deleted')->eq(0)
->fetch();
if(!$wikiSpace)
{
$wikiSpace = new stdClass();
$wikiSpace->name = $this->lang->doclib->migratedWiki;
$wikiSpace->parent = 0;
$wikiSpace->type = 'custom';
$wikiSpace->acl = 'open';
$wikiSpace->deleted = 0;
$this->dao->insert(TABLE_DOCLIB)->data($wikiSpace)->exec();
if(dao::isError()) return false;
$wikiSpace->id = $this->dao->lastInsertID();
}
/* Move wiki libs to the migrated wiki space. */
foreach($wikis as $wiki)
{
$this->dao->update(TABLE_DOCLIB)
->set('parent')->eq($wikiSpace->id)
->set('type')->eq('custom')
->where('id')->eq($wiki)
->exec();
}
if(dao::isError()) return false;
return true;
}
/**
* 初始化发布关联数据。
* Init release related data.
*
* @access public
* @return bool
*/
public function initReleaseRelated(): bool
{
$releaseID = $this->config->lastReleaseID ?? 0;
$releases = $this->dao->select('id, project, build, branch, releases, stories, bugs, leftBugs')->from(TABLE_RELEASE)->where('id')->gt($releaseID)->orderBy('id')->limit(100)->fetchAll('id');
if(!$releases)
{
$this->dao->delete()->from(TABLE_CRON)
->where('`command`')->eq('moduleName=upgrade&methodName=ajaxInitReleaseRelated')
->andWhere('type')->eq('zentao')
->andWhere('status')->eq('normal')
->exec();
$this->loadModel('setting')->deleteItem('owner=system&module=common&key=lastReleaseID');
return !dao::isError();
}
$this->dao->begin();
$this->dao->delete()->from(TABLE_RELEASERELATED)->where('release')->in(array_keys($releases))->exec();
$fieldPairs = ['project' => 'project', 'build' => 'build', 'branch' => 'branch', 'release' => 'releases', 'story' => 'stories', 'bug' => 'bugs', 'leftBug' => 'leftBugs'];
$related = new stdClass();
foreach($releases as $release)
{
$related->release = $release->id;
foreach($fieldPairs as $objectType => $field)
{
$related->objectType = $objectType;
if(!$release->$field) continue;
$objectIdList = array_unique(array_filter(explode(',', $release->$field)));
foreach($objectIdList as $objectID)
{
if(!preg_match('/^\d+$/', $objectID)) continue; // Filter invalid objectID.
$related->objectID = $objectID;
$this->dao->insert(TABLE_RELEASERELATED)->data($related)->exec();
}
}
$releaseID = $release->id;
}
$this->loadModel('setting')->setItem('system.common.lastReleaseID', $releaseID);
if(dao::isError())
{
$this->dao->rollBack();
return false;
}
return $this->dao->commit();
}
/**
* 转换数据库的字符集。
* Convert database charset.
*
* @access public
* @return bool
*/
public function convertCharset(): bool
{
if($this->config->db->driver != 'mysql') return true;
$dbVersion = $this->loadModel('install')->getDatabaseVersion();
if(version_compare($dbVersion, '5.6', '<')) return true;
/* 获取服务器的字符集和排序规则。Get server charset and collation. */
$result = $this->dbh->getServerCharsetAndCollation($this->config->db->name);
$serverCharset = $result['charset'];
$serverCollation = $result['collation'];
/* 如果服务器的字符集不支持配置的字符集,则不转换。If server charset does not support configured charset, do not convert. */
if($this->config->db->encoding != $serverCharset) return true;
/* 获取当前数据库的字符集和排序规则。Get current database charset and collation. */
$result = $this->dbh->getDatabaseCharsetAndCollation($this->config->db->name);
$dbCharset = $result['charset'];
$dbCollation = $result['collation'];
if($dbCharset != $serverCharset || $dbCollation != $serverCollation)
{
/* 转换数据库的字符集和排序规则。Convert database charset and collation. */
$this->dbh->exec("ALTER DATABASE `{$this->config->db->name}` CHARACTER SET={$serverCharset} COLLATE={$serverCollation}");
}
/* 获取当前数据库中所有表的排序规则。Get all tables collation in current database. */
$tableCollations = $this->dao->select('TABLE_NAME AS name, TABLE_COLLATION AS collation')->from('information_schema.TABLES')
->where('TABLE_SCHEMA')->eq($this->config->db->name)
->andWhere('TABLE_TYPE')->eq('BASE TABLE')
->fetchPairs();
foreach($tableCollations as $tableName => $tableCollation)
{
if(strpos($tableName, $this->config->db->prefix) !== 0) continue;
if($tableCollation == $serverCollation) continue;
$tableName = '`' . $tableName . '`';
if($tableName == TABLE_METRICLIB || $tableName == TABLE_ACTION || $tableName == TABLE_HISTORY) continue;
/* 转换表的字符集和排序规则。Convert table charset and collation. */
$this->dbh->exec("ALTER TABLE {$tableName} CONVERT TO CHARACTER SET {$serverCharset} COLLATE {$serverCollation}");
}
$this->loadModel('setting')->setItems('system.common.global', ['dbConvertedTime' => helper::now(), 'dbCharset' => $serverCharset, 'dbCollation' => $serverCollation]);
return true;
}
/**
* 计算立项的分支。
* Process charter branch.
*
* @access public
* @return bool
*/
public function processCharterBranch()
{
/* 根据当前版本获取立项关联的产品ID和对象ID。 */
$objectType = $this->config->edition == 'ipd' ? 'roadmap' : 'plan';
$charterObjects = $this->dao->select("id,product,{$objectType}")->from(TABLE_CHARTER)->fetchAll('id');
/* 获取立项关联的对象ID列表。 */
$objectIdList = array_column($charterObjects, $objectType);
$objectIdList = implode(',', $objectIdList);
$objectIdList = explode(',', $objectIdList);
$objectIdList = array_filter($objectIdList);
$objectIdList = array_unique($objectIdList);
$objectIdList = implode(',', $objectIdList);
/* 获取立项关联对象的分支。 */
$objects = $this->dao->select('id,product,branch')->from($this->config->edition == 'ipd' ? TABLE_ROADMAP : TABLE_PRODUCTPLAN)->where('id')->in($objectIdList)->fetchGroup('product', 'id');
$charterProduct = new stdclass();
foreach($charterObjects as $charterID => $charterObject)
{
$charterProduct->charter = $charterID;
$productIdList = explode(',', $charterObject->product);
foreach($productIdList as $productID)
{
if($productID == '') continue;
$charterProduct->product = $productID;
if(!isset($objects[$productID]))
{
$charterProduct->branch = '0';
$charterProduct->{$objectType} = '';
$this->dao->replace(TABLE_CHARTERPRODUCT)->data($charterProduct)->exec();
}
else
{
foreach($objects[$productID] as $objectID => $object)
{
if(strpos(',' . $charterObject->{$objectType} . ',', ",{$objectID},") === false) continue;
$branches = explode(',', (string)$object->branch);
foreach($branches as $branch)
{
$charterProduct->branch = $branch;
$charterProduct->{$objectType} = $objectID;
$this->dao->replace(TABLE_CHARTERPRODUCT)->data($charterProduct)->exec();
}
}
}
}
}
return true;
}
/**
* 处理action表的product字段,将数据转移到actionproduct表。
* Process product field of action table, move to actionproduct table.
*
* @access public
* @return bool
*/
public function processActionProduct()
{
$this->dao->exec('TRUNCATE TABLE ' . TABLE_ACTIONPRODUCT);
$this->dao->exec('TRUNCATE TABLE ' . TABLE_ACTIONRECENT);
$stmt = $this->dao->select('id,product')->from(TABLE_ACTION)->where('product')->ne('')->andWhere('product')->ne(',0,')->query();
$sql = 'INSERT INTO ' . TABLE_ACTIONPRODUCT . '(`action`, `product`) VALUES';
$values = array();
while($action = $stmt->fetch())
{
$productIdList = array_unique(array_filter(explode(',', $action->product)));
foreach($productIdList as $productID)
{
if(!is_numeric($productID)) continue;
$values[] = "({$action->id}, {$productID})";
}
if(count($values) >= 5000)
{
$this->dao->exec($sql . implode(',', $values));
$values = array();
}
}
if($values) $this->dao->exec($sql . implode(',', $values));
$fields = "`id`,`objectType`,`objectID`,`product`,`project`,`execution`,`actor`,`action`,`date`,`comment`,`files`,`extra`,`read`,`vision`,`efforted`";
$this->dao->exec('INSERT INTO ' . TABLE_ACTIONRECENT . "({$fields}) SELECT {$fields} FROM " . TABLE_ACTION . " WHERE `date` >= '" . date('Y-m-d', strtotime('-1 month')) . "'");
return true;
}
/**
* 修改执行模块工作流动作名称
* Fix workflow action and field name for execution.
*
* @access public
* @return bool
*/
public function fixWorkflowNameForExecution()
{
$this->app->loadLang('execution');
$this->dao->update(TABLE_WORKFLOWACTION)->set('name')->eq($this->lang->execution->allExecutionAB)->where('module')->eq('execution')->andWhere('action')->eq('all')->exec();
$this->dao->update(TABLE_WORKFLOWACTION)->set('name')->eq($this->lang->execution->create)->where('module')->eq('execution')->andWhere('action')->eq('create')->exec();
$this->dao->update(TABLE_WORKFLOWACTION)->set('name')->eq($this->lang->execution->delete)->where('module')->eq('execution')->andWhere('action')->eq('delete')->exec();
$this->dao->update(TABLE_WORKFLOWACTION)->set('name')->eq($this->lang->execution->edit)->where('module')->eq('execution')->andWhere('action')->eq('edit')->exec();
$this->dao->update(TABLE_WORKFLOWACTION)->set('name')->eq($this->lang->execution->view)->where('module')->eq('execution')->andWhere('action')->eq('view')->exec();
$this->dao->update(TABLE_WORKFLOWACTION)->set('name')->eq($this->lang->execution->batchEdit)->where('module')->eq('execution')->andWhere('action')->eq('batchedit')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->id)->where('module')->eq('execution')->andWhere('field')->eq('id')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->type)->where('module')->eq('execution')->andWhere('field')->eq('type')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->name)->where('module')->eq('execution')->andWhere('field')->eq('name')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->code)->where('module')->eq('execution')->andWhere('field')->eq('code')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->status)->where('module')->eq('execution')->andWhere('field')->eq('status')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->desc)->where('module')->eq('execution')->andWhere('field')->eq('desc')->exec();
$this->dao->update(TABLE_WORKFLOWFIELD)->set('name')->eq($this->lang->execution->PM)->where('module')->eq('execution')->andWhere('field')->eq('PM')->exec();
return true;
}
/**
* 获取需要升级的文档模板。
* Get doc templates need to upgrade.
*
* @access public
* @return array
*/
public function getUpgradeDocTemplates(): array
{
$templateList = $this->dao->select('t1.*, t2.title, t2.content, t2.type AS contentType, t1.version')->from(TABLE_DOC)->alias('t1')
->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id = t2.doc && t1.version = t2.version')
->where('t1.deleted')->eq(0)
->andWhere('t1.templateType')->notIn(array('', 'reportTemplate', 'projectReport'))
->andWhere('t1.lib')->eq(0)
->andWhere('t1.module')->eq('')
->fetchAll('id', false);
if(empty($templateList)) return array();
$htmlTemplates = array_filter($templateList, function($template) {return $template->type == 'html' || $template->type == 'markdown';});
$wikiTemplates = array_filter($templateList, function($template) {return $template->type == 'book';});
return array('all' => array_keys($templateList), 'html' => array_keys($htmlTemplates), 'wiki' => array_keys($wikiTemplates));
}
/**
* 升级文档模板,设置新编辑器文档的 html。
* Upgrade doc template, set doc html generated by new editor.
*
* @param int $docID
* @param int $version
* @param string $content
* @return bool
*/
public function upgradeDocTemplate(int $docID, int $version)
{
$docContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($docID)->andWhere('version')->eq($version)->fetch();
if(empty($docContent)) return false;
$this->app->loadLang('doc');
$newDocContent = clone $docContent;
$newDocContent->version = $docContent->version + 1;
$newDocContent->type = 'doc';
$newDocContent->rawContent = json_encode(array('$migrate' => $docContent->type == 'markdown' ? 'markdown' : 'html', '$data' => $docContent->content));
$newDocContent->addedBy = 'system';
$newDocContent->addedDate = helper::now();
$newDocContent->editedBy = 'system';
$newDocContent->editedDate = helper::now();
unset($newDocContent->id);
$this->dao->insert(TABLE_DOCCONTENT)->data($newDocContent)->exec();
$this->dao->update(TABLE_DOC)->set('version')->eq($newDocContent->version)->where('id')->eq($docID)->exec();
$this->loadModel('action')->create('docTemplate', $docID, 'convertDocTemplate', sprintf($this->lang->doc->docTemplateConvertComment, "#$docContent->version"), '', 'system');
if(dao::isError()) return false;
return true;
}
/**
* 升级wiki类型的文档模板。
* Upgrade templates of wiki.
*
* @param array $wikis
* @return bool
*/
public function upgradeWikiTemplates(array $wikis = array())
{
/* 读取实际的requestType后续用来创建链接。*/
/* Read the actual requestType and use it to create a link. */
global $config, $filter;
$myConfig = $this->app->getConfigRoot() . 'my.php';
if(file_exists($myConfig)) include $myConfig;
$this->app->loadLang('doc');
$wikiList = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->in($wikis)->fetchAll('doc');
foreach($wikis as $bookID)
{
/* 历史版本的文档模板更新content内容,文章显示为链接。*/
/* The historical version of the template updates the content, and the article is displayed as a link. */
$oldContent = $wikiList[$bookID];
$oldTemplate = $this->convertWiki2Html((int)$bookID, 0, true);
$this->dao->update(TABLE_DOCCONTENT)
->set('type')->eq('doc')
->set('content')->eq($oldTemplate)
->set('rawContent')->eq(json_encode(array('$migrate' => 'html', '$data' => $oldTemplate)))
->where('doc')->eq($bookID)
->andWhere('version')->eq($oldContent->version)
->exec();
/* 添加新的版本显示转换后的wiki模板。*/
/* Add a new version to display the converted wiki template. */
$templateHtml = $this->convertWiki2Html((int)$bookID);
$newContent = clone $oldContent;
$newContent->version = $oldContent->version + 1;
$newContent->type = 'doc';
$newContent->content = $templateHtml;
$newContent->rawContent = json_encode(array('$migrate' => 'html', '$data' => $templateHtml));
$newContent->addedBy = 'system';
$newContent->addedDate = helper::now();
$newContent->editedBy = 'system';
$newContent->editedDate = helper::now();
unset($newContent->id);
$this->dao->insert(TABLE_DOCCONTENT)->data($newContent)->exec();
$this->dao->update(TABLE_DOC)->set('version')->eq($newContent->version)->where('id')->eq($bookID)->exec();
$this->loadModel('action')->create('docTemplate', (int)$bookID, 'convertDocTemplate', sprintf($this->lang->doc->docTemplateConvertComment, "#$oldContent->version"), '', 'system');
}
/* 还原requestType配置。*/
/* Restore the config of requestType. */
$this->config->set('requestType', 'GET');
if(dao::isError()) return false;
return true;
}
/**
* 将wiki下的章节和文章合并为一个html。
* Merge chapters and articles from the wiki into HTML.
*
* @param int $bookID
* @param int $nodeID
* @param bool $showLink 文章是否显示为链接
* @return string
*/
public function convertWiki2Html(int $bookID = 0, int $nodeID = 0, bool $showLink = false): string
{
$template = $this->dao->select('t1.*, t2.content AS content')->from(TABLE_DOC)->alias('t1')
->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id = t2.doc')
->where('t1.id')->eq($nodeID ? $nodeID : $bookID)
->fetch();
$children = $this->dao->select('*')->from(TABLE_DOC)
->where('template')->eq($bookID)
->andWhere('deleted')->eq(0)
->andWhere('parent')->eq($nodeID ? $nodeID : $bookID)
->orderBy('`order`, id')
->fetchAll('id');
$childrenHtml = '';
if($children)
{
foreach($children as $child) $childrenHtml .= $this->convertWiki2Html($bookID, $child->id, $showLink);
}
$templateHtml = '';
$levelMark = $template->grade > 0 && $template->grade < 7 ? "h{$template->grade}" : 'p';
if($template->type == 'book') $templateHtml .= $childrenHtml;
if(!empty($template->chapterType) && strpos('input,system', $template->chapterType) !== false)
{
$templateHtml .= "<{$levelMark}>" . $template->title . "</{$levelMark}>";
if(!$showLink && $template->chapterType == 'system')
{
/* 当模板章节使用系统数据时,将系统数据章节转换为动态区块。*/
/* When chapters use system data, convert chapters into dynamic blocks. */
$templateBlock = $this->getTemplateBlock((int)$template->template);
$templateHtml .= empty($templateBlock) ? '' : "<div class='tml-zentaolist' data-title='{$templateBlock['blockTitle']}' data-export-url='{$templateBlock['exportUrl']}' data-fetcher='{$templateBlock['fetcherUrl']}'></div>";
}
$templateHtml .= $childrenHtml;
}
if($template->type == 'article')
{
if($showLink)
{
/* 历史版本的文档模板中,文章显示链接,点击链接查看文章的具体内容。*/
/* In the historical version of the template, the article displays a link. Click on the link to view the content of the article. */
$articleLink = helper::createLink('baseline', 'view', "articleID={$template->id}");
$articleLink = $this->config->requestType == 'GET' ? str_replace('upgrade.php?', 'index.php?', $articleLink) : $articleLink;
$templateHtml .= "<a class='iframe' href='{$articleLink}' data-toggle='modal' data-size='lg'>{$template->title}</a><br>";
}
else
{
$templateHtml .= "<{$levelMark}>" . $template->title . "</{$levelMark}>" . $template->content . $childrenHtml;
}
}
return $templateHtml;
}
/**
* 获取文档模板的动态区块。
* Get the block of doc template.
*
* @param int $templateID
* @return array
*/
public function getTemplateBlock(int $templateID): array
{
$templateType = $this->dao->findByID($templateID)->from(TABLE_DOC)->fetch('templateType');
if(strpos(',HLDS,DDS,DBDS,ADS,ITTC,STTC,SRS,PP,', ",{$templateType},") === false) return array();
$blockType = $templateType;
if($templateType == 'SRS') $blockType = 'projectStory';
if($templateType == 'PP') $blockType = 'gantt';
if(strpos(',ITTC,STTC,', $templateType) !== false) $blockType = 'projectCase';
$blockTitle = $exportUrl = $fetcherUrl = '';
$searchTabList = $blockType == 'gantt' ? array('all' => '') : $this->lang->docTemplate->searchTabList[$blockType];
$blockTitle = ($blockType == 'projectStory' ? $searchTabList['allstory'] : $searchTabList['all']) . $this->lang->docTemplate->of . $this->lang->docTemplate->zentaoList[$blockType];
$settingUrl = helper::createLink('doc', 'buildZentaoConfig', "type={$blockType}&oldBlockID={blockID}&isTemplate=1");
$blockContent = array('searchTab' => $blockType == 'projectStory' ? 'allstory' : 'all');
if($templateType == 'ITTC') $blockContent = array('searchTab' => 'all', 'caseStage' => 'intergrate');
if($templateType == 'STTC') $blockContent = array('searchTab' => 'all', 'caseStage' => 'system');
if($templateType == 'PP') $blockContent = array();
$templateBlock = new stdClass();
$templateBlock->doc = $templateID;
$templateBlock->type = $blockType;
$templateBlock->settings = $this->config->requestType == 'GET' ? str_replace(array('upgrade.php?', 'install.php?'), 'index.php?', $settingUrl) : $settingUrl;
$templateBlock->content = json_encode($blockContent);
$this->dao->insert(TABLE_DOCBLOCK)->data($templateBlock)->exec();
$templateBlockID = $this->dao->lastInsertId();
$exportUrl = helper::createLink('doc', 'ajaxExportZentaoList', "blockID=__TML_ZENTAOLIST__{$templateBlockID}");
$fetcherUrl = helper::createLink('doc', 'zentaoList', "type={$blockType}&blockID=__TML_ZENTAOLIST__{$templateBlockID}");
if($this->config->requestType == 'GET') list($exportUrl, $fetcherUrl) = str_replace(array('upgrade.php?', 'install.php?'), 'index.php?', array($exportUrl, $fetcherUrl));
return array('blockTitle' => $blockTitle, 'exportUrl' => $exportUrl, 'fetcherUrl' => $fetcherUrl);
}
/**
*
* 为项目流程内置其他活动。
* Add other activity.
*
* @access public
* @return void
*/
public function addWorkflowGroupOtherActivity()
{
$projectModules = $this->dao->select('root,id')->from(TABLE_MODULE)->where('type')->eq('process')->andWhere('extra')->eq('project')->fetchPairs();
$groupList = $this->dao->select('*')->from(TABLE_WORKFLOWGROUP)->where('type')->eq('project')->andWhere('projectModel')->ne('kanban')->fetchAll();
foreach($groupList as $group)
{
if($this->config->edition != 'open' && $group->projectModel == 'ipd')
{
$this->loadModel('workflowGroup')->addProcessAndActivity($group, 'ipd');
}
elseif(!empty($projectModules[$group->id]))
{
$this->createOtherActivity($projectModules[$group->id], $group->id);
}
}
}
/**
* 为项目流程内置默认分类。
* Add deliverable default modules.
*
* @access public
* @return void
*/
public function addDefaultDeliverableModule()
{
$groupList = $this->dao->select('id')->from(TABLE_WORKFLOWGROUP)->where('type')->eq('project')->andWhere('projectModel')->ne('kanban')->fetchAll();
foreach($groupList as $group)
{
foreach($this->lang->upgrade->deliverableModule as $code => $name) $this->createDeliverableModule($group->id, $name, $code);
}
}
/**
* 升级设计类型为交付物。
* Upgrade design type to deliverable.
*
* @access public
* @return void
*/
public function upgradeDesignToDeliverable()
{
$this->app->loadLang('design');
$clientLang = $this->app->getClientLang();
$typeList = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('design')
->andWhere('section')->eq('typeList')
->andWhere('lang')->in(array($clientLang, 'all'))
->orderBy('id_asc')
->fetchPairs();
$plusTypeList = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('design')
->andWhere('section')->eq('plusTypeList')
->andWhere('lang')->in(array($clientLang, 'all'))
->orderBy('id_asc')
->fetchPairs();
if($typeList) $this->lang->design->typeList = $typeList;
if($plusTypeList) $this->lang->design->plusTypeList = $plusTypeList;
$modelList = array('waterfall', 'waterfallplus', 'ipd');
$moduleList = $this->dao->select('t1.id,t1.name,t2.projectModel,t2.id as workflowGroup')->from(TABLE_MODULE)->alias('t1')
->leftJoin(TABLE_WORKFLOWGROUP)->alias('t2')->on('t1.root=t2.id')
->where('t1.type')->eq('deliverable')
->andWhere('t1.extra')->eq('design')
->andWhere('t2.projectModel')->in($modelList)
->fetchAll();
$activityList = $this->dao->select('t1.*')->from(TABLE_ACTIVITY)->alias('t1')->leftJoin(TABLE_PROCESS)->alias('t2')->on('t1.process=t2.id')
->where('t1.name')->eq($this->lang->other)
->andWhere('t2.name')->eq($this->lang->other)
->fetchGroup('workflowGroup');
$projectWorkflowGroup = $this->dao->select('id,workflowGroup')->from(TABLE_PROJECT)->where('type')->eq('project')->fetchGroup('workflowGroup', 'id');
$deliverable = new stdClass();
$deliverable->status = 'enabled';
$deliverable->createdBy = 'system';
$deliverable->createdDate = helper::now();
$deliverable->systemList = 1;
$deliverable->trimmable = '1';
$deliverable->template = '[]';
$deliverableStage = new stdClass();
$deliverableStage->required = '0';
$deliverableStage->stage = 'project';
foreach($moduleList as $module)
{
if(empty($activityList[$module->workflowGroup])) continue;
$otherActivity = reset($activityList[$module->workflowGroup]);
$otherActivityID = $otherActivity->id;
$nameFilter = array();
$deliverable->workflowGroup = $module->workflowGroup;
$deliverable->module = $module->id;
$deliverable->activity = $otherActivityID;
foreach(array_filter($this->lang->design->typeList) as $key => $value)
{
if(empty($value) || !in_array($module->projectModel, array('waterfall', 'ipd'))) continue;
$deliverable->category = $key; // 将设计的类型转换为交付物的类型。
$deliverableID = $this->addDeliverable((string)$value, $deliverable, $deliverableStage, $nameFilter);
/* 将历史设计的设计类型替换为交付物ID。 */
if(!empty($projectWorkflowGroup[$module->workflowGroup]))
{
$projects = array_keys($projectWorkflowGroup[$module->workflowGroup]);
$this->dao->update(TABLE_DESIGN)->set('type')->eq($deliverableID)->where('type')->eq($key)->andWhere('project')->in($projects)->exec();
}
}
foreach(array_filter($this->lang->design->plusTypeList) as $key => $value)
{
if(empty($value) || $module->projectModel != 'waterfallplus') continue;
$deliverable->category = $key; // 将设计的类型转换为交付物的类型。
$deliverableID = $this->addDeliverable((string)$value, $deliverable, $deliverableStage, $nameFilter);
/* 将历史设计的设计类型替换为交付物ID。 */
if(!empty($projectWorkflowGroup[$module->workflowGroup]))
{
$projects = array_keys($projectWorkflowGroup[$module->workflowGroup]);
$this->dao->update(TABLE_DESIGN)->set('type')->eq($deliverableID)->where('type')->eq($key)->andWhere('project')->in($projects)->exec();
}
}
}
}
/**
* 创建交付物。
* Add deliverable.
*
* @param string $name
* @param object $deliverable
* @param object $deliverableStage
* @param array $nameFilter
* @access public
* @return int
*/
public function addDeliverable(string $name, object $deliverable, object $deliverableStage, array &$nameFilter = array()): int
{
/* 重名的交付物名称后面加数字。 */
if(!empty($nameFilter[$name]))
{
$deliverable->name = $name . $nameFilter[$name];
$nameFilter[$name] ++;
}
else
{
$deliverable->name = $name;
$nameFilter[$name] = 1;
}
$this->dao->insert(TABLE_DELIVERABLE)->data($deliverable)->exec();
$deliverableID = $this->dao->lastInsertID();
$deliverableStage->deliverable = $deliverableID;
$this->dao->insert(TABLE_DELIVERABLESTAGE)->data($deliverableStage)->exec();
return $deliverableID;
}
/**
* 将通用交付物升级为项目流程下的交付物。
* Upgrade deliverable.
*
* @access public
* @return void
*/
public function upgradeDeliverable()
{
$this->app->loadLang('tree');
$deliverable = new stdClass();
$deliverable->status = 'disabled';
$deliverable->trimmable = '1';
$deliverableStage = new stdClass();
$deliverableStage->stage = 'project';
$deliverableStage->required = '0';
$deliverableList = array();
$nameFilter = array(); // 过滤重名交付物。
$workflowGroups = $this->dao->select('id,deliverable,projectModel,projectType')->from(TABLE_WORKFLOWGROUP)->where('type')->eq('project')->andWhere('projectModel')->ne('kanban')->fetchAll();
$deliverables = $this->dao->select('id,name,model,`desc`,createdBy,createdDate')->from(TABLE_DELIVERABLE)->where('deleted')->eq('0')->andWhere('model')->ne('')->fetchAll('id');
$fileList = $this->dao->select('id,title,objectType,objectID')->from(TABLE_FILE)->where('objectType')->eq('deliverable')->fetchAll('objectID');
$otherModules = $this->dao->select('root,id')->from(TABLE_MODULE)->where('type')->eq('deliverable')->andWhere('extra')->eq('other')->fetchPairs();
$activityList = $this->dao->select('t1.*')->from(TABLE_ACTIVITY)->alias('t1')->leftJoin(TABLE_PROCESS)->alias('t2')->on('t1.process=t2.id')
->where('t1.name')->eq($this->lang->other)
->andWhere('t2.name')->eq($this->lang->other)
->fetchGroup('workflowGroup');
/* 将旧的交付物按照适用范围升级到各个项目流程中。 */
foreach($deliverables as $oldDeliverable)
{
$models = explode(',', $oldDeliverable->model);
/* 原瀑布、敏捷追加融合模式。 */
foreach($models as $model)
{
if(strpos($model, 'product_waterfall') !== false) $models[] = str_replace('product_waterfall', 'product_waterfallplus', $model);
if(strpos($model, 'project_waterfall') !== false) $models[] = str_replace('project_waterfall', 'project_waterfallplus', $model);
if(strpos($model, 'product_scrum') !== false) $models[] = str_replace('product_scrum', 'product_agileplus', $model);
if(strpos($model, 'project_scrum') !== false) $models[] = str_replace('project_scrum', 'project_agileplus', $model);
}
foreach($models as $model)
{
foreach($workflowGroups as $workflowGroup)
{
if($workflowGroup->projectModel == 'kanban') $workflowGroup->projectModel = 'scrum_kanban';
if(!empty($deliverableList[$workflowGroup->id][$oldDeliverable->id])) continue;
if(strpos($model, "{$workflowGroup->projectType}_{$workflowGroup->projectModel}") === false) continue;
if(empty($activityList[$workflowGroup->id])) continue;
$otherActivity = reset($activityList[$workflowGroup->id]);
$otherActivityID = $otherActivity->id;
$deliverableFile = $fileList[$oldDeliverable->id]; // 原交付物只会上传一个附件。
$deliverable->workflowGroup = $workflowGroup->id;
$deliverable->desc = $oldDeliverable->desc;
$deliverable->module = isset($otherModules[$workflowGroup->id]) ? $otherModules[$workflowGroup->id] : '0';
$deliverable->activity = $otherActivityID;
$deliverable->template = $deliverableFile ? '{"new_0":{"name":"' . $deliverableFile->title . '","doc":"","fileID":"' . $deliverableFile->id . '"}}' : '[]';
$deliverable->createdBy = $oldDeliverable->createdBy;
$deliverable->createdDate = !empty($oldDeliverable->createdDate) ? $oldDeliverable->createdDate : null;
/* 重名的交付物名称后面加数字。 */
if(!empty($nameFilter[$workflowGroup->id][$oldDeliverable->name]))
{
$deliverable->name = $oldDeliverable->name . $nameFilter[$workflowGroup->id][$oldDeliverable->name];
$nameFilter[$workflowGroup->id][$oldDeliverable->name] ++;
}
else
{
$deliverable->name = $oldDeliverable->name;
$nameFilter[$workflowGroup->id][$oldDeliverable->name] = 1;
}
$this->dao->insert(TABLE_DELIVERABLE)->data($deliverable)->exec();
$deliverableID = $this->dao->lastInsertID();
$deliverableList[$workflowGroup->id][$oldDeliverable->id] = $deliverableID;
$deliverableStage->deliverable = $deliverableID;
$this->dao->insert(TABLE_DELIVERABLESTAGE)->data($deliverableStage)->exec();
}
}
}
$sprintFilter = array(); // 过滤重复交付物检查规则。
/* 按照项目流程下的交付物配置设置各个交付物的检查规则。 */
foreach($workflowGroups as $workflowGroup)
{
/* 解析项目流程的交付物配置。 */
$groupDeliverable = !empty($workflowGroup->deliverable) ? json_decode($workflowGroup->deliverable, true) : array();
foreach($groupDeliverable as $stageCode => $methodDeliverable)
{
foreach($methodDeliverable as $deliverableConfigs)
{
foreach($deliverableConfigs as $config)
{
if(empty($deliverableList[$workflowGroup->id][$config['deliverable']])) continue;
$projectModel = $workflowGroup->projectModel;
if($projectModel == 'waterfallplus') $projectModel = 'waterfall';
if($projectModel == 'agileplus') $projectModel = 'scrum';
$deliverableID = $deliverableList[$workflowGroup->id][$config['deliverable']];
$workflowGroupModel = "{$workflowGroup->projectType}_{$projectModel}";
$deliverableStage->stage = $stageCode == $workflowGroupModel ? 'project' : str_replace("{$workflowGroupModel}_", '', $stageCode);
$deliverableStage->required = !empty($config['required']) ? '1' : '0';
$deliverableStage->deliverable = $deliverableID;
/* 如果这个交付物在项目流程下有交付物配置,则启用交付物并删除默认的交付物检查规则。 */
if(!isset($sprintFilter[$deliverableID]))
{
$this->dao->update(TABLE_DELIVERABLE)->set('status')->eq('enabled')->where('id')->eq($deliverableID)->exec();
$this->dao->delete()->from(TABLE_DELIVERABLESTAGE)->where('deliverable')->eq($deliverableID)->andWhere('stage')->eq('project')->exec();
}
/* 按照交付物配置生成交付物检查规则。 */
if(in_array($deliverableStage->stage, array('short', 'long', 'ops')))
{
/* 将原来不同类型的迭代合并成一个迭代。 */
$deliverableStage->stage = 'sprint';
/* 其中有一个类型是必填的合并后的结果就是必填。 */
if(isset($sprintFilter[$deliverableID]['sprint']) && empty($sprintFilter[$deliverableID]['sprint']) && !empty($deliverableStage->required))
{
$this->dao->update(TABLE_DELIVERABLESTAGE)->set('required')->eq('1')->where('deliverable')->eq($deliverableID)->andWhere('stage')->eq('sprint')->exec();
}
if(!isset($sprintFilter[$deliverableID]['sprint']))
{
$this->dao->insert(TABLE_DELIVERABLESTAGE)->data($deliverableStage)->exec();
}
}
elseif(!isset($sprintFilter[$deliverableID][$deliverableStage->stage]))
{
$this->dao->insert(TABLE_DELIVERABLESTAGE)->data($deliverableStage)->exec();
}
$sprintFilter[$deliverableID][$deliverableStage->stage] = $deliverableStage->required;
}
}
}
$this->createOtherDeliverable($workflowGroup->id); // 创建名为其他的交付物类型。
}
/* 将已升级的历史交付物数据删掉。 */
$this->dao->delete()->from(TABLE_DELIVERABLE)->where('id')->in(array_keys($deliverables))->andWhere('workflowGroup')->eq('0')->exec();
/* 升级完后把没用的字段删掉。 */
$this->dao->exec("ALTER TABLE " . TABLE_DELIVERABLE . " CHANGE `module` `module` mediumint(8) unsigned NOT NULL DEFAULT '0';");
if($deliverableList) $this->upgradeProjectDeliverable($deliverableList);
}
/**
* 内置交付物其他分类。
* Create other module.
*
* @param int $workflowGroupID
* @param string $name
* @param string $extra
* @access public
* @return int
*/
public function createDeliverableModule(int $workflowGroupID, string $name, string $extra = ''): int
{
$module = new stdclass();
$module->root = $workflowGroupID;
$module->branch = '0';
$module->name = $name;
$module->parent = '0';
$module->path = '';
$module->grade = '1';
$module->order = '0';
$module->type = 'deliverable';
$module->from = '0';
$module->owner = '';
$module->collector = null;
$module->short = '';
$module->extra = $extra;
$module->deleted = '0';
$this->dao->insert(TABLE_MODULE)->data($module)->exec();
$moduleID = $this->dao->lastInsertID();
$this->dao->update(TABLE_MODULE)->set('path')->eq(",$moduleID,")->where('id')->eq($moduleID)->exec();
return $moduleID;
}
/**
* 创建名为其他的交付物类型。
* Create other deliverable.
*
* @param int $workflowGroupID
* @param int $moduleID
* @access public
* @return void
*/
public function createOtherDeliverable(int $workflowGroupID)
{
$moduleID = $this->dao->select('id')->from(TABLE_MODULE)->where('type')->eq('deliverable')->andWhere('root')->eq($workflowGroupID)->andWhere('extra')->eq('other')->fetch('id');
$activityID = $this->dao->select('id')->from(TABLE_ACTIVITY)->where('name')->eq($this->lang->other)->andWhere('workflowGroup')->eq($workflowGroupID)->fetch('id');
/* 创建名为其他的交付物类型。 */
$deliverable = new stdclass();
$deliverable->name = $this->lang->other;
$deliverable->category = 'other';
$deliverable->module = $moduleID;
$deliverable->activity = $activityID ? $activityID : 0;
$deliverable->workflowGroup = $workflowGroupID;
$deliverable->template = '[]';
$deliverable->trimmable = '1';
$deliverable->builtin = '1';
$deliverable->status = 'enabled';
$deliverable->createdBy = 'system';
$deliverable->createdDate = helper::now();
$this->dao->insert(TABLE_DELIVERABLE)->data($deliverable)->exec();
$deliverableID = $this->dao->lastInsertID();
$stageList = $this->config->edition != 'biz' ? $this->loadModel('deliverable')->buildStageList($workflowGroupID) : array();
foreach($stageList as $key => $stage)
{
$deliverableStage = new stdclass();
$deliverableStage->deliverable = $deliverableID;
$deliverableStage->stage = $key;
$deliverableStage->required = '0';
$this->dao->insert(TABLE_DELIVERABLESTAGE)->data($deliverableStage)->exec();
}
}
/**
* 内置默认的过程活动。
* Create other process and activity.
*
* @param int $moduleID
* @param int $workflowGroupID
* @access public
* @return int
*/
public function createOtherActivity(int $moduleID, int $workflowGroupID): int
{
$existsProcess = $this->dao->select('id')->from(TABLE_PROCESS)->where('workflowGroup')->eq($workflowGroupID)->andWhere('name')->eq($this->lang->other)->fetch('id');
if($existsProcess) return $existsProcess;
$process = new stdclass();
$process->workflowGroup = $workflowGroupID;
$process->name = $this->lang->other;
$process->module = $moduleID;
$process->createdBy = 'system';
$process->createdDate = '2020-01-09 00:00:00';
$process->editedDate = null;
$process->assignedDate = null;
$process->deleted = '0';
$this->dao->insert(TABLE_PROCESS)->data($process)->exec();
$processID = $this->dao->lastInsertID();
$this->dao->update(TABLE_PROCESS)->set('`order`')->eq($processID * 5)->where('id')->eq($processID)->exec();
$activity = new stdclass();
$activity->workflowGroup = $workflowGroupID;
$activity->process = $processID;
$activity->name = $this->lang->other;
$activity->optional = 'no';
$activity->createdBy = 'system';
$activity->createdDate = '2020-01-09 00:00:00';
$activity->editedDate = null;
$activity->assignedDate = null;
$activity->deleted = '0';
$this->dao->insert(TABLE_ACTIVITY)->data($activity)->exec();
$activityID = $this->dao->lastInsertID();
$this->dao->update(TABLE_ACTIVITY)->set('`order`')->eq($activityID * 5)->where('id')->eq($activityID)->exec();
return $activityID;
}
/**
* 内置测试环节列表交付物。
* Buildin testcase stage deliverable.
*
* @access public
* @return bool
*/
public function buildinTestcaseStageDeliverable(): bool
{
$this->app->loadLang('testcase');
$clientLang = $this->app->getClientLang();
$testcaseStageList = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('testcase')
->andWhere('section')->eq('stageList')
->andWhere('lang')->in(array($clientLang, 'all'))
->orderBy('id_asc')
->fetchPairs();
if(empty($testcaseStageList)) $testcaseStageList = $this->lang->testcase->stageList;
$this->createTestcaseDeliverable($testcaseStageList);
return true;
}
/**
* 创建测试用例阶段类型交付物。
* Create testcase deliverable.
*
* @param array $modules
* @access public
* @return bool
*/
public function createTestcaseDeliverable(array $modules): bool
{
$testModules = $this->dao->select('root,id')->from(TABLE_MODULE)
->where('type')->eq('deliverable')
->andWhere('extra')->eq('test')
->fetchPairs();
$testDeliverable = $this->dao->select('category')->from(TABLE_DELIVERABLE)
->where('builtin')->eq('0')
->andWhere('module')->in($testModules)
->fetchPairs();
/* 已经存在但是被删除的交付物进行还原操作。 */
foreach($modules as $key => $name)
{
if(empty($name)) continue;
if(!empty($testDeliverable[$key]))
{
$this->dao->update(TABLE_DELIVERABLE)->set('deleted')->eq('0')->set('name')->eq($name . $this->lang->upgrade->list)->where('category')->eq($key)->andWhere('builtin')->eq('0')->exec();
unset($modules[$key]);
}
}
$activityList = $this->dao->select('t1.*')->from(TABLE_ACTIVITY)->alias('t1')->leftJoin(TABLE_PROCESS)->alias('t2')->on('t1.process=t2.id')
->where('t1.name')->eq($this->lang->other)
->andWhere('t2.name')->eq($this->lang->other)
->fetchGroup('workflowGroup');
if(empty($modules)) return true;
$deliverable = new stdClass();
$deliverable->status = 'enabled';
$deliverable->createdBy = 'system';
$deliverable->createdDate = helper::now();
$deliverable->template = '[]';
$deliverable->trimmable = '1';
$deliverable->builtin = '0';
$deliverable->systemList = 1;
$deliverableStage = new stdClass();
$deliverableStage->required = '0';
$deliverableStage->stage = 'project';
$workflows = $this->dao->select('id,projectModel')->from(TABLE_WORKFLOWGROUP)->where('type')->eq('project')->andWhere('projectModel')->ne('kanban')->fetchAll('id');
foreach($workflows as $workflow)
{
$groupID = $workflow->id;
if(empty($activityList[$groupID])) continue;
$otherActivity = reset($activityList[$groupID]);
$otherActivityID = $otherActivity->id;
$deliverable->module = zget($testModules, $workflow->id, 0);
$deliverable->workflowGroup = $workflow->id;
$deliverable->activity = $otherActivityID;
foreach($modules as $key => $name)
{
if(empty($name)) continue;
$deliverable->category = $key; // 标记交付物的类型。
$this->addDeliverable($name . $this->lang->upgrade->list, $deliverable, $deliverableStage);
}
}
return true;
}
/**
* 删除测试用例阶段交付物。
* Delete testcase deliverable.
*
* @param array $modules
* @access public
* @return bool
*/
public function deleteTestcaseDeliverable(array $modules): bool
{
foreach($modules as $key => $name)
{
$this->dao->update(TABLE_DELIVERABLE)->set('deleted')->eq('1')->where('category')->eq($key)->andWhere('builtin')->eq('0')->exec();
}
return true;
}
/**
* 变更测试用例阶段交付物名称。
* Change testcase deliverable.
*
* @param array $modules
* @access public
* @return bool
*/
public function changeTestcaseDeliverable(array $modules): bool
{
foreach($modules as $key => $name)
{
$this->dao->update(TABLE_DELIVERABLE)->set('name')->eq($name . $this->lang->upgrade->list)->where('category')->eq($key)->andWhere('builtin')->eq('0')->exec();
}
return true;
}
/**
* 升级项目和迭代的交付物配置。
* Upgrade project deliverable.
*
* @param array $deliverableList
* @access public
* @return void
*/
public function upgradeProjectDeliverable(array $deliverableList)
{
$projectList = $this->dao->select('id,deliverable,workflowGroup,type,project')->from(TABLE_PROJECT)->where('deliverable')->ne('')->fetchAll('id');
$projectMainLibPairs = $this->dao->select('project, id')->from(TABLE_DOCLIB)->where('main')->eq('1')->andWhere('type')->eq('project')->andWhere('project')->in(array_keys($projectList))->fetchPairs();
$executionMainLibPairs = $this->dao->select('execution, id')->from(TABLE_DOCLIB)->where('main')->eq('1')->andWhere('type')->eq('execution')->andWhere('execution')->in(array_keys($projectList))->fetchPairs();
$fileList = $this->dao->select('*')->from(TABLE_FILE)->where('deleted')->eq('0')->andWhere('extra')->like('deliverable%')->fetchAll('id');
$otherDeliverablePairs = $this->dao->select('workflowGroup, id')->from(TABLE_DELIVERABLE)->where('category')->eq('other')->andWhere('builtin')->eq('1')->fetchPairs();
foreach($projectList as $project)
{
$oldProjectDeliverable = !empty($project->deliverable) ? json_decode($project->deliverable, true) : array();
if(empty($oldProjectDeliverable)) continue;
if($project->type != 'project' && isset($projectList[$project->project])) $project->workflowGroup = $projectList[$project->project]->workflowGroup; // 执行用项目的项目流程
/* 解析项目的交付物配置。 */
foreach($oldProjectDeliverable as $methodDeliverable)
{
foreach($methodDeliverable as $deliverableConfigs)
{
foreach($deliverableConfigs as $config)
{
if(empty($config['doc']) && empty($config['file'])) continue;
$oldDeliverableID = $config['deliverable'];
if(strpos($oldDeliverableID, 'new_') !== false)
{
$deliverableID = $otherDeliverablePairs[$project->workflowGroup]; // 其他类型的交付物。
}
else
{
$deliverableID = !empty($deliverableList[$project->workflowGroup][$oldDeliverableID]) ? $deliverableList[$project->workflowGroup][$oldDeliverableID] : $oldDeliverableID;
}
$newProjectDeliverable = new stdclass();
$newProjectDeliverable->deliverable = $deliverableID;
$newProjectDeliverable->project = $project->type == 'project' ? $project->id : $project->project;
$newProjectDeliverable->submitFrom = $project->id;
$newProjectDeliverable->status = 'draft';
/* 将原来的附件类型的交付物变成附件类型的文档并关联到交付物。 */
if(!empty($config['file']) && !empty($fileList[$config['file']]))
{
$file = $fileList[$config['file']];
$newProjectDeliverable->doc = $this->moveFileToDoc($project, $projectMainLibPairs, $executionMainLibPairs, $file);
$newProjectDeliverable->createdBy = $file->addedBy;
$newProjectDeliverable->createdDate = substr($file->addedDate, 0, 10);
}
else
{
$newProjectDeliverable->doc = $config['doc'];
}
$this->dao->insert(TABLE_PROJECTDELIVERABLE)->data($newProjectDeliverable)->exec();
}
}
}
}
/* 之前交付物没存文档名称、版本,升级的时候补上。 */
$emptyNameDocs = $this->dao->select('t1.id,t2.title,t2.version,t2.addedBy,t2.addedDate')->from(TABLE_PROJECTDELIVERABLE)->alias('t1')
->leftJoin(TABLE_DOC)->alias('t2')->on('t1.doc=t2.id')
->where('t1.name')->eq('')
->andWhere('t2.deleted')->eq('0')
->fetchAll('id');
foreach($emptyNameDocs as $deliverableID => $doc)
{
$deliverableCreatedDate = date('Y-m-d', strtotime($doc->addedDate));
$this->dao->update(TABLE_PROJECTDELIVERABLE)
->set('name')->eq($doc->title)
->set('docVersion')->eq($doc->version)
->set('createdBy')->eq($doc->addedBy)
->set('createdDate')->eq($deliverableCreatedDate)
->where('id')->eq($deliverableID)
->exec();
}
$this->dao->query("ALTER TABLE " . TABLE_PROJECT . " DROP COLUMN `deliverable`;");
}
/**
* 将附件类型的交付物变成附件类型的文档并关联到交付物。
* Move file to doc.
*
* @param object $project
* @param array $projectMainLibPairs
* @param array $executionMainLibPairs
* @param array $files
* @access public
* @return int
*/
public function moveFileToDoc(object $project, array $projectMainLibPairs, array $executionMainLibPairs, object $file): int
{
$projectType = $project->type == 'project' ? 'project' : 'execution';
$title = $file->title;
$position = strrpos($title, '.');
if($position > 0) $title = substr($title, 0, $position);
$doc = new stdclass();
$doc->title = $title;
$doc->lib = $projectType == 'project' ? $projectMainLibPairs[$project->id] : $executionMainLibPairs[$project->id];
$doc->project = $projectType == 'project' ? $project->id : 0;
$doc->execution = $projectType == 'execution' ? $project->id : 0;
$doc->addedBy = 'admin';
$doc->addedDate = helper::today();
$doc->acl = 'open';
$doc->vision = 'rnd';
$doc->version = 1;
$doc->type = 'attachment';
$this->dao->insert(TABLE_DOC)->data($doc)->exec();
$docID = $this->dao->lastInsertID();
$this->dao->update(TABLE_FILE)->set('objectID')->eq($docID)->set('objectType')->eq('doc')->where('id')->eq($file->id)->exec(); // 将附件的对象变成文档。
$docContent = new stdclass();
$docContent->doc = $docID;
$docContent->title = $title;
$docContent->content = '';
$docContent->type = 'attachment';
$docContent->digest = '';
$docContent->version = 1;
$docContent->files = ",$file->id";
$this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
return $docID;
}
/**
* 将检查单迁移到内置项目流程中。
* Migrate auditcl to built-in project workflow.
*
* @access public
* @return void
*/
public function upgradeAuditcl()
{
$workflows = $this->dao->select('id,projectModel,projectType,main')->from(TABLE_WORKFLOWGROUP)->where('type')->eq('project')->andWhere('projectModel')->in('scrum,agileplus,waterfall,waterfallplus')->fetchAll('id');
foreach($workflows as $id => $workflow)
{
if($workflow->projectType == 'product' && $workflow->main == '1')
{
$this->dao->update(TABLE_AUDITCL)->set('workflowGroup')->eq($id)->where('model')->eq($workflow->projectModel)->exec();
}
else
{
/* 复制一份检查单放到项目型项目流程中。*/
$idMap = array();
$auditclList = $this->dao->select('*')->from(TABLE_AUDITCL)->where('model')->eq($workflow->projectModel)->fetchAll('id', false);
foreach($auditclList as $auditcl)
{
$oldID = $auditcl->id;
unset($auditcl->id);
$auditcl->workflowGroup = $id;
$auditcl->model = '';
if(helper::isZeroDate($auditcl->editedDate)) $auditcl->editedDate = null;
if(helper::isZeroDate($auditcl->assignedDate)) $auditcl->assignedDate = null;
$this->dao->insert(TABLE_AUDITCL)->data($auditcl)->exec();
$idMap[$oldID] = $this->dao->lastInsertID();
}
$projectIdList = $this->dao->select('id')->from(TABLE_PROJECT)->where('workflowGroup')->eq($id)->fetchPairs();
foreach($idMap as $oldID => $newID)
{
$this->dao->update(TABLE_NC)->set('listID')->eq($newID)->where('listID')->eq($oldID)->andWhere('project')->in($projectIdList)->exec();
}
}
}
/* 将auditplan中的execution迁移到nc中。 */
$ncs = $this->dao->select('t1.id, t2.execution')->from(TABLE_NC)->alias('t1')
->leftJoin(TABLE_AUDITPLAN)->alias('t2')->on('t1.auditplan=t2.id')
->where('t2.execution')->ne('0')
->fetchPairs();
foreach($ncs as $ncID => $executionID)
{
$this->dao->update(TABLE_NC)->set('execution')->eq($executionID)->where('id')->eq($ncID)->exec();
}
$this->dao->exec("ALTER TABLE " . TABLE_AUDITCL . " DROP `model`;");
}
/**
* 之前IPD、融合瀑布使用的是瀑布项目流程,融合敏捷使用的是敏捷项目流程。现在统一使用自己的项目流程。
* Before IPD, fusion waterfall uses the waterfall project workflow, and fusion agile uses the agile project workflow. Now use the own project workflow.
*
* @access public
* @return void
*/
public function modifyProjectWorkflowGroup()
{
if(!in_array($this->config->edition, array('ipd', 'max'))) return; // 开源版、企业版没有项目流程
$projectGroup = $this->dao->select('id,workflowGroup,model,category,hasProduct')->from(TABLE_PROJECT)->where('model')->in('ipd,waterfallplus,agileplus')->andWhere('deleted')->eq('0')->fetchGroup('workflowGroup', 'id');
$workflows = $this->dao->select('*')->from(TABLE_WORKFLOWGROUP)->where('id')->in(array_keys($projectGroup))->fetchAll('id', false);
$tableList = array(
TABLE_WORKFLOW,
TABLE_WORKFLOWFIELD,
TABLE_WORKFLOWACTION,
TABLE_WORKFLOWLABEL,
TABLE_WORKFLOWLAYOUT,
TABLE_WORKFLOWUI
);
$this->loadModel('project');
$codeWorkflowPairs = array();
$oldNewFlowPairs = array();
foreach($workflows as $workflow)
{
$projects = $projectGroup[$workflow->id];
$index = 1;
$name = $workflow->name;
foreach($projects as $project)
{
$oldWorkflowGroupID = $project->workflowGroup;
$hasProduct = $project->hasProduct ? 'product' : 'project';
$code = $project->model == 'ipd' ? strtolower($project->category) . $hasProduct : $project->model . $hasProduct;
$projectType = $project->model == 'ipd' ? strtolower($project->category) : $hasProduct;
if($projectType == 'cpd') $projectType = 'cpd' . $hasProduct;
unset($workflow->id);
if(isset($codeWorkflowPairs[$code][$oldWorkflowGroupID]))
{
$this->dao->update(TABLE_PROJECT)->set('workflowGroup')->eq($codeWorkflowPairs[$code][$oldWorkflowGroupID])->where('id')->eq($project->id)->exec();
continue;
}
$workflow->name = $name . $index;
$workflow->createdDate = helper::now();
$workflow->projectModel = $project->model;
$workflow->projectType = $projectType;
$workflow->code = $code;
$workflow->editedDate = null;
$workflow->main = '0';
$this->dao->insert(TABLE_WORKFLOWGROUP)->data($workflow)->exec();
$newWorkflowGroupID = $this->dao->lastInsertId();
$this->dao->update(TABLE_PROJECT)->set('workflowGroup')->eq($newWorkflowGroupID)->where('id')->eq($project->id)->exec();
foreach($tableList as $table)
{
$this->project->copyWorkflow($project->workflowGroup, $newWorkflowGroupID, $table);
}
if(!isset($oldNewFlowPairs[$oldWorkflowGroupID])) $oldNewFlowPairs[$oldWorkflowGroupID] = array();
$oldNewFlowPairs[$oldWorkflowGroupID][] = $newWorkflowGroupID;
$codeWorkflowPairs[$code][$oldWorkflowGroupID] = $newWorkflowGroupID;
$index++;
}
}
/* 处理报告模板的适用流程。*/
$reportTemplateList = $this->dao->select('id,objects')->from(TABLE_DOC)->where('templateType')->eq('reportTemplate')->fetchPairs('id');
foreach($reportTemplateList as $templateID => $objects)
{
$newObjects = array();
$objectIdList = array_filter(explode(',', $objects));
foreach($objectIdList as $objectID)
{
$newObjects[] = $objectID;
if(!isset($oldNewFlowPairs[$objectID])) continue;
$newObjects = array_merge($newObjects, $oldNewFlowPairs[$objectID]);
}
if(!empty($newObjects))
{
$newObjectList = implode(',', array_filter(array_unique($newObjects)));
$this->dao->update(TABLE_DOC)->set('objects')->eq($newObjectList)->where('id')->eq($templateID)->exec();
}
}
}
/**
* 升级流程和活动, 将流程和活动中的数据迁移到流程中。
* Upgrade process and activity.
*
* @access public
* @return void
*/
public function upgradeProcessAndActivity()
{
$this->loadModel('workflowgroup');
$this->app->loadLang('process');
$groupList = $this->upgradeTao->getWorkflowGroupForProcess();
$classifyModule = array();
foreach($groupList as $groupID => $group)
{
$projectModel = $group->projectModel;
if($projectModel == 'agileplus') $projectModel = 'scrum';
if($projectModel == 'waterfallplus') $projectModel = 'waterfall';
if(in_array($group->projectModel, array('scrum', 'agileplus', 'waterfall', 'waterfallplus')))
{
$projectActivity = $this->dao->select('t1.id')->from(TABLE_PROGRAMACTIVITY)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->leftJoin(TABLE_WORKFLOWGROUP)->alias('t3')->on('t2.workflowGroup=t3.id')
->where('t3.projectModel')->eq($group->projectModel)
->limit(1)
->fetch('id');
if($projectActivity)
{
/* 更新groupID到过程表,获取旧分类和新模块的对应关系。 */
if($group->projectType == 'product' && $group->main == '1')
{
$classifyModule = $this->upgradeTao->handleBuildinWorkflowGroup($group, $groupID, $classifyModule);
}
else
{
$classifyModule = $this->upgradeTao->handleNeedCopyWorkflowGroup($group, $groupID, $classifyModule);
$this->upgradeTao->migrateOutputToDeliverable($group);
}
}
else
{
$this->workflowgroup->addProcessAndActivity($group, $projectModel);
$this->dao->delete()->from(TABLE_AUDITCL)->where('workflowGroup')->eq($groupID)->exec(); // 新增活动删除旧的检查单
}
}
else
{
$this->workflowgroup->addProcessAndActivity($group, $projectModel);
$this->dao->delete()->from(TABLE_AUDITCL)->where('workflowGroup')->eq($groupID)->exec(); // 新增活动删除旧的检查单
}
}
/* 迁移过程分类到模块表。 */
$this->upgradeTao->updateProcessModules($classifyModule);
/* 删除旧分类。 */
$this->dao->delete()->from(TABLE_PROCESS)->where('workflowGroup')->eq(0)->exec();
$this->dao->delete()->from(TABLE_ACTIVITY)->where('workflowGroup')->eq(0)->exec();
$this->dao->delete()->from(TABLE_LANG)->where('module')->eq('process')->exec();
}
/**
* 获取需要更新的项目报告数据。
* Get project reports that need to be updated.
*
* @access public
* @return array
*/
public function getUpgradeProjectReports(): array
{
$reports = $this->dao->select('t1.id,t1.project,t1.weekStart,t2.model AS projectModel,t2.status AS projectStatus,t2.realBegan,t2.realEnd,t2.suspendedDate')->from(TABLE_WEEKLYREPORT)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
->where('t2.status')->ne('wait')
->orderBy('t1.project asc, t1.weekStart asc')
->fetchAll();
$projectReports = $this->dao->select('id')->from(TABLE_PROJECT)->where('type')->eq('project')->andWhere('model')->in('waterfall,waterfallplus,ipd')->fetchAll();
if(empty($reports) && empty($projectReports)) return array();
$thisSunday = date('Y-m-d', strtotime('this Sunday'));
foreach($reports as $report)
{
if(in_array($report->projectModel, array('scrum', 'agileplus', 'kanban'))) continue;
if($report->projectStatus == 'doing') $report->realBegan = !helper::isZeroDate($report->realBegan) ? $report->realBegan : $report->weekStart;
if(helper::isZeroDate($report->realBegan)) continue;
$beginTimestame = strtotime($report->realBegan);
$day = date('w', $beginTimestame);
if($day == 0) $day = 7;
$report->projectBegin = date('Y-m-d', $beginTimestame - (($day - 1) * 24 * 3600));
/* Filter date < project begin date report. */
if($report->weekStart < $report->projectBegin) continue;
if($report->projectStatus == 'doing') $report->projectEnd = $thisSunday;
if($report->projectStatus == 'suspended') $report->projectEnd = $report->suspendedDate;
if($report->projectStatus == 'closed') $report->projectEnd = $report->realEnd;
/* Filter date > project end date report. */
$report->projectEnd = helper::isZeroDate($report->projectEnd) ? '' : date('Y-m-d', strtotime($report->projectEnd));
if(!$report->projectEnd || $report->weekStart > $report->projectEnd) continue;
$projectReports[] = $report;
}
return $projectReports;
}
/**
* 升级项目报告数据。
* Upgrade project report data.
*
* @param array $data
* @access public
* @return bool
*/
public function upgradeProjectReport(array $data): bool
{
$report = new stdclass();
if(!empty($data['weekStart']))
{
$weekNumber = ceil(helper::diffDate($data['weekStart'], $data['projectBegin']) / 7) + 1;
$weekEnd = date('Y-m-d', strtotime('+6 day', strtotime($data['weekStart'])));
$report->project = $data['project'];
$report->title = sprintf($this->lang->upgrade->weeklyReportTitle, $weekNumber, $data['weekStart'], $weekEnd);
$report->module = 'week';
$report->addedDate = $data['weekStart'] . ' 00:00:00';
$report->weeklyDate = str_replace('-', '', $data['weekStart']);
}
else
{
$report->title = $this->lang->upgrade->milestoneTitle;
$report->module = 'milestone';
$report->project = $data['id'];
$report->addedDate = helper::now();
}
$report->templateType = 'projectReport';
$report->addedBy = 'system';
$this->dao->insert(TABLE_DOC)->data($report)->exec();
return !dao::isError();
}
/**
* 内置基线评审到项目流程中。
* Buildin baseline review flow.
*
* @access public
* @return bool
*/
public function buildinBaselineReview(): bool
{
$reviewFlow = new stdclass();
$reviewFlow->flow = '1';
$reviewFlow->objectID = '0';
$reviewFlow->relatedBy = 'system';
$reviewFlow->relatedDate = helper::now();
$reviewFlow->extra = 'baseline';
$workflows = $this->dao->select('id')->from(TABLE_WORKFLOWGROUP)->where('projectModel')->notin('scrum,projectModel')->fetchAll('id');
foreach($workflows as $workflow)
{
foreach($this->lang->upgrade->baselineReview as $key => $name)
{
$reviewFlow->root = $workflow->id;
$reviewFlow->objectType = $key;
$this->dao->insert(TABLE_APPROVALFLOWOBJECT)->data($reviewFlow)->exec();
}
}
return true;
}
/**
* 升级检查单分类到项目流程下。
* Upgrade reviewcl category.
*
* @access public
* @return bool
*/
public function upgradeReviewclCategory(): bool
{
$this->loadModel('custom');
$clientLang = $this->app->getClientLang();
$categoryList = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('reviewcl')
->andWhere('section')->eq('waterfallCategoryList')
->andWhere('lang')->in(array($clientLang, 'all'))
->orderBy('id_asc')
->fetchPairs();
$categoryPlusList = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('reviewcl')
->andWhere('section')->eq('waterfallplusCategoryList')
->andWhere('lang')->in(array($clientLang, 'all'))
->orderBy('id_asc')
->fetchPairs();
if(empty($categoryList) && empty($categoryPlusList)) return true;
$workflows = $this->dao->select('id,projectModel')->from(TABLE_WORKFLOWGROUP)->where('projectModel')->in('waterfall,waterfallplus')->fetchAll('id');
foreach($workflows as $workflow)
{
if($workflow->projectModel == 'waterfall' && !empty($categoryList))
{
foreach($categoryList as $key => $value) $this->custom->setItem("{$clientLang}.reviewcl.{$workflow->id}CategoryList.{$key}.1", $value);
}
if($workflow->projectModel == 'waterfallplus' && !empty($categoryPlusList))
{
foreach($categoryPlusList as $key => $value) $this->custom->setItem("{$clientLang}.reviewcl.{$workflow->id}CategoryList.{$key}.1", $value);
}
}
$this->dao->delete()->from(TABLE_LANG)->where('section')->in('waterfallCategoryList,waterfallplusCategoryList')->exec();
return true;
}
/**
* 升级项目评审对象到项目流程中。
* Upgrade baseline objects.
*
* @access public
* @return void
*/
public function upgradeBaselineObjects()
{
$this->app->loadLang('baseline');
$clientLang = $this->app->getClientLang();
$objectList = $this->dao->select('`key`, value')->from(TABLE_LANG)
->where('module')->eq('baseline')
->andWhere('section')->eq('objectList')
->andWhere('lang')->in(array($clientLang, 'all'))
->orderBy('id_asc')
->fetchPairs();
if(empty($objectList) && !empty($this->lang->baseline->objectList)) $objectList = $this->lang->baseline->objectList;
if(empty($objectList)) $objectList = $this->lang->upgrade->reviewObjectList;
$workflowGroups = $this->dao->select('id, projectModel, projectType')->from(TABLE_WORKFLOWGROUP)->where('type')->eq('project')->fetchAll('id');
$moduleGroup = $this->dao->select('id, root, extra')->from(TABLE_MODULE)
->where('type')->eq('deliverable')
->andWhere('root')->in(array_keys($workflowGroups))
->fetchGroup('root', 'extra');
$reviewclList = $this->dao->select('*')->from(TABLE_REVIEWCL)->where('type')->ne('')->fetchGroup('type');
$activityList = $this->dao->select('t1.*')->from(TABLE_ACTIVITY)->alias('t1')->leftJoin(TABLE_PROCESS)->alias('t2')->on('t1.process=t2.id')
->where('t1.name')->eq($this->lang->other)
->andWhere('t2.name')->eq($this->lang->other)
->fetchGroup('workflowGroup');
$actionIdList = array();
$reviewclActions = $this->dao->select('*')->from(TABLE_ACTION)->where('objectType')->eq('reviewcl')->fetchGroup('objectID', 'id');
foreach($reviewclActions as $actions) $actionIdList = arrayUnion($actionIdList, $actions);
$reviewclHistory = $this->dao->select('*')->from(TABLE_HISTORY)->where('action')->in(array_keys($actionIdList))->fetchGroup('action', 'id');
$deliverable = new stdClass();
$deliverable->status = 'enabled';
$deliverable->createdBy = 'system';
$deliverable->createdDate = helper::now();
$deliverable->trimmable = '1';
$deliverable->template = '[]';
$deliverableStage = new stdClass();
$deliverableStage->stage = 'project';
$deliverableStage->required = '0';
$reviewFlow = new stdclass();
$reviewFlow->flow = 1;
$reviewFlow->objectType = 'deliverable';
$reviewFlow->relatedBy = 'system';
$reviewFlow->relatedDate = helper::now();
$reviewFlow->extra = 'review';
$upgradeReviewcls = array();
$categoryModuleMap = array('PP' => 'plan', 'SRS' => 'story');
foreach($workflowGroups as $groupID => $workflowGroup)
{
$projectModel = $workflowGroup->projectModel;
if(empty($activityList[$groupID])) continue;
$otherActivity = reset($activityList[$groupID]);
$otherActivityID = $otherActivity->id;
$nameFilter = array();
$deliverable->workflowGroup = $groupID;
$deliverable->activity = $otherActivityID;
foreach(array_filter($objectList) as $key => $value)
{
if(empty($value)) continue;
if(in_array($key, array('HLDS', 'DDS', 'ADS', 'DBDS', 'ITTC', 'STTC', 'intergrate', 'system'))) continue; // 设计类型和测试类型的上面处理过了,跳过。
if($key == 'PP' && in_array($projectModel, array('scrum', 'agileplus'))) continue; // 敏捷、融合敏捷没有项目计划
if($key == 'SRS') $value = $this->lang->upgrade->reviewObjectList['SRS']; // 将软件需求规格说明书改成项目需求规格说明书
$deliverable->category = $key; // 标记交付物的类型。
$deliverable->systemList = (int)in_array($key, ['PP', 'SRS']);
/* 将原来的评审对象放到新交付物的模块下:计划类、需求类、设计类、测试类。其他类放到其他模块下。 */
$moduleKey = isset($categoryModuleMap[$key]) ? $categoryModuleMap[$key] : 'other';
$deliverable->module = isset($moduleGroup[$groupID][$moduleKey]) ? $moduleGroup[$groupID][$moduleKey]->id : 0;
$deliverableID = $this->addDeliverable((string)$value, $deliverable, $deliverableStage, $nameFilter);
$reviewFlow->root = $groupID;
$reviewFlow->objectID = $deliverableID;
$this->dao->insert(TABLE_APPROVALFLOWOBJECT)->data($reviewFlow)->exec();
$flowID = $this->dao->lastInsertID();
/* 迁移评审对象对应的检查清单。 */
if(empty($reviewclList[$projectModel])) continue;
foreach($reviewclList[$projectModel] as $reviewcl)
{
if($key != $reviewcl->object) continue;
$upgradeReviewcls[$reviewcl->id] = $reviewcl->id;
$reviewclData = clone $reviewcl;
unset($reviewclData->id);
unset($reviewclData->editedDate);
unset($reviewclData->assignedDate);
$reviewclData->workflowGroup = $groupID;
$reviewclData->object = $flowID;
$this->dao->insert(TABLE_REVIEWCL)->data($reviewclData)->exec();
$reviewclID = $this->dao->lastInsertID();
$this->dao->update(TABLE_REVIEWISSUE)->set('listID')->eq($reviewclID)->where('listID')->eq($reviewcl->id)->exec();
/* 迁移检查清单历史记录。 */
if(empty($reviewclActions[$reviewcl->id])) continue;
foreach($reviewclActions[$reviewcl->id] as $action)
{
$actionData = clone $action;
unset($actionData->id);
$actionData->objectID = $reviewclID;
$this->dao->insert(TABLE_ACTION)->data($actionData)->exec();
if(empty($reviewclHistory[$action->id])) continue;
$actionID = $this->dao->lastInsertID();
foreach($reviewclHistory[$action->id] as $history)
{
unset($history->id);
$history->action = $actionID;
$this->dao->insert(TABLE_HISTORY)->data($history)->exec();
}
}
}
}
}
/* 将已升级的历史检查清单数据删掉。 */
$this->dao->delete()->from(TABLE_REVIEWCL)->where('id')->in($upgradeReviewcls)->exec();
$this->dao->exec("ALTER TABLE " . TABLE_REVIEWCL . " CHANGE `object` `object` mediumint(8) unsigned NOT NULL DEFAULT '0';");
$this->dao->exec("ALTER TABLE " . TABLE_REVIEWCL . " DROP `type`;");
$this->dao->exec('ALTER TABLE ' . TABLE_PROCESS . ' DROP `model`');
$this->dao->exec('ALTER TABLE ' . TABLE_PROCESS . ' DROP `type`');
}
/**
* 升级项目评审对象到项目交付物。
* Upgrade review to deliverable.
*
* @access public
* @return void
*/
public function upgradeReviewToDeliverable()
{
if(!in_array($this->config->edition, array('ipd', 'max'))) return; // 开源版、企业版没有评审功能。
$this->loadModel('review');
$this->loadModel('baseline');
$reviewPointList = $this->config->edition == 'ipd' ? $this->lang->review->reviewPoint->titleList : array();
$reviews = $this->dao->select('t1.*, t2.category, t2.version')->from(TABLE_REVIEW)->alias('t1')
->leftJoin(TABLE_OBJECT)->alias('t2')->on('t1.object=t2.id')
->where('t1.deleted')->eq('0')
->beginIF($reviewPointList)->andWhere('t2.category')->notin(array_keys($reviewPointList))->fi() // IPD 模式下,只升级非评审点的评审。
->orderBy('t1.id_asc')
->fetchAll('id');
$projectDeliverables = $this->dao->select('t1.id, t2.id as deliverable, t2.category')->from(TABLE_PROJECT)->alias('t1')
->leftJoin(TABLE_DELIVERABLE)->alias('t2')->on('t1.workflowGroup=t2.workflowGroup')
->where('t2.category')->ne('')
->fetchGroup('id', 'category');
$docIdList = array_column($reviews, 'doc');
$docIdList = array_filter($docIdList);
$docList = $this->dao->select('id, title')->from(TABLE_DOC)->where('id')->in($docIdList)->fetchPairs();
$projectMainLibPairs = $this->dao->select('project, id')->from(TABLE_DOCLIB)->where('main')->eq('1')->andWhere('type')->eq('project')->fetchPairs();
$fileGroup = $this->dao->select('id, objectID')->from(TABLE_FILE)->where('deleted')->eq('0')->andWhere('objectType')->eq('review')->fetchGroup('objectID', 'id');
$doc = new stdclass();
$doc->version = 1;
$doc->type = 'text';
$doc->acl = 'open';
$doc->addedBy = 'system';
$doc->addedDate = helper::now();
foreach($reviews as $review)
{
if(!isset($projectDeliverables[$review->project][$review->category])) continue;
$files = '';
if(!empty($fileGroup[$review->id]))
{
$fileIdList = array_keys($fileGroup[$review->id]);
$files = implode(',', $fileIdList);
}
/* 不是系统模板生成、也没选文档的,自动生成文档。 */
if(!$review->template && !$review->doc)
{
$doc->title = $review->title;
$doc->lib = zget($projectMainLibPairs, $review->project, 0);
$doc->project = $review->project;
$doc->addedBy = $review->createdBy;
$doc->addedDate = $review->createdDate;
$doc->version = 1;
$doc->type = $files ? 'attachment' : 'text';
$this->dao->insert(TABLE_DOC)->data($doc)->exec();
$review->doc = $this->dao->lastInsertID();
$review->docVersion = 1;
$docContent = new stdclass();
$docContent->type = 'doc';
$docContent->doc = $review->doc;
$docContent->title = $review->title;
$docContent->files = $files;
$docContent->version = 1;
$this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
$this->dao->update(TABLE_FILE)->set('objectType')->eq('doc')->set('objectID')->eq($review->doc)->where('id')->in($files)->exec();
}
elseif($review->doc && $files)
{
/* 原来有文档也有附件,将附件关联到文档。 */
$maxVersion = $this->dao->select('max(version) as version')->from(TABLE_DOCCONTENT)->where('doc')->eq($review->doc)->fetch('version');
$docContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($review->doc)->andWhere('version')->eq($review->docVersion)->fetch();
unset($docContent->id);
$docContent->version = $maxVersion + 1;
$docContent->files = $files;
$review->docVersion = $docContent->version;
$this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
$this->dao->update(TABLE_FILE)->set('objectType')->eq('doc')->set('objectID')->eq($review->doc)->where('id')->in($files)->exec();
$this->dao->update(TABLE_DOC)->set('version')->eq($review->docVersion)->where('id')->eq($review->doc)->exec();
$this->dao->update(TABLE_REVIEW)->set('docVersion')->eq($review->docVersion)->where('id')->eq($review->id)->exec();
}
$deliverable = new stdclass();
$deliverable->name = (!empty($docList[$review->doc]) && $docList[$review->doc] != $review->title) ? $docList[$review->doc] : $review->title; // 优先使用文档的标题
$deliverable->project = $review->project;
$deliverable->submitFrom = $review->project;
$deliverable->review = $review->id;
$deliverable->deliverable = $projectDeliverables[$review->project][$review->category]->deliverable;
$deliverable->doc = $review->doc;
$deliverable->docVersion = $review->docVersion;
$deliverable->createdBy = $review->createdBy;
$deliverable->createdDate = $review->createdDate;
$deliverable->status = $review->status;
$deliverable->version = $review->version;
$deliverableID = $this->dao->select('id')->from(TABLE_PROJECTDELIVERABLE)
->where('project')->eq($deliverable->project)
->andWhere('deliverable')->eq($deliverable->deliverable)
->andWhere('doc')->eq($deliverable->doc)
->fetch('id');
/* 保证交付物在项目下的唯一性,如果不存在,则插入。 */
if(!$deliverableID)
{
$this->dao->insert(TABLE_PROJECTDELIVERABLE)->data($deliverable)->exec();
$deliverableID = $this->dao->lastInsertID();
}
else
{
$this->dao->update(TABLE_PROJECTDELIVERABLE)->set('docVersion')->eq($deliverable->docVersion)->set('review')->eq($deliverable->review)->where('id')->eq($deliverableID)->exec();
}
$this->dao->update(TABLE_REVIEW)->set('deliverable')->eq($deliverableID)->set('title')->eq($deliverable->name)->where('id')->eq($review->id)->exec();
$this->dao->update(TABLE_OBJECT)->set('category')->eq($deliverable->deliverable)->where('id')->eq($review->object)->exec(); // 将评审对象改成交付物ID
/* 如果是系统模板类型、但没有选系统模板生成,则复制一份交付物,让用户升级上来后可以继续使用系统模板。 */
if(in_array($review->category, array('PP', 'SRS', 'HLDS', 'DDS', 'ADS', 'DBDS', 'intergrate', 'system')) && !$review->template)
{
$deliverable->name = zget($this->lang->upgrade->reviewObjectList, $review->category);
$deliverable->doc = 0;
$deliverable->docVersion = 0;
$deliverable->status = 'draft';
$deliverable->version = '';
$deliverable->review = 0;
$deliverableID = $this->dao->select('id')->from(TABLE_PROJECTDELIVERABLE)
->where('project')->eq($deliverable->project)
->andWhere('deliverable')->eq($deliverable->deliverable)
->andWhere('doc')->eq($deliverable->doc)
->fetch('id');
if(!$deliverableID) $this->dao->insert(TABLE_PROJECTDELIVERABLE)->data($deliverable)->exec();
}
}
$this->dao->exec('ALTER TABLE ' . TABLE_REVIEW . ' DROP `doc`');
}
/**
* 升级历史基线。
* Upgrade baseline.
*
* @access public
* @return void
*/
public function upgradeBaseline()
{
$baselines = $this->dao->select('id,`from`')->from(TABLE_OBJECT)->where('type')->eq('taged')->fetchAll();
$reviews = $this->dao->select('id,object,deliverable')->from(TABLE_REVIEW)->where('status')->eq('pass')->fetchAll('object');
foreach($baselines as $baseline)
{
if(empty($reviews[$baseline->from])) continue;
$review = $reviews[$baseline->from];
$data = new stdclass();
$data->category = $review->deliverable;
$data->categoryVersion = json_encode(array($review->deliverable => $review->id));
$data->status = 'pass';
$this->dao->update(TABLE_OBJECT)->data($data)->where('id')->eq($baseline->id)->exec();
$this->dao->update(TABLE_REVIEW)->set('isBaseline')->eq('1')->where('id')->eq($review->id)->exec();
}
return true;
}
/**
* 新增交付物权限。
* Add deliverable privs for all groups.
*
* @access public
* @return void
*/
public function addDeliverablePrivs()
{
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('project')->andWhere('method')->eq('deliverable')->fetchPairs();
$group = new stdclass();
$group->module = 'project';
foreach($groups as $groupID)
{
foreach(array('createDeliverable', 'deleteDeliverable', 'viewDeliverable') as $method)
{
$group->method = $method;
$group->group = $groupID;
$this->dao->insert(TABLE_GROUPPRIV)->data($group)->exec();
}
}
}
/**
* 升级开源版的阶段。
* Update stage for PMS.
*
* @access public
* @return void
*/
public function upgradeStage4PMS()
{
if($this->config->edition != 'open') return true;
$stageGroup = $this->dao->select('*')->from(TABLE_STAGE)->where('workflowGroup')->eq(0)->fetchGroup('projectType', 'id');
if(empty($stageGroup)) return true;
$typeNamePairs = array();
$needDeleteStage = array();
foreach($stageGroup as $projectModel => $stageList)
{
if($projectModel == 'ipd') continue;
foreach($stageList as $stageInfo)
{
if(isset($typeNamePairs[$stageInfo->type]) && in_array($stageInfo->name, $typeNamePairs[$stageInfo->type]))
{
$needDeleteStage[$stageInfo->id] = $stageInfo->id;
continue;
}
if(!isset($typeNamePairs[$stageInfo->type])) $typeNamePairs[$stageInfo->type] = array();
$typeNamePairs[$stageInfo->type][] = $stageInfo->name;
}
}
if(!empty($needDeleteStage)) $this->dao->delete()->from(TABLE_STAGE)->where('id')->in($needDeleteStage)->exec();
return true;
}
/**
* 升级阶段列表及评审点。
* Update stage and point.
*
* @access public
* @return void
*/
public function upgradeStageAndPoint()
{
$stageGroup = $this->dao->select('*')->from(TABLE_STAGE)->where('workflowGroup')->eq(0)->fetchGroup('projectType', 'id');
if(empty($stageGroup)) return true;
$this->app->loadConfig('project');
$this->app->loadConfig('stage');
$workflowList = $this->dao->select('projectModel, code, id')->from(TABLE_WORKFLOWGROUP)->where('projectModel')->in('waterfall,waterfallplus,ipd')->fetchAll('id');
foreach($workflowList as $flow)
{
$projectModel = $flow->projectModel;
if(!isset($stageGroup[$projectModel])) continue;
$flowCode = $flow->code;
foreach($stageGroup[$projectModel] as $stage)
{
if($flowCode == 'tpdproduct' && !in_array($stage->type, $this->config->project->categoryStages['TPD'])) continue;
if($flowCode == 'cbbproduct' && !in_array($stage->type, $this->config->project->categoryStages['CBB'])) continue;
if(in_array($flowCode, array('cpdproduct', 'cpdproject')) && !in_array($stage->type, $this->config->project->categoryStages['CPD'])) continue;
unset($stage->id);
$stage->workflowGroup = $flow->id;
if(empty($stage->editedDate)) $stage->editedDate = null;
$this->dao->insert(TABLE_STAGE)->data($stage)->exec();
$stageID = $this->dao->lastInsertID();
/* 添加IPD项目流程阶段的评审点。*/
if($projectModel == 'ipd')
{
$decision = new stdClass();
$decision->builtin = '1';
$decision->createdBy = 'system';
$decision->createdDate = helper::now();
$decisionFlow = new stdClass();
$decisionFlow->flow = 1;
$decisionFlow->objectType = 'decision';
$decisionFlow->relatedBy = 'system';
$decisionFlow->relatedDate = helper::now();
foreach($this->config->stage->ipdReviewPoint->{$stage->type} as $index => $point)
{
$decision->workflowGroup = $stage->workflowGroup;
$decision->stage = $stageID;
$decision->order = $index + 1;
$decision->title = $point;
$decision->type = strpos($point, 'TR') !== false ? 'TR' : 'DCP';
$decision->category = $point;
$this->dao->insert(TABLE_DECISION)->data($decision)->exec();
$decisionID = $this->dao->lastInsertID();
$decisionFlow->root = $stage->workflowGroup;
$decisionFlow->objectID = $decisionID;
$decisionFlow->extra = $decision->type;
$this->dao->insert(TABLE_APPROVALFLOWOBJECT)->data($decisionFlow)->exec();
}
}
}
}
return true;
}
/**
* 升级评审点评审。
* Upgrade object of decision.
*
* @access public
* @return void
*/
public function upgradeObjectOfDecision()
{
$ipdProject = $this->dao->select('id,workflowGroup')->from(TABLE_PROJECT)->where('model')->eq('ipd')->andWhere('type')->eq('project')->fetchPairs('id');
if(empty($ipdProject)) return true;
$this->app->loadConfig('review');
$stagePointGroup = array();
$ipdProjectStage = $this->dao->select('id,project,attribute')->from(TABLE_PROJECT)->where('project')->in(array_keys($ipdProject))->andWhere('type')->eq('stage')->fetchGroup('project', 'attribute');
foreach($ipdProjectStage as $projectID => $stageList)
{
if(!isset($stagePointGroup[$projectID])) $stagePointGroup[$projectID] = array();
foreach($stageList as $attribute => $stage)
{
foreach($this->config->review->ipdReviewPoint->$attribute as $pointCategory)
{
$stagePointGroup[$projectID][$pointCategory] = $stage->id;
}
}
}
$decisionGroup = $this->dao->select('id,workflowGroup,category')->from(TABLE_DECISION)->fetchGroup('workflowGroup', 'category');
$ipdStagePoints = $this->dao->select('*')->from(TABLE_OBJECT)->where('project')->in(array_keys($ipdProject))->andWhere('category')->in($this->config->review->ipdPointOrder)->fetchAll();
foreach($ipdStagePoints as $point)
{
$category = $point->category;
$projectID = $point->project;
$workflowGroup = $ipdProject[$projectID];
$this->dao->update(TABLE_OBJECT)
->set('type')->eq('decision')
->set('categoryTitle')->eq($category)
->set('execution')->eq(!empty($stagePointGroup[$projectID][$category]) ? $stagePointGroup[$projectID][$category] : 0)
->set('category')->eq(!empty($decisionGroup[$workflowGroup][$category]->id) ? $decisionGroup[$workflowGroup][$category]->id : 0)
->where('id')->eq($point->id)
->exec();
}
return true;
}
/**
* 调整 21.7.5 版本的权限
* Adjust priv 21.7.5.
*
* @access public
* @return bool
*/
public function adjustPriv21_7_5()
{
$weeklyMethods = array('browse', 'create', 'edit', 'delete', 'view', 'exportReport', 'manageCategroy');
$templateMethods = array('browse', 'create', 'edit', 'delete', 'view', 'pause', 'cron', 'addCategory', 'editCategory', 'deleteCategory');
$groups = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)->where('module')->eq('weekly')->fetchPairs('group', 'group');
$data = array();
foreach($groups as $groupID)
{
foreach($weeklyMethods as $method) $data[] = "('{$groupID}', 'weekly', '{$method}')";
$data[] = "('{$groupID}', 'milestone', 'index')";
$data[] = "('{$groupID}', 'milestone', 'saveOtherProblem')";
}
foreach($templateMethods as $method) $data[] = "('1', 'reporttemplate', '{$method}')";
foreach($weeklyMethods as $method) $data[] = "('1', 'weekly', '{$method}')";
$data[] = "('1', 'milestone', 'index')";
$data[] = "('1', 'milestone', 'saveOtherProblem')";
$sql = 'REPLACE INTO ' . TABLE_GROUPPRIV . '(`group`, `module`, `method`) VALUES ' . implode(',', $data);
$this->dao->exec($sql);
return true;
}
/**
* 四个项目模型的功能开关合并成一个。
* Merge four project model feature switches into one.
* 内置AI禅道智能体。
* Initialize the AI prompts.
*
* @access public
* @return bool
*/
public function upgradeClosedFeature()
{
$disabledFeatures = $this->dao->select('value')->from(TABLE_CONFIG)->where('`key`')->eq('closedFeatures')->andWhere('owner')->eq('system')->fetch('value');
if(empty($disabledFeatures)) return true;
$disabledFeatures = explode(',', $disabledFeatures);
$countFeature = array();
foreach($this->config->featureGroup->project as $newFeature)
{
$countFeature[$newFeature] = 0;
foreach($disabledFeatures as $key => $feature)
{
$feature = strtolower(trim($feature));
if(strpos($feature, $newFeature) !== false)
{
$countFeature[$newFeature]++;
unset($disabledFeatures[$key]);
}
}
}
/* 之前在所有模型下都关闭的功能升级上来才关闭,否则开启。*/
$disabledFeatures = array_values($disabledFeatures);
foreach($countFeature as $feature => $count)
{
if($count == 4 || ($count == 2 && in_array($feature, array('gapanalysis', 'researchplan', 'track'))))
{
$disabledFeatures[] = 'project' . ucfirst($feature);
}
}
$disabledFeatures = implode(',', array_unique($disabledFeatures));
$this->dao->update(TABLE_CONFIG)->set('value')->eq($disabledFeatures)->where('`key`')->eq('closedFeatures')->andWhere('owner')->eq('system')->exec();
}
/**
* 之前object表中data字段直接存入URL,如果改变请求方式则无法使用,现在升级为模块名、方法名、参数的形式。
* Before the object table directly stores the URL in the data field, if the request method changes, it cannot be used, now upgrade to the form of module name, method name, and parameters.
*
* @access public
* @return bool
*/
public function parseDocFetcherURL()
{
$objects = $this->dao->select('id,data')->from(TABLE_OBJECT)->where('data')->like('%tml-zentaolist%')->fetchPairs();
$cleanValue = function($value)
{
if(preg_match('/__(\w+)__([0-9]+)/', $value, $m)) return $m[2];
return $value;
};
$parsePathInfoUrl = function($url) use ($cleanValue)
{
$url = ltrim($url, '/');
$url = preg_replace('/\.html$/', '', $url);
$parts = explode('-', $url);
array_shift($parts); // 删掉模块名
array_shift($parts); // 删掉方法名
$params = [];
foreach($parts as $index => $value)
{
$index ++;
$key = "param{$index}";
$value = $cleanValue($value);
$params[$key] = $value;
}
return $params;
};
$parseGetUrl = function($url) use ($cleanValue)
{
$parts = parse_url($url);
parse_str($parts['query'] ?? '', $query);
unset($query['m'], $query['f']);
$params = [];
$index = 1;
foreach($query as $key => $value)
{
$key = 'param' . $index;
$value = $cleanValue($value);
$params[$key] = $value;
$index++;
}
return $params;
};
foreach($objects as $id => $data)
{
$data = json_decode($data, true);
$html = $data['$data'];
preg_match('/data-export-url=\'([^\']+)\'/', $html, $exportMatch);
preg_match('/data-fetcher=\'([^\']+)\'/', $html, $fetcherMatch);
$exportUrl = $exportMatch[1] ?? null;
$fetcherUrl = $fetcherMatch[1] ?? null;
if(!$exportUrl && !$fetcherUrl) continue;
if(strpos($exportUrl, '?') !== false)
{
$data['exportParams'] = $parseGetUrl($exportUrl);
}
else
{
$data['exportParams'] = $parsePathInfoUrl($exportUrl);
}
if(strpos($fetcherUrl, '?') !== false)
{
$data['fetcherParams'] = $parseGetUrl($fetcherUrl);
}
else
{
$data['fetcherParams'] = $parsePathInfoUrl($fetcherUrl);
}
$data['$data'] = str_replace($exportMatch[1], '', $data['$data']);
$data['$data'] = str_replace($fetcherMatch[1], '', $data['$data']);
$data = json_encode($data);
$this->dao->update(TABLE_OBJECT)->set('data')->eq($data)->where('id')->eq($id)->exec();
}
}
/**
* 处理升级新增的工作流数据源。
* Process workflow datasource.
*
* @param array $codeList
* @access public
* @return void
*/
public function processWorkflowDatasource($codeList)
{
$datasources = $this->dao->select('id,code')->from(TABLE_WORKFLOWDATASOURCE)->where('code')->in(array_keys($codeList))->fetchAll();
foreach($datasources as $datasource)
{
list($module, $field) = explode('_', $codeList[$datasource->code]);
$this->dao->update(TABLE_WORKFLOWFIELD)->set('options')->eq($datasource->id)->where('module')->eq($module)->andWhere('field')->eq($field)->exec();
}
}
/**
* 内置AI禅道智能体。
* Initialize the AI prompts.
*
* @access public
* @return bool
*/
public function initAIPrompts(): void
{
$this->app->loadConfig('ai');
foreach($this->config->ai->initAIPrompts as $aiPrompt)
{
if(!$this->checkExistAIPrompt($aiPrompt)) return;
$index = $aiPrompt->id;
unset($aiPrompt->id);
$aiPrompt->createdBy = 'system';
$aiPrompt->createdDate = helper::now();
$this->dao->insert(TABLE_AI_AGENT)->data($aiPrompt)->autoCheck()->exec();
$promptID = $this->dao->lastInsertID();
if(!empty($this->config->ai->initAIPromptFields[$index])) $this->initAIPromptFields($this->config->ai->initAIPromptFields[$index], $promptID);
}
}
/**
* 检查AI禅道智能体唯一性。
* Check AI zentao agent unique.
*
* @param object $aiPrompt
* @access public
* @return bool
*/
public function checkExistAIPrompt(object $aiPrompt): bool
{
$count = $this->dao->select('COUNT(1) AS count')->from(TABLE_AI_AGENT)
->where('name')->eq($aiPrompt->name)
->fetch('count');
return $count == 0;
}
/**
* 修改用户表的last字段类型。
* Alter user table field last type.
*
* @access public
* @return bool
*/
public function alterUserTableFields(): bool
{
$tableDesc = $this->dao->descTable(TABLE_USER);
if(isset($tableDesc['last']) && stripos($tableDesc['last']->type, 'datetime') !== false) return true;
$sql = 'ALTER TABLE ' . TABLE_USER . ' ADD `last_tmp` datetime DEFAULT NULL AFTER `last`';
$this->dao->exec($sql);
$users = $this->dao->select('id, last')->from(TABLE_USER)->where('last')->ne(0)->fetchPairs();
foreach($users as $id => $last)
{
$last = date('Y-m-d H:i:s', $last);
$this->dao->update(TABLE_USER)->set('last_tmp')->eq($last)->where('id')->eq($id)->exec();
}
$sql = 'ALTER TABLE ' . TABLE_USER . ' DROP `last`';
$this->dao->exec($sql);
$sql = 'ALTER TABLE ' . TABLE_USER . ' CHANGE COLUMN `last_tmp` `last` datetime DEFAULT NULL';
$this->dao->exec($sql);
return !dao::isError();
}
/**
* 内置AI禅道智能体和相关字段。
* Initialize the AI prompts and fields.
*
* @param array $aiPromptFields
* @param int $promptID
* @access public
* @return bool
*/
public function initAIPromptFields(array $aiPromptFields, int $promptID): void
{
foreach($aiPromptFields as $field)
{
$field->appID = $promptID;
$this->dao->insert(TABLE_AI_AGENTFIELD)->data($field, 'id')->exec();
}
}
/*
* 升级项目流程相关的权限。
* Update the priv of workflow group.
*
* @access public
* @return bool
*/
public function updateWorkflowGroupPriv(): bool
{
$designFlowGroupIdList = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('workflowfield')
->andWhere('method')->eq('browse')
->fetchPairs();
foreach($designFlowGroupIdList as $groupID) $this->dao->insert(TABLE_GROUPPRIV)->set('group')->eq($groupID)->set('module')->eq('workflowgroup')->set('method')->eq('designFlow')->exec();
$flowReportGroupIdList = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('workflowgroup')
->andWhere('method')->eq('design')
->fetchPairs();
foreach($flowReportGroupIdList as $groupID) $this->dao->insert(TABLE_GROUPPRIV)->set('group')->eq($groupID)->set('module')->eq('workflowgroup')->set('method')->eq('report')->exec();
$setDelieverableGroupIdList = $this->dao->select('`group`')->from(TABLE_GROUPPRIV)
->where('module')->eq('workflowgroup')
->andWhere('method')->eq('deliverable')
->fetchPairs();
foreach($setDelieverableGroupIdList as $groupID)
{
$this->dao->insert(TABLE_GROUPPRIV)->set('group')->eq($groupID)->set('module')->eq('deliverable')->set('method')->eq('enable')->exec();
$this->dao->insert(TABLE_GROUPPRIV)->set('group')->eq($groupID)->set('module')->eq('deliverable')->set('method')->eq('disable')->exec();
}
$this->dao->delete()->from(TABLE_GROUPPRIV)->where('module')->eq('workflowgroup')->andWhere('method')->eq('deliverable')->exec();
return true;
}
/**
* 处理报告模板中内置的适用流程。
* Upgrade report template objects.
*
* @access public
* @return bool
*/
public function upgradeReportTemplateObjects(): bool
{
$templateList = $this->dao->select('id,objects')->from(TABLE_DOC)->where('templateType')->eq('reportTemplate')->fetchPairs('id');
$workflowList = $this->dao->select('id,projectModel,projectType')->from(TABLE_WORKFLOWGROUP)->where('main')->eq('1')->fetchAll('id');
$flowGroup = array();
foreach($workflowList as $flowID => $flow)
{
if(!isset($flowGroup[$flow->projectModel])) $flowGroup[$flow->projectModel] = array();
if(!isset($flowGroup[$flow->projectModel][$flow->projectType])) $flowGroup[$flow->projectModel][$flow->projectType] = array();
$flowGroup[$flow->projectModel][$flow->projectType][] = $flowID;
}
foreach($templateList as $templateID => $objects)
{
$newObjects = array();
$objectIdList = array_filter(explode(',', $objects));
foreach($objectIdList as $objectID)
{
$newObjects[] = $objectID;
if(!isset($workflowList[$objectID])) continue;
$projectModel = $workflowList[$objectID]->projectModel;
$projectType = $workflowList[$objectID]->projectType;
if($projectModel == 'scrum' && $projectType == 'product') $newObjects = array_merge($newObjects, $flowGroup['agileplus']['product']);
if($projectModel == 'scrum' && $projectType == 'project') $newObjects = array_merge($newObjects, $flowGroup['agileplus']['project']);
if($projectModel == 'waterfall' && $projectType == 'product') $newObjects = array_merge($newObjects, $flowGroup['waterfallplus']['product'], $flowGroup['ipd']['ipd'], $flowGroup['ipd']['tpd'], $flowGroup['ipd']['cbb'], $flowGroup['ipd']['cpdproduct']);
if($projectModel == 'waterfall' && $projectType == 'project') $newObjects = array_merge($newObjects, $flowGroup['waterfallplus']['project'], $flowGroup['ipd']['cpdproject']);
}
if(!empty($newObjects))
{
$newObjectList = implode(',', array_filter(array_unique($newObjects)));
$this->dao->update(TABLE_DOC)->set('objects')->eq($newObjectList)->where('id')->eq($templateID)->exec();
}
}
return true;
}
}