Compare commits

..

3 Commits

Author SHA1 Message Date
Anne-Cath
eb4cefdd95 N°9934 - Tickets are no more automatically resolved on iTop Combodo - WIP 2026-08-18 16:40:13 +02:00
Eric Espie
eb2dcbc425 WIP 2026-08-18 16:01:47 +02:00
Anne-Cath
1bddb7cfd8 N°9934 - Tickets are no more automatically resolved on iTop Combodo 2026-08-17 17:39:18 +02:00
9 changed files with 137 additions and 81 deletions

View File

@@ -3983,27 +3983,6 @@ HTML;
$sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data'); $sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled
$value = json_decode($sTagSetJson, true); $value = json_decode($sTagSetJson, true);
if ($this->IsNew()) {
if (is_array($value['orig_value'])) {
foreach ($value['orig_value'] as $val) {
if (!in_array($val, $value['removed'])) {
$value['added'][] = $val;
}
}
}
} else {
$aCurrentValues = $this->Get($sAttCode)->GetValues();
foreach ($value['orig_value'] as $val) {
if (!in_array($val, $aCurrentValues) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
$value['added'][] = $val;
}
}
foreach ($aCurrentValues as $val) {
if (!in_array($val, $value['orig_value']) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
$value['removed'][] = $val;
}
}
}
} }
break; break;

View File

@@ -40,7 +40,7 @@ class DBObjectSet implements iDBObjectSetIterator
/** /**
* @var array * @var array
*/ */
protected $m_aAttToLoad; public $m_aAttToLoad;
/** /**
* @var null|array * @var null|array
*/ */

View File

