From bec8b57fe15ab110b2320cf1d0e232ea27035408 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Wed, 23 Mar 2011 16:22:35 +0000 Subject: [PATCH] Implementation of bulk modify and bulk apply stimulus... to be tested ! SVN:trunk[1145] --- application/cmdbabstract.class.inc.php | 124 ++- application/displayblock.class.inc.php | 42 +- application/itopwebpage.class.inc.php | 15 + application/ui.extkeywidget.class.inc.php | 40 +- application/ui.htmleditorwidget.class.inc.php | 7 +- application/ui.passwordwidget.class.inc.php | 19 +- application/webpage.class.inc.php | 3 + application/wizardhelper.class.inc.php | 5 + core/dbobject.class.php | 2 +- core/dbobjectset.class.php | 103 ++ core/metamodel.class.php | 15 +- core/userrights.class.inc.php | 165 ++++ css/light-grey.css | 32 +- dictionaries/dictionary.itop.ui.php | 21 + images/transp-lock.png | Bin 390 -> 406 bytes js/extkeywidget.js | 21 + js/forms-json-utils.js | 45 +- js/utils.js | 72 ++ js/wizardhelper.js | 17 +- pages/UI.php | 934 +++++++++++++++--- portal/index.php | 2 +- 21 files changed, 1458 insertions(+), 226 deletions(-) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 72a6e1fbf..8b0dd75c8 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -76,7 +76,15 @@ interface iDisplay abstract class cmdbAbstractObject extends CMDBObject implements iDisplay { protected $m_iFormId; // The ID of the form used to edit the object (when in edition mode !) + static $iGlobalFormId = 1; + /** + * returns what will be the next ID for the forms + */ + public static function GetNextFormId() + { + return 1 + self::$iGlobalFormId; + } public static function GetUIPage() { return '../pages/UI.php'; @@ -380,7 +388,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay $aReasons = array(); $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); $sSynchroIcon = ''; - if ($iSynchroFlags & OPT_ATT_READONLY) + if ($iSynchroFlags & OPT_ATT_SLAVE) { $sSynchroIcon = " "; $sTip = ''; @@ -391,7 +399,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay $oPage->add_ready_script("$('#synchro_$iInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); } - $val['value'] .= $sSynchroIcon; + $val['comments'] = $sSynchroIcon; // The field is visible, add it to the current column $aDetails[$sTab][$sColIndex][] = $val; $iInputId++; @@ -555,7 +563,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay { if (!$bSingleSelectMode) { - $aAttribs['form::select'] = array('label' => "", 'description' => Dict::S('UI:SelectAllToggle+')); + $aAttribs['form::select'] = array('label' => "", 'description' => Dict::S('UI:SelectAllToggle+')); } else { @@ -596,13 +604,21 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay } if ($bSelectMode) { - if ($bSingleSelectMode) + if (array_key_exists('selection_enabled', $aExtraParams) && isset($aExtraParams['selection_enabled'][$oObj->GetKey()])) { - $aRow['form::select'] = "GetKey()."\">"; + $sDisabled = ($aExtraParams['selection_enabled'][$oObj->GetKey()]) ? '' : ' disabled="disabled"'; } else { - $aRow['form::select'] = "GetKey()."\">"; + $sDisabled = ''; + } + if ($bSingleSelectMode) + { + $aRow['form::select'] = "GetKey()."\">"; + } + else + { + $aRow['form::select'] = "GetKey()."\">"; } } foreach($aList as $sAttCode) @@ -1238,6 +1254,7 @@ EOF $sSeconds = ""; $sHidden = ""; $sHTMLValue = Dict::Format('UI:DurationForm_Days_Hours_Minutes_Seconds', $sDays, $sHours, $sMinutes, $sSeconds).$sHidden." ".$sValidationField; + $oPage->add_ready_script("$('#{$iId}').bind('update', function(evt, sFormId) { return ToggleDurationField('$iId'); });"); break; case 'Password': @@ -1304,6 +1321,7 @@ EOF $iMaxComboLength = $oAttDef->GetMaximumComboLength(); $oWidget = new UIExtKeyWidget($sAttCode, $sClass, $oAttDef->GetLabel(), $aAllowedValues, $value, $iId, $bMandatory, $sNameSuffix, $sFieldPrefix, $sFormPrefix); $sHTMLValue = $oWidget->Display($oPage, $aArgs); + $sHTMLValue .= "\n"; break; case 'String': @@ -1362,14 +1380,15 @@ EOF public function DisplayModifyForm(WebPage $oPage, $aExtraParams = array()) { - static $iGlobalFormId = 1; - $iGlobalFormId++; + self::$iGlobalFormId++; $sPrefix = ''; if (isset($aExtraParams['formPrefix'])) { $sPrefix = $aExtraParams['formPrefix']; } - $this->m_iFormId = $sPrefix.$iGlobalFormId; + $aFieldsComments = (isset($aExtraParams['fieldsComments'])) ? $aExtraParams['fieldsComments'] : array(); + + $this->m_iFormId = $sPrefix.self::$iGlobalFormId; $sClass = get_class($this); $oAppContext = new ApplicationContext(); $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); @@ -1448,6 +1467,8 @@ EOF foreach($aFields as $sAttCode) { $aVal = null; + $sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ' '; + $sInfos = ' '; $iFlags = $this->GetAttributeFlags($sAttCode); $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); if ( (!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0)) @@ -1458,7 +1479,7 @@ EOF { // State attribute is always read-only from the UI $sHTMLValue = $this->GetStateLabel(); - $aVal = array('label' => $oAttDef->GetLabel(), 'value' => $sHTMLValue); + $aVal = array('label' => $oAttDef->GetLabel(), 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos); } else { @@ -1471,15 +1492,15 @@ EOF } else { - if ($iFlags & OPT_ATT_READONLY) + if ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE)) { // Check if the attribute is not read-only becuase of a synchro... $aReasons = array(); - $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); $sSynchroIcon = ''; - if ($iSynchroFlags & OPT_ATT_READONLY) + if ($iFlags & OPT_ATT_SLAVE) { + $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); $sSynchroIcon = " "; $sTip = ''; foreach($aReasons as $aRow) @@ -1490,9 +1511,10 @@ EOF } // Attribute is read-only - $sHTMLValue = $this->GetAsHTML($sAttCode).$sSynchroIcon; + $sHTMLValue = $this->GetAsHTML($sAttCode);; $sHTMLValue .= ''; $aFieldsMap[$sAttCode] = $sInputId; + $sComments = $sSynchroIcon; } else { @@ -1503,13 +1525,13 @@ EOF $aFieldsMap[$sAttCode] = $sInputId; } - $aVal = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $sHTMLValue); + $aVal = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos); } } } else { - $aVal = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $this->GetAsHTML($sAttCode)); + $aVal = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $this->GetAsHTML($sAttCode), 'comments' => $sComments, 'infos' => $sInfos); } } if ($aVal != null) @@ -1548,22 +1570,48 @@ EOF $oPage->add("\n"); } $oPage->add($oAppContext->GetForForm()); + // Custom operation for the form ? + if (isset($aExtraParams['custom_operation'])) + { + $sOperation = $aExtraParams['custom_operation']; + } + else if ($iKey > 0) + { + $sOperation = 'apply_modify'; + } + else + { + $sOperation = 'apply_new'; + } + + // Custom label for the apply button ? + if (isset($aExtraParams['custom_button'])) + { + $sApplyButton = $aExtraParams['custom_button']; + } + else if ($iKey > 0) + { + $sApplyButton = Dict::S('UI:Button:Apply'); + } + else + { + $sApplyButton = Dict::S('UI:Button:Create'); + } + if ($iKey > 0) { - // The object already exists in the database, it's modification + // The object already exists in the database, it's a modification $oPage->add("\n"); - $oPage->add("\n"); -// $oPage->add("    \n"); + $oPage->add("\n"); $oPage->add("    \n"); - $oPage->add("\n"); + $oPage->add("\n"); } else { // The object does not exist in the database it's a creation - $oPage->add("\n"); -// $oPage->add("    \n"); + $oPage->add("\n"); $oPage->add("    \n"); - $oPage->add("\n"); + $oPage->add("\n"); } // Hook the cancel button via jQuery so that it can be unhooked easily as well if needed $sDefaultUrl = '../pages/UI.php?operation=cancel'; @@ -1572,11 +1620,12 @@ EOF $iFieldsCount = count($aFieldsMap); $sJsonFieldsMap = json_encode($aFieldsMap); + $sState = $this->GetState(); $oPage->add_script( <<$oAttDef) { if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) { - $aLinks = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", ''); + $aLinks = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null); + if (is_null($aLinks)) continue; + $sLinkedClass = $oAttDef->GetLinkedClass(); $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); @@ -1965,10 +2017,18 @@ EOF else if ($oAttDef->IsWritable()) { $iFlags = $this->GetAttributeFlags($sAttCode); - if ($iFlags & (OPT_ATT_HIDDEN | OPT_ATT_READONLY)) + if ( $iFlags & (OPT_ATT_HIDDEN | OPT_ATT_READONLY)) { // Non-visible, or read-only attribute, do nothing } + elseif($iFlags & OPT_ATT_SLAVE) + { + $value = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null); + if (!is_null($value) && ($value != $this->Get($sAttCode))) + { + $aErrors[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $oAttDef->GetLabel()); + } + } elseif ($oAttDef->GetEditClass() == 'Document') { // There should be an uploaded file with the named attr_ @@ -1993,14 +2053,9 @@ EOF elseif ($oAttDef->GetEditClass() == 'Duration') { $rawValue = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null); - if (!is_array($rawValue)) - { - $iValue = null; - } - else - { - $iValue = (((24*$rawValue['d'])+$rawValue['h'])*60 +$rawValue['m'])*60 + $rawValue['s']; - } + if (!is_array($rawValue)) continue; + + $iValue = (((24*$rawValue['d'])+$rawValue['h'])*60 +$rawValue['m'])*60 + $rawValue['s']; $this->Set($sAttCode, $iValue); $previousValue = $this->Get($sAttCode); if ($previousValue !== $iValue) @@ -2023,6 +2078,7 @@ EOF } } } + return $aErrors; } protected function DBInsertTracked_Internal($bDoNotReload = false) diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 6cd987d7c..7d5189d4f 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -1092,8 +1092,48 @@ class MenuBlock extends DisplayBlock // many objects in the set, possible actions are: new / modify all / delete all $sUrl = utils::GetAbsoluteUrl(); if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:New'), 'url' => "../pages/$sUIPage?operation=new&class=$sClass&$sContext{$sDefault}"); } - //if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => 'Modify All...', 'url' => "../pages/$sUIPage?operation=modify_all&filter=$sFilter&$sContext"); } + if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:ModifyAll'), 'url' => "../pages/$sUIPage?operation=select_for_modify_all&class=$sClass&filter=$sFilter&sContext"); } if ($bIsBulkDeleteAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:BulkDelete'), 'url' => "../pages/$sUIPage?operation=select_for_deletion&filter=$sFilter&$sContext"); } + + // Stimuli + $aStates = MetaModel::EnumStates($sClass); + if (count($aStates) > 0) + { + // Life cycle actions may be available... if all objects are in the same state + $oSet->Rewind(); + $aStates = array(); + while($oObj = $oSet->Fetch()) + { + $aStates[$oObj->GetState()] = $oObj->GetState(); + } + $oSet->Rewind(); + if (count($aStates) == 1) + { + // All objects are in the same state... + $sState = array_pop($aStates); + $aTransitions = Metamodel::EnumTransitions($sClass, $sState); + if (count($aTransitions)) + { + $this->AddMenuSeparator($aActions); + $aStimuli = Metamodel::EnumStimuli($sClass); + foreach($aTransitions as $sStimulusCode => $aTransitionDef) + { + $oChecker = new StimulusChecker($this->m_oFilter, $sState, $sStimulusCode); + $iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? $oChecker->IsAllowed() : UR_ALLOWED_NO; + switch($iActionAllowed) + { + case UR_ALLOWED_YES: + case UR_ALLOWED_DEPENDS: + $aActions[] = array('label' => $aStimuli[$sStimulusCode]->GetLabel(), 'url' => "../pages/UI.php?operation=select_bulk_stimulus&stimulus=$sStimulusCode&state=$sState&class=$sClass&filter=$sFilter&$sContext"); + break; + + default: + // Do nothing + } + } + } + } + } $this->AddMenuSeparator($aActions); $aActions[] = array ('label' => Dict::S('UI:Menu:EMail'), 'url' => "mailto:?subject=".$oSet->GetFilter()->__DescribeHTML()."&body=".urlencode("$sUrl?operation=search&filter=$sFilter&$sContext")); $aActions[] = array ('label' => Dict::S('UI:Menu:CSVExport'), 'url' => "../pages/$sUIPage?operation=search&filter=$sFilter&format=csv&$sContext"); diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index d2e11b5ca..32f42af83 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -399,6 +399,21 @@ EOF } var oUserPreferences = $sUserPrefs; + + // For disabling the CKEditor at init time when the corresponding textarea is disabled ! + CKEDITOR.plugins.add( 'disabler', + { + init : function( editor ) + { + editor.on( 'instanceReady', function(e) + { + e.removeListener(); + $('#'+ editor.name).trigger('update'); + }); + } + + }); + EOF ); diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index a0376d4e4..73c0994f7 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -145,6 +145,7 @@ class UIExtKeyWidget <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sClass}', '{$this->sAttCode}', '{$this->sNameSuffix}', $sSelectMode, $sWizHelper); oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; + $('#$this->iId').bind('update', function() { oACWidget_{$this->iId}.Update(); } ); EOF ); @@ -168,20 +169,22 @@ EOF // the input for the auto-complete $sHTMLValue = "aAllowedValues)."\" type=\"text\" id=\"label_$this->iId\" size=\"30\" maxlength=\"$iFieldSize\" value=\"$sDisplayValue\"/> "; - $sHTMLValue .= "iId}.Search();\"> "; + $sHTMLValue .= "iId}.Search();\">iId}\" style=\"border:0;vertical-align:middle;\" src=\"../images/mini_search.gif\" /> "; // another hidden input to store & pass the object's Id $sHTMLValue .= "iId\" name=\"{$sAttrFieldPrefix}{$this->sFieldPrefix}{$this->sAttCode}{$this->sNameSuffix}\" value=\"$this->value\" />\n"; // Scripts to start the autocomplete and bind some events to it - $oPage->add_ready_script( + $sDialogTitle = addslashes($this->sTitle); + $oPage->add_ready_script( <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sClass}', '{$this->sAttCode}', '{$this->sNameSuffix}', $sSelectMode, $sWizHelper); oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; $('#label_$this->iId').autocomplete('./ajax.render.php', { scroll:true, minChars:{$iMinChars}, formatItem:formatItem, autoFill:false, matchContains:true, keyHolder:'#{$this->iId}', extraParams:{operation:'autocomplete', sclass:'{$this->sClass}',attCode:'{$this->sAttCode}'}}); $('#label_$this->iId').blur(function() { $(this).search(); } ); $('#label_$this->iId').result( function(event, data, formatted) { OnAutoComplete('{$this->iId}', event, data, formatted); } ); - $('#ac_dlg_$this->iId').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, title: '{$this->sTitle}', resizeStop: oACWidget_{$this->iId}.UpdateSizes, close: oACWidget_{$this->iId}.OnClose }); + $('#$this->iId').bind('update', function() { oACWidget_{$this->iId}.Update(); } ); + $('#ac_dlg_$this->iId').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, title: '$sDialogTitle', resizeStop: oACWidget_{$this->iId}.UpdateSizes, close: oACWidget_{$this->iId}.OnClose }); EOF ); @@ -190,7 +193,7 @@ EOF } if ($bCreate) { - $sHTMLValue .= "iId}.CreateObject();\"> "; + $sHTMLValue .= "iId}.CreateObject();\">iId}\" style=\"border:0;vertical-align:middle;\" src=\"../images/mini_add.gif\" /> "; $oPage->add_at_the_end('
'); } $sHTMLValue .= "iId}\">"; @@ -259,11 +262,12 @@ EOF */ public function GetObjectCreationForm(WebPage $oPage) { + $sDialogTitle = addslashes($this->sTitle); $oPage->add('
'); $oPage->add("

".MetaModel::GetClassIcon($this->sTargetClass)." ".Dict::Format('UI:CreationTitle_Class', MetaModel::GetName($this->sTargetClass))."

\n"); cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, null, array(), array('formPrefix' => $this->iId, 'noRelations' => true)); $oPage->add('
'); - $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: $(window).width()*0.8, height: 'auto', autoOpen: false, modal: true, title: '$this->sTitle'});\n"); + $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: $(window).width()*0.8, height: 'auto', autoOpen: false, modal: true, title: '$sDialogTitle'});\n"); $oPage->add_ready_script("$('#dcr_{$this->iId} form').removeAttr('onsubmit');"); $oPage->add_ready_script("$('#dcr_{$this->iId} form').bind('submit.uilinksWizard', oACWidget_{$this->iId}.DoCreateObject);"); } @@ -274,17 +278,21 @@ EOF public function DoCreateObject($oPage) { $oObj = MetaModel::NewObject($this->sTargetClass); - $oObj->UpdateObject($this->sFormPrefix.$this->iId); - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $sUserString = CMDBChange::GetCurrentUserName(); - $oMyChange->Set("userinfo", $sUserString); - $iChangeId = $oMyChange->DBInsert(); - $oObj->DBInsertTracked($oMyChange); - - return array('name' => $oObj->GetName(), 'id' => $oObj->GetKey()); - - //return array('name' => 'test', 'id' => '42'); + $aErrors = $oObj->UpdateObject($this->sFormPrefix.$this->iId); + if (count($aErrors) == 0) + { + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $sUserString = CMDBChange::GetCurrentUserName(); + $oMyChange->Set("userinfo", $sUserString); + $iChangeId = $oMyChange->DBInsert(); + $oObj->DBInsertTracked($oMyChange); + return array('name' => $oObj->GetName(), 'id' => $oObj->GetKey()); + } + else + { + return array('name' => implode(' ', $aErrors), 'id' => 0); + } } } ?> diff --git a/application/ui.htmleditorwidget.class.inc.php b/application/ui.htmleditorwidget.class.inc.php index 0aa15786e..c240e9ea6 100644 --- a/application/ui.htmleditorwidget.class.inc.php +++ b/application/ui.htmleditorwidget.class.inc.php @@ -61,14 +61,14 @@ class UIHTMLEditorWidget $sHelpText = $this->m_sHelpText; $sValidationField = $this->m_sValidationField; - $sHtmlValue = "
$sValidationField
"; + $sHtmlValue = "
$sValidationField
"; // Replace the text area with CKEditor // To change the default settings of the editor, // a) edit the file /js/ckeditor/config.js // b) or override some of the configuration settings, using the second parameter of ckeditor() $sLanguage = strtolower(trim(UserRights::GetUserLanguage())); - $oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, { language : '$sLanguage' , contentsLanguage : '$sLanguage' });"); // Transform $iId into a CKEdit + $oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, { language : '$sLanguage' , contentsLanguage : '$sLanguage', extraPlugins: 'disabler' });"); // Transform $iId into a CKEdit // Please read... // ValidateCKEditField triggers a timer... calling itself indefinitely @@ -78,7 +78,8 @@ class UIHTMLEditorWidget // The most relevant solution would be to implement a plugin to CKEdit, and handle the internal events like: setData, insertHtml, insertElement, loadSnapshot, key, afterUndo, afterRedo // Could also be bound to 'instanceReady.ckeditor' - $oPage->add_ready_script("$('#$iId').bind('validate', function(evt, sFormId) { return ValidateCKEditField('$iId', '', {$this->m_sMandatory}, sFormId, '') } );"); + $oPage->add_ready_script("$('#$iId').bind('validate', function(evt, sFormId) { return ValidateCKEditField('$iId', '', {$this->m_sMandatory}, sFormId, '') } );\n"); + $oPage->add_ready_script("$('#$iId').bind('update', function() { BlockField('cke_$iId', $('#$iId').attr('disabled')); } );\n"); return $sHtmlValue; } diff --git a/application/ui.passwordwidget.class.inc.php b/application/ui.passwordwidget.class.inc.php index 4bea0a559..fc33208cf 100644 --- a/application/ui.passwordwidget.class.inc.php +++ b/application/ui.passwordwidget.class.inc.php @@ -57,13 +57,28 @@ class UIPasswordWidget $sChangedValue = (($sPasswordValue != '*****') || ($sConfirmPasswordValue != '*****')) ? 1 : 0; $sHtmlValue = ''; $sHtmlValue = ' 
'; - $sHtmlValue .= ' '.Dict::S('UI:PasswordConfirm').' '; + $sHtmlValue .= ' '.Dict::S('UI:PasswordConfirm').' '; $sHtmlValue .= ''; $oPage->add_ready_script("$('#$this->iId').bind('keyup change', function(evt) { return PasswordFieldChanged('$this->iId') } );"); // Bind to a custom event: validate $oPage->add_ready_script("$('#$this->iId').bind('keyup change validate', function(evt, sFormId) { return ValidatePasswordField('$this->iId', sFormId) } );"); // Bind to a custom event: validate $oPage->add_ready_script("$('#{$this->iId}_confirm').bind('keyup change', function(evt, sFormId) { return ValidatePasswordField('$this->iId', sFormId) } );"); // Bind to a custom event: validate - + $oPage->add_ready_script("$('#{$this->iId}').bind('update', function(evt, sFormId) + { + if ($(this).attr('disabled')) + { + $('#{$this->iId}_confirm').attr('disabled', 'disabled'); + $('#{$this->iId}_changed').attr('disabled', 'disabled'); + $('#{$this->iId}_reset').attr('disabled', 'disabled'); + } + else + { + $('#{$this->iId}_confirm').removeAttr('disabled'); + $('#{$this->iId}_changed').removeAttr('disabled'); + $('#{$this->iId}_reset').removeAttr('disabled'); + } + } + );"); // Bind to a custom event: update to handle enabling/disabling return $sHtmlValue; } } diff --git a/application/webpage.class.inc.php b/application/webpage.class.inc.php index caa30a2ce..e17dc532e 100644 --- a/application/webpage.class.inc.php +++ b/application/webpage.class.inc.php @@ -258,6 +258,9 @@ class WebPage { $sHtml .= "".$aAttrib['label']."".$aAttrib['value']."\n"; } + $sComment = (isset($aAttrib['comments'])) ? $aAttrib['comments'] : ' '; + $sInfo = (isset($aAttrib['infos'])) ? $aAttrib['infos'] : ' '; + $sHtml .= "$sComment$sInfo\n"; $sHtml .= "\n"; } $sHtml .= "\n"; diff --git a/application/wizardhelper.class.inc.php b/application/wizardhelper.class.inc.php index db231ca9a..08f9e8f99 100644 --- a/application/wizardhelper.class.inc.php +++ b/application/wizardhelper.class.inc.php @@ -121,6 +121,11 @@ class WizardHelper } } } + if (isset($this->m_aData['m_sState']) && !empty($this->m_aData['m_sState'])) + { + $oObj->Set(MetaModel::GetStateAttributeCode($this->m_aData['m_sClass']), $this->m_aData['m_sState']); + } + return $oObj; } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 274258fb0..79253af2d 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1543,7 +1543,7 @@ abstract class DBObject { if (($oSyncAttr->Get('attcode') == $sAttCode) && ($oSyncAttr->Get('update') == 1) && ($oSyncAttr->Get('update_policy') == 'master_locked')) { - $iFlags |= OPT_ATT_READONLY; + $iFlags |= OPT_ATT_SLAVE; $sUrl = $oSource->GetApplicationUrl($this, $oReplica); $aReason[] = array('name' => $oSource->GetName(), 'description' => $oSource->Get('description'), 'url_application' => $sUrl); } diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index faeab6b1b..023668022 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -526,6 +526,109 @@ class DBObjectSet } return $aRelatedObjs; } + + /** + * Builds an object that contains the values that are common to all the objects + * in the set. If for a given attribute, objects in the set have various values + * then the resulting object will contain null for this value. + * @param $aValues Hash Output: the distribution of the values, in the set, for each attribute + * @return Object + */ + public function ComputeCommonObject(&$aValues) + { + $sClass = $this->GetClass(); + $aList = MetaModel::FlattenZlist(MetaModel::GetZListItems($sClass, 'details')); + $aValues = array(); + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsScalar()) + { + $aValues[$sAttCode] = array(); + } + } + $this->Rewind(); + while($oObj = $this->Fetch()) + { + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) + { + $currValue = $oObj->Get($sAttCode); + if (is_object($currValue)) continue; // Skip non scalar values... + if(!array_key_exists($currValue, $aValues[$sAttCode])) + { + $aValues[$sAttCode][$currValue] = array('count' => 1, 'display' => $oObj->GetAsHTML($sAttCode)); + } + else + { + $aValues[$sAttCode][$currValue]['count']++; + } + } + } + } + + foreach($aValues as $sAttCode => $aMultiValues) + { + if (count($aMultiValues) > 1) + { + uasort($aValues[$sAttCode], 'HashCountComparison'); + } + } + + + // Now create an object that has values for the homogenous values only + $oCommonObj = new $sClass(); // @@ What if the class is abstract ? + $aComments = array(); + + $iFormId = cmdbAbstractObject::GetNextFormId(); // Identifier that prefixes all the form fields + $sReadyScript = ''; + $aDependsOn = array(); + $sFormPrefix = '2_'; + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) + { + if ($oAttDef->GetEditClass() == 'One Way Password') + { + $oCommonObj->Set($sAttCode, null); + } + else + { + $iCount = count($aValues[$sAttCode]); + if ($iCount == 1) + { + // Homogenous value + reset($aValues[$sAttCode]); + $aKeys = array_keys($aValues[$sAttCode]); + $currValue = $aKeys[0]; // The only value is the first key + $oCommonObj->Set($sAttCode, $currValue); + } + else + { + // Non-homogenous value + $oCommonObj->Set($sAttCode, null); + } + } + } + } + $this->Rewind(); + return $oCommonObj; + } +} + +/** + * Helper function to perform a custom sort of a hash array + */ +function HashCountComparison($a, $b) // Sort descending on 'count' +{ + if ($a['count'] == $b['count']) + { + return 0; + } + return ($a['count'] > $b['count']) ? -1 : 1; } ?> diff --git a/core/metamodel.class.php b/core/metamodel.class.php index c18acb527..12f157c96 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -98,6 +98,13 @@ define('OPT_ATT_MUSTCHANGE', 8); * @package iTopORM */ define('OPT_ATT_MUSTPROMPT', 16); +/** + * Specifies that the attribute is in 'slave' mode compared to one data exchange task: + * it should not be edited inside iTop anymore + * + * @package iTopORM + */ +define('OPT_ATT_SLAVE', 32); /** * DB Engine -should be moved into CMDBSource @@ -3542,9 +3549,13 @@ abstract class MetaModel // Note: load the dictionary as soon as possible, because it might be // needed when some error occur - foreach (self::$m_oConfig->GetDictionaries() as $sModule => $sToInclude) + if (!Dict::InCache()) { - self::Plugin($sConfigFile, 'dictionaries', $sToInclude); + foreach (self::$m_oConfig->GetDictionaries() as $sModule => $sToInclude) + { + self::Plugin($sConfigFile, 'dictionaries', $sToInclude); + } + Dict::InitCache(); } // Set the language... after the dictionaries have been loaded! Dict::SetDefaultLanguage(self::$m_oConfig->GetDefaultLanguage()); diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index f9275a0b8..b2b3da512 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -816,4 +816,169 @@ class UserRights } } +/** + * Helper class to get the number/list of items for which a given action is allowed/possible + */ +class ActionChecker +{ + var $oFilter; + var $iActionCode; + var $iAllowedCount = null; + var $aAllowedIDs = null; + + public function __construct(DBObjectSearch $oFilter, $iActionCode) + { + $this->oFilter = $oFilter; + $this->iActionCode = $iActionCode; + $this->iAllowedCount = null; + $this->aAllowedIDs = null; + } + + /** + * returns the number of objects for which the action is allowed + * @return integer The number of "allowed" objects 0..N + */ + public function GetAllowedCount() + { + if ($this->iAllowedCount == null) $this->CheckObjects(); + return $this->iAllowedCount; + } + + /** + * If IsAllowed returned UR_ALLOWED_DEPENDS, this methods returns + * an array of ObjKey => Status (true|false) + * @return array + */ + public function GetAllowedIDs() + { + if ($this->aAllowedIDs == null) $this->IsAllowed(); + return $this->aAllowedIDs; + } + + /** + * Check if the speficied stimulus is allowed for the set of objects + * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS + */ + public function IsAllowed() + { + $sClass = $this->oFilter->GetClass(); + $oSet = new DBObjectSet($this->oFilter); + $iActionAllowed = UserRights::IsActionAllowed($sClass, $oSet, $this->iActionCode); + if ($iActionAllowed == UR_ALLOWED_DEPENDS) + { + // Check for each object if the action is allowed or not + $this->aAllowedIDs = array(); + $oSet->Rewind(); + $this->iAllowedCount = 0; + while($oObj = $oSet->Fetch()) + { + $oObjSet = DBObjectSet::FromArray($sClass, array($oObj)); + if (UserRights::IsActionAllowed($sClass, $this->iActionCode, $oObjSet) == UR_ALLOWED_NO) + { + $this->aAllowedIDs[$oObj->GetKey()] = false; + } + else + { + // Assume UR_ALLOWED_YES, since there is just one object ! + $this->aAllowedIDs[$oObj->GetKey()] = true; + $this->iAllowedCount++; + } + } + } + else if ($iActionAllowed == UR_ALLOWED_YES) + { + $this->iAllowedCount = $oSet->Count(); + $this->aAllowedIDs = array(); // Optimization: not filled when Ok for all objects + } + else // UR_ALLOWED_NO + { + $this->iAllowedCount = 0; + $this->aAllowedIDs = array(); + } + return $iActionAllowed; + } +} + +/** + * Helper class to get the number/list of items for which a given stimulus can be applied (allowed & possible) + */ +class StimulusChecker extends ActionChecker +{ + var $sState = null; + + public function __construct(DBObjectSearch $oFilter, $sState, $iStimulusCode) + { + parent::__construct($oFilter, $iStimulusCode); + $this->sState = $sState; + } + + /** + * Check if the speficied stimulus is allowed for the set of objects + * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS + */ + public function IsAllowed() + { + $sClass = $this->oFilter->GetClass(); + $oSet = new DBObjectSet($this->oFilter); + $iActionAllowed = UserRights::IsStimulusAllowed($sClass, $oSet, $this->iActionCode); + if ($iActionAllowed == UR_ALLOWED_NO) + { + $this->iAllowedCount = 0; + $this->aAllowedIDs = array(); + } + else // Even if UR_ALLOWED_YES, we need to check if each object is in the appropriate state + { + // Hmmm, may not be needed right now because we limit the "multiple" action to object in + // the same state... may be useful later on if we want to extend this behavior... + + // Check for each object if the action is allowed or not + $this->aAllowedIDs = array(); + $oSet->Rewind(); + $iAllowedCount = 0; + $iActionAllowed = UR_ALLOWED_DEPENDS; + while($oObj = $oSet->Fetch()) + { + $aTransitions = $oObj->EnumTransitions(); + if (array_key_exists($this->iActionCode, $aTransitions)) + { + // Temporary optimization possible: since the current implementation + // of IsActionAllowed does not perform a 'per instance' check, we could + // skip this second validation phase and assume it would return UR_ALLOWED_YES + $oObjSet = DBObjectSet::FromArray($sClass, array($oObj)); + if (UserRights::IsActionAllowed($sClass, $this->iActionCode, $oObjSet) == UR_ALLOWED_NO) + { + $this->aAllowedIDs[$oObj->GetKey()] = false; + } + else + { + // Assume UR_ALLOWED_YES, since there is just one object ! + $this->aAllowedIDs[$oObj->GetKey()] = true; + $this->iState = $oObj->GetState(); + $this->iAllowedCount++; + } + } + else + { + $this->aAllowedIDs[$oObj->GetKey()] = false; + } + } + } + + if ($this->iAllowedCount == $oSet->Count()) + { + $iActionAllowed = UR_ALLOWED_YES; + } + if ($this->iAllowedCount == 0) + { + $iActionAllowed = UR_ALLOWED_NO; + } + + return $iActionAllowed; + } + + public function GetState() + { + return $this->iState; + } +} ?> diff --git a/css/light-grey.css b/css/light-grey.css index c5b8862cc..f8afe7a3e 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -126,9 +126,9 @@ th.headerSortDown { td { font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 8pt; + font-size: 12px; color:#696969; - background-color: #ffffff; + nobackground-color: #ffffff; padding: 0px; } @@ -142,7 +142,7 @@ td.label { font-family: Tahoma, Verdana, Arial, Helvetica; font-size: 12px; color: #000000; - background-color:#f6f6f6; + nobackground-color:#f6f6f6; padding: 0.25em; font-weight:bold; } @@ -938,3 +938,29 @@ span.form_validation { vertical-align:middle; text-align:center; } +.mono_value { + display: inline-block; + background-color: #3c3; + color: #fff; + font-weight:bold; + padding: 3px; + padding-left: 5px; + padding-right: 5px; + margin-left:3px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; +} +.multi_values { + display: inline-block; + background-color: #c33; + color: #fff; + font-weight:bold; + padding: 3px; + padding-left: 5px; + padding-right: 5px; + margin-left:3px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; +} \ No newline at end of file diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php index dddc5dfc0..cb3ab7a24 100644 --- a/dictionaries/dictionary.itop.ui.php +++ b/dictionaries/dictionary.itop.ui.php @@ -885,5 +885,26 @@ When associated with a trigger, each action is given an "order" number, specifyi 'Portal:Attachment_No_To_Ticket_Name' => 'Attachment #%1$d to %2$s (%3$s)', 'Enum:Undefined' => 'Undefined', 'UI:DurationForm_Days_Hours_Minutes_Seconds' => '%1$s Days %2$s Hours %3$s Minutes %4$s Seconds', + 'UI:ModifyAllPageTitle' => 'Modify All', + 'UI:Modify_N_ObjectsOf_Class' => 'Modifying %1$d objects of class %2$s', + 'UI:Modify_M_ObjectsOf_Class_OutOf_N' => 'Modifying %1$d objects of class %2$s out of %3$d', + 'UI:Menu:ModifyAll' => 'Modify...', + 'UI:Button:ModifyAll' => 'Modify All', + 'UI:Button:PreviewModifications' => 'Preview Modifications >>', + 'UI:ModifiedObject' => 'Object Modified', + 'UI:BulkModifyStatus' => 'Operation', + 'UI:BulkModifyStatus+' => 'Status of the operation', + 'UI:BulkModifyErrors' => 'Errors (if any)', + 'UI:BulkModifyErrors+' => 'Errors preventing the modification', + 'UI:BulkModifyStatusOk' => 'Ok', + 'UI:BulkModifyStatusError' => 'Error', + 'UI:BulkModifyStatusModified' => 'Modified', + 'UI:BulkModifyStatusSkipped' => 'Skipped', + 'UI:BulkModify_Count_DistinctValues' => '%1$d distinct values:', + 'UI:BulkModify:Value_Exists_N_Times' => '%1$s, %2$d time(s)', + 'UI:BulkModify:N_MoreValues' => '%1$d more values...', + 'UI:AttemptingToSetAReadOnlyAttribute_Name' => 'Attempting to set the read-only field: %1$s', + 'UI:FailedToApplyStimuli' => 'The action has failed.', + 'UI:StimulusModify_N_ObjectsOf_Class' => '%1$s: Modifying %2$d objects of class %3$s', )); ?> diff --git a/images/transp-lock.png b/images/transp-lock.png index 91bb10cfc1a6f32a99cf2d6357491e5ec202078d..5628d0daf8f8b9abc5f2b85a1e72c0427cc1a12f 100644 GIT binary patch delta 302 zcmV+}0nz@31C|4jF%Je}OGiWi{{a60|De66laVeU2o@9@FcLq+MUhr0fAL8~K~y-) zt&|~>!axv3Uj|OF0-p;|5eW7G$cP9`I}~R^;W{E>4zL%9K%}@pd;}+esukOlnxrVG z-PtE6`LAZWrzb1Rk{A&=0DE8qd_tGSLIGbfub1N=aCQ?@xH-GZDO?=TBuW}*!_@v!(5mV= zbmtgmXI}Xi43_)pj!on47dvXh@J*hAl=tKt#okk&V9GTX000002uVdwM6N<$f)-qY A761SM delta 288 zcmV+*0pI?X1BL^TF_A+Z2m%f;I!aJBT#;xffAdL1K~y-)t(38i!axv3pZBPMfF?$| zz#@PGNGv&|03up&;BcEDq5$Vkt^o(g0n#+#fC}!U3vSk0TL=VuFI@XS?au6MR+c3( zB60xsz#3RapKibfXjJtaXR;s>kx$?RY;OreoWML4G@b~mfJe3!Q=>Y0f6^@vNK~NA_r$hOkwY=3n{F#s;a($ zj|9ojV!jdz_=tJE9RGl`o0!7Q*;P*A;(#Vm(l{HY?oC0bs^`$1W0;+J=jJOwH5$uAcOQ$j{5(PaPt3IG5}MNUMnLSTZrl7SZh diff --git a/js/extkeywidget.js b/js/extkeywidget.js index fc362f9c6..6ee794aef 100644 --- a/js/extkeywidget.js +++ b/js/extkeywidget.js @@ -45,6 +45,7 @@ function ExtKeyWidget(id, sClass, sAttCode, sSuffix, bSelectMode, oWizHelper) this.Search = function() { + if($('#'+me.id).attr('disabled')) return; // Disabled, do nothing $('#ac_dlg_'+me.id).dialog('open'); this.UpdateSizes(); this.UpdateButtons(); @@ -189,6 +190,7 @@ function ExtKeyWidget(id, sClass, sAttCode, sSuffix, bSelectMode, oWizHelper) this.CreateObject = function(oWizHelper) { + if($('#'+me.id).attr('disabled')) return; // Disabled, do nothing // Query the server to get the form to create a target object if (me.bSelectMode) { @@ -309,4 +311,23 @@ function ExtKeyWidget(id, sClass, sAttCode, sSuffix, bSelectMode, oWizHelper) } return false; // do NOT submit the form } + + this.Update = function() + { + if ($('#'+me.id).attr('disabled')) + { + $('#v_'+me.id).html(''); + $('#label_'+me.id).attr('disabled', 'disabled'); + $('#label_'+me.id).css({'background': 'transparent'}); + $('#mini_add_'+me.id).hide(); + $('#mini_search_'+me.id).hide(); + } + else + { + $('#label_'+me.id).attr('disabled', ''); + $('#label_'+me.id).css({'background': '#fff url(../images/ac-background.gif) no-repeat right'}); + $('#mini_add_'+me.id).show(); + $('#mini_search_'+me.id).show(); + } + } } \ No newline at end of file diff --git a/js/forms-json-utils.js b/js/forms-json-utils.js index ac97abfe5..ed394311b 100644 --- a/js/forms-json-utils.js +++ b/js/forms-json-utils.js @@ -174,26 +174,33 @@ function ReportFieldValidationStatus(sFieldId, sFormId, bValid) function ValidateField(sFieldId, sPattern, bMandatory, sFormId, nullValue) { var bValid = true; - var currentVal = $('#'+sFieldId).val(); + if ($('#'+sFieldId).attr('disabled')) + { + bValid = true; // disabled fields are not checked + } + else + { + var currentVal = $('#'+sFieldId).val(); - if (currentVal == '$$NULL$$') // Convention to indicate a non-valid value since it may have to be passed as text - { - bValid = false; - } - else if (bMandatory && (currentVal == nullValue)) - { - bValid = false; - } - else if (currentVal == nullValue) - { - // An empty field is Ok... - bValid = true; - } - else if (sPattern != '') - { - re = new RegExp(sPattern); - //console.log('Validating field: '+sFieldId + ' current value: '+currentVal + ' pattern: '+sPattern ); - bValid = re.test(currentVal); + if (currentVal == '$$NULL$$') // Convention to indicate a non-valid value since it may have to be passed as text + { + bValid = false; + } + else if (bMandatory && (currentVal == nullValue)) + { + bValid = false; + } + else if (currentVal == nullValue) + { + // An empty field is Ok... + bValid = true; + } + else if (sPattern != '') + { + re = new RegExp(sPattern); + //console.log('Validating field: '+sFieldId + ' current value: '+currentVal + ' pattern: '+sPattern ); + bValid = re.test(currentVal); + } } ReportFieldValidationStatus(sFieldId, sFormId, bValid); //console.log('Form: '+sFormId+' Validating field: '+sFieldId + ' current value: '+currentVal+' pattern: '+sPattern+' result: '+bValid ); diff --git a/js/utils.js b/js/utils.js index 90eb082be..6dabec73f 100644 --- a/js/utils.js +++ b/js/utils.js @@ -212,3 +212,75 @@ function CheckAll(sSelector, bValue) this.checked = value; }); } + +/** + * Toggle (enabled/disabled) the specified field of a form + */ +function ToogleField(value, field_id) +{ + if (value) + { + $('#'+field_id).removeAttr('disabled'); + } + else + { + $('#'+field_id).attr('disabled', 'disabled'); + } + $('#'+field_id).trigger('update'); + $('#'+field_id).trigger('validate'); +} + +/** + * For the fields that cannot be visually disabled, they can be blocked + * @return + */ +function BlockField(field_id, bBlocked) +{ + if (bBlocked) + { + $('#'+field_id).block({ message: ' ** disabled ** '}); + } + else + { + $('#'+field_id).unblock(); + } +} + +/** + * Updates (enables/disables) a "duration" field + */ +function ToggleDurationField(field_id) +{ + // Toggle all the subfields that compose the "duration" input + aSubFields = new Array('d', 'h', 'm', 's'); + + if ($('#'+field_id).attr('disabled')) + { + for(var i=0; iDisplay($oP, 0); + + // The object could be listed, check if it is actually allowed to view it + $oSet = CMDBObjectSet::FromObject($oObj); + if (UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) == UR_ALLOWED_NO) + { + throw new SecurityException('User not allowed to view this object', array('class' => $sClass, 'id' => $id)); + } + $oP->set_title(Dict::Format('UI:DetailsPageTitle', $oObj->GetName(), $sClassLabel)); + $oObj->DisplayDetails($oP); +} + +/** + * Displays the result of a search request + * @param $oP WebPage Web page for the output + * @param $oFilter DBObjectSearch The search of objects to display + * @param $bSearchForm boolean Whether or not to display the search form at the top the page + * @param $sBaseClass string The base class for the search (can be different from the actual class of the results) + * @param $sFormat string The format to use for the output: csv or html + */ +function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '') +{ + $oSet = new DBObjectSet($oFilter); + if ($bSearchForm) + { + $aParams = array('open' => true); + if (!empty($sBaseClass)) + { + $aParams['baseClass'] = $sBaseClass; + } + $oBlock = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams); + $oBlock->Display($oP, 0); + } + if (strtolower($sFormat) == 'csv') + { + $oBlock = new DisplayBlock($oFilter, 'csv', false); + $oBlock->Display($oP, 1); + $oP->add_ready_script(" $('#csv').css('height', '95%');"); // adjust the size of the block + } + else + { + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, 1); + } +} + +/** + * Displays a form (checkboxes) to select the objects for which to apply a given action + * Only the objects for which the action is valid can be checked. By default all valid objects are checked + * @param $oP WebPage The page for output + * @param $oFilter DBObjectSearch The filter that defines the list of objects + * @param $sNextOperation string The next operation (code) to be executed when the form is submitted + * @param $oChecker ActionChecker The helper class/instance used to check for which object the action is valid + * @return none + */ +function DisplayMultipleSelectionForm($oP, $oFilter, $sNextOperation, $oChecker, $aExtraFormParams = array()) +{ + $oAppContext = new ApplicationContext(); + $iBulkActionAllowed = $oChecker->IsAllowed(); + $sClass = $oFilter->GetClass(); + $aExtraParams = array('selection_type' => 'multiple', 'selection_mode' => true, 'display_limit' => false, 'menu' => false); + if ($iBulkActionAllowed == UR_ALLOWED_DEPENDS) + { + $aAllowed = array(); + $aExtraParams['selection_enabled'] = $oChecker->GetAllowedIDs(); + } + else if(UR_ALLOWED_NO) + { + throw new ApplicationException(Dict::Format('UI:ActionNotAllowed')); + } + + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oP->add("
\n"); + $oP->add("\n"); + $oP->add("GetClass()."\">\n"); + $oP->add("Serialize()."\">\n"); + foreach($aExtraFormParams as $sName => $sValue) + { + $oP->add("\n"); + } + $oP->add($oAppContext->GetForForm()); + $oBlock->Display($oP, 1, $aExtraParams); + $oP->add("  \n"); + $oP->add("
\n"); + $oP->add_ready_script("CheckAll('.selectList1:not(:disabled)', true);\n"); +} + /*********************************************************************************** * * Main user interface page, starts here @@ -469,17 +569,34 @@ try $oP = new iTopWebPage(Dict::S('UI:WelcomeToITop')); + // All the following actions use advanced forms that require more javascript to be loaded switch($operation) { - case 'details': + case 'new': // Form to create a new object + case 'modify': // Form to modify an object + case 'apply_new': // Creation of a new object + case 'apply_modify': // Applying the modifications to an existing object + case 'form_for_modify_all': // Form to modify multiple objects (bulk modify) + case 'bulk_stimulus': // For to apply a stimulus to multiple objects + case 'stimulus': // Form displayed when applying a stimulus (state change) + $oP->add_linked_script("../js/json.js"); + $oP->add_linked_script("../js/forms-json-utils.js"); + $oP->add_linked_script("../js/wizardhelper.js"); + $oP->add_linked_script("../js/wizard.utils.js"); + $oP->add_linked_script("../js/linkswidget.js"); + $oP->add_linked_script("../js/extkeywidget.js"); + $oP->add_linked_script("../js/jquery.blockUI.js"); + break; + } + switch($operation) + { + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'details': // Details of an object $sClass = utils::ReadParam('class', ''); - $sClassLabel = MetaModel::GetName($sClass); $id = utils::ReadParam('id', ''); - $oSearch = new DBObjectSearch($sClass); - $oBlock = new DisplayBlock($oSearch, 'search', false); - $oBlock->Display($oP, 0); - if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid ! + if ( empty($sClass) || empty($id)) { throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); } @@ -499,18 +616,13 @@ try } else { - // The object could be listed, check if it is actually allowed to view it - $oSet = CMDBObjectSet::FromObject($oObj); - if (UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) == UR_ALLOWED_NO) - { - throw new SecurityException('User not allowed to view this object', array('class' => $sClass, 'id' => $id)); - } - $oP->set_title(Dict::Format('UI:DetailsPageTitle', $oObj->GetName(), $sClassLabel)); - $oObj->DisplayDetails($oP); + DisplayDetails($oP, $sClass, $oObj, $id); } break; - case 'search_oql': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'search_oql': // OQL query $sOQLClass = utils::ReadParam('oql_class', ''); $sOQLClause = utils::ReadParam('oql_clause', ''); $sFormat = utils::ReadParam('format', ''); @@ -523,28 +635,12 @@ try $sOQL = "SELECT $sOQLClass $sOQLClause"; try { - $oFilter = DBObjectSearch::FromOQL($sOQL); // To Do: Make sure we don't bypass security - $oSet = new DBObjectSet($oFilter); - if ($bSearchForm) - { - $oBlock = new DisplayBlock($oFilter, 'search', false); - $oBlock->Display($oP, 0); - } - if (strtolower($sFormat) == 'csv') - { - $oBlock = new DisplayBlock($oFilter, 'csv', false); - $oBlock->Display($oP, 'csv'); - $oPage->add_ready_script(" $('#csv').css('height', '95%');"); // adjust the size of the block - } - else - { - $oBlock = new DisplayBlock($oFilter, 'list', false); - $oBlock->Display($oP, 1); - } + $oFilter = DBObjectSearch::FromOQL($sOQL); + DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat); } catch(CoreException $e) { - $oFilter = new DBObjectSearch($sOQLClass); // To Do: Make sure we don't bypass security + $oFilter = new DBObjectSearch($sOQLClass); $oSet = new DBObjectSet($oFilter); if ($bSearchForm) { @@ -559,7 +655,9 @@ try } break; - case 'search_form': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'search_form': // Search form $sClass = utils::ReadParam('class', ''); $sFormat = utils::ReadParam('format', 'html'); $bSearchForm = utils::ReadParam('search_form', true); @@ -569,32 +667,12 @@ try } $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); $oFilter = new DBObjectSearch($sClass); - $oSet = new DBObjectSet($oFilter); - if ($bSearchForm) - { - $aParams = array('open' => true); - $sBaseClass = utils::ReadParam('baseClass', ''); - if (!empty($sBaseClass)) - { - $aParams['baseClass'] = $sBaseClass; - } - $oBlock = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams); - $oBlock->Display($oP, 0); - } - if (strtolower($sFormat) == 'csv') - { - $oBlock = new DisplayBlock($oFilter, 'csv', false); - $oBlock->Display($oP, 1); - $oP->add_ready_script(" $('#csv').css('height', '95%');"); // adjust the size of the block - } - else - { - $oBlock = new DisplayBlock($oFilter, 'list', false); - $oBlock->Display($oP, 1); - } + DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); break; - case 'search': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'search': // Serialized CMDBSearchFilter $sFilter = utils::ReadParam('filter', ''); $sFormat = utils::ReadParam('format', ''); $bSearchForm = utils::ReadParam('search_form', true); @@ -605,26 +683,12 @@ try $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); // TO DO: limit the search filter by the user context $oFilter = CMDBSearchFilter::unserialize($sFilter); // TO DO : check that the filter is valid - $oSet = new DBObjectSet($oFilter); - if ($bSearchForm) - { - $oBlock = new DisplayBlock($oFilter, 'search', false); - $oBlock->Display($oP, 0); - } - if (strtolower($sFormat) == 'csv') - { - $oBlock = new DisplayBlock($oFilter, 'csv', false); - $oBlock->Display($oP, 'csv'); - $oP->add_ready_script(" $('#csv').css('height', '95%');"); // adjust the size of the block - } - else - { - $oBlock = new DisplayBlock($oFilter, 'list', false); - $oBlock->Display($oP, 1); - } + DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); break; - case 'full_text': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'full_text': // Global "google-like" search $sFullText = trim(utils::ReadParam('text', '')); if (empty($sFullText)) { @@ -687,14 +751,9 @@ try } break; - case 'modify': - $oP->add_linked_script("../js/json.js"); - $oP->add_linked_script("../js/forms-json-utils.js"); - $oP->add_linked_script("../js/wizardhelper.js"); - $oP->add_linked_script("../js/wizard.utils.js"); - $oP->add_linked_script("../js/linkswidget.js"); - $oP->add_linked_script("../js/extkeywidget.js"); - $oP->add_linked_script("../js/jquery.blockUI.js"); + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'modify': // Form to modify an object $sClass = utils::ReadParam('class', ''); $sClassLabel = MetaModel::GetName($sClass); $id = utils::ReadParam('id', ''); @@ -729,7 +788,278 @@ try } break; - case 'new': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'select_for_modify_all': // Select the list of objects to be modified (bulk modify) + $oP->set_title(Dict::S('UI:ModifyAllPageTitle')); + $sFilter = utils::ReadParam('filter', ''); + if (empty($sFilter)) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); + } + // TO DO: limit the search filter by the user context + $oFilter = DBObjectSearch::unserialize($sFilter); // TO DO : check that the filter is valid + $sClass = $oFilter->GetClass(); + $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_MODIFY); + $oP->add("

Modify All...

\n"); + + DisplayMultipleSelectionForm($oP, $oFilter, 'form_for_modify_all', $oChecker); + break; + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'form_for_modify_all': // Form to modify multiple objects (bulk modify) + $sFilter = utils::ReadParam('filter', ''); + $sClass = utils::ReadParam('class', ''); + $aSelectedObj = utils::ReadParam('selectObject', array()); + if (count($aSelectedObj) > 0) + { + $iAllowedCount = count($aSelectedObj); + $sSelectedObj = implode(',', $aSelectedObj); + + $sOQL = "SELECT $sClass WHERE id IN (".$sSelectedObj.")"; + $oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL)); + + // Compute the distribution of the values for each field to determine which of the "scalar" fields are homogenous + $aList = MetaModel::FlattenZlist(MetaModel::GetZListItems($sClass, 'details')); + $aValues = array(); + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsScalar()) + { + $aValues[$sAttCode] = array(); + } + } + while($oObj = $oSet->Fetch()) + { + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) + { + $currValue = $oObj->Get($sAttCode); + if (is_object($currValue)) continue; // Skip non scalar values... + if(!array_key_exists($currValue, $aValues[$sAttCode])) + { + $aValues[$sAttCode][$currValue] = array('count' => 1, 'display' => $oObj->GetAsHTML($sAttCode)); + } + else + { + $aValues[$sAttCode][$currValue]['count']++; + } + } + } + } + // Now create an object that has values for the homogenous values only + $oDummyObj = new $sClass(); // @@ What if the class is abstract ? + $aComments = array(); + function MyComparison($a, $b) // Sort descending + { + if ($a['count'] == $b['count']) + { + return 0; + } + return ($a['count'] > $b['count']) ? -1 : 1; + } + + $iFormId = cmdbAbstractObject::GetNextFormId(); // Identifier that prefixes all the form fields + $sReadyScript = ''; + $aDependsOn = array(); + $sFormPrefix = '2_'; + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aPrerequisites = MetaModel::GetPrequisiteAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one + if (count($aPrerequisites) > 0) + { + // When 'enabling' a field, all its prerequisites must be enabled too + $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aPrerequisites)."']"; + $oP->add_ready_script("$('#enable_{$sFormPrefix}{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n"); + } + $aDependents = MetaModel::GetDependentAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one + if (count($aDependents) > 0) + { + // When 'disabling' a field, all its dependent fields must be disabled too + $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aDependents)."']"; + $oP->add_ready_script("$('#enable_{$sFormPrefix}{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); + } + if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) + { + if ($oAttDef->GetEditClass() == 'One Way Password') + { + + $sTip = "Unknown values"; + $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"; + + $oDummyObj->Set($sAttCode, null); + $aComments[$sAttCode] = ''; + $aComments[$sAttCode] .= '
?
'; + $sReadyScript .= 'ToogleField(false, \''.$iFormId.'_'.$sAttCode.'\');'."\n"; + } + else + { + $iCount = count($aValues[$sAttCode]); + if ($iCount == 1) + { + // Homogenous value + reset($aValues[$sAttCode]); + $aKeys = array_keys($aValues[$sAttCode]); + $currValue = $aKeys[0]; // The only value is the first key + //echo "

current value for $sAttCode : $currValue

"; + $oDummyObj->Set($sAttCode, $currValue); + $aComments[$sAttCode] = ''; + $aComments[$sAttCode] .= '
1
'; + } + else + { + // Non-homogenous value + $aMultiValues = $aValues[$sAttCode]; + uasort($aMultiValues, 'MyComparison'); + $iMaxCount = 5; + $sTip = "

".Dict::Format('UI:BulkModify_Count_DistinctValues', $iCount)."

    "; + $index = 0; + foreach($aMultiValues as $sCurrValue => $aVal) + { + $sDisplayValue = empty($aVal['display']) ? ''.Dict::S('Enum:Undefined').'' : str_replace(array("\n", "\r"), " ", $aVal['display']); + $sTip .= "
  • ".Dict::Format('UI:BulkModify:Value_Exists_N_Times', $sDisplayValue, $aVal['count'])."
  • "; + $index++; + if ($iMaxCount == $index) + { + $sTip .= "
  • ".Dict::Format('UI:BulkModify:N_MoreValues', count($aMultiValues) - $iMaxCount)."
  • "; + break; + } + } + $sTip .= "

"; + $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"; + + $oDummyObj->Set($sAttCode, null); + $aComments[$sAttCode] = ''; + $aComments[$sAttCode] .= '
'.$iCount.'
'; + } + $sReadyScript .= 'ToogleField('.(($iCount == 1) ? 'true': 'false').', \''.$iFormId.'_'.$sAttCode.'\');'."\n"; + } + } + } + + $oP->add("
\n"); + $oP->add("

".$oDummyObj->GetIcon()." ".Dict::Format('UI:Modify_M_ObjectsOf_Class_OutOf_N', $iAllowedCount, $sClass, $iAllowedCount)."

\n"); + $oP->add("
\n"); + + $oP->add("
\n"); + $oDummyObj->DisplayModifyForm($oP, array('fieldsComments' => $aComments, 'noRelations' => true, 'custom_operation' => 'preview_or_modify_all', 'custom_button' => Dict::S('UI:Button:PreviewModifications'), 'selectObj' => $sSelectedObj, 'filter' => $sFilter, 'preview_mode' => true, 'disabled_fields' => '{}')); + $oP->add("
\n"); + $oP->add_ready_script($sReadyScript); + } // Else no object selected ??? + else + { + $oP->p("No object selected !, nothing to do"); + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'preview_or_modify_all': // Preview or apply bulk modify + $sFilter = utils::ReadParam('filter', ''); + $sClass = utils::ReadParam('class', ''); + $bPreview = utils::ReadParam('preview_mode', ''); + $sSelectedObj = utils::ReadParam('selectObj', ''); + if ( empty($sClass) || empty($sSelectedObj)) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'selectObj')); + } + $aSelectedObj = explode(',', $sSelectedObj); + $aHeaders = array( + 'form::select' => array('label' => "", 'description' => Dict::S('UI:SelectAllToggle+')), + 'object' => array('label' => MetaModel::GetName($sClass), 'description' => Dict::S('UI:ModifiedObject')), + 'status' => array('label' => Dict::S('UI:ModifyAllStatus'), 'description' => Dict::S('UI:ModifyAllStatus+')), + 'errors' => array('label' => Dict::S('UI:ModifyAllErrors'), 'description' => Dict::S('UI:ModifyAllErrors+')), + ); + $aRows = array(); + + $oP->add("
\n"); + $oP->add("

".MetamOdel::GetClassIcon($sClass)." ".Dict::Format('UI:Modify_N_ObjectsOf_Class', count($aSelectedObj), $sClass)."

\n"); + $oP->add("
\n"); + $oP->set_title(Dict::Format('UI:Modify_N_ObjectsOf_Class', count($aSelectedObj), $sClass)); + if (!$bPreview) + { + // Not in preview mode, do the update for real + $sTransactionId = utils::ReadPostedParam('transaction_id', ''); + if (!utils::IsTransactionValid($sTransactionId, false)) + { + $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetName(), $sClassLabel)); + $oP->p("".Dict::S('UI:Error:ObjectAlreadyUpdated')."\n"); + } + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $sUserString = CMDBChange::GetCurrentUserName(); + $oMyChange->Set("userinfo", $sUserString); + $iChangeId = $oMyChange->DBInsert(); + utils::RemoveTransaction($sTransactionId); + } + foreach($aSelectedObj as $iId) + { + $oObj = MetaModel::GetObject($sClass, $iId); + $aErrors = $oObj->UpdateObject(''); + $bResult = (count($aErrors) == 0); + if ($bResult) + { + list($bResult, $aErrors) = $oObj->CheckToWrite(true /* Enforce Read-only fields */); + } + if ($bPreview) + { + $sStatus = $bResult ? Dict::S('UI:BulkModifyStatusOk') : Dict::S('UI:BulkModifyStatusError'); + } + else + { + $sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped'); + } + $sCSSClass = $bResult ? HILIGHT_CLASS_NONE : HILIGHT_CLASS_CRITICAL; + $sChecked = $bResult ? 'checked' : ''; + $sDisabled = $bResult ? '' : 'disabled'; + $aRows[] = array( + 'form::select' => "", + 'object' => $oObj->GetHyperlink(), + 'status' => $sStatus, + 'errors' => '

'.($bResult ? '': implode('

', $aErrors)).'

', + '@class' => $sCSSClass, + ); + if ($bResult && (!$bPreview)) + { + $oObj->DBUpdateTracked($oMyChange); + } + } + $oP->Table($aHeaders, $aRows); + if ($bPreview) + { + // Form to submit: + $oP->add("
\n"); + $aDefaults = utils::ReadParam('default', array()); + $oP->add($oAppContext->GetForForm()); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("    \n"); + $oP->add("\n"); + foreach($_POST as $sKey => $value) + { + if (preg_match('/attr_(.+)/', $sKey, $aMatches)) + { + $oP->add("\n"); + } + } + $oP->add("
\n"); + } + else + { + $oP->add("\n"); + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'new': // Form to create a new object $sClass = utils::ReadParam('class', ''); $sStateCode = utils::ReadParam('state', ''); $bCheckSubClass = utils::ReadParam('checkSubclass', true); @@ -737,14 +1067,6 @@ try { throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); } - // Note: code duplicated to the case 'apply_modify' when a data integrity issue has been found - $oP->add_linked_script("../js/json.js"); - $oP->add_linked_script("../js/forms-json-utils.js"); - $oP->add_linked_script("../js/wizardhelper.js"); - $oP->add_linked_script("../js/wizard.utils.js"); - $oP->add_linked_script("../js/linkswidget.js"); - $oP->add_linked_script("../js/extkeywidget.js"); - $oP->add_linked_script("../js/jquery.blockUI.js"); $aArgs = utils::ReadParam('default', array()); $aContext = $oAppContext->GetAsHash(); @@ -840,7 +1162,9 @@ try } break; - case 'apply_modify': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'apply_modify': // Applying the modifications to an existing object $sClass = utils::ReadPostedParam('class', ''); $sClassLabel = MetaModel::GetName($sClass); $id = utils::ReadPostedParam('id', ''); @@ -894,14 +1218,6 @@ try $bDisplayDetails = false; // Found issues, explain and give the user a second chance // - // Note: code duplicated from the case 'modify' - $oP->add_linked_script("../js/json.js"); - $oP->add_linked_script("../js/forms-json-utils.js"); - $oP->add_linked_script("../js/wizardhelper.js"); - $oP->add_linked_script("../js/wizard.utils.js"); - $oP->add_linked_script("../js/linkswidget.js"); - $oP->add_linked_script("../js/extkeywidget.js"); - $oP->add_linked_script("../js/jquery.blockUI.js"); $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetName(), $sClassLabel)); $oP->add("
\n"); $oP->add("

".$oObj->GetIcon()." ".Dict::Format('UI:ModificationTitle_Class_Object', $sClassLabel, $oObj->GetName())."

\n"); @@ -921,9 +1237,10 @@ try } break; - case 'select_for_deletion': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'select_for_deletion': // Select multiple objects for deletion $sFilter = utils::ReadParam('filter', ''); - $sFormat = utils::ReadParam('format', ''); if (empty($sFilter)) { throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); @@ -943,13 +1260,19 @@ try $oP->add("\n"); break; - case 'bulk_delete_confirmed': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'bulk_delete_confirmed': // Confirm bulk deletion of objects $sTransactionId = utils::ReadPostedParam('transaction_id', ''); if (!utils::IsTransactionValid($sTransactionId)) { throw new ApplicationException(Dict::S('UI:Error:ObjectsAlreadyDeleted')); } - case 'bulk_delete': + // Fall through + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'bulk_delete': // Actual bulk deletion (if confirmed) $sClass = utils::ReadPostedParam('class', ''); $sClassLabel = MetaModel::GetName($sClass); $aSelectObject = utils::ReadPostedParam('selectObject', ''); @@ -970,8 +1293,10 @@ try DeleteObjects($oP, $sClass, $aObjects, ($operation == 'bulk_delete_confirmed')); break; - case 'delete': - case 'delete_confirmed': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'delete': // Deletion (preview) + case 'delete_confirmed': // Deletion (confirmed) $sClass = utils::ReadParam('class', ''); $sClassLabel = MetaModel::GetName($sClass); $id = utils::ReadParam('id', ''); @@ -984,7 +1309,9 @@ try DeleteObjects($oP, $sClass, array($oObj), ($operation == 'delete_confirmed')); break; - case 'apply_new': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'apply_new': // Creation of a new object $sClass = utils::ReadPostedParam('class', ''); $sClassLabel = MetaModel::GetName($sClass); $sTransactionId = utils::ReadPostedParam('transaction_id', ''); @@ -1024,14 +1351,6 @@ try { // Found issues, explain and give the user a second chance // - // Note: code similar to the case 'modify' - $oP->add_linked_script("../js/json.js"); - $oP->add_linked_script("../js/forms-json-utils.js"); - $oP->add_linked_script("../js/wizardhelper.js"); - $oP->add_linked_script("../js/wizard.utils.js"); - $oP->add_linked_script("../js/linkswidget.js"); - $oP->add_linked_script("../js/extkeywidget.js"); - $oP->add_linked_script("../js/jquery.blockUI.js"); $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); $oP->add("

".MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); $oP->add("
\n"); @@ -1043,7 +1362,9 @@ try } break; - case 'wizard_apply_new': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'wizard_apply_new': // no more used ??? $sJson = utils::ReadPostedParam('json_obj', ''); $oWizardHelper = WizardHelper::FromJSON($sJson); $sTransactionId = utils::ReadPostedParam('transaction_id', ''); @@ -1071,7 +1392,318 @@ try } break; - case 'stimulus': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'select_bulk_stimulus': // Form displayed when applying a stimulus to many objects + $sFilter = utils::ReadParam('filter', ''); + $sStimulus = utils::ReadParam('stimulus', ''); + $sState = utils::ReadParam('state', ''); + if (empty($sFilter) || empty($sStimulus) || empty($sState)) + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); + } + $oFilter = DBObjectSearch::unserialize($sFilter); + $sClass = $oFilter->GetClass(); + $aStimuli = MetaModel::EnumStimuli($sClass); + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $oP->set_title($sActionLabel); + $oP->add(''); + + $oChecker = new StimulusChecker($oFilter, $sState, $sStimulus); + $aExtraFormParams = array('stimulus' => $sStimulus, 'state' => $sState); + DisplayMultipleSelectionForm($oP, $oFilter, 'bulk_stimulus', $oChecker, $aExtraFormParams); + break; + + case 'bulk_stimulus': + $oP->set_title(Dict::S('UI:ApplyStimulusMultiple')); + $sFilter = utils::ReadParam('filter', ''); + $sStimulus = utils::ReadParam('stimulus', ''); + $sState = utils::ReadParam('state', ''); + $aSelectObject = utils::ReadPostedParam('selectObject', array()); + if (empty($sFilter) || empty($sStimulus) || empty($sState)) + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); + } + $oFilter = DBObjectSearch::unserialize($sFilter); + $sClass = $oFilter->GetClass(); + if (count($aSelectObject) == 0) + { + // Nothing to do, no object was selected ! + } + else + { + $aTransitions = MetaModel::EnumTransitions($sClass, $sState); + $aStimuli = MetaModel::EnumStimuli($sClass); + + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $aTransition = $aTransitions[$sStimulus]; + $sTargetState = $aTransition['target_state']; + $aStates = MetaModel::EnumStates($sClass); + $aTargetStateDef = $aStates[$sTargetState]; + + $oP->set_title(Dict::Format('UI:StimulusModify_N_ObjectsOf_Class', $sActionLabel, count($aSelectObject), $sClass)); + $oP->add(''); + + $aExpectedAttributes = $aTargetStateDef['attribute_list']; + $aDetails = array(); + $iFieldIndex = 0; + $aFieldsMap = array(); + $aValues = array(); + $aObjects = array(); + foreach($aSelectObject as $iId) + { + $aObjects[] = MetaModel::GetObject($sClass, $iId); + } + $oSet = DBObjectSet::FromArray($sClass, $aObjects); + $oObj = $oSet->ComputeCommonObject($aValues); + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + $oObj->Set($sStateAttCode,$sTargetState); + $sReadyScript = ''; + foreach($aExpectedAttributes as $sAttCode => $iExpectCode) + { + // Prompt for an attribute if + // - the attribute must be changed or must be displayed to the user for confirmation + // - or the field is mandatory and currently empty + if ( ($iExpectCode & (OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) || + (($iExpectCode & OPT_ATT_MANDATORY) && ($oObj->Get($sAttCode) == '')) ) + { + $aAttributesDef = MetaModel::ListAttributeDefs($sClass); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aPrerequisites = MetaModel::GetPrequisiteAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one + if (count($aPrerequisites) > 0) + { + // When 'enabling' a field, all its prerequisites must be enabled too + $sFieldList = "['".implode("','", $aPrerequisites)."']"; + $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n"); + } + $aDependents = MetaModel::GetDependentAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one + if (count($aDependents) > 0) + { + // When 'disabling' a field, all its dependent fields must be disabled too + $sFieldList = "['".implode("','", $aDependents)."']"; + $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); + } + $aArgs = array('this' => $oObj); + $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sAttCode, '', $iExpectCode, $aArgs); + $sComments = ''; + if (count($aValues[$sAttCode]) == 1) + { + $sComments .= '
1
'; + } + else + { + // Non-homogenous value + $iMaxCount = 5; + $sTip = "

".Dict::Format('UI:BulkModify_Count_DistinctValues', count($aValues[$sAttCode]))."

    "; + $index = 0; + foreach($aValues[$sAttCode] as $sCurrValue => $aVal) + { + $sDisplayValue = empty($aVal['display']) ? ''.Dict::S('Enum:Undefined').'' : str_replace(array("\n", "\r"), " ", $aVal['display']); + $sTip .= "
  • ".Dict::Format('UI:BulkModify:Value_Exists_N_Times', $sDisplayValue, $aVal['count'])."
  • "; + $index++; + if ($iMaxCount == $index) + { + $sTip .= "
  • ".(count($aMultiValues) - $iMaxCount)." more different values...
  • "; + break; + } + if ($iMaxCount == $index) + { + $sTip .= "
  • ".Dict::Format('UI:BulkModify:N_MoreValues', count($aValues[$sAttCode]) - $iMaxCount)."
  • "; + break; + } + } + $sTip .= "

"; + $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n"; + $sComments .= '
'.count($aValues[$sAttCode]).'
'; + } + $aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => "$sHTMLValue", 'comments' => $sComments); + $aFieldsMap[$sAttCode] = $sAttCode; + $iFieldIndex++; + } + } + $oObj->DisplayBareProperties($oP); + $oP->add("
\n"); + $oP->add("
\n"); + $oP->details($aDetails); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add($oAppContext->GetForForm()); + $oP->add("\n"); + $oP->add("    \n"); + $oP->add("\n"); + $oP->add("
\n"); + $oP->add("
\n"); + $iFieldsCount = count($aFieldsMap); + $sJsonFieldsMap = json_encode($aFieldsMap); + + $oP->add_script( +<<add_ready_script( +<<p(Dict::S('UI:Error:ObjectAlreadyUpdated')); + } + else + { + // For archiving the modification + $oFilter = DBObjectSearch::unserialize($sFilter); + $sClass = $oFilter->GetClass(); + $aObjects = array(); + foreach($aSelectObject as $iId) + { + $aObjects[] = MetaModel::GetObject($sClass, $iId); + } + + $aTransitions = MetaModel::EnumTransitions($sClass, $sState); + $aStimuli = MetaModel::EnumStimuli($sClass); + + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + + $oP->set_title(Dict::Format('UI:StimulusModify_N_ObjectsOf_Class', $sActionLabel, count($aObjects), $sClass)); + $oP->add(''); + + $oSet = DBObjectSet::FromArray($sClass, $aObjects); + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $sUserString = CMDBChange::GetCurrentUserName(); + $oMyChange->Set("userinfo", $sUserString); + $iChangeId = $oMyChange->DBInsert(); + + // For reporting + $aHeaders = array( + 'object' => array('label' => MetaModel::GetName($sClass), 'description' => Dict::S('UI:ModifiedObject')), + 'status' => array('label' => Dict::S('UI:BulkModifyStatus'), 'description' => Dict::S('UI:BulkModifyStatus+')), + 'errors' => array('label' => Dict::S('UI:BulkModifyErrors'), 'description' => Dict::S('UI:BulkModifyErrors+')), + ); + $aRows = array(); + while ($oObj = $oSet->Fetch()) + { + $sError = Dict::S('UI:BulkModifyStatusOk'); + try + { + $aTransitions = $oObj->EnumTransitions(); + $aStimuli = MetaModel::EnumStimuli($sClass); + if (!isset($aTransitions[$sStimulus])) + { + throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus, $oObj->GetName(), $oObj->GetStateLabel())); + } + else + { + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $aTransition = $aTransitions[$sStimulus]; + $sTargetState = $aTransition['target_state']; + $aTargetStates = MetaModel::EnumStates($sClass); + $aTargetState = $aTargetStates[$sTargetState]; + $aExpectedAttributes = $aTargetState['attribute_list']; + $aDetails = array(); + $aErrors = array(); + foreach($aExpectedAttributes as $sAttCode => $iExpectCode) + { + $iFlags = $oObj->GetAttributeFlags($sAttCode); + if (($iExpectCode & (OPT_ATT_MUSTCHANGE|OPT_ATT_MUSTPROMPT)) || ($oObj->Get($sAttCode) == '') ) + { + $paramValue = utils::ReadPostedParam("attr_$sAttCode", ''); + if ( ($iFlags & OPT_ATT_SLAVE) && ($paramValue != $oObj->Get($sAttCode)) ) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aErrors[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $oAttDef->GetLabel()); + } + else + { + $oObj->Set($sAttCode, $paramValue); + } + } + } + if (count($aErrors) == 0) + { + if ($oObj->ApplyStimulus($sStimulus)) + { + list($bResult, $aErrors) = $oObj->CheckToWrite(); + $sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped'); + if ($bResult) + { + $oObj->DBUpdateTracked($oMyChange); + } + else + { + $sError = '

'.implode('

',$aErrors)."

\n"; + } + } + else + { + $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); + $sError = '

'.Dict::S('UI:FailedToApplyStimuli')."

\n"; + } + } + else + { + $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); + $sError = '

'.implode('

',$aErrors)."

\n"; + } + } + } + catch(Exception $e) + { + $sError = $e->getMessage(); + $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); + } + $aRows[] = array( + 'object' => $oObj->GetHyperlink(), + 'status' => $sStatus, + 'errors' => $sError, + ); + } + $oP->Table($aHeaders, $aRows); + // Back to the list + $sURL = "./UI.php?operation=search&filter=$sFilter&".$oAppContext->GetForLink(); + $oP->add(''); + } + break; + + case 'stimulus': // Form displayed when applying a stimulus (state change) $sClass = utils::ReadParam('class', ''); $id = utils::ReadParam('id', ''); $sStimulus = utils::ReadParam('stimulus', ''); @@ -1094,13 +1726,6 @@ try $aTransition = $aTransitions[$sStimulus]; $sTargetState = $aTransition['target_state']; $aTargetStates = MetaModel::EnumStates($sClass); - $oP->add_linked_script("../js/json.js"); - $oP->add_linked_script("../js/forms-json-utils.js"); - $oP->add_linked_script("../js/wizardhelper.js"); - $oP->add_linked_script("../js/wizard.utils.js"); - $oP->add_linked_script("../js/linkswidget.js"); - $oP->add_linked_script("../js/extkeywidget.js"); - $oP->add_linked_script("../js/jquery.blockUI.js"); $oP->add("
\n"); $oP->add("

$sActionLabel - {$oObj->GetName()}

\n"); $oP->set_title($sActionLabel); @@ -1150,7 +1775,7 @@ try $oP->add_script( <<add("
\n"); - //$oP->add("

$sActionLabel - {$oObj->GetName()}

\n"); - //$oP->add("

$sActionDetails

\n"); - //$oP->p(Dict::Format('UI:Apply_Stimulus_On_Object_In_State_ToTarget_State', $sActionLabel, $oObj->GetName(), $oObj->GetStateLabel(), $sTargetState)); - //$oP->add("
\n"); $aTargetState = $aTargetStates[$sTargetState]; $aExpectedAttributes = $aTargetState['attribute_list']; $aDetails = array(); + $aErrors = array(); foreach($aExpectedAttributes as $sAttCode => $iExpectCode) { + $iFlags = $oObj_>GetAttributeFlags($sAttCode); if (($iExpectCode & (OPT_ATT_MUSTCHANGE|OPT_ATT_MUSTPROMPT)) || ($oObj->Get($sAttCode) == '') ) { $paramValue = utils::ReadPostedParam("attr_$sAttCode", ''); + if ( ($iFlags & OPT_ATT_SLAVE) && ($paramValue != $oObj->Get($sAttCode))) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aErrors[] = Dict::Format('UI:AttemptingToChangeASlaveAttribute_Name', $oAttDef->GetLabel()); + } + else + { $oObj->Set($sAttCode, $paramValue); } } + } + if (count($aErrors) == 0) + { if ($oObj->ApplyStimulus($sStimulus)) { $oMyChange = MetaModel::NewObject("CMDBChange"); @@ -1224,6 +1858,15 @@ EOF $oObj->DBUpdateTracked($oMyChange); $oP->p(Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())); } + else + { + $oP->p(Dict::S('UI:FailedToApplyStimuli')); + } + } + else + { + $oP->p(implode('

', $aErrors)); + } } $oObj->DisplayDetails($oP); } @@ -1234,7 +1877,9 @@ EOF } break; - case 'modify_links': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'modify_links': // ?? still used ?? $sClass = utils::ReadParam('class', ''); $sLinkAttr = utils::ReadParam('link_attr', ''); $sTargetClass = utils::ReadParam('target_class', ''); @@ -1249,7 +1894,9 @@ EOF $oWizard->Display($oP, array('StartWithAdd' => $bAddObjects)); break; - case 'do_modify_links': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'do_modify_links': // ?? still used ?? $aLinks = utils::ReadPostedParam('linkId', array()); $sLinksToRemove = trim(utils::ReadPostedParam('linksToRemove', '')); $aLinksToRemove = array(); @@ -1341,7 +1988,9 @@ EOF $oObj->DisplayDetails($oP); break; - case 'swf_navigator': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'swf_navigator': // Graphical display of the relations "impact" / "depends on" $sClass = utils::ReadParam('class', ''); $id = utils::ReadParam('id', 0); $sRelation = utils::ReadParam('relation', 'impact'); @@ -1400,12 +2049,16 @@ EOF $oP->SetCurrentTab(''); break; - case 'cancel': + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'cancel': // An action was cancelled $oP->set_title(Dict::S('UI:OperationCancelled')); $oP->add('

'.Dict::S('UI:OperationCancelled').'

'); break; - default: + /////////////////////////////////////////////////////////////////////////////////////////// + + default: // Menu node rendering (templates) $oMenuNode = ApplicationMenu::GetMenuNode(ApplicationMenu::GetActiveNodeId()); if (is_object($oMenuNode)) { @@ -1413,6 +2066,9 @@ EOF $oMenuNode->RenderContent($oP, $oAppContext->GetAsHash()); $oP->set_title($oMenuNode->GetLabel()); } + + /////////////////////////////////////////////////////////////////////////////////////////// + } $oKPI->ComputeAndReport('GUI creation before output'); diff --git a/portal/index.php b/portal/index.php index eb69451e8..96b5321e6 100644 --- a/portal/index.php +++ b/portal/index.php @@ -274,7 +274,7 @@ function RequestCreationForm($oP, $oUserOrg) $sJsonFieldsMap = json_encode($aFieldsMap); $oP->add_ready_script( << new var oWizardHelper = new WizardHelper('UserRequest', ''); oWizardHelper.SetFieldsMap($sJsonFieldsMap); oWizardHelper.SetFieldsCount($iFieldsCount);