diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index bba3790f8..753b905ab 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -44,6 +44,7 @@ require_once(APPROOT.'/application/ui.passwordwidget.class.inc.php'); require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php'); require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php'); require_once(APPROOT.'/application/datatable.class.inc.php'); +require_once(APPROOT.'/sources/renderer/console/consoleformrenderer.class.inc.php'); abstract class cmdbAbstractObject extends CMDBObject implements iDisplay { @@ -1712,7 +1713,8 @@ EOF { $bMandatory = 'true'; } - $sValidationField = ""; + $sValidationSpan = ""; + $sReloadSpan = ""; $sHelpText = htmlentities($oAttDef->GetHelpOnEdition(), ENT_QUOTES, 'UTF-8'); $aEventsList = array(); switch($oAttDef->GetEditClass()) @@ -1725,7 +1727,7 @@ EOF { $sDisplayValue = date($oAttDef->GetDateFormat()); } - $sHTMLValue = " {$sValidationField}"; + $sHTMLValue = " {$sValidationSpan}{$sReloadSpan}"; break; case 'DateTime': @@ -1736,7 +1738,7 @@ EOF { $sDisplayValue = date($oAttDef->GetDateFormat()); } - $sHTMLValue = " {$sValidationField}"; + $sHTMLValue = " {$sValidationSpan}{$sReloadSpan}"; break; case 'Duration': @@ -1752,7 +1754,7 @@ EOF $sMinutes = ""; $sSeconds = ""; $sHidden = ""; - $sHTMLValue = Dict::Format('UI:DurationForm_Days_Hours_Minutes_Seconds', $sDays, $sHours, $sMinutes, $sSeconds).$sHidden." ".$sValidationField; + $sHTMLValue = Dict::Format('UI:DurationForm_Days_Hours_Minutes_Seconds', $sDays, $sHours, $sMinutes, $sSeconds).$sHidden." ".$sValidationSpan.$sReloadSpan; $oPage->add_ready_script("$('#{$iId}').bind('update', function(evt, sFormId) { return ToggleDurationField('$iId'); });"); break; @@ -1760,7 +1762,7 @@ EOF $aEventsList[] ='validate'; $aEventsList[] ='keyup'; $aEventsList[] ='change'; - $sHTMLValue = " {$sValidationField}"; + $sHTMLValue = " {$sValidationSpan}{$sReloadSpan}"; break; case 'OQLExpression': @@ -1799,7 +1801,7 @@ EOF $sAdditionalStuff = ""; } // Ok, the text area is drawn here - $sHTMLValue = "
$sAdditionalStuff{$sValidationField}
"; + $sHTMLValue = "
$sAdditionalStuff{$sValidationSpan}{$sReloadSpan}
"; break; @@ -1825,12 +1827,12 @@ EOF $sPreviousLog = is_object($value) ? $value->GetAsHTML($oPage, true /* bEditMode */, array('AttributeText', 'RenderWikiHtml')) : ''; $iEntriesCount = is_object($value) ? count($value->GetIndex()) : 0; $sHidden = ""; // To know how many entries the case log already contains - $sHTMLValue = "
$sHeader$sPreviousLog{$sValidationField}
$sHidden
"; + $sHTMLValue = "
$sHeader$sPreviousLog{$sValidationSpan}{$sReloadSpan}
$sHidden
"; $oPage->add_ready_script("$('#$iId').bind('keyup change validate', function(evt, sFormId) { return ValidateCaseLogField('$iId', $bMandatory, sFormId) } );"); // Custom validation function break; case 'HTML': - $oWidget = new UIHTMLEditorWidget($iId, $oAttDef, $sNameSuffix, $sFieldPrefix, $sHelpText, $sValidationField, $value, $bMandatory); + $oWidget = new UIHTMLEditorWidget($iId, $oAttDef, $sNameSuffix, $sFieldPrefix, $sHelpText, $sValidationSpan.$sReloadSpan, $value, $bMandatory); $sHTMLValue = $oWidget->Display($oPage, $aArgs); break; @@ -1862,7 +1864,7 @@ EOF $sHTMLValue = "\n"; $sHTMLValue .= "\n"; $sHTMLValue .= "$sFileName
\n"; - $sHTMLValue .= " {$sValidationField}\n"; + $sHTMLValue .= " {$sValidationSpan}{$sReloadSpan}\n"; break; case 'StopWatch': @@ -1902,12 +1904,59 @@ EOF $sHTMLValue .= $oAttDef->GetDisplayForm($value, $oPage, true); $sHTMLValue .= ''; $sHTMLValue .= ''; - $sHTMLValue .= ''.$sValidationField.''; + $sHTMLValue .= ''.$sValidationSpan.$sReloadSpan.''; $sHTMLValue .= ''; $sHTMLValue .= ''; $oPage->add_ready_script("$('#$iId :input').bind('keyup change validate', function(evt, sFormId) { return ValidateRedundancySettings('$iId',sFormId); } );"); // Custom validation function break; + case 'CustomFields': + $sHTMLValue = ''; + $sHTMLValue .= ''; + $sHTMLValue .= ''; + $sHTMLValue .= ''; // No validation span for this one: it does handle its own validation! + $sHTMLValue .= ''; + $sHTMLValue .= '
'; + $sHTMLValue .= '
'; + $sHTMLValue .= '
'; + $sHTMLValue .= '
'; + $sHTMLValue .= '
'; + $sHTMLValue .= '
'.$sReloadSpan.'
'; + $sHTMLValue .= "\n"; + + $oForm = $value->GetForm($sFormPrefix); + $oRenderer = new \Combodo\iTop\Renderer\Console\ConsoleFormRenderer($oForm); + $aRenderRes = $oRenderer->Render(); + + $aFormHandlerOptions = array( + 'wizard_helper_var_name' => 'oWizardHelper'.$sFormPrefix, + 'custom_field_attcode' => $sAttCode + ); + $sFormHandlerOptions = json_encode($aFormHandlerOptions); + $aFieldSetOptions = array( + 'field_identifier_attr' => 'data-field-id', // convention: fields are rendered into a div and are identified by this attribute + 'fields_list' => $aRenderRes, + 'fields_impacts' => $oForm->GetFieldsImpacts(), + 'form_path' => $oForm->GetId() + ); + $sFieldSetOptions = json_encode($aFieldSetOptions); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/form_handler.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/console_form_handler.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/field_set.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/form_field.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/subform_field.js'); + $oPage->add_ready_script("$('#{$iId}_console_form').console_form_handler($sFormHandlerOptions);"); + $oPage->add_ready_script("$('#{$iId}_field_set').field_set($sFieldSetOptions);"); + $oPage->add_ready_script("$('#{$iId}_console_form').console_form_handler('alignColumns');"); + $oPage->add_ready_script("$('#{$iId}_console_form').console_form_handler('option', 'field_set', $('#{$iId}_field_set'));"); + // field_change must be processed to refresh the hidden value at anytime + $oPage->add_ready_script("$('#{$iId}_console_form .field_set').bind('field_change', function() { $('#{$iId}').val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values'))); });"); + // update_value is triggered when preparing the wizard helper object for ajax calls + $oPage->add_ready_script("$('#{$iId}').bind('update_value', function() { $(this).val(JSON.stringify($('#{$iId}_field_set').triggerHandler('get_current_values'))); });"); + // validate is triggered by CheckFields, on all the input fields, once at page init and once before submitting the form + $oPage->add_ready_script("$('#{$iId}').bind('validate', function(evt, sFormId) { return ValidateCustomFields('$iId', sFormId) } );"); // Custom validation function + break; + case 'String': default: $aEventsList[] ='validate'; @@ -1925,7 +1974,7 @@ EOF case 'radio_vertical': $sHTMLValue = ''; $bVertical = ($sDisplayStyle != 'radio_horizontal'); - $sHTMLValue = $oPage->GetRadioButtons($aAllowedValues, $value, $iId, "attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}", $bMandatory, $bVertical, $sValidationField); + $sHTMLValue = $oPage->GetRadioButtons($aAllowedValues, $value, $iId, "attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}", $bMandatory, $bVertical, $sValidationSpan.$sReloadSpan); $aEventsList[] ='change'; break; @@ -1946,13 +1995,13 @@ EOF } $sHTMLValue .= "\n"; } - $sHTMLValue .= " {$sValidationField}\n"; + $sHTMLValue .= " {$sValidationSpan}{$sReloadSpan}\n"; $aEventsList[] ='change'; } } else { - $sHTMLValue = " {$sValidationField}"; + $sHTMLValue = " {$sValidationSpan}{$sReloadSpan}"; $aEventsList[] ='keyup'; $aEventsList[] ='change'; } @@ -2977,6 +3026,10 @@ EOF $this->Set($sAttCode, $iValue); } } + elseif ($oAttDef->GetEditClass() == 'CustomFields') + { + $this->Set($sAttCode, $value); + } else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() && (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE))) { @@ -3089,6 +3142,10 @@ EOF { $value = $oAttDef->ReadValueFromPostedForm($sFormPrefix); } + elseif ($oAttDef->GetEditClass() == 'CustomFields') + { + $value = $oAttDef->ReadValueFromPostedForm($this, $sFormPrefix); + } else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() && (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE)) ) { diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index b7993579a..561f1b4b6 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -1,5 +1,5 @@ iId}\">"; + $sValidationField = "iId}\">iId}\">"; $sHTMLValue = ''; $bVertical = ($sDisplayStyle != 'radio_horizontal'); $bExtensions = false; @@ -305,7 +305,7 @@ EOF } if (($sDisplayStyle == 'select') || ($sDisplayStyle == 'list')) { - $sHTMLValue .= "iId}\">"; + $sHTMLValue .= "iId}\">iId}\">"; } $sHTMLValue .= ""; // end of no wrap return $sHTMLValue; diff --git a/application/ui.passwordwidget.class.inc.php b/application/ui.passwordwidget.class.inc.php index d348649fa..0950dd08a 100644 --- a/application/ui.passwordwidget.class.inc.php +++ b/application/ui.passwordwidget.class.inc.php @@ -58,7 +58,7 @@ class UIPasswordWidget $sConfirmPasswordValue = $aPasswordValues ? $aPasswordValues['confirm'] : '*****'; $sChangedValue = (($sPasswordValue != '*****') || ($sConfirmPasswordValue != '*****')) ? 1 : 0; $sHtmlValue = ''; - $sHtmlValue = ' 
'; + $sHtmlValue = ' 
'; $sHtmlValue .= ' '.Dict::S('UI:PasswordConfirm').' '; $sHtmlValue .= ''; diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index f9b99f46b..d4c8eb20a 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -31,6 +31,9 @@ require_once('ormstopwatch.class.inc.php'); require_once('ormpassword.class.inc.php'); require_once('ormcaselog.class.inc.php'); require_once('htmlsanitizer.class.inc.php'); +require_once(APPROOT.'sources/autoload.php'); +require_once('customfieldshandler.class.inc.php'); +require_once('ormcustomfieldsvalue.class.inc.php'); /** * MissingColumnException - sent if an attribute is being created but the column is missing in the row @@ -178,7 +181,6 @@ abstract class AttributeDefinition private function ConsistencyCheck() { - // Check that any mandatory param has been specified // $aExpectedParams = $this->ListExpectedParams(); @@ -192,7 +194,18 @@ abstract class AttributeDefinition throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); } } - } + } + + /** + * Check the validity of the given value + * @param DBObject $oHostObject + * @param string An error if any, null otherwise + */ + public function CheckValue(DBObject $oHostObject, $value) + { + // todo: factorize here the cases implemented into DBObject + return true; + } // table, key field, name field public function ListDBJoins() @@ -212,8 +225,9 @@ abstract class AttributeDefinition public function IsExternalField() {return false;} public function IsWritable() {return false;} public function LoadInObject() {return true;} + public function LoadFromDB() {return true;} public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);} - public function GetValue($oHostObject){return null;} // must return the value if LoadInObject returns false + public function GetValue($oHostObject, $bOriginal = false){return null;} // must return the value if LoadInObject returns false public function IsNullAllowed() {return true;} public function GetCode() {return $this->m_sCode;} public function GetMirrorLinkAttribute() {return null;} @@ -417,7 +431,7 @@ abstract class AttributeDefinition return call_user_func($sComputeFunc); } - abstract public function GetDefaultValue(); + abstract public function GetDefaultValue(DBObject $oHostObject = null); // // To be overloaded in subclasses @@ -503,7 +517,7 @@ abstract class AttributeDefinition /** * List the available verbs for 'GetForTemplate' */ - public static function EnumTemplateVerbs() + public function EnumTemplateVerbs() { return array( '' => 'Plain text (unlocalized) representation', @@ -692,21 +706,9 @@ class AttributeLinkedSet extends AttributeDefinition public function GetValuesDef() {return $this->Get("allowed_values");} public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");} - public function GetDefaultValue($aArgs = array()) + public function GetDefaultValue(DBObject $oHostObject = null) { - // Note: so far, this feature is a prototype, - // later, the argument 'this' should always be present in the arguments - // - if (($this->IsParam('default_value')) && array_key_exists('this', $aArgs)) - { - $aValues = $this->Get('default_value')->GetValues($aArgs); - $oSet = DBObjectSet::FromArray($this->Get('linked_class'), $aValues); - return $oSet; - } - else - { - return DBObjectSet::FromScratch($this->Get('linked_class')); - } + return DBObjectSet::FromScratch($this->Get('linked_class')); } public function GetTrackingLevel() @@ -849,7 +851,7 @@ class AttributeLinkedSet extends AttributeDefinition /** * List the available verbs for 'GetForTemplate' */ - public static function EnumTemplateVerbs() + public function EnumTemplateVerbs() { return array( '' => 'Plain text (unlocalized) representation', @@ -1295,7 +1297,7 @@ class AttributeDBFieldVoid extends AttributeDefinition public function IsScalar() {return true;} public function IsWritable() {return true;} public function GetSQLExpr() {return $this->Get("sql");} - public function GetDefaultValue() {return $this->MakeRealValue("", null);} + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);} public function IsNullAllowed() {return false;} // @@ -1368,7 +1370,7 @@ class AttributeDBField extends AttributeDBFieldVoid { return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed")); } - public function GetDefaultValue() {return $this->MakeRealValue($this->Get("default_value"), null);} + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue($this->Get("default_value"), $oHostObject);} public function IsNullAllowed() {return $this->Get("is_null_allowed");} } @@ -1480,7 +1482,7 @@ class AttributeObjectKey extends AttributeDBFieldVoid public function GetEditClass() {return "String";} protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");} - public function GetDefaultValue() {return 0;} + public function GetDefaultValue(DBObject $oHostObject = null) {return 0;} public function IsNullAllowed() { return $this->Get("is_null_allowed"); @@ -1854,9 +1856,9 @@ class AttributeClass extends AttributeString parent::__construct($sCode, $aParams); } - public function GetDefaultValue() + public function GetDefaultValue(DBObject $oHostObject = null) { - $sDefault = parent::GetDefaultValue(); + $sDefault = parent::GetDefaultValue($oHostObject); if (!$this->IsNullAllowed() && $this->IsNull($sDefault)) { // For this kind of attribute specifying null as default value @@ -1953,7 +1955,7 @@ class AttributeFinalClass extends AttributeString { $this->m_sValue = $sValue; } - public function GetDefaultValue() + public function GetDefaultValue(DBObject $oHostObject = null) { return $this->m_sValue; } @@ -2312,7 +2314,7 @@ class AttributeText extends AttributeString { $sClass = $aMatches[1]; $sName = $aMatches[2]; - + if (MetaModel::IsValidClass($sClass)) { $sClassLabel = MetaModel::GetName($sClass); @@ -2357,7 +2359,7 @@ class AttributeText extends AttributeString { $sClassLabel = $aMatches[1]; $sName = $aMatches[2]; - + if (!MetaModel::IsValidClass($sClassLabel)) { $sClass = MetaModel::GetClassFromLabel($sClassLabel); @@ -2548,8 +2550,8 @@ class AttributeCaseLog extends AttributeLongText return (string) $value; } } - - public function GetDefaultValue() {return new ormCaseLog();} + + public function GetDefaultValue(DBObject $oHostObject = null) {return new ormCaseLog();} public function Equals($val1, $val2) {return ($val1->GetText() == $val2->GetText());} @@ -2719,7 +2721,7 @@ class AttributeCaseLog extends AttributeLongText /** * List the available verbs for 'GetForTemplate' */ - public static function EnumTemplateVerbs() + public function EnumTemplateVerbs() { return array( '' => 'Plain text representation of all the log entries', @@ -3372,9 +3374,9 @@ class AttributeDateTime extends AttributeDBField // This has been done at the time when itop was using TIMESTAMP columns, // now that iTop is using DATETIME columns, it seems possible to have IsNullAllowed returning false... later when this is needed public function IsNullAllowed() {return true;} - public function GetDefaultValue() + public function GetDefaultValue(DBObject $oHostObject = null) { - $default = parent::GetDefaultValue(); + $default = parent::GetDefaultValue($oHostObject); if (!parent::IsNullAllowed()) { @@ -3769,7 +3771,7 @@ class AttributeExternalKey extends AttributeDBFieldVoid public function GetDisplayStyle() { return $this->GetOptional('display_style', 'select'); } - public function GetDefaultValue() {return 0;} + public function GetDefaultValue(DBObject $oHostObject = null) {return 0;} public function IsNullAllowed() { if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys')) @@ -4163,10 +4165,10 @@ class AttributeExternalField extends AttributeDefinition return $oExtAttDef->GetSQLExpr(); } - public function GetDefaultValue() + public function GetDefaultValue(DBObject $oHostObject = null) { $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetDefaultValue(); + return $oExtAttDef->GetDefaultValue(); } public function IsNullAllowed() { @@ -4308,8 +4310,8 @@ class AttributeBlob extends AttributeDefinition public function IsDirectField() {return true;} public function IsScalar() {return true;} public function IsWritable() {return true;} - public function GetDefaultValue() {return "";} - public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);} + public function GetDefaultValue(DBObject $oHostObject = null) {return "";} + public function IsNullAllowed(DBObject $oHostObject = null) {return $this->GetOptional("is_null_allowed", false);} public function GetEditValue($sValue, $oHostObj = null) { @@ -4569,7 +4571,7 @@ class AttributeStopWatch extends AttributeDefinition public function IsDirectField() {return true;} public function IsScalar() {return true;} public function IsWritable() {return false;} - public function GetDefaultValue() {return $this->NewStopWatch();} + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->NewStopWatch();} public function GetEditValue($value, $oHostObj = null) { @@ -5186,9 +5188,21 @@ class AttributeSubItem extends AttributeDefinition public function IsDirectField() {return true;} public function IsScalar() {return true;} public function IsWritable() {return false;} - public function GetDefaultValue() {return null;} + public function GetDefaultValue(DBObject $oHostObject = null) {return null;} // public function IsNullAllowed() {return false;} - public function LoadInObject() {return false;} // if this verb returns true, then GetValue must be implemented + + public function LoadInObject() {return false;} // if this verb returns false, then GetValue must be implemented + + /** + * Used by DBOBject::Get() + */ + public function GetValue($oHostObject, $bOriginal = false) + { + $oParent = $this->GetTargetAttDef(); + $parentValue = $oHostObject->GetStrict($oParent->GetCode()); + $res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject); + return $res; + } // // protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) @@ -5237,16 +5251,6 @@ class AttributeSubItem extends AttributeDefinition return $res; } - /** - * Used by DBOBject::Get() - */ - public function GetValue($parentValue, $oHostObject = null) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject); - return $res; - } - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { $oParent = $this->GetTargetAttDef(); @@ -5303,7 +5307,7 @@ class AttributeOneWayPassword extends AttributeDefinition public function IsDirectField() {return true;} public function IsScalar() {return true;} public function IsWritable() {return true;} - public function GetDefaultValue() {return "";} + public function GetDefaultValue(DBObject $oHostObject = null) {return "";} public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);} // Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted) @@ -5676,7 +5680,7 @@ class AttributeComputedFieldVoid extends AttributeDefinition public function IsScalar() {return true;} public function IsWritable() {return false;} public function GetSQLExpr() {return null;} - public function GetDefaultValue() {return $this->MakeRealValue("", null);} + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);} public function IsNullAllowed() {return false;} // @@ -5827,7 +5831,7 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid { $this->m_sValue = $sValue; } - public function GetDefaultValue() + public function GetDefaultValue(DBObject $oHostObject = null) { return $this->m_sValue; } @@ -5913,7 +5917,7 @@ class AttributeRedundancySettings extends AttributeDBField return 20; } - public function GetDefaultValue($aArgs = array()) + public function GetDefaultValue(DBObject $oHostObject = null) { $sRet = 'disabled'; if ($this->Get('enabled')) @@ -6268,3 +6272,251 @@ class AttributeRedundancySettings extends AttributeDBField return $sRet; } } + +/** + * Custom fields managed by an external implementation + * + * @package iTopORM + */ +class AttributeCustomFields extends AttributeDefinition +{ + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("handler_class")); + } + + public function GetEditClass() {return "CustomFields";} + public function IsWritable() {return true;} + public function LoadFromDB() {return false;} // See ReadValue... + + public function GetDefaultValue(DBObject $oHostObject = null) + { + return new ormCustomFieldsValue($oHostObject, $this->GetCode()); + } + + public function GetBasicFilterOperators() {return array();} + public function GetBasicFilterLooseOperator() {return '';} + public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} + + /** + * @param DBObject $oHostObject + * @param ormCustomFieldsValue|null $oValue + * @return CustomFieldsHandler + */ + public function GetHandler(DBObject $oHostObject, ormCustomFieldsValue $oValue = null) + { + $sHandlerClass = $this->Get('handler_class'); + $oHandler = new $sHandlerClass($oHostObject, $this->GetCode()); + if (!is_null($oValue)) + { + $oHandler->SetCurrentValues($oValue->GetValues()); + } + return $oHandler; + } + + public function GetPrerequisiteAttributes($sClass = null) + { + $sHandlerClass = $this->Get('handler_class'); + return $sHandlerClass::GetPrerequisiteAttributes($sClass); + } + + public function GetEditValue($sValue, $oHostObj = null) + { + return 'GetEditValueNotImplemented for '.get_class($this); + } + + /** + * Makes the string representation out of the values given by the form defined in GetDisplayForm + */ + public function ReadValueFromPostedForm($oHostObject, $sFormPrefix) + { + $aRawData = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->GetCode()}", '{}', 'raw_data'), true); + return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aRawData); + } + + public function MakeRealValue($proposedValue, $oHostObject) + { + if (is_object($proposedValue) && ($proposedValue instanceof ormCustomFieldsValue)) + { + return $proposedValue; + } + elseif (is_string($proposedValue)) + { + $aValues = json_decode($proposedValue, true); + return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); + } + throw new Exception('Unexpected type for the value of a custom fields attribute: '.gettype($proposedValue)); + } + + /** + * Override to build the relevant form field + * + * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behaves more like a Prepare. + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $oField = new Combodo\iTop\Form\Field\SubFormField($this->GetCode(), $sParentFormId); + $oField->SetForm($this->GetForm($oObject)); + } + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + /** + * @param DBObject $oHostObject + * @return Combodo\iTop\Form\Form + */ + public function GetForm(DBObject $oHostObject, $sFormPrefix = null) + { + $oHandler = $this->GetHandler($oHostObject, $oHostObject->Get($this->GetCode())); + $sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode(); + $oHandler->BuildForm($sFormId); + return $oHandler->GetForm(); + } + + /** + * Read the data from where it has been stored. This verb must be implemented as soon as LoadFromDB returns false and LoadInObject returns true + * @param $oHostObject + * @return ormCustomFieldsValue + */ + public function ReadValue($oHostObject) + { + $oHandler = $this->GetHandler($oHostObject); + $aValues = $oHandler->ReadValues(); + $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); + return $oRet; + } + + /** + * Record the data (currently in the processing of recording the host object) + * It is assumed that the data has been checked prior to calling Write() + * @param DBObject $oHostObject + * @param ormCustomFieldsValue|null $oValue (null is the default value) + */ + public function WriteValue(DBObject $oHostObject, ormCustomFieldsValue $oValue = null) + { + $oHandler = $this->GetHandler($oHostObject, $oHostObject->Get($this->GetCode())); + if (is_null($oValue)) + { + $aValues = array(); + } + else + { + $aValues = $oValue->GetValues(); + } + return $oHandler->WriteValues($aValues); + } + + /** + * Check the validity of the data + * @param DBObject $oHostObject + * @param $value + * @return bool|string true or error message + */ + public function CheckValue(DBObject $oHostObject, $value) + { + try + { + $oHandler = $this->GetHandler($oHostObject, $value); + $oHandler->BuildForm(); + $oForm = $oHandler->GetForm(); + $oForm->Validate(); + if ($oForm->GetValid()) + { + $ret = true; + } + else + { + $aMessages = array(); + foreach ($oForm->GetErrorMessages() as $sFieldId => $aFieldMessages) + { + $aMessages[] = $sFieldId.': '.implode(', ', $aFieldMessages); + } + $ret = 'Invalid value: '.implode(', ', $aMessages); + } + } + catch (Exception $e) + { + $ret = $e->getMessage(); + } + return $ret; + } + + /** + * Cleanup data upon object deletion (object id still available here) + * @param DBObject $oHostObject + */ + public function DeleteValue(DBObject $oHostObject) + { + $oHandler = $this->GetHandler($oHostObject, $oHostObject->Get($this->GetCode())); + return $oHandler->DeleteValues(); + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + return $value->GetAsHTML($bLocalize); + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + return $value->GetAsXML($bLocalize); + } + + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + return $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText); + } + + /** + * List the available verbs for 'GetForTemplate' + */ + public function EnumTemplateVerbs() + { + $sHandlerClass = $this->Get('handler_class'); + return $sHandlerClass::EnumTemplateVerbs(); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $oHostObject DBObject The object + * @param $bLocalize bool Whether or not to localize the value + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + return $value->GetForTemplate($sVerb, $bLocalize); + } + + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + return null; + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + */ + public function GetForJSON($value) + { + return null; + } + + /** + * Helper to form a value, given JSON decoded data + * The operation is the opposite to GetForJSON + */ + public function FromJSONToValue($json) + { + return null; + } + + public function Equals($val1, $val2) + { + return $val1->Equals($val2); + } +} + diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index bdae0c013..c9e424d65 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -302,14 +302,10 @@ abstract class CMDBObject extends DBObject { // Stop watches - record changes for sub items only (they are visible, the rest is not visible) // - if (is_null($original)) - { - $original = new OrmStopWatch(); - } foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef) { - $item_value = $oSubItemAttDef->GetValue($value); - $item_original = $oSubItemAttDef->GetValue($original); + $item_value = $oSubItemAttDef->GetValue($this); + $item_original = $oSubItemAttDef->GetValue($this, true); if ($item_value != $item_original) { diff --git a/core/customfieldshandler.class.inc.php b/core/customfieldshandler.class.inc.php new file mode 100644 index 000000000..19d21f1fe --- /dev/null +++ b/core/customfieldshandler.class.inc.php @@ -0,0 +1,127 @@ + + +use Combodo\iTop\Form\Form; +use Combodo\iTop\Form\FormManager; + +/** + * Base class to implement a handler for AttributeCustomFields + * + * @copyright Copyright (C) 2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +abstract class CustomFieldsHandler +{ + protected $oHostObject; + protected $sAttCode; + protected $aValues; + protected $oForm; + + /** + * This constructor's prototype must be frozen. + * Any specific behavior must be implemented in BuildForm() + * + * @param DBObject $oHostObject + * @param $sAttCode + */ + final public function __construct(DBObject $oHostObject, $sAttCode) + { + $this->oHostObject = $oHostObject; + $this->sAttCode = $sAttCode; + $this->aValues = null; + } + + abstract public function BuildForm($sFormId); + + /** + * + * @return \Combodo\iTop\Form\Form + */ + public function GetForm() + { + return $this->oForm; + } + + public function SetCurrentValues($aValues) + { + $this->aValues = $aValues; + } + + static public function GetPrerequisiteAttributes($sClass = null) + { + return array(); + } + + /** + * List the available verbs for 'GetForTemplate' + */ + static public function EnumTemplateVerbs() + { + return array(); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $aValues array The current values + * @param $sVerb string The verb specifying the representation of the value + * @param $bLocalize bool Whether or not to localize the value + * @return string + */ + abstract public function GetForTemplate($aValues, $sVerb, $bLocalize = true); + + /** + * @param $aValues + * @param bool|true $bLocalize + * @return mixed + */ + abstract public function GetAsHTML($aValues, $bLocalize = true); + + /** + * @param $aValues + * @param bool|true $bLocalize + * @return mixed + */ + abstract public function GetAsXML($aValues, $bLocalize = true); + + /** + * @param $aValues + * @param string $sSeparator + * @param string $sTextQualifier + * @param bool|true $bLocalize + * @return mixed + */ + abstract public function GetAsCSV($aValues, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true); + + /** + * @return array Associative array id => value + */ + abstract public function ReadValues(); + + /** + * Record the data (currently in the processing of recording the host object) + * It is assumed that the data has been checked prior to calling Write() + * @param array Associative array id => value + */ + abstract public function WriteValues($aValues); + + /** + * Cleanup data upon object deletion (object id still available here) + */ + abstract public function DeleteValues(); +} diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 469eb697b..c0ed8e287 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -116,7 +116,7 @@ abstract class DBObject implements iDisplay // set default values foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) { - $this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue(); + $this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue($this); $this->m_aOrigValues[$sAttCode] = null; if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName)) { @@ -291,16 +291,30 @@ abstract class DBObject implements iDisplay if (!$oAttDef->LoadInObject()) continue; - // Note: we assume that, for a given attribute, if it can be loaded, - // then one column will be found with an empty suffix, the others have a suffix - // Take care: the function isset will return false in case the value is null, - // which is something that could happen on open joins - $sAttRef = $sClassAlias.$sAttCode; - - if (array_key_exists($sAttRef, $aRow)) + unset($value); + $bIsDefined = false; + if ($oAttDef->LoadFromDB()) { - $value = $oAttDef->FromSQLToValue($aRow, $sAttRef); + // Note: we assume that, for a given attribute, if it can be loaded, + // then one column will be found with an empty suffix, the others have a suffix + // Take care: the function isset will return false in case the value is null, + // which is something that could happen on open joins + $sAttRef = $sClassAlias.$sAttCode; + if (array_key_exists($sAttRef, $aRow)) + { + $value = $oAttDef->FromSQLToValue($aRow, $sAttRef); + $bIsDefined = true; + } + } + else + { + $value = $oAttDef->ReadValue($this); + $bIsDefined = true; + } + + if ($bIsDefined) + { $this->m_aCurrValues[$sAttCode] = $value; if (is_object($value)) { @@ -380,7 +394,7 @@ abstract class DBObject implements iDisplay { if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sAttCode)) { - $this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue(); + $this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue($this); unset($this->m_aLoadedAtt[$sCode]); } } @@ -492,9 +506,7 @@ abstract class DBObject implements iDisplay if (!$oAttDef->LoadInObject()) { - $sParentAttCode = $oAttDef->GetParentAttCode(); - $parentValue = $this->GetStrict($sParentAttCode); - $value = $oAttDef->GetValue($parentValue, $this); + $value = $oAttDef->GetValue($this); } else { @@ -550,7 +562,7 @@ abstract class DBObject implements iDisplay } else { - $this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue(); + $this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue($this); } $this->m_aLoadedAtt[$sCode] = true; } @@ -1109,6 +1121,10 @@ abstract class DBObject implements iDisplay return "Wrong format [$toCheck]"; } } + else + { + return $oAtt->CheckValue($this, $toCheck); + } return true; } @@ -1405,6 +1421,17 @@ abstract class DBObject implements iDisplay } } + // used both by insert/update + private function WriteExternalAttributes() + { + foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if (!$oAttDef->LoadInObject()) continue; + if ($oAttDef->LoadFromDB()) continue; + $oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]); + } + } + // Note: this is experimental - it was designed to speed up the setup of iTop // Known limitations: // - does not work with multi-table classes (issue with the unique id to maintain in several tables) @@ -1591,6 +1618,8 @@ abstract class DBObject implements iDisplay } $this->DBWriteLinks(); + $this->WriteExternalAttributes(); + $this->m_bIsInDB = true; $this->m_bDirty = false; @@ -1878,6 +1907,7 @@ abstract class DBObject implements iDisplay } $this->DBWriteLinks(); + $this->WriteExternalAttributes(); $this->m_bDirty = false; $this->AfterUpdate(); @@ -1981,6 +2011,10 @@ abstract class DBObject implements iDisplay } MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); } + elseif (!$oAttDef->LoadFromDB()) + { + $oAttDef->DeleteValue($this); + } } foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass) @@ -2222,7 +2256,7 @@ abstract class DBObject implements iDisplay public function Reset($sAttCode) { $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - $this->Set($sAttCode, $oAttDef->GetDefaultValue()); + $this->Set($sAttCode, $oAttDef->GetDefaultValue($this)); return true; } diff --git a/core/ormcustomfieldsvalue.class.inc.php b/core/ormcustomfieldsvalue.class.inc.php new file mode 100644 index 000000000..5720560f1 --- /dev/null +++ b/core/ormcustomfieldsvalue.class.inc.php @@ -0,0 +1,101 @@ + + + +/** + * Base class to hold the value managed by CustomFieldsHandler + * + * @copyright Copyright (C) 2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class ormCustomFieldsValue +{ + protected $oHostObject; + protected $sAttCode; + protected $aCurrentValues; + + /** + * @param DBObject $oHostObject + * @param $sAttCode + */ + public function __construct(DBObject $oHostObject, $sAttCode, $aCurrentValues = null) + { + $this->oHostObject = $oHostObject; + $this->sAttCode = $sAttCode; + $this->aCurrentValues = $aCurrentValues; + } + + public function GetValues() + { + return $this->aCurrentValues; + } + + /** + * Wrapper used when the only thing you have is the value... + * @return \Combodo\iTop\Form\Form + */ + public function GetForm() + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + return $oAttDef->GetForm($this->oHostObject); + } + + public function GetAsHTML($bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->oHostObject, $this); + return $oHandler->GetAsHTML($this->aCurrentValues, $bLocalize); + } + + public function GetAsXML($bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->oHostObject, $this); + return $oHandler->GetAsXML($this->aCurrentValues, $bLocalize); + } + + public function GetAsCSV($sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->oHostObject, $this); + return $oHandler->GetAsCSV($this->aCurrentValues, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $bLocalize bool Whether or not to localize the value + */ + public function GetForTemplate($sVerb, $bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->oHostObject, $this); + return 'template...verb='.$sVerb.' sur "'.json_encode($this->aCurrentValues).'"'; + } + + /** + * @param ormCustomFieldsValue $fellow + * @return bool + */ + public function Equals(ormCustomFieldsValue $oReference) + { + return (json_encode($this->aCurrentValues) === json_encode($oReference->aCurrentValues)); + } +} diff --git a/js/console_form_handler.js b/js/console_form_handler.js new file mode 100644 index 000000000..7432157a9 --- /dev/null +++ b/js/console_form_handler.js @@ -0,0 +1,115 @@ +// Copyright (C) 2010-2016 Combodo SARL +// +// 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 + +//iTop Form handler +; +$(function() +{ + // the widget definition, where 'itop' is the namespace, + // 'consoleform_handler' the widget name + $.widget( 'itop.console_form_handler', $.itop.form_handler, + { + // default options + options: + { + wizard_helper_var_name: '', // Name of the global variable pointing to the wizard helper + custom_field_attcode: '' + }, + + // the constructor + _create: function() + { + var me = this; + + this.element + .addClass('console_form_handler'); + + this.options.oWizardHelper = window[this.options.wizard_helper_var_name]; + + this._super(); + }, + + // events bound via _bind are removed automatically + // revert other modifications here + _destroy: function() + { + this.element + .removeClass('console_form_handler'); + this._super(); + }, + _onUpdateFields: function(event, data) + { + var me = this; + var sFormPath = data.form_path; + var sUpdateUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'; + + $(this.element.find('[data-form-path="' + sFormPath + '"]')).block({message:''}); + $.post( + sUpdateUrl, + { + operation: 'custom_fields_update', + attcode: this.options.custom_field_attcode, + //current_values: this.getCurrentValues(), + requested_fields: data.requested_fields, + form_path: sFormPath, + json_obj: this.options.oWizardHelper.UpdateWizardToJSON() + }, + function(data){ + me._onUpdateSuccess(data, sFormPath); + } + ) + .fail(function(data){ me._onUpdateFailure(data, sFormPath); }) + .always(function(data){ + me.alignColumns(); + $(me.element.find('[data-form-path="' + sFormPath + '"]')).unblock(); + me._onUpdateAlways(data, sFormPath); + }); + }, + // On initialization or update + alignColumns: function() + { + var iMaxWidth = 0; + var oLabels = $(this.element.find('td.form-field-label')); + // Reset the width to the automatic (original) value + oLabels.width(''); + oLabels.each(function() { + iMaxWidth = Math.max(iMaxWidth, $(this).width()); + }); + oLabels.width(iMaxWidth); + }, + // Intended for overloading in derived classes + _onSubmitClick: function() + { + }, + // Intended for overloading in derived classes + _onCancelClick: function() + { + }, + // Intended for overloading in derived classes + _onUpdateFailure: function(data) + { + }, + // Intended for overloading in derived classes + _disableFormBeforeLoading: function() + { + }, + // Intended for overloading in derived classes + _enableFormAfterLoading: function() + { + }, + }); +}); diff --git a/js/extkeywidget.js b/js/extkeywidget.js index 6cfe63dc5..4e179a1c8 100644 --- a/js/extkeywidget.js +++ b/js/extkeywidget.js @@ -1,4 +1,4 @@ -// Copyright (C) 2010-2012 Combodo SARL +// Copyright (C) 2010-2016 Combodo SARL // // This file is part of iTop. // @@ -28,7 +28,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper this.ajax_request = null; this.bSelectMode = bSelectMode; // true if the edited field is a SELECT, false if it's an autocomplete this.bSearchMode = bSearchMode; // true if selecting a value in the context of a search form - this.v_html = ''; var me = this; this.Init = function() @@ -55,8 +54,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper // Query the server to get the form to search for target objects if (me.bSelectMode) { - me.v_html = $('#v_'+me.id).html(); - $('#v_'+me.id).html(''); + $('#fstatus_'+me.id).html(''); } else { @@ -284,8 +282,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper // Query the server to get the form to create a target object if (me.bSelectMode) { - me.v_html = $('#v_'+me.id).html(); - $('#v_'+me.id).html(''); + $('#fstatus_'+me.id).html(''); } else { @@ -336,7 +333,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper { if (me.bSelectMode) { - $('#v_'+me.id).html(me.v_html); } else { @@ -446,8 +442,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper if (me.bSelectMode) { - me.v_html = $('#v_'+me.id).html(); - $('#v_'+me.id).html(''); + $('#fstatus_'+me.id).html(''); } else { @@ -501,7 +496,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper { if (me.bSelectMode) { - $('#v_'+me.id).html(me.v_html); } else { diff --git a/js/field_set.js b/js/field_set.js index e3d808d7f..22fbe7258 100644 --- a/js/field_set.js +++ b/js/field_set.js @@ -1,3 +1,20 @@ +// Copyright (C) 2010-2016 Combodo SARL +// +// 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 + //iTop Field set //Used by itop.form_handler and itop.subform_field to list their fields ; @@ -36,15 +53,15 @@ $(function() this.element .bind('field_change', function(oEvent, oData){ - console.log('field_set: field_change'); + //console.log('field_set: field_change'); me._onFieldChange(oEvent, oData); }) .bind('update_form', function(oEvent, oData){ - console.log('field_set: update_form'); + //console.log('field_set: update_form'); me._onUpdateForm(oEvent, oData); }) .bind('get_current_values', function(oEvent, oData){ - console.log('field_set: get_current_values'); + //console.log('field_set: get_current_values'); return me._onGetCurrentValues(oEvent, oData); }) .bind('validate', function(oEvent, oData){ @@ -52,7 +69,7 @@ $(function() { oData = {}; } - console.log('field_set: validate'); + //console.log('field_set: validate'); return me._onValidate(oEvent, oData); }); @@ -175,8 +192,9 @@ $(function() } // Adding code to the dom - this.options.script_element.append('\n\n// Appended by update on ' + Date() + '\n' + this.buildData.script_code); - this.options.style_element.append('\n\n// Appended by update on ' + Date() + '\n' + this.buildData.style_code); + // Note : We use text() instead of append(), otherwise the code will be interpreted as DOM tags (text + + ...) and can break some browsers + this.options.script_element.text( this.options.script_element.text() + '\n\n// Appended by update on ' + Date() + '\n' + this.buildData.script_code); + this.options.style_element.text( this.options.style_element.text() + '\n\n// Appended by update on ' + Date() + '\n' + this.buildData.style_code); // Evaluating script code as adding it to dom did not executed it (only script from update !) eval(this.buildData.script_code); @@ -291,7 +309,7 @@ $(function() var oField = this.options.fields_list[i]; if(oField.id === undefined) { - console.log('Field set : An field must have at least an id property.'); + console.log('Field set : A field must have at least an id property.'); return false; } diff --git a/js/forms-json-utils.js b/js/forms-json-utils.js index d87f4ae9b..e6a549715 100644 --- a/js/forms-json-utils.js +++ b/js/forms-json-utils.js @@ -1,3 +1,20 @@ +// Copyright (C) 2010-2016 Combodo SARL +// +// 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 + // ID of the (hidden) form field used to store the JSON representation of the // object being edited in this page var sJsonFieldId = 'json_object'; @@ -417,6 +434,15 @@ function ValidateRedundancySettings(sFieldId, sFormId) return bValid; } +//Special validation function for custom fields +function ValidateCustomFields(sFieldId, sFormId) +{ + var oFieldSet = $('#'+sFieldId+'_console_form').console_form_handler('option', 'field_set'); + bValid = oFieldSet.triggerHandler('validate'); + ReportFieldValidationStatus(sFieldId, sFormId, bValid, ''); + return bValid; +} + // Manage a 'duration' field function UpdateDuration(iId) { diff --git a/js/wizardhelper.js b/js/wizardhelper.js index 8dae04f13..2b3b1f1a8 100644 --- a/js/wizardhelper.js +++ b/js/wizardhelper.js @@ -1,3 +1,20 @@ +// Copyright (C) 2010-2016 Combodo SARL +// +// 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 + // Wizard Helper JavaScript class to communicate with the WizardHelper PHP class if (!Array.prototype.indexOf) // Emulation of the indexOf function for IE and old browsers @@ -151,6 +168,7 @@ function WizardHelper(sClass, sFormPrefix, sState) { operation: 'wizard_helper', json_obj: this.ToJSON() }, function(html){ $('#ajax_content').html(html); + $('.blockUI').parent().unblock(); //console.log('data received:', oWizardHelper); //oWizardHelper.FromJSON(json_data); //oWizardHelper.UpdateFields(); // Is done directly in the html provided by ajax.render.php @@ -191,7 +209,8 @@ function WizardHelper(sClass, sFormPrefix, sState) { sAttCode = aFieldNames[index]; sFieldId = this.GetFieldId(sAttCode); - $('#v_'+sFieldId).html(''); + $('#fstatus_'+sFieldId).html(''); + $('#field_'+sFieldId).closest('td').block({message:''}); this.RequestAllowedValues(sAttCode); index++; } diff --git a/pages/ajax.render.php b/pages/ajax.render.php index b582cd26a..5495d868d 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -1,5 +1,5 @@ GetFieldsForDefaultValue() as $sAttCode) { $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $defaultValue = $oAttDef->GetDefaultValue(); + $defaultValue = $oAttDef->GetDefaultValue($oObj); $oWizardHelper->SetDefaultValue($sAttCode, $defaultValue); $oObj->Set($sAttCode, $defaultValue); } @@ -2448,6 +2448,28 @@ EOF } } break; + + case 'custom_fields_update': + $oPage->SetContentType('application/json'); + $sAttCode = utils::ReadParam('attcode', ''); + $aRequestedFields = utils::ReadParam('requested_fields', array()); + $sRequestedFieldsFormPath = utils::ReadParam('form_path', ''); + $sJson = utils::ReadParam('json_obj', '', false, 'raw_data'); + + $oWizardHelper = WizardHelper::FromJSON($sJson); + $oObj = $oWizardHelper->GetTargetObject(); + + $oOrmCustomFieldValue = $oObj->Get($sAttCode); + $oForm = $oOrmCustomFieldValue->GetForm(); + $oSubForm = $oForm->FindSubForm($sRequestedFieldsFormPath); + $oRenderer = new \Combodo\iTop\Renderer\Console\ConsoleFormRenderer($oSubForm); + $aRenderRes = $oRenderer->Render($aRequestedFields); + + $aResult = array(); + $aResult['form']['updated_fields'] = $aRenderRes; + $oPage->add(json_encode($aResult)); + break; + default: $oPage->p("Invalid query."); } diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 460c58d47..543bfae4c 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1206,6 +1206,10 @@ EOF; $aParameters['min_up_mode'] = $this->GetMandatoryPropString($oField, 'min_up_mode'); $aParameters['min_up_type'] = $this->GetMandatoryPropString($oField, 'min_up_type'); } + elseif ($sAttType == 'AttributeCustomFields') + { + $aParameters['handler_class'] = $this->GetMandatoryPropString($oField, 'handler_class'); + } else { $aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')" diff --git a/sources/form/field/subformfield.class.inc.php b/sources/form/field/subformfield.class.inc.php index 9f313fd3c..1048a13e7 100644 --- a/sources/form/field/subformfield.class.inc.php +++ b/sources/form/field/subformfield.class.inc.php @@ -61,7 +61,7 @@ class SubFormField extends Field */ public function Validate() { - $this->oForm->Validate(); + return $this->oForm->Validate(); } /** @@ -106,4 +106,13 @@ class SubFormField extends Field $this->oForm->SetCurrentValues($value); return $this; } + + /** + * @param $sFormPath + * @return Form|null + */ + public function FindSubForm($sFormPath) + { + return $this->oForm->FindSubForm($sFormPath); + } } diff --git a/sources/form/form.class.inc.php b/sources/form/form.class.inc.php index 0b1d88a65..2490eb49d 100644 --- a/sources/form/form.class.inc.php +++ b/sources/form/form.class.inc.php @@ -19,6 +19,7 @@ namespace Combodo\iTop\Form; +use Combodo\iTop\Form\Field\SubFormField; use \Exception; use \Dict; use \Combodo\iTop\Form\Field\Field; @@ -312,83 +313,108 @@ class Form * @param string $sDependsOnId * @return \Combodo\iTop\Form\Form */ - public function AddFieldDependency($sFieldId, $sDependsOnId) - { - if (!array_key_exists($sFieldId, $this->aDependencies)) - { - $this->aDependencies[$sFieldId] = array(); - } - $this->aDependencies[$sFieldId][] = $sDependsOnId; - return $this; - } + public function AddFieldDependency($sFieldId, $sDependsOnId) + { + if (!array_key_exists($sFieldId, $this->aDependencies)) + { + $this->aDependencies[$sFieldId] = array(); + } + $this->aDependencies[$sFieldId][] = $sDependsOnId; + return $this; + } + + /** + * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array + * (It kind of reversed the dependencies array) + * + * eg : + * - 'service' => array('subservice', 'template') + * - 'subservice' => array() + * - ... + * + * @return array + */ + public function GetFieldsImpacts() + { + $aRes = array(); + + foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds) + { + foreach ($aDependentFieldsIds as $sDependentFieldId) + { + if (!array_key_exists($sDependentFieldId, $aRes)) + { + $aRes[$sDependentFieldId] = array(); + } + $aRes[$sDependentFieldId][] = $sImpactedFieldId; + } + } + + return $aRes; + } /** - * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array - * (It kind of reversed the dependencies array) - * - * eg : - * - 'service' => array('subservice', 'template') - * - 'subservice' => array() - * - ... - * - * @return array + * @param $sFormPath + * @return Form|null */ - public function GetFieldsImpacts() + public function FindSubForm($sFormPath) { - $aRes = array(); - - foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds) + $ret = null; + if ($sFormPath == $this->sId) { - foreach ($aDependentFieldsIds as $sDependentFieldId) + $ret = $this; + } + else + { + foreach ($this->aFields as $oField) { - if (!array_key_exists($sDependentFieldId, $aRes)) + if ($oField instanceof SubFormField) { - $aRes[$sDependentFieldId] = array(); + $ret = $oField->FindSubForm($sFormPath); + if ($ret !== null) break; } - $aRes[$sDependentFieldId][] = $sImpactedFieldId; } } - - return $aRes; + return $ret; } /** * */ - public function Finalize() - { - //TODO : Call GetOrderedFields - // Must call OnFinalize on each fields, regarding the dependencies order - // On a SubFormField, will call its own Finalize - foreach ($this->aFields as $sId => $oField) - { - $oField->OnFinalize(); - } - } + public function Finalize() + { + //TODO : Call GetOrderedFields + // Must call OnFinalize on each fields, regarding the dependencies order + // On a SubFormField, will call its own Finalize + foreach ($this->aFields as $sId => $oField) + { + $oField->OnFinalize(); + } + } /** * Validate the form and return if it's valid or not * * @return boolean */ - public function Validate() - { - $this->SetValid(true); - $this->EmptyErrorMessages(); + public function Validate() + { + $this->SetValid(true); + $this->EmptyErrorMessages(); - foreach ($this->aFields as $oField) - { - if (!$oField->Validate()) - { - $this->SetValid(false); - foreach ($oField->GetErrorMessages() as $sErrorMessage) - { - $this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid()); - } - } - } + foreach ($this->aFields as $oField) + { + if (!$oField->Validate()) + { + $this->SetValid(false); + foreach ($oField->GetErrorMessages() as $sErrorMessage) + { + $this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid()); + } + } + } - return $this->GetValid(); - } + return $this->GetValid(); + } } diff --git a/sources/renderer/console/consoleformrenderer.class.inc.php b/sources/renderer/console/consoleformrenderer.class.inc.php index 79da01cae..66d228a0e 100644 --- a/sources/renderer/console/consoleformrenderer.class.inc.php +++ b/sources/renderer/console/consoleformrenderer.class.inc.php @@ -33,7 +33,9 @@ class ConsoleFormRenderer extends FormRenderer public function __construct(Form $oForm) { parent::__construct($oForm); + $this->AddSupportedField('HiddenField', 'ConsoleSimpleFieldRenderer'); $this->AddSupportedField('StringField', 'ConsoleSimpleFieldRenderer'); + $this->AddSupportedField('SelectField', 'ConsoleSimpleFieldRenderer'); $this->AddSupportedField('SubFormField', 'ConsoleSubFormFieldRenderer'); } } \ No newline at end of file diff --git a/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php b/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php index 3dc862aba..536c24830 100644 --- a/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php +++ b/sources/renderer/console/fieldrenderer/consolesimplefieldrenderer.class.inc.php @@ -18,6 +18,7 @@ namespace Combodo\iTop\Renderer\Console\FieldRenderer; +use Combodo\iTop\Form\Field\StringField; use \Dict; use Combodo\iTop\Renderer\FieldRenderer; use Combodo\iTop\Renderer\RenderingOutput; @@ -29,35 +30,60 @@ class ConsoleSimpleFieldRenderer extends FieldRenderer $oOutput = new RenderingOutput(); $sFieldClass = get_class($this->oField); - // TODO : Shouldn't we have a field type so we don't have to maintain FQN classname ? - // Rendering field in edition mode - if (!$this->oField->GetReadOnly()) + if ($sFieldClass == 'Combodo\\iTop\\Form\\Field\\HiddenField') { - switch ($sFieldClass) - { - case 'Combodo\\iTop\\Form\\Field\\StringField': - if ($this->oField->GetLabel() !== '') - { - $oOutput->AddHtml(''); - } - $oOutput->AddHtml(''); - $oOutput->AddHtml(''); - break; - } + $oOutput->AddHtml(''); } - // ... and in read-only mode else { + $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + if ($this->oField->GetLabel() != '') + { + $oOutput->AddHtml(''); + } switch ($sFieldClass) { case 'Combodo\\iTop\\Form\\Field\\StringField': - if ($this->oField->GetLabel() !== '') + $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + break; + + case 'Combodo\\iTop\\Form\\Field\\SelectField': + $oOutput->AddHtml(''); break; } + $oOutput->AddHtml(''); + $oOutput->AddHtml('
'); + if ($this->oField->GetReadOnly()) { - $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + $oOutput->AddHtml(''.htmlentities($this->oField->GetCurrentValue(), ENT_QUOTES, 'UTF-8').''); } - $oOutput->AddHtml('
' . $this->oField->GetCurrentValue() . '
'); + else + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml('
'); + if ($this->oField->GetReadOnly()) + { + $aChoices = $this->oField->GetChoices(); + $sCurrentLabel = isset($aChoices[$this->oField->GetCurrentValue()]) ? $aChoices[$this->oField->GetCurrentValue()] : '' ; + $oOutput->AddHtml(''); + $oOutput->AddHtml(''.htmlentities($sCurrentLabel, ENT_QUOTES, 'UTF-8').''); + } + else + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml('
'); } switch ($sFieldClass) @@ -65,7 +91,22 @@ class ConsoleSimpleFieldRenderer extends FieldRenderer case 'Combodo\\iTop\\Form\\Field\\StringField': $oOutput->AddJs( <<oField->GetGlobalId()}").off("change").on("change keyup", function(){ + $("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){ + var me = this; + + $(this).closest(".field_set").trigger("field_change", { + id: $(me).attr("id"), + name: $(me).closest(".form_field").attr("data-field-id"), + value: $(me).val() + }); + }); +EOF + ); + break; + case 'Combodo\\iTop\\Form\\Field\\SelectField': + $oOutput->AddJs( +<<oField->GetGlobalId()}").off("change").on("change", function(){ var me = this; $(this).closest(".field_set").trigger("field_change", { @@ -88,7 +129,6 @@ EOF 'message' => Dict::S($oValidator->GetErrorMessage()) ); } - $sValidators = json_encode($aValidators); $sFormFieldOptions = <<AddJs( + <<AddJs( + <<AddJs( - <<