diff --git a/application/datamodel.application.xml b/application/datamodel.application.xml index 54400e526..89012fada 100644 --- a/application/datamodel.application.xml +++ b/application/datamodel.application.xml @@ -304,8 +304,8 @@ - - A stimulus is about to be applied to an object + + Manage the allowed transitions in current object state. The only action allowed is to deny transitions with DBObject::DenyTransition() cmdbAbstractObject @@ -314,89 +314,9 @@ The object where the stimulus is targeted DBObject - - Current stimulus applied - string - - - Object previous state - string - - - Object new state - string - - - The object must be saved in the database - boolean - - - Debug string - string - - - - - A stimulus has been applied to an object - - cmdbAbstractObject - - - - The object where the stimulus is targeted - DBObject - - - Current stimulus applied - string - - - Object previous state - string - - - Object new state - string - - - The object is asked to be saved in the database - boolean - - - Debug string - string - - - - - A stimulus has failed - - cmdbAbstractObject - - - - The action that failed to apply the stimulus - string - - - The object where the stimulus is targeted - DBObject - - - Current stimulus applied - string - - - Object previous state - string - - - Object new state - string - - - The object must be saved in the database - boolean + + The list of available stimuli in the current state + array Debug string diff --git a/core/action.class.inc.php b/core/action.class.inc.php index 5bcb16318..f8e9e3674 100644 --- a/core/action.class.inc.php +++ b/core/action.class.inc.php @@ -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'); + } } diff --git a/core/asynctask.class.inc.php b/core/asynctask.class.inc.php index 9f950a05f..d67c93f5e 100644 --- a/core/asynctask.class.inc.php +++ b/core/asynctask.class.inc.php @@ -15,6 +15,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see +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"; + } +} \ No newline at end of file diff --git a/core/config.class.inc.php b/core/config.class.inc.php index e7a32ab10..a2f7300d5 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -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', diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml index 9a971a597..e28ea03bd 100644 --- a/core/datamodel.core.xml +++ b/core/datamodel.core.xml @@ -193,6 +193,9 @@ 10 + + 20 + @@ -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); } +]]> + + + + true + public + Get('notifications.itop.send_asynchronously'); + } ]]> diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 2d627b681..e452f1815 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -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; } diff --git a/dictionaries/cs.dictionary.itop.core.php b/dictionaries/cs.dictionary.itop.core.php index 12d816d2b..eba89bbfd 100755 --- a/dictionaries/cs.dictionary.itop.core.php +++ b/dictionaries/cs.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/da.dictionary.itop.core.php b/dictionaries/da.dictionary.itop.core.php index 6e04906a4..567b83434 100644 --- a/dictionaries/da.dictionary.itop.core.php +++ b/dictionaries/da.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/de.dictionary.itop.core.php b/dictionaries/de.dictionary.itop.core.php index 0b1da549f..6e7fdda6e 100644 --- a/dictionaries/de.dictionary.itop.core.php +++ b/dictionaries/de.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index 241adbfa1..565680f59 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/es_cr.dictionary.itop.core.php b/dictionaries/es_cr.dictionary.itop.core.php index 313fcf1f9..c0be0870d 100644 --- a/dictionaries/es_cr.dictionary.itop.core.php +++ b/dictionaries/es_cr.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 71ac7bd4e..11efcce46 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/hu.dictionary.itop.core.php b/dictionaries/hu.dictionary.itop.core.php index c078984c2..21036c3ac 100755 --- a/dictionaries/hu.dictionary.itop.core.php +++ b/dictionaries/hu.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/it.dictionary.itop.core.php b/dictionaries/it.dictionary.itop.core.php index 905e3a9f8..8b2bbda79 100644 --- a/dictionaries/it.dictionary.itop.core.php +++ b/dictionaries/it.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/ja.dictionary.itop.core.php b/dictionaries/ja.dictionary.itop.core.php index 9c4c084da..41715fdec 100644 --- a/dictionaries/ja.dictionary.itop.core.php +++ b/dictionaries/ja.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/nl.dictionary.itop.core.php b/dictionaries/nl.dictionary.itop.core.php index f2a3a2f99..cca7d22f0 100644 --- a/dictionaries/nl.dictionary.itop.core.php +++ b/dictionaries/nl.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/pl.dictionary.itop.core.php b/dictionaries/pl.dictionary.itop.core.php index 026713c53..eb5780195 100644 --- a/dictionaries/pl.dictionary.itop.core.php +++ b/dictionaries/pl.dictionary.itop.core.php @@ -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.', diff --git a/dictionaries/pt_br.dictionary.itop.core.php b/dictionaries/pt_br.dictionary.itop.core.php index bc85eb4a0..a26e03e12 100644 --- a/dictionaries/pt_br.dictionary.itop.core.php +++ b/dictionaries/pt_br.dictionary.itop.core.php @@ -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', diff --git a/dictionaries/ru.dictionary.itop.core.php b/dictionaries/ru.dictionary.itop.core.php index ebe09af24..d39988de0 100644 --- a/dictionaries/ru.dictionary.itop.core.php +++ b/dictionaries/ru.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/sk.dictionary.itop.core.php b/dictionaries/sk.dictionary.itop.core.php index cbf7e8a1d..f7c1bdc6c 100644 --- a/dictionaries/sk.dictionary.itop.core.php +++ b/dictionaries/sk.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/tr.dictionary.itop.core.php b/dictionaries/tr.dictionary.itop.core.php index 62c0f25a7..8e389ee39 100644 --- a/dictionaries/tr.dictionary.itop.core.php +++ b/dictionaries/tr.dictionary.itop.core.php @@ -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.~~', diff --git a/dictionaries/zh_cn.dictionary.itop.core.php b/dictionaries/zh_cn.dictionary.itop.core.php index ee1fab040..cb99b7d44 100644 --- a/dictionaries/zh_cn.dictionary.itop.core.php +++ b/dictionaries/zh_cn.dictionary.itop.core.php @@ -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个触发器才会启用.', diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index a298d8c7a..291822234 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -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', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index fb0e4eb9e..10d6f13b6 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -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', diff --git a/sources/Service/Notification/Event/EventiTopNotificationService.php b/sources/Service/Notification/Event/EventiTopNotificationService.php new file mode 100644 index 000000000..10af05a33 --- /dev/null +++ b/sources/Service/Notification/Event/EventiTopNotificationService.php @@ -0,0 +1,72 @@ +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; + } +} diff --git a/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php b/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php index ae26e1b97..41901e46e 100644 --- a/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php +++ b/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php @@ -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 + ], + ]; + } } \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php index 67ba6e8f1..342364383 100644 --- a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php +++ b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php @@ -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); + } + } + }