@@ -1642,10 +1642,10 @@ abstract class MetaModel
if ($sFilterCode == 'id') { if ($sFilterCode == 'id') {
return true; return true;
} }
if (array_key_exists($sClass, self::$m_aMagicFields) && array_key_exists($sFilterCode, self::$m_aMagicFields[$sClass])) { if (array_key_exists($sClass, self::$m_aMagicFields) && in_array($sFilterCode, self::$m_aMagicFields[$sClass])) {
return true; return true;
} }
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && array_key_exists($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) { if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && in_array($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
return false; return false;
} }
return self::IsValidAttCode($sClass, $sFilterCode); return self::IsValidAttCode($sClass, $sFilterCode);
@@ -2963,6 +2963,7 @@ abstract class MetaModel
self::$m_aMagicFields[] = $sClass; self::$m_aMagicFields[] = $sClass;
} }
self::$m_aMagicFields[$sClass][] = $sCode; self::$m_aMagicFields[$sClass][] = $sCode;
self::$m_aAttribOrigins[$sClass][$sCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
} }
if (!$oAttDef->IsSearchable()) { if (!$oAttDef->IsSearchable()) {
if (!array_key_exists($sClass, self::$m_aFilterForbiddenAttributes)) { if (!array_key_exists($sClass, self::$m_aFilterForbiddenAttributes)) {

View File

@@ -60,15 +60,13 @@ class OQLActualClassTreeResolver
$aTranslateFields = []; $aTranslateFields = [];
foreach ($aExpectedAttributes as $sAttCode => $oExpression) { foreach ($aExpectedAttributes as $sAttCode => $oExpression) {
// 'id' is managed later // 'id' is managed later
if ($sAttCode == 'id') { if ($sAttCode === 'id') {
continue; continue;
} }
// Attributes can be stored in attributes list or for magic ones into filter codes list. // Attributes can be stored in attributes list or for magic ones into filter codes list.
$sOriginClass = null; $sOriginClass = null;
if (MetaModel::IsValidAttCode($sClass, $sAttCode)) { if (MetaModel::IsValidAttCode($sClass, $sAttCode) || MetaModel::IsValidFilterCode($sClass, $sAttCode)) {
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sAttCode); $sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sAttCode);
} elseif ($sAttCode == 'id') {
$sOriginClass = $sClass;
} else { } else {
continue; continue;
} }

View File

@@ -21,6 +21,7 @@
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSet;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Spinner\SpinnerUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Spinner\SpinnerUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
@@ -409,11 +410,8 @@ JS
$sEnvironment = addslashes(utils::GetCurrentEnvironment()); $sEnvironment = addslashes(utils::GetCurrentEnvironment());
$oBackupModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitBackup); $oModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitBackup);
$sBackupModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oBackupModalSpinner); $sModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oModalSpinner);
$oRestoreModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitRestore);
$sRestoreModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oRestoreModalSpinner);
$oP->add_script( $oP->add_script(
<<<JS <<<JS
@@ -426,7 +424,7 @@ function LaunchBackupNow()
{ {
const oModal = CombodoModal.OpenModal({ const oModal = CombodoModal.OpenModal({
title: '$sBackUpNow', title: '$sBackUpNow',
content: `$sBackupModalSpinnerHtml` content: `$sModalSpinnerHtml`
}); });
var oParams = {}; var oParams = {};
@@ -452,10 +450,10 @@ function LaunchRestoreNow(sBackupFile, sConfirmationMessage)
{ {
return; return;
} }
const oModal = CombodoModal.OpenModal({ const oModal = CombodoModal.OpenModal({
title: '$sRestore', title: '$sRestore',
content: `$sRestoreModalSpinnerHtml` content: '<i class="ajax-spin fas fa-sync-alt fa-spin"></i> $sPleaseWaitRestore'
}); });
$('#backup_success').addClass('ibo-is-hidden'); $('#backup_success').addClass('ibo-is-hidden');

View File

@@ -51,8 +51,6 @@ class WizStepModulesChoice extends AbstractWizStepInstall
private array $aAnalyzeInstallationModules = []; private array $aAnalyzeInstallationModules = [];
private ?MissingDependencyException $oMissingDependencyException = null; private ?MissingDependencyException $oMissingDependencyException = null;
private array $aFlagsByChoiceId = [];
public function __construct(WizardController $oWizard, $sCurrentState, bool $bOverWriteConfig = true) public function __construct(WizardController $oWizard, $sCurrentState, bool $bOverWriteConfig = true)
{ {
parent::__construct($oWizard, $sCurrentState); parent::__construct($oWizard, $sCurrentState);
@@ -292,34 +290,6 @@ class WizStepModulesChoice extends AbstractWizStepInstall
$oPage->warning($sManualInstallError); $oPage->warning($sManualInstallError);
} }
// Build the default choices
$aDefaults = $this->GetDefaults($aStepInfo, $this->aAnalyzeInstallationModules);
$index = $this->GetStepIndex();
// retrieve the saved selection
// use json_encode:decode to store a hash array: step_id => array(input_name => selected_input_id)
$aParameters = json_decode($this->oWizard->GetParameter('selected_components', '{}'), true);
if (!isset($aParameters[$index])) {
$aParameters[$index] = $aDefaults;
}
$aSelectedComponents = $aParameters[$index];
$bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false);
$aOptions = $aStepInfo['options'] ?? [];
foreach ($aOptions as $index => $aChoice) {
$sChoiceId = self::$SEP.$index;
$this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, false, $bDisableUninstallCheck);
}
if (!$this->bCanMoveForward) {
if (SetupUtils::IsConnectableToITopHub($this->aAnalyzeInstallationModules)) {
$oPage->error('Due to some inconsistencies the upgrade can\'t continue. You must deactivate "consistency protections" in the previous steps and restore a consistent environment.');
} else {
$oPage->error('Due to some inconsistencies the upgrade can\'t continue, please contact Combodo support.');
}
}
$oPage->add('<div class="module-selection-banner">'); $oPage->add('<div class="module-selection-banner">');
$sBannerPath = isset($aStepInfo['banner']) ? $aStepInfo['banner'] : ''; $sBannerPath = isset($aStepInfo['banner']) ? $aStepInfo['banner'] : '';
if (!empty($sBannerPath)) { if (!empty($sBannerPath)) {
@@ -338,6 +308,18 @@ class WizStepModulesChoice extends AbstractWizStepInstall
$oPage->add('<span>'.$sDescription.'</span>'); $oPage->add('<span>'.$sDescription.'</span>');
$oPage->add('</div>'); $oPage->add('</div>');
// Build the default choices
$aDefaults = $this->GetDefaults($aStepInfo, $this->aAnalyzeInstallationModules);
$index = $this->GetStepIndex();
// retrieve the saved selection
// use json_encode:decode to store a hash array: step_id => array(input_name => selected_input_id)
$aParameters = json_decode($this->oWizard->GetParameter('selected_components', '{}'), true);
if (!isset($aParameters[$index])) {
$aParameters[$index] = $aDefaults;
}
$aSelectedComponents = $aParameters[$index];
$oPage->add('<div class="module-selection-body">'); $oPage->add('<div class="module-selection-body">');
$this->DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults); $this->DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults);
$oPage->add('</div>'); $oPage->add('</div>');
@@ -760,12 +742,8 @@ EOF
return $this->aSteps[$index] ?? null; return $this->aSteps[$index] ?? null;
} }
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck) public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode)
{ {
if (array_key_exists($sChoiceId, $this->aFlagsByChoiceId)) {
return $this->aFlagsByChoiceId[$sChoiceId];
}
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']); $oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
//If the extension is missing from disk, it won't exist in the ExtensionsMap, thus returning null //If the extension is missing from disk, it won't exist in the ExtensionsMap, thus returning null
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled(); $bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled();
@@ -806,7 +784,7 @@ EOF
$aOptions = $aChoice['sub_options']['options'] ?? []; $aOptions = $aChoice['sub_options']['options'] ?? [];
foreach ($aOptions as $index => $aSubChoice) { foreach ($aOptions as $index => $aSubChoice) {
$sSubChoiceId = $sChoiceId.self::$SEP.$index; $sSubChoiceId = $sChoiceId.self::$SEP.$index;
$aSubFlags = $this->ComputeChoiceFlags($aSubChoice, $sSubChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck); $aSubFlags = $this->ComputeChoiceFlags($aSubChoice, $sSubChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $bUpgradeMode);
if ($aSubFlags['checked']) { if ($aSubFlags['checked']) {
$bChecked = true; $bChecked = true;
if ($aSubFlags['disabled']) { if ($aSubFlags['disabled']) {
@@ -818,7 +796,7 @@ EOF
} }
} }
$aFlags = [ return [
'uninstallable' => $bCanBeUninstalled, 'uninstallable' => $bCanBeUninstalled,
'dependency_issue' => $bDependencyIssue, 'dependency_issue' => $bDependencyIssue,
'mandatory' => $bMandatory, 'mandatory' => $bMandatory,
@@ -827,11 +805,6 @@ EOF
'disabled' => $bDisabled, 'disabled' => $bDisabled,
'checked' => $bChecked, 'checked' => $bChecked,
]; ];
$this->bCanMoveForward = $this->bCanMoveForward && $this->CanMoveForwardFromChoiceFlags($aFlags, $bDisableUninstallCheck);
$this->aFlagsByChoiceId[$sChoiceId] = $aFlags;
return $aFlags;
} }
public function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false) public function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false)
@@ -843,7 +816,8 @@ EOF
foreach ($aOptions as $index => $aChoice) { foreach ($aOptions as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index; $sChoiceId = $sParentId.self::$SEP.$index;
$aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck); $aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
$this->bCanMoveForward = $this->bCanMoveForward && $this->CanMoveForwardFromChoiceFlags($aFlags, $bDisableUninstallCheck);
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $sChoiceId, $aFlags); $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $sChoiceId, $aFlags);
} }
@@ -882,7 +856,7 @@ EOF
$bSelected = ($sChoiceId === $sChoiceIdNone); $bSelected = ($sChoiceId === $sChoiceIdNone);
} }
$aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck); $aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
//ComputeChoiceFlags does not completely compute alternative flags //ComputeChoiceFlags does not completely compute alternative flags
$aFlags['disabled'] = $bDisabled; $aFlags['disabled'] = $bDisabled;
$aFlags['checked'] = $bSelected; $aFlags['checked'] = $bSelected;

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<classes>
<class id="OQLResolverParent" _created_in="itop-structure" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>true</abstract>
<key_type>autoincrement</key_type>
<db_table>oqlresolverparent</db_table>
<db_key_field>id</db_key_field>
<fields_semantic>
<state_attribute>status</state_attribute>
</fields_semantic>
</properties>
<presentation/>
<methods/>
<fields>
<field id="cumulatedpending" xsi:type="AttributeStopWatch">
<states>
<state id="pending"/>
</states>
<working_time/>
<thresholds/>
<goal/>
</field><field id="status" xsi:type="AttributeEnum">
<always_load_in_tables>true</always_load_in_tables>
<sort_type>rank</sort_type>
<values>
<value id="new">
<code>new</code>
</value>
<value id="pending">
<code>pending</code>
</value>
</values>
<sql>status</sql>
<default_value>new</default_value>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="OQLResolverChild" _created_in="itop-structure" _delta="define">
<parent>OQLResolverParent</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>oqlresolverchild</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
</classes>
</itop_design>

View File

@@ -0,0 +1,41 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use DBObjectSet;
use DBSearch;
use MetaModel;
class OQLResolverTest extends ItopCustomDatamodelTestCase
{
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/Delta/delta_oql_resolver.xml';
}
public function testQueryOnMagicalFields()
{
// Given
$sObjectKey = $this->GivenObjectInDB('OQLResolverChild', ['name' => 'toto', 'status' => 'new']);
$oObject = MetaModel::GetObject('OQLResolverChild', $sObjectKey);
// When actions ApplyStimulus then next action fails
$sFilter = "SELECT OQLResolverChild WHERE ISNULL(cumulatedpending_laststart)";
$oSearch = DBSearch::FromOQL($sFilter);
$oSet = new DBObjectSet($oSearch);
$oSet->OptimizeColumnLoad(['OQLResolverChild' => ['cumulatedpending']]);
$oActual = $oSet->Fetch();
// Then
// Check status...
$this->assertEquals($oObject->Get('name'), $oActual->Get('name'), 'The query should have returned the object');
}
}

View File

@@ -593,7 +593,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
public function testComputeChoiceFlags($aExtensionsOnDiskOrDb, $aWizardStepDefinition, $bIsCurrentSelected, $bDisableUninstallChecks, $aExpectedFlags) public function testComputeChoiceFlags($aExtensionsOnDiskOrDb, $aWizardStepDefinition, $bIsCurrentSelected, $bDisableUninstallChecks, $aExpectedFlags)
{ {
$this->oWizStepModulesChoiceFake->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsOnDiskOrDb)); $this->oWizStepModulesChoiceFake->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsOnDiskOrDb));
$aFlags = $this->oWizStepModulesChoiceFake->ComputeChoiceFlags($aWizardStepDefinition, '_0', $bIsCurrentSelected ? ['_0' => '_0'] : [], false, $bDisableUninstallChecks); $aFlags = $this->oWizStepModulesChoiceFake->ComputeChoiceFlags($aWizardStepDefinition, '_0', $bIsCurrentSelected ? ['_0' => '_0'] : [], false, $bDisableUninstallChecks, true);
$this->assertEquals($aExpectedFlags, $aFlags); $this->assertEquals($aExpectedFlags, $aFlags);
} }