mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-13 21:58:43 +02:00
Compare commits
39 Commits
issue/6667
...
issue/6716
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bd72409f1 | ||
|
|
3eb06f8ada | ||
|
|
44c189223e | ||
|
|
7fdbb59c30 | ||
|
|
5acf38ac36 | ||
|
|
3e258f32cc | ||
|
|
3c51d6fb98 | ||
|
|
7cfe1389aa | ||
|
|
7292a8540b | ||
|
|
f65c690462 | ||
|
|
ecf8bc42fa | ||
|
|
e76728b2bf | ||
|
|
faba812fc1 | ||
|
|
add433d702 | ||
|
|
9c99cb35e5 | ||
|
|
9d392ad167 | ||
|
|
ea8509db1f | ||
|
|
df25ce76b6 | ||
|
|
e946fc65fc | ||
|
|
d203e075a8 | ||
|
|
dbe2f66539 | ||
|
|
0d8ff7bbac | ||
|
|
48eb022824 | ||
|
|
03c9ffc033 | ||
|
|
61a9a4ac65 | ||
|
|
38962e68ee | ||
|
|
483dbb4a5d | ||
|
|
1f4dcc4f9e | ||
|
|
e86309669e | ||
|
|
6d6f55acf7 | ||
|
|
6ebcd44bb1 | ||
|
|
f8fb51fea0 | ||
|
|
bf768311c2 | ||
|
|
d797436786 | ||
|
|
b508c0d983 | ||
|
|
351893bbdd | ||
|
|
59e4bb028f | ||
|
|
6d895371ec | ||
|
|
ab91631e68 |
@@ -34,15 +34,15 @@ class AuditCategory extends cmdbAbstractObject
|
||||
{
|
||||
$aParams = array
|
||||
(
|
||||
"category" => "application, grant_by_profile",
|
||||
"key_type" => "autoincrement",
|
||||
"name_attcode" => "name",
|
||||
"state_attcode" => "",
|
||||
"reconc_keys" => array('name'),
|
||||
"db_table" => "priv_auditcategory",
|
||||
"db_key_field" => "id",
|
||||
"category" => "application,grant_by_profile",
|
||||
"key_type" => "autoincrement",
|
||||
"name_attcode" => "name",
|
||||
"state_attcode" => "",
|
||||
"reconc_keys" => array('name'),
|
||||
"db_table" => "priv_auditcategory",
|
||||
"db_key_field" => "id",
|
||||
"db_finalclass_field" => "",
|
||||
'style' => new ormStyle(null, null, null, null, null, '../images/icons/icons8-audit-folder.svg'),
|
||||
'style' => new ormStyle(null, null, null, null, null, '../images/icons/icons8-audit-folder.svg'),
|
||||
);
|
||||
MetaModel::Init_Params($aParams);
|
||||
MetaModel::Init_AddAttribute(new AttributeString("name", array("description"=>"Short name for this category", "allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
|
||||
|
||||
@@ -35,7 +35,7 @@ class AuditDomain extends cmdbAbstractObject
|
||||
{
|
||||
$aParams = array
|
||||
(
|
||||
"category" => "application, grant_by_profile",
|
||||
"category" => "application,grant_by_profile",
|
||||
"key_type" => "autoincrement",
|
||||
"name_attcode" => "name",
|
||||
"complementary_name_attcode" => array('description'),
|
||||
|
||||
@@ -35,7 +35,7 @@ class AuditRule extends cmdbAbstractObject
|
||||
{
|
||||
$aParams = array
|
||||
(
|
||||
"category" => "application, grant_by_profile",
|
||||
"category" => "application,grant_by_profile",
|
||||
"key_type" => "autoincrement",
|
||||
"name_attcode" => "name",
|
||||
"state_attcode" => "",
|
||||
|
||||
@@ -1246,6 +1246,10 @@ JS
|
||||
} else {
|
||||
$oBlock = DashletFactory::MakeForDashletBadge($sClassIconUrl, $sHyperlink, $iCount, $sClassLabel, null, null, $aRefreshParams);
|
||||
}
|
||||
$sClassDescription = MetaModel::GetClassDescription($sClass);
|
||||
if (utils::IsNotNullOrEmptyString($sClassDescription)) {
|
||||
$oBlock->SetClassDescription($sClassDescription);
|
||||
}
|
||||
|
||||
return $oBlock;
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ class QueryOQL extends Query
|
||||
}
|
||||
catch
|
||||
(OQLException $e) {
|
||||
$oAlert = AlertUIBlockFactory::MakeForFailure(Dict::Format('UI:RunQuery:Error'), $e->getHtmlDesc())
|
||||
$oAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:RunQuery:Error'), $e->getHtmlDesc())
|
||||
->SetIsClosable(false)
|
||||
->SetIsCollapsible(false);
|
||||
$oAlert->AddCSSClass('mb-5');
|
||||
|
||||
@@ -178,17 +178,18 @@ class UILinksWidget
|
||||
|
||||
$oDisplayBlock = new DisplayBlock($oFilter, 'search', false);
|
||||
$oBlock->AddSubBlock($oDisplayBlock->GetDisplay($oPage, "SearchFormToAdd_{$sLinkedSetId}",
|
||||
array(
|
||||
'menu' => false,
|
||||
[
|
||||
'menu' => false,
|
||||
'result_list_outer_selector' => "SearchResultsToAdd_{$sLinkedSetId}",
|
||||
'table_id' => "add_{$sLinkedSetId}",
|
||||
'table_inner_id' => "ResultsToAdd_{$sLinkedSetId}",
|
||||
'selection_mode' => true,
|
||||
'json' => $sJson,
|
||||
'cssCount' => '#count_'.$this->m_sAttCode.$this->m_sNameSuffix,
|
||||
'query_params' => $oFilter->GetInternalParams(),
|
||||
'hidden_criteria' => $sAlreadyLinkedExpression,
|
||||
)));
|
||||
'table_id' => "add_{$sLinkedSetId}",
|
||||
'table_inner_id' => "ResultsToAdd_{$sLinkedSetId}",
|
||||
'selection_mode' => true,
|
||||
'json' => $sJson,
|
||||
'cssCount' => '#count_'.$this->m_sAttCode.$this->m_sNameSuffix,
|
||||
'query_params' => $oFilter->GetInternalParams(),
|
||||
'hidden_criteria' => $sAlreadyLinkedExpression,
|
||||
'submit_on_load' => false,
|
||||
]));
|
||||
|
||||
$oBlock->AddForm();
|
||||
}
|
||||
|
||||
@@ -650,6 +650,9 @@ class ActionEmail extends ActionNotification
|
||||
$aMessageContent['subject'] = 'TEST['.$aMessageContent['subject'].']';
|
||||
$aMessageContent['body'] = $sTestBody;
|
||||
$aMessageContent['to'] = $this->Get('test_recipient');
|
||||
// N°6677 Ensure emails in test are never sent to cc'd and bcc'd addresses
|
||||
$aMessageContent['cc'] = '';
|
||||
$aMessageContent['bcc'] = '';
|
||||
}
|
||||
// Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
|
||||
$aMessageContent['in_reply_to'] = $aMessageContent['references'];
|
||||
|
||||
@@ -219,6 +219,19 @@
|
||||
<field id="friendlyname" xsi:type="AttributeFriendlyName"/>
|
||||
</fields>
|
||||
</class>
|
||||
<class id="AuditDomain" _delta="define">
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
<properties>
|
||||
<category>application, grant_by_profile</category>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="name" xsi:type="AttributeString"/>
|
||||
<field id="description" xsi:type="AttributeString"/>
|
||||
<field id="icon" xsi:type="AttributeImage"/>
|
||||
<field id="categories_list" xsi:type="AttributeLinkedSet"/>
|
||||
<field id="friendlyname" xsi:type="AttributeFriendlyName"/>
|
||||
</fields>
|
||||
</class>
|
||||
<class id="Query" _delta="define">
|
||||
<!-- Generated by toolkit/export-class-to-meta.php -->
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventException;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Service\Events\EventServiceLog;
|
||||
use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager;
|
||||
|
||||
/**
|
||||
@@ -203,6 +205,8 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
const MAX_UPDATE_LOOP_COUNT = 10;
|
||||
|
||||
private $aEventListeners = [];
|
||||
|
||||
/**
|
||||
* DBObject constructor.
|
||||
*
|
||||
@@ -255,6 +259,10 @@ abstract class DBObject implements iDisplay
|
||||
$this->RegisterEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see RegisterCRUDListener
|
||||
* @see EventService::RegisterListener()
|
||||
*/
|
||||
protected function RegisterEventListeners()
|
||||
{
|
||||
}
|
||||
@@ -6181,6 +6189,51 @@ abstract class DBObject implements iDisplay
|
||||
return OPT_ATT_NORMAL;
|
||||
}
|
||||
|
||||
public final function GetListeners(): array
|
||||
{
|
||||
$aListeners = [];
|
||||
foreach ($this->aEventListeners as $aEventListener) {
|
||||
$aListeners = array_merge($aListeners, $aEventListener);
|
||||
}
|
||||
return $aListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for a specific event. The method to call will be saved in the object instance itself whereas calling {@see EventService::RegisterListener()} would
|
||||
* save a callable (thus the method name AND the whole DBObject instance)
|
||||
*
|
||||
* @param string $sEvent corresponding event
|
||||
* @param string $callback The callback method to call
|
||||
* @param float $fPriority optional priority for callback order
|
||||
* @param string $sModuleId
|
||||
*
|
||||
* @see EventService::RegisterListener()
|
||||
*
|
||||
* @since 3.1.0-3 3.1.1 3.2.0 N°6716
|
||||
*/
|
||||
final protected function RegisterCRUDListener(string $sEvent, string $callback, float $fPriority = 0.0, string $sModuleId = '')
|
||||
{
|
||||
$aEventCallbacks = $this->aEventListeners[$sEvent] ?? [];
|
||||
|
||||
$aEventCallbacks[] = array(
|
||||
'event' => $sEvent,
|
||||
'callback' => $callback,
|
||||
'priority' => $fPriority,
|
||||
'module' => $sModuleId,
|
||||
);
|
||||
usort($aEventCallbacks, function ($a, $b) {
|
||||
$fPriorityA = $a['priority'];
|
||||
$fPriorityB = $b['priority'];
|
||||
if ($fPriorityA == $fPriorityB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($fPriorityA < $fPriorityB) ? -1 : 1;
|
||||
});
|
||||
|
||||
$this->aEventListeners[$sEvent] = $aEventCallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sEvent
|
||||
* @param array $aEventData
|
||||
@@ -6192,15 +6245,53 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
public function FireEvent(string $sEvent, array $aEventData = array()): void
|
||||
{
|
||||
if (EventService::IsEventRegistered($sEvent)) {
|
||||
$aEventData['debug_info'] = 'from: '.get_class($this).':'.$this->GetKey();
|
||||
$aEventData['object'] = $this;
|
||||
$aEventSources = [$this->m_sObjectUniqId];
|
||||
foreach (MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL, false) as $sClass) {
|
||||
$aEventSources[] = $sClass;
|
||||
$aEventData['debug_info'] = 'from: '.get_class($this).':'.$this->GetKey();
|
||||
$aEventData['object'] = $this;
|
||||
|
||||
// Call local listeners first
|
||||
$aEventCallbacks = $this->aEventListeners[$sEvent] ?? [];
|
||||
$oFirstException = null;
|
||||
$sFirstExceptionMessage = '';
|
||||
foreach ($aEventCallbacks as $aEventCallback) {
|
||||
$oKPI = new ExecutionKPI();
|
||||
$sCallback = $aEventCallback['callback'];
|
||||
if (!method_exists($this, $sCallback)) {
|
||||
EventServiceLog::Error("Callback '".get_class($this).":$sCallback' does not exist");
|
||||
continue;
|
||||
}
|
||||
EventServiceLog::Debug("Fire event '$sEvent' calling '".get_class($this).":$sCallback'");
|
||||
try {
|
||||
call_user_func([$this, $sCallback], new EventData($sEvent, null, $aEventData));
|
||||
}
|
||||
catch (EventException $e) {
|
||||
EventServiceLog::Error("Event '$sEvent' for '$sCallback'} failed with blocking error: ".$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$sMessage = "Event '$sEvent' for '$sCallback'} failed with non-blocking error: ".$e->getMessage();
|
||||
EventServiceLog::Error($sMessage);
|
||||
if (is_null($oFirstException)) {
|
||||
$sFirstExceptionMessage = $sMessage;
|
||||
$oFirstException = $e;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$oKPI->ComputeStats('FireEvent', $sEvent);
|
||||
}
|
||||
EventService::FireEvent(new EventData($sEvent, $aEventSources, $aEventData));
|
||||
}
|
||||
if (!is_null($oFirstException)) {
|
||||
throw new Exception($sFirstExceptionMessage, $oFirstException->getCode(), $oFirstException);
|
||||
}
|
||||
|
||||
// Call global event listeners
|
||||
if (!EventService::IsEventRegistered($sEvent)) {
|
||||
return;
|
||||
}
|
||||
$aEventSources = [];
|
||||
foreach (MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL, false) as $sClass) {
|
||||
$aEventSources[] = $sClass;
|
||||
}
|
||||
EventService::FireEvent(new EventData($sEvent, $aEventSources, $aEventData));
|
||||
}
|
||||
|
||||
//////////////////
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
@@ -116,33 +116,50 @@ class Dict
|
||||
* @return string
|
||||
*/
|
||||
public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
|
||||
{
|
||||
$aInfo = self::GetLabelAndLangCode($sStringCode, $sDefault, $bUserLanguageOnly);
|
||||
return $aInfo['label'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localised string from the dictonary with its associated lang code
|
||||
*
|
||||
* @param string $sStringCode The code identifying the dictionary entry
|
||||
* @param string $sDefault Default value if there is no match in the dictionary
|
||||
* @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise
|
||||
*
|
||||
* @return array{
|
||||
* lang: string, label: string
|
||||
* } with localized label string and used lang code
|
||||
*/
|
||||
private static function GetLabelAndLangCode($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
|
||||
{
|
||||
// Attempt to find the string in the user language
|
||||
//
|
||||
$sLangCode = self::GetUserLanguage();
|
||||
self::InitLangIfNeeded($sLangCode);
|
||||
|
||||
if (!array_key_exists($sLangCode, self::$m_aData))
|
||||
if (! array_key_exists($sLangCode, self::$m_aData))
|
||||
{
|
||||
IssueLog::Warning("Cannot find $sLangCode in dictionnaries. default labels displayed");
|
||||
IssueLog::Warning("Cannot find $sLangCode in all registered dictionaries.");
|
||||
// It may happen, when something happens before the dictionaries get loaded
|
||||
return $sStringCode;
|
||||
return [ 'label' => $sStringCode, 'lang' => $sLangCode ];
|
||||
}
|
||||
$aCurrentDictionary = self::$m_aData[$sLangCode];
|
||||
if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary))
|
||||
{
|
||||
return $aCurrentDictionary[$sStringCode];
|
||||
return [ 'label' => $aCurrentDictionary[$sStringCode], 'lang' => $sLangCode ];
|
||||
}
|
||||
if (!$bUserLanguageOnly)
|
||||
{
|
||||
// Attempt to find the string in the default language
|
||||
//
|
||||
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
|
||||
|
||||
|
||||
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
|
||||
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
|
||||
{
|
||||
return $aDefaultDictionary[$sStringCode];
|
||||
return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => self::$m_sDefaultLanguage ];
|
||||
}
|
||||
// Attempt to find the string in english
|
||||
//
|
||||
@@ -151,17 +168,17 @@ class Dict
|
||||
$aDefaultDictionary = self::$m_aData['EN US'];
|
||||
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
|
||||
{
|
||||
return $aDefaultDictionary[$sStringCode];
|
||||
return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => 'EN US' ];
|
||||
}
|
||||
}
|
||||
// Could not find the string...
|
||||
//
|
||||
if (is_null($sDefault))
|
||||
{
|
||||
return $sStringCode;
|
||||
return [ 'label' => $sStringCode, 'lang' => null ];
|
||||
}
|
||||
|
||||
return $sDefault;
|
||||
return [ 'label' => $sDefault, 'lang' => null ];
|
||||
}
|
||||
|
||||
|
||||
@@ -177,19 +194,25 @@ class Dict
|
||||
*/
|
||||
public static function Format($sFormatCode /*, ... arguments ... */)
|
||||
{
|
||||
$sLocalizedFormat = self::S($sFormatCode);
|
||||
['label' => $sLocalizedFormat, 'lang' => $sLangCode] = self::GetLabelAndLangCode($sFormatCode);
|
||||
|
||||
$aArguments = func_get_args();
|
||||
array_shift($aArguments);
|
||||
|
||||
|
||||
if ($sLocalizedFormat == $sFormatCode)
|
||||
{
|
||||
// Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded)
|
||||
return $sFormatCode.' - '.implode(', ', $aArguments);
|
||||
}
|
||||
|
||||
return vsprintf($sLocalizedFormat, $aArguments);
|
||||
try{
|
||||
return vsprintf($sLocalizedFormat, $aArguments);
|
||||
} catch(\Throwable $e){
|
||||
\IssueLog::Error("Cannot format dict key", null, ["sFormatCode" => $sFormatCode, "sLangCode" => $sLangCode, 'exception_msg' => $e->getMessage() ]);
|
||||
return $sFormatCode.' - '.implode(', ', $aArguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize a the entries for a given language (replaces the former Add() method)
|
||||
* @param string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US'
|
||||
@@ -199,7 +222,7 @@ class Dict
|
||||
{
|
||||
self::$m_aData[$sLanguageCode] = $aEntries;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the list of available languages
|
||||
* @param hash $aLanguagesList
|
||||
@@ -260,7 +283,7 @@ class Dict
|
||||
{
|
||||
$sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php';
|
||||
require_once($sDictFile);
|
||||
|
||||
|
||||
if (self::GetApcService()->function_exists('apc_store')
|
||||
&& (self::$m_sApplicationPrefix !== null))
|
||||
{
|
||||
@@ -270,7 +293,7 @@ class Dict
|
||||
}
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enable caching (cached using APC)
|
||||
* @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance
|
||||
@@ -313,14 +336,14 @@ class Dict
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US')
|
||||
{
|
||||
$aMissing = array(); // Strings missing for the target language
|
||||
$aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary
|
||||
$aNotTranslated = array(); // Strings having the same value in both dictionaries
|
||||
$aOK = array(); // Strings having different values in both dictionaries
|
||||
|
||||
|
||||
foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue)
|
||||
{
|
||||
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode]))
|
||||
@@ -328,7 +351,7 @@ class Dict
|
||||
$aMissing[$sStringCode] = $sValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue)
|
||||
{
|
||||
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef]))
|
||||
@@ -351,7 +374,7 @@ class Dict
|
||||
}
|
||||
return array($aMissing, $aUnexpected, $aNotTranslated, $aOK);
|
||||
}
|
||||
|
||||
|
||||
public static function Dump()
|
||||
{
|
||||
MyHelpers::var_dump_html(self::$m_aData);
|
||||
@@ -374,7 +397,7 @@ class Dict
|
||||
// No need to actually load the strings since it's only used to know the list of languages
|
||||
// at setup time !!
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export all the dictionary entries - of the given language - whose code matches the given prefix
|
||||
* missing entries in the current language will be replaced by entries in the default language
|
||||
@@ -387,7 +410,7 @@ class Dict
|
||||
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
|
||||
$aEntries = array();
|
||||
$iLength = strlen($sStartingWith);
|
||||
|
||||
|
||||
// First prefill the array with entries from the default language
|
||||
foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry)
|
||||
{
|
||||
@@ -396,7 +419,7 @@ class Dict
|
||||
$aEntries[$sCode] = $sEntry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Now put (overwrite) the entries for the user language
|
||||
foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,8 @@ $ibo-dashlet-badge--icon--size: 48px !default;
|
||||
|
||||
$ibo-dashlet-badge--action-icon--margin-right: $ibo-spacing-300 !default;
|
||||
|
||||
$ibo-dashlet-badge--body--tooltip-title--margin-bottom: $ibo-spacing-500 !default;
|
||||
|
||||
/* CSS variables (can be changed directly from the browser) */
|
||||
:root {
|
||||
--ibo-dashlet-badge--min-width: #{$ibo-dashlet-badge--min-width};
|
||||
@@ -74,18 +76,27 @@ $ibo-dashlet-badge--action-icon--margin-right: $ibo-spacing-300 !default;
|
||||
@extend %ibo-hyperlink-inherited-colors;
|
||||
}
|
||||
}
|
||||
.ibo-dashlet-badge--action-list-count{
|
||||
margin-right: $ibo-dashlet-badge--action-list-count--margin-right;
|
||||
@extend %ibo-font-ral-bol-450;
|
||||
|
||||
.ibo-dashlet-badge--action-list-count {
|
||||
margin-right: $ibo-dashlet-badge--action-list-count--margin-right;
|
||||
@extend %ibo-font-ral-bol-450;
|
||||
}
|
||||
.ibo-dashlet-badge--action-list-label{
|
||||
display: inline-block;
|
||||
@extend %ibo-text-truncated-with-ellipsis;
|
||||
|
||||
.ibo-dashlet-badge--action-list-label {
|
||||
display: inline-block;
|
||||
@extend %ibo-text-truncated-with-ellipsis;
|
||||
}
|
||||
.ibo-dashlet-badge--action-create{
|
||||
@extend %ibo-baseline-centered-content;
|
||||
@extend %ibo-font-size-150;
|
||||
|
||||
.ibo-dashlet-badge--action-create {
|
||||
@extend %ibo-baseline-centered-content;
|
||||
@extend %ibo-font-size-150;
|
||||
}
|
||||
.ibo-dashlet-badge--action-create-icon{
|
||||
margin-right: $ibo-dashlet-badge--action-icon--margin-right;
|
||||
|
||||
.ibo-dashlet-badge--action-create-icon {
|
||||
margin-right: $ibo-dashlet-badge--action-icon--margin-right;
|
||||
}
|
||||
|
||||
.ibo-dashlet-badge--body--tooltip-title {
|
||||
@extend %ibo-font-weight-600;
|
||||
margin-bottom: $ibo-dashlet-badge--body--tooltip-title--margin-bottom;
|
||||
}
|
||||
|
||||
@@ -344,6 +344,7 @@ class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExt
|
||||
while ($oAttachment = $oSet->Fetch())
|
||||
{
|
||||
$oTempAttachment = clone $oAttachment;
|
||||
$oTempAttachment->Set('expire', time() + utils::GetConfig()->Get('draft_attachments_lifetime'));
|
||||
$oTempAttachment->Set('item_id', null);
|
||||
$oTempAttachment->Set('temp_id', $sTempId);
|
||||
$oTempAttachment->DBInsert();
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
<group id="Audit" _delta="define">
|
||||
<classes>
|
||||
<!-- This class list is also present in AdminTools group -->
|
||||
<class id="AuditDomain"/>
|
||||
<class id="AuditCategory"/>
|
||||
<class id="AuditRule"/>
|
||||
<class id="ResourceRunQueriesMenu"/>
|
||||
@@ -166,6 +167,7 @@
|
||||
<class id="URP_UserProfile"/>
|
||||
<class id="URP_Profiles"/>
|
||||
<!-- Audit group -->
|
||||
<class id="AuditDomain"/>
|
||||
<class id="AuditCategory"/>
|
||||
<class id="AuditRule"/>
|
||||
<!-- Query group -->
|
||||
|
||||
@@ -99,9 +99,9 @@ Dict::Add('FR FR', 'French', 'Français', array(
|
||||
'Class:Contract/Attribute:organization_name' => 'Nom client',
|
||||
'Class:Contract/Attribute:organization_name+' => 'Nom commun',
|
||||
'Class:Contract/Attribute:contacts_list' => 'Contacts',
|
||||
'Class:Contract/Attribute:contacts_list+' => 'Tous les contacts for ce contrat client',
|
||||
'Class:Contract/Attribute:contacts_list+' => 'Tous les contacts pour ce contrat client',
|
||||
'Class:Contract/Attribute:documents_list' => 'Documents',
|
||||
'Class:Contract/Attribute:documents_list+' => 'Tous les documents for ce contrat client',
|
||||
'Class:Contract/Attribute:documents_list+' => 'Tous les documents pour ce contrat client',
|
||||
'Class:Contract/Attribute:description' => 'Description',
|
||||
'Class:Contract/Attribute:description+' => '',
|
||||
'Class:Contract/Attribute:start_date' => 'Date de début',
|
||||
|
||||
@@ -157,6 +157,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
|
||||
'Class:ProviderContract/Attribute:contracttype_id+' => '',
|
||||
'Class:ProviderContract/Attribute:contracttype_name' => 'Vertragstyp-Name',
|
||||
'Class:ProviderContract/Attribute:contracttype_name+' => '',
|
||||
'Class:ProviderContract/Attribute:services_list' => 'Services',
|
||||
'Class:ProviderContract/Attribute:services_list+' => 'Alle für diesen Vertrag erworbenen Services',
|
||||
));
|
||||
|
||||
//
|
||||
|
||||
@@ -169,6 +169,8 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'Class:ProviderContract/Attribute:contracttype_id+' => '',
|
||||
'Class:ProviderContract/Attribute:contracttype_name' => 'Contract type name',
|
||||
'Class:ProviderContract/Attribute:contracttype_name+' => '',
|
||||
'Class:ProviderContract/Attribute:services_list' => 'Services',
|
||||
'Class:ProviderContract/Attribute:services_list+' => 'All the services purchased with this contract',
|
||||
));
|
||||
|
||||
//
|
||||
|
||||
@@ -157,6 +157,8 @@ Dict::Add('FR FR', 'French', 'Français', array(
|
||||
'Class:ProviderContract/Attribute:contracttype_id+' => '',
|
||||
'Class:ProviderContract/Attribute:contracttype_name' => 'Nom Type de contrat',
|
||||
'Class:ProviderContract/Attribute:contracttype_name+' => '',
|
||||
'Class:ProviderContract/Attribute:services_list' => 'Services',
|
||||
'Class:ProviderContract/Attribute:services_list+' => 'Tous les services achetés par ce contrat',
|
||||
));
|
||||
|
||||
//
|
||||
|
||||
@@ -837,7 +837,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:RunQuery:DevelopedOQLCount' => 'Developed OQL for count',
|
||||
'UI:RunQuery:ResultSQLCount' => 'Resulting SQL for count',
|
||||
'UI:RunQuery:ResultSQL' => 'Resulting SQL',
|
||||
'UI:RunQuery:Error' => 'An error occured while running the query',
|
||||
'UI:RunQuery:Error' => 'An error occured while running the query: %1$s',
|
||||
'UI:Query:UrlForExcel' => 'URL to use for MS-Excel web queries',
|
||||
'UI:Query:UrlV1' => 'The list of fields has been left unspecified. The page <em>export-V2.php</em> cannot be invoked without this information. Therefore, the URL suggested here below points to the legacy page: <em>export.php</em>. This legacy version of the export has the following limitation: the list of exported fields may vary depending on the output format and the data model of '.ITOP_APPLICATION_SHORT.'. <br/>Should you want to guarantee that the list of exported columns will remain stable on the long run, then you must specify a value for the attribute "Fields" and use the page <em>export-V2.php</em>.',
|
||||
'UI:Schema:Title' => ITOP_APPLICATION_SHORT.' objects schema',
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
|
||||
'Core:DeletedObjectLabel' => '%1s (törölve)',
|
||||
'Core:DeletedObjectLabel' => '%1$s (törölve)',
|
||||
'Core:DeletedObjectTip' => 'A %1$s objektum törölve (%2$s)',
|
||||
'Core:UnknownObjectLabel' => 'Objektum nem található (osztály: %1$s, id: %2$d)',
|
||||
'Core:UnknownObjectTip' => 'Az objektumot nem sikerült megtalálni. Lehet, hogy már törölték egy ideje, és a naplót azóta törölték.',
|
||||
|
||||
@@ -509,7 +509,7 @@ Reméljük, hogy ezt a verziót ugyanúgy kedvelni fogja, mint ahogy mi élvezt
|
||||
'UI:Error:2ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s és %2$s.',
|
||||
'UI:Error:3ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s, %2$s és %3$s.',
|
||||
'UI:Error:4ParametersMissing' => 'Hiba: a következő paramétereket meg kell adni ennél a műveletnél: %1$s, %2$s, %3$s és %4$s.',
|
||||
'UI:Error:IncorrectOQLQuery_Message' => 'Hiba: nem megfelelő OQL lekérdezés: %1$',
|
||||
'UI:Error:IncorrectOQLQuery_Message' => 'Hiba: nem megfelelő OQL lekérdezés: %1$s',
|
||||
'UI:Error:AnErrorOccuredWhileRunningTheQuery_Message' => 'Hiba történt a lekérdezés futtatása közben: %1$s',
|
||||
'UI:Error:ObjectAlreadyUpdated' => 'Hiba: az objketum már korábban módosításra került.',
|
||||
'UI:Error:ObjectCannotBeUpdated' => 'Hiba: az objektum nem frissíthető.',
|
||||
@@ -715,7 +715,7 @@ Reméljük, hogy ezt a verziót ugyanúgy kedvelni fogja, mint ahogy mi élvezt
|
||||
'UI:CSVReport-Value-Issue-Null' => 'A nulla nem engedélyezett',
|
||||
'UI:CSVReport-Value-Issue-NotFound' => 'Az objektum nincs meg',
|
||||
'UI:CSVReport-Value-Issue-FoundMany' => '%1$d egyezés található',
|
||||
'UI:CSVReport-Value-Issue-Readonly' => 'A \'%1$\'s attribútum csak olvasható (jelenlegi érték: %2$s, várható érték: %3$s)',
|
||||
'UI:CSVReport-Value-Issue-Readonly' => 'A \'%1$s attribútum csak olvasható (jelenlegi érték: %2$s, várható érték: %3$s)',
|
||||
'UI:CSVReport-Value-Issue-Format' => 'A bevitel feldolgozása sikertelen: %1$s',
|
||||
'UI:CSVReport-Value-Issue-NoMatch' => 'A \'%1$s\' attribútum nem várt értéket kapott: nincs egyezés, ellenőrizze a beírást',
|
||||
'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s~~',
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* @licence http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
Dict::Add('JA JP', 'Japanese', '日本語', array(
|
||||
'Core:DeletedObjectLabel' => '%1s (削除されました)',
|
||||
'Core:DeletedObjectLabel' => '%1$s (削除されました)',
|
||||
'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)',
|
||||
'Core:UnknownObjectLabel' => 'オブジェクトは見つかりません (クラス: %1$s, id: %2$d)',
|
||||
'Core:UnknownObjectTip' => 'オブジェクトは見つかりません。しばらく前に削除され、その後ログが削除されたかもしれません。',
|
||||
|
||||
@@ -915,7 +915,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => '%2$sクラスの%1$dオブジェクトの削除',
|
||||
'UI:Delete:CannotDeleteBecause' => '削除できません: %1$s',
|
||||
'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => '自動的に削除されるべきですが、出来ません。: %1$s',
|
||||
'UI:Delete:MustBeDeletedManuallyButNotPossible' => '手動で削除されるべきですが、出来ません。: %1$',
|
||||
'UI:Delete:MustBeDeletedManuallyButNotPossible' => '手動で削除されるべきですが、出来ません。: %1$s',
|
||||
'UI:Delete:WillBeDeletedAutomatically' => '自動的に削除されます。',
|
||||
'UI:Delete:MustBeDeletedManually' => '手動で削除されるべきです。',
|
||||
'UI:Delete:CannotUpdateBecause_Issue' => '自動的に更新されるべきですが、しかし: %1$s',
|
||||
@@ -1159,8 +1159,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'Enum:Undefined' => '未定義',
|
||||
'UI:DurationForm_Days_Hours_Minutes_Seconds' => '%1$s 日 %2$s 時 %3$s 分 %4$s 秒',
|
||||
'UI:ModifyAllPageTitle' => '全てを修正',
|
||||
'UI:Modify_ObjectsOf_Class' => 'Modifying objects of class %1$s~~',
|
||||
'UI:Modify_N_ObjectsOf_Class' => 'クラス%2$Sの%1$dオブジェクトを修正',
|
||||
'UI:Modify_N_ObjectsOf_Class' => 'クラス%2$sの%1$dオブジェクトを修正',
|
||||
'UI:Modify_M_ObjectsOf_Class_OutOf_N' => 'クラス%2$sの%3$d中%1$dを修正',
|
||||
'UI:Menu:ModifyAll' => '修正...',
|
||||
'UI:Menu:ModifyAll_Class' => 'Modify %1$s objects...~~',
|
||||
@@ -1180,7 +1179,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:BulkModify_Count_DistinctValues' => '%1$d 個の個別の値:',
|
||||
'UI:BulkModify:Value_Exists_N_Times' => '%1$s, %2$d 回存在',
|
||||
'UI:BulkModify:N_MoreValues' => '%1$d 個以上の値...',
|
||||
'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$にセットしょうとしています。',
|
||||
'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$sにセットしょうとしています。',
|
||||
'UI:FailedToApplyStimuli' => 'アクションは失敗しました。',
|
||||
'UI:StimulusModify_N_ObjectsOf_Class' => '%1$s: クラス%3$sの%2$dオブジェクトを修正',
|
||||
'UI:CaseLogTypeYourTextHere' => 'テキストを入力ください:',
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
*/
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', array(
|
||||
'Core:DeletedObjectLabel' => '%1ы (удален)',
|
||||
'Core:DeletedObjectLabel' => '%1$sы (удален)',
|
||||
'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)',
|
||||
'Core:UnknownObjectLabel' => 'Объект не найден (class: %1$s, id: %2$d)',
|
||||
'Core:UnknownObjectTip' => 'Объект не удается найти. Возможно, он был удален некоторое время назад, и журнал с тех пор был очищен.',
|
||||
|
||||
@@ -497,7 +497,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
||||
'UI:Error:MandatoryTemplateParameter_group_by' => 'Parameter group_by je povinný. Skontrolujte definíciu šablóny zobrazenia.',
|
||||
'UI:Error:InvalidGroupByFields' => 'Neplatný zoznam polí pre skupinu podľa: "%1$s".',
|
||||
'UI:Error:UnsupportedStyleOfBlock' => 'Chyba: nepodporovaný štýl bloku: "%1$s".',
|
||||
'UI:Error:IncorrectLinkDefinition_LinkedClass_Class' => 'Nesprávna definícia spojenia : trieda objektov na manažovanie : %l$s nebol nájdený ako externý kľúč v triede %2$s',
|
||||
'UI:Error:IncorrectLinkDefinition_LinkedClass_Class' => 'Nesprávna definícia spojenia : trieda objektov na manažovanie : %1$s nebol nájdený ako externý kľúč v triede %2$s',
|
||||
'UI:Error:Object_Class_Id_NotFound' => 'Objekt: %1$s:%2$d nebol nájdený.',
|
||||
'UI:Error:WizardCircularReferenceInDependencies' => 'Chyba: Cyklický odkaz v závislostiach medzi poliami, skontrolujte dátový model.',
|
||||
'UI:Error:UploadedFileTooBig' => 'Nahraný súbor je príliš veľký. (Max povolená veľkosť je %1$s). Ak chcete zmeniť tento limit, obráťte sa na správcu ITOP . (Skontrolujte, PHP konfiguráciu pre upload_max_filesize a post_max_size na serveri).',
|
||||
@@ -1301,7 +1301,7 @@ Keď sú priradené spúštačom, každej akcii je dané číslo "príkazu", šp
|
||||
'UI:DashletGroupBy:Prop-GroupBy:DayOfMonth' => 'Deň v mesiaci pre %1$s',
|
||||
'UI:DashletGroupBy:Prop-GroupBy:Select-Hour' => '%1$s (hodina)',
|
||||
'UI:DashletGroupBy:Prop-GroupBy:Select-Month' => '%1$s (mesiac)',
|
||||
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek' => '%1$ (deň v týžni)',
|
||||
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek' => '%1$s (deň v týžni)',
|
||||
'UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth' => '%1$s (deň v mesiaci)',
|
||||
'UI:DashletGroupBy:MissingGroupBy' => 'Prosím zvoľte pole na ktorom objekty budú zoskupené spolu',
|
||||
'UI:DashletGroupByPie:Label' => 'Koláčový graf',
|
||||
|
||||
@@ -278,7 +278,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
|
||||
'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s\'nin değeri %2$s olarak atandı (önceki değer: %3$s)',
|
||||
'Change:AttName_SetTo' => '%1$s\'nin değeri %2$s olarak atandı',
|
||||
'Change:Text_AppendedTo_AttName' => '%2$s\'ye %1$s eklendi',
|
||||
'Change:AttName_Changed_PreviousValue_OldValue' => '%1$\'nin değeri deiştirildi, önceki değer: %2$s',
|
||||
'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s nin değeri deiştirildi, önceki değer: %2$s',
|
||||
'Change:AttName_Changed' => '%1$s değiştirildi',
|
||||
'Change:AttName_EntryAdded' => '%1$s değiştirilmiş, yeni giriş eklendi.',
|
||||
'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s~~',
|
||||
|
||||
@@ -27,8 +27,8 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
|
||||
'UI:Layout:NavigationMenu:MenuFilter:Input:Hint' => 'As correspondências em todos os grupos de menus serão exibidas',
|
||||
'UI:Layout:NavigationMenu:MenuFilter:Placeholder:Hint' => 'Nenhum resultado para este filtro de menu',
|
||||
'UI:Layout:NavigationMenu:UserInfo:WelcomeMessage:Text' => 'Olá %1$s!',
|
||||
'UI:Layout:NavigationMenu:UserInfo:Picture:AltText' => 'Imagem do contato %1$',
|
||||
'UI:Layout:NavigationMenu:UserInfo:Picture:AltText' => 'Imagem do contato %1$s',
|
||||
'UI:Layout:NavigationMenu:UserMenu:Toggler:Label' => 'Abrir menu do usuário',
|
||||
'UI:Layout:NavigationMenu:KeyboardShortcut:FocusFilter' => 'Filtrar entradas de menu',
|
||||
|
||||
));
|
||||
));
|
||||
|
||||
@@ -117,6 +117,7 @@ $(function()
|
||||
locked_by_someone_else: 'locked_by_someone_else',
|
||||
},
|
||||
},
|
||||
release_lock_promise_resolve: null, // N°4494 - Resolve callback of the Promise used for the action following the log entry send, which must be done only once the lock is released
|
||||
|
||||
// the constructor
|
||||
_create: function () {
|
||||
@@ -500,7 +501,7 @@ $(function()
|
||||
{
|
||||
// Hide all filters' options only if click wasn't on one of them
|
||||
if(($(oEvent.target).closest(this.js_selectors.activity_filter_options_toggler).length === 0)
|
||||
&& $(oEvent.target).closest(this.js_selectors.activity_filter_options).length === 0) {
|
||||
&& $(oEvent.target).closest(this.js_selectors.activity_filter_options).length === 0) {
|
||||
this._HideAllFiltersOptions();
|
||||
}
|
||||
},
|
||||
@@ -947,9 +948,9 @@ $(function()
|
||||
|
||||
// Send request to server
|
||||
$.post(
|
||||
GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
|
||||
oParams,
|
||||
'json'
|
||||
GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
|
||||
oParams,
|
||||
'json'
|
||||
)
|
||||
.fail(function (oXHR, sStatus, sErrorThrown) {
|
||||
CombodoModal.OpenErrorModal(sErrorThrown);
|
||||
@@ -972,12 +973,24 @@ $(function()
|
||||
|
||||
// For now, we don't hide the forms as the user may want to add something else
|
||||
me.element.find(me.js_selectors.caselog_entry_form).trigger('clear_entry.caselog_entry_form.itop');
|
||||
|
||||
// Redirect to stimulus
|
||||
// - Convert undefined, null and empty string to null
|
||||
sStimulusCode = ((sStimulusCode ?? '') === '') ? null : sStimulusCode;
|
||||
if (null !== sStimulusCode) {
|
||||
window.location.href = GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=stimulus&class='+me._GetHostObjectClass()+'&id='+me._GetHostObjectID()+'&stimulus='+sStimulusCode;
|
||||
if (me.options.lock_enabled) {
|
||||
// Use a Promise to ensure that we redirect to the stimulus page ONLY when the lock is released, otherwise we might lock ourselves
|
||||
const oPromise = new Promise(function(resolve) {
|
||||
// Store the resolve callback so we can call it later from outside
|
||||
me.release_lock_promise_resolve = resolve;
|
||||
});
|
||||
oPromise.then(function () {
|
||||
window.location.href = GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=stimulus&class='+me._GetHostObjectClass()+'&id='+me._GetHostObjectID()+'&stimulus='+sStimulusCode;
|
||||
// Resolve callback is reinitialized in case the redirection fails for any reason and we might need to retry
|
||||
me.release_lock_promise_resolve = null;
|
||||
});
|
||||
} else {
|
||||
window.location.href = GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=stimulus&class='+me._GetHostObjectClass()+'&id='+me._GetHostObjectID()+'&stimulus='+sStimulusCode;
|
||||
}
|
||||
}
|
||||
})
|
||||
.always(function () {
|
||||
@@ -995,7 +1008,7 @@ $(function()
|
||||
_IncreaseTabTogglerMessagesCounter: function(sCaseLogAttCode){
|
||||
let oTabTogglerCounter = this._GetTabTogglerFromCaseLogAttCode(sCaseLogAttCode).find('[data-role="ibo-activity-panel--tab-title-messages-count"]');
|
||||
let iNewCounterValue = parseInt(oTabTogglerCounter.attr('data-messages-count')) + 1;
|
||||
|
||||
|
||||
oTabTogglerCounter.attr('data-messages-count', iNewCounterValue).text(iNewCounterValue);
|
||||
},
|
||||
/**
|
||||
@@ -1143,11 +1156,10 @@ $(function()
|
||||
else {
|
||||
oParams.operation = 'check_lock_state';
|
||||
}
|
||||
|
||||
$.post(
|
||||
this.options.lock_endpoint,
|
||||
oParams,
|
||||
'json'
|
||||
this.options.lock_endpoint,
|
||||
oParams,
|
||||
'json'
|
||||
)
|
||||
.fail(function (oXHR, sStatus, sErrorThrown) {
|
||||
// In case of HTTP request failure (not lock request), put the details in the JS console
|
||||
@@ -1196,6 +1208,9 @@ $(function()
|
||||
// Tried to release our lock
|
||||
else if ('release_lock' === oParams.operation) {
|
||||
sNewLockStatus = me.enums.lock_status.unknown;
|
||||
if (me.release_lock_promise_resolve !== null) {
|
||||
me.release_lock_promise_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
// Just checked if object was locked
|
||||
@@ -1430,9 +1445,9 @@ $(function()
|
||||
limit_results_length: bLimitResultsLength,
|
||||
};
|
||||
$.post(
|
||||
this.options.load_more_entries_endpoint,
|
||||
oParams,
|
||||
'json'
|
||||
this.options.load_more_entries_endpoint,
|
||||
oParams,
|
||||
'json'
|
||||
)
|
||||
.fail(function (oXHR, sStatus, sErroThrown) {
|
||||
CombodoModal.OpenErrorModal(sErrorThrown);
|
||||
|
||||
@@ -153,16 +153,15 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH
|
||||
"dataType": "html"
|
||||
})
|
||||
.done(function (data) {
|
||||
/* N°6152 - Hide during data loading and before open */
|
||||
$('#dlg_'+me.id).hide();
|
||||
$('#dlg_'+me.id).html(data);
|
||||
window[sPromiseId].then(function () {
|
||||
$('#dlg_'+me.id).dialog('open');
|
||||
me.UpdateSizes(null, null);
|
||||
if (me.bDoSearch)
|
||||
{
|
||||
if (me.bDoSearch) {
|
||||
me.SearchObjectsToAdd();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
$('#count_'+me.id).change(function () {
|
||||
let c = this.value;
|
||||
me.UpdateButtons(c);
|
||||
|
||||
@@ -105,7 +105,7 @@ if ($oFilter != null)
|
||||
$aExtraParams['action'] = utils::GetAbsoluteUrlAppRoot().'pages/UniversalSearch.php';
|
||||
$aExtraParams['table_id'] = '1';
|
||||
$aExtraParams['search_header_force_dropdown'] = $sSearchHeaderForceDropdown;
|
||||
//$aExtraParams['class'] = $sClassName;
|
||||
$aExtraParams['submit_on_load'] = false;
|
||||
$oBlock->Display($oP, 0, $aExtraParams);
|
||||
|
||||
// Search results
|
||||
|
||||
@@ -290,7 +290,6 @@ function DisplayEvents(WebPage $oPage, $sClass)
|
||||
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) {
|
||||
if (!MetaModel::IsAbstract($sChildClass)) {
|
||||
$oObject = MetaModel::NewObject($sChildClass);
|
||||
$aSources[] = $oObject->GetObjectUniqId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -299,7 +298,6 @@ function DisplayEvents(WebPage $oPage, $sClass)
|
||||
}
|
||||
} else {
|
||||
$oObject = MetaModel::NewObject($sClass);
|
||||
$aSources[] = $oObject->GetObjectUniqId();
|
||||
foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sParentClass) {
|
||||
$aSources[] = $sParentClass;
|
||||
}
|
||||
@@ -320,12 +318,19 @@ function DisplayEvents(WebPage $oPage, $sClass)
|
||||
});
|
||||
$aColumns = [
|
||||
'event' => ['label' => Dict::S('UI:Schema:Events:Event')],
|
||||
'listener' => ['label' => Dict::S('UI:Schema:Events:Listener')],
|
||||
'callback' => ['label' => Dict::S('UI:Schema:Events:Listener')],
|
||||
'priority' => ['label' => Dict::S('UI:Schema:Events:Rank')],
|
||||
'module' => ['label' => Dict::S('UI:Schema:Events:Module')],
|
||||
];
|
||||
// Get the object listeners first
|
||||
$aRows = [];
|
||||
$oReflectionClass = new ReflectionClass($sClass);
|
||||
if ($oReflectionClass->isInstantiable()) {
|
||||
/** @var DBObject $oClass */
|
||||
$oClass = new $sClass();
|
||||
$aRows = $oClass->GetListeners();
|
||||
}
|
||||
|
||||
foreach ($aListeners as $aListener) {
|
||||
if (is_object($aListener['callback'][0])) {
|
||||
$sListenerClass = $sClass;
|
||||
@@ -343,7 +348,7 @@ function DisplayEvents(WebPage $oPage, $sClass)
|
||||
}
|
||||
$aRows[] = [
|
||||
'event' => $aListener['event'],
|
||||
'listener' => $sListener,
|
||||
'callback' => $sListener,
|
||||
'priority' => $aListener['priority'],
|
||||
'module' => $aListener['module'],
|
||||
];
|
||||
|
||||
@@ -109,6 +109,7 @@ try
|
||||
$aExtraParams['action'] = utils::GetAbsoluteUrlAppRoot().'pages/tagadmin.php';
|
||||
$aExtraParams['table_id'] = '1';
|
||||
$aExtraParams['search_header_force_dropdown'] = $sSearchHeaderForceDropdown;
|
||||
$aExtraParams['submit_on_load'] = false;
|
||||
$oBlock->Display($oP, 0, $aExtraParams);
|
||||
|
||||
// Search results
|
||||
|
||||
@@ -1449,17 +1449,12 @@ EOF
|
||||
}
|
||||
$sMethods .= "\n $sCallbackFct\n\n";
|
||||
}
|
||||
if (strpos($sCallback, '::') === false) {
|
||||
$sEventListener = '[$this, \''.$sCallback.'\']';
|
||||
} else {
|
||||
$sEventListener = "'$sCallback'";
|
||||
}
|
||||
|
||||
$sListenerRank = (float)($oListener->GetChildText('rank', '0'));
|
||||
$sEvents .= <<<PHP
|
||||
|
||||
// listenerId = $sListenerId
|
||||
Combodo\iTop\Service\Events\EventService::RegisterListener("$sEventName", $sEventListener, \$this->m_sObjectUniqId, [], null, $sListenerRank, '$sModuleRelativeDir');
|
||||
\$this->RegisterCRUDListener("$sEventName", '$sCallback', $sListenerRank, '$sModuleRelativeDir');
|
||||
PHP;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\Dashlet;
|
||||
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\tJSRefreshCallback;
|
||||
use utils;
|
||||
|
||||
class DashletBadge extends DashletContainer
|
||||
{
|
||||
@@ -29,6 +30,11 @@ class DashletBadge extends DashletContainer
|
||||
protected $iCount;
|
||||
/** @var string */
|
||||
protected $sClassLabel;
|
||||
/**
|
||||
* @var string
|
||||
* @since 3.1.1 3.2.0
|
||||
*/
|
||||
protected $sClassDescription;
|
||||
|
||||
/** @var string */
|
||||
protected $sCreateActionUrl;
|
||||
@@ -62,6 +68,7 @@ class DashletBadge extends DashletContainer
|
||||
$this->sCreateActionUrl = $sCreateActionUrl;
|
||||
$this->sCreateActionLabel = $sCreateActionLabel;
|
||||
$this->aRefreshParams = $aRefreshParams;
|
||||
$this->sClassDescription = '';
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +192,37 @@ class DashletBadge extends DashletContainer
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @since 3.1.1 3.2.0
|
||||
*/
|
||||
public function GetClassDescription(): string
|
||||
{
|
||||
return $this->sClassDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClassDescription
|
||||
*
|
||||
* @return DashletBadge
|
||||
* @since 3.1.1 3.2.0
|
||||
*/
|
||||
public function SetClassDescription(string $sClassDescription)
|
||||
{
|
||||
$this->sClassDescription = $sClassDescription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @since 3.1.1
|
||||
*/
|
||||
public function HasClassDescription(): bool
|
||||
{
|
||||
return utils::IsNotNullOrEmptyString($this->sClassDescription);
|
||||
}
|
||||
|
||||
public function GetJSRefresh(): string
|
||||
{
|
||||
return "$('#".$this->sId."').block();
|
||||
|
||||
@@ -70,6 +70,9 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock
|
||||
/** @var AttributeLinkedSet $oAttDef attribute link set */
|
||||
protected AttributeLinkedSet $oAttDef;
|
||||
|
||||
/** @var bool $bIsAttEditable Is attribute editable */
|
||||
protected bool $bIsAttEditable;
|
||||
|
||||
/** @var string $sTargetClass links target classname */
|
||||
protected string $sTargetClass;
|
||||
|
||||
@@ -119,11 +122,12 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock
|
||||
private function Init()
|
||||
{
|
||||
$this->sTargetClass = $this->GetTargetClass();
|
||||
|
||||
$this->InitIsAttEditable();
|
||||
|
||||
// User rights
|
||||
$this->bIsAllowCreate = UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES;
|
||||
$this->bIsAllowModify = UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES;
|
||||
$this->bIsAllowDelete = UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES;
|
||||
$this->bIsAllowCreate = $this->bIsAttEditable && UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES;
|
||||
$this->bIsAllowModify = $this->bIsAttEditable && UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES;
|
||||
$this->bIsAllowDelete = $this->bIsAttEditable && UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +200,26 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock
|
||||
$this->AddSubBlock($oBlock->GetRenderContent($oPage, $this->GetExtraParam(), $this->sTableId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \CoreException
|
||||
*/
|
||||
private function InitIsAttEditable(): void
|
||||
{
|
||||
$iFlags = 0;
|
||||
|
||||
if ($this->oDbObject->IsNew())
|
||||
{
|
||||
$iFlags = $this->oDbObject->GetInitialStateAttributeFlags($this->sAttCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
$iFlags = $this->oDbObject->GetAttributeFlags($this->sAttCode);
|
||||
}
|
||||
|
||||
$this->bIsAttEditable = !($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE | OPT_ATT_HIDDEN));
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTableId.
|
||||
*
|
||||
|
||||
@@ -733,6 +733,7 @@ class AjaxRenderController
|
||||
} else {
|
||||
$oFullSetFilter = new DBObjectSearch($sRemoteClass);
|
||||
}
|
||||
$oFullSetFilter->SetShowObsoleteData(utils::ShowObsoleteData());
|
||||
$oWidget->DoAddObjects($oPage, $iMaxAddedId, $oFullSetFilter, $oObj);
|
||||
$oKPI->ComputeAndReport('Data write');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Closure;
|
||||
use Combodo\iTop\Service\Events\Description\EventDescription;
|
||||
use ContextTag;
|
||||
use CoreException;
|
||||
use DBObject;
|
||||
use Exception;
|
||||
use ExecutionKPI;
|
||||
use ReflectionClass;
|
||||
@@ -53,6 +54,12 @@ final class EventService
|
||||
/**
|
||||
* Register a callback for a specific event
|
||||
*
|
||||
* **Warning** : be ultra careful on memory footprint ! each callback will be saved in {@see aEventListeners}, and a callback is
|
||||
* made of the whole object instance and the method name ({@link https://www.php.net/manual/en/language.types.callable.php}).
|
||||
* For example to register on DBObject instances, you should better use {@see DBObject::RegisterCRUDListener()}
|
||||
*
|
||||
* @uses aEventListeners
|
||||
*
|
||||
* @api
|
||||
* @param string $sEvent corresponding event
|
||||
* @param callable $callback The callback to call
|
||||
@@ -61,8 +68,12 @@ final class EventService
|
||||
* @param array|string|null $context context filter
|
||||
* @param float $fPriority optional priority for callback order
|
||||
*
|
||||
* @return string Id of the registration
|
||||
* @return string registration identifier
|
||||
*
|
||||
* @see DBObject::RegisterCRUDListener() to register in DBObject instances instead, to reduce memory footprint (callback saving)
|
||||
*
|
||||
* @since 3.1.0 method creation
|
||||
* @since 3.1.0-3 3.1.1 3.2.0 N°6716 PHPDoc change to warn on memory footprint, and {@see DBObject::RegisterCRUDListener()} alternative
|
||||
*/
|
||||
public static function RegisterListener(string $sEvent, callable $callback, $sEventSource = null, array $aCallbackData = [], $context = null, float $fPriority = 0.0, $sModuleId = ''): string
|
||||
{
|
||||
|
||||
@@ -3,7 +3,16 @@
|
||||
{% apply spaceless %}
|
||||
<div class="ibo-dashlet-badge--body{% if oUIBlock.IsHidden() %} ibo-is-hidden{% endif %}" id="{{ oUIBlock.GetId() }}"
|
||||
data-role="ibo-dashlet-badge--body"
|
||||
data-tooltip-content="{{ oUIBlock.GetClassLabel() }}">
|
||||
{% if oUIBlock.HasClassDescription() %}
|
||||
{# Display both class name and description as the name could be truncated if too long #}
|
||||
data-tooltip-content="{{ '<div class="ibo-dashlet-badge--body--tooltip-title">'|escape }}{{ oUIBlock.GetClassLabel() }}{{ '</div><div class="ibo-dashlet-badge--body--tooltip-description">'|escape }}{{ oUIBlock.GetClassDescription() }}{{ '</div>'|escape }}"
|
||||
data-tooltip-html-enabled="true"
|
||||
{% else %}
|
||||
{# Display only class name as it could be truncated if too long #}
|
||||
data-tooltip-content="{{ oUIBlock.GetClassLabel() }}"
|
||||
{% endif %}
|
||||
{# Delay display to avoid having all tooltips appearing when mouse is just passing through the tabs #}
|
||||
data-tooltip-show-delay="300">
|
||||
<div class="ibo-dashlet-badge--icon-container">
|
||||
{# Mind the empty "alt" attribute https://www.w3.org/WAI/tutorials/images/decorative/ #}
|
||||
<img class="ibo-dashlet-badge--icon" src="{{ oUIBlock.GetClassIconUrl() }}" alt="">
|
||||
|
||||
@@ -23,16 +23,16 @@
|
||||
{% if aAction.confirmation is defined %}
|
||||
|
||||
// Prepare confirmation title
|
||||
let sTitle = `{{ 'UI:Datatables:RowActions:ConfirmationDialog'|dict_s|raw }}`;
|
||||
let sTitle = '{{ 'UI:Datatables:RowActions:ConfirmationDialog'|dict_s }}';
|
||||
{% if aAction.confirmation.title is defined %}
|
||||
sTitle = `{{ aAction.confirmation.title|dict_s|raw }}`;
|
||||
sTitle = '{{ aAction.confirmation.title|dict_s }}';
|
||||
{% endif %}
|
||||
sTitle = sTitle.replaceAll('{item}', aRowData['{{ aAction.confirmation.row_data }}']);
|
||||
|
||||
// Prepare confirmation message
|
||||
let sMessage = `{{ 'UI:Datatables:RowActions:ConfirmationMessage'|dict_s|raw }}`;
|
||||
let sMessage = '{{ 'UI:Datatables:RowActions:ConfirmationMessage'|dict_s }}';
|
||||
{% if aAction.confirmation.message is defined %}
|
||||
sMessage = `{{ aAction.confirmation.message|dict_s|raw }}`;
|
||||
sMessage = '{{ aAction.confirmation.message|dict_s }}';
|
||||
{% endif %}
|
||||
sMessage = sMessage.replaceAll('{item}', aRowData['{{ aAction.confirmation.row_data }}']);
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
[infra]
|
||||
php_version=7.4-apache
|
||||
; N°6629 perf bug on some tests on mariadb for now, so specifying MySQL
|
||||
db_version=5.7
|
||||
|
||||
[itop]
|
||||
itop_setup=tests/setup_params/default-params.xml
|
||||
itop_backup=tests/backups/backup-itop.tar.gz
|
||||
|
||||
@@ -87,6 +87,10 @@ Add annotation `@runInSeparateProcess`
|
||||
Each and every test case will run in a separate
|
||||
process.
|
||||
|
||||
Note : before N°6658 (3.0.4 / 3.1.1 / 3.2.0) we were also adding the `@backupGlobals disabled`
|
||||
and `@preserveGlobalState disabled` annotations. This is no longer necessary as the first has this default value
|
||||
already, and the second one is now set in iTopTestCase as a PHP class attribute.
|
||||
|
||||
#### At the test class level
|
||||
Add annotation `@runTestsInSeparateProcesses`
|
||||
Each and every test case in the class will run in a separate
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Integration;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
|
||||
class DictionariesConsistencyAfterSetupTest extends ItopTestCase
|
||||
{
|
||||
//used by testDictEntryValues
|
||||
//to filter false positive broken traductions
|
||||
private static $aLabelCodeNotToCheck = [
|
||||
//use of Dict::S not Format
|
||||
"UI:Audit:PercentageOk",
|
||||
|
||||
//unused dead labels
|
||||
"Class:DatacenterDevice/Attribute:redundancy/count",
|
||||
"Class:DatacenterDevice/Attribute:redundancy/disabled",
|
||||
"Class:DatacenterDevice/Attribute:redundancy/percent",
|
||||
"Class:TriggerOnThresholdReached/Attribute:threshold_index+"
|
||||
];
|
||||
|
||||
public function FormatProvider(){
|
||||
return [
|
||||
'key does not exist in dictionnary' => [
|
||||
'sTemplate' => null,
|
||||
'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1',
|
||||
],
|
||||
'traduction that breaks expected nb of arguments' => [
|
||||
'sTemplate' => 'toto %1$s titi %2$s',
|
||||
'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1',
|
||||
],
|
||||
'traduction ok' => [
|
||||
'sTemplate' => 'toto %1$s titi',
|
||||
'sExpectedTraduction' => 'toto 1 titi',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sTemplate : if null it will not create dict entry
|
||||
* @since 2.7.10 N°5491 - Inconsistent dictionary entries regarding arguments to pass to Dict::Format
|
||||
* @dataProvider FormatProvider
|
||||
*/
|
||||
public function testFormatWithOneArgumentAndCustomKey(?string $sTemplate, $sExpectedTranslation){
|
||||
//tricky way to mock GetLabelAndLangCode behavior via connected user language
|
||||
$sLangCode = \Dict::GetUserLanguage();
|
||||
$aDictByLang = $this->GetNonPublicStaticProperty(\Dict::class, 'm_aData');
|
||||
$sDictKey = 'ITOP::DICT:FORMAT:BROKEN:KEY';
|
||||
|
||||
if (! is_null($sTemplate)){
|
||||
$aDictByLang[$sLangCode][$sDictKey] = $sTemplate;
|
||||
}
|
||||
|
||||
$this->SetNonPublicStaticProperty(\Dict::class, 'm_aData', $aDictByLang);
|
||||
|
||||
$this->assertEquals($sExpectedTranslation, \Dict::Format($sDictKey, "1"));
|
||||
}
|
||||
|
||||
//test works after setup (no annotation @beforesetup)
|
||||
//even if it does not extend ItopDataTestCase
|
||||
private function ReadDictKeys($sLangCode) : array {
|
||||
\Dict::InitLangIfNeeded($sLangCode);
|
||||
|
||||
$aDictEntries = $this->GetNonPublicStaticProperty(\Dict::class, 'm_aData');
|
||||
return $aDictEntries[$sLangCode];
|
||||
}
|
||||
|
||||
/**
|
||||
* foreach dictionnary label map (key/value) it counts the number argument that should be passed to use Dict::Format
|
||||
* examples:
|
||||
* for "gabu zomeu" label there are no args
|
||||
* for "shadok %1 %2 %3" there are 3 args
|
||||
*
|
||||
* limitation: there is no validation check for "%3 itop %2 combodo" which seems unconsistent
|
||||
* @param $aDictEntry
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function GetKeyArgCountMap($aDictEntry) {
|
||||
$aKeyArgsCount = [];
|
||||
foreach ($aDictEntry as $sKey => $sValue){
|
||||
$aKeyArgsCount[$sKey] = $this->countArg($sValue);
|
||||
}
|
||||
ksort($aKeyArgsCount);
|
||||
return $aKeyArgsCount;
|
||||
}
|
||||
|
||||
private function countArg($sLabel) {
|
||||
$iMaxIndex = 0;
|
||||
if (preg_match_all("/%(\d+)/", $sLabel, $aMatches)){
|
||||
$aSubMatches = $aMatches[1];
|
||||
if (is_array($aSubMatches)){
|
||||
foreach ($aSubMatches as $aCurrentMatch){
|
||||
$iIndex = $aCurrentMatch;
|
||||
$iMaxIndex = ($iMaxIndex < $iIndex) ? $iIndex : $iMaxIndex;
|
||||
}
|
||||
}
|
||||
} else if ((false !== strpos($sLabel, "%s"))
|
||||
|| (false !== strpos($sLabel, "%d"))
|
||||
){
|
||||
$iMaxIndex = 1;
|
||||
}
|
||||
|
||||
return $iMaxIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: hardcoded list of languages
|
||||
* It is hard to have it dynamically via Dict::GetLanguages as for each lang Dict::Init should be called first
|
||||
**/
|
||||
public function LangCodeProvider(){
|
||||
return [
|
||||
'cs' => [ 'CS CZ' ],
|
||||
'da' => [ 'DA DA' ],
|
||||
'de' => [ 'DE DE' ],
|
||||
'en' => [ 'EN US' ],
|
||||
'es' => [ 'ES CR' ],
|
||||
'fr' => [ 'FR FR' ],
|
||||
'hu' => [ 'HU HU' ],
|
||||
'it' => [ 'IT IT' ],
|
||||
'ja' => [ 'JA JP' ],
|
||||
'nl' => [ 'NL NL' ],
|
||||
'pt' => [ 'PT BR' ],
|
||||
'ru' => [ 'RU RU' ],
|
||||
'sk' => [ 'SK SK' ],
|
||||
'tr' => [ 'TR TR' ],
|
||||
'zh' => [ 'ZH CN' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* compare en and other dictionaries and check that for all labels there is the same number of arguments
|
||||
* if not Dict::Format could raise an exception for some languages. translation should be done again...
|
||||
* @dataProvider LangCodeProvider
|
||||
*/
|
||||
public function testDictEntryValues($sLanguageCodeToTest)
|
||||
{
|
||||
$sReferenceLangCode = 'EN US';
|
||||
$aReferenceLangDictEntry = $this->ReadDictKeys($sReferenceLangCode);
|
||||
|
||||
$aDictEntry = $this->ReadDictKeys($sLanguageCodeToTest);
|
||||
|
||||
|
||||
$aKeyArgsCountMap = [];
|
||||
$aKeyArgsCountMap[$sReferenceLangCode] = $this->GetKeyArgCountMap($aReferenceLangDictEntry);
|
||||
//$aKeyArgsCountMap[$sCode] = $this->GetKeyArgCountMap($aDictEntry);
|
||||
|
||||
//set user language
|
||||
$this->SetNonPublicStaticProperty(\Dict::class, 'm_sCurrentLanguage', $sLanguageCodeToTest);
|
||||
|
||||
$aMismatchedKeys = [];
|
||||
|
||||
foreach ($aKeyArgsCountMap[$sReferenceLangCode] as $sKey => $iExpectedNbOfArgs){
|
||||
if (in_array($sKey, self::$aLabelCodeNotToCheck)){
|
||||
//false positive: do not test
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($sKey, $aDictEntry)){
|
||||
$aPlaceHolders = [];
|
||||
for ($i=0; $i<$iExpectedNbOfArgs; $i++){
|
||||
$aPlaceHolders[]=$i;
|
||||
}
|
||||
|
||||
$sLabelTemplate = $aDictEntry[$sKey];
|
||||
try{
|
||||
vsprintf($sLabelTemplate, $aPlaceHolders);
|
||||
} catch(\Throwable $e){
|
||||
$sError = $e->getMessage();
|
||||
if (array_key_exists($sError, $aMismatchedKeys)){
|
||||
$aMismatchedKeys[$sError][$sKey] = $iExpectedNbOfArgs;
|
||||
} else {
|
||||
$aMismatchedKeys[$sError] = [$sKey => $iExpectedNbOfArgs];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$iCount = 0;
|
||||
foreach ($aMismatchedKeys as $sError => $aKeys){
|
||||
var_dump($sError);
|
||||
foreach ($aKeys as $sKey => $iExpectedNbOfArgs) {
|
||||
$iCount++;
|
||||
if ($sReferenceLangCode === $sLanguageCodeToTest) {
|
||||
var_dump([
|
||||
'key label' => $sKey,
|
||||
'expected nb of expected args' => $iExpectedNbOfArgs,
|
||||
"key value in $sLanguageCodeToTest" => $aDictEntry[$sKey],
|
||||
]);
|
||||
} else {
|
||||
var_dump([
|
||||
'key label' => $sKey,
|
||||
'expected nb of expected args' => $iExpectedNbOfArgs,
|
||||
"key value in $sLanguageCodeToTest" => $aDictEntry[$sKey],
|
||||
"key value in $sReferenceLangCode" => $aReferenceLangDictEntry[$sKey],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sErrorMsg = sprintf("%s broken propertie(s) on $sLanguageCodeToTest dictionaries! either change the dict value in $sLanguageCodeToTest or add it in ignored label list (cf aLabelCodeNotToCheck)", $iCount);
|
||||
$this->assertEquals([], $aMismatchedKeys, $sErrorMsg);
|
||||
}
|
||||
|
||||
public function VsprintfProvider(){
|
||||
return [
|
||||
'not enough args' => [
|
||||
"sLabelTemplate" => "$1%s",
|
||||
"aPlaceHolders" => [],
|
||||
],
|
||||
'exact nb of args' => [
|
||||
"sLabelTemplate" => "$1%s",
|
||||
"aPlaceHolders" => ["1"],
|
||||
],
|
||||
'too much args' => [
|
||||
"sLabelTemplate" => "$1%s",
|
||||
"aPlaceHolders" => ["1", "2"],
|
||||
],
|
||||
'\"% ok\" without args' => [
|
||||
"sLabelTemplate" => "% ok",
|
||||
"aPlaceHolders" => [],
|
||||
],
|
||||
'\"% ok $1%s\" without args' => [
|
||||
"sLabelTemplate" => "% ok",
|
||||
"aPlaceHolders" => ['1'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider VsprintfProvider
|
||||
public function testVsprintf($sLabelTemplate, $aPlaceHolders){
|
||||
try{
|
||||
$this->markTestSkipped("usefull to check a specific PHP version behavior");
|
||||
vsprintf($sLabelTemplate, $aPlaceHolders);
|
||||
$this->assertTrue(true);
|
||||
} catch(\Throwable $e) {
|
||||
$this->assertTrue(false, "label \'" . $sLabelTemplate . " failed with " . var_export($aPlaceHolders, true) );
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
51
tests/php-unit-tests/perf-tests.xml.dist
Normal file
51
tests/php-unit-tests/perf-tests.xml.dist
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
|
||||
bootstrap="unittestautoload.php"
|
||||
backupGlobals="true"
|
||||
colors="true"
|
||||
columns="120"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnError="false"
|
||||
stopOnFailure="false"
|
||||
stopOnIncomplete="false"
|
||||
stopOnRisky="false"
|
||||
stopOnSkipped="false"
|
||||
verbose="true"
|
||||
printerClass="\Sempro\PHPUnitPrettyPrinter\PrettyPrinterForPhpUnit9"
|
||||
>
|
||||
|
||||
<extensions>
|
||||
<extension class="Combodo\iTop\Test\UnitTest\Hook\TestsRunStartHook" />
|
||||
</extensions>
|
||||
|
||||
<php>
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
<ini name="error_reporting" value="E_ALL"/>
|
||||
<ini name="display_errors" value="On"/>
|
||||
<ini name="log_errors" value="On"/>
|
||||
<ini name="html_errors" value="Off"/>
|
||||
<env name="PHPUNIT_PRETTY_PRINT_PROGRESS" value="true"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<!-- Unitary tests -->
|
||||
<testsuite name="Perf">
|
||||
<directory>perf-tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<!-- Code coverage white list -->
|
||||
<filter>
|
||||
<whitelist>
|
||||
<file>../../core/apc-emulation.php</file>
|
||||
<file>../../core/ormlinkset.class.inc.php</file>
|
||||
<file>../../datamodels/2.x/itop-tickets/main.itop-tickets.php</file>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
</phpunit>
|
||||
93
tests/php-unit-tests/perf-tests/BulkDBObjectTest.php
Normal file
93
tests/php-unit-tests/perf-tests/BulkDBObjectTest.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
class BulkDBObjectTest extends ItopDataTestCase
|
||||
{
|
||||
const CREATE_TEST_ORG = true;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testInsertAndReadPersonObjects()
|
||||
{
|
||||
echo "Part 1: Insert Persons\n";
|
||||
|
||||
$sOrgId = $this->getTestOrgId();
|
||||
$idx = 1;
|
||||
$fStart = microtime(true);
|
||||
$fMaxExecutionTimeAllowed = 40.0;
|
||||
$iInitialPeak = 0;
|
||||
$sInitialPeak = '';
|
||||
$fStartLoop = $fStart;
|
||||
for ($i = 0; $i < 3000; $i++) {
|
||||
$oPerson = $this->CreateObject(Person::class, ['org_id' => $sOrgId, 'name' => "Person_$i", 'first_name' => 'John']);
|
||||
if (0 == ($idx % 100)) {
|
||||
$fDuration = microtime(true) - $fStartLoop;
|
||||
$iMemoryPeakUsage = memory_get_peak_usage();
|
||||
if ($iInitialPeak === 0) {
|
||||
$iInitialPeak = $iMemoryPeakUsage;
|
||||
$sInitialPeak = \utils::BytesToFriendlyFormat($iInitialPeak, 4);
|
||||
}
|
||||
|
||||
$sCurrPeak = \utils::BytesToFriendlyFormat($iMemoryPeakUsage, 4);
|
||||
echo "$idx ".sprintf('%.1f ms', $fDuration * 1000)." - Peak Memory Usage: $sCurrPeak\n";
|
||||
$this->assertTrue($iMemoryPeakUsage === $iInitialPeak, "Peak memory changed from $sInitialPeak to $sCurrPeak after $idx insert loops"); $fStartLoop = microtime(true);
|
||||
|
||||
$fTotalDuration = microtime(true) - $fStart;
|
||||
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
|
||||
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed ($idx insert loops)");
|
||||
}
|
||||
$idx++;
|
||||
}
|
||||
|
||||
$fTotalDuration = microtime(true) - $fStart;
|
||||
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
|
||||
echo "Total duration: $sTotalDuration\n\n";
|
||||
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "Total execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed");
|
||||
|
||||
//////////////////////
|
||||
// Part 2 Fetch all the created persons
|
||||
echo "Part 1: Fetch Persons\n";
|
||||
|
||||
$oSearch = DBSearch::FromOQL('SELECT Person WHERE org_id=:org_id');
|
||||
$oSet = new DBObjectSet($oSearch, [], ['org_id' => $sOrgId]);
|
||||
$idx = 1;
|
||||
$iInitialPeak = 0;
|
||||
$sInitialPeak = '';
|
||||
$fMaxExecutionTimeAllowed = 0.5;
|
||||
$fStart = microtime(true);
|
||||
$fStartLoop = $fStart;
|
||||
while ($oContact = $oSet->Fetch()) {
|
||||
if (0 == ($idx % 100)) {
|
||||
$fDuration = microtime(true) - $fStartLoop;
|
||||
$iMemoryPeakUsage = memory_get_peak_usage();
|
||||
if ($iInitialPeak === 0) {
|
||||
$iInitialPeak = $iMemoryPeakUsage;
|
||||
$sInitialPeak = \utils::BytesToFriendlyFormat($iInitialPeak, 4);
|
||||
}
|
||||
|
||||
$sCurrPeak = \utils::BytesToFriendlyFormat($iMemoryPeakUsage, 4);
|
||||
echo "$idx ".sprintf('%.1f ms', $fDuration * 1000)." - Peak Memory Usage: $sCurrPeak\n";
|
||||
$this->assertTrue($iMemoryPeakUsage === $iInitialPeak, "Peak memory changed from $sInitialPeak to $sCurrPeak after $idx fetch loops");
|
||||
$fStartLoop = microtime(true);
|
||||
|
||||
$fTotalDuration = microtime(true) - $fStart;
|
||||
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
|
||||
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "Total execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed ($idx fetch loops)");
|
||||
}
|
||||
$idx++;
|
||||
}
|
||||
$fTotalDuration = microtime(true) - $fStart;
|
||||
$sTotalDuration = sprintf('%.3f s', $fTotalDuration);
|
||||
echo "Total duration: $sTotalDuration\n\n";
|
||||
$this->assertTrue($fTotalDuration < $fMaxExecutionTimeAllowed, "Total execution time $sTotalDuration should be < $fMaxExecutionTimeAllowed");
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@
|
||||
|
||||
<testsuites>
|
||||
<!-- Unitary tests -->
|
||||
<testsuite name="Perf">
|
||||
<directory>perf-tests</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Application">
|
||||
<directory>unitary-tests/application</directory>
|
||||
</testsuite>
|
||||
|
||||
@@ -16,9 +16,7 @@ namespace Combodo\iTop\Test\UnitTest;
|
||||
use ArchivedObjectException;
|
||||
use CMDBObject;
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Config;
|
||||
use Contact;
|
||||
use DBObject;
|
||||
use DBObjectSet;
|
||||
@@ -34,7 +32,6 @@ use MetaModel;
|
||||
use Person;
|
||||
use PluginManager;
|
||||
use Server;
|
||||
use SetupUtils;
|
||||
use TagSetFieldData;
|
||||
use Ticket;
|
||||
use URP_UserProfile;
|
||||
@@ -57,6 +54,7 @@ define('TAG_ATTCODE', 'domains');
|
||||
*
|
||||
* @since 2.7.7 3.0.1 3.1.0 N°4624 processIsolation is disabled by default and must be enabled in each test needing it (basically all tests using
|
||||
* iTop datamodel)
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6658 move some setUp/tearDown code to the corresponding methods *BeforeClass to speed up tests process time.
|
||||
*/
|
||||
abstract class ItopDataTestCase extends ItopTestCase
|
||||
{
|
||||
@@ -269,7 +267,9 @@ abstract class ItopDataTestCase extends ItopTestCase
|
||||
$oMyObj->DBInsert();
|
||||
$iKey = $oMyObj->GetKey();
|
||||
$this->debug("Created $sClass::$iKey");
|
||||
$this->aCreatedObjects[] = $oMyObj;
|
||||
if (!static::USE_TRANSACTION) {
|
||||
$this->aCreatedObjects[] = $oMyObj;
|
||||
}
|
||||
|
||||
return $oMyObj;
|
||||
}
|
||||
@@ -950,6 +950,9 @@ abstract class ItopDataTestCase extends ItopTestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6658 method creation
|
||||
*/
|
||||
protected function assertDBChangeOpCount(string $sClass, $iId, int $iExpectedCount)
|
||||
{
|
||||
$oSearch = new \DBObjectSearch('CMDBChangeOp');
|
||||
|
||||
@@ -10,14 +10,12 @@ use CMDBSource;
|
||||
use MySQLTransactionNotClosedException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SetupUtils;
|
||||
use const DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
|
||||
/**
|
||||
* Class ItopTestCase
|
||||
*
|
||||
* Helper class to extend for tests that DO NOT need to access the DataModel or the Database
|
||||
*
|
||||
* @author Eric Espie <eric.espie@combodo.com>
|
||||
* @package Combodo\iTop\Test\UnitTest
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6658 move some setUp/tearDown code to the corresponding methods *BeforeClass to speed up tests process time.
|
||||
*/
|
||||
abstract class ItopTestCase extends TestCase
|
||||
{
|
||||
@@ -25,7 +23,10 @@ abstract class ItopTestCase extends TestCase
|
||||
public static $DEBUG_UNIT_TEST = false;
|
||||
|
||||
/**
|
||||
* Override the default value to disable the backup of globals in case of tests run in a separate process
|
||||
* @link https://docs.phpunit.de/en/9.6/annotations.html#preserveglobalstate PHPUnit `preserveGlobalState` annotation documentation
|
||||
*
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6658 Override default value creation so that we don't need to add the annotation on each test classes that have runInSeparateProcess.
|
||||
* This parameter isn't used when test is run in the same process so ok to change it globally !
|
||||
*/
|
||||
protected $preserveGlobalState = false;
|
||||
|
||||
@@ -76,7 +77,9 @@ abstract class ItopTestCase extends TestCase
|
||||
|
||||
/**
|
||||
* @throws \MySQLTransactionNotClosedException see N°5538
|
||||
*
|
||||
* @since 2.7.8 3.0.3 3.1.0 N°5538
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6658 if transaction not closed, we are now doing a rollback
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
@@ -92,28 +95,20 @@ abstract class ItopTestCase extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper than can be called in the context of a data provider */
|
||||
/**
|
||||
* Helper than can be called in the context of a data provider
|
||||
*
|
||||
* @since 3.0.4 3.1.1 3.2.0 N°6658 method creation
|
||||
*/
|
||||
public static function GetAppRoot()
|
||||
{
|
||||
if (defined('APPROOT')) {
|
||||
return APPROOT;
|
||||
}
|
||||
$sSearchPath = __DIR__;
|
||||
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
|
||||
if (file_exists($sSearchPath.'/approot.inc.php')) {
|
||||
break;
|
||||
}
|
||||
$iOffsetSep = strrpos($sSearchPath, '/');
|
||||
if ($iOffsetSep === false) {
|
||||
$iOffsetSep = strrpos($sSearchPath, '\\');
|
||||
if ($iOffsetSep === false) {
|
||||
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
|
||||
return 'Could not find the approot file in '.$sSearchPath;
|
||||
}
|
||||
}
|
||||
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
|
||||
}
|
||||
return $sSearchPath.'/';
|
||||
|
||||
$sAppRootPath = static::GetFirstDirUpContainingFile(__DIR__, 'approot.inc.php');
|
||||
|
||||
return $sAppRootPath . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,6 +147,23 @@ abstract class ItopTestCase extends TestCase
|
||||
require_once $this->GetAppRoot() . $sFileRelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to load a module file. The caller test must be in that module !
|
||||
* Will browse dir up to find a module.*.php
|
||||
*
|
||||
* @param string $sFileRelPath for example 'portal/src/Helper/ApplicationHelper.php'
|
||||
* @since 2.7.10 3.1.1 3.2.0 N°6709 method creation
|
||||
*/
|
||||
protected function RequireOnceCurrentModuleFile(string $sFileRelPath): void
|
||||
{
|
||||
$aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
|
||||
$sCallerFileFullPath = $aStack[0]['file'];
|
||||
$sCallerDir = dirname($sCallerFileFullPath);
|
||||
|
||||
$sModuleRootPath = static::GetFirstDirUpContainingFile($sCallerDir, 'module.*.php');
|
||||
require_once $sModuleRootPath . $sFileRelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require once a unit test file (eg. a mock class) from its relative path from the *current* dir.
|
||||
* This ensure that required files don't crash when unit tests dir is moved in the iTop structure (see N°5608)
|
||||
@@ -169,6 +181,26 @@ abstract class ItopTestCase extends TestCase
|
||||
require_once $sCallerDirAbsPath . DIRECTORY_SEPARATOR . $sFileRelPath;
|
||||
}
|
||||
|
||||
private static function GetFirstDirUpContainingFile(string $sSearchPath, string $sFileToFindGlobPattern): ?string
|
||||
{
|
||||
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
|
||||
$aGlobFiles = glob($sSearchPath . '/' . $sFileToFindGlobPattern);
|
||||
if (is_array($aGlobFiles) && (count($aGlobFiles) > 0)) {
|
||||
return $sSearchPath . '/';
|
||||
}
|
||||
$iOffsetSep = strrpos($sSearchPath, '/');
|
||||
if ($iOffsetSep === false) {
|
||||
$iOffsetSep = strrpos($sSearchPath, '\\');
|
||||
if ($iOffsetSep === false) {
|
||||
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
|
||||
return 'Could not find the approot file in ' . $sSearchPath;
|
||||
}
|
||||
}
|
||||
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function debug($sMsg)
|
||||
{
|
||||
if (static::$DEBUG_UNIT_TEST) {
|
||||
@@ -243,9 +275,8 @@ abstract class ItopTestCase extends TestCase
|
||||
return $method->invokeArgs($oObject, $aArgs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @since 3.1.0
|
||||
* @since 2.7.10 3.1.0
|
||||
*/
|
||||
public function GetNonPublicStaticProperty(string $sClass, string $sProperty)
|
||||
{
|
||||
@@ -272,7 +303,7 @@ abstract class ItopTestCase extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0
|
||||
* @since 2.7.10 3.1.0
|
||||
*/
|
||||
private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty
|
||||
{
|
||||
@@ -298,7 +329,7 @@ abstract class ItopTestCase extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0
|
||||
* @since 2.7.10 3.1.0
|
||||
*/
|
||||
public function SetNonPublicStaticProperty(string $sClass, string $sProperty, $value)
|
||||
{
|
||||
@@ -378,4 +409,4 @@ abstract class ItopTestCase extends TestCase
|
||||
}
|
||||
closedir($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*/
|
||||
class ApplicationObjectExtensionTest extends \Combodo\iTop\Test\UnitTest\ItopDataTestCase
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use ContactType;
|
||||
use CoreException;
|
||||
use DBObject;
|
||||
use DBObject\MockDBObjectWithCRUDEventListener;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use lnkPersonToTeam;
|
||||
@@ -518,6 +519,24 @@ class CRUDEventTest extends ItopDataTestCase
|
||||
|
||||
$this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
|
||||
}
|
||||
|
||||
// Tests with MockDBObject
|
||||
public function testFireCRUDEvent()
|
||||
{
|
||||
$this->RequireOnceUnitTestFile('DBObject/MockDBObjectWithCRUDEventListener.php');
|
||||
|
||||
// For Metamodel list of classes
|
||||
MockDBObjectWithCRUDEventListener::Init();
|
||||
$oDBObject = new MockDBObjectWithCRUDEventListener();
|
||||
$oDBObject2 = new MockDBObjectWithCRUDEventListener();
|
||||
|
||||
$oDBObject->FireEvent(MockDBObjectWithCRUDEventListener::TEST_EVENT);
|
||||
|
||||
$this->assertNotNull($oDBObject->oEventDataReceived);
|
||||
$this->assertNull($oDBObject2->oEventDataReceived);
|
||||
|
||||
//echo($oDBObject->oEventDataReceived->Get('debug_info'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace DBObject;
|
||||
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use MetaModel;
|
||||
|
||||
class MockDBObjectWithCRUDEventListener extends \DBObject
|
||||
{
|
||||
const TEST_EVENT = 'test_event';
|
||||
public $oEventDataReceived = null;
|
||||
|
||||
public static function Init()
|
||||
{
|
||||
$aParams = array
|
||||
(
|
||||
'category' => 'bizmodel, searchable',
|
||||
'key_type' => 'autoincrement',
|
||||
'name_attcode' => '',
|
||||
'state_attcode' => '',
|
||||
'reconc_keys' => [],
|
||||
'db_table' => 'priv_unit_tests_mock',
|
||||
'db_key_field' => 'id',
|
||||
'db_finalclass_field' => '',
|
||||
'display_template' => '',
|
||||
'indexes' => [],
|
||||
);
|
||||
MetaModel::Init_Params($aParams);
|
||||
}
|
||||
|
||||
protected function RegisterEventListeners()
|
||||
{
|
||||
$this->RegisterCRUDListener(self::TEST_EVENT, 'TestEventCallback', 0, 'unit-test');
|
||||
}
|
||||
|
||||
public function TestEventCallback(EventData $oEventData)
|
||||
{
|
||||
$this->oEventDataReceived = $oEventData;
|
||||
}
|
||||
}
|
||||
@@ -941,4 +941,39 @@ class DBObjectTest extends ItopDataTestCase
|
||||
{
|
||||
return $this->aReloadCount[$sClass][$sKey] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0-3 3.1.1 3.2.0 N°6716 test creation
|
||||
*/
|
||||
public function testConstructorMemoryFootprint():void
|
||||
{
|
||||
$idx = 1;
|
||||
$fStart = microtime(true);
|
||||
$fStartLoop = $fStart;
|
||||
$iInitialPeak = 0;
|
||||
|
||||
for ($i = 0; $i < 5000; $i++) {
|
||||
/** @noinspection PhpUnusedLocalVariableInspection We intentionally use a reference that will disappear on each loop */
|
||||
$oPerson = MetaModel::NewObject(\Person::class);
|
||||
if (0 == ($idx % 100)) {
|
||||
$fDuration = microtime(true) - $fStartLoop;
|
||||
$iMemoryPeakUsage = memory_get_peak_usage();
|
||||
if ($iInitialPeak === 0) {
|
||||
$iInitialPeak = $iMemoryPeakUsage;
|
||||
$sInitialPeak = \utils::BytesToFriendlyFormat($iInitialPeak, 4);
|
||||
}
|
||||
|
||||
$sCurrPeak = \utils::BytesToFriendlyFormat($iMemoryPeakUsage, 4);
|
||||
echo "$idx ".sprintf('%.1f ms', $fDuration * 1000)." - Peak Memory Usage: $sCurrPeak\n";
|
||||
|
||||
$this->assertTrue($iMemoryPeakUsage === $iInitialPeak, "Peak memory changed from $sInitialPeak to $sCurrPeak after $i loops");
|
||||
|
||||
$fStartLoop = microtime(true);
|
||||
}
|
||||
$idx++;
|
||||
}
|
||||
|
||||
$fTotalDuration = microtime(true) - $fStart;
|
||||
echo 'Total duration: '.sprintf('%.3f s', $fTotalDuration)."\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,13 @@
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use MockValueSetObjects;
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
use MetaModel;
|
||||
use ValueSetDefinition;
|
||||
use ValueSetEnum;
|
||||
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*/
|
||||
class ValueSetDefinitionTest extends ItopTestCase
|
||||
{
|
||||
|
||||
@@ -14,8 +14,6 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*/
|
||||
class TemporaryObjectManagerTest extends ItopDataTestCase
|
||||
{
|
||||
|
||||
@@ -15,8 +15,6 @@ use DBObject;
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*/
|
||||
class TemporaryObjectRepositoryTest extends ItopDataTestCase
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user