You are browsing the documentation for iTop 2.6 which is not the current version.

Consider browsing to iTop 3.1 documentation

Calculated field & Cascading update

Assuming you want to define a “calculated” field on Class A, which would depend on data defined on class(es) B object(s), something more complex than just an ExternalField. For examples:

  1. On a Ticket, you want the time spent on all of its WorkOrders
  2. On a Ticket, you want the count of manually added CIs.
  3. On FunctionalCI, you want a Location field, which would be simply the location for PhysicalDevice, and a computed location on Logical Device, based on the location of the Physical device on which the Logical is running.

You want to be able to search on that “calculated” field.
You want to be able to run audit against that “calculated” field.

Note that such “calculated” field induces risks on performance while updating large number of Class B objects

Generic Strategy

This is one strategy to implement such “calculated” field. We will store it in database. In order to ensure that it is always accurate, you need to identify when it must be recomputed and do it. For this define/overwrite those functions:

On Class A

  • ComputeValues(): calculate the “calculated” field value based on other fields / related objects.
  • GetInitialStateAttributeFlags(): to specify that the “calculated” field is hidden in creation form
  • GetAttributeFlag(): to specify that the “calculated” field is read-only on modification

On depending Classes B

  • OnUpdate(), OnDelete(): Check if a field has been modified which would impact a “calculated” field of Class A, set a flag to say it.
  • AfterInsert,AfterUpdate(),AfterDelete: If needed (based on above set flag), force recomputation on all objects A which have a “calculated” field which are impacted by that change.

Tips & explanations

  • We use AfterXXX() to cascade the change, and not OnXXX(), because otherwise the remote object would not yet be saved in database, so the computation based on ComputeValues would use the inaccurate data found in database.
  • We use OnUpdate() to check if we need to cascade the change, because after update/delete the array $this→ListChanges() is empy.
  • ComputeValues() is called quite often, but not when loading the object in memory.

Use Case: Sum

In this use case, we want to Sum on a Ticket the timespent on all WorkOrders of this Ticket.

  • First we need to add a time_spent integer attribute on WorkOrder and Ticket classes
  • In order to ensure that Ticket::time_spent is always accurate, we need to identify when it must be recomputed:
    • When the Ticket is updated
    • When a WorkOrder is created → we need to recompute its Ticket
    • When a WorkOrder is updated → we need to recompute its former and its new Ticket
    • When a WorkOrder is deleted → we need to recompute its former Ticket
  • Then define/overwrite those functions:

On Class Ticket

  • ComputeTimeSpent(): calculate the “time_spent” field value based on “time_spent” of WorOrders.
  • OnUpdate(): the list of Workorders cannot be modified from a Ticket in the default datamodel, but if this is enable with the edit mode then this function is needed.
  • GetInitialStateAttributeFlags(): to specify that the “calculated” field is hidden on Ticket creation form
  • GetAttributeFlag(): to specify that the “calculated” field is read-only on Ticket modification
class::Ticket
public function ComputeTimeSpent()
{
   $iSum = 0;
   $oWorkOrderSet = $this->Get('workorder_list');
   while($oWorkOrder = $oWorkOrderSet->Fetch())
   {
        $iSum += $oWorkOrder->Get('time_spent');
   }
   $this->Set('time_spent', $iSum);
}
 
public function OnUpdate()
{
  $aChanges = $this->ListChanges();
  if (array_key_exists('workorder_list', $aChanges))
  {
      $this->ComputeTimeSpent();
  }
}
 
public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array())
{
   // Hide the calculated field in object creation form
   if (($sAttCode == 'time_spent'))
         return(OPT_ATT_HIDDEN | parent::GetInitialStateAttributeFlags($sAttCode, $aReasons));
   return parent::GetInitialStateAttributeFlags($sAttCode, $aReasons);
}
 
public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
{
   // Force the computed field to be read-only, preventing it to be written
   if (($sAttCode == 'time_spent'))
         return(OPT_ATT_READONLY | parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState));
   return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
}

On Class WorkOrder

  • OnUpdate, OnDelete: If field ticket_id has been modified then store temporarily the previous value.
  • AfterInsert,AfterUpdate,AfterDelete: Force recomputation of current and former Ticket.
class::WorkOrder
public function UpdateTicket($id)
{
   if ($id != 0) 
   {
      $oObject = MetaModel::GetObject('Ticket', $id, false);
      // FYI: MetaModel::GetObject('Ticket', 0); generates a FatalError
      if (is_object($oObject))  // in case the user is not allowed to see the object
      {
         $oObject->ComputeTimeSpent();
         $oObject->DBUpdate();
      }
   }              
}
 
public function OnUpdate()
{
  $aChanges = $this->ListChanges();
  if (array_key_exists('ticket_id', $aChanges))
  {
      // store in the WorkOrder memory object the previous value
      $this->i_PreviousTicketId = $this->GetOriginal('ticket_id');
  }
  if (array_key_exists('time_spent', $aChanges))
  {
      // record in the WorkOrder memory object that time spent was changed
      $this->i_TimeChanged = true;
  }
}
 
public function AfterUpdate()
{
   // The WorkOrder is updated in DB and Time spent was changed,
   if (isset($this->i_TimeChanged))
   {
       // we need to recompute TimeSpent on the Ticket
       $this->UpdateTicket($this->Get('ticket_id'));
   }
   // If there was a "former" Ticket then we also need to update it
   if (isset($this->i_PreviousTicketId )) $this->UpdateTicket($this->i_PreviousTicketId);
}
 
public function AfterInsert()
{
   // A new Workorder was created with time_spent, so let's recompute the Ticket 
   if ($this->Get('time_spent') > 0)   $this->UpdateTicket($this->Get('ticket_id'));
}
 
public function OnDelete()
{
   // If the deleted Workorder had some time spent set, 
   // then let's flag that a recomputation is needed, and store the former Ticket id for later use
   if ($this->GetOriginal('time_spent') > 0)   
   {
      $this->i_PreviousTicketId = $this->GetOriginal('ticket_id');
   }
}
 
public function AfterDelete()
{
   // If a recomputation of the former Ticket is needed, let's do it
   if (isset($this->i_PreviousTicketId )) $this->UpdateTicket($this->i_PreviousTicketId);
}
This example was improved by checking if the WorkOrder::time_spent was modified in OnXXXX() function, then set a flag and use it in AfterXXX() to call UpdateTicket() only when truly needed
2_6_0/customization/cascade-update.txt · Last modified: 2020/02/05 11:42 (external edit)
Back to top
Contact us