Merge remote-tracking branch 'origin/support/3.2' into develop

This commit is contained in:
Eric Espie
2024-03-01 09:44:24 +01:00
27 changed files with 479 additions and 138 deletions

View File

@@ -304,8 +304,8 @@
</event_datum>
</event_data>
</event>
<event id="EVENT_DB_BEFORE_APPLY_STIMULUS" _delta="define">
<description>A stimulus is about to be applied to an object</description>
<event id="EVENT_ENUM_TRANSITIONS" _delta="define">
<description>Manage the allowed transitions in current object state. The only action allowed is to deny transitions with DBObject::DenyTransition()</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
@@ -314,89 +314,9 @@
<description>The object where the stimulus is targeted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="stimulus">
<description>Current stimulus applied</description>
<type>string</type>
</event_datum>
<event_datum id="previous_state">
<description>Object previous state</description>
<type>string</type>
</event_datum>
<event_datum id="new_state">
<description>Object new state</description>
<type>string</type>
</event_datum>
<event_datum id="save_object">
<description>The object must be saved in the database</description>
<type>boolean</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_DB_AFTER_APPLY_STIMULUS" _delta="define">
<description>A stimulus has been applied to an object</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object where the stimulus is targeted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="stimulus">
<description>Current stimulus applied</description>
<type>string</type>
</event_datum>
<event_datum id="previous_state">
<description>Object previous state</description>
<type>string</type>
</event_datum>
<event_datum id="new_state">
<description>Object new state</description>
<type>string</type>
</event_datum>
<event_datum id="save_object">
<description>The object is asked to be saved in the database</description>
<type>boolean</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_DB_APPLY_STIMULUS_FAILED" _delta="define">
<description>A stimulus has failed</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="action">
<description>The action that failed to apply the stimulus</description>
<type>string</type>
</event_datum>
<event_datum id="object">
<description>The object where the stimulus is targeted</description>
<type>DBObject</type>
</event_datum>
<event_datum id="stimulus">
<description>Current stimulus applied</description>
<type>string</type>
</event_datum>
<event_datum id="previous_state">
<description>Object previous state</description>
<type>string</type>
</event_datum>
<event_datum id="new_state">
<description>Object new state</description>
<type>string</type>
</event_datum>
<event_datum id="save_object">
<description>The object must be saved in the database</description>
<type>boolean</type>
<event_datum id="allowed_stimuli">
<description>The list of available stimuli in the current state</description>
<type>array</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>

View File

@@ -81,6 +81,7 @@ abstract class Action extends cmdbAbstractObject
MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("trigger_list",
array("linked_class" => "lnkTriggerAction", "ext_key_to_me" => "action_id", "ext_key_to_remote" => "trigger_id", "allowed_values" => null, "count_min" => 0, "count_max" => 0, "depends_on" => array(), "display_style" => 'property')));
MetaModel::Init_AddAttribute(new AttributeEnum("asynchronous", array("allowed_values" => new ValueSetEnum(['use_global_setting' => 'Use global settings','yes' => 'Yes' ,'no' => 'No']), "sql" => "asynchronous", "default_value" => 'use_global_setting', "is_null_allowed" => false, "depends_on" => array())));
// Display lists
// - Attributes to be displayed for the complete details
@@ -196,7 +197,20 @@ abstract class Action extends cmdbAbstractObject
}
/**
* @throws InvalidConfigParamException
* @param \Combodo\iTop\Application\WebPage\WebPage $oPage
*
* @throws \ApplicationException
* @throws \ArchivedObjectException
* @throws \ConfigException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \DictExceptionMissingString
* @throws \InvalidConfigParamException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
* @throws \ReflectionException
* @since 3.2.0 N°5472 method creation
*/
public function GetLastExecutionsTabContent(WebPage $oPage): void
@@ -227,6 +241,32 @@ abstract class Action extends cmdbAbstractObject
$oPage->AddUiBlock($oExecutionsListBlock);
}
/**
* Will be overloaded by the children classes to return the value of their global asynchronous setting (eg. `email_asynchronous` for `\ActionEmail`, `prefer_asynchronous` for `\ActionWebhook`, ...)
*
* @return bool true if the global setting for this kind of action if to be executed asynchronously, false otherwise.
* @since 3.2.0
*/
public static function GetAsynchronousGlobalSetting(): bool
{
return false;
}
/**
* @return bool true if that action instance should be executed asynchronously, otherwise false
* @throws \ArchivedObjectException
* @throws \CoreException
* @since 3.2.0
*/
public function IsAsynchronous(): bool
{
$sAsynchronous = $this->Get('asynchronous');
if ($sAsynchronous === 'use_global_setting') {
return static::GetAsynchronousGlobalSetting();
}
return $sAsynchronous === 'yes';
}
}
/**
@@ -381,6 +421,7 @@ class ActionEmail extends ActionNotification
),
'fieldset:ActionEmail:trigger' => array(
0 => 'trigger_list',
1 => 'asynchronous'
),
),
'col:col2' => array(
@@ -616,7 +657,7 @@ class ActionEmail extends ActionNotification
else
{
$aErrors = [];
$iRes = $oEmail->Send($aErrors, false, $oLog); // allow asynchronous mode
$iRes = $oEmail->Send($aErrors, $this->IsAsynchronous() ? Email::ENUM_SEND_FORCE_ASYNCHRONOUS : Email::ENUM_SEND_FORCE_SYNCHRONOUS, $oLog);
switch ($iRes)
{
case EMAIL_SEND_OK:
@@ -877,4 +918,13 @@ class ActionEmail extends ActionNotification
}
}
}
/**
* @inheritDoc
* @since 3.2.0
*/
public static function GetAsynchronousGlobalSetting(): bool
{
return utils::GetConfig()->Get('email_asynchronous');
}
}

View File

@@ -15,6 +15,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Service\Notification\Event\EventiTopNotificationService;
/**
@@ -457,3 +458,87 @@ class AsyncSendEmail extends AsyncTask
return '';
}
}
/**
* An async notification to be sent to iTop users
* @since 3.2.0
*/
class AsyncSendiTopNotifications extends AsyncTask {
public static function Init()
{
$aParams = array
(
"category" => "core/cmdb",
"key_type" => "autoincrement",
"name_attcode" => "created",
"state_attcode" => "",
"reconc_keys" => array(),
"db_table" => "priv_async_send_itop_notifications",
"db_key_field" => "id",
"db_finalclass_field" => "",
);
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
MetaModel::Init_AddAttribute(new AttributeText("recipients", array("allowed_values"=>null, "sql"=>"recipients", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "allowed_values"=>null, "sql"=>"action_id", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "allowed_values"=>null, "sql"=>"trigger_id", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("title", array("allowed_values"=>null, "sql"=>"title", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values"=>null, "sql"=>"object_id", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeString("object_class", array("allowed_values"=>null, "sql"=>"object_class", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeText("url", array("allowed_values"=>null, "sql"=>"url", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
}
/**
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*/
public static function AddToQueue(int $iActionId, int $iTriggerId, array $aRecipients, string $sMessage, string $sTitle, string $sUrl, int $iObjectId, ?string $sObjectClass): void
{
$oNew = new static();
$oNew->Set('action_id', $iActionId);
$oNew->Set('trigger_id', $iTriggerId);
$oNew->Set('recipients', json_encode($aRecipients));
$oNew->Set('message', $sMessage);
$oNew->Set('title', $sTitle);
$oNew->Set('url', $sUrl);
$oNew->Set('object_id', $iObjectId);
$oNew->Set('object_class', $sObjectClass);
$oNew->SetCurrentDate('date');
$oNew->DBInsert();
}
/**
* @inheritDoc
*/
public function DoProcess()
{
$oAction = MetaModel::GetObject('Action', $this->Get('action_id'));
$iTriggerId = $this->Get('trigger_id');
$aRecipients = json_decode($this->Get('recipients'));
$sMessage = $this->Get('message');
$sTitle = $this->Get('title');
$sUrl = $this->Get('url');
$iObjectId = $this->Get('object_id');
$sObjectClass = $this->Get('object_class');
$sDate = $this->Get('date');
foreach ($aRecipients as $iRecipientId)
{
$oEvent = EventiTopNotificationService::MakeEventFromAction($oAction, $iRecipientId, $iTriggerId, $sMessage, $sTitle, $sUrl, $iObjectId, $sObjectClass, $sDate);
$oEvent->DBInsertNoReload();
}
return "Sent";
}
}

View File

@@ -1619,6 +1619,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'notifications.itop.send_asynchronously' => [
'type' => 'bool',
'description' => 'If true then iTop notifications will be sent asynchronously',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'notifications.itop.newsroom_cache_time' => [
'type' => 'integer',
'description' => 'Duration in min between each fetch for notifications in newsroom',

View File

@@ -193,6 +193,9 @@
<item id="trigger_list">
<rank>10</rank>
</item>
<item id="asynchronous">
<rank>20</rank>
</item>
</items>
</item>
</items>
@@ -258,7 +261,23 @@
$oRecipientsSearch = DBObjectSearch::FromOQL($this->Get('recipients'));
$oRecipientsSearch->AllowAllData();
$oRecipientsSet = new DBObjectSet($oRecipientsSearch);
$bIsAsync = $this->IsAsynchronous();
[$sPreviousLanguage, $aPreviousPluginProperties] = $this->SetNotificationLanguage();
if($bIsAsync === true){
$aRecipients = [];
}
$sMessage = MetaModel::ApplyParams($this->Get('message'), $aContextArgs);
$sTitle = MetaModel::ApplyParams($this->Get('title'), $aContextArgs);
$sUrl = MetaModel::ApplyParams($this->Get('url'), $aContextArgs);
$iObjectId = 0;
$sObjectClass = null;
if (array_key_exists('this->object()', $aContextArgs)) {
$iObjectId = $aContextArgs['this->object()']->GetKey();
$sObjectClass = get_class($aContextArgs['this->object()']);
}
while ($oRecipient = $oRecipientsSet->Fetch()) {
// Skip recipients that have no users
if (get_class($oRecipient) === Person::class && UserRights::GetUserFromPerson($oRecipient) === null) {
@@ -267,45 +286,32 @@
if (!\Combodo\iTop\Service\Notification\NotificationsService::GetInstance()->IsSubscribed($oTrigger, $this, $oRecipient)) {
continue;
}
if (array_key_exists('this->object()', $aContextArgs)) {
$iObjectId = $aContextArgs['this->object()']->GetKey();
$sObjectClass = get_class($aContextArgs['this->object()']);
} else {
$iObjectId = 0;
$sObjectClass = null;
}
$oEvent = new EventiTopNotification();
$oEvent->Set('title', MetaModel::ApplyParams($this->Get('title'), $aContextArgs));
$oEvent->Set('message', MetaModel::ApplyParams($this->Get('message'), $aContextArgs));
// Compute icon
// - First check if one is defined on the action
if (false === $this->Get('icon')->IsEmpty()) {
$oIcon = $this->Get('icon');
}
// - Then, check if the action is for a DM object and if its class has an icon
elseif ($iObjectId > 0 && utils::IsNotNullOrEmptyString(MetaModel::GetClassIcon($sObjectClass, false))) {
$oIcon = MetaModel::GetAttributeDef(EventiTopNotification::class, 'icon')->MakeRealValue(MetaModel::GetClassIcon($sObjectClass, false), $oEvent);
}
// - Otherwise, fallback on the compact logo of the application
else {
$oIcon = MetaModel::GetAttributeDef(EventiTopNotification::class, 'icon')->MakeRealValue(\Combodo\iTop\Application\Branding::GetCompactMainLogoAbsoluteUrl(), $oEvent);
}
$oEvent->Set('icon', $oIcon);
$oEvent->Set('priority', $this->Get('priority'));
$oEvent->Set('contact_id', $oRecipient->GetKey());
$oEvent->Set('trigger_id', $oTrigger->GetKey());
$oEvent->Set('action_id', $this->GetKey());
$iObjectId = array_key_exists('this->object()', $aContextArgs) ? $aContextArgs['this->object()']->GetKey() : 0;
$oEvent->Set('object_id', $iObjectId);
$oEvent->Set('url', MetaModel::ApplyParams($this->Get('url'), $aContextArgs));
$oEvent->DBInsertNoReload();
if($bIsAsync === true) {
$aRecipients[] = $oRecipient->GetKey();
} else {
$oEvent = Combodo\iTop\Service\Notification\Event\EventiTopNotificationService::MakeEventFromAction($this, $oRecipient->GetKey(), $oTrigger->GetKey(), $sMessage, $sTitle, $sUrl, $iObjectId, $sObjectClass);
$oEvent->DBInsertNoReload();
}
\Combodo\iTop\Service\Notification\NotificationsService::GetInstance()->RegisterSubscription($oTrigger, $this, $oRecipient);
}
if ($bIsAsync === true) {
AsyncSendiTopNotifications::AddToQueue($this->GetKey(), $oTrigger->GetKey(), $aRecipients, $sMessage, $sTitle, $sUrl, $iObjectId, $sObjectClass);
}
$this->SetNotificationLanguage($sPreviousLanguage, $aPreviousPluginProperties['language_code'] ?? null);
}
]]></code>
</method>
<method id="GetAsynchronousGlobalSetting">
<comment></comment>
<static>true</static>
<access>public</access>
<code><![CDATA[
public static function GetAsynchronousGlobalSetting(): bool
{
return utils::GetConfig()->Get('notifications.itop.send_asynchronously');
}
]]></code>
</method>
</methods>

View File

@@ -210,6 +210,7 @@ abstract class DBObject implements iDisplay
const MAX_UPDATE_LOOP_COUNT = 10;
private $aEventListeners = [];
private array $aAllowedTransitions = [];
/**
* DBObject constructor.
@@ -4404,9 +4405,32 @@ abstract class DBObject implements iDisplay
]);
}
$this->aAllowedTransitions = $aSortedTransitions;
$this->FireEvent(EVENT_ENUM_TRANSITIONS, ['allowed_stimuli' => array_keys($aSortedTransitions)]);
$aSortedTransitions = $this->aAllowedTransitions;
$this->aAllowedTransitions = [];
return $aSortedTransitions;
}
/**
* Remove a transition for a specific stimulus.
* This is only usable by EVENT_ENUM_TRANSITIONS listeners in order
* to manage the allowed transitions in the current object state.
*
* @param string $sStimulus
*
* @return void
* @api
* @since 3.1.2
*/
public function DenyTransition(string $sStimulus): void
{
if (isset($this->aAllowedTransitions[$sStimulus])) {
unset($this->aAllowedTransitions[$sStimulus]);
}
}
/**
* Helper to reset a stop-watch
* Suitable for use as a lifecycle action
@@ -4492,14 +4516,6 @@ abstract class DBObject implements iDisplay
$sNewState = $aTransitionDef['target_state'];
$this->Set($sStateAttCode, $sNewState);
$aEventData = [
'stimulus' => $sStimulusCode,
'previous_state' => $sPreviousState,
'new_state' => $sNewState,
'save_object' => !$bDoNotWrite,
];
$this->FireEvent(EVENT_DB_BEFORE_APPLY_STIMULUS, $aEventData);
// $aTransitionDef is an
// array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD
@@ -4584,8 +4600,6 @@ abstract class DBObject implements iDisplay
if (!$bDoNotWrite) {
$this->DBWrite();
}
$this->FireEvent(EVENT_DB_AFTER_APPLY_STIMULUS, $aEventData);
}
else
{
@@ -4594,8 +4608,6 @@ abstract class DBObject implements iDisplay
{
$this->m_aCurrValues[$sAttCode] = $aBackupValues[$sAttCode];
}
$aEventData['action'] = $sActionDesc;
$this->FireEvent(EVENT_DB_APPLY_STIMULUS_FAILED, $aEventData);
}
return $bSuccess;
}

View File

@@ -456,6 +456,11 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Související triggery',
'Class:Action/Attribute:trigger_list+' => 'Triggery spojené s touto akcí',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Typ',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -454,6 +454,11 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Relaterede Triggere',
'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action~~',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Type',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -453,6 +453,11 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Zugehörige Trigger',
'Class:Action/Attribute:trigger_list+' => 'Trigger, die mit dieser Aktion verknüpft sind',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Typ',
'Class:Action/Attribute:finalclass+' => 'Name der instanziierbaren Klasse',
'Action:WarningNoTriggerLinked' => 'Warnung, es ist kein Trigger mit dieser Aktion verknüpft. Die Aktion ist nicht aktiv solange nicht mindestens 1 Trigger verknüpft ist.',

View File

@@ -536,6 +536,11 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Related Triggers',
'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action',
'Class:Action/Attribute:asynchronous' => 'Asynchronous',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes',
'Class:Action/Attribute:asynchronous/Value:no' => 'No',
'Class:Action/Attribute:finalclass' => 'Action sub-class',
'Class:Action/Attribute:finalclass+' => 'Name of the final class',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.',

View File

@@ -455,6 +455,11 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
'Class:Action/Attribute:status/Value:disabled+' => 'Inactivo',
'Class:Action/Attribute:trigger_list' => 'Disparadores Relacionados',
'Class:Action/Attribute:trigger_list+' => 'Disparadores Asociados a esta Acción',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Clase',
'Class:Action/Attribute:finalclass+' => 'Clase',
'Action:WarningNoTriggerLinked' => 'Advertencia, ningún disparador está ligado a esta acción. No se activara hasta que tenga al menos una acción.',

View File

@@ -488,6 +488,11 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Déclencheurs liés',
'Class:Action/Attribute:trigger_list+' => 'Déclencheurs à l\'origine de cette action',
'Class:Action/Attribute:asynchronous' => 'Asynchrone',
'Class:Action/Attribute:asynchronous+' => 'L\'action est-elle exécutée en arrière plan ?',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Utiliser le paramétrage global',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Oui',
'Class:Action/Attribute:asynchronous/Value:no' => 'Non',
'Class:Action/Attribute:finalclass' => 'Sous-classe d\'Action',
'Class:Action/Attribute:finalclass+' => 'Nom de la classe instanciable',
'Action:WarningNoTriggerLinked' => 'Attention, aucun déclencheur n\'est associé à l\'action. Elle ne sera pas active tant qu\'elle n\'en aura pas au moins 1.',

View File

@@ -454,6 +454,11 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Kapcsolódó eseményindítók',
'Class:Action/Attribute:trigger_list+' => 'Eseményindítók amik ehhez a művelethez vannak rendelve',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Művelet típus',
'Class:Action/Attribute:finalclass+' => 'A végleges osztály neve',
'Action:WarningNoTriggerLinked' => 'Figyelmeztetés, nincs a művelethez kapcsolódó eseményindító. Addig nem lesz aktív, amíg legalább 1 nem lesz.',

View File

@@ -454,6 +454,11 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Triggers correlati',
'Class:Action/Attribute:trigger_list+' => 'Triggers colleagati a questa azione',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Tipo',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -452,6 +452,11 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'Class:Action/Attribute:status/Value:disabled+' => '非アクティブ',
'Class:Action/Attribute:trigger_list' => '関連トリガー',
'Class:Action/Attribute:trigger_list+' => 'このアクションにリンクされたトリガー',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'タイプ',
'Class:Action/Attribute:finalclass+' => 'タイプ',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -460,6 +460,11 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Gerelateerde triggers',
'Class:Action/Attribute:trigger_list+' => 'Triggers gelinkt aan deze actie',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Type',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Opgelet: er is geen trigger gelinkt aan deze actie. Zonder minstens 1 actieve trigger zal de actie nooit uitgevoerd worden.',

View File

@@ -453,6 +453,11 @@ Dict::Add('PL PL', 'Polish', 'Polski', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Powiązane wyzwalacze',
'Class:Action/Attribute:trigger_list+' => 'Wyzwalacze powiązane z działaniem',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Podklasa działania',
'Class:Action/Attribute:finalclass+' => 'Nazwa ostatniej klasy',
'Action:WarningNoTriggerLinked' => 'Ostrzeżenie, żaden wyzwalacz nie jest powiązany z akcją. Nie będzie aktywny, dopóki nie będzie miał co najmniej 1.',

View File

@@ -454,6 +454,11 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Gatilhos relacionados',
'Class:Action/Attribute:trigger_list+' => 'Gatilhos associadas à esta ação',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Tipo',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Aviso, nenhum gatilho está associado à ação. Não será ativo até que esta ação tenha pelo menos um gatilho associado',

View File

@@ -441,6 +441,11 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Связанные триггеры',
'Class:Action/Attribute:trigger_list+' => 'Триггеры, которые запускают данное действие',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Тип',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -451,6 +451,11 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'Súvisiace spúštače',
'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action~~',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Typ',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -462,6 +462,11 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
'Class:Action/Attribute:status/Value:disabled+' => '',
'Class:Action/Attribute:trigger_list' => 'İlgili tetikleyiciler',
'Class:Action/Attribute:trigger_list+' => 'İşleme bağlı tetikleyici',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => 'Tip',
'Class:Action/Attribute:finalclass+' => '',
'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.~~',

View File

@@ -535,6 +535,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'Class:Action/Attribute:status/Value:disabled+' => '停用',
'Class:Action/Attribute:trigger_list' => '相关的触发器',
'Class:Action/Attribute:trigger_list+' => '此操作关联的触发器',
'Class:Action/Attribute:asynchronous' => 'Asynchronous~~',
'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in background or not~~',
'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting~~',
'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes~~',
'Class:Action/Attribute:asynchronous/Value:no' => 'No~~',
'Class:Action/Attribute:finalclass' => '操作类型',
'Class:Action/Attribute:finalclass+' => '根本属性的名称',
'Action:WarningNoTriggerLinked' => '警告, 此动作没有关联任何触发器. 至少关联1个触发器才会启用.',

View File

@@ -26,6 +26,7 @@ return array(
'Archive_Tar' => $vendorDir . '/pear/archive_tar/Archive/Tar.php',
'ArchivedObjectException' => $baseDir . '/application/exceptions/ArchivedObjectException.php',
'AsyncSendEmail' => $baseDir . '/core/asynctask.class.inc.php',
'AsyncSendiTopNotifications' => $baseDir . '/core/asynctask.class.inc.php',
'AsyncTask' => $baseDir . '/core/asynctask.class.inc.php',
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'AttributeApplicationLanguage' => $baseDir . '/core/attributedef.class.inc.php',
@@ -496,6 +497,7 @@ return array(
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => $baseDir . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
'Combodo\\iTop\\Service\\Module\\ModuleService' => $baseDir . '/sources/Service/Module/ModuleService.php',
'Combodo\\iTop\\Service\\Notification\\Event\\EventiTopNotificationGC' => $baseDir . '/sources/Service/Notification/Event/EventiTopNotificationGC.php',
'Combodo\\iTop\\Service\\Notification\\Event\\EventiTopNotificationService' => $baseDir . '/sources/Service/Notification/Event/EventiTopNotificationService.php',
'Combodo\\iTop\\Service\\Notification\\NotificationsRepository' => $baseDir . '/sources/Service/Notification/NotificationsRepository.php',
'Combodo\\iTop\\Service\\Notification\\NotificationsService' => $baseDir . '/sources/Service/Notification/NotificationsService.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',

View File

@@ -401,6 +401,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Archive_Tar' => __DIR__ . '/..' . '/pear/archive_tar/Archive/Tar.php',
'ArchivedObjectException' => __DIR__ . '/../..' . '/application/exceptions/ArchivedObjectException.php',
'AsyncSendEmail' => __DIR__ . '/../..' . '/core/asynctask.class.inc.php',
'AsyncSendiTopNotifications' => __DIR__ . '/../..' . '/core/asynctask.class.inc.php',
'AsyncTask' => __DIR__ . '/../..' . '/core/asynctask.class.inc.php',
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'AttributeApplicationLanguage' => __DIR__ . '/../..' . '/core/attributedef.class.inc.php',
@@ -871,6 +872,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
'Combodo\\iTop\\Service\\Module\\ModuleService' => __DIR__ . '/../..' . '/sources/Service/Module/ModuleService.php',
'Combodo\\iTop\\Service\\Notification\\Event\\EventiTopNotificationGC' => __DIR__ . '/../..' . '/sources/Service/Notification/Event/EventiTopNotificationGC.php',
'Combodo\\iTop\\Service\\Notification\\Event\\EventiTopNotificationService' => __DIR__ . '/../..' . '/sources/Service/Notification/Event/EventiTopNotificationService.php',
'Combodo\\iTop\\Service\\Notification\\NotificationsRepository' => __DIR__ . '/../..' . '/sources/Service/Notification/NotificationsRepository.php',
'Combodo\\iTop\\Service\\Notification\\NotificationsService' => __DIR__ . '/../..' . '/sources/Service/Notification/NotificationsService.php',
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',

View File

@@ -0,0 +1,72 @@
<?php
namespace Combodo\iTop\Service\Notification\Event;
use Action;
use Combodo\iTop\Application\Branding;
use EventiTopNotification;
use MetaModel;
use utils;
/**
* Class EventiTopNotificationService
*
* Service to create EventiTopNotification objects from various sources.
*
* @package Combodo\iTop\Service\Notification\Event
* @since 3.2.0
* @api
*/
class EventiTopNotificationService {
/**
* @param \Action $oAction
* @param int $iContactId
* @param int $iTriggerId
* @param string $sMessage
* @param string $sTitle
* @param string $sUrl
* @param int $iObjectId
* @param string|null $sObjectClass
* @param string|null $sDate
*
* @return \EventiTopNotification
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
*/
public static function MakeEventFromAction(Action $oAction, int $iContactId, int $iTriggerId, string $sMessage, string $sTitle, string $sUrl, int $iObjectId, ?string $sObjectClass, string|null $sDate = null): EventiTopNotification
{
$oEvent = new EventiTopNotification();
$oEvent->Set('title', $sTitle);
$oEvent->Set('message', $sMessage);
// Compute icon
// - First check if one is defined on the action
if (false === $oAction->Get('icon')->IsEmpty()) {
$oIcon = $oAction->Get('icon');
}
// - Then, check if the action is for a DM object and if its class has an icon
elseif ($iObjectId > 0 && utils::IsNotNullOrEmptyString(MetaModel::GetClassIcon($sObjectClass, false))) {
$oIcon = MetaModel::GetAttributeDef(EventiTopNotification::class, 'icon')->MakeRealValue(MetaModel::GetClassIcon($sObjectClass, false), $oEvent);
}
// - Otherwise, fallback on the compact logo of the application
else {
$oIcon = MetaModel::GetAttributeDef(EventiTopNotification::class, 'icon')->MakeRealValue(Branding::GetCompactMainLogoAbsoluteUrl(), $oEvent);
}
$oEvent->Set('icon', $oIcon);
$oEvent->Set('priority', $oAction->Get('priority'));
$oEvent->Set('contact_id', $iContactId);
$oEvent->Set('trigger_id', $iTriggerId);
$oEvent->Set('action_id', $oAction->GetKey());
$oEvent->Set('object_id', $iObjectId);
$oEvent->Set('url', $sUrl);
if ($sDate !== null) {
$oEvent->Set('date', $sDate);
} else {
$oEvent->SetCurrentDate('date');
}
return $oEvent;
}
}

View File

@@ -318,4 +318,65 @@ HTML
],
];
}
/**
* @dataProvider asynchronousValuesContentProvider
*/
public function testAsynchronousValues($sActionAsyncValue, $sConfigAsyncValue, $sExpectedValue)
{
$oConfig = utils::GetConfig();
$sCurrentEmailAsync = $oConfig->Get('email_asynchronous');
$oConfig->Set('email_asynchronous', $sConfigAsyncValue);
$oActionEmail = MetaModel::NewObject('ActionEmail', [
'name' => 'Test action',
'status' => 'disabled',
'from' => 'unit-test@openitop.org',
'subject' => 'Test subject',
'body' => 'Test body',
'asynchronous' => $sActionAsyncValue,
]);
self::assertEquals($sExpectedValue, $oActionEmail->IsAsynchronous());
$oConfig->Set('email_asynchronous', $sCurrentEmailAsync);
}
public function asynchronousValuesContentProvider()
{
return [
'ActionEmail is asynchronous' => [
'yes',
false,
true
],
'ActionEmail is not asynchronous' => [
'no',
true,
false
],
'ActionEmail is asynchronous and config is asynchronous' => [
'yes',
true,
true
],
'ActionEmail is not asynchronous and config is not asynchronous' => [
'no',
false,
false
],
'ActionEmail follows global settings, config is not asynchronous' => [
'use_global_setting',
false,
false
],
'ActionEmail follows global settings, config is asynchronous' => [
'use_global_setting',
true,
true
],
];
}
}

View File

@@ -32,6 +32,7 @@ use const EVENT_DB_CHECK_TO_DELETE;
use const EVENT_DB_CHECK_TO_WRITE;
use const EVENT_DB_COMPUTE_VALUES;
use const EVENT_DB_LINKS_CHANGED;
use const EVENT_ENUM_TRANSITIONS;
class CRUDEventTest extends ItopDataTestCase
{
@@ -624,6 +625,29 @@ class CRUDEventTest extends ItopDataTestCase
//echo($oDBObject->oEventDataReceived->Get('debug_info'));
}
public function testEnumTransitions()
{
$oEventReceiver = new CRUDEventReceiver($this);
$oEventReceiver->RegisterCRUDListeners();
// Object with no lifecycle
/** @var DBObject $oPerson */
$oPerson = $this->CreatePerson(1);
$oEventReceiver->AddCallback(EVENT_ENUM_TRANSITIONS, Person::class, 'EnumTransitions');
self::CleanCallCount();
$oPerson->EnumTransitions();
$this->assertEquals(0, self::$iEventCalls);
// Object with lifecycle
$oTicket = $this->CreateTicket(1);
$oEventReceiver->AddCallback(EVENT_ENUM_TRANSITIONS, UserRequest::class, 'EnumTransitions');
self::CleanCallCount();
$aTransitions = $oTicket->EnumTransitions();
$this->assertEquals(1, self::$aEventCalls[EVENT_ENUM_TRANSITIONS]);
$this->assertEquals(1, self::$iEventCalls);
$this->assertCount(0, $aTransitions);
}
}
/**
@@ -713,7 +737,7 @@ class CRUDEventReceiver extends ClassesWithDebug
$aCallBack = $this->aCallbacks[$sEvent][$sClass];
if ($aCallBack['count'] > 0) {
$this->aCallbacks[$sEvent][$sClass]['count']--;
call_user_func($this->aCallbacks[$sEvent][$sClass]['callback'], $oObject);
call_user_func($this->aCallbacks[$sEvent][$sClass]['callback'], $oData);
}
}
}
@@ -730,6 +754,7 @@ class CRUDEventReceiver extends ClassesWithDebug
$this->oTestCase->EventService_RegisterListener(EVENT_DB_ABOUT_TO_DELETE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_AFTER_DELETE, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
$this->oTestCase->EventService_RegisterListener(EVENT_ENUM_TRANSITIONS, [$this, 'OnEvent']);
return;
}
@@ -739,9 +764,10 @@ class CRUDEventReceiver extends ClassesWithDebug
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function AddRoleToLink($oObject): void
private function AddRoleToLink(EventData $oData): void
{
$this->Debug(__METHOD__);
$oObject = $oData->Get('object');
$oContactType = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.$oObject->GetKey()]);
$oContactType->DBInsert();
$oObject->Set('role_id', $oContactType->GetKey());
@@ -750,27 +776,44 @@ class CRUDEventReceiver extends ClassesWithDebug
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function SetPersonFunction($oObject): void
private function SetPersonFunction(EventData $oData): void
{
$this->Debug(__METHOD__);
$oObject = $oData->Get('object');
$oObject->Set('function', 'CRUD_function_'.rand());
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function SetPersonFirstName($oObject): void
private function SetPersonFirstName(EventData $oData): void
{
$this->Debug(__METHOD__);
$oObject = $oData->Get('object');
$oObject->Set('first_name', 'CRUD_first_name_'.rand());
}
/**
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
*/
private function CheckCrudStack(DBObject $oObject): void
private function CheckCrudStack(EventData $oData): void
{
$this->Debug(__METHOD__);
$oObject = $oData->Get('object');
self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(get_class($oObject), $oObject->GetKey());
}
private function EnumTransitions(EventData $oData): void
{
$this->Debug(__METHOD__);
/** @var \DBObject $oObject */
$oObject = $oData->Get('object');
$aAllowedStimuli = $oData->Get('allowed_stimuli');
// Deny all transitions
foreach ($aAllowedStimuli as $sStimulus) {
$this->debug(" * Deny $sStimulus");
$oObject->DenyTransition($sStimulus);
}
}
}