Compare commits

..

3 Commits

46 changed files with 429 additions and 588 deletions

View File

@@ -145,7 +145,7 @@ class DataFeatureRemovalController extends Controller
$bForceCompilation = Session::Get('bForceCompilation', false);
try {
$this->Compile($aRemoveExtensionCodes, $bForceCompilation);
$this->Compile($aAddedExtensions, $aRemoveExtensionCodes, $bForceCompilation);
} catch (CoreException $e) {
$aParams['DataFeatureRemovalErrorMessage'] = $e->getHtmlDesc();
$this->DisplayPage($aParams, 'AnalysisResult');
@@ -162,7 +162,7 @@ class DataFeatureRemovalController extends Controller
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
$aHiddenInputs['selected_extensions'] = $this->ConvertIntoSetupFormat($aSelectedExtensions);
$oRunTimeEnvironment = $this->GetRuntimeEnvironment($aRemovedExtensions);
$oRunTimeEnvironment = $this->GetRuntimeEnvironment($aAddedExtensions, $aRemovedExtensions);
$aSearchDirs = [$oRunTimeEnvironment->GetBuildDir()];
$aSelectedModules = $oRunTimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions, $aSearchDirs);
$aHiddenInputs['selected_modules'] = $this->ConvertIntoSetupFormat($aSelectedModules);
@@ -213,13 +213,14 @@ class DataFeatureRemovalController extends Controller
}
/**
* @param array $aAddedExtensions
* @param array $aRemovedExtensions
* @param bool $bForceCompilation
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
private function Compile(array $aRemovedExtensions, bool $bForceCompilation = true): void
private function Compile(array $aAddedExtensions, array $aRemovedExtensions, bool $bForceCompilation = true): void
{
$sSourceEnv = MetaModel::GetEnvironment();
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
@@ -234,15 +235,15 @@ class DataFeatureRemovalController extends Controller
null,
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
);
$this->GetRuntimeEnvironment($aRemovedExtensions)->CompileFrom($sSourceEnv);
$this->GetRuntimeEnvironment($aAddedExtensions, $aRemovedExtensions)->CompileFrom($sSourceEnv);
}
}
private function GetRuntimeEnvironment(array $aRemovedExtensions): RunTimeEnvironment
private function GetRuntimeEnvironment(array $aAddedExtensions, array $aRemovedExtensions): RunTimeEnvironment
{
if (is_null($this->oRuntimeEnvironment)) {
$sSourceEnv = MetaModel::GetEnvironment();
$this->oRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $aRemovedExtensions);
$this->oRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $aAddedExtensions, $aRemovedExtensions);
}
return $this->oRuntimeEnvironment;

View File

@@ -460,18 +460,6 @@
<extkey_attcode>parent_incident_id</extkey_attcode>
<target_attcode>ref</target_attcode>
</field>
<field id="parent_request_id" xsi:type="AttributeExternalKey">
<filter><![CDATA[SELECT UserRequest WHERE id != :this->id AND status NOT IN ('rejected','resolved','closed')]]></filter>
<dependencies/>
<sql>parent_request_id</sql>
<target_class>UserRequest</target_class>
<is_null_allowed>true</is_null_allowed>
<on_target_delete>DEL_MANUAL</on_target_delete>
</field>
<field id="parent_request_ref" xsi:type="AttributeExternalField">
<extkey_attcode>parent_request_id</extkey_attcode>
<target_attcode>ref</target_attcode>
</field>
<field id="parent_problem_id" xsi:type="AttributeExternalKey">
<sql>parent_problem_id</sql>
<target_class>Problem</target_class>
@@ -999,9 +987,6 @@
<attribute id="parent_incident_id">
<read_only/>
</attribute>
<attribute id="parent_request_id">
<read_only/>
</attribute>
<attribute id="parent_change_id">
<read_only/>
</attribute>
@@ -1316,20 +1301,90 @@
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildRequestLog()
{
if (MetaModel::IsValidClass('UserRequest')) {
return $this->UpdateChildTicketLog('UserRequest', 'parent_incident_id', ['public_log' => 'public_log', 'private_log' => 'private_log'] );
}
return true;
if (!MetaModel::IsValidClass('UserRequest')) return true; // Do nothing
$oLog = $this->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;
}]]></code>
</method>
<method id="UpdateChildIncidentLog">
<static>false</static>
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildIncidentLog()
{
return $this->UpdateChildTicketLog('Incident', 'parent_incident_id', ['public_log' => 'public_log', 'private_log' => 'private_log']);
$oLog = $this->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;
}]]></code>
</method>
<method id="ComputeImpactedItems">
@@ -1503,9 +1558,6 @@
<item id="parent_incident_id">
<rank>10</rank>
</item>
<item id="parent_request_id">
<rank>15</rank>
</item>
<item id="parent_problem_id">
<rank>20</rank>
</item>

View File

@@ -44,8 +44,6 @@ Dict::Add('EN US', 'English', 'English', [
'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' => '<i><u>Public log entry from parent Incident %2$s:</u></i><br><br>',
'Class:Incident/Method:UpdateChildTicketWith:private_log' => '<i>Private log entry from parent Incident [[Incident:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions
@@ -195,10 +193,6 @@ Dict::Add('EN US', 'English', 'English', [
'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',

View File

@@ -179,19 +179,15 @@ Dict::Add('FR FR', 'French', 'Français', [
'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. incident parent',
'Class:Incident/Attribute:parent_incident_ref' => 'Référence 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' => 'Réf. changement parent',
'Class:Incident/Attribute:parent_change_ref' => 'Ref 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. problème lié',
'Class:Incident/Attribute:parent_problem_ref' => 'Référence problème lié',
'Class:Incident/Attribute:parent_problem_ref+' => '',
'Class:Incident/Attribute:related_request_list' => 'Requêtes filles',
'Class:Incident/Attribute:related_request_list+' => '',

View File

@@ -85,13 +85,6 @@
<class id="Attachment"/>
</classes>
</group>
<group id="Ticket" _delta="define">
<classes>
<class id="Ticket"/>
<class id="WorkOrder"/>
<class id="Attachment"/>
</classes>
</group>
<group id="Portal" _delta="define">
<classes>
<class id="lnkFunctionalCIToTicket"/>
@@ -212,60 +205,6 @@
</group>
</groups>
<profiles>
<profile id="5500" _delta="define">
<name>Configuration ReadOnly</name>
<description>This read-only profile allows to see CIs objects.</description>
<groups>
<group id="Configuration">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="5501" _delta="define">
<name>Ticket ReadOnly</name>
<description>This read-only profile allows to see Ticket objects.</description>
<groups>
<group id="Ticket">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="5502" _delta="define">
<name>Service Catalog ReadOnly</name>
<description>This read-only profile allows to see Service Catalog objects.</description>
<groups>
<group id="Service">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="117" _delta="define">
<name>SuperUser</name>
<description>This profile allows all actions which are not Administrator restricted.</description>

View File

@@ -1451,17 +1451,44 @@
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildRequestLog()
{
return $this->UpdateChildTicketLog('UserRequest', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log' ]);
}]]></code>
</method>
<method id="UpdateChildIncidentLog">
<static>false</static>
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildIncidentLog()
{
return $this->UpdateChildTicketLog('Incident', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log']);
$oLog = $this->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;
}]]></code>
</method>
<method id="ComputeImpactedItems">
@@ -1499,7 +1526,6 @@
parent::OnUpdate();
$this->Set('last_update', time());
$this->UpdateChildRequestLog();
$this->UpdateChildIncidentLog();
}]]></code>
</method>
</methods>

View File

@@ -37,8 +37,6 @@ Dict::Add('EN US', 'English', 'English', [
'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' => '<i><u>Public log automatic copy from parent User Request %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Private log automatic copy from parent User Request [[UserRequest:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions

View File

@@ -42,8 +42,6 @@ Dict::Add('FR FR', 'French', 'Français', [
'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' => '<i><u>Copie automatique du log public de la demande parente %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Copie automatique du log privé de la demande parente [[UserRequest:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions

View File

@@ -1486,8 +1486,44 @@
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildRequestLog()
{
return $this->UpdateChildTicketLog('UserRequest', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log'] );
{
$oLog = $this->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;
}]]></code>
</method>
<method id="ComputeImpactedItems">

View File

@@ -41,8 +41,6 @@ Dict::Add('EN US', 'English', 'English', [
'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' => '<i><u>Public log automatic copy from parent User Request %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Private log automatic copy from parent User Request [[UserRequest:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions

View File

@@ -42,8 +42,6 @@ Dict::Add('FR FR', 'French', 'Français', [
'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' => '<i><u>Copie automatique du log public de la demande parente %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Copie automatique du log privé de la demande parente [[UserRequest:%1$s]]:</i><br><br>',
'Menu:UserRequest:MyWorkOrders' => 'Tâches qui me sont assignées',
'Menu:UserRequest:MyWorkOrders+' => '',
'Class:Problem:KnownProblemList' => 'Problèmes connus',

View File

@@ -351,63 +351,6 @@
}]]></code>
<arguments/>
</method>
<method id="UpdateChildTicketLog">
<comment><![CDATA[/**
*
* Remove the current user associated Person from the contacts_list of this Ticket
* No error if there is no associated Person or if the Person is not in the list
* @return bool Return true
* @param string $sChildClass The class name of the child ticket (e.g. 'Incident', 'UserRequest', etc.)
* @param string $sChildParentAttCode The external key in the child class pointing to the parent ticket (e.g. 'parent_request_id')
* @param array $aLogAttCodes An array of parent caselog attribute codes and for each the corresponding child log attribute code to update (e.g. ['public_log' => '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:<parent_ticket_class>/Method:UpdateChildTicketWith:<caselog_attcode>' 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: <log entry from parent.public_log>" in the child private_log
*
*/]]>
</comment>
<static>false</static>
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildTicketLog($sChildClass, $sChildParentAttCode, $aLogAttCodes)
{
if (!MetaModel::IsValidClass($sChildClass) || (!MetaModel::IsValidAttCode($sChildClass, $sChildParentAttCode))) {
ErrorLog::Debug("Invalid class ($sChildClass) or attribute code ($sChildParentAttCode) provided to UpdateChildTicketLog","DataModel");
return true; // Do nothing
}
$oAttDef = MetaModel::GetAttributeDef($sChildClass, $sChildParentAttCode);
if (!$oAttDef instanceof AttributeExternalKey || ($oAttDef->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;
}]]></code>
</method>
</methods>
<presentation>
<details>

View File

@@ -21,5 +21,4 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -19,5 +19,4 @@ Dict::Add('EN US', 'English', 'English', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('FR FR', 'French', 'Français', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'Plus d\'informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Forcer la désinstallation',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Plus d\'actions',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,5 +21,4 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -759,7 +759,7 @@ class iTopExtensionsMap
public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSourceOnly = iTopExtension::SOURCE_REMOTE)
{
foreach ($this->GetAllExtensions() as $oExtension) {
if (($oExtension->sSource == $sInSourceOnly) &&
if (/*($oExtension->sSource == $sInSourceOnly) &&*/
($oExtension->bMarkedAsChosen == true) &&
(array_key_exists($sModuleNameToFind, $oExtension->aModuleVersion))) {
return true;

View File

@@ -2,6 +2,7 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use Config;
use iTopExtensionsMap;
use RunTimeEnvironment;
use SetupUtils;
@@ -9,15 +10,17 @@ use SetupUtils;
class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
{
protected array $aExtensionsToRemoveByCode;
protected array $aExtensionCodesToAddByCode;
/**
* Toolset for building a run-time environment
*
* @param string $sSourceEnv: environment from which setup is inspired to simulate extension removal and usee CompileFrom...
*/
public function __construct($sSourceEnv = ITOP_DEFAULT_ENV, array $aExtensionCodesToRemove = [])
public function __construct($sSourceEnv = ITOP_DEFAULT_ENV, array $aExtensionCodesToAdd = [], array $aExtensionCodesToRemove = [])
{
parent::__construct($sSourceEnv, false);
$this->aExtensionCodesToAddByCode = $aExtensionCodesToAdd;
$this->aExtensionsToRemoveByCode = $aExtensionCodesToRemove;
$this->Prepare($sSourceEnv, $this->sBuildEnv);
}
@@ -34,13 +37,18 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sBuildEnv-modules");
SetupUtils::copydir(APPROOT."/conf/$sSourceEnv", APPROOT."/conf/$sBuildEnv");
$this->DeclareExtensionAsRemoved($this->aExtensionsToRemoveByCode);
}
$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
$sSourceDir = $oSourceConfig->Get('source_dir');
list($aExtraDirs, ) = $this->GetDirsToCompile($sSourceDir, $sSourceEnv);
private function DeclareExtensionAsRemoved(array $aExtensionCodes): void
{
$oExtensionsMap = new iTopExtensionsMap($this->sBuildEnv);
$oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes);
$this->InitExtensionMap($aExtraDirs, $oSourceConfig);
$this->GetExtensionMap()->DeclareExtensionAsRemoved($this->aExtensionsToRemoveByCode);
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if (array_key_exists($oExtension->sCode, $this->aExtensionCodesToAddByCode)) {
$oExtension->bMarkedAsChosen = true;
}
}
}
public function Cleanup(): void

View File

@@ -17,6 +17,11 @@ class SetupAudit extends AbstractSetupAudit
$this->sEnvAfter = $sEnvAfter ?? "$sEnvBefore-build";
}
public function GetEnvAfter(): string
{
return $this->sEnvAfter;
}
public function ComputeClasses(): void
{
if ($this->bClassesInitialized) {

View File

@@ -71,15 +71,37 @@ class RunTimeEnvironment
return $this->oExtensionsMap;
}
public function InitExtensionMap($aExtraDirs, $oSourceConfig)
protected function GetDirsToCompile(string $sSourceDir, string $sSourceEnv): array
{
// Actually read the modules available for the build environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen all the "remote" modules present in the
// build environment (data/<build-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->oExtensionsMap = new iTopExtensionsMap($this->sBuildEnv, $aExtraDirs);
$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
$sSourceDirFull = APPROOT.$sSourceDir;
if (!is_dir($sSourceDirFull)) {
throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)");
}
$aDirsToCompile = [$sSourceDirFull];
if (is_dir(APPROOT.'extensions')) {
$aDirsToCompile[] = APPROOT.'extensions';
}
$sExtraDir = utils::GetDataPath().$this->sBuildEnv.'-modules/';
if (is_dir($sExtraDir)) {
$aDirsToCompile[] = $sExtraDir;
}
$aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile);
$aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs);
return [$aExtraDirs, $aDirsToCompile];
}
public function InitExtensionMap(array $aExtraDirs, Config $oSourceConfig)
{
if (is_null($this->oExtensionsMap)) {
// Actually read the modules available for the build environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen all the "remote" modules present in the
// build environment (data/<build-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->oExtensionsMap = new iTopExtensionsMap($this->sBuildEnv, $aExtraDirs);
$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
}
}
/**
@@ -444,32 +466,12 @@ class RunTimeEnvironment
/**
* Get the installed modules (only the installed ones)
* @return \MFModule[]
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir): array
{
$sSourceDirFull = APPROOT.$sSourceDir;
if (!is_dir($sSourceDirFull)) {
throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)");
}
$aDirsToCompile = [$sSourceDirFull];
if (is_dir(APPROOT.'extensions')) {
$aDirsToCompile[] = APPROOT.'extensions';
}
$sExtraDir = utils::GetDataPath().$this->sBuildEnv.'-modules/';
if (is_dir($sExtraDir)) {
$aDirsToCompile[] = $sExtraDir;
}
$aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile);
$aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs);
list($aExtraDirs, $aDirsToCompile) = $this->GetDirsToCompile($sSourceDir, $sSourceEnv);
$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
// Actually read the modules available for the build environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen all the "remote" modules present in the
// build environment (data/<build-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->InitExtensionMap($aExtraDirs, $oSourceConfig);
$this->GetExtensionMap()->LoadChoicesFromDatabase($oSourceConfig);
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
@@ -477,12 +479,10 @@ class RunTimeEnvironment
$this->GetExtensionMap()->MarkAsChosen($oExtension->sCode);
}
}
$aModulesToLoad = $this->GetModulesToLoad($this->sFinalEnv, $aDirsToCompile);
$aAvailableModules = $this->AnalyzeInstallation($oSourceConfig, $aDirsToCompile, true, $aModulesToLoad);
// Do load the required modules
//
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
$aRet = [];
@@ -1700,21 +1700,29 @@ class RunTimeEnvironment
}
$aExtensionDirs = [];
$aFromSelectedExtensionModules=[];
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if ($oExtension->bMarkedAsChosen && is_dir($oExtension->sSourceDir)) {
$aExtensionDirs [] = $oExtension->sSourceDir;
$aFromSelectedExtensionModules = array_merge($aFromSelectedExtensionModules, $oExtension->aModules);
}
}
SetupLog::Info(__METHOD__, null, ['ext_dirs' => $aExtensionDirs]);
$aModuleIdsToLoad = InstallationChoicesToModuleConverter::GetInstance()->GetModules($aChoices, $aSearchDirs, $sInstallFilePath, $aExtensionDirs);
$aModulesToLoad = [];
foreach ($aModuleIdsToLoad as $sModuleId) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
$aModulesToLoad[] = $sModuleName;
}
foreach ($aFromSelectedExtensionModules as $sModuleName){
if (! in_array($sModuleName, $aModulesToLoad)){
$aModulesToLoad[] = $sModuleName;
}
}
return $aModulesToLoad;
}

View File

@@ -258,7 +258,7 @@ class InstallationFileService
public function ProcessDefaultModules(): void
{
$sProductionModuleDir = APPROOT.'data/'.$this->sTargetEnvironment.'-modules/';
$oConfig = new Config(APPCONF.$this->sTargetEnvironment.'/'.ITOP_CONFIG_FILE, false);
$oConfig = new Config(APPCONF.$this->sTargetEnvironment.'/'.ITOP_CONFIG_FILE);
$aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation($oConfig, $this->GetExtraDirs());

View File

@@ -18,12 +18,10 @@ class Toggler extends Input
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/input/input-toggler';
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'base/components/input/input-toggler';
protected string $sTooltip = '';
public function __construct(?string $sId = null, string $sTooltip = '')
public function __construct(?string $sId = null)
{
parent::__construct($sId);
$this->SetType('checkbox');
$this->SetTooltip($sTooltip);
}
public function SetIsToggled(bool $bIsToggled): static
@@ -35,13 +33,4 @@ class Toggler extends Input
{
return $this->IsChecked();
}
public function GetTooltip(): string
{
return $this->sTooltip;
}
public function SetTooltip($sTooltip): void
{
$this->sTooltip = $sTooltip;
}
}

View File

@@ -182,14 +182,13 @@ class ExtensionDetails extends UIContentBlock
$sName = 'aSelectedExtensions['.$this->GetCode().']';
$this->oToggler = new Toggler();
$this->oToggler->SetName($sName);
$this->oToggler->SetTooltip(Dict::Format('UI:Layout:ExtensionsDetails:TogglerTooltip' ,$this->GetLabel()));
$this->oToggler->AddCSSClass('toggler-install');
}
protected function InitializePopoverMenu()
{
$this->oPopoverMenu = new PopoverMenu();
$oPopoverOpenButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S('UI:Layout:ExtensionsDetails:MoreActions'), 'dropdown-menu-'.$this->GetCode());
$oPopoverOpenButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S('UI:Layout:ExtensionsDetails:MoreActions'));
$this->oPopoverMenu->SetTogglerFromBlock($oPopoverOpenButton);
$this->oMoreActions = new UIContentBlock();
$this->oMoreActions->AddSubBlock($this->oPopoverMenu);

View File

@@ -1794,11 +1794,9 @@ JS;
}
/**
* @param bool $bReturnOutput
*
* @throws \Exception
*/
protected function output_dict_entries($bReturnOutput = false)
protected function output_dict_entries()
{
if ($this->sContentType != 'text/plain' && $this->sContentType != 'application/json' && $this->sContentType != 'application/javascript') {
/** @var \iBackofficeDictEntriesExtension $oExtensionInstance */

View File

@@ -2,7 +2,7 @@
{% block iboInput %}
<span class="ibo-toggler--wrapper">
{{ parent() }}
<a class="ibo-toggler--slider" title="{{ oUIBlock.GetTooltip() }}" href="#"></a>
<span class="ibo-toggler--slider"></span>
<input class="ibo-toggler--hidden" type="hidden" name="{{ oUIBlock.GetName() }}" value="{% if oUIBlock.IsChecked() %}on{% else %}off{% endif %}"/>
</span>
{% endblock %}

View File

@@ -87,6 +87,8 @@ abstract class ItopDataTestCase extends ItopTestCase
*/
public const DEFAULT_TEST_ENVIRONMENT = 'production';
public const USE_TRANSACTION = true;
public const CREATE_TEST_ORG = false;
protected static $aURP_Profiles = [
'Administrator' => 1,
'Portal user' => 2,
@@ -100,15 +102,9 @@ abstract class ItopDataTestCase extends ItopTestCase
'Service Manager' => 10,
'Document author' => 11,
'Portal power user' => 12,
'Business partner user' => 40,
'REST Services User' => 1024,
'Configuration ReadOnly' => 5500,
'Ticket ReadOnly' => 5501,
'Service Catalog ReadOnly' => 5502,
];
public const CREATE_TEST_ORG = false;
/**
* This method is called before the first test of this test class is run (in the current process).
*/
@@ -1467,42 +1463,16 @@ abstract class ItopDataTestCase extends ItopTestCase
]);
}
/**
* @description To avoid adding finalclasses parameters to GivenUserInDB
* @param string $sPassword
* @param array $aProfiles Profile names Example: ['Administrator']
* @param bool $bReturnLogin
*
* @return string|int The unique login
* @throws \Exception
*/
protected function GivenTokenUserInDB(array $aProfiles, bool $bReturnLogin = true): string|int
{
$sLogin = 'demo_test_'.uniqid(__CLASS__, true);
$aProfileList = array_map(function ($sProfileId) {
return 'profileid:'.self::$aURP_Profiles[$sProfileId];
}, $aProfiles);
$iUser = $this->GivenObjectInDB('UserToken', [
'login' => $sLogin,
'language' => 'EN US',
'profile_list' => $aProfileList,
]);
return $bReturnLogin ? $sLogin : $iUser;
}
/**
* @param string $sPassword
* @param array $aProfiles Profile names Example: ['Administrator']
* @param string|null $sLogin
* @param string|null $sUserId
* @param bool $bReturnLogin
*
* @return string The unique login
* @throws \Exception
*/
protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null, bool $bReturnLogin = true): string
protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null): string
{
if (is_null($sLogin)) {
$sLogin = 'demo_test_'.uniqid(__CLASS__, true);
@@ -1519,7 +1489,7 @@ abstract class ItopDataTestCase extends ItopTestCase
'profile_list' => $aProfileList,
]);
return $bReturnLogin ? $sLogin : $sUserId;
return $sLogin;
}
/**

View File

@@ -108,7 +108,7 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
/**
* @inheritDoc
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir): array
{
\SetupLog::Info(__METHOD__);
$aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir);

View File

@@ -29,11 +29,11 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreCannotSaveObjectException;
use CoreException;
use DBObject;
use DBObjectSearch;
use DBObjectSet;
use DeleteException;
use Dict;
use MetaModel;
use UserLocal;
use UserRights;
@@ -81,54 +81,6 @@ class UserRightsTest extends ItopDataTestCase
return $oUser;
}
/**
* @param array $aProfileIds
* @param array $aShouldBeAllowedToSeeClass
* @param array $aShouldBeAllowedToEditClass
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \DictExceptionUnknownLanguage
* @throws \MySQLException
* @throws \OQLException
* @dataProvider ReadOnlyProvider
*/
public function testReadOnlyUser(array $aProfileIds, array $aShouldBeAllowedToSeeClass, array $aShouldBeAllowedToEditClass): void
{
$oUser = $this->GivenUserWithProfiles('test1', $aProfileIds);
$oUser->DBInsert();
$_SESSION = [];
UserRights::Login($oUser->Get('login'));
$aClassesToTest = ['FunctionalCI', 'Ticket', 'ServiceFamily'];
foreach ($aClassesToTest as $sClass) {
$bShouldBeAllowedToSee = in_array($sClass, $aShouldBeAllowedToSeeClass);
$bIsAllowedReading = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_READ);
$this->assertSame(
$bShouldBeAllowedToSee,
$bIsAllowedReading,
"User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToSee ? "" : "NOT ")."be allowed to see class $sClass"
);
$bShouldBeAllowedToEdit = in_array($sClass, $aShouldBeAllowedToEditClass);
$bIsAllowedEditing = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY);
$this->assertSame(
$bIsAllowedEditing,
$bShouldBeAllowedToEdit,
"User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToEdit ? "" : "NOT ")."be allowed to edit class $sClass"
);
}
}
protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DBObject
{
$oProfiles = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class));
@@ -481,7 +433,7 @@ class UserRightsTest extends ItopDataTestCase
$oUser = $this->GivenUserWithProfiles('test1', [$iProfileId, 2]);
$this->expectException(CoreCannotSaveObjectException::class);
$this->expectExceptionMessage(Dict::Format('Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice', PORTAL_PROFILE_NAME));
$this->expectExceptionMessage('Profile "Portal user" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)');
$oUser->DBInsert();
}
@@ -620,82 +572,4 @@ class UserRightsTest extends ItopDataTestCase
$oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]);
static::assertNull($oUser, 'FindUser should return null when the login is unknown');
}
protected function ReadOnlyProvider(): array
{
return [
'CI' => [
'ProfilesId' => [
5500,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets' => [
'ProfilesId' => [
5501,
],
'ShouldBeAllowedToSeeClasses' => [
'Ticket',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Catalog' => [
'ProfilesId' => [
5502,
],
'ShouldBeAllowedToSeeClasses' => [
'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'CI and Tickets' => [
'ProfilesId' => [
5500, 5501,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket',
],
'ShouldBeAllowedToEditClasses' => [],
],
'CI and Catalog' => [
'ProfilesId' => [
5500, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets and Catalog' => [
'ProfilesId' => [
5501, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets and Catalog + profile Ccnfiguration Manager' => [
'ProfilesId' => [
5501, 5502, 3,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => ['FunctionalCI'],
],
'CI, Tickets and Catalog' => [
'ProfilesId' => [
5500, 5501, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
];
}
}

View File

@@ -1,166 +0,0 @@
<?php
// Copyright (c) 2010-2024 Combodo SAS
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
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;
}
}

View File

@@ -9,11 +9,14 @@ use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use MetaModel;
use SetupUtils;
class SetupAuditTest extends ItopCustomDatamodelTestCase
{
public const ENVT = 'php-unit-extensionremoval-tests';
private ?string $sDirPathToRemoveFromExtensionFolder = null;
public function GetDatamodelDeltaAbsPath(): string
{
//no delta: empty path provided
@@ -44,6 +47,14 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
$this->RequireOnceItopFile('/setup/feature_removal/DryRemovalRuntimeEnvironment.php');
}
protected function tearDown(): void
{
parent::tearDown();
if (!is_null($this->sDirPathToRemoveFromExtensionFolder) && is_dir($this->sDirPathToRemoveFromExtensionFolder)) {
SetupUtils::rrmdir($this->sDirPathToRemoveFromExtensionFolder);
}
}
public function GetTestEnvironment(): string
{
return self::ENVT;
@@ -51,7 +62,11 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
public function testComputeDryRemoval()
{
$oDryRemovalRuntimeEnvt = new DryRemovalRuntimeEnvironment($this->GetTestEnvironment(), ['nominal_ext1', 'finalclass_ext2']);
$this->sDirPathToRemoveFromExtensionFolder = APPROOT.'/extensions/finalclass_ext3';
SetupUtils::copydir(__DIR__.'/other_features/finalclass_ext3', $this->sDirPathToRemoveFromExtensionFolder);
clearstatcache();
$oDryRemovalRuntimeEnvt = new DryRemovalRuntimeEnvironment($this->GetTestEnvironment(), ['finalclass_ext3' => 'Ext For Test3'], ['nominal_ext1', 'finalclass_ext2']);
$oDryRemovalRuntimeEnvt->CompileFrom($this->GetTestEnvironment());
$oSetupAudit = new SetupAudit($this->GetTestEnvironment());
@@ -67,6 +82,19 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
"FinalClassFeature2Module1MyFinalClassFromLocation" => 0,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->RunDataAudit());
$aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($oSetupAudit->GetEnvAfter());
$expected = [
"FinalClassFeature3Module1MyClass",
"FinalClassFeature3Module1MyFinalClassFromLocation",
];
foreach ($expected as $sAddedClass) {
$this->assertContains(
$sAddedClass,
$aClassesAfter,
"After DryRemoval compilation DM should contain classes coming from finalclass_ext3 extension"
);
}
}
public function testGetRemovedClassesFromSetupWizard()

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>finalclass_ext3</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test3]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>finalclass_ext3_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="FinalClassFeature3Module1MyFinalClassFromLocation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature3Module1MyFinalClassFromLocation</db_table>
<naming>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name2" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name2" xsi:type="AttributeString">
<sql>name2</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
<methods/>
<presentation/>
<parent>Location</parent>
</class>
<class id="FinalClassFeature3Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature3Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

View File

@@ -0,0 +1,16 @@
<?php
//// PHP Data Model definition file
//
//// WARNING - WARNING - WARNING
//// DO NOT EDIT THIS FILE (unless you know what you are doing)
////
//// If you use supply a datamodel.xxxx.xml file with your module
//// the this file WILL BE overwritten by the compilation of the
//// module (during the setup) if the datamodel.xxxx.xml file
//// contains the definition of new classes or menus.
////
//// The recommended way to define new classes (for iTop 2.0) is via the XML definition.
//// This file remains in the module's template only for the cases where there is:
//// - either no new class or menu defined in the XML file
//// - or no XML file at all supplied by the module

View File

@@ -0,0 +1,51 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'finalclass_ext3_module1/6.6.6',
[
// Identification
//
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.finalclass_ext3_module1.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [// Module specific settings go here, if any
],
]
);