From b97c7433c8d41d8c2805bb8e65d0c848a2dca437 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Fri, 3 Jan 2025 11:57:51 +0100 Subject: [PATCH 01/10] =?UTF-8?q?:art:=20N=C2=B07633=20Cosmetics=20on=20un?= =?UTF-8?q?it=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/BaseTestCase/ItopDataTestCase.php | 23 ++++- .../unitary-tests/core/UserRightsTest.php | 83 +++++++++++-------- 2 files changed, 69 insertions(+), 37 deletions(-) diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 30a94d743..d0a4aa408 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -925,11 +925,12 @@ abstract class ItopDataTestCase extends ItopTestCase * * @param $iExpectedCount Number of MySQL queries that should be executed * @param callable $oFunction Operations to perform + * @param string $sMessage Message to display in case of failure * * @throws \MySQLException * @throws \MySQLQueryHasNoResultException */ - protected function assertDBQueryCount($iExpectedCount, callable $oFunction) + protected function assertDBQueryCount($iExpectedCount, callable $oFunction, $sMessage = '') { $iInitialCount = (int) CMDBSource::QueryToScalar("SHOW SESSION STATUS LIKE 'Queries'", 1); $oFunction(); @@ -937,7 +938,13 @@ abstract class ItopDataTestCase extends ItopTestCase $iCount = $iFinalCount - 1 - $iInitialCount; if ($iCount != $iExpectedCount) { - $this->fail("Expected $iExpectedCount queries. $iCount have been executed."); + if ($sMessage === '') { + $sMessage = "Expected $iExpectedCount queries. $iCount have been executed."; + } + else { + $sMessage .= " - Expected $iExpectedCount queries. $iCount have been executed."; + } + $this->fail($sMessage); } else { @@ -960,6 +967,18 @@ abstract class ItopDataTestCase extends ItopTestCase $this->assertEquals($iExpectedCount, $iCount, "Found $iCount changes for object $sClass::$iId"); } + /** + * @since 3.2.1 + */ + protected static function assertIsDBObject(string $sExpectedClass, ?int $iExpectedKey, $oObject, ?string $sMessage = '') + { + self::assertNotNull($oObject, $sMessage); + self::assertInstanceOf($sExpectedClass, $oObject, $sMessage); + if ($iExpectedKey !== null) { + self::assertEquals($iExpectedKey, $oObject->GetKey(), $sMessage); + } + } + /** * Import a set of XML files describing a consistent set of iTop objects * @param string[] $aFiles diff --git a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php index 6a3d3d7a2..5a743a0e8 100644 --- a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php +++ b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php @@ -491,52 +491,65 @@ class UserRightsTest extends ItopDataTestCase public function testFindUser_ExistingInternalUser() { - $sLogin = 'UserRightsFindUser'.uniqid(); - $iKey = $this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'])->GetKey(); - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + $sLogin = 'AnInternalUser'.uniqid(); + $iKey = $this->GivenObjectInDB(\UserLocal::class, ['login' => $sLogin]); - $this->assertNotNull($oUser); - $this->assertEquals($iKey, $oUser->GetKey()); - $this->assertEquals(\UserLocal::class, get_class($oUser)); + $this->assertDBQueryCount( + 1, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'A query should be performed the first time FindUser is called' + ); - $this->assertDBQueryCount(0, function() use ($sLogin, $iKey){ - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - static::assertEquals($iKey, $oUser->GetKey()); - static::assertEquals(\UserLocal::class, get_class($oUser)); - }); + $this->assertDBQueryCount( + 0, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'The cache should prevent additional queries on subsequent calls' + ); } public function testFindUser_ExistingExternalUser() { - $sLogin = 'UserRightsFindUser'.uniqid(); + $sLogin = 'AnExternalUser'.uniqid(); + $iKey = $this->GivenObjectInDB(\UserExternal::class, ['login' => $sLogin]); - $iKey = $this->GivenObjectInDB(\UserExternal::class, [ - 'login' => $sLogin, - 'language' => 'EN US', - ]); + $this->assertDBQueryCount( + 2, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'Some queries should be performed the first time FindUser is called' + ); - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - - $this->assertNotNull($oUser); - $this->assertEquals($iKey, $oUser->GetKey()); - $this->assertEquals(\UserExternal::class, get_class($oUser)); - - $this->assertDBQueryCount(0, function() use ($sLogin, $iKey){ - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - static::assertEquals($iKey, $oUser->GetKey()); - static::assertEquals(\UserExternal::class, get_class($oUser)); - }); + $this->assertDBQueryCount( + 0, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'The cache should prevent additional queries on subsequent calls' + ); } - public function testFindUser_UnknownLogin_AvoidSameSqlQueryTwice() + public function testFindUser_UnknownLogin() { - $sLogin = 'UserRightsFindUser'.uniqid(); - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - $this->assertNull($oUser); + $sLogin = 'NobodyLogin'; - $this->assertDBQueryCount(0, function() use ($sLogin){ - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - $this->assertNull($oUser); - }); + $this->assertDBQueryCount( + 2, + fn() => $this->FindUserAndAssertItWasNotFound($sLogin), + 'Some queries should be performed the first time FindUser is called' + ); + + $this->assertDBQueryCount( + 0, + fn() => $this->FindUserAndAssertItWasNotFound($sLogin), + 'The cache should prevent additional queries on subsequent calls' + ); + } + + public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) + { + $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); + } + public function FindUserAndAssertItWasNotFound($sLogin) + { + $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + static::assertNull($oUser, 'FindUser should return null when the login is unknown'); } } From 71b3a415a6c8ee2ac61ef109e7e67a6684106b7c Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 3 Jan 2025 16:17:51 +0100 Subject: [PATCH 02/10] =?UTF-8?q?N=C2=B07206=20-=20Add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/BaseTestCase/ItopDataTestCase.php | 8 ++ .../core/TriggerOnStateEnterTest.php | 91 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index d0a4aa408..fe0f38f3c 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -435,6 +435,14 @@ abstract class ItopDataTestCase extends ItopTestCase $this->RemoveObjects($sTagClass, "SELECT $sTagClass WHERE code = '$sTagCode'"); } + public function RemoveAllObjects($sClassName) + { + $oSet = new \DBObjectSet(new \DBObjectSearch($sClassName)); + while ($oObject = $oSet->Fetch()) { + $oObject->DBDelete(); + } + } + private function RemoveObjects($sClass, $sOQL) { $oFilter = DBSearch::FromOQL($sOQL); diff --git a/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php b/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php new file mode 100644 index 000000000..32134c1e2 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php @@ -0,0 +1,91 @@ +RemoveAllObjects(\Trigger::class); + $this->RemoveAllObjects(\EventNotificationEmail::class); + } + + public function testIsTriggeredOnTransition() + { + $iTrigger = $this->GivenTriggerWithAction('TriggerOnStateEnter', 'assigned'); + $oUserRequest = $this->GivenUserRequest('new'); + + $oUserRequest->ApplyStimulus('ev_assign'); + $this->AssertTriggerExecuted($iTrigger, 1, 'The trigger should have been executed'); + + $oUserRequest->ApplyStimulus('ev_assign'); + $this->AssertTriggerExecuted($iTrigger, 1, 'The trigger should not be executed when stimulus not expected in current state'); + } + + public function testIsTriggeredOnTransitionStayingInSameState() + { + $iTrigger = $this->GivenTriggerWithAction('TriggerOnStateEnter', 'assigned'); + $oUserRequest = $this->GivenUserRequest('new'); + $oUserRequest->ApplyStimulus('ev_assign'); + + $bTransitionned = $oUserRequest->ApplyStimulus('ev_reassign'); + $this->assertTrue($bTransitionned, 'The stimulus should have been accepted'); + return; + + $this->MarkTestSkipped('This test fails because the trigger is not executed'); + $this->AssertTriggerExecuted($iTrigger, 2, 'The trigger should have been executed twice'); + } + public function testIsTriggeredOnNewObject() + { + $iTrigger = $this->GivenTriggerWithAction('TriggerOnStateEnter', 'new'); + $oUserRequest = $this->GivenUserRequest('new'); + $this->AssertTriggerExecuted($iTrigger, 0, 'The trigger TriggerOnStateEnter should not be executed on created object'); + } + + private function GivenTriggerWithAction(string $sTriggerClass, string $sState) + { + $iTrigger = $this->GivenObjectInDB($sTriggerClass, [ + 'description' => 'Description', + 'target_class' => 'UserRequest', + 'state' => $sState, + ]); + $this->GivenObjectInDB('ActionEmail', [ + 'from' => 'test@combodo.com', + 'subject' => 'Subject', + 'body' => 'Body', + 'description' => 'Description', + 'test_recipient' => 'test@combodo.com', + 'name' => 'UserRequest', + 'asynchronous' => 'yes', + 'trigger_list' => [ + "trigger_id:$iTrigger", + ], + ]); + return $iTrigger; + } + + private function AssertTriggerExecuted(int $iTrigger, $iCount, $sMessage = '') + { + $oSearch = new \DBObjectSearch('EventNotificationEmail'); + $oSearch->AddCondition('trigger_id', $iTrigger); + $oSet = new \DBObjectSet($oSearch); + $this->assertEquals($iCount, $oSet->Count(), $sMessage); + } + + public function GivenUserRequest(string $sStatus): ?\DBObject + { + $iUserRequest = $this->GivenObjectInDB('UserRequest', [ + 'title' => 'Title', + 'description' => 'Description', + 'status' => $sStatus, + ]); + return MetaModel::GetObject('UserRequest', $iUserRequest); + } +} \ No newline at end of file From a91de9fb365537957d0ddf56beef6005202ef992 Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 3 Jan 2025 17:04:41 +0100 Subject: [PATCH 03/10] :white_check_mark: Add helpers for stopwatches manipulation --- .../src/BaseTestCase/ItopDataTestCase.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index fe0f38f3c..8b99c7da4 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -1433,4 +1433,28 @@ abstract class ItopDataTestCase extends ItopTestCase self::markTestSkipped("Test skipped: module '$sModule' is not present"); } } + + static protected function StartStopwatchInThePast(DBObject $oObject, string $sStopwatchAttCode, int $iDelayInSecond) + { + $iStartDate = time() - $iDelayInSecond; + /** @var \ormStopWatch $oStopwatch */ + $oStopwatch = $oObject->Get($sStopwatchAttCode); + $oAttDef = MetaModel::GetAttributeDef(get_class($oObject), $sStopwatchAttCode); + $oStopwatch->Start($oObject, $oAttDef, $iStartDate); + $oStopwatch->ComputeDeadlines($oObject, $oAttDef); + $oObject->Set($sStopwatchAttCode, $oStopwatch); + } + + + static protected function StopStopwatchInTheFuture(DBObject $oObject, string $sStopwatchAttCode, int $iDelayInSecond) + { + $iEndDate = time() + $iDelayInSecond; + /** @var \ormStopWatch $oStopwatch */ + $oStopwatch = $oObject->Get($sStopwatchAttCode); + $oAttDef = MetaModel::GetAttributeDef(get_class($oObject), $sStopwatchAttCode); + $oStopwatch->Stop($oObject, $oAttDef, $iEndDate); + $oStopwatch->ComputeDeadlines($oObject, $oAttDef); + $oObject->Set($sStopwatchAttCode, $oStopwatch); + } + } From 1225ee1e788c70d3979c06ddbcd1a7a9906f5b4a Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 3 Jan 2025 17:18:24 +0100 Subject: [PATCH 04/10] :white_check_mark: improve test --- .../unitary-tests/application/MenuNodeTest.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php b/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php index 4c9961ece..053fef232 100644 --- a/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php +++ b/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php @@ -20,16 +20,6 @@ class MenuNodeTest extends ItopDataTestCase { $this->oUR = $this->CreateUserRequest(666, $aUserRequestCustomParams); } - private function StartStopwatchInThePast(\UserRequest $oTicket, string $sAttCode, int $iSecDelay) - { - $iStartDate = time() - $iSecDelay; - /** @var \ormStopWatch $oStopwatch */ - $oStopwatch = $oTicket->Get($sAttCode); - $oAttDef = \MetaModel::GetAttributeDef(get_class($oTicket), $sAttCode); - $oStopwatch->Start($oTicket, $oAttDef, $iStartDate); - $oStopwatch->ComputeDeadlines($oTicket, $oAttDef); - $oTicket->Set($sAttCode, $oStopwatch); - } public function RenderOQLSearchProvider() { @@ -70,7 +60,7 @@ OQL; */ public function testRenderOQLSearchOqlWithDateFormatOnDeadline() { - $this->StartStopwatchInThePast($this->oUR, 'ttr', 10); + static::StartStopwatchInThePast($this->oUR, 'ttr', 10); $sOql = << Date: Mon, 6 Jan 2025 11:06:18 +0100 Subject: [PATCH 05/10] =?UTF-8?q?N=C2=B07383=20Show=20data=20table=20even?= =?UTF-8?q?=20when=20there=20is=20no=20data=20after=20filtering=20(#695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bricks/manage/layout-table.html.twig | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig index 9d018912f..2f01faf12 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig @@ -23,25 +23,23 @@ {% set iTableCount = 0 %} {% if aGroupingAreasData|length > 0 %} {% for aAreaData in aGroupingAreasData %} - {% if aAreaData.iItemsCount > 0 %} - {% set iTableCount = iTableCount + 1 %} -
-
-

{{ aAreaData.sTitle }}

- {% if bCanExport %} - - - - {% endif %} -
-
-
-
+ {% set iTableCount = iTableCount + 1 %} +
+
+

{{ aAreaData.sTitle }}

+ {% if bCanExport %} + + + + {% endif %}
- {% endif %} +
+
+
+
{% endfor %} {% endif %} From 73bb80ebeacbbb131860548aa29ef9132c03f80b Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Mon, 6 Jan 2025 11:31:22 +0100 Subject: [PATCH 06/10] =?UTF-8?q?N=C2=B07206=20-=20Force=20DBUpdate()=20wh?= =?UTF-8?q?en=20a=20transition=20is=20asked,=20and=20it=20leads=20to=20the?= =?UTF-8?q?=20same=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/dbobject.class.php | 8 ++++++++ tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php | 4 ++-- .../unitary-tests/core/TriggerOnStateEnterTest.php | 6 ++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/dbobject.class.php b/core/dbobject.class.php index bf51353eb..48b3364ed 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2867,6 +2867,14 @@ abstract class DBObject implements iDisplay protected function ListChangedValues(array $aProposal) { $aDelta = array(); + $sClass = get_class($this); + if (MetaModel::HasLifecycle($sClass) && utils::IsNotNullOrEmptyString($this->sStimulusBeingApplied)) { + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + if (!in_array($sStateAttCode, $aProposal)) { + // Same state but the transition was asked, act as if the state was changed + $aDelta[$sStateAttCode] = $this->m_aCurrValues[$sStateAttCode]; + } + } foreach ($aProposal as $sAtt => $proposedValue) { if (!array_key_exists($sAtt, $this->m_aOrigValues)) diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index 463d1c44b..52cbe3436 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -9,9 +9,9 @@ namespace Combodo\iTop\Test\UnitTest; use CMDBSource; use DeprecatedCallsLog; use MySQLTransactionNotClosedException; +use PHPUnit\Framework\TestCase; use ReflectionMethod; use SetupUtils; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpKernel\KernelInterface; use const DEBUG_BACKTRACE_IGNORE_ARGS; @@ -23,7 +23,7 @@ use const DEBUG_BACKTRACE_IGNORE_ARGS; * * @since 3.0.4 3.1.1 3.2.0 N°6658 move some setUp/tearDown code to the corresponding methods *BeforeClass to speed up tests process time. */ -abstract class ItopTestCase extends KernelTestCase +abstract class ItopTestCase extends TestCase { public const TEST_LOG_DIR = 'test'; diff --git a/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php b/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php index 32134c1e2..f7e878354 100644 --- a/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php +++ b/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php @@ -35,11 +35,9 @@ class TriggerOnStateEnterTest extends ItopDataTestCase $oUserRequest = $this->GivenUserRequest('new'); $oUserRequest->ApplyStimulus('ev_assign'); - $bTransitionned = $oUserRequest->ApplyStimulus('ev_reassign'); - $this->assertTrue($bTransitionned, 'The stimulus should have been accepted'); - return; + $bTransitioned = $oUserRequest->ApplyStimulus('ev_reassign'); + $this->assertTrue($bTransitioned, 'The stimulus should have been accepted'); - $this->MarkTestSkipped('This test fails because the trigger is not executed'); $this->AssertTriggerExecuted($iTrigger, 2, 'The trigger should have been executed twice'); } public function testIsTriggeredOnNewObject() From 301a7a92a040ce58ea4f1c2a0afb395672c3df56 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Mon, 6 Jan 2025 16:52:32 +0100 Subject: [PATCH 07/10] =?UTF-8?q?N=C2=B07145=20-=20UI=20-=20Init=20Value?= =?UTF-8?q?=20DateTime=20and=20Date=20with=20Day=20Time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/cmdbabstract.class.inc.php | 12 +- core/attributedef.class.inc.php | 22 +++- .../core/AttributeDefinitionTest.php | 109 +++++++++++++++++- 3 files changed, 138 insertions(+), 5 deletions(-) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 2acfd9f41..0d4ec9275 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -3439,8 +3439,18 @@ EOF } $sInputType = ''; $sInputId = 'att_'.$iFieldIndex; + $value = $this->Get($sAttCode); + $sDisplayValue = $this->GetEditValue($sAttCode); + if ($oAttDef instanceof AttributeDateTime && !$oAttDef->IsNullAllowed() && $value === $oAttDef->GetNullValue()) { + $value = $oAttDef->GetDefaultValue($this); + if ($value !== $oAttDef->GetNullValue()) { + // Set default date + $this->Set($sAttCode, $value); + $sDisplayValue = $this->GetEditValue($sAttCode); + } + } $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, - $this->Get($sAttCode), $this->GetEditValue($sAttCode), $sInputId, '', $iExpectCode, + $value, $sDisplayValue, $sInputId, '', $iExpectCode, $aArgs, true, $sInputType); $aAttrib = array( 'label' => ''.$oAttDef->GetLabel().'', diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 26e3095b2..c90328426 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -6347,7 +6347,11 @@ class AttributeDateTime extends AttributeDBField $oFormField = parent::MakeFormField($oObject, $oFormField); // After call to the parent as it sets the current value - $oFormField->SetCurrentValue($this->GetFormat()->Format($oObject->Get($this->GetCode()))); + $oValue = $oObject->Get($this->GetCode()); + if ($oValue === $this->GetNullValue()) { + $oValue = $this->GetDefaultValue($oObject); + } + $oFormField->SetCurrentValue($this->GetFormat()->Format($oValue)); return $oFormField; } @@ -6433,8 +6437,20 @@ class AttributeDateTime extends AttributeDBField public function GetDefaultValue(DBObject $oHostObject = null) { - if (!$this->IsNullAllowed()) { - return date($this->GetInternalFormat()); + $sDefaultValue = $this->Get('default_value'); + if (!$this->IsNullAllowed() && $sDefaultValue !== '') { + try { + $oDate = new DateTimeImmutable($sDefaultValue); + } + catch (Exception $e) { + IssueLog::Error($e->getMessage(), null, [ + 'class' => get_class($this), + 'name' => $this->GetCode(), + 'stack' => $e->getTraceAsString()]); + return $this->GetNullValue(); + } + + return $oDate->format($this->GetInternalFormat()); } return $this->GetNullValue(); } diff --git a/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php b/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php index b3dccfc54..e875fbacb 100644 --- a/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php +++ b/tests/php-unit-tests/unitary-tests/core/AttributeDefinitionTest.php @@ -2,6 +2,8 @@ namespace Combodo\iTop\Test\UnitTest\Core; +use AttributeDate; +use AttributeDateTime; use Change; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use MetaModel; @@ -229,7 +231,7 @@ PHP $this->assertEquals($bComputationExpected, $oAttDef->HasPHPComputation(), "Standard DataModel should be configured with property 'has_php_computation'=$sComputationExpected for $sClass:$sAttCode"); } - public function WithConstraintParameterProvider() + public static function WithConstraintParameterProvider() { return [ ['User', 'profile_list', true, true], @@ -238,4 +240,109 @@ PHP ['Ticket', 'functionalcis_list', false, true], ]; } + + public function testDateTimeEmptyDefaultReturnsNullAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDateTime('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + self::assertNull($defaultValue, 'Empty default value for DateTime attribute should give null default value'); + self::assertEmpty($oField->GetCurrentValue(), 'Empty default value for DateTime attribute should give empty form field'); + } + + public function testDateEmptyDefaultReturnsNullAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDate('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + self::assertNull($defaultValue, 'Empty default value for Date attribute should give null default value'); + self::assertEmpty($oField->GetCurrentValue(), 'Empty default value for DateTime attribute should give empty form field'); + } + + + public function testDateTimeNowAsDefaultGivesCurrentDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDateTime('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => 'now', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $sNow = date($oDateAttribute->GetInternalFormat()); + self::assertEquals($sNow, $defaultValue, 'Now as default value for DateTime attribute should give current date as default value'); + self::assertEquals($sNow, $oField->GetCurrentValue(), 'Now as default value for DateTime attribute should give current date as form field'); + } + + + public function testDateNowAsDefaultGivesCurrentDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDate('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => 'now', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $sNow = date($oDateAttribute->GetInternalFormat()); + self::assertEquals($sNow, $defaultValue, 'Now as default value for Date attribute should give current date as default value'); + self::assertEquals($sNow, $oField->GetCurrentValue(), 'Now as default value for Date attribute should give current date as form field'); + } + + public function testDateTimeIntervalAsDefaultGivesCorrectDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDateTime('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '+1day', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $oDate = new \DateTimeImmutable('+1day'); + $sExpected = $oDate->format($oDateAttribute->GetInternalFormat()); + self::assertEquals($sExpected, $defaultValue, 'Interval as default value for DateTime attribute should give correct date as default value'); + self::assertEquals($sExpected, $oField->GetCurrentValue(), 'Interval as default value for DateTime attribute should give correct date as form field'); + } + + public function testDateIntervalAsDefaultGivesCorrectDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDate('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '+1day', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $oDate = new \DateTimeImmutable('+1day'); + $sExpected = $oDate->format($oDateAttribute->GetInternalFormat()); + self::assertEquals($sExpected, $defaultValue, 'Interval as default value for Date attribute should give correct date as default value'); + self::assertEquals($sExpected, $oField->GetCurrentValue(), 'Interval as default value for Date attribute should give correct date as form field'); + } + } \ No newline at end of file From 6948c594c24c009f214ca6ef1d373b26aa5f6f64 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Tue, 7 Jan 2025 10:18:23 +0100 Subject: [PATCH 08/10] =?UTF-8?q?N=C2=B07145=20-=20UI=20-=20Init=20Value?= =?UTF-8?q?=20DateTime=20and=20Date=20with=20Day=20Time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/attributedef.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index c90328426..dc29a2289 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -6438,7 +6438,7 @@ class AttributeDateTime extends AttributeDBField public function GetDefaultValue(DBObject $oHostObject = null) { $sDefaultValue = $this->Get('default_value'); - if (!$this->IsNullAllowed() && $sDefaultValue !== '') { + if (!$this->IsNullAllowed() && utils::IsNotNullOrEmptyString($sDefaultValue)) { try { $oDate = new DateTimeImmutable($sDefaultValue); } From 346a8eadec55f33a0449b57eef1d834f4093851e Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Tue, 7 Jan 2025 11:29:33 +0100 Subject: [PATCH 09/10] =?UTF-8?q?N=C2=B05791=20-=20Obsolescence=20conditio?= =?UTF-8?q?n=20containing=20IN=20makes=20object=20creation=20fail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/oql/expression.class.inc.php | 13 ++++++++++++- .../unitary-tests/core/ExpressionEvaluateTest.php | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/oql/expression.class.inc.php b/core/oql/expression.class.inc.php index 88cc64b2e..ea797a2b5 100644 --- a/core/oql/expression.class.inc.php +++ b/core/oql/expression.class.inc.php @@ -575,6 +575,9 @@ class BinaryExpression extends Expression case 'LIKE': $sType = 'like'; break; + case 'IN': + $sType = 'in'; + break; default: throw new Exception("Operator '$sOperator' not yet supported"); } @@ -641,6 +644,9 @@ class BinaryExpression extends Expression $sEscaped = str_replace(array('%', '_', '\\\\.*', '\\\\.'), array('.*', '.', '%', '_'), $sEscaped); $result = (int) preg_match("/$sEscaped/i", $mLeft); break; + case 'in': + $result = in_array($mLeft, $mRight); + break; } return $result; } @@ -2250,7 +2256,12 @@ class ListExpression extends Expression */ public function Evaluate(array $aArgs) { - throw new Exception('list expression not yet supported'); + //throw new Exception('list expression not yet supported'); + $aResult = []; + foreach ($this->m_aExpressions as $oExpressions) { + $aResult[] = $oExpressions->Evaluate($aArgs); + } + return $aResult; } /** diff --git a/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php b/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php index 1d6cb5307..6b9847fe0 100644 --- a/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php +++ b/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php @@ -90,7 +90,7 @@ class ExpressionEvaluateTest extends ItopDataTestCase { $aExpressions = array( // Test case to isolate for troubleshooting purposes - array('1+1', 2), + array("'a' IN ('a', 'b')", true), ); } else @@ -141,6 +141,9 @@ class ExpressionEvaluateTest extends ItopDataTestCase array('"2020-06-12 17:35:13" < "2020-06-12"', 0), array('"2020-06-12 00:00:00" = "2020-06-12"', 0), + // IN operator + array("'a' IN ('a', 'b')", true), + // Logical operators array('0 AND 0', 0), array('1 AND 0', 0), From 0e8ddf990c2a0b181282f0d9d01c43468ca9c21e Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Tue, 7 Jan 2025 13:21:13 +0100 Subject: [PATCH 10/10] =?UTF-8?q?N=C2=B07145=20-=20UI=20-=20Init=20Value?= =?UTF-8?q?=20DateTime=20and=20Date=20with=20Day=20Time=20(Fix=20Event=20c?= =?UTF-8?q?lass)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/event.class.inc.php | 2 +- tests/php-unit-tests/unitary-tests/core/EventIssueTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/event.class.inc.php b/core/event.class.inc.php index 90d900257..6a731f1cc 100644 --- a/core/event.class.inc.php +++ b/core/event.class.inc.php @@ -39,7 +39,7 @@ class Event extends DBObject implements iDisplay MetaModel::Init_Params($aParams); //MetaModel::Init_InheritAttributes(); 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 AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"now", "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); // MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); diff --git a/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php b/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php index 62faa35a4..400a37180 100644 --- a/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php +++ b/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php @@ -62,7 +62,7 @@ class EventIssueTest extends ItopDataTestCase $oEventIssue->DBInsert(); } catch (CoreException $e) { - $this->fail('we should be able to persist the object though it contains long values in its attributes'); + $this->fail('we should be able to persist the object though it contains long values in its attributes: '.$e->getMessage()); } $this->assertTrue(true); }