diff --git a/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml b/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml
index 3be94a74b2..663ba74ff9 100755
--- a/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml
+++ b/datamodels/2.x/itop-incident-mgmt-itil/datamodel.itop-incident-mgmt-itil.xml
@@ -460,6 +460,18 @@
parent_incident_id
ref
+
+ id AND status NOT IN ('rejected','resolved','closed')]]>
+
+ parent_request_id
+ UserRequest
+ true
+ DEL_MANUAL
+
+
+ parent_request_id
+ ref
+
parent_problem_id
Problem
@@ -987,6 +999,9 @@
+
+
+
@@ -1301,90 +1316,20 @@
LifecycleAction
Get('public_log');
- $sLogPublic = $oLog->GetModifiedEntry('html');
- if ($sLogPublic != '')
- {
- $sOQL = "SELECT UserRequest WHERE parent_incident_id=:ticket";
- $oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oRequest = $oChildRequestSet->Fetch())
- {
- $oRequest->set('public_log',$sLogPublic);
- $oRequest->DBUpdate();
- }
-
- }
- $oLog = $this->Get('private_log');
- $sLogPrivate = $oLog->GetModifiedEntry('html');
- if ($sLogPrivate != '')
- {
- $sOQL = "SELECT UserRequest WHERE parent_incident_id=:ticket";
- $oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oRequest = $oChildRequestSet->Fetch())
- {
- $oRequest->set('private_log',$sLogPrivate);
- $oRequest->DBUpdate();
- }
- }
- return true;
-
+ if (MetaModel::IsValidClass('UserRequest')) {
+ return $this->UpdateChildTicketLog('UserRequest', 'parent_incident_id', ['public_log' => 'public_log', 'private_log' => 'private_log'] );
+ }
+ return true;
}]]>
+
false
public
LifecycleAction
Get('public_log');
- $sLogPublic = $oLog->GetModifiedEntry('html');
- if ($sLogPublic != '')
- {
- $sOQL = "SELECT Incident WHERE parent_incident_id=:ticket";
- $oChildIncidentSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oIncident = $oChildIncidentSet->Fetch())
- {
- $oIncident->set('public_log',$sLogPublic);
- $oIncident->DBUpdate();
- }
-
- }
- $oLog = $this->Get('private_log');
- $sLogPrivate = $oLog->GetModifiedEntry('html');
- if ($sLogPrivate != '')
- {
- $sOQL = "SELECT Incident WHERE parent_incident_id=:ticket";
- $oChildIncidentSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oIncident = $oChildIncidentSet->Fetch())
- {
- $oIncident->set('private_log',$sLogPrivate);
- $oIncident->DBUpdate();
- }
- }
- return true;
-
+ return $this->UpdateChildTicketLog('Incident', 'parent_incident_id', ['public_log' => 'public_log', 'private_log' => 'private_log']);
}]]>
@@ -1558,6 +1503,9 @@
-
10
+ -
+ 15
+
-
20
diff --git a/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/en.dict.itop-incident-mgmt-itil.php b/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/en.dict.itop-incident-mgmt-itil.php
index 2966031a09..a5383abb92 100644
--- a/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/en.dict.itop-incident-mgmt-itil.php
+++ b/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/en.dict.itop-incident-mgmt-itil.php
@@ -44,6 +44,8 @@
'UI-IncidentManagementOverview-OpenIncidentByStatus' => 'Open incidents by status',
'UI-IncidentManagementOverview-OpenIncidentByAgent' => 'Open incidents by agent',
'UI-IncidentManagementOverview-OpenIncidentByCustomer' => 'Open incidents by customer',
+ 'Class:Incident/Method:UpdateChildTicketWith:public_log' => 'Public log entry from parent Incident %2$s:
',
+ 'Class:Incident/Method:UpdateChildTicketWith:private_log' => 'Private log entry from parent Incident [[Incident:%1$s]]:
',
]);
// Dictionnay conventions
@@ -193,6 +195,10 @@
'Class:Incident/Attribute:parent_incident_id+' => '',
'Class:Incident/Attribute:parent_incident_ref' => 'Parent incident ref',
'Class:Incident/Attribute:parent_incident_ref+' => '',
+ 'Class:Incident/Attribute:parent_request_id' => 'Parent request',
+ 'Class:Incident/Attribute:parent_request_id+' => '',
+ 'Class:Incident/Attribute:parent_request_ref' => 'Parent request ref',
+ 'Class:Incident/Attribute:parent_request_ref+' => '',
'Class:Incident/Attribute:parent_change_id' => 'Parent change',
'Class:Incident/Attribute:parent_change_id+' => '',
'Class:Incident/Attribute:parent_change_ref' => 'Parent change ref',
diff --git a/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/fr.dict.itop-incident-mgmt-itil.php b/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/fr.dict.itop-incident-mgmt-itil.php
index c7672c7027..7b329d4a2b 100644
--- a/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/fr.dict.itop-incident-mgmt-itil.php
+++ b/datamodels/2.x/itop-incident-mgmt-itil/dictionaries/fr.dict.itop-incident-mgmt-itil.php
@@ -179,15 +179,19 @@
'Class:Incident/Attribute:pending_reason+' => '',
'Class:Incident/Attribute:parent_incident_id' => 'Incident parent',
'Class:Incident/Attribute:parent_incident_id+' => '',
- 'Class:Incident/Attribute:parent_incident_ref' => 'Référence incident parent',
+ 'Class:Incident/Attribute:parent_incident_ref' => 'Réf. incident parent',
'Class:Incident/Attribute:parent_incident_ref+' => '',
+ 'Class:Incident/Attribute:parent_request_id' => 'Demande parente',
+ 'Class:Incident/Attribute:parent_request_id+' => '',
+ 'Class:Incident/Attribute:parent_request_ref' => 'Réf. demande parente',
+ 'Class:Incident/Attribute:parent_request_ref+' => '',
'Class:Incident/Attribute:parent_change_id' => 'Changement parent',
'Class:Incident/Attribute:parent_change_id+' => '',
- 'Class:Incident/Attribute:parent_change_ref' => 'Ref Changement parent',
+ 'Class:Incident/Attribute:parent_change_ref' => 'Réf. changement parent',
'Class:Incident/Attribute:parent_change_ref+' => '',
'Class:Incident/Attribute:parent_problem_id' => 'Problème lié',
'Class:Incident/Attribute:parent_problem_id+' => '',
- 'Class:Incident/Attribute:parent_problem_ref' => 'Référence problème lié',
+ 'Class:Incident/Attribute:parent_problem_ref' => 'Réf. problème lié',
'Class:Incident/Attribute:parent_problem_ref+' => '',
'Class:Incident/Attribute:related_request_list' => 'Requêtes filles',
'Class:Incident/Attribute:related_request_list+' => '',
diff --git a/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml b/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml
index 766272cbec..9169fe93eb 100755
--- a/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml
+++ b/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml
@@ -1451,44 +1451,17 @@
public
LifecycleAction
UpdateChildTicketLog('UserRequest', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log' ]);
+}]]>
+
+
+ false
+ public
+ LifecycleAction
+ Get('public_log');
- $sLogPublic = $oLog->GetModifiedEntry('html');
- if ($sLogPublic != '')
- {
- $sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
- $oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oRequest = $oChildRequestSet->Fetch())
- {
- $oRequest->set('public_log',$sLogPublic);
- $oRequest->DBUpdate();
- }
-
- }
- $oLog = $this->Get('private_log');
- $sLogPrivate = $oLog->GetModifiedEntry('html');
- if ($sLogPrivate != '')
- {
- $sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
- $oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oRequest = $oChildRequestSet->Fetch())
- {
- $oRequest->set('private_log',$sLogPrivate);
- $oRequest->DBUpdate();
- }
- }
- return true;
-
+ return $this->UpdateChildTicketLog('Incident', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log']);
}]]>
@@ -1526,6 +1499,7 @@
parent::OnUpdate();
$this->Set('last_update', time());
$this->UpdateChildRequestLog();
+ $this->UpdateChildIncidentLog();
}]]>
diff --git a/datamodels/2.x/itop-request-mgmt-itil/dictionaries/en.dict.itop-request-mgmt-itil.php b/datamodels/2.x/itop-request-mgmt-itil/dictionaries/en.dict.itop-request-mgmt-itil.php
index a2d2fe1861..4c7c504fd0 100644
--- a/datamodels/2.x/itop-request-mgmt-itil/dictionaries/en.dict.itop-request-mgmt-itil.php
+++ b/datamodels/2.x/itop-request-mgmt-itil/dictionaries/en.dict.itop-request-mgmt-itil.php
@@ -37,6 +37,8 @@
'UI-RequestManagementOverview-OpenRequestByCustomer' => 'Open requests by customer',
'Class:UserRequest:KnownErrorList' => 'Known Errors',
'Class:UserRequest:KnownErrorList+' => 'Known Errors related to Functional CI linked to the current ticket',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => 'Public log automatic copy from parent User Request %2$s:
',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => 'Private log automatic copy from parent User Request [[UserRequest:%1$s]]:
',
]);
// Dictionnay conventions
diff --git a/datamodels/2.x/itop-request-mgmt-itil/dictionaries/fr.dict.itop-request-mgmt-itil.php b/datamodels/2.x/itop-request-mgmt-itil/dictionaries/fr.dict.itop-request-mgmt-itil.php
index b61397a8fd..ec07842c85 100644
--- a/datamodels/2.x/itop-request-mgmt-itil/dictionaries/fr.dict.itop-request-mgmt-itil.php
+++ b/datamodels/2.x/itop-request-mgmt-itil/dictionaries/fr.dict.itop-request-mgmt-itil.php
@@ -42,6 +42,8 @@
'UI-RequestManagementOverview-OpenRequestByCustomer' => 'Requêtes ouvertes par client',
'Class:UserRequest:KnownErrorList' => 'Erreurs connues',
'Class:UserRequest:KnownErrorList+' => 'Erreurs connues liées à des éléments de configuration impactés par ce ticket',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => 'Copie automatique du log public de la demande parente %2$s:
',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => 'Copie automatique du log privé de la demande parente [[UserRequest:%1$s]]:
',
]);
// Dictionnay conventions
diff --git a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml
index db1edcb30d..97025b5810 100755
--- a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml
+++ b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml
@@ -1486,44 +1486,8 @@
public
LifecycleAction
Get('public_log');
- $sLogPublic = $oLog->GetModifiedEntry('html');
- if ($sLogPublic != '')
- {
- $sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
- $oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oRequest = $oChildRequestSet->Fetch())
- {
- $oRequest->set('public_log',$sLogPublic);
- $oRequest->DBUpdate();
- }
-
- }
- $oLog = $this->Get('private_log');
- $sLogPrivate = $oLog->GetModifiedEntry('html');
- if ($sLogPrivate != '')
- {
- $sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
- $oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
- array(),
- array(
- 'ticket' => $this->GetKey(),
- )
- );
- while($oRequest = $oChildRequestSet->Fetch())
- {
- $oRequest->set('private_log',$sLogPrivate);
- $oRequest->DBUpdate();
- }
- }
- return true;
-
+{
+ return $this->UpdateChildTicketLog('UserRequest', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log'] );
}]]>
diff --git a/datamodels/2.x/itop-request-mgmt/dictionaries/en.dict.itop-request-mgmt.php b/datamodels/2.x/itop-request-mgmt/dictionaries/en.dict.itop-request-mgmt.php
index 1aeaac0830..5e8f40bacb 100644
--- a/datamodels/2.x/itop-request-mgmt/dictionaries/en.dict.itop-request-mgmt.php
+++ b/datamodels/2.x/itop-request-mgmt/dictionaries/en.dict.itop-request-mgmt.php
@@ -41,6 +41,8 @@
'Menu:UserRequest:MyWorkOrders+' => 'All work orders assigned to me',
'Class:Problem:KnownProblemList' => 'Known problems',
'Tickets:Related:OpenIncidents' => 'Open incidents',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => 'Public log automatic copy from parent User Request %2$s:
',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => 'Private log automatic copy from parent User Request [[UserRequest:%1$s]]:
',
]);
// Dictionnay conventions
diff --git a/datamodels/2.x/itop-request-mgmt/dictionaries/fr.dict.itop-request-mgmt.php b/datamodels/2.x/itop-request-mgmt/dictionaries/fr.dict.itop-request-mgmt.php
index 802d264f02..5e9bc7a2ef 100644
--- a/datamodels/2.x/itop-request-mgmt/dictionaries/fr.dict.itop-request-mgmt.php
+++ b/datamodels/2.x/itop-request-mgmt/dictionaries/fr.dict.itop-request-mgmt.php
@@ -42,6 +42,8 @@
'UI-RequestManagementOverview-OpenRequestByCustomer' => 'Requêtes ouvertes par organisation',
'Class:UserRequest:KnownErrorList' => 'Erreurs connues',
'Class:UserRequest:KnownErrorList+' => 'Erreurs connues liées à des éléments de configuration impactés par ce ticket',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => 'Copie automatique du log public de la demande parente %2$s:
',
+ 'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => 'Copie automatique du log privé de la demande parente [[UserRequest:%1$s]]:
',
'Menu:UserRequest:MyWorkOrders' => 'Tâches qui me sont assignées',
'Menu:UserRequest:MyWorkOrders+' => '',
'Class:Problem:KnownProblemList' => 'Problèmes connus',
diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
index f9a5ec37f6..99ee4c78e8 100755
--- a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
+++ b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
@@ -351,6 +351,63 @@
}]]>
+
+ 'private_log'])
+ * So in the example parent.public_log will be copied into each child.private_log
+ * with a prefix using a dictionary entry like 'Class:/Method:UpdateChildTicketWith:' with one placeholder %1$s for the parent ticket ref
+ * resulting in an entry like "Copy of public log entry from parent Incident I-000123: " in the child private_log
+ *
+ */]]>
+
+ false
+ public
+ LifecycleAction
+ GetTargetClass() !== get_class($this))) {
+ ErrorLog::Debug("Attribute $sChildParentAttCode should be an external key of class $sChildClass, pointing to class ".get_class($this),"DataModel");
+ return true; // Do nothing
+ }
+
+ $sParentClass = get_class($this);
+ $aChildEntries = [];
+ foreach ($aLogAttCodes as $sParentAttCode => $sChildAttCode) {
+ if (MetaModel::IsValidAttCode($sParentClass, $sParentAttCode) && MetaModel::GetAttributeDef($sParentClass, $sParentAttCode) instanceof AttributeCaseLog
+ && MetaModel::IsValidAttCode($sChildClass, $sChildAttCode) && MetaModel::GetAttributeDef($sChildClass, $sChildAttCode) instanceof AttributeCaseLog
+ && (!utils::IsNullOrEmptyString($this->Get($sParentAttCode)->GetModifiedEntry('html')))) {
+ $aChildEntries[$sChildAttCode] = Dict::Format('Class:'.$sParentClass.'/Method:UpdateChildTicketWith:'.$sParentAttCode, $this->GetKey(), $this->Get('ref')).$this->Get($sParentAttCode)->GetModifiedEntry('html');
+ }
+ }
+ if ($aChildEntries == []) {
+ return true; // nothing to update
+ }
+
+ $sOQL = "SELECT $sChildClass WHERE $sChildParentAttCode = :ticket_id";
+ $oChildSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sOQL), [], ['ticket_id' => $this->GetKey()]);
+ while ($oChild = $oChildSet->Fetch()) {
+ if (is_object($oChild)) { // Seems that empty set, maybe in case of OQL syntax error, can return a single empty object instead of no object at all
+ foreach ($aChildEntries as $sAttCode => $sEntry) {
+ $oChild->Set($sAttCode, $sEntry);
+ }
+ $oChild->DBUpdate();
+ }
+ }
+ return true;
+
+ }]]>
+
diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateChildTicketLogTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateChildTicketLogTest.php
new file mode 100644
index 0000000000..5781d0711b
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-tickets/UpdateChildTicketLogTest.php
@@ -0,0 +1,166 @@
+
+//
+
+namespace Combodo\iTop\Test\UnitTest\Module\iTopTickets;
+
+use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
+use ormCaseLog;
+use MetaModel;
+
+class UpdateChildTicketLogTest extends ItopDataTestCase
+{
+ public function testUpdateChildTicketLog_PublicLogOnTwoChild(): void
+ {
+ //Given a parent ticket with two child ticket
+ list($iParentTicket, $aChildrenTree) = $this->GivenUserRequests(2);
+ $this->assertCount(2, $aChildrenTree[$iParentTicket], 'The test setup should create exactly two child tickets.');
+ $sParentPublicLogEntry = 'This is a public log entry for the parent ticket.';
+ $oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
+
+ // When I enter a public_log entry for the parent ticket
+ $oParentTicket->Set('public_log', $sParentPublicLogEntry);
+ $oParentTicket->DBUpdate();
+
+ // Then the log should be copied to all descendants and contain parent references recursively
+ $this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'public_log', $sParentPublicLogEntry);
+ }
+
+ public function testUpdateChildTicketLog_PrivateAndPublicLog(): void
+ {
+ //Given a parent ticket with two child ticket
+ list($iParentTicket, $aChildrenTree) = $this->GivenUserRequests(3);
+ $sParentPublicLogEntry = 'This is a public log entry for the parent ticket.';
+ $sParentPrivateLogEntry = 'This is a private log entry for the parent ticket.';
+
+ // When I enter both a public_log and a private_log entry for the parent ticket
+ $oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
+ $oParentTicket->Set('public_log', $sParentPublicLogEntry);
+ $oParentTicket->Set('private_log', $sParentPrivateLogEntry);
+ $oParentTicket->DBUpdate();
+
+ // Then both logs should be copied to all descendants and keep ancestor references recursively
+ $this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'public_log', $sParentPublicLogEntry);
+ $this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'private_log', $sParentPrivateLogEntry);
+ }
+
+ public function testUpdateChildTicketLog_PrivateLogOnMultipleLevels(): void
+ {
+ //Given a parent ticket with two child ticket
+ list($iParentTicket, $aChildrenTree) = $this->GivenUserRequests(1, 4);
+ $sParentPrivateLogEntry = 'This is a private log entry for the parent ticket.';
+
+ // When I enter both a public_log and a private_log entry for the parent ticket
+ $oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
+ $oParentTicket->Set('private_log', $sParentPrivateLogEntry);
+ $oParentTicket->DBUpdate();
+
+ // Then the private log should be copied on each level with parent + grand-parent +... references
+ $this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'private_log', $sParentPrivateLogEntry);
+ }
+
+ private function AssertLogContainsParentsRefOrKeyRecursively(int $iParentTicket, array $aChildrenTree, string $sLogAttCode, string $sExpectedEntry, array $aAncestors = []): void
+ {
+ $oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
+ $aAncestorsToFind = array_merge($aAncestors, [[
+ 'id' => (string) $iParentTicket,
+ 'ref' => (string) $oParentTicket->Get('ref'),
+ ]]);
+
+ foreach ($aChildrenTree as $iChildOrIndex => $aChildNodeOrId) {
+ if (is_array($aChildNodeOrId)) {
+ $iChildTicket = (int) $iChildOrIndex;
+ $aGrandChildrenTree = $aChildNodeOrId;
+ } else {
+ $iChildTicket = (int) $aChildNodeOrId;
+ $aGrandChildrenTree = [];
+ }
+
+ $oChildTicket = MetaModel::GetObject('UserRequest', $iChildTicket);
+ $sLastLogEntry = $oChildTicket->Get($sLogAttCode)->GetLatestEntry();
+ $this->assertNotEmpty($sLastLogEntry, "The $sLogAttCode entry was not copied to child ticket #$iChildTicket.");
+ $this->assertStringContainsString($sExpectedEntry, $sLastLogEntry, "The $sLogAttCode entry on child ticket #$iChildTicket does not contain the original parent entry.");
+ foreach ($aAncestorsToFind as $aAncestor) {
+ $bContainsAncestorRef = strpos($sLastLogEntry, $aAncestor['ref']) !== false;
+ $bContainsAncestorId = strpos($sLastLogEntry, $aAncestor['id']) !== false;
+ $this->assertTrue(
+ $bContainsAncestorRef || $bContainsAncestorId,
+ "The $sLogAttCode entry on child ticket #$iChildTicket does not contain ancestor ref '{$aAncestor['ref']}' nor ancestor id '{$aAncestor['id']}'."
+ );
+ }
+
+ if ($aGrandChildrenTree !== []) {
+ $this->AssertLogContainsParentsRefOrKeyRecursively($iChildTicket, $aGrandChildrenTree, $sLogAttCode, $sExpectedEntry, $aAncestorsToFind);
+ }
+ }
+ }
+ /**
+ * @return array
+ * @throws \Exception
+ */
+ private function GivenUserRequests(int $iCount, int $iLevel = 2): array
+ {
+ $iOrg = $this->GivenObjectInDB('Organization', [
+ 'name' => 'Test Organization for Log Update',
+ ]);
+ // Given a parent ticket
+ $iParentTicket = $this->GivenObjectInDB('UserRequest', [
+ 'title' => 'Parent Ticket for Log Update Test',
+ 'description' => 'This is the parent ticket for testing log updates.',
+ 'org_id' => $iOrg,
+ 'ref' => 'R-00001',
+ ]);
+
+ $iRemainingLevels = max(0, $iLevel - 1);
+ $iRefCounter = 1;
+ $aChildrenTree = [
+ $iParentTicket => $this->GivenChildTicketsRecursive($iParentTicket, $iCount, $iRemainingLevels, $iOrg, $iRefCounter),
+ ];
+
+ return [$iParentTicket, $aChildrenTree];
+ }
+
+ private function GivenChildTicketsRecursive(int $iParentTicket, int $iCount, int $iRemainingLevels, int $iOrg, int &$iRefCounter): array
+ {
+ if ($iRemainingLevels <= 0) {
+ return [];
+ }
+
+ $aChildren = [];
+ for ($i = 1; $i <= $iCount; $i++) {
+ $iRefCounter++;
+ $sRef = sprintf('R-%05d', $iRefCounter);
+ $iChildTicket = $this->GivenObjectInDB('UserRequest', [
+ 'title' => "Child Ticket $sRef for Log Update Test",
+ 'description' => "This is child ticket $sRef for testing log updates.",
+ 'org_id' => $iOrg,
+ 'parent_request_id' => $iParentTicket,
+ 'ref' => $sRef,
+ ]);
+
+ if ($iRemainingLevels > 1) {
+ $aChildren[$iChildTicket] = $this->GivenChildTicketsRecursive($iChildTicket, $iCount, $iRemainingLevels - 1, $iOrg, $iRefCounter);
+ } else {
+ $aChildren[] = $iChildTicket;
+ }
+ }
+
+ return $aChildren;
+ }
+}