mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-16 17:04:11 +01:00
Compare commits
48 Commits
class_extr
...
saas-1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b448e29f5 | ||
|
|
7cb6af0a2b | ||
|
|
ddc9952ec1 | ||
|
|
a1a9ffe192 | ||
|
|
7728082c00 | ||
|
|
970183ef45 | ||
|
|
6c2db1e687 | ||
|
|
32d74fbc8e | ||
|
|
477f2f51e9 | ||
|
|
94ea8e60e8 | ||
|
|
b9a00b15f5 | ||
|
|
e87f5af465 | ||
|
|
97d717b016 | ||
|
|
251fd3c67b | ||
|
|
7b0a569c64 | ||
|
|
7176bc8686 | ||
|
|
9c0b906ded | ||
|
|
0533916dad | ||
|
|
60b08586c2 | ||
|
|
28df2942e4 | ||
|
|
cd48d2ad37 | ||
|
|
9db2205241 | ||
|
|
045985cd5b | ||
|
|
809b371520 | ||
|
|
9bbc7342b8 | ||
|
|
973c435138 | ||
|
|
1c3dfd6491 | ||
|
|
9400b697eb | ||
|
|
49748a0374 | ||
|
|
163276a6c2 | ||
|
|
9b0c2f7324 | ||
|
|
e1807f598f | ||
|
|
02e63fff64 | ||
|
|
0864f05d9f | ||
|
|
1bbcd9656a | ||
|
|
34d8e52c22 | ||
|
|
ac7309e48c | ||
|
|
c8fade6013 | ||
|
|
ad052dd861 | ||
|
|
a5ea868609 | ||
|
|
0b03b3ef4d | ||
|
|
174cace20a | ||
|
|
e3f5dbfc80 | ||
|
|
40e24c25a2 | ||
|
|
c6e4466c53 | ||
|
|
6638eb4adc | ||
|
|
eb40968e34 | ||
|
|
766c9f0e7e |
@@ -9,6 +9,7 @@ use Combodo\iTop\Application\Helper\WebResourcesHelper;
|
||||
require_once(APPROOT.'/application/utils.inc.php');
|
||||
require_once(APPROOT.'/application/template.class.inc.php');
|
||||
require_once(APPROOT."/application/user.dashboard.class.inc.php");
|
||||
require_once(APPROOT."/setup/parentmenunodecompiler.class.inc.php");
|
||||
|
||||
|
||||
/**
|
||||
@@ -103,7 +104,7 @@ class ApplicationMenu
|
||||
{
|
||||
self::$sFavoriteSiloQuery = $sOQL;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the query used to limit the list of displayed organizations in the drop-down menu
|
||||
* @return string The OQL query returning a list of Organization objects
|
||||
@@ -273,12 +274,23 @@ class ApplicationMenu
|
||||
continue;
|
||||
}
|
||||
|
||||
$aSubMenuNodes = static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams);
|
||||
if (! ParentMenuNodeCompiler::$bUseLegacyMenuCompilation && !($oMenuNode instanceof ShortcutMenuNode)){
|
||||
if (is_array($aSubMenuNodes) && 0 === sizeof($aSubMenuNodes)){
|
||||
IssueLog::Error('Empty menu node not displayed', LogChannels::CONSOLE, [
|
||||
'menu_node_class' => get_class($oMenuNode),
|
||||
'menu_node_label' => $oMenuNode->GetLabel(),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$aMenuGroups[] = [
|
||||
'sId' => $oMenuNode->GetMenuID(),
|
||||
'sIconCssClasses' => $oMenuNode->GetDecorationClasses(),
|
||||
'sInitials' => $oMenuNode->GetInitials(),
|
||||
'sTitle' => $oMenuNode->GetTitle(),
|
||||
'aSubMenuNodes' => static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams),
|
||||
'aSubMenuNodes' => $aSubMenuNodes,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -525,7 +537,7 @@ EOF
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the currently active menu (if any, otherwise the first menu is the default)
|
||||
* @return string The Id of the currently active menu
|
||||
@@ -533,7 +545,7 @@ EOF
|
||||
public static function GetActiveNodeId()
|
||||
{
|
||||
$oAppContext = new ApplicationContext();
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
if ($sMenuId === null)
|
||||
{
|
||||
$sMenuId = self::GetDefaultMenuId();
|
||||
@@ -643,7 +655,7 @@ abstract class MenuNode
|
||||
|
||||
/**
|
||||
* Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu
|
||||
*/
|
||||
*/
|
||||
protected $m_aEnableStimuli;
|
||||
|
||||
/**
|
||||
@@ -804,7 +816,7 @@ abstract class MenuNode
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a limiting display condition for the same menu node. The conditions will be combined with a AND
|
||||
* @param $oMenuNode MenuNode Another definition of the same menu node, with potentially different access restriction
|
||||
@@ -977,7 +989,7 @@ class TemplateMenuNode extends MenuNode
|
||||
* @var string
|
||||
*/
|
||||
protected $sTemplateFile;
|
||||
|
||||
|
||||
/**
|
||||
* Create a menu item based on a custom template and inserts it into the application's main menu
|
||||
* @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary)
|
||||
@@ -1048,7 +1060,7 @@ class OQLMenuNode extends MenuNode
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $bSearchFormOpen;
|
||||
|
||||
|
||||
/**
|
||||
* Extra parameters to be passed to the display block to fine tune its appearence
|
||||
*/
|
||||
@@ -1081,7 +1093,7 @@ class OQLMenuNode extends MenuNode
|
||||
// Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects
|
||||
// of the class specified by the OQL...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set some extra parameters to be passed to the display block to fine tune its appearence
|
||||
* @param array $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters
|
||||
@@ -1109,7 +1121,7 @@ class OQLMenuNode extends MenuNode
|
||||
'Menu_'.$this->GetMenuId(),
|
||||
$this->bSearch, // Search pane
|
||||
$this->bSearchFormOpen, // Search open
|
||||
$oPage,
|
||||
$oPage,
|
||||
array_merge($this->m_aParams, $aExtraParams),
|
||||
true
|
||||
);
|
||||
@@ -1343,10 +1355,10 @@ class NewObjectMenuNode extends MenuNode
|
||||
{
|
||||
// Enable this menu, only if the current user has enough rights to create such an object, or an object of
|
||||
// any child class
|
||||
|
||||
|
||||
$aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
|
||||
$bActionIsAllowed = false;
|
||||
|
||||
|
||||
foreach($aSubClasses as $sCandidateClass)
|
||||
{
|
||||
if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
|
||||
@@ -1355,7 +1367,7 @@ class NewObjectMenuNode extends MenuNode
|
||||
break; // Enough for now
|
||||
}
|
||||
}
|
||||
return $bActionIsAllowed;
|
||||
return $bActionIsAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1497,7 +1509,7 @@ class DashboardMenuNode extends MenuNode
|
||||
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1538,7 +1550,7 @@ class ShortcutContainerMenuNode extends MenuNode
|
||||
$sName = $this->GetMenuId().'_'.$oShortcut->GetKey();
|
||||
new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++);
|
||||
}
|
||||
|
||||
|
||||
// Complete the tree
|
||||
//
|
||||
parent::PopulateChildMenus();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
|
||||
|
||||
@@ -323,12 +324,12 @@ EOF
|
||||
EOF
|
||||
);
|
||||
$sHTMLValue .= "<div class=\"ibo-input-select--action-buttons\">";
|
||||
$sHTMLValue .= " <div class=\"ibo-input-select--action-button ibo-input-select--action-button--clear ibo-is-hidden\" id=\"mini_clear_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.Clear();\" data-tooltip-content='".Dict::S('UI:Button:Clear')."'><i class=\"fas fa-times\"></i></div>";
|
||||
$sHTMLValue .= " <a href=\"#\" class=\"ibo-input-select--action-button ibo-input-select--action-button--clear ibo-is-hidden\" id=\"mini_clear_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.Clear();\" data-tooltip-content='".Dict::S('UI:Button:Clear')."'><i class=\"fas fa-times\"></i></a>";
|
||||
}
|
||||
if ($bCreate && $bExtensions) {
|
||||
$sCallbackName = (MetaModel::IsAbstract($this->sTargetClass)) ? 'SelectObjectClass' : 'CreateObject';
|
||||
|
||||
$sHTMLValue .= "<div class=\"ibo-input-select--action-button ibo-input-select--action-button--create\" id=\"mini_add_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.{$sCallbackName}();\" data-tooltip-content='".Dict::S('UI:Button:Create')."'><i class=\"fas fa-plus\"></i></div>";
|
||||
$sHTMLValue .= "<a href=\"#\" class=\"ibo-input-select--action-button ibo-input-select--action-button--create\" id=\"mini_add_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.{$sCallbackName}();\" data-tooltip-content='".Dict::S('UI:Button:Create')."'><i class=\"fas fa-plus\"></i></a>";
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
if ($('#ajax_{$this->iId}').length == 0)
|
||||
@@ -339,7 +340,7 @@ JS
|
||||
);
|
||||
}
|
||||
if ($bExtensions && MetaModel::IsHierarchicalClass($this->sTargetClass) !== false) {
|
||||
$sHTMLValue .= "<div class=\"ibo-input-select--action-button ibo-input-select--action-button--hierarchy\" id=\"mini_tree_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.HKDisplay();\" data-tooltip-content='".Dict::S('UI:Button:SearchInHierarchy')."'><i class=\"fas fa-sitemap\"></i></div>";
|
||||
$sHTMLValue .= "<a href=\"#\" class=\"ibo-input-select--action-button ibo-input-select--action-button--hierarchy\" id=\"mini_tree_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.HKDisplay();\" data-tooltip-content='".Dict::S('UI:Button:SearchInHierarchy')."'><i class=\"fas fa-sitemap\"></i></a>";
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
if ($('#ac_tree_{$this->iId}').length == 0)
|
||||
@@ -350,7 +351,7 @@ JS
|
||||
);
|
||||
}
|
||||
if ($oAllowedValues->CountExceeds($iMaxComboLength)) {
|
||||
$sHTMLValue .= " <div class=\"ibo-input-select--action-button ibo-input-select--action-button--search\" id=\"mini_search_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.Search();\" data-tooltip-content='".Dict::S('UI:Button:Search')."'><i class=\"fas fa-search\"></i></div>";
|
||||
$sHTMLValue .= " <a href=\"#\" class=\"ibo-input-select--action-button ibo-input-select--action-button--search\" id=\"mini_search_{$this->iId}\" onClick=\"oACWidget_{$this->iId}.Search();\" data-tooltip-content='".Dict::S('UI:Button:Search')."'><i class=\"fas fa-search\"></i></a>";
|
||||
}
|
||||
$sHTMLValue .= "</div>";
|
||||
$sHTMLValue .= "</div>";
|
||||
@@ -904,7 +905,7 @@ JS
|
||||
{
|
||||
// For security reasons: check that the "proposed" class is actually a subclass of the linked class
|
||||
// and that the current user is allowed to create objects of this class
|
||||
$aSubClasses = MetaModel::EnumChildClasses($this->sTargetClass);
|
||||
$aSubClasses = MetaModel::EnumChildClasses($this->sTargetClass, ENUM_CHILD_CLASSES_ALL);
|
||||
$aPossibleClasses = array();
|
||||
foreach($aSubClasses as $sCandidateClass)
|
||||
{
|
||||
@@ -924,6 +925,7 @@ JS
|
||||
$sDialogTitleEscaped = addslashes($sDialogTitle);
|
||||
$oPage->add_ready_script("$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', maxHeight: $(window).height() - 50, autoOpen: false, modal: true, title: '$sDialogTitleEscaped'});\n");
|
||||
$oPage->add_ready_script("$('#ac_create_{$this->iId} form').removeAttr('onsubmit');");
|
||||
$oPage->add_ready_script("$('#ac_create_{$this->iId} form').find('select').attr('id', 'ac_create_{$this->iId}_select');");
|
||||
$oPage->add_ready_script("$('#ac_create_{$this->iId} form').on('submit.uilinksWizard', oACWidget_{$this->iId}.DoSelectObjectClass);");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF
|
||||
|
||||
/**
|
||||
* CellChangeSpec
|
||||
* A series of classes, keeping the information about a given cell: could it be changed or not (and why)?
|
||||
* A series of classes, keeping the information about a given cell: could it be changed or not (and why)?
|
||||
*
|
||||
* @package iTopORM
|
||||
*/
|
||||
@@ -42,6 +42,17 @@ abstract class CellChangeSpec
|
||||
return $this->m_sOql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
*/
|
||||
public function GetDisplayableValueAndDescription(): string
|
||||
{
|
||||
return sprintf("%s%s",
|
||||
$this->GetDisplayableValue(),
|
||||
$this->GetDescription()
|
||||
);
|
||||
}
|
||||
|
||||
abstract public function GetDescription();
|
||||
}
|
||||
|
||||
@@ -86,26 +97,90 @@ class CellStatus_Issue extends CellStatus_Modify
|
||||
parent::__construct($proposedValue, $previousValue);
|
||||
}
|
||||
|
||||
public function GetDescription()
|
||||
public function GetDisplayableValue()
|
||||
{
|
||||
if (is_null($this->m_proposedValue))
|
||||
{
|
||||
return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason);
|
||||
return Dict::Format('UI:CSVReport-Value-SetIssue');
|
||||
}
|
||||
return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason);
|
||||
return Dict::Format('UI:CSVReport-Value-ChangeIssue', \utils::EscapeHtml($this->m_proposedValue));
|
||||
}
|
||||
|
||||
public function GetDescription()
|
||||
{
|
||||
return $this->m_sReason;
|
||||
}
|
||||
/*
|
||||
* @since 3.1.0 N°5305
|
||||
*/
|
||||
public function GetDisplayableValueAndDescription(): string
|
||||
{
|
||||
return sprintf("%s. %s",
|
||||
$this->GetDisplayableValue(),
|
||||
$this->GetDescription()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellStatus_SearchIssue extends CellStatus_Issue
|
||||
{
|
||||
public function __construct()
|
||||
/** @var string|null $m_sAllowedValues */
|
||||
private $m_sAllowedValues;
|
||||
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
* @var string $sSerializedSearch
|
||||
*/
|
||||
private $sSerializedSearch;
|
||||
|
||||
/** @var string|null $m_sTargetClass */
|
||||
private $m_sTargetClass;
|
||||
|
||||
/**
|
||||
* CellStatus_SearchIssue constructor.
|
||||
* @since 3.1.0 N°5305
|
||||
*
|
||||
* @param string $sOql : main message
|
||||
* @param string $sReason : main message
|
||||
* @param null $sClass : used for additional message that provides allowed values for current class $sClass
|
||||
* @param null $sAllowedValues : used for additional message that provides allowed values $sAllowedValues for current class
|
||||
*/
|
||||
public function __construct($sSerializedSearch, $sReason, $sClass=null, $sAllowedValues=null)
|
||||
{
|
||||
parent::__construct(null, null, null);
|
||||
parent::__construct(null, null, $sReason);
|
||||
$this->sSerializedSearch = $sSerializedSearch;
|
||||
$this->m_sAllowedValues = $sAllowedValues;
|
||||
$this->m_sTargetClass = $sClass;
|
||||
}
|
||||
|
||||
public function GetDisplayableValue()
|
||||
{
|
||||
if (null === $this->m_sReason) {
|
||||
return Dict::Format('UI:CSVReport-Value-NoMatch', '');
|
||||
}
|
||||
|
||||
return $this->m_sReason;
|
||||
}
|
||||
|
||||
public function GetDescription()
|
||||
{
|
||||
return Dict::S('UI:CSVReport-Value-NoMatch');
|
||||
if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) ||
|
||||
\utils::IsNullOrEmptyString($this->m_sTargetClass)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
* @return string
|
||||
*/
|
||||
public function GetSearchLinkUrl()
|
||||
{
|
||||
return sprintf("UI.php?operation=search&filter=%s",
|
||||
rawurlencode($this->sSerializedSearch)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +201,24 @@ class CellStatus_NullIssue extends CellStatus_Issue
|
||||
class CellStatus_Ambiguous extends CellStatus_Issue
|
||||
{
|
||||
protected $m_iCount;
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
* @var string
|
||||
*/
|
||||
protected $sSerializedSearch;
|
||||
|
||||
public function __construct($previousValue, $iCount, $sOql)
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
*
|
||||
* @param $previousValue
|
||||
* @param int $iCount
|
||||
* @param string $sSerializedSearch
|
||||
*
|
||||
*/
|
||||
public function __construct($previousValue, $iCount, $sSerializedSearch)
|
||||
{
|
||||
$this->m_iCount = $iCount;
|
||||
$this->m_sQuery = $sOql;
|
||||
$this->sSerializedSearch = $sSerializedSearch;
|
||||
parent::__construct(null, $previousValue, '');
|
||||
}
|
||||
|
||||
@@ -139,12 +227,23 @@ class CellStatus_Ambiguous extends CellStatus_Issue
|
||||
$sCount = $this->m_iCount;
|
||||
return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
* @return string
|
||||
*/
|
||||
public function GetSearchLinkUrl()
|
||||
{
|
||||
return sprintf("UI.php?operation=search&filter=%s",
|
||||
rawurlencode($this->sSerializedSearch)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RowStatus
|
||||
* A series of classes, keeping the information about a given row: could it be changed or not (and why)?
|
||||
* A series of classes, keeping the information about a given row: could it be changed or not (and why)?
|
||||
*
|
||||
* @package iTopORM
|
||||
*/
|
||||
@@ -211,6 +310,26 @@ class RowStatus_Issue extends RowStatus
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* class dedicated to testability
|
||||
* not used/ignored in csv imports UI/CLI
|
||||
* @since 3.1.0 N°5305
|
||||
*/
|
||||
class RowStatus_Error extends RowStatus
|
||||
{
|
||||
/** @var string */
|
||||
protected $m_sError;
|
||||
|
||||
public function __construct($sError)
|
||||
{
|
||||
$this->m_sError = $sError;
|
||||
}
|
||||
|
||||
public function GetDescription()
|
||||
{
|
||||
return $this->m_sError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BulkChange
|
||||
@@ -220,17 +339,35 @@ class RowStatus_Issue extends RowStatus
|
||||
*/
|
||||
class BulkChange
|
||||
{
|
||||
protected $m_sClass;
|
||||
/** @var string */
|
||||
protected $m_sClass;
|
||||
protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
|
||||
// #@# todo: rename the variables to sColIndex
|
||||
protected $m_aAttList; // attcode => iCol
|
||||
protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
|
||||
protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey)
|
||||
protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported
|
||||
protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined)
|
||||
protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat
|
||||
protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum)
|
||||
protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias
|
||||
/** @var array<string, string> attcode as key, iCol as value */
|
||||
protected $m_aAttList;
|
||||
/** @var array<string, array<string, string>> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */
|
||||
protected $m_aExtKeys;
|
||||
/** @var string[] list of attcode (attcode = 'id' for the pkey) */
|
||||
protected $m_aReconcilKeys;
|
||||
/** @var string OQL - if specified, then the missing items will be reported */
|
||||
protected $m_sSynchroScope;
|
||||
/**
|
||||
* @var array<string, mixed> attcode as key, attvalue as value. Values to be set when an object gets out of scope
|
||||
* (ignored if no scope has been defined)
|
||||
*/
|
||||
protected $m_aOnDisappear;
|
||||
/**
|
||||
* @see DateTime::createFromFormat
|
||||
* @var string Date format specification
|
||||
*/
|
||||
protected $m_sDateFormat;
|
||||
/**
|
||||
* @see AttributeEnum
|
||||
* @var boolean true if Values in the data set are localized
|
||||
*/
|
||||
protected $m_bLocalizedValues;
|
||||
/** @var array Cache for resolving external keys based on the given search criterias */
|
||||
protected $m_aExtKeysMappingCache;
|
||||
|
||||
public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false)
|
||||
{
|
||||
@@ -261,30 +398,30 @@ class BulkChange
|
||||
$this->m_sReportCsvSep = $sSeparator;
|
||||
$this->m_sReportCsvDelimiter = $sDelimiter;
|
||||
}
|
||||
|
||||
|
||||
protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
|
||||
{
|
||||
$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
|
||||
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
||||
foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
|
||||
foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol)
|
||||
{
|
||||
if ($sForeignAttCode == 'id')
|
||||
if ($sReconKeyAttCode == 'id')
|
||||
{
|
||||
$value = (int) $aRowData[$iCol];
|
||||
}
|
||||
else
|
||||
{
|
||||
// The foreign attribute is one of our reconciliation key
|
||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
|
||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
|
||||
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||
}
|
||||
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
|
||||
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
|
||||
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
||||
}
|
||||
|
||||
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
||||
$aKeys = $oExtObjects->ToArray();
|
||||
return array($oReconFilter->ToOql(), $aKeys);
|
||||
return array($oReconFilter, $aKeys);
|
||||
}
|
||||
|
||||
// Returns true if the CSV data specifies that the external key must be left undefined
|
||||
@@ -318,10 +455,10 @@ class BulkChange
|
||||
{
|
||||
$aResults = array();
|
||||
$aErrors = array();
|
||||
|
||||
|
||||
// External keys reconciliation
|
||||
//
|
||||
foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
|
||||
foreach($this->m_aExtKeys as $sAttCode => $aReconKeys)
|
||||
{
|
||||
// Skip external keys used for the reconciliation process
|
||||
// if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
|
||||
@@ -330,7 +467,7 @@ class BulkChange
|
||||
|
||||
if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
|
||||
{
|
||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
||||
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||
{
|
||||
// Default reporting
|
||||
// $aRowData[$iCol] is always null
|
||||
@@ -352,25 +489,24 @@ class BulkChange
|
||||
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
||||
|
||||
$aCacheKeys = array();
|
||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
||||
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||
{
|
||||
// The foreign attribute is one of our reconciliation key
|
||||
if ($sForeignAttCode == 'id')
|
||||
if ($sReconKeyAttCode == 'id')
|
||||
{
|
||||
$value = $aRowData[$iCol];
|
||||
}
|
||||
else
|
||||
{
|
||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
|
||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
|
||||
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||
}
|
||||
$aCacheKeys[] = $value;
|
||||
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
|
||||
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
|
||||
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
||||
}
|
||||
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
|
||||
$iForeignKey = null;
|
||||
$sOQL = '';
|
||||
// TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
|
||||
if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache))
|
||||
{
|
||||
@@ -379,9 +515,8 @@ class BulkChange
|
||||
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
|
||||
{
|
||||
// Cache hit
|
||||
$iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
|
||||
$iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
|
||||
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
|
||||
$sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql'];
|
||||
// Record the hit
|
||||
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
|
||||
}
|
||||
@@ -389,34 +524,35 @@ class BulkChange
|
||||
{
|
||||
// Cache miss, let's initialize it
|
||||
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
||||
$iCount = $oExtObjects->Count();
|
||||
if ($iCount == 1)
|
||||
$iObjectFoundCount = $oExtObjects->Count();
|
||||
if ($iObjectFoundCount == 1)
|
||||
{
|
||||
$oForeignObj = $oExtObjects->Fetch();
|
||||
$iForeignKey = $oForeignObj->GetKey();
|
||||
}
|
||||
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
|
||||
'c' => $iCount,
|
||||
'c' => $iObjectFoundCount,
|
||||
'k' => $iForeignKey,
|
||||
'oql' => $oReconFilter->ToOql(),
|
||||
'h' => 0, // number of hits on this cache entry
|
||||
);
|
||||
}
|
||||
switch($iCount)
|
||||
switch($iObjectFoundCount)
|
||||
{
|
||||
case 0:
|
||||
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
|
||||
$aResults[$sAttCode]= new CellStatus_SearchIssue();
|
||||
break;
|
||||
|
||||
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
|
||||
$aResults[$sAttCode] = $oCellStatus_SearchIssue;
|
||||
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Do change the external key attribute
|
||||
$oTargetObj->Set($sAttCode, $iForeignKey);
|
||||
break;
|
||||
|
||||
// Do change the external key attribute
|
||||
$oTargetObj->Set($sAttCode, $iForeignKey);
|
||||
break;
|
||||
|
||||
default:
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount);
|
||||
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL);
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount);
|
||||
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +569,7 @@ class BulkChange
|
||||
else
|
||||
{
|
||||
$aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
|
||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
||||
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||
{
|
||||
// Report the change on reconciliation values as well
|
||||
$aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol]));
|
||||
@@ -446,7 +582,7 @@ class BulkChange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set the object attributes
|
||||
//
|
||||
foreach ($this->m_aAttList as $sAttCode => $iCol)
|
||||
@@ -487,7 +623,13 @@ class BulkChange
|
||||
$value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||
if (is_null($value) && (strlen($aRowData[$iCol]) > 0))
|
||||
{
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
|
||||
if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet){
|
||||
/** @var AttributeDefinition $oAttributeDefinition */
|
||||
$oAttributeDefinition = $oAttDef;
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues()));
|
||||
} else {
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -504,7 +646,7 @@ class BulkChange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reporting on fields
|
||||
//
|
||||
$aChangedFields = $oTargetObj->ListChanges();
|
||||
@@ -556,7 +698,7 @@ class BulkChange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Checks
|
||||
//
|
||||
$res = $oTargetObj->CheckConsistency();
|
||||
@@ -567,12 +709,101 @@ class BulkChange
|
||||
}
|
||||
return $aResults;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* search with current permissions did not match
|
||||
* let's search why and give some more feedbacks to the user through proper labels
|
||||
*
|
||||
* @param DBObjectSearch $oDbSearchWithConditions search used to find external key
|
||||
*
|
||||
* @return \CellStatus_SearchIssue
|
||||
* @throws \CoreException
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
*
|
||||
* @since 3.1.0 N°5305
|
||||
*/
|
||||
protected function GetCellSearchIssue($oDbSearchWithConditions) : CellStatus_SearchIssue {
|
||||
//current search with current permissions did not match
|
||||
//let's search why and give some more feedback to the user
|
||||
|
||||
$sSerializedSearch = $oDbSearchWithConditions->serialize();
|
||||
|
||||
// Count all objects with all permissions without any condition
|
||||
$oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass());
|
||||
$oDbSearchWithoutAnyCondition->AllowAllData(true);
|
||||
$oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
|
||||
$iAllowAllDataObjectCount = $oExtObjectSet->Count();
|
||||
|
||||
if ($iAllowAllDataObjectCount === 0) {
|
||||
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass());
|
||||
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
|
||||
}
|
||||
|
||||
// Count all objects with current user permissions
|
||||
$oDbSearchWithoutAnyCondition->AllowAllData(false);
|
||||
$oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
|
||||
$iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count();
|
||||
|
||||
if ($iCurrentUserRightsObjectCount === 0){
|
||||
// No objects visible by current user
|
||||
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
|
||||
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
|
||||
}
|
||||
|
||||
try{
|
||||
$aDisplayedAllowedValues = [];
|
||||
// Possibles values are displayed to UI user. we have to limit the amount of displayed values
|
||||
$oExtObjectSetWithCurrentUserPermissions->SetLimit(4);
|
||||
for($i = 0; $i < 3; $i++){
|
||||
/** @var \DBObject $oVisibleObject */
|
||||
$oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch();
|
||||
if (is_null($oVisibleObject)){
|
||||
break;
|
||||
}
|
||||
|
||||
$aCurrentAllowedValueFields = [];
|
||||
foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue){
|
||||
$aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode);
|
||||
}
|
||||
$aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields);
|
||||
|
||||
}
|
||||
$allowedValues = implode(", ", $aDisplayedAllowedValues);
|
||||
if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3){
|
||||
$allowedValues .= "...";
|
||||
}
|
||||
} catch(Exception $e) {
|
||||
IssueLog::Error("failure during CSV import when fetching few visible objects: ", null,
|
||||
[ 'target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()]
|
||||
);
|
||||
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
|
||||
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
|
||||
}
|
||||
|
||||
if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) {
|
||||
// No match and some objects NOT visible by current user. including current search maybe...
|
||||
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass());
|
||||
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
|
||||
}
|
||||
|
||||
// No match. This is not linked to any right issue
|
||||
// Possible values: DD,DD
|
||||
$aCurrentValueFields = [];
|
||||
foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue){
|
||||
$aCurrentValueFields[] = $sValue;
|
||||
}
|
||||
$value =implode(" ", $aCurrentValueFields);
|
||||
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value);
|
||||
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
|
||||
}
|
||||
|
||||
protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
|
||||
{
|
||||
$aResults = array();
|
||||
$aErrors = array();
|
||||
|
||||
|
||||
// External keys
|
||||
//
|
||||
foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
|
||||
@@ -585,7 +816,7 @@ class BulkChange
|
||||
$aResults[$iCol] = new CellStatus_Void('?');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update attributes
|
||||
//
|
||||
foreach($this->m_aOnDisappear as $sAttCode => $value)
|
||||
@@ -596,7 +827,7 @@ class BulkChange
|
||||
}
|
||||
$oTargetObj->Set($sAttCode, $value);
|
||||
}
|
||||
|
||||
|
||||
// Reporting on fields
|
||||
//
|
||||
$aChangedFields = $oTargetObj->ListChanges();
|
||||
@@ -616,7 +847,7 @@ class BulkChange
|
||||
$aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Checks
|
||||
//
|
||||
$res = $oTargetObj->CheckConsistency();
|
||||
@@ -674,14 +905,16 @@ class BulkChange
|
||||
}
|
||||
|
||||
$aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
|
||||
|
||||
|
||||
if (count($aErrors) > 0)
|
||||
{
|
||||
$sErrors = implode(', ', $aErrors);
|
||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
||||
//__ERRORS__ used by tests only
|
||||
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
|
||||
return $oTargetObj;
|
||||
}
|
||||
|
||||
|
||||
// Check that any external key will have a value proposed
|
||||
$aMissingKeys = array();
|
||||
foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey)
|
||||
@@ -689,7 +922,7 @@ class BulkChange
|
||||
if (!$oExtKey->IsNullAllowed())
|
||||
{
|
||||
if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList))
|
||||
{
|
||||
{
|
||||
$aMissingKeys[] = $oExtKey->GetLabel();
|
||||
}
|
||||
}
|
||||
@@ -745,14 +978,16 @@ class BulkChange
|
||||
{
|
||||
$sErrors = implode(', ', $aErrors);
|
||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
||||
//__ERRORS__ used by tests only
|
||||
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$aChangedFields = $oTargetObj->ListChanges();
|
||||
if (count($aChangedFields) > 0)
|
||||
{
|
||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
|
||||
|
||||
|
||||
// Optionaly record the results
|
||||
//
|
||||
if ($oChange)
|
||||
@@ -794,9 +1029,11 @@ class BulkChange
|
||||
{
|
||||
$sErrors = implode(', ', $aErrors);
|
||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
||||
//__ERRORS__ used by tests only
|
||||
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$aChangedFields = $oTargetObj->ListChanges();
|
||||
if (count($aChangedFields) > 0)
|
||||
{
|
||||
@@ -821,7 +1058,7 @@ class BulkChange
|
||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function Process(CMDBChange $oChange = null)
|
||||
{
|
||||
if ($oChange)
|
||||
@@ -866,7 +1103,7 @@ class BulkChange
|
||||
foreach ($this->m_aAttList as $sAttCode => $iCol)
|
||||
{
|
||||
if ($sAttCode == 'id') continue;
|
||||
|
||||
|
||||
$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
|
||||
if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime
|
||||
{
|
||||
@@ -881,14 +1118,18 @@ class BulkChange
|
||||
$sFormat = $sDateFormat;
|
||||
}
|
||||
$oFormat = new DateTimeFormat($sFormat);
|
||||
$sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33'));
|
||||
$sRegExp = $oFormat->ToRegExpr('/');
|
||||
if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol]))
|
||||
$sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample);
|
||||
if (!preg_match($sRegExp, $sValue))
|
||||
{
|
||||
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
||||
$aResult[$iRow][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
$oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]);
|
||||
$oDate = DateTime::createFromFormat($sFormat, $sValue);
|
||||
if ($oDate !== false)
|
||||
{
|
||||
$sNewDate = $oDate->format($oAttDef->GetInternalFormat());
|
||||
@@ -898,7 +1139,7 @@ class BulkChange
|
||||
{
|
||||
// Leave the cell unchanged
|
||||
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
||||
$aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, utils::HtmlEntities($this->m_aData[$iRow][$iCol]), Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
||||
$aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -952,23 +1193,26 @@ class BulkChange
|
||||
else
|
||||
{
|
||||
// The value has to be found or verified
|
||||
list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
|
||||
|
||||
|
||||
/** var DBObjectSearch $oReconFilter */
|
||||
list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
|
||||
|
||||
if (count($aMatches) == 1)
|
||||
{
|
||||
$oRemoteObj = reset($aMatches); // first item
|
||||
$valuecondition = $oRemoteObj->GetKey();
|
||||
$aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
|
||||
}
|
||||
}
|
||||
elseif (count($aMatches) == 0)
|
||||
{
|
||||
$aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
|
||||
}
|
||||
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
|
||||
$aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
|
||||
}
|
||||
else
|
||||
{
|
||||
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
|
||||
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1019,7 +1263,7 @@ class BulkChange
|
||||
default:
|
||||
// Found several matches, ambiguous
|
||||
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
|
||||
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
|
||||
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
|
||||
$aResult[$iRow]["finalclass"]= 'n/a';
|
||||
}
|
||||
}
|
||||
@@ -1110,7 +1354,7 @@ class BulkChange
|
||||
}
|
||||
}
|
||||
$oBulkChanges->Seek(0);
|
||||
|
||||
|
||||
$aDetails = array();
|
||||
while ($oChange = $oBulkChanges->Fetch())
|
||||
{
|
||||
@@ -1274,7 +1518,7 @@ EOF
|
||||
$oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
|
||||
$sOldValue = $oOldTarget->GetHyperlink();
|
||||
}
|
||||
|
||||
|
||||
$sNewValue = Dict::S('UI:UndefinedObject');
|
||||
if ($oOperation->Get('newvalue') != 0)
|
||||
{
|
||||
@@ -1300,11 +1544,11 @@ EOF
|
||||
}
|
||||
else
|
||||
{
|
||||
$aAttributes[$sAttCode] = 1;
|
||||
$aAttributes[$sAttCode] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aDetails = array();
|
||||
foreach($aObjects as $iUId => $aObjData)
|
||||
{
|
||||
@@ -1356,6 +1600,6 @@ EOF
|
||||
$aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
|
||||
}
|
||||
$oPage->table($aConfig, $aDetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1238,6 +1238,14 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'navigation_menu.show_organization_filter' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Display organization filter in menu',
|
||||
'default' => true,
|
||||
'value' => true,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'quick_create.enabled' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Whether or not the quick create is enabled',
|
||||
@@ -1416,6 +1424,14 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'allow_rest_services_via_tokens' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'When set to true, REST endpoint token authorization works even with secure_rest_services set.',
|
||||
'default' => false,
|
||||
'value' => false,
|
||||
'source_of_value' => '',
|
||||
'show_in_conf_sample' => false,
|
||||
],
|
||||
'search_manual_submit' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Force manual submit of search all requests',
|
||||
@@ -1861,7 +1877,7 @@ class Config
|
||||
}
|
||||
if (strlen($sNoise) > 0)
|
||||
{
|
||||
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
|
||||
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
|
||||
throw new ConfigException('Syntax error in configuration file',
|
||||
array('file' => $sConfigFile, 'error' => '<tt>'.htmlentities($sNoise, ENT_QUOTES, 'UTF-8').'</tt>'));
|
||||
}
|
||||
@@ -2671,7 +2687,7 @@ class ConfigPlaceholdersResolver
|
||||
}
|
||||
|
||||
$sPattern = '/\%(env|server)\((\w+)\)(?:\?:(\w*))?\%/'; //3 capturing groups, ie `%env(HTTP_PORT)?:8080%` produce: `env` `HTTP_PORT` and `8080`.
|
||||
|
||||
|
||||
if (! preg_match_all($sPattern, $rawValue, $aMatchesCollection, PREG_SET_ORDER))
|
||||
{
|
||||
return $rawValue;
|
||||
|
||||
@@ -253,3 +253,6 @@ $ibo-field--enable-bulk--checkbox--margin-left: $ibo-spacing-300 !default;
|
||||
margin-left: $ibo-field--enable-bulk--checkbox--margin-left;
|
||||
}
|
||||
|
||||
.ibo-input-select--action-buttons a {
|
||||
@extend %ibo-hyperlink-inherited-colors;
|
||||
}
|
||||
7
css/backoffice/pages/_csv-import.scss
vendored
7
css/backoffice/pages/_csv-import.scss
vendored
@@ -27,11 +27,6 @@ tr.ibo-csv-import--row-unchanged td {
|
||||
border-bottom: 1px $ibo-color-grey-400 solid;
|
||||
}
|
||||
|
||||
.wizContainer table tr.ibo-csv-import--row-error td {
|
||||
border-bottom: 1px $ibo-color-grey-400 solid;
|
||||
background-color: $ibo-color-red-200;
|
||||
}
|
||||
|
||||
tr.ibo-csv-import--row-modified td {
|
||||
border-bottom: 1px $ibo-color-grey-400 solid;
|
||||
}
|
||||
@@ -44,4 +39,4 @@ tr.ibo-csv-import--row-added td {
|
||||
font-size: 4em;
|
||||
color: $ibo-color-primary-400;
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ $ibo-sticky-sentinel-bottom--height: $ibo-sticky-sentinel--height !default;
|
||||
opacity: 1 !important; /* Note: !important is necessary as it needs to overload any standard rules */
|
||||
}
|
||||
|
||||
.ibo-is-disabled {
|
||||
cursor: not-allowed !important; /* Note: !important is necessary as it needs to overload any standard rules */
|
||||
}
|
||||
|
||||
/****************************/
|
||||
/* Disposition / alignement */
|
||||
/****************************/
|
||||
|
||||
@@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte třídu pro hledání: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Upraveno',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Nemůže být změněno - důvod: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Nemůže být změněno na %1$s - důvod: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Žádná shoda',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů',
|
||||
'UI:CSVReport-Row-Unchanged' => 'nezměněn',
|
||||
|
||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Vælg klasse at søge efter: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Ændret',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Kunne ikke ændres - årsag: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Kunne ikke ændres til %1$s - årsag: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter',
|
||||
'UI:CSVReport-Row-Unchanged' => 'Uændret',
|
||||
|
||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Wählen Sie für die Suche die Klasse aus: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modifiziert',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Konnte nicht geändert werden - Grund: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Konnte nicht zu %1$s geändert werden - Grund: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Kein Treffer',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden',
|
||||
'UI:CSVReport-Row-Unchanged' => 'Unverändert',
|
||||
|
||||
@@ -650,9 +650,14 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Select the class to search: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modified',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Invalid value for attribute',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'',
|
||||
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Some possible \'%1$s\' value(s): %2$s',
|
||||
'UI:CSVReport-Value-NoMatch-NoObject' => 'There are no \'%1$s\' objects',
|
||||
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'There are no \'%1$s\' objects found with your current profile',
|
||||
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'There are some \'%1$s\' objects not visible with your current profile',
|
||||
|
||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
||||
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
||||
@@ -666,11 +671,13 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:CSVReport-Value-Issue-Readonly' => 'The attribute \'%1$s\' is read-only and cannot be modified (current value: %2$s, proposed value: %3$s)',
|
||||
'UI:CSVReport-Value-Issue-Format' => 'Failed to process input: %1$s',
|
||||
'UI:CSVReport-Value-Issue-NoMatch' => 'Unexpected value for attribute \'%1$s\': no match found, check spelling',
|
||||
'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s',
|
||||
'UI:CSVReport-Value-Issue-Unknown' => 'Unexpected value for attribute \'%1$s\': %2$s',
|
||||
'UI:CSVReport-Row-Issue-Inconsistent' => 'Attributes not consistent with each others: %1$s',
|
||||
'UI:CSVReport-Row-Issue-Attribute' => 'Unexpected attribute value(s)',
|
||||
'UI:CSVReport-Row-Issue-MissingExtKey' => 'Could not be created, due to missing external key(s): %1$s',
|
||||
'UI:CSVReport-Row-Issue-DateFormat' => 'wrong date format',
|
||||
'UI:CSVReport-Row-Issue-ExpectedDateFormat' => 'Expected format: %1$s',
|
||||
'UI:CSVReport-Row-Issue-Reconciliation' => 'failed to reconcile',
|
||||
'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation',
|
||||
'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s',
|
||||
|
||||
@@ -644,9 +644,9 @@ Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleccione la clase a buscar: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modificado',
|
||||
'UI:CSVReport-Value-SetIssue' => 'No puede ser modificado - motivo: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'No puede ser cambiado a %1$s - motivo: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No hay Coincidencias',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos',
|
||||
'UI:CSVReport-Row-Unchanged' => 'Sin Cambios',
|
||||
|
||||
@@ -633,9 +633,14 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modifié',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Modification impossible - cause : %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\' - cause : %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Valeur invalide',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\'',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance avec \'%1$s\'',
|
||||
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Valeur(s) possible(s) pour l\'objet \'%1$s\' : %2$s',
|
||||
'UI:CSVReport-Value-NoMatch-NoObject' => 'Il n\'y a aucun objet \'%1$s\'',
|
||||
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'Il n\'y a aucun objet \'%1$s\' visible par votre utilisateur',
|
||||
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'Il existe des objet(s) \'%1$s\' non visible(s) par votre utilisateur',
|
||||
|
||||
'UI:CSVReport-Value-Missing' => 'Absence de valeur obligatoire',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés',
|
||||
'UI:CSVReport-Row-Unchanged' => 'inchangé',
|
||||
|
||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Keresendő osztály kiválasztása:',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modified~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
||||
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',
|
||||
|
||||
@@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleziona la classe per la ricerca: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modified~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
||||
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',
|
||||
|
||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => '検索するクラスを選択してください。',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => '修正済み',
|
||||
'UI:CSVReport-Value-SetIssue' => '変更出来ません - 理由: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '%1$s へ変更出来ません - 理由: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'マッチしません',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => '必須の値がありません',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト',
|
||||
'UI:CSVReport-Row-Unchanged' => '未変更',
|
||||
|
||||
@@ -644,9 +644,9 @@ We hopen dat je even hard van deze versie geniet als dat we zelf ervan hebben ge
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecteer de klasse om te zoeken: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Aangepast',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Kon niet worden aangepast - reden: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Kon niet worden aangepast naar %1$s - reden: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Geen match',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten',
|
||||
'UI:CSVReport-Row-Unchanged' => 'onveranderd',
|
||||
|
||||
@@ -643,9 +643,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Wybierz klasę do przeszukania: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Zmodyfikowano',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Nie można było zmienić - powód: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Nie można zmienić na %1$s - powód: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Nie pasuje',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów',
|
||||
'UI:CSVReport-Row-Unchanged' => 'niezmieniony',
|
||||
|
||||
@@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecione a classe para pesquisar: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Modificado',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Não pode ser modificado - razão: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Não pode ser modificado para %1$s - razão: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Não combina',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
||||
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
||||
|
||||
@@ -645,9 +645,9 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Изменен',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Не может быть изменен - причина: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Не может быть изменен %1$s - причина: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Нет совпадений',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов',
|
||||
'UI:CSVReport-Row-Unchanged' => 'без изменений',
|
||||
|
||||
@@ -634,9 +634,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte triedu na vyhľadávanie: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Upravený',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Nemožno zmeniť - dôvod: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Nemožno zmeniť na %1$s - dôvod: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Žiadna zhoda',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov',
|
||||
'UI:CSVReport-Row-Unchanged' => 'Nezmený',
|
||||
|
||||
@@ -661,9 +661,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Aranacak sınıfı seçiniz: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Değiştiridi',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Değiştirilemedi - Sebep: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '%1$s olarak değiştirilemedi - Sebep: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Eşleşme yok',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum',
|
||||
'UI:CSVReport-Row-Unchanged' => 'Değiştirilmedi',
|
||||
|
||||
@@ -649,9 +649,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => '选择要搜索的类别: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => '已修改',
|
||||
'UI:CSVReport-Value-SetIssue' => '无法修改 - 原因: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '无法修改成 %1$s - 原因: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => '不匹配',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => '缺少必填项',
|
||||
'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象',
|
||||
'UI:CSVReport-Row-Unchanged' => '保持不变',
|
||||
|
||||
@@ -634,7 +634,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
// will force it be of the same class as the previous call)
|
||||
me.sTargetClass = me.sOriginalTargetClass;
|
||||
|
||||
me.CreateObject(oWizHelper);
|
||||
me.CreateObject();
|
||||
};
|
||||
|
||||
this.DoSelectObjectClass = function () {
|
||||
@@ -646,13 +646,12 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
|
||||
// Setting new target class
|
||||
me.sTargetClass = oSelectedClass.val();
|
||||
|
||||
// Opening real creation form
|
||||
me.CreateObject(true);
|
||||
$('#ac_create_'+me.id).dialog('close');
|
||||
me.CreateObject();
|
||||
};
|
||||
|
||||
this.CreateObject = function (oWizHelper) {
|
||||
this.CreateObject = function (bTargetClassSelected) {
|
||||
if ($('#'+me.id).prop('disabled')) {
|
||||
return;
|
||||
} // Disabled, do nothing
|
||||
@@ -670,7 +669,8 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
sAttCode: me.sAttCode,
|
||||
'json': me.oWizardHelper.ToJSON(),
|
||||
operation: 'objectCreationForm',
|
||||
ajax_promise_id: sPromiseId
|
||||
ajax_promise_id: sPromiseId,
|
||||
bTargetClassSelected: bTargetClassSelected
|
||||
};
|
||||
|
||||
// Make sure that we cancel any pending request before issuing another
|
||||
@@ -678,6 +678,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
me.StopPendingRequest();
|
||||
|
||||
// Run the query and get the result back directly in HTML
|
||||
var sLocalTargetClass = me.sTargetClass; // Remember the target class since it will be reset when closing the dialog
|
||||
me.ajax_request = $.post(AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), theMap,
|
||||
function (data) {
|
||||
$('#ajax_'+me.id).html(data);
|
||||
@@ -687,6 +688,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
// Modify the action of the cancel button
|
||||
$('#ac_create_'+me.id+' button.cancel').off('click').on('click', me.CloseCreateObject);
|
||||
me.ajax_request = null;
|
||||
me.sTargetClass = sLocalTargetClass;
|
||||
// Adjust the dialog's size to fit into the screen
|
||||
if ($('#ac_create_'+me.id).width() > ($(window).width()-40)) {
|
||||
$('#ac_create_'+me.id).width($(window).width()-40);
|
||||
@@ -714,6 +716,10 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper
|
||||
$('#ac_create_'+me.id).dialog("destroy");
|
||||
$('#ac_create_'+me.id).remove();
|
||||
$('#ajax_'+me.id).html('');
|
||||
// Resetting target class to its original value
|
||||
// (If not done, closing the dialog and trying to create a object again
|
||||
// will force it be of the same class as the previous call)
|
||||
me.sTargetClass = me.sOriginalTargetClass;
|
||||
};
|
||||
|
||||
this.DoCreateObject = function () {
|
||||
|
||||
@@ -124,5 +124,16 @@ $.widget( "itop.regulartabs", $.ui.tabs, {
|
||||
this._off( prevPanels.not( this.panels ) );
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
// JQuery UI overload
|
||||
disable: function(index){
|
||||
const panel = this._getPanelForTab( index );
|
||||
panel.addClass('ibo-is-hidden'); // Do not use .hide() since it alters the tab state
|
||||
this._super( index );
|
||||
},
|
||||
// JQuery UI overload
|
||||
enable: function(index) {
|
||||
const panel = this._getPanelForTab( index );
|
||||
panel.removeClass('ibo-is-hidden'); // Do not use .show() since it alters the tab state
|
||||
this._super( index );
|
||||
},
|
||||
});
|
||||
|
||||
@@ -377,4 +377,16 @@ $.widget( "itop.scrollabletabs", $.ui.tabs, {
|
||||
setTab : function(tab){
|
||||
this.active = tab;
|
||||
},
|
||||
});
|
||||
// JQuery UI overload
|
||||
disable: function(index){
|
||||
const panel = this._getPanelForTab( this.tabs[index] );
|
||||
panel.addClass('ibo-is-hidden'); // Do not use .hide() since it alters the tab state
|
||||
this._super( index );
|
||||
},
|
||||
// JQuery UI overload
|
||||
enable: function(index) {
|
||||
const panel = this._getPanelForTab( this.tabs[index] );
|
||||
panel.removeClass('ibo-is-hidden'); // Do not use .show() since it alters the tab state
|
||||
this._super( index );
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ $(function()
|
||||
css_classes:
|
||||
{
|
||||
is_hidden: 'ibo-is-hidden',
|
||||
is_disabled: 'ibo-is-disabled',
|
||||
is_transparent: 'ibo-is-transparent',
|
||||
is_opaque: 'ibo-is-opaque',
|
||||
is_scrollable: 'ibo-is-scrollable',
|
||||
@@ -252,6 +253,11 @@ $(function()
|
||||
// Prevent anchor default behaviour
|
||||
oEvent.preventDefault();
|
||||
|
||||
if (oExtraTabTogglerElem.attr('aria-disabled') === 'true') {
|
||||
// Corresponding tab is disabled, do nothing
|
||||
oEvent.stopPropagation();
|
||||
return;
|
||||
}
|
||||
// Trigger click event on real tab toggler (the hidden one)
|
||||
const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, '');
|
||||
this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click');
|
||||
@@ -297,17 +303,30 @@ $(function()
|
||||
const sTabId = oTabHeaderElem.attr('data-tab-id');
|
||||
const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');
|
||||
|
||||
// Disabled tabs should be disabled in the ExtraTabs list as well
|
||||
let bIsDisabled = false;
|
||||
if (oTabHeaderElem.attr('aria-disabled') === 'true') {
|
||||
bIsDisabled = true;
|
||||
}
|
||||
// Manually check if the tab header is visible if the info isn't passed
|
||||
if (bIsVisible === null) {
|
||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
||||
}
|
||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
||||
}
|
||||
|
||||
// Hide/show the corresponding extra tab element
|
||||
if (bIsVisible) {
|
||||
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
||||
}
|
||||
if (bIsVisible) {
|
||||
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
||||
}
|
||||
// Enable/disable the corresponding extra tab element
|
||||
if (bIsDisabled) {
|
||||
oMatchingExtraTabElem.attr('aria-disabled', 'true');
|
||||
oMatchingExtraTabElem.addClass(this.css_classes.is_disabled);
|
||||
} else {
|
||||
oMatchingExtraTabElem.attr('aria-disabled', 'false');
|
||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_disabled);
|
||||
}
|
||||
},
|
||||
// - Update extra tabs list
|
||||
_updateExtraTabsList: function () {
|
||||
@@ -326,7 +345,7 @@ $(function()
|
||||
* @return {string} The [data-tab-id] of the iIdx-th tab (zero based). Can return undefined if it has not [data-tab-id] attribute
|
||||
* @private
|
||||
*/
|
||||
_getTabIdFromTabIndex(iIdx) {
|
||||
_getTabIdFromTabIndex: function(iIdx) {
|
||||
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header).eq(iIdx).attr('data-tab-id');
|
||||
},
|
||||
/**
|
||||
@@ -334,10 +353,41 @@ $(function()
|
||||
* @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
|
||||
* @private
|
||||
*/
|
||||
_getTabIndexFromTabId(sId) {
|
||||
_getTabIndexFromTabId: function(sId) {
|
||||
const oTabElem = this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
|
||||
|
||||
return oTabElem.length === 0 ? 0 : oTabElem.prevAll().length;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param sId {string} The [data-tab-id] of the tab
|
||||
* @return {Object} The jQuery object representing the tab element
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_getTabElementFromTabId: function(sId) {
|
||||
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
|
||||
},
|
||||
/**
|
||||
* @param sId {string} The [data-tab-id] of the tab
|
||||
* @return {Object} The jQuery object representing the tab element
|
||||
*/
|
||||
disableTab: function(sId){
|
||||
const tabsWidget = this.GetTabsWidget();
|
||||
const iIdx = this._getTabIndexFromTabId(sId);
|
||||
tabsWidget.disable(iIdx);
|
||||
const tabElement = this._getTabElementFromTabId(sId);
|
||||
this._updateTabHeaderDisplay(tabElement);
|
||||
},
|
||||
/**
|
||||
* @param sId {string} The [data-tab-id] of the tab
|
||||
* @return {Object} The jQuery object representing the tab element
|
||||
*/
|
||||
enableTab: function(sId){
|
||||
const tabsWidget = this.GetTabsWidget();
|
||||
const iIdx = this._getTabIndexFromTabId(sId);
|
||||
tabsWidget.enable(iIdx);
|
||||
const tabElement = this._getTabElementFromTabId(sId);
|
||||
this._updateTabHeaderDisplay(tabElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -410,9 +410,10 @@ try
|
||||
$iInputId = utils::ReadParam('iInputId', '');
|
||||
$sAttCode = utils::ReadParam('sAttCode', '');
|
||||
$sJson = utils::ReadParam('json', '', false, 'raw_data');
|
||||
// Building form, if target class is abstract we ask the user for the desired leaf class
|
||||
$bTargetClassSelected = utils::ReadParam('bTargetClassSelected', '', false, 'raw_data');
|
||||
// Building form, if target class has child classes we ask the user for the desired leaf class, unless we've already done just that
|
||||
$oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, $sAttCode, false);
|
||||
if(MetaModel::IsAbstract($sTargetClass))
|
||||
if(!$bTargetClassSelected && MetaModel::HasChildrenClasses($sTargetClass))
|
||||
{
|
||||
$oWidget->GetClassSelectionForm($oPage);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ try {
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the most frequent (and regularly occuring) character among the given set, in the specified lines
|
||||
* @param array $aCSVData The input data, one entry per line
|
||||
@@ -175,7 +175,7 @@ try {
|
||||
}
|
||||
$iLine++;
|
||||
}
|
||||
|
||||
|
||||
$aScores = array();
|
||||
foreach($aGuesses as $sSep => $aData)
|
||||
{
|
||||
@@ -186,7 +186,7 @@ try {
|
||||
$sSeparator = $aKeys[0]; // Take the first key, the one with the best score
|
||||
return $sSeparator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to predict the CSV parameters based on the input data
|
||||
* @param string $sCSVData The input data
|
||||
@@ -197,10 +197,10 @@ try {
|
||||
$aData = explode("\n", $sCSVData);
|
||||
$sSeparator = GuessFromFrequency($aData, array("\t", ',', ';', '|')); // Guess the most frequent (and regular) character on each line
|
||||
$sQualifier = GuessFromFrequency($aData, array('"', "'")); // Guess the most frequent (and regular) character on each line
|
||||
|
||||
|
||||
return array('separator' => $sSeparator, 'qualifier' => $sQualifier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display a banner for the special "synchro" mode
|
||||
* @param WebPage $oP The Page for the output
|
||||
@@ -216,6 +216,7 @@ try {
|
||||
* Add a paragraph to the body of the page
|
||||
*
|
||||
* @param string $s_html
|
||||
* @param ?string $sLinkUrl
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -260,9 +261,9 @@ try {
|
||||
$sSynchroScope = utils::ReadParam('synchro_scope', '', false, 'raw_data');
|
||||
$sDateTimeFormat = utils::ReadParam('date_time_format', 'default');
|
||||
$sCustomDateTimeFormat = utils::ReadParam('custom_date_time_format', (string)AttributeDateTime::GetFormat(), false, 'raw_data');
|
||||
|
||||
|
||||
$sChosenDateFormat = ($sDateTimeFormat == 'default') ? (string)AttributeDateTime::GetFormat() : $sCustomDateTimeFormat;
|
||||
|
||||
|
||||
if (!empty($sSynchroScope))
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL($sSynchroScope);
|
||||
@@ -277,7 +278,7 @@ try {
|
||||
$sSynchroScope = '';
|
||||
$aSynchroUpdate = null;
|
||||
}
|
||||
|
||||
|
||||
// Parse the data set
|
||||
$oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop'));
|
||||
$aData = $oCSVParser->ToArray($iSkippedLines);
|
||||
@@ -287,10 +288,10 @@ try {
|
||||
$aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error
|
||||
$iRealSkippedLines++;
|
||||
}
|
||||
|
||||
|
||||
// Format for the line numbers
|
||||
$sMaxLen = (strlen(''.count($aData)) < 3) ? 3 : strlen(''.count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3
|
||||
|
||||
|
||||
// Compute the list of search/reconciliation criteria
|
||||
$aSearchKeys = array();
|
||||
foreach($aSearchFields as $index => $sDummy)
|
||||
@@ -304,16 +305,16 @@ try {
|
||||
}
|
||||
else
|
||||
{
|
||||
$aSearchKeys[$sSearchField] = '';
|
||||
$aSearchKeys[$sSearchField] = '';
|
||||
}
|
||||
if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField))
|
||||
{
|
||||
// Remove invalid or unmapped search fields
|
||||
$aSearchFields[$index] = null;
|
||||
unset($aSearchKeys[$sSearchField]);
|
||||
unset($aSearchKeys[$sSearchField]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compute the list of fields and external keys to process
|
||||
$aExtKeys = array();
|
||||
$aAttributes = array();
|
||||
@@ -346,13 +347,13 @@ try {
|
||||
}
|
||||
else
|
||||
{
|
||||
$aAttributes[$sAttCode] = $iIndex;
|
||||
$aAttributes[$sAttCode] = $iIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$oMyChange = null;
|
||||
if (!$bSimulate)
|
||||
{
|
||||
@@ -362,8 +363,6 @@ try {
|
||||
CMDBObject::SetTrackOrigin(CMDBChangeOrigin::CSV_INTERACTIVE);
|
||||
$oMyChange = CMDBObject::GetCurrentChange();
|
||||
}
|
||||
CMDBObject::SetTrackOrigin('csv-interactive');
|
||||
|
||||
$oBulk = new BulkChange(
|
||||
$sClassName,
|
||||
$aData,
|
||||
@@ -373,7 +372,7 @@ try {
|
||||
empty($sSynchroScope) ? null : $sSynchroScope,
|
||||
$aSynchroUpdate,
|
||||
$sChosenDateFormat, // date format
|
||||
true // localize
|
||||
true // localize
|
||||
);
|
||||
$oBulk->SetReportHtml();
|
||||
|
||||
@@ -440,7 +439,6 @@ try {
|
||||
|
||||
case 'RowStatus_NewObj':
|
||||
$iCreated++;
|
||||
$sFinalClass = $aResRow['finalclass'];
|
||||
$sStatus = '<img src="../images/added.png" title="'.Dict::S('UI:CSVReport-Icon-Created').'">';
|
||||
$sCSSRowClass = 'ibo-csv-import--row-added';
|
||||
if ($bSimulate) {
|
||||
@@ -456,7 +454,7 @@ try {
|
||||
case 'RowStatus_Issue':
|
||||
$iErrors++;
|
||||
$sMessage .= GetDivAlert($oStatus->GetDescription());
|
||||
$sStatus = '<img src="../images/error.png" title="'.Dict::S('UI:CSVReport-Icon-Error').'">';//translate
|
||||
$sStatus = '<div class="ibo-csv-import--cell-error"><i class="fas fa-exclamation-triangle" title="'.Dict::S('UI:CSVReport-Icon-Error').'" /></div>';//translate
|
||||
$sCSSMessageClass = 'ibo-csv-import--cell-error';
|
||||
$sCSSRowClass = 'ibo-csv-import--row-error';
|
||||
if (array_key_exists($iLine, $aData)) {
|
||||
@@ -477,33 +475,36 @@ try {
|
||||
if (isset($aExternalKeysByColumn[$iNumber - 1])) {
|
||||
$sExtKeyName = $aExternalKeysByColumn[$iNumber - 1];
|
||||
$oExtKeyCellStatus = $aResRow[$sExtKeyName];
|
||||
switch (get_class($oExtKeyCellStatus)) {
|
||||
case 'CellStatus_Issue':
|
||||
case 'CellStatus_SearchIssue':
|
||||
case 'CellStatus_NullIssue':
|
||||
case 'CellStatus_Ambiguous':
|
||||
$sCellMessage .= GetDivAlert($oExtKeyCellStatus->GetDescription());
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
$oCellStatus = $oExtKeyCellStatus;
|
||||
}
|
||||
$sHtmlValue = $oCellStatus->GetDisplayableValue();
|
||||
switch (get_class($oCellStatus)) {
|
||||
case 'CellStatus_Issue':
|
||||
case 'CellStatus_NullIssue':
|
||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'</div>';
|
||||
break;
|
||||
|
||||
case 'CellStatus_SearchIssue':
|
||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">ERROR: '.$sHtmlValue.$sCellMessage.'</div>';
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
|
||||
'<a href="',
|
||||
$oCellStatus->GetSearchLinkUrl(),
|
||||
'"><div class="ibo-csv-import--cell-error">',
|
||||
Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue),
|
||||
GetDivAlert($oCellStatus->GetDescription()),
|
||||
'<i class="fas fa-search"></i></div><a/>'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'CellStatus_Ambiguous':
|
||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error" >'.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.'</div>';
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
|
||||
'<a href="',
|
||||
$oCellStatus->GetSearchLinkUrl(),
|
||||
'"><i class="fas fa-search"/><div class="ibo-csv-import--cell-error">',
|
||||
Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue),
|
||||
GetDivAlert($oCellStatus->GetDescription()),
|
||||
'<i class="fas fa-search"></i></div><a/>'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'CellStatus_Modify':
|
||||
@@ -592,7 +593,7 @@ try {
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged));
|
||||
$oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})");
|
||||
|
||||
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<img src="../images/error.png"> '.sprintf($aDisplayFilters['errors'], $iErrors), '', "1", "show_errors", "checkbox");
|
||||
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<i class="fas fa-exclamation-triangle" style="color:#A33; background-color: #FFF0F0;"> '.sprintf($aDisplayFilters['errors'], $iErrors).'</i></i>', '', "1", "show_errors", "checkbox");
|
||||
$oCheckBoxUnchanged->GetInput()->SetIsChecked(true);
|
||||
$oCheckBoxUnchanged->SetBeforeInput(false);
|
||||
$oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
@@ -679,7 +680,7 @@ try {
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$sErrors = json_encode(Dict::Format('UI:CSVImportError_items', $iErrors));
|
||||
$sCreated = json_encode(Dict::Format('UI:CSVImportCreated_items', $iCreated));
|
||||
$sModified = json_encode(Dict::Format('UI:CSVImportModified_items', $iModified));
|
||||
@@ -774,7 +775,7 @@ EOF
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
* Perform the actual load of the CSV data and display the results
|
||||
@@ -798,7 +799,7 @@ EOF
|
||||
$oField->AddSubBlock($oText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Simulate the load of the CSV data and display the results
|
||||
* @param WebPage $oPage The web page to display the wizard
|
||||
@@ -810,7 +811,7 @@ EOF
|
||||
$oPage->AddSubBlock($oPanel);
|
||||
ProcessCSVData($oPage, true /* simulate */);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select the mapping between the CSV column and the fields of the objects
|
||||
* @param WebPage $oPage The web page to display the wizard
|
||||
@@ -923,10 +924,10 @@ EOF
|
||||
$aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name');
|
||||
$sFieldsMapping = addslashes(json_encode($aFieldsMapping));
|
||||
$sSearchFields = addslashes(json_encode($aSearchFields));
|
||||
|
||||
|
||||
$oPage->add_ready_script("DoMapping('$sFieldsMapping', '$sSearchFields');"); // There is already a class selected, run the mapping
|
||||
}
|
||||
|
||||
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
var aDefaultKeys = new Array();
|
||||
@@ -1142,7 +1143,7 @@ EOF
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select the options of the CSV load and check for CSV parsing errors
|
||||
* @param WebPage $oPage The current web page
|
||||
@@ -1166,7 +1167,7 @@ EOF
|
||||
$sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data');
|
||||
}
|
||||
$sEncoding = utils::ReadParam('encoding', 'UTF-8');
|
||||
|
||||
|
||||
// Compute a subset of the data set, now that we know the charset
|
||||
if ($sEncoding == 'UTF-8')
|
||||
{
|
||||
@@ -1183,7 +1184,7 @@ EOF
|
||||
{
|
||||
$sUTF8Data = iconv($sEncoding, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
|
||||
}
|
||||
|
||||
|
||||
$aGuesses = GuessParameters($sUTF8Data); // Try to predict the parameters, based on the input data
|
||||
|
||||
$iSkippedLines = utils::ReadParam('nb_skipped_lines', '');
|
||||
@@ -1191,7 +1192,7 @@ EOF
|
||||
$sTextQualifier = utils::ReadParam('text_qualifier', '', false, 'raw_data');
|
||||
if ($sTextQualifier == '') // May be set to an empty value by the previous page
|
||||
{
|
||||
$sTextQualifier = $aGuesses['qualifier'];
|
||||
$sTextQualifier = $aGuesses['qualifier'];
|
||||
}
|
||||
$sOtherTextQualifier = in_array($sTextQualifier, array('"', "'")) ? '' : $sTextQualifier;
|
||||
$bHeaderLine = utils::ReadParam('header_line', 0);
|
||||
@@ -1610,7 +1611,7 @@ EOF
|
||||
null, AjaxTab::ENUM_TAB_PLACEHOLDER_MISC);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch($iStep)
|
||||
{
|
||||
case 11:
|
||||
@@ -1618,45 +1619,45 @@ EOF
|
||||
$oPage = new AjaxPage('');
|
||||
BulkChange::DisplayImportHistory($oPage);
|
||||
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tableHover();');
|
||||
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
|
||||
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
|
||||
break;
|
||||
|
||||
|
||||
case 10:
|
||||
// Case generated by BulkChange::DisplayImportHistory
|
||||
$iChange = (int)utils::ReadParam('changeid', 0);
|
||||
BulkChange::DisplayImportHistoryDetails($oPage, $iChange);
|
||||
break;
|
||||
|
||||
|
||||
case 5:
|
||||
LoadData($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 4:
|
||||
Preview($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 3:
|
||||
SelectMapping($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 2:
|
||||
SelectOptions($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 1:
|
||||
case 6: // Loop back here when we are done
|
||||
default:
|
||||
Welcome($oPage);
|
||||
}
|
||||
|
||||
|
||||
$oPage->output();
|
||||
}
|
||||
catch(CoreException $e)
|
||||
{
|
||||
require_once(APPROOT.'/setup/setuppage.class.inc.php');
|
||||
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
|
||||
$oP->output();
|
||||
|
||||
if (MetaModel::IsLogEnabledIssue())
|
||||
@@ -1684,8 +1685,8 @@ catch(Exception $e)
|
||||
{
|
||||
require_once(APPROOT.'/setup/setuppage.class.inc.php');
|
||||
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
|
||||
$oP->output();
|
||||
|
||||
if (MetaModel::IsLogEnabledIssue())
|
||||
@@ -1705,4 +1706,4 @@ catch(Exception $e)
|
||||
|
||||
IssueLog::Error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,55 +139,58 @@ JS
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||
$oFavoriteOrganizationsForm = new Form();
|
||||
$oFavoriteOrganizationsBlock->AddSubBlock($oFavoriteOrganizationsForm);
|
||||
// Favorite organizations: the organizations listed in the drop-down menu
|
||||
$sOQL = ApplicationMenu::GetFavoriteSiloQuery();
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
$bIsSiloSelectionEnabled = MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
|
||||
if ($bIsSiloSelectionEnabled)
|
||||
{
|
||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||
$oFavoriteOrganizationsForm = new Form();
|
||||
$oFavoriteOrganizationsBlock->AddSubBlock($oFavoriteOrganizationsForm);
|
||||
// Favorite organizations: the organizations listed in the drop-down menu
|
||||
$sOQL = ApplicationMenu::GetFavoriteSiloQuery();
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
|
||||
$aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null);
|
||||
$aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null);
|
||||
|
||||
$sIdFavoriteOrganizations = 1;
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oBlock->GetDisplay($oP, $sIdFavoriteOrganizations, [
|
||||
'menu' => false,
|
||||
'selection_mode' => true,
|
||||
'selection_type' => 'multiple',
|
||||
'table_id' => 'user_prefs',
|
||||
'surround_with_panel' => false,
|
||||
'selected_rows' => $aFavoriteOrgs,
|
||||
]));
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
$sIdFavoriteOrganizations = 1;
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oBlock->GetDisplay($oP, $sIdFavoriteOrganizations, [
|
||||
'menu' => false,
|
||||
'selection_mode' => true,
|
||||
'selection_type' => 'multiple',
|
||||
'table_id' => 'user_prefs',
|
||||
'surround_with_panel' => false,
|
||||
'selected_rows' => $aFavoriteOrgs,
|
||||
]));
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
|
||||
// Button toolbar
|
||||
$oFavoriteOrganizationsToolBar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oFavoriteOrganizationsToolBar);
|
||||
// Button toolbar
|
||||
$oFavoriteOrganizationsToolBar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oFavoriteOrganizationsToolBar);
|
||||
|
||||
// - Cancel button
|
||||
$oFavoriteOrganizationsCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'));
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsCancelButton);
|
||||
$oFavoriteOrganizationsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oFavoriteOrganizationsSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply', true);
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsSubmitButton);
|
||||
// - Cancel button
|
||||
$oFavoriteOrganizationsCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'));
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsCancelButton);
|
||||
$oFavoriteOrganizationsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oFavoriteOrganizationsSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply', true);
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsSubmitButton);
|
||||
|
||||
// TODO 3.0 have this code work again, currently it prevents the display of favorite organizations and shortcuts.
|
||||
// if ($aFavoriteOrgs == null) {
|
||||
// // All checked
|
||||
// $oP->add_ready_script(
|
||||
// <<<JS
|
||||
// $('#$sIdFavoriteOrganizations.checkAll').prop('checked', true);
|
||||
// checkAllDataTable('datatable_$sIdFavoriteOrganizations',true,'$sIdFavoriteOrganizations');
|
||||
//JS
|
||||
// );
|
||||
//
|
||||
// }
|
||||
|
||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||
// TODO 3.0 have this code work again, currently it prevents the display of favorite organizations and shortcuts.
|
||||
// if ($aFavoriteOrgs == null) {
|
||||
// // All checked
|
||||
// $oP->add_ready_script(
|
||||
// <<<JS
|
||||
// $('#$sIdFavoriteOrganizations.checkAll').prop('checked', true);
|
||||
// checkAllDataTable('datatable_$sIdFavoriteOrganizations',true,'$sIdFavoriteOrganizations');
|
||||
//JS
|
||||
// );
|
||||
//
|
||||
// }
|
||||
|
||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Shortcuts
|
||||
|
||||
@@ -23,15 +23,15 @@ use Combodo\iTop\DesignElement;
|
||||
|
||||
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||
require_once(APPROOT.'setup/parentmenunodecompiler.class.inc.php');
|
||||
require_once(APPROOT.'core/moduledesign.class.inc.php');
|
||||
|
||||
|
||||
class DOMFormatException extends Exception
|
||||
{
|
||||
/**
|
||||
* Overrides the Exception default constructor to automatically add informations about the concerned node (path and
|
||||
* line number)
|
||||
*
|
||||
*
|
||||
* @param string $message
|
||||
* @param $code
|
||||
* @param $previous
|
||||
@@ -49,7 +49,7 @@ class DOMFormatException extends Exception
|
||||
|
||||
/**
|
||||
* Compiler class
|
||||
*/
|
||||
*/
|
||||
class MFCompiler
|
||||
{
|
||||
const DATA_PRECOMPILED_FOLDER = 'data'.DIRECTORY_SEPARATOR.'precompiled_styles'.DIRECTORY_SEPARATOR;
|
||||
@@ -315,7 +315,7 @@ class MFCompiler
|
||||
apc_clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" of all modules
|
||||
@@ -327,21 +327,16 @@ class MFCompiler
|
||||
*/
|
||||
protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
|
||||
{
|
||||
$aAllClasses = array(); // flat list of classes
|
||||
$aModulesInfo = array(); // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
$aAllClasses = []; // flat list of classes
|
||||
$aModulesInfo = []; // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
|
||||
// Determine the target modules for the MENUS
|
||||
//
|
||||
$aMenuNodes = array();
|
||||
$aMenusByModule = array();
|
||||
foreach ($this->oFactory->GetNodes('menus/menu') as $oMenuNode)
|
||||
{
|
||||
$sMenuId = $oMenuNode->getAttribute('id');
|
||||
$aMenuNodes[$sMenuId] = $oMenuNode;
|
||||
|
||||
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
||||
$aMenusByModule[$sModuleMenu][] = $sMenuId;
|
||||
}
|
||||
/**
|
||||
* @since 3.1 N°4762
|
||||
*/
|
||||
$oParentMenuNodeCompiler = new ParentMenuNodeCompiler($this);
|
||||
$oParentMenuNodeCompiler->LoadXmlMenus($this->oFactory);
|
||||
|
||||
// Determine the target module (exactly one!) for USER RIGHTS
|
||||
// This used to be based solely on the module which created the user_rights node first
|
||||
@@ -386,6 +381,7 @@ class MFCompiler
|
||||
|
||||
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
||||
|
||||
$oParentMenuNodeCompiler->LoadModuleMenuInfo($aModules);
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleName = $oModule->GetName();
|
||||
$sModuleVersion = $oModule->GetVersion();
|
||||
@@ -418,7 +414,7 @@ class MFCompiler
|
||||
$sCompiledCode .= $this->CompileConstant($oConstant)."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet)
|
||||
@@ -461,7 +457,7 @@ class MFCompiler
|
||||
}
|
||||
}
|
||||
|
||||
if (!array_key_exists($sModuleName, $aMenusByModule))
|
||||
if (is_null($oParentMenuNodeCompiler->GetMenusByModule($sModuleName)))
|
||||
{
|
||||
$this->Log("Found module without menus declared: $sModuleName");
|
||||
}
|
||||
@@ -481,75 +477,19 @@ class $sMenuCreationClass extends ModuleHandlerAPI
|
||||
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
||||
|
||||
EOF;
|
||||
// Preliminary: determine parent menus not defined within the current module
|
||||
$aMenusToLoad = array();
|
||||
$aParentMenus = array();
|
||||
foreach($aMenusByModule[$sModuleName] as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
||||
if ($sParent = $oMenuNode->GetChildText('parent', null))
|
||||
{
|
||||
$aMenusToLoad[] = $sParent;
|
||||
$aParentMenus[] = $sParent;
|
||||
}
|
||||
// Note: the order matters: the parents must be defined BEFORE
|
||||
$aMenusToLoad[] = $sMenuId;
|
||||
}
|
||||
$aMenusToLoad = array_unique($aMenusToLoad);
|
||||
$aMenuLinesForAll = array();
|
||||
$aMenuLinesForAdmins = array();
|
||||
$aAdminMenus = array();
|
||||
foreach($aMenusToLoad as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
||||
if (is_null($oMenuNode))
|
||||
{
|
||||
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
||||
}
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup')
|
||||
{
|
||||
// Note: this algorithm is wrong
|
||||
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
||||
// 2 - check recursively that child nodes are not empty themselves
|
||||
// Future algorithm:
|
||||
// a- browse the modules and build the menu tree
|
||||
// b- browse the tree and blacklist empty menus
|
||||
// c- before compiling, discard if blacklisted
|
||||
if (!in_array($oMenuNode->getAttribute("id"), $aParentMenus))
|
||||
{
|
||||
// Discard empty menu groups
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
$aMenuLines = $this->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
catch (DOMFormatException $e)
|
||||
{
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]))
|
||||
{
|
||||
$aMenuLinesForAdmins = array_merge($aMenuLinesForAdmins, $aMenuLines);
|
||||
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$aMenuLinesForAll = array_merge($aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
}
|
||||
|
||||
$oParentMenuNodeCompiler->CompileModuleMenus($oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
|
||||
$sIndent = "\t\t";
|
||||
foreach ($aMenuLinesForAll as $sPHPLine)
|
||||
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAll() as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
||||
}
|
||||
if (count($aMenuLinesForAdmins) > 0)
|
||||
if (count($oParentMenuNodeCompiler->GetMenuLinesForAdmins()) > 0)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
||||
$sCompiledCode .= $sIndent."{\n";
|
||||
foreach ($aMenuLinesForAdmins as $sPHPLine)
|
||||
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAdmins() as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
||||
}
|
||||
@@ -581,7 +521,7 @@ EOF;
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create (overwrite if existing) the compiled file
|
||||
//
|
||||
if (strlen($sCompiledCode) > 0)
|
||||
@@ -625,7 +565,7 @@ EOF;
|
||||
$aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
|
||||
}
|
||||
} // foreach module
|
||||
|
||||
|
||||
// Compile the dictionaries -out of the modules
|
||||
//
|
||||
$sDictDir = $sTempTargetDir.'/dictionaries';
|
||||
@@ -654,7 +594,7 @@ EOF;
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compile the portals
|
||||
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
|
||||
$this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
@@ -666,7 +606,7 @@ EOF;
|
||||
// Compile the XML parameters
|
||||
$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
|
||||
$this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
|
||||
if (array_key_exists('_core_', $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets['_core_']['after'] as $aSnippet)
|
||||
@@ -700,7 +640,7 @@ EOF;
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
// Autoload
|
||||
$sPHPFile = $sTempTargetDir.'/autoload.php';
|
||||
$sPHPFileContent =
|
||||
$sPHPFileContent =
|
||||
<<<EOF
|
||||
<?php
|
||||
//
|
||||
@@ -709,7 +649,7 @@ EOF;
|
||||
//
|
||||
EOF
|
||||
;
|
||||
|
||||
|
||||
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
|
||||
$sPHPFileContent .= implode("\n", $aDataModelFiles);
|
||||
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
|
||||
@@ -717,14 +657,14 @@ EOF
|
||||
$sModulesInfo = str_replace("'".$sRelFinalTargetDir."/", "\$sCurrEnv.'/", $sModulesInfo);
|
||||
$sPHPFileContent .= "\nfunction GetModulesInfo()\n{\n\$sCurrEnv = 'env-'.utils::GetCurrentEnvironment();\nreturn ".$sModulesInfo.";\n}\n";
|
||||
file_put_contents($sPHPFile, $sPHPFileContent);
|
||||
|
||||
|
||||
} // DoCompile()
|
||||
|
||||
/**
|
||||
* Helper to form a valid ZList from the array built by GetNodeAsArrayOfItems()
|
||||
*
|
||||
* @param array $aItems
|
||||
*/
|
||||
*/
|
||||
protected function ArrayOfItemsToZList(&$aItems)
|
||||
{
|
||||
// Note: $aItems can be null in some cases so we have to protect it otherwise a PHP warning will be thrown during the foreach
|
||||
@@ -756,7 +696,7 @@ EOF
|
||||
* Helper to format the flags for an attribute, in a given state
|
||||
* @param object $oAttNode DOM node containing the information to build the flags
|
||||
* Returns string PHP flags, based on the OPT_ATT_ constants, or empty (meaning 0, can be omitted)
|
||||
*/
|
||||
*/
|
||||
protected function FlagsToPHP($oAttNode)
|
||||
{
|
||||
static $aNodeAttributeToFlag = array(
|
||||
@@ -766,7 +706,7 @@ EOF
|
||||
'must_change' => 'OPT_ATT_MUSTCHANGE',
|
||||
'hidden' => 'OPT_ATT_HIDDEN',
|
||||
);
|
||||
|
||||
|
||||
$aFlags = array();
|
||||
foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag)
|
||||
{
|
||||
@@ -778,7 +718,7 @@ EOF
|
||||
}
|
||||
if (empty($aFlags))
|
||||
{
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
}
|
||||
$sRes = implode(' | ', $aFlags);
|
||||
return $sRes;
|
||||
@@ -800,7 +740,7 @@ EOF
|
||||
'details' => 'LINKSET_TRACKING_DETAILS',
|
||||
'all' => 'LINKSET_TRACKING_ALL',
|
||||
);
|
||||
|
||||
|
||||
static $aXmlToPHP_Others = array(
|
||||
'none' => 'ATTRIBUTE_TRACKING_NONE',
|
||||
'all' => 'ATTRIBUTE_TRACKING_ALL',
|
||||
@@ -841,7 +781,7 @@ EOF
|
||||
'in_place' => 'LINKSET_EDITMODE_INPLACE',
|
||||
'add_remove' => 'LINKSET_EDITMODE_ADDREMOVE',
|
||||
);
|
||||
|
||||
|
||||
if (!array_key_exists($sEditMode, $aXmlToPHP))
|
||||
{
|
||||
throw new DOMFormatException("Edit mode: unknown value '$sEditMode'");
|
||||
@@ -849,10 +789,10 @@ EOF
|
||||
return $aXmlToPHP[$sEditMode];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Format a path (file or url) as an absolute path or relative to the module or the app
|
||||
*/
|
||||
*/
|
||||
protected function PathToPHP($sPath, $sModuleRelativeDir, $bIsUrl = false)
|
||||
{
|
||||
if ($sPath == '')
|
||||
@@ -945,7 +885,7 @@ EOF
|
||||
else
|
||||
{
|
||||
throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1049,7 +989,7 @@ EOF
|
||||
|
||||
/**
|
||||
* Adds quotes and escape characters
|
||||
*/
|
||||
*/
|
||||
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
|
||||
{
|
||||
if ($bSimpleQuotes)
|
||||
@@ -1084,7 +1024,7 @@ EOF
|
||||
$sScalar = (string)(int)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'float':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1096,7 +1036,7 @@ EOF
|
||||
$sScalar = (string)(float)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'bool':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1328,7 +1268,7 @@ EOF
|
||||
// $oField
|
||||
$sAttCode = $oField->getAttribute('id');
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
$aDependencies = array();
|
||||
$oDependencies = $oField->GetOptionalElement('dependencies');
|
||||
if (!is_null($oDependencies))
|
||||
@@ -1340,9 +1280,9 @@ EOF
|
||||
}
|
||||
}
|
||||
$sDependencies = 'array('.implode(', ', $aDependencies).')';
|
||||
|
||||
|
||||
$aParameters = array();
|
||||
|
||||
|
||||
if ($sAttType == 'AttributeLinkedSetIndirect')
|
||||
{
|
||||
$aParameters['linked_class'] = $this->GetMandatoryPropString($oField, 'linked_class');
|
||||
@@ -2416,7 +2356,7 @@ CSS;
|
||||
* @return array
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
protected function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||
public function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||
{
|
||||
$this->CompileFiles($oMenu, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
|
||||
|
||||
@@ -2512,11 +2452,11 @@ CSS;
|
||||
case '1':
|
||||
$sSearchFormOpen = 'true';
|
||||
break;
|
||||
|
||||
|
||||
case '0':
|
||||
$sSearchFormOpen = 'false';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$sSearchFormOpen = 'true';
|
||||
}
|
||||
@@ -2605,7 +2545,7 @@ CSS;
|
||||
foreach($this->oFactory->ListFields($oClass) as $oField)
|
||||
{
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
if (($sAttType == 'AttributeExternalKey') || ($sAttType == 'AttributeHierarchicalKey'))
|
||||
{
|
||||
$sOnTargetDel = $oField->GetChildText('on_target_delete');
|
||||
@@ -2631,7 +2571,7 @@ CSS;
|
||||
$oClasses = $oGroup->GetUniqueElement('classes');
|
||||
foreach($oClasses->getElementsByTagName('class') as $oClass)
|
||||
{
|
||||
|
||||
|
||||
$sClass = $oClass->getAttribute("id");
|
||||
$aClasses[] = $sClass;
|
||||
|
||||
@@ -2649,7 +2589,7 @@ CSS;
|
||||
$aProfiles[1] = array(
|
||||
'name' => 'Administrator',
|
||||
'description' => 'Has the rights on everything (bypassing any control)',
|
||||
);
|
||||
);
|
||||
|
||||
$aGrants = array();
|
||||
$oProfiles = $oUserRightsNode->GetUniqueElement('profiles');
|
||||
@@ -2681,7 +2621,7 @@ CSS;
|
||||
}
|
||||
$sGrant = $oAction->GetText();
|
||||
$bGrant = ($sGrant == 'allow');
|
||||
|
||||
|
||||
if ($sGroupId == '*')
|
||||
{
|
||||
$aGrantClasses = array('*');
|
||||
@@ -2920,7 +2860,7 @@ Dict::SetLanguagesList(
|
||||
$sLanguagesDump
|
||||
);
|
||||
EOF;
|
||||
|
||||
|
||||
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
|
||||
}
|
||||
|
||||
@@ -2957,7 +2897,7 @@ EOF;
|
||||
{
|
||||
throw new DOMFormatException('Could not find the file with ref '.$sFileId);
|
||||
}
|
||||
|
||||
|
||||
$sName = $oNodes->item(0)->GetChildText('name');
|
||||
$sData = base64_decode($oNodes->item(0)->GetChildText('data'));
|
||||
$aPathInfo = pathinfo($sName);
|
||||
@@ -2971,7 +2911,7 @@ EOF;
|
||||
}
|
||||
$oParentNode = $oFileRef->parentNode;
|
||||
$oParentNode->removeChild($oFileRef);
|
||||
|
||||
|
||||
$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
|
||||
$oParentNode->appendChild($oTextNode);
|
||||
}
|
||||
@@ -3284,8 +3224,8 @@ EOF;
|
||||
{
|
||||
SetupUtils::rrmdir($sTempTargetDir.'/branding/images');
|
||||
}
|
||||
|
||||
// Compile themes
|
||||
|
||||
// Compile themes
|
||||
$this->CompileThemes($oBrandingNode, $sTempTargetDir);
|
||||
}
|
||||
}
|
||||
@@ -3326,11 +3266,11 @@ EOF;
|
||||
{
|
||||
$aPortalsConfig[$sPortalId]['deny'][] = $oProfile->getAttribute('id');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uasort($aPortalsConfig, array(get_class($this), 'SortOnRank'));
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Portal(s) definition(s) extracted from the XML definition at compile time\n";
|
||||
@@ -3373,7 +3313,7 @@ EOF;
|
||||
$oParamsReader = new MFParameters($oParams);
|
||||
$aParametersConfig[$sModuleId] = $oParamsReader->GetAll();
|
||||
}
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Modules parameters extracted from the XML definition at compile time\n";
|
||||
|
||||
287
setup/parentmenunodecompiler.class.inc.php
Normal file
287
setup/parentmenunodecompiler.class.inc.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @since 3.1 N°4762
|
||||
*/
|
||||
class ParentMenuNodeCompiler
|
||||
{
|
||||
const COMPILED = 1;
|
||||
const COMPILING = 2;
|
||||
|
||||
public static $bUseLegacyMenuCompilation = false;
|
||||
|
||||
/**
|
||||
* @var MFCompiler
|
||||
*/
|
||||
private $oMFCompiler;
|
||||
|
||||
/**
|
||||
* admin menus declaration lines: result of module menu compilation
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuLinesForAdmins = [];
|
||||
|
||||
/**
|
||||
* non-admin menus declaration lines: result of module menu compilation
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuLinesForAll = [];
|
||||
|
||||
/**
|
||||
* use to handle menu group compilation recurring algorithm
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuProcessStatus = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenuNodes = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenusByModule = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aMenusToLoadByModule = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $aParentMenusByModule = [];
|
||||
|
||||
/**
|
||||
* used by overall algo
|
||||
* @var array
|
||||
*/
|
||||
private $aParentMenuNodes = [];
|
||||
|
||||
/**
|
||||
* used by new algo
|
||||
* @var array
|
||||
*/
|
||||
private $aParentAdminMenus = [];
|
||||
|
||||
/**
|
||||
* used by overall algo
|
||||
* @var array
|
||||
*/
|
||||
private $aParentModuleRootDirs = [];
|
||||
|
||||
public function __construct(MFCompiler $oMFCompiler) {
|
||||
$this->oMFCompiler = $oMFCompiler;
|
||||
}
|
||||
|
||||
public static function UseLegacyMenuCompilation(){
|
||||
self::$bUseLegacyMenuCompilation = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ModelFactory $oFactory
|
||||
* Initialize menu nodes arrays
|
||||
* @return void
|
||||
*/
|
||||
public function LoadXmlMenus(\ModelFactory $oFactory) : void {
|
||||
foreach ($oFactory->GetNodes('menus/menu') as $oMenuNode) {
|
||||
$sMenuId = $oMenuNode->getAttribute('id');
|
||||
$this->aMenuNodes[$sMenuId] = $oMenuNode;
|
||||
|
||||
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
||||
$this->aMenusByModule[$sModuleMenu][] = $sMenuId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aModules
|
||||
* Initialize arrays related to parent/child menus
|
||||
* @return void
|
||||
*/
|
||||
public function LoadModuleMenuInfo($aModules) : void
|
||||
{
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleRootDir = $oModule->GetRootDir();
|
||||
$sModuleName = $oModule->GetName();
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aMenusByModule)) {
|
||||
$aMenusToLoad = [];
|
||||
$aParentMenus = [];
|
||||
|
||||
foreach ($this->aMenusByModule[$sModuleName] as $sMenuId) {
|
||||
$oMenuNode = $this->aMenuNodes[$sMenuId];
|
||||
|
||||
if (self::$bUseLegacyMenuCompilation){
|
||||
if ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||
$aMenusToLoad[] = $sParent;
|
||||
$aParentMenus[] = $sParent;
|
||||
}
|
||||
} else {
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') {
|
||||
$this->aParentModuleRootDirs[$sMenuId] = $sModuleRootDir;
|
||||
}
|
||||
|
||||
if ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||
$aMenusToLoad[] = $sParent;
|
||||
$aParentMenus[] = $sParent;
|
||||
|
||||
$this->aParentModuleRootDirs[$sParent] = $sModuleRootDir;
|
||||
}
|
||||
|
||||
if (array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||
$this->aParentMenuNodes[$sMenuId] = $oMenuNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: the order matters: the parents must be defined BEFORE
|
||||
$aMenusToLoad[] = $sMenuId;
|
||||
}
|
||||
|
||||
$this->aMenusToLoadByModule[$sModuleName] = array_unique($aMenusToLoad);
|
||||
$this->aParentMenusByModule[$sModuleName] = array_unique($aParentMenus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" for one module at a time
|
||||
* @param \MFModule $oModule
|
||||
* @param string $sTempTargetDir
|
||||
* @param string $sFinalTargetDir
|
||||
* @param string $sRelativeDir
|
||||
* @param Page $oP
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function CompileModuleMenus(MFModule $oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP = null) : void
|
||||
{
|
||||
$this->aMenuLinesForAdmins = [];
|
||||
$this->aMenuLinesForAll = [];
|
||||
$aAdminMenus = [];
|
||||
|
||||
$sModuleRootDir = $oModule->GetRootDir();
|
||||
$sModuleName = $oModule->GetName();
|
||||
|
||||
$aParentMenus = $this->aParentMenusByModule[$sModuleName];
|
||||
foreach($this->aMenusToLoadByModule[$sModuleName] as $sMenuId)
|
||||
{
|
||||
$oMenuNode = $this->aMenuNodes[$sMenuId];
|
||||
if (is_null($oMenuNode))
|
||||
{
|
||||
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
||||
}
|
||||
|
||||
if (self::$bUseLegacyMenuCompilation) {
|
||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') {
|
||||
// Note: this algorithm is wrong
|
||||
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
||||
// 2 - check recursively that child nodes are not empty themselves
|
||||
// Future algorithm:
|
||||
// a- browse the modules and build the menu tree
|
||||
// b- browse the tree and blacklist empty menus
|
||||
// c- before compiling, discard if blacklisted
|
||||
if (! in_array($oMenuNode->getAttribute("id"), $aParentMenus)) {
|
||||
// Discard empty menu groups
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (array_key_exists($sMenuId, $this->aParentMenuNodes)) {
|
||||
// compile parent menus recursively
|
||||
$this->CompileParentMenuNode($sMenuId, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//both new/legacy algo: compile leaf menu
|
||||
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
catch (DOMFormatException $e)
|
||||
{
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]) || isset($this->aParentAdminMenus[$sParent]))
|
||||
{
|
||||
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform parent menu compilation including its ancestrors (recursively)
|
||||
* @param string $sMenuId
|
||||
* @param string $sTempTargetDir
|
||||
* @param string $sFinalTargetDir
|
||||
* @param string $sRelativeDir
|
||||
* @param Page $oP
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function CompileParentMenuNode(string $sMenuId, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP = null) : void
|
||||
{
|
||||
$oMenuNode = $this->aParentMenuNodes[$sMenuId];
|
||||
$sStatus = array_key_exists($sMenuId, $this->aMenuProcessStatus) ? $this->aMenuProcessStatus[$sMenuId] : null;
|
||||
if ($sStatus === self::COMPILED){
|
||||
//node already processed before
|
||||
return;
|
||||
} else if ($sStatus === self::COMPILING){
|
||||
throw new \Exception("Cyclic dependency between parent menus ($sMenuId)");
|
||||
}
|
||||
|
||||
$this->aMenuProcessStatus[$sMenuId] = self::COMPILING;
|
||||
|
||||
try {
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (! empty($sParent)){
|
||||
//compile parents before (even parent of parents ... recursively)
|
||||
$this->CompileParentMenuNode($sParent, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
}
|
||||
|
||||
if (! array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||
throw new Exception("Failed to process parent menu '$sMenuId' that is referenced by a child but not defined");
|
||||
}
|
||||
$sModuleRootDir = $this->aParentModuleRootDirs[$sMenuId];
|
||||
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
} catch (DOMFormatException $e) {
|
||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||
}
|
||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($this->aParentAdminMenus[$sParent])) {
|
||||
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||
$this->aParentAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||
} else {
|
||||
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||
}
|
||||
|
||||
$this->aMenuProcessStatus[$sMenuId] = self::COMPILED;
|
||||
}
|
||||
|
||||
public function GetMenusByModule(string $sModuleName) : ?array
|
||||
{
|
||||
if (array_key_exists($sModuleName, $this->aMenusByModule)) {
|
||||
return $this->aMenusByModule[$sModuleName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function GetMenuLinesForAdmins(): array {
|
||||
return $this->aMenuLinesForAdmins;
|
||||
}
|
||||
|
||||
public function GetMenuLinesForAll(): array {
|
||||
return $this->aMenuLinesForAll;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\Field;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
@@ -45,6 +46,11 @@ class Field extends UIContentBlock
|
||||
protected $sValueRaw;
|
||||
/** @var string */
|
||||
protected $sLabel;
|
||||
/**
|
||||
* @var string
|
||||
* @since 3.1.0
|
||||
*/
|
||||
protected $sDescription = '';
|
||||
/** @var string */
|
||||
protected $sValueId;
|
||||
|
||||
@@ -354,4 +360,34 @@ class Field extends UIContentBlock
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public function GetDescription(): string
|
||||
{
|
||||
return $this->sDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @return $this
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public function SetDescription(string $sDescription)
|
||||
{
|
||||
$this->sDescription = $sDescription;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return bool
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public function HasDescription(): bool
|
||||
{
|
||||
return utils::IsNotNullOrEmptyString($this->GetDescription());
|
||||
}
|
||||
}
|
||||
@@ -86,23 +86,51 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
|
||||
return $oField;
|
||||
}
|
||||
|
||||
public static function MakeLarge(string $sLabel, string $sValueHtml = '')
|
||||
/**
|
||||
* @api
|
||||
* @param string $sLabel
|
||||
* @param string $sValueHtml
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
||||
*/
|
||||
public static function MakeLarge(string $sLabel, string $sValueHtml = '', string $sDescription = '')
|
||||
{
|
||||
$oField = new Field($sLabel, new Html($sValueHtml));
|
||||
$oField->SetDescription($sDescription);
|
||||
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_LARGE);
|
||||
return $oField;
|
||||
}
|
||||
|
||||
public static function MakeSmall(string $sLabel, string $sValueHtml = '')
|
||||
/**
|
||||
* @api
|
||||
* @param string $sLabel
|
||||
* @param string $sValueHtml
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
||||
*/
|
||||
public static function MakeSmall(string $sLabel, string $sValueHtml = '', string $sDescription = '')
|
||||
{
|
||||
$oField = new Field($sLabel, new Html($sValueHtml));
|
||||
$oField->SetDescription($sDescription);
|
||||
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_SMALL);
|
||||
return $oField;
|
||||
}
|
||||
|
||||
public static function MakeStandard(string $sLabel = '', string $sLayout = Field::ENUM_FIELD_LAYOUT_SMALL, ?string $sId = null)
|
||||
/**
|
||||
* @api
|
||||
* @param string $sLabel
|
||||
* @param string $sLayout
|
||||
* @param string|null $sId
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
||||
*/
|
||||
public static function MakeStandard(string $sLabel = '', string $sLayout = Field::ENUM_FIELD_LAYOUT_SMALL, ?string $sId = null, string $sDescription = '')
|
||||
{
|
||||
$oField = new Field($sLabel, null, $sId);
|
||||
$oField->SetDescription($sDescription);
|
||||
$oField->SetLayout($sLayout);
|
||||
return $oField;
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@ class NewsroomMenuFactory
|
||||
return $oMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is any Newsroom provider configured
|
||||
* @return boolean
|
||||
*/
|
||||
public static function HasProviders()
|
||||
{
|
||||
$aProviders = MetaModel::EnumPlugins('iNewsroomProvider');
|
||||
return count($aProviders) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare parameters for the newsroom JS widget
|
||||
*
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base\Layout\NavigationMenu;
|
||||
|
||||
|
||||
use ApplicationContext;
|
||||
use ApplicationMenu;
|
||||
use appUserPreferences;
|
||||
@@ -35,6 +34,7 @@ use MetaModel;
|
||||
use UIExtKeyWidget;
|
||||
use UserRights;
|
||||
use utils;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\NewsroomMenu\NewsroomMenuFactory;
|
||||
|
||||
/**
|
||||
* Class NavigationMenu
|
||||
@@ -196,7 +196,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -269,7 +269,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
*/
|
||||
public function IsNewsroomEnabled(): bool
|
||||
{
|
||||
return MetaModel::GetConfig()->Get('newsroom_enabled');
|
||||
return (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,6 +290,14 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the silo selection is enabled, false otherwise
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public function IsSiloSelectionEnabled() : bool {
|
||||
return MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \CoreException
|
||||
@@ -301,6 +309,10 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
$this->bHasSiloSelected = false;
|
||||
$this->sSiloLabel = null;
|
||||
|
||||
if (! $this->IsSiloSelectionEnabled()){
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO 3.0 Use components if we have the time to build select/autocomplete components before release
|
||||
// List of visible Organizations
|
||||
$iCount = 0;
|
||||
@@ -343,7 +355,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
$this->aSiloSelection['html'] = '<form data-role="ibo-navigation-menu--silo-selection--form" action="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php">'; //<select class="org_combo" name="c[org_id]" title="Pick an organization" onChange="this.form.submit();">';
|
||||
|
||||
$oPage = new \CaptureWebPage();
|
||||
|
||||
|
||||
$oWidget = new UIExtKeyWidget('Organization', 'org_id', '', true /* search mode */);
|
||||
$iMaxComboLength = MetaModel::GetConfig()->Get('max_combo_length');
|
||||
$this->aSiloSelection['html'] .= $oWidget->DisplaySelect($oPage, $iMaxComboLength, false, Dict::S('UI:Layout:NavigationMenu:Silo:Label'), $oSet, $iCurrentOrganization, false, 'c[org_id]', '',
|
||||
@@ -376,7 +388,7 @@ $sAddClearButton
|
||||
JS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute if the menu is expanded or collapsed
|
||||
*
|
||||
@@ -479,4 +491,4 @@ JS;
|
||||
{
|
||||
return "[data-role='".static::BLOCK_CODE."']";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class NavigationMenuFactory
|
||||
{
|
||||
|
||||
$oNewsroomMenu = null;
|
||||
if (MetaModel::GetConfig()->Get('newsroom_enabled'))
|
||||
if (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders())
|
||||
{
|
||||
$oNewsroomMenu = NewsroomMenuFactory::MakeNewsroomMenuForNavigationMenu();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ function UsageAndExit($oP)
|
||||
}
|
||||
else
|
||||
{
|
||||
$oP->p("The parameter 'data_sources' is mandatory, and must contain a comma separated list of data sources\n");
|
||||
$oP->p("The parameter 'data_sources' is mandatory, and must contain a comma separated list of data sources\n");
|
||||
}
|
||||
$oP->output();
|
||||
exit -2;
|
||||
@@ -105,6 +105,9 @@ if (utils::IsModeCLI())
|
||||
else
|
||||
{
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
|
||||
//N°6022 - Make synchro scripts work by http via token authentication with SYNCHRO scopes
|
||||
$oCtx = new ContextTag(ContextTag::TAG_SYNCHRO);
|
||||
LoginWebPage::DoLogin(); // Check user rights and prompt if needed
|
||||
}
|
||||
|
||||
@@ -166,7 +169,7 @@ foreach(explode(',', $sDataSourcesList) as $iSDS)
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$oP->add($e->getMessage());
|
||||
$oP->add($e->getMessage());
|
||||
if ($bSimulate)
|
||||
{
|
||||
CMDBSource::Query('ROLLBACK');
|
||||
|
||||
@@ -255,7 +255,7 @@ if (utils::IsModeCLI())
|
||||
{
|
||||
// Next steps:
|
||||
// specific arguments: 'csvfile'
|
||||
//
|
||||
//
|
||||
$sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data');
|
||||
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data');
|
||||
$sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data');
|
||||
@@ -282,6 +282,9 @@ if (utils::IsModeCLI())
|
||||
else
|
||||
{
|
||||
require_once APPROOT.'/application/loginwebpage.class.inc.php';
|
||||
|
||||
//N°6022 - Make synchro scripts work by http via token authentication with SYNCHRO scopes
|
||||
$oCtx = new ContextTag(ContextTag::TAG_SYNCHRO);
|
||||
LoginWebPage::DoLogin(); // Check user rights and prompt if needed
|
||||
|
||||
$sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data');
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
{% endif %}
|
||||
>
|
||||
<div class="ibo-field--label">{{ oUIBlock.GetLabel()|raw }}
|
||||
{% if oUIBlock.HasDescription() %}
|
||||
<span class="ibo-has-description" data-tooltip-content="{{ oUIBlock.GetDescription() }}" data-tooltip-max-width="600px" ></span>
|
||||
{% endif %}
|
||||
{% if oUIBlock.GetLayout() == constant("Combodo\\iTop\\Application\\UI\\Base\\Component\\Field\\Field::ENUM_FIELD_LAYOUT_LARGE") %}
|
||||
{% if oUIBlock.GetComments() %}
|
||||
<div class="ibo-field--comments">{{ oUIBlock.GetComments()|raw }}</div>
|
||||
|
||||
@@ -27,8 +27,8 @@ namespace Combodo\iTop\Test\UnitTest;
|
||||
*/
|
||||
|
||||
use ArchivedObjectException;
|
||||
use CMDBSource;
|
||||
use CMDBObject;
|
||||
use CMDBSource;
|
||||
use Contact;
|
||||
use DBObject;
|
||||
use DBObjectSet;
|
||||
@@ -70,6 +70,7 @@ define('TAG_ATTCODE', 'domains');
|
||||
class ItopDataTestCase extends ItopTestCase
|
||||
{
|
||||
private $iTestOrgId;
|
||||
|
||||
// For cleanup
|
||||
private $aCreatedObjects = array();
|
||||
|
||||
@@ -409,10 +410,25 @@ class ItopDataTestCase extends ItopTestCase
|
||||
* @param string $sLogin
|
||||
* @param int $iProfileId
|
||||
*
|
||||
* @return \DBObject
|
||||
* @return \UserLocal
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function CreateUser($sLogin, $iProfileId, $sPassword=null, $iContactid=2)
|
||||
{
|
||||
$oUser = $this->CreateContactlessUser($sLogin, $iProfileId, $sPassword);
|
||||
$oUser->Set('contactid', $iContactid);
|
||||
$oUser->DBWrite();
|
||||
return $oUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sLogin
|
||||
* @param int $iProfileId
|
||||
*
|
||||
* @return \UserLocal
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function CreateContactlessUser($sLogin, $iProfileId, $sPassword=null)
|
||||
{
|
||||
if (empty($sPassword)){
|
||||
$sPassword = $sLogin;
|
||||
@@ -422,8 +438,8 @@ class ItopDataTestCase extends ItopTestCase
|
||||
$oUserProfile->Set('profileid', $iProfileId);
|
||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||
$oSet = DBObjectSet::FromObject($oUserProfile);
|
||||
/** @var \UserLocal $oUser */
|
||||
$oUser = $this->createObject('UserLocal', array(
|
||||
'contactid' => $iContactid,
|
||||
'login' => $sLogin,
|
||||
'password' => $sPassword,
|
||||
'language' => 'EN US',
|
||||
@@ -448,8 +464,8 @@ class ItopDataTestCase extends ItopTestCase
|
||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||
/** @var DBObjectSet $oSet */
|
||||
$oSet = $oUser->Get('profile_list');
|
||||
$oSet->AddObject($oUserProfile);
|
||||
$oUser = $this->updateObject('UserLocal', $oUser->GetKey(), array(
|
||||
$oSet->AddItem($oUserProfile);
|
||||
$oUser = $this->updateObject(\User::class, $oUser->GetKey(), array(
|
||||
'profile_list' => $oSet,
|
||||
));
|
||||
$this->debug("Updated {$oUser->GetName()} ({$oUser->GetKey()})");
|
||||
@@ -835,7 +851,7 @@ class ItopDataTestCase extends ItopTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a consistent set of iTop objects from the specified XML text string
|
||||
* Import a consistent set of iTop objects from the specified XML text string
|
||||
* @param string $sXmlDataset
|
||||
* @param boolean $bSearch If true, a search will be performed on each object (based on its reconciliation keys)
|
||||
* before trying to import it (existing objects will be updated)
|
||||
|
||||
67
test/application/UI/Base/Layout/NavigationMenuTest.php
Normal file
67
test/application/UI/Base/Layout/NavigationMenuTest.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace UI\Base\Layout;
|
||||
|
||||
use ApplicationContext;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\NavigationMenu\NavigationMenu;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
|
||||
/**
|
||||
*
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
* Class NavigationMenuTest
|
||||
*
|
||||
* @package UI\Base\Layout
|
||||
*/
|
||||
class NavigationMenuTest extends ItopDataTestCase {
|
||||
public function IsAllowedProvider(){
|
||||
return [
|
||||
'show menu' => [ true ],
|
||||
'hide menu' => [ false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider IsAllowedProvider
|
||||
* test used to make sure backward compatibility is ensured
|
||||
*/
|
||||
public function testIsAllowed($bExpectedIsAllowed=true){
|
||||
\MetaModel::GetConfig()->Set('navigation_menu.show_organization_filter', $bExpectedIsAllowed);
|
||||
$oNavigationMenu = new NavigationMenu(
|
||||
$this->createMock(ApplicationContext::class),
|
||||
$this->createMock(PopoverMenu::class));
|
||||
|
||||
$isAllowed = $oNavigationMenu->IsSiloSelectionEnabled();
|
||||
$this->assertEquals($bExpectedIsAllowed, $isAllowed);
|
||||
}
|
||||
|
||||
public function testIsAllowed_BackwardCompatibility_NoVariableInConfFile(){
|
||||
\MetaModel::GetConfig()->Set('navigation_menu.show_organization_filter', false);
|
||||
|
||||
$sTmpFilePath = tempnam(sys_get_temp_dir(), 'test_');
|
||||
$oInitConfig = \MetaModel::GetConfig();
|
||||
$oInitConfig->WriteToFile($sTmpFilePath);
|
||||
|
||||
//remove variable for the test
|
||||
$aLines = file($sTmpFilePath);
|
||||
|
||||
$aRows = array();
|
||||
|
||||
foreach ($aLines as $key => $sLine) {
|
||||
if (!preg_match('/navigation_menu.show_organization_filter/', $sLine)) {
|
||||
$aRows[] = $sLine;
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($sTmpFilePath, implode("\n", $aRows));
|
||||
$oTempConfig = new \Config($sTmpFilePath);
|
||||
|
||||
$isAllowed = $oTempConfig->Get('navigation_menu.show_organization_filter');
|
||||
|
||||
$this->assertEquals(true, $isAllowed);
|
||||
unlink($sTmpFilePath);
|
||||
}
|
||||
}
|
||||
344
test/core/BulkChangeExtKeyTest.php
Normal file
344
test/core/BulkChangeExtKeyTest.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use MetaModel;
|
||||
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*
|
||||
* created a dedicated test for external keys imports.
|
||||
*
|
||||
* Class BulkChangeExtKeyTest
|
||||
*
|
||||
* @package Combodo\iTop\Test\UnitTest\Core
|
||||
*/
|
||||
class BulkChangeExtKeyTest extends ItopDataTestCase {
|
||||
const CREATE_TEST_ORG = true;
|
||||
|
||||
/**
|
||||
* this test may delete Person objects to cover all usecases
|
||||
* DO NOT CHANGE USE_TRANSACTION value to avoid any DB loss!
|
||||
*/
|
||||
const USE_TRANSACTION = true;
|
||||
|
||||
private $sUid;
|
||||
|
||||
protected function setUp() : void {
|
||||
parent::setUp();
|
||||
require_once(APPROOT.'core/bulkchange.class.inc.php');
|
||||
}
|
||||
|
||||
private function deleteAllRacks(){
|
||||
$oSearch = \DBSearch::FromOQL("SELECT Rack");
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
$iCount = $oSet->Count();
|
||||
if ($iCount != 0){
|
||||
while ($oRack = $oSet->Fetch()){
|
||||
$oRack->DBDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ReconciliationKeyProvider
|
||||
*/
|
||||
public function testExternalFieldIssueImportFail_NoObjectAtAll($bIsRackReconKey){
|
||||
$this->deleteAllRacks();
|
||||
|
||||
$this->performBulkChangeTest(
|
||||
'There are no \'Rack\' objects',
|
||||
"",
|
||||
null,
|
||||
$bIsRackReconKey
|
||||
);
|
||||
}
|
||||
|
||||
public function createRackObjects($aRackDict) {
|
||||
foreach ($aRackDict as $iOrgId => $aRackNames) {
|
||||
foreach ($aRackNames as $sRackName) {
|
||||
$this->createObject('Rack', ['name' => $sRackName, 'description' => "${sRackName}Desc", 'org_id' => $iOrgId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createAnotherUserInAnotherOrg() {
|
||||
$oOrg2 = $this->CreateOrganization('UnitTestOrganization2');
|
||||
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Configuration Manager'), true);
|
||||
|
||||
$sUid = $this->GetUid();
|
||||
|
||||
$oUserProfile = new \URP_UserProfile();
|
||||
$oUserProfile->Set('profileid', $oProfile->GetKey());
|
||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||
$oSet = \DBObjectSet::FromObject($oUserProfile);
|
||||
|
||||
$oPerson = $this->CreatePerson('666', $oOrg2->GetKey());
|
||||
$oUser = $this->createObject('UserLocal', array(
|
||||
'contactid' => $oPerson->GetKey(),
|
||||
'login' => $sUid,
|
||||
'password' => "ABCdef$sUid@12345",
|
||||
'language' => 'EN US',
|
||||
'profile_list' => $oSet,
|
||||
));
|
||||
|
||||
$oAllowedOrgList = $oUser->Get('allowed_org_list');
|
||||
/** @var \URP_UserOrg $oUserOrg */
|
||||
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oOrg2->GetKey(),]);
|
||||
$oAllowedOrgList->AddItem($oUserOrg);
|
||||
$oUser->Set('allowed_org_list', $oAllowedOrgList);
|
||||
$oUser->DBWrite();
|
||||
return [$oOrg2, $oUser];
|
||||
}
|
||||
|
||||
public function ReconciliationKeyProvider(){
|
||||
return [
|
||||
'rack_id NOT a reconcilication key' => [ false ],
|
||||
'rack_id reconcilication key' => [ true ],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @dataProvider ReconciliationKeyProvider
|
||||
*/
|
||||
public function testExternalFieldIssueImportFail_NoObjectVisibleByCurrentUser($bIsRackReconKey){
|
||||
$this->deleteAllRacks();
|
||||
$this->createRackObjects(
|
||||
[
|
||||
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
|
||||
]
|
||||
);
|
||||
|
||||
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
|
||||
\UserRights::Login($oUser->Get('login'));
|
||||
|
||||
$this->performBulkChangeTest(
|
||||
"There are no 'Rack' objects found with your current profile",
|
||||
"",
|
||||
$oOrg2,
|
||||
$bIsRackReconKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ReconciliationKeyProvider
|
||||
*/
|
||||
public function testExternalFieldIssueImportFail_SomeObjectVisibleByCurrentUser($bIsRackReconKey){
|
||||
$this->deleteAllRacks();
|
||||
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
|
||||
$this->createRackObjects(
|
||||
[
|
||||
$this->getTestOrgId() => ['RackTest1', 'RackTest2'],
|
||||
$oOrg2->GetKey() => ['RackTest3', 'RackTest4'],
|
||||
]
|
||||
);
|
||||
|
||||
\UserRights::Login($oUser->Get('login'));
|
||||
|
||||
$this->performBulkChangeTest(
|
||||
"There are some 'Rack' objects not visible with your current profile",
|
||||
"Some possible 'Rack' value(s): RackTest3, RackTest4",
|
||||
$oOrg2,
|
||||
$bIsRackReconKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ReconciliationKeyProvider
|
||||
*/
|
||||
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser($bIsRackReconKey){
|
||||
$this->deleteAllRacks();
|
||||
$this->createRackObjects(
|
||||
[
|
||||
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
|
||||
]
|
||||
);
|
||||
|
||||
$this->performBulkChangeTest(
|
||||
"No match for value 'UnexistingRack'",
|
||||
"Some possible 'Rack' value(s): RackTest1, RackTest2, RackTest3...",
|
||||
null,
|
||||
$bIsRackReconKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ReconciliationKeyProvider
|
||||
*/
|
||||
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_AmbigousMatch($bIsRackReconKey){
|
||||
$this->deleteAllRacks();
|
||||
$this->createRackObjects(
|
||||
[
|
||||
$this->getTestOrgId() => ['UnexistingRack', 'UnexistingRack']
|
||||
]
|
||||
);
|
||||
|
||||
$this->performBulkChangeTest(
|
||||
"Invalid value for attribute",
|
||||
"Ambiguous: found 2 objects",
|
||||
null,
|
||||
$bIsRackReconKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'Found 2 matches'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @dataProvider ReconciliationKeyProvider
|
||||
*/
|
||||
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_FurtherExtKeyForRack($bIsRackReconKey){
|
||||
$this->deleteAllRacks();
|
||||
$this->createRackObjects(
|
||||
[
|
||||
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
|
||||
]
|
||||
);
|
||||
|
||||
$aCsvData = [["UnexistingRackDescription"]];
|
||||
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1, "description" => 3]];
|
||||
|
||||
$sSearchLinkUrl = 'UI.php?operation=search&filter=%255B%2522SELECT%2B%2560Rack%2560%2BFROM%2BRack%2BAS%2B%2560Rack%2560%2BWHERE%2B%2528%2528%2560Rack%2560.%2560name%2560%2B%253D%2B%253Aname%2529%2BAND%2B%2528%2560Rack%2560.%2560description%2560%2B%253D%2B%253Adescription%2529%2529%2522%252C%257B%2522name%2522%253A%2522UnexistingRack%2522%252C%2522description%2522%253A%2522UnexistingRackDescription%2522%257D%252C%255B%255D%255D'
|
||||
;
|
||||
$this->performBulkChangeTest(
|
||||
"No match for value 'UnexistingRack UnexistingRackDescription'",
|
||||
"Some possible 'Rack' value(s): RackTest1 RackTest1Desc, RackTest2 RackTest2Desc, RackTest3 RackTest3Desc...",
|
||||
null,
|
||||
$bIsRackReconKey,
|
||||
$aCsvData,
|
||||
$aExtKeys,
|
||||
$sSearchLinkUrl
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function GetUid(){
|
||||
if (is_null($this->sUid)){
|
||||
$this->sUid = date('dmYHis');
|
||||
}
|
||||
|
||||
return $this->sUid;
|
||||
}
|
||||
|
||||
/** *
|
||||
* @param $aInitData
|
||||
* @param $aCsvData
|
||||
* @param $aAttributes
|
||||
* @param $aExtKeys
|
||||
* @param $aReconcilKeys
|
||||
*/
|
||||
public function performBulkChangeTest($sExpectedDisplayableValue, $sExpectedDescription, $oOrg, $bIsRackReconKey,
|
||||
$aAdditionalCsvData=null, $aExtKeys=null, $sSearchLinkUrl=null, $sError="Object not found") {
|
||||
if ($sSearchLinkUrl===null){
|
||||
$sSearchLinkUrl = 'UI.php?operation=search&filter=%255B%2522SELECT%2B%2560Rack%2560%2BFROM%2BRack%2BAS%2B%2560Rack%2560%2BWHERE%2B%2528%2560Rack%2560.%2560name%2560%2B%253D%2B%253Aname%2529%2522%252C%257B%2522name%2522%253A%2522UnexistingRack%2522%257D%252C%255B%255D%255D';
|
||||
}
|
||||
if (is_null($oOrg)){
|
||||
$iOrgId = $this->getTestOrgId();
|
||||
$sOrgName = "UnitTestOrganization";
|
||||
}else{
|
||||
$iOrgId = $oOrg->GetKey();
|
||||
$sOrgName = $oOrg->Get('name');
|
||||
}
|
||||
|
||||
$sUid = $this->GetUid();
|
||||
|
||||
$aCsvData = [[$sOrgName, "UnexistingRack", "$sUid"]];
|
||||
if ($aAdditionalCsvData !== null){
|
||||
foreach ($aAdditionalCsvData as $i => $aData){
|
||||
foreach ($aData as $sData){
|
||||
$aCsvData[$i][] = $sData;
|
||||
}
|
||||
}
|
||||
}
|
||||
$aAttributes = ["name" => 2];
|
||||
if ($aExtKeys == null){
|
||||
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1]];
|
||||
}
|
||||
$aReconcilKeys = [ "name" ];
|
||||
|
||||
$aResult = [
|
||||
0 => $sOrgName,
|
||||
"org_id" => $iOrgId,
|
||||
1 => "UnexistingRack",
|
||||
2 => "\"$sUid\"",
|
||||
"rack_id" => [
|
||||
$sExpectedDisplayableValue,
|
||||
$sExpectedDescription
|
||||
],
|
||||
"__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => $sError,
|
||||
];
|
||||
|
||||
if ($bIsRackReconKey){
|
||||
$aReconcilKeys[] = "rack_id";
|
||||
$aResult[2] = $sUid;
|
||||
$aResult["__STATUS__"] = "Issue: failed to reconcile";
|
||||
}
|
||||
|
||||
|
||||
CMDBSource::Query('START TRANSACTION');
|
||||
//change value during the test
|
||||
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
|
||||
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
|
||||
|
||||
$this->debug("aCsvData:".json_encode($aCsvData[0]));
|
||||
$this->debug("aReconcilKeys:". var_export($aReconcilKeys));
|
||||
$oBulk = new \BulkChange(
|
||||
"Server",
|
||||
$aCsvData,
|
||||
$aAttributes,
|
||||
$aExtKeys,
|
||||
$aReconcilKeys,
|
||||
null,
|
||||
null,
|
||||
"Y-m-d H:i:s", // date format
|
||||
true // localize
|
||||
);
|
||||
$this->debug("BulkChange:");
|
||||
$oChange = \CMDBObject::GetCurrentChange();
|
||||
$this->debug("GetCurrentChange:");
|
||||
$aRes = $oBulk->Process($oChange);
|
||||
$this->debug("Process:");
|
||||
static::assertNotNull($aRes);
|
||||
$this->debug("assertNotNull:");
|
||||
var_dump($aRes);
|
||||
foreach ($aRes as $aRow) {
|
||||
if (array_key_exists('__STATUS__', $aRow)) {
|
||||
$sStatus = $aRow['__STATUS__'];
|
||||
$this->debug("sStatus:".$sStatus->GetDescription());
|
||||
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
|
||||
foreach ($aRow as $i => $oCell) {
|
||||
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
|
||||
$this->debug("i:".$i);
|
||||
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
|
||||
if (array_key_exists($i,$aResult)) {
|
||||
$this->debug("aResult:".var_export($aResult[$i]));
|
||||
if ($oCell instanceof \CellStatus_SearchIssue ||
|
||||
$oCell instanceof \CellStatus_Ambiguous) {
|
||||
$this->assertEquals($aResult[$i][0], $oCell->GetDisplayableValue(),
|
||||
"failure on ".get_class($oCell).' cell type');
|
||||
$this->assertEquals($sSearchLinkUrl, $oCell->GetSearchLinkUrl(),
|
||||
"failure on ".get_class($oCell).' cell type');
|
||||
$this->assertEquals($aResult[$i][1], $oCell->GetDescription(),
|
||||
"failure on ".get_class($oCell).' cell type');
|
||||
}
|
||||
}
|
||||
} else if ($i === "__ERRORS__") {
|
||||
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
|
||||
$this->assertEquals( $sErrors, $oCell->GetDescription());
|
||||
}
|
||||
}
|
||||
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
|
||||
}
|
||||
}
|
||||
MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
|
||||
//bug 2888: csv import / data synchro issue with password validation
|
||||
public function testPasswordBulkChangeIssue() {
|
||||
/** @var Personn $oPerson */
|
||||
/** @var \Person $oPerson */
|
||||
$oPerson = $this->createObject('Person', array(
|
||||
'first_name' => 'isaac',
|
||||
'name' => 'asimov',
|
||||
@@ -104,13 +104,13 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
if (array_key_exists('__STATUS__', $aRow)) {
|
||||
$sStatus = $aRow['__STATUS__'];
|
||||
//$this->debug("sStatus:".$sStatus->GetDescription());
|
||||
$this->assertEquals($sStatus->GetDescription(), $aResult["__STATUS__"]);
|
||||
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
|
||||
foreach ($aRow as $i => $oCell) {
|
||||
if ($i != "finalclass" && $i != "__STATUS__") {
|
||||
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
|
||||
$this->debug("i:".$i);
|
||||
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
|
||||
$this->debug("aResult:".$aResult[$i]);
|
||||
$this->assertEquals($oCell->GetDisplayableValue(), $aResult[$i]);
|
||||
$this->assertEquals($aResult[$i], $oCell->GetDisplayableValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,28 +131,37 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "date", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "Server1", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
],
|
||||
"Case 1 : no match" => [
|
||||
[["Bad", "Server1", "1", "production", ""]],
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
["org_id" => "",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
["org_id" => "No match for value 'Bad'",1 => "Server1",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
],
|
||||
"Case 10 : Missing mandatory value" => [
|
||||
[["", "Server1", "1", "production", ""]],
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ "org_id" => "", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[ "org_id" => "Invalid value for attribute", 1 => "Server1", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
],
|
||||
"Case 6 : Unexpected value" => [
|
||||
[["Demo", "Server1", "1", "<svg onclick\"alert(1)\">", ""]],
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[0 => "Demo", "org_id" => "3", 1 => "Server1", 2 => "1", 3 => "<svg onclick"alert(1)">", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[
|
||||
0 => "Demo",
|
||||
"org_id" => "3",
|
||||
1 => "Server1",
|
||||
2 => "1",
|
||||
3 => "'<svg onclick"alert(1)">' is an invalid value",
|
||||
4 => "",
|
||||
"id" => 1,
|
||||
"__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Unexpected value for attribute 'status': no match found, check spelling"],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -172,17 +181,20 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
//change value during the test
|
||||
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
|
||||
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
|
||||
/** @var Server $oServer */
|
||||
$oServer = $this->createObject('Server', array(
|
||||
'name' => $aInitData[1],
|
||||
'status' => $aInitData[2],
|
||||
'org_id' => $aInitData[0],
|
||||
'purchase_date' => $aInitData[3],
|
||||
));
|
||||
$aCsvData[0][2]=$oServer->GetKey();
|
||||
$aResult[2]=$oServer->GetKey();
|
||||
$aResult["id"]=$oServer->GetKey();
|
||||
$this->debug("oServer->GetKey():".$oServer->GetKey());
|
||||
|
||||
if (is_array($aInitData) && sizeof($aInitData) != 0) {
|
||||
/** @var Server $oServer */
|
||||
$oServer = $this->createObject('Server', array(
|
||||
'name' => $aInitData[1],
|
||||
'status' => $aInitData[2],
|
||||
'org_id' => $aInitData[0],
|
||||
'purchase_date' => $aInitData[3],
|
||||
));
|
||||
$aCsvData[0][2]=$oServer->GetKey();
|
||||
$aResult[2]=$oServer->GetKey();
|
||||
$aResult["id"]=$oServer->GetKey();
|
||||
$this->debug("oServer->GetKey():".$oServer->GetKey());
|
||||
}
|
||||
$this->debug("aCsvData:".json_encode($aCsvData[0]));
|
||||
$this->debug("aReconcilKeys:".$aReconcilKeys[0]);
|
||||
$oBulk = new \BulkChange(
|
||||
@@ -207,13 +219,16 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
if (array_key_exists('__STATUS__', $aRow)) {
|
||||
$sStatus = $aRow['__STATUS__'];
|
||||
$this->debug("sStatus:".$sStatus->GetDescription());
|
||||
$this->assertEquals($sStatus->GetDescription(), $aResult["__STATUS__"]);
|
||||
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
|
||||
foreach ($aRow as $i => $oCell) {
|
||||
if ($i != "finalclass" && $i != "__STATUS__") {
|
||||
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
|
||||
$this->debug("i:".$i);
|
||||
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
|
||||
$this->debug("aResult:".$aResult[$i]);
|
||||
$this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue());
|
||||
$this->assertEquals( $aResult[$i], $oCell->GetDisplayableValue(), "failure on " . get_class($oCell) . ' cell type');
|
||||
} else if ($i === "__ERRORS__") {
|
||||
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
|
||||
$this->assertEquals( $sErrors, $oCell->GetDescription());
|
||||
}
|
||||
}
|
||||
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
|
||||
@@ -225,21 +240,58 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
|
||||
public function CSVImportProvider() {
|
||||
return [
|
||||
"Case 6 - 1 : Unexpected value" => [
|
||||
"Case 6 - 1 : Unexpected value (update)" => [
|
||||
["1", "ServerTest", "production", ""],
|
||||
[["Demo", "ServerTest", "key", "BadValue", ""]],
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "BadValue", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[
|
||||
0 => "Demo",
|
||||
"org_id" => "3",
|
||||
1 => "ServerTest",
|
||||
2 => "1",
|
||||
3 => "'BadValue' is an invalid value",
|
||||
4 => "",
|
||||
"id" => 1,
|
||||
"__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock",
|
||||
],
|
||||
],
|
||||
"Case 6 - 2 : Unexpected value" => [
|
||||
"Case 6 - 2 : Unexpected value (update)" => [
|
||||
["1", "ServerTest", "production", ""],
|
||||
[["Demo", "ServerTest", "key", "<svg onclick\"alert(1)\">", ""]],
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[0 => "Demo", "org_id" => "3", 1 => "ServerTest", 2 => "1", 3 => "<svg onclick"alert(1)">", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[
|
||||
0 => "Demo",
|
||||
"org_id" => "3",
|
||||
1 => "ServerTest",
|
||||
2 => "1",
|
||||
3 => "'<svg onclick"alert(1)">' is an invalid value",
|
||||
4 => "",
|
||||
"id" => 1,
|
||||
"__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock",
|
||||
],
|
||||
],
|
||||
"Case 6 - 3 : Unexpected value (creation)" => [
|
||||
[],
|
||||
[["Demo", "ServerTest", "<svg onclick\"alert(1)\">", ""]],
|
||||
["name" => 1, "status" => 2, "purchase_date" => 3],
|
||||
["org_id" => ["name" => 0]],
|
||||
["name"],
|
||||
[
|
||||
0 => "Demo",
|
||||
"org_id" => "3",
|
||||
1 => "\"ServerTest\"",
|
||||
2 => "'<svg onclick"alert(1)">' is an invalid value",
|
||||
3 => "",
|
||||
"id" => 1,
|
||||
"__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Allowed 'status' value(s): implementation,obsolete,production,stock",
|
||||
],
|
||||
],
|
||||
"Case 8 : unchanged name" => [
|
||||
["1", "<svg onclick\"alert(1)\">", "production", ""],
|
||||
@@ -263,7 +315,7 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "date", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'date' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
],
|
||||
"Case 9 - 2: wrong date format" => [
|
||||
["1", "ServerTest", "production", ""],
|
||||
@@ -271,7 +323,7 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "<svg onclick"alert(1)">", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'<svg onclick"alert(1)">' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
],
|
||||
"Case 1 - 1 : no match" => [
|
||||
["1", "ServerTest", "production", ""],
|
||||
@@ -279,7 +331,9 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "Bad", "org_id" => "",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[ 0 => "Bad", "org_id" => "No match for value 'Bad'",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Object not found",
|
||||
],
|
||||
],
|
||||
"Case 1 - 2 : no match" => [
|
||||
["1", "ServerTest", "production", ""],
|
||||
@@ -287,7 +341,9 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "<svg fonclick"alert(1)">", "org_id" => "",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[ 0 => "<svg fonclick"alert(1)">", "org_id" => "No match for value '<svg fonclick\"alert(1)\">'",1 => "ServerTest",2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Object not found",
|
||||
],
|
||||
],
|
||||
"Case 10 : Missing mandatory value" => [
|
||||
["1", "ServerTest", "production", ""],
|
||||
@@ -295,7 +351,9 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "", "org_id" => "", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)"],
|
||||
[ 0 => "", "org_id" => "Invalid value for attribute", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "", "id" => 1, "__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||
"__ERRORS__" => "Null not allowed",
|
||||
],
|
||||
],
|
||||
|
||||
"Case 0 : Date format" => [
|
||||
@@ -304,7 +362,7 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
["name" => 1, "id" => 2, "status" => 3, "purchase_date" => 4],
|
||||
["org_id" => ["name" => 0]],
|
||||
["id"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "2020-20-03", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
[ 0 => "Demo", "org_id" => "n/a", 1 => "ServerTest", 2 => "1", 3 => "production", 4 => "'2020-20-03' is an invalid value", "id" => 1, "__STATUS__" => "Issue: wrong date format"],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -326,20 +384,22 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
//change value during the test
|
||||
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
|
||||
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
|
||||
/** @var Server $oServer */
|
||||
$oOrganisation = $this->createObject('Organization', array(
|
||||
'name' =>$aInitData[0]
|
||||
));
|
||||
$aResult["org_id"]=$oOrganisation->GetKey();
|
||||
$oServer = $this->createObject('Server', array(
|
||||
'name' => $aInitData[1],
|
||||
'status' => $aInitData[2],
|
||||
'org_id' => $oOrganisation->GetKey(),
|
||||
'purchase_date' => $aInitData[3],
|
||||
));
|
||||
$aCsvData[0][2]=$oServer->GetKey();
|
||||
$aResult[2]=$oServer->GetKey();
|
||||
$aResult["id"]=$oServer->GetKey();
|
||||
if (is_array($aInitData) && sizeof($aInitData) != 0) {
|
||||
/** @var Server $oServer */
|
||||
$oOrganisation = $this->createObject('Organization', array(
|
||||
'name' => $aInitData[0]
|
||||
));
|
||||
$aResult["org_id"] = $oOrganisation->GetKey();
|
||||
$oServer = $this->createObject('Server', array(
|
||||
'name' => $aInitData[1],
|
||||
'status' => $aInitData[2],
|
||||
'org_id' => $oOrganisation->GetKey(),
|
||||
'purchase_date' => $aInitData[3],
|
||||
));
|
||||
$aCsvData[0][2]=$oServer->GetKey();
|
||||
$aResult[2]=$oServer->GetKey();
|
||||
$aResult["id"]=$oServer->GetKey();
|
||||
}
|
||||
$oBulk = new \BulkChange(
|
||||
"Server",
|
||||
$aCsvData,
|
||||
@@ -356,15 +416,17 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
static::assertNotNull($aRes);
|
||||
foreach ($aRes as $aRow) {
|
||||
foreach ($aRow as $i => $oCell) {
|
||||
if ($i != "finalclass" && $i != "__STATUS__") {
|
||||
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
|
||||
$this->debug("i:".$i);
|
||||
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
|
||||
$this->debug("aResult:".$aResult[$i]);
|
||||
$this->assertEquals($aResult[$i], $oCell->GetDisplayableValue());
|
||||
}
|
||||
elseif ($i == "__STATUS__") {
|
||||
} elseif ($i == "__STATUS__") {
|
||||
$sStatus = $aRow['__STATUS__'];
|
||||
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
|
||||
} else if ($i === "__ERRORS__") {
|
||||
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
|
||||
$this->assertEquals( $sErrors, $oCell->GetDescription());
|
||||
}
|
||||
}
|
||||
$this->assertEquals($aResult[0], $aRow[0]->GetDisplayableValue());
|
||||
@@ -402,4 +464,4 @@ class BulkChangeTest extends ItopDataTestCase {
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -108,8 +108,8 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
$sAdminLogin = "admin-user-".$sUid;
|
||||
$sImpersonatedLogin = "impersonated-user-".$sUid;
|
||||
|
||||
$iAdminUserId = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
|
||||
$this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
|
||||
$oAdminUser = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
|
||||
$oImpersonatedUser = $this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
|
||||
|
||||
$_SESSION = [];
|
||||
\UserRights::Login($sAdminLogin);
|
||||
@@ -124,28 +124,31 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
if (is_null($sTrackInfo)){
|
||||
CMDBObject::SetTrackInfo(null);
|
||||
} else {
|
||||
$sTrackInfo = $this->ReplaceByFriendlyNames($sTrackInfo, $oAdminUser, $oImpersonatedUser);
|
||||
CMDBObject::SetTrackInfo($sTrackInfo);
|
||||
}
|
||||
|
||||
$this->CreateSimpleObject();
|
||||
if (is_null($sTrackInfo)){
|
||||
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
} else {
|
||||
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
}
|
||||
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
'TrackInfo : admin userid');
|
||||
|
||||
\UserRights::Impersonate($sImpersonatedLogin);
|
||||
$this->CreateSimpleObject();
|
||||
|
||||
if (is_null($sExpectedChangeLogWhenImpersonation)){
|
||||
self::assertEquals("AdminSurName AdminName on behalf of ImpersonatedSurName ImpersonatedName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
$sExpectedMsg = $this->ReplaceByFriendlyNames("AdminSurName AdminName on behalf of ImpersonatedSurName ImpersonatedName", $oAdminUser, $oImpersonatedUser);
|
||||
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : impersonation');
|
||||
} else {
|
||||
self::assertEquals($sExpectedChangeLogWhenImpersonation, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
$sExpectedMsg = $this->ReplaceByFriendlyNames($sExpectedChangeLogWhenImpersonation, $oAdminUser, $oImpersonatedUser);
|
||||
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : impersonation');
|
||||
}
|
||||
|
||||
@@ -155,13 +158,13 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
\UserRights::Deimpersonate();
|
||||
$this->CreateSimpleObject();
|
||||
if (is_null($sTrackInfo)){
|
||||
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
} else {
|
||||
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
}
|
||||
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
'TrackInfo : admin userid');
|
||||
|
||||
// restore initial conditions
|
||||
@@ -169,6 +172,12 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
CMDBObject::SetTrackInfo($sInitialTrackInfo);
|
||||
}
|
||||
|
||||
private function ReplaceByFriendlyNames($sMessage, $oAdminUser, $oImpersonatedUser) : string {
|
||||
$sNewMessage = str_replace('AdminSurName AdminName', $oAdminUser->GetFriendlyName(), $sMessage);
|
||||
$sNewMessage = str_replace('ImpersonatedSurName ImpersonatedName', $oImpersonatedUser->GetFriendlyName(), $sNewMessage);
|
||||
return $sNewMessage;
|
||||
}
|
||||
|
||||
private function CreateSimpleObject(){
|
||||
/** @var \DocumentWeb $oTestObject */
|
||||
$oTestObject = MetaModel::NewObject('DocumentWeb');
|
||||
@@ -178,7 +187,7 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
$oTestObject->DBWrite();
|
||||
}
|
||||
|
||||
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): int {
|
||||
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): \UserLocal {
|
||||
/** @var \Person $oPerson */
|
||||
$oPerson = $this->createObject('Person', array(
|
||||
'name' => $sName,
|
||||
@@ -187,8 +196,9 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
));
|
||||
|
||||
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => $sProfileName), true);
|
||||
/** @var \UserLocal $oUser */
|
||||
$oUser = $this->CreateUser($sLogin, $oProfile->GetKey(), "1234567Azert@", $oPerson->GetKey());
|
||||
|
||||
return $oUser->GetKey();
|
||||
return $oUser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Combodo\iTop\Test\UnitTest\Integration;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
|
||||
use Dict;
|
||||
|
||||
class DictionariesConsistencyTest extends ItopTestCase
|
||||
{
|
||||
@@ -148,4 +149,67 @@ class DictionariesConsistencyTest extends ItopTestCase
|
||||
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
|
||||
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ImBulChanportCsvMessageStillOkProvider
|
||||
* make sure N°5305 dictionary changes are still here and UI remains unbroken for any lang
|
||||
*/
|
||||
public function testImportCsvMessageStillOk($sLangCode, $sDictFile)
|
||||
{
|
||||
$aFailedLabels = [];
|
||||
$aLabelsToTest = [
|
||||
'UI:CSVReport-Value-SetIssue' => [],
|
||||
'UI:CSVReport-Value-ChangeIssue' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch-PossibleValues' => [ 'arg1', 'arg2' ],
|
||||
'UI:CSVReport-Value-NoMatch-NoObject' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => [ 'arg1' ],
|
||||
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => [ 'arg1' ],
|
||||
];
|
||||
|
||||
$sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode));
|
||||
require_once(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/languages.php');
|
||||
Dict::SetUserLanguage($sLanguageCode);
|
||||
foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs){
|
||||
try{
|
||||
$sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs);
|
||||
var_dump($sLabelValue);
|
||||
} catch (\Exception $e){
|
||||
$aFailedLabels[] = $sLabelKey;
|
||||
|
||||
var_dump([
|
||||
'exception' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'label_name' => $sLabelKey,
|
||||
'label_args' =>$aLabelArgs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->assertEquals([], $aFailedLabels, "test fail for lang $sLangCode and labels (" . implode(",", $aFailedLabels) . ')');
|
||||
}
|
||||
|
||||
public function ImportCsvMessageStillOkProvider(){
|
||||
return $this->GetDictFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* return a map linked to *.dict.php files that are generated after setup
|
||||
* each entry key is lang code (example 'en')
|
||||
* each value is an array with lang code (again) and dict file path
|
||||
* @return array
|
||||
*/
|
||||
private function GetDictFiles() : array {
|
||||
$aDictFiles = [];
|
||||
|
||||
foreach (glob(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/*.dict.php') as $sDictFile){
|
||||
if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)){
|
||||
$sLangCode = $aMatches[1];
|
||||
$aDictFiles[$sLangCode] = [
|
||||
'lang' => $sLangCode,
|
||||
'file' => $sDictFile
|
||||
];
|
||||
}
|
||||
}
|
||||
return $aDictFiles;
|
||||
}
|
||||
}
|
||||
|
||||
139
test/setup/MFCompilerMenuTest.php
Normal file
139
test/setup/MFCompilerMenuTest.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Setup;
|
||||
|
||||
use ApplicationMenu;
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
use Config;
|
||||
use MetaModel;
|
||||
use MFCompiler;
|
||||
use ParentMenuNodeCompiler;
|
||||
use RunTimeEnvironment;
|
||||
|
||||
/**
|
||||
* @group menu_compilation
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
* @since 3.1 N°4762
|
||||
* @covers \MFCompiler::DoCompile
|
||||
*/
|
||||
class MFCompilerMenuTest extends ItopTestCase {
|
||||
private static $aPreviousEnvMenus;
|
||||
private static $aPreviousEnvMenuCount;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
require_once APPROOT . 'setup/compiler.class.inc.php';
|
||||
require_once APPROOT . 'setup/modelfactory.class.inc.php';
|
||||
require_once APPROOT . 'application/utils.inc.php';
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function GetCurrentEnvDeltaXmlPath(string $sEnv) : string {
|
||||
return APPROOT."data/$sEnv.delta.xml";
|
||||
}
|
||||
|
||||
public function CompileMenusProvider(){
|
||||
return [
|
||||
'legacy_algo' => [ 'sEnv' => 'legacy_algo', 'bLegacyMenuCompilation' => true ],
|
||||
'menu_compilation_fix' => [ 'sEnv' => 'menu_compilation_fix', 'bLegacyMenuCompilation' => false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider CompileMenusProvider
|
||||
*/
|
||||
public function testCompileMenus($sEnv, $bLegacyMenuCompilation){
|
||||
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
|
||||
|
||||
//copy conf from production to phpunit context
|
||||
$sDirPath = dirname($sConfigFilePath);
|
||||
if (! is_dir($sDirPath)){
|
||||
mkdir($sDirPath);
|
||||
}
|
||||
$oConfig = new Config(\utils::GetConfigFilePath());
|
||||
$oConfig->WriteToFile($sConfigFilePath);
|
||||
|
||||
$oConfig = new Config($sConfigFilePath);
|
||||
if ($bLegacyMenuCompilation){
|
||||
ParentMenuNodeCompiler::UseLegacyMenuCompilation();
|
||||
}
|
||||
$oConfig->WriteToFile();
|
||||
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
|
||||
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
|
||||
$oConfig->WriteToFile();
|
||||
|
||||
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||
|
||||
$aMenuGroups = ApplicationMenu::GetMenuGroups();
|
||||
if (! is_null(static::$aPreviousEnvMenus)){
|
||||
$this->assertEquals(static::$aPreviousEnvMenus, $aMenuGroups);
|
||||
} else {
|
||||
$this->assertNotEquals([], $aMenuGroups);
|
||||
}
|
||||
static::$aPreviousEnvMenus = $aMenuGroups;
|
||||
|
||||
$aMenuCount = ApplicationMenu::GetMenusCount();
|
||||
|
||||
if (! is_null(static::$aPreviousEnvMenuCount)){
|
||||
$this->assertEquals(static::$aPreviousEnvMenuCount, $aMenuCount);
|
||||
} else {
|
||||
$this->assertNotEquals([], $aMenuCount);
|
||||
}
|
||||
static::$aPreviousEnvMenuCount = $aMenuCount;
|
||||
}
|
||||
|
||||
public function CompileMenusWithDeltaProvider(){
|
||||
return [
|
||||
'Menus are broken with specific delta XML using LEGACY algo' => [ 'sDeltaFile' => 'delta_broken_menus.xml', 'sEnv' => 'broken_menus', 'bLegacyMenuCompilation' => true ],
|
||||
'Menus repaired using same delta XML with NEW algo' => [ 'sDeltaFile' => 'delta_broken_menus.xml', 'sEnv' => 'fixed_menus', 'bLegacyMenuCompilation' => false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider CompileMenusWithDeltaProvider
|
||||
*/
|
||||
public function testCompileMenusWithDelta($sDeltaFile, $sEnv, $bLegacyMenuCompilation){
|
||||
$sProvidedDeltaPath = __DIR__.'/ressources/datamodels/'.$sDeltaFile;
|
||||
if (is_file($sProvidedDeltaPath)){
|
||||
$sDeltaXmlPath = $this->GetCurrentEnvDeltaXmlPath($sEnv);
|
||||
copy($sProvidedDeltaPath, $sDeltaXmlPath);
|
||||
}
|
||||
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
|
||||
|
||||
//copy conf from production to phpunit context
|
||||
$sDirPath = dirname($sConfigFilePath);
|
||||
if (! is_dir($sDirPath)){
|
||||
mkdir($sDirPath);
|
||||
}
|
||||
$oConfig = new Config(\utils::GetConfigFilePath());
|
||||
$oConfig->WriteToFile($sConfigFilePath);
|
||||
|
||||
$oConfig = new Config($sConfigFilePath);
|
||||
if ($bLegacyMenuCompilation){
|
||||
ParentMenuNodeCompiler::UseLegacyMenuCompilation();
|
||||
}
|
||||
$oConfig->WriteToFile();
|
||||
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
|
||||
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
|
||||
$oConfig->WriteToFile();
|
||||
|
||||
if ($bLegacyMenuCompilation){
|
||||
/**
|
||||
* PHP Notice: Undefined index: ConfigManagement in /var/www/html/iTop/env-broken_menus/itop-structure/model.itop-structure.php on line 925
|
||||
*/
|
||||
error_reporting(E_ALL & ~E_NOTICE);
|
||||
$this->expectErrorMessage("Call to a member function GetIndex() on null");
|
||||
}
|
||||
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||
|
||||
$this->assertNotEquals([], ApplicationMenu::GetMenuGroups());
|
||||
$this->assertNotEquals([], ApplicationMenu::GetMenusCount());
|
||||
}
|
||||
}
|
||||
14
test/setup/ressources/datamodels/delta_broken_menus.xml
Normal file
14
test/setup/ressources/datamodels/delta_broken_menus.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0">
|
||||
<menus>
|
||||
<menu id="Contact" xsi:type="DashboardMenuNode" _created_in="itop-config-mgmt" _delta="must_exist">
|
||||
<parent _delta="redefine">ConfigManagementOverview</parent>
|
||||
</menu>
|
||||
<menu id="Location" xsi:type="OQLMenuNode" _created_in="itop-config-mgmt" _delta="delete">
|
||||
<parent _delta="redefine">ConfigManagementOverview</parent>
|
||||
</menu>
|
||||
<menu id="Document" xsi:type="OQLMenuNode" _created_in="itop-config-mgmt" _delta="must_exist">
|
||||
<parent _delta="redefine">ConfigManagementOverview</parent>
|
||||
</menu>
|
||||
</menus>
|
||||
</itop_design>
|
||||
@@ -848,89 +848,6 @@ $oSearch->AddCondition_PointingTo($oOrgSearch, "org_id");
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Test bulk load API
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class TestItopBulkLoad extends TestBizModel
|
||||
{
|
||||
static public function GetName()
|
||||
{
|
||||
return 'Itop - test BulkChange class';
|
||||
}
|
||||
|
||||
static public function GetDescription()
|
||||
{
|
||||
return 'Execute a bulk change at the Core API level';
|
||||
}
|
||||
|
||||
protected function DoExecute()
|
||||
{
|
||||
$sLogin = 'testbulkload_'.time();
|
||||
|
||||
$oParser = new CSVParser("login,contactid->name,password,profile_list
|
||||
_1_$sLogin,Picasso,secret1,profileid:10;reason:service manager|profileid->name:Problem Manager;'reason:toto;problem manager'
|
||||
_2_$sLogin,Picasso,secret2,
|
||||
", ',', '"');
|
||||
$aData = $oParser->ToArray(1, array('_login', '_contact_name', '_password', '_profiles'));
|
||||
self::DumpVariable($aData);
|
||||
|
||||
$oUser = new UserLocal();
|
||||
$oUser->Set('login', 'patator');
|
||||
$oUser->Set('password', 'patator');
|
||||
//$oUser->Set('contactid', 0);
|
||||
//$oUser->Set('language', $sLanguage);
|
||||
|
||||
$aProfiles = array(
|
||||
array(
|
||||
'profileid' => 10, // Service Manager
|
||||
'reason' => 'service manager',
|
||||
),
|
||||
array(
|
||||
'profileid->name' => 'Problem Manager',
|
||||
'reason' => 'problem manager',
|
||||
),
|
||||
);
|
||||
|
||||
$oBulk = new BulkChange(
|
||||
'UserLocal',
|
||||
$aData,
|
||||
// attributes
|
||||
array('login' => '_login', 'password' => '_password', 'profile_list' => '_profiles'),
|
||||
// ext keys
|
||||
array('contactid' => array('name' => '_contact_name')),
|
||||
// reconciliation
|
||||
array('login'),
|
||||
// Synchro - scope
|
||||
"SELECT UserLocal",
|
||||
// Synchro - set attribute on missing objects
|
||||
array ('password' => 'terminated', 'login' => 'terminated'.time())
|
||||
);
|
||||
|
||||
if (false)
|
||||
{
|
||||
$oMyChange = MetaModel::NewObject("CMDBChange");
|
||||
$oMyChange->Set("date", time());
|
||||
$oMyChange->Set("userinfo", "Testor");
|
||||
$iChangeId = $oMyChange->DBInsert();
|
||||
// echo "Created new change: $iChangeId</br>";
|
||||
}
|
||||
|
||||
echo "<h3>Planned for loading...</h3>";
|
||||
$aRes = $oBulk->Process();
|
||||
self::DumpVariable($aRes);
|
||||
if (false)
|
||||
{
|
||||
echo "<h3>Go for loading...</h3>";
|
||||
$aRes = $oBulk->Process($oMyChange);
|
||||
self::DumpVariable($aRes);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Test data load
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
198
test/webservices/ImportTest.php
Normal file
198
test/webservices/ImportTest.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Webservices;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use MetaModel;
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*/
|
||||
class ImportTest extends ItopDataTestCase {
|
||||
const USE_TRANSACTION = false;
|
||||
|
||||
private $sUrl;
|
||||
private $sUid;
|
||||
private $sLogin;
|
||||
private $sPassword = "abcDEF12345##";
|
||||
private $sTmpFile = "";
|
||||
private $oOrg;
|
||||
|
||||
protected function tearDown() : void{
|
||||
parent::tearDown();
|
||||
if (!empty($this->sTmpFile) && is_file($this->sTmpFile)){
|
||||
unlink($this->sTmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp() : void{
|
||||
parent::setUp();
|
||||
|
||||
$this->sTmpFile = tempnam(sys_get_temp_dir(), 'import_csv_');
|
||||
|
||||
require_once(APPROOT.'application/startup.inc.php');
|
||||
$this->sUid = date('dmYHis');
|
||||
$this->sLogin = "import-" .$this->sUid;
|
||||
$this->oOrg = $this->CreateOrganization($this->sUid);
|
||||
|
||||
$sConfigFile = \utils::GetConfig()->GetLoadedFile();
|
||||
@chmod($sConfigFile, 0770);
|
||||
$this->sUrl = \MetaModel::GetConfig()->Get('app_root_url');
|
||||
@chmod($sConfigFile, 0444); // Read-only
|
||||
|
||||
$oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true);
|
||||
$oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true);
|
||||
|
||||
if (is_object($oRestProfile) && is_object($oAdminProfile))
|
||||
{
|
||||
$oUser = $this->CreateUser($this->sLogin, $oRestProfile->GetKey(), $this->sPassword);
|
||||
$this->AddProfileToUser($oUser, $oAdminProfile->GetKey());
|
||||
} else {
|
||||
throw new \Exception("setup failed. test cannot work as usual");
|
||||
}
|
||||
}
|
||||
|
||||
public function ImportOkProvider(){
|
||||
return [
|
||||
'with reconciliation key' => [ "sReconciliationKeys" => "name,first_name,org_id->name" ],
|
||||
'without reconciliation key' => [ "sReconciliationKeys" => null ],
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @dataProvider ImportOkProvider
|
||||
*/
|
||||
public function testImportOk($sReconciliationKeys){
|
||||
$sFirstName = "firstname_UID";
|
||||
$sLastName = "lastname_UID";
|
||||
$sEmail = "email_UID@toto.fr";
|
||||
|
||||
$this->performImportTesting(
|
||||
'"first_name","name", "email", "org_id->name"',
|
||||
sprintf('"%s", "%s", "%s", UID', $sFirstName, $sLastName, $sEmail),
|
||||
sprintf('ORGID;"%s";"%s";"%s"', $sFirstName, $sLastName, $sEmail),
|
||||
$sReconciliationKeys,
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
public function ImportFailProvider(){
|
||||
return [
|
||||
'without reconciliation key' => [
|
||||
"sReconciliationKeys" => null,
|
||||
"sExpectedLastLineNeedle" => 'Issue: Unexpected attribute value(s);n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): '
|
||||
],
|
||||
'with reconciliation key' => [
|
||||
"sReconciliationKeys" => "name,first_name,org_id->name",
|
||||
"sExpectedLastLineNeedle" => 'Issue: failed to reconcile;n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): '
|
||||
],
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @dataProvider ImportFailProvider
|
||||
*/
|
||||
|
||||
public function testImportFail_ExternalKey($sReconciliationKeys, $sExpectedLastLineNeedle){
|
||||
$sFirstName = "firstname_UID";
|
||||
$sLastName = "lastname_UID";
|
||||
$sEmail = "email_UID@toto.fr";
|
||||
|
||||
$this->performImportTesting(
|
||||
'"first_name","name", "email", "org_id->name"',
|
||||
sprintf('"%s", "%s", "%s", gabuzomeu', $sFirstName, $sLastName, $sEmail),
|
||||
$sExpectedLastLineNeedle,
|
||||
$sReconciliationKeys,
|
||||
1,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
public function testImportFail_Enum(){
|
||||
$sFirstName = "firstname_UID";
|
||||
$sLastName = "lastname_UID";
|
||||
$sEmail = "email_UID@toto.fr";
|
||||
|
||||
$this->performImportTesting(
|
||||
'"first_name","name", "email", "org_id->name", status',
|
||||
sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail),
|
||||
sprintf(
|
||||
'Issue: Unexpected attribute value(s);n/a;n/a;ORGID;"%s";"%s";"%s";\'toto\' is an invalid value. Unexpected value for attribute \'status\': Value not allowed [toto]', $sFirstName, $sLastName, $sEmail
|
||||
),
|
||||
null,
|
||||
1,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
public function testImportFail_Date(){
|
||||
$sFirstName = "firstname_UID";
|
||||
$sLastName = "lastname_UID";
|
||||
$sEmail = "email_UID@toto.fr";
|
||||
|
||||
$this->performImportTesting(
|
||||
'"first_name","name", "email", "org_id->name", obsolescence_date',
|
||||
sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail),
|
||||
sprintf(
|
||||
'Issue: Internal error: Exception, Wrong format for date attribute obsolescence_date, expecting "Y-m-d" and got "toto";n/a;n/a;n/a;%s;%s;%s;toto', $sFirstName, $sLastName, $sEmail
|
||||
),
|
||||
null,
|
||||
1,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private function performImportTesting($sCsvHeaders, $sCsvFirstLineValues, $sExpectedLastLineNeedle, $sReconciliationKeys=null, $iExpectedIssue=1, $iExpectedCreated=0) {
|
||||
$sContent = <<<CSVFILE
|
||||
$sCsvHeaders
|
||||
$sCsvFirstLineValues
|
||||
CSVFILE;
|
||||
file_put_contents($this->sTmpFile, str_replace("UID", $this->sUid, $sContent));
|
||||
|
||||
$aParams = [
|
||||
'class' => 'Person',
|
||||
'csvfile' => $this->sTmpFile,
|
||||
'charset' => 'UTF-8',
|
||||
'no_localize' => '1',
|
||||
'output' => 'details',
|
||||
];
|
||||
|
||||
if (null != $sReconciliationKeys){
|
||||
$aParams["reconciliationkeys"] = $sReconciliationKeys;
|
||||
}
|
||||
|
||||
$aRes = \utils::ExecITopScript('webservices/import.php', $aParams, $this->sLogin, $this->sPassword);
|
||||
$aOutput = $aRes[1];
|
||||
$sOutput = implode("\n", $aOutput);
|
||||
$sLastline = $aOutput[sizeof($aOutput) - 1];
|
||||
$iRes = $aRes[0];
|
||||
$this->assertEquals(0, $iRes, $sOutput);
|
||||
$this->assertContains("#Issues: $iExpectedIssue", $sOutput, $sOutput);
|
||||
$this->assertContains("#Warnings: 0", $sOutput, $sOutput);
|
||||
$this->assertContains("#Created: $iExpectedCreated", $sOutput, $sOutput);
|
||||
$this->assertContains("#Updated: 0", $sOutput, $sOutput);
|
||||
var_dump($sLastline);
|
||||
if ($iExpectedCreated === 1) {
|
||||
$this->assertContains("created;Person", $sLastline, $sLastline);
|
||||
}
|
||||
|
||||
$iOrgId = $this->oOrg->GetKey();
|
||||
$sLastLineNeedle = $sExpectedLastLineNeedle;
|
||||
foreach (["ORGID" => $iOrgId, "UID" => $this->sUid] as $sSearch => $sReplace){
|
||||
$sLastLineNeedle = str_replace($sSearch, $sReplace, $sLastLineNeedle);
|
||||
}
|
||||
$this->assertContains($sLastLineNeedle, $sLastline, $sLastline);
|
||||
|
||||
$sPattern = "/Person;(\d+);/";
|
||||
if (preg_match($sPattern,$sLastline,$aMatches)){
|
||||
var_dump($aMatches);
|
||||
$iObjId = $aMatches[1];
|
||||
$oObj = MetaModel::GetObject("Person", $iObjId);
|
||||
$oObj->DBDelete();
|
||||
}
|
||||
|
||||
//date
|
||||
//ext key
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,7 @@ if (utils::IsModeCLI())
|
||||
{
|
||||
// Next steps:
|
||||
// specific arguments: 'csvfile'
|
||||
//
|
||||
//
|
||||
$sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data');
|
||||
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data');
|
||||
$sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data');
|
||||
@@ -273,7 +273,7 @@ try
|
||||
//
|
||||
// Read parameters
|
||||
//
|
||||
$sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves
|
||||
$sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves
|
||||
$sSep = ReadParam($oP, 'separator', 'raw_data');
|
||||
$sQualifier = ReadParam($oP, 'qualifier', 'raw_data');
|
||||
$sCharSet = ReadParam($oP, 'charset', 'raw_data');
|
||||
@@ -326,7 +326,7 @@ try
|
||||
{
|
||||
$sDateFormat = null;
|
||||
}
|
||||
|
||||
|
||||
if ($sCharSet == '')
|
||||
{
|
||||
$sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset');
|
||||
@@ -444,7 +444,7 @@ try
|
||||
{
|
||||
$sUTF8Data = iconv($sCharSet, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
|
||||
}
|
||||
$oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier);
|
||||
$oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier);
|
||||
|
||||
// Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute
|
||||
$aRawFieldList = $oCSVParser->ListFields();
|
||||
@@ -466,7 +466,7 @@ try
|
||||
// Remove any trailing "star" character before the arrow (->)
|
||||
// A star character at the end can be used to indicate a mandatory field
|
||||
$sFieldName = $aMatches[1].'->'.$aMatches[2];
|
||||
}
|
||||
}
|
||||
if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames))
|
||||
{
|
||||
$aColumns = $aKnownColumnNames[strtolower($sFieldName)];
|
||||
@@ -488,7 +488,7 @@ try
|
||||
throw new BulkLoadException("Unknown column: '$sSafeName'. Possible columns: ".implode(', ', array_keys($aKnownColumnNames)));
|
||||
}
|
||||
}
|
||||
// Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->')
|
||||
// Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->')
|
||||
|
||||
$aAttList = array();
|
||||
$aExtKeys = array();
|
||||
@@ -723,7 +723,7 @@ try
|
||||
}
|
||||
CMDBObject::SetTrackInfo($sMoreInfo);
|
||||
CMDBObject::SetTrackOrigin('csv-import.php');
|
||||
|
||||
|
||||
$oMyChange = CMDBObject::GetCurrentChange();
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ try
|
||||
break;
|
||||
case 'RowStatus_Issue':
|
||||
$iCountErrors++;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($bWritten)
|
||||
@@ -837,7 +837,7 @@ try
|
||||
$aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aResultDisp = array(); // to be displayed
|
||||
foreach($aRes as $iRow => $aRowData)
|
||||
{
|
||||
@@ -864,14 +864,16 @@ try
|
||||
foreach($aRowData as $key => $value)
|
||||
{
|
||||
$sKey = (string) $key;
|
||||
|
||||
|
||||
if ($sKey == '__STATUS__') continue;
|
||||
//__ERRORS__ used by tests only
|
||||
if ($sKey == '__ERRORS__') continue;
|
||||
if ($sKey == 'finalclass') continue;
|
||||
if ($sKey == 'id') continue;
|
||||
|
||||
|
||||
if (is_object($value))
|
||||
{
|
||||
$aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription();
|
||||
$aRowDisp["$sKey"] = $value->GetDisplayableValueAndDescription();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -885,15 +887,15 @@ try
|
||||
}
|
||||
catch(BulkLoadException $e)
|
||||
{
|
||||
$oP->add_comment($e->getMessage());
|
||||
$oP->add_comment($e->getMessage());
|
||||
}
|
||||
catch(SecurityException $e)
|
||||
{
|
||||
$oP->add_comment($e->getMessage());
|
||||
$oP->add_comment($e->getMessage());
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$oP->add_comment((string)$e);
|
||||
$oP->add_comment((string)$e);
|
||||
}
|
||||
|
||||
$oP->output();
|
||||
|
||||
Reference in New Issue
Block a user