Ver código fonte

Customer portal : User Profile brick that allows basic Contact informations edition, password / preferences change from the portal

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4068 a333f486-631f-4898-b8df-5754b55c2be0
glajarige 9 anos atrás
pai
commit
bfee62379d

+ 2 - 0
datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php

@@ -67,6 +67,8 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Brick:Portal:UserProfile:Password:Title' => 'Password',
 	'Brick:Portal:UserProfile:Password:ChoosePassword' => 'Choose password',
 	'Brick:Portal:UserProfile:Password:ConfirmPassword' => 'Confirm password',
+	'Brick:Portal:UserProfile:Password:CantChangeContactAdministrator' => 'To change your password, please contact your iTop administrator',
+	'Brick:Portal:UserProfile:Password:CantChangeForUnknownReason' => 'Can\'t change password, please contact your iTop administrator',
 	'Brick:Portal:UserProfile:PersonalInformations:Title' => 'Personal informations',
 	'Brick:Portal:UserProfile:Photo:Title' => 'Photo',
 ));

+ 2 - 0
datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php

@@ -67,6 +67,8 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Brick:Portal:UserProfile:Password:Title' => 'Mot de passe',
 	'Brick:Portal:UserProfile:Password:ChoosePassword' => 'Choisissez un mot de passe',
 	'Brick:Portal:UserProfile:Password:ConfirmPassword' => 'Confirmer le mot de passe',
+	'Brick:Portal:UserProfile:Password:CantChangeContactAdministrator' => 'Veuillez vous adresser à votre administrateur iTop pour changer votre mot de passe',
+	'Brick:Portal:UserProfile:Password:CantChangeForUnknownReason' => 'Impossible de modifier votre mot de passe, veuillez contacter votre administrateur iTop',
 	'Brick:Portal:UserProfile:PersonalInformations:Title' => 'Informations personnelles',
 	'Brick:Portal:UserProfile:Photo:Title' => 'Photo',
 ));

+ 6 - 8
datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php

@@ -25,7 +25,6 @@ use \Symfony\Component\HttpFoundation\Response;
 use \Symfony\Component\HttpFoundation\RedirectResponse;
 use \Symfony\Component\HttpKernel\HttpKernelInterface;
 use \Exception;
-use \SecurityException;
 use \FileUploadException;
 use \utils;
 use \Dict;
@@ -36,10 +35,8 @@ use \BinaryExpression;
 use \FieldExpression;
 use \VariableExpression;
 use \DBObjectSet;
-use \CMDBObject;
 use \cmdbAbstractObject;
 use \UserRights;
-use \Combodo\iTop\Portal\Brick\BrowseBrick;
 use \Combodo\iTop\Portal\Helper\ApplicationHelper;
 use \Combodo\iTop\Portal\Helper\SecurityHelper;
 use \Combodo\iTop\Portal\Helper\ContextManipulatorHelper;
@@ -133,7 +130,9 @@ class ObjectController extends AbstractController
 		}
 		
 		// Checking security layers
-		if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId))
+		// Warning : This is a dirty quick fix to allow editing its own contact information
+		$bAllowWrite = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId());
+		if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite)
 		{
 			$oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
 		}
@@ -432,7 +431,6 @@ class ObjectController extends AbstractController
 			$aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal);
 			$aFormData['submit_callback'] = $aCallbackUrls['submit'];
 			$aFormData['cancel_callback'] = $aCallbackUrls['cancel'];
-			//var_dump($aFormData);
 
 			// Preparing renderer
 			// Note : We might need to distinguish form & renderer endpoints
@@ -501,9 +499,9 @@ class ObjectController extends AbstractController
 						// Otherwise, we show the object if there is no default
 						else
 						{
-							$aFormData['validation']['redirection'] = array(
-								'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey()))
-							);
+//							$aFormData['validation']['redirection'] = array(
+//								'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey()))
+//							);
 						}
 					}
 					break;

+ 159 - 12
datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php

@@ -19,12 +19,16 @@
 
 namespace Combodo\iTop\Portal\Controller;
 
+use \Exception;
 use \UserRights;
 use \Silex\Application;
 use \Symfony\Component\HttpFoundation\Request;
 use \Combodo\iTop\Portal\Helper\ApplicationHelper;
 use \Combodo\iTop\Portal\Brick\UserProfileBrick;
 use \Combodo\iTop\Portal\Controller\ObjectController;
+use \Combodo\iTop\Portal\Form\PreferencesFormManager;
+use \Combodo\iTop\Portal\Form\PasswordFormManager;
+use \Combodo\iTop\Renderer\Bootstrap\BsFormRenderer;
 
 class UserProfileBrickController extends BrickController
 {
@@ -55,22 +59,165 @@ class UserProfileBrickController extends BrickController
 		}
 
 		$aData = array();
-
-		// Retrieving current contact
-		$oCurContact = UserRights::GetContactObject();
-		$sCurContactClass = get_class($oCurContact);
-		$sCurContactId = $oCurContact->GetKey();
 		
-		// Preparing contact form
-		$aData['forms']['contact'] = ObjectController::HandleForm($oRequest, $oApp, ObjectController::ENUM_MODE_EDIT, $sCurContactClass, $sCurContactId);
-//		var_dump($aData['forms']['contact']);
-//		die();
+		// If this is ajax call, we are just submiting preferences or password forms
+		if ($oRequest->isXmlHttpRequest())
+		{
+			$aCurrentValues = $oRequest->request->get('current_values');
+			$sFormType = $aCurrentValues['form_type'];
+			if ($sFormType === PreferencesFormManager::FORM_TYPE)
+			{
+				$aData['form'] = $this->HandlePreferencesForm($oRequest, $oApp);
+			}
+			elseif ($sFormType === PasswordFormManager::FORM_TYPE)
+			{
+				$aData['form'] = $this->HandlePasswordForm($oRequest, $oApp);
+			}
+			else
+			{
+				throw new Exception('Unknown form type.');
+			}
+			$oResponse = $oApp->json($aData);
+		}
+		// Else, we are displaying page for first time
+		else
+		{
+			// Retrieving current contact
+			$oCurContact = UserRights::GetContactObject();
+			$sCurContactClass = get_class($oCurContact);
+			$sCurContactId = $oCurContact->GetKey();
+
+			// Preparing forms
+			$aData['forms']['contact'] = ObjectController::HandleForm($oRequest, $oApp, ObjectController::ENUM_MODE_EDIT, $sCurContactClass, $sCurContactId, $oBrick->GetForm());
+			$aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $oApp);
+			// - If user can change password, we display the form
+			$aData['forms']['password'] = (UserRights::CanChangePassword()) ? $this->HandlePasswordForm($oRequest, $oApp) : null;
+
+			$aData = $aData + array(
+				'oBrick' => $oBrick
+			);
 
-		$aData = $aData + array(
-			'oBrick' => $oBrick
+			$oResponse = $oApp['twig']->render($oBrick->GetPageTemplatePath(), $aData);
+		}
+
+		return $oResponse;
+	}
+
+	public function HandlePreferencesForm(Request $oRequest, Application $oApp)
+	{
+		$aFormData = array();
+		$oRequestParams = $oRequest->request;
+
+		// Handling form
+		$sOperation = $oRequestParams->get('operation');
+		// - Create
+		if ($sOperation === null)
+		{
+			// - Creating renderer
+			$oFormRenderer = new BsFormRenderer();
+			$oFormRenderer->SetEndpoint($_SERVER['REQUEST_URI']);
+			// - Creating manager
+			$oFormManager = new PreferencesFormManager();
+			$oFormManager->SetRenderer($oFormRenderer)
+				->Build();
+		}
+		// - Submit
+		else if ($sOperation === 'submit')
+		{
+			$sFormManagerClass = $oRequestParams->get('formmanager_class');
+			$sFormManagerData = $oRequestParams->get('formmanager_data');
+			if ($sFormManagerClass === null || $sFormManagerData === null)
+			{
+				$oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.');
+			}
+
+			// Rebuilding manager from json
+			$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
+			// Applying modification to object
+			$aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values')));
+			// Reloading page only if preferences were changed
+			if (($aFormData['validation']['valid'] === true) && !empty($aFormData['validation']['messages']['success']))
+			{
+				$aFormData['validation']['redirection'] = array(
+					'url' => $oApp['url_generator']->generate('p_user_profile_brick'),
+				);
+			}
+		}
+		else
+		{
+			// Else, submit from another form
+		}
+
+		// Preparing field_set data
+		$aFieldSetData = array(
+			'fields_list' => $oFormManager->GetRenderer()->Render(),
+			'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(),
+			'form_path' => $oFormManager->GetForm()->GetId()
 		);
 
-		return $oApp['twig']->render($oBrick->GetPageTemplatePath(), $aData);
+		// Preparing form data
+		$aFormData['id'] = $oFormManager->GetForm()->GetId();
+		$aFormData['formmanager_class'] = $oFormManager->GetClass();
+		$aFormData['formmanager_data'] = $oFormManager->ToJSON();
+		$aFormData['renderer'] = $oFormManager->GetRenderer();
+		$aFormData['fieldset'] = $aFieldSetData;
+
+		return $aFormData;
+	}
+
+	public function HandlePasswordForm(Request $oRequest, Application $oApp)
+	{
+		$aFormData = array();
+		$oRequestParams = $oRequest->request;
+
+		// Handling form
+		$sOperation = $oRequestParams->get('operation');
+		// - Create
+		if ($sOperation === null)
+		{
+			// - Creating renderer
+			$oFormRenderer = new BsFormRenderer();
+			$oFormRenderer->SetEndpoint($_SERVER['REQUEST_URI']);
+			// - Creating manager
+			$oFormManager = new PasswordFormManager();
+			$oFormManager->SetRenderer($oFormRenderer)
+				->Build();
+		}
+		// - Submit
+		else if ($sOperation === 'submit')
+		{
+			$sFormManagerClass = $oRequestParams->get('formmanager_class');
+			$sFormManagerData = $oRequestParams->get('formmanager_data');
+			if ($sFormManagerClass === null || $sFormManagerData === null)
+			{
+				$oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.');
+			}
+
+			// Rebuilding manager from json
+			$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
+			// Applying modification to object
+			$aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values')));
+		}
+		else
+		{
+			// Else, submit from another form
+		}
+
+		// Preparing field_set data
+		$aFieldSetData = array(
+			'fields_list' => $oFormManager->GetRenderer()->Render(),
+			'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(),
+			'form_path' => $oFormManager->GetForm()->GetId()
+		);
+
+		// Preparing form data
+		$aFormData['id'] = $oFormManager->GetForm()->GetId();
+		$aFormData['formmanager_class'] = $oFormManager->GetClass();
+		$aFormData['formmanager_data'] = $oFormManager->ToJSON();
+		$aFormData['renderer'] = $oFormManager->GetRenderer();
+		$aFormData['fieldset'] = $aFieldSetData;
+
+		return $aFormData;
 	}
 
 }

+ 110 - 0
datamodels/2.x/itop-portal-base/portal/src/entities/userprofilebrick.class.inc.php

@@ -18,6 +18,8 @@
 
 namespace Combodo\iTop\Portal\Brick;
 
+use \DOMFormatException;
+use \Combodo\iTop\DesignElement;
 use \Combodo\iTop\Portal\Brick\PortalBrick;
 
 /**
@@ -34,6 +36,114 @@ class UserProfileBrick extends PortalBrick
 	const DEFAUT_TITLE = 'Brick:Portal:UserProfile:Title';
 
 	static $sRouteName = 'p_user_profile_brick';
+	protected $aForm;
+
+	public function __construct()
+	{
+		parent::__construct();
+
+		$this->aForm = array(
+			'id' => 'default-user-profile',
+			'type' => 'zlist',
+			'fields' => 'details',
+			'layout' => null
+		);
+	}
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetForm()
+	{
+		return $this->aForm;
+	}
+
+	/**
+	 *
+	 * @param array $aForm
+	 * @return \Combodo\iTop\Portal\Brick\UserProfileBrick
+	 */
+	public function SetForm($aForm)
+	{
+		$this->aForm = $aForm;
+		return $this;
+	}
+
+	/**
+	 * Load the brick's data from the xml passed as a ModuleDesignElement.
+	 * This is used to set all the brick attributes at once.
+	 *
+	 * @param \Combodo\iTop\DesignElement $oMDElement
+	 * @return UserProfileBrick
+	 * @throws DOMFormatException
+	 */
+	public function LoadFromXml(DesignElement $oMDElement)
+	{
+		parent::LoadFromXml($oMDElement);
+
+		// Checking specific elements
+		foreach ($oMDElement->GetNodes('./*') as $oBrickSubNode)
+		{
+			switch ($oBrickSubNode->nodeName)
+			{
+				case 'form':
+					// Note : This is inspired by Combodo\iTop\Portal\Helper\ApplicationHelper::LoadFormsConfiguration()
+					// Enumerating fields
+					if ($oBrickSubNode->GetOptionalElement('fields') !== null)
+					{
+						$this->aForm['type'] = 'custom_list';
+						$this->aForm['fields'] = array();
+
+						foreach ($oBrickSubNode->GetOptionalElement('fields')->GetNodes('field') as $oFieldNode)
+						{
+							$sFieldId = $oFieldNode->getAttribute('id');
+							if ($sFieldId !== '')
+							{
+								$aField = array();
+								// Parsing field options like read_only, hidden and mandatory
+								if ($oFieldNode->GetOptionalElement('read_only'))
+								{
+									$aField['readonly'] = ($oFieldNode->GetOptionalElement('read_only')->GetText('true') === 'true') ? true : false;
+								}
+								if ($oFieldNode->GetOptionalElement('mandatory'))
+								{
+									$aField['mandatory'] = ($oFieldNode->GetOptionalElement('mandatory')->GetText('true') === 'true') ? true : false;
+								}
+								if ($oFieldNode->GetOptionalElement('hidden'))
+								{
+									$aField['hidden'] = ($oFieldNode->GetOptionalElement('hidden')->GetText('true') === 'true') ? true : false;
+								}
+
+								$this->aForm['fields'][$sFieldId] = $aField;
+							}
+							else
+							{
+								throw new DOMFormatException('Field tag must have an id attribute', null, null, $oFormNode);
+							}
+						}
+					}
+					// Parsing presentation
+					if ($oBrickSubNode->GetOptionalElement('twig') !== null)
+					{
+						// Extracting the twig template and removing the first and last lines (twig tags)
+						$sXml = $oBrickSubNode->GetOptionalElement('twig')->Dump(true);
+						//$sXml = $oMDElement->saveXML($oBrickSubNode->GetOptionalElement('twig'));
+						$sXml = preg_replace('/^.+\n/', '', $sXml);
+						$sXml = preg_replace('/\n.+$/', '', $sXml);
+
+						$this->aForm['layout'] = array(
+							'type' => (preg_match('/\{\{|\{\#|\{\%/', $sXml) === 1) ? 'twig' : 'xhtml',
+							'content' => $sXml
+						);
+					}
+					break;
+			}
+		}
+
+		return $this;
+	}
+
 }
 
 ?>

+ 33 - 15
datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php

@@ -20,10 +20,10 @@
 namespace Combodo\iTop\Portal\Form;
 
 use \Exception;
-use \DOMFormatException;
 use \Silex\Application;
 use \utils;
 use \Dict;
+use \UserRights;
 use \MetaModel;
 use \CMDBSource;
 use \DBObject;
@@ -31,21 +31,11 @@ use \DBObjectSet;
 use \DBObjectSearch;
 use \DBObjectSetComparator;
 use \InlineImage;
-use \UserRights;
 use \AttributeDateTime;
 use \Combodo\iTop\Form\FormManager;
 use \Combodo\iTop\Form\Form;
 use \Combodo\iTop\Form\Field\FileUploadField;
-use \Combodo\iTop\Form\Field\HiddenField;
 use \Combodo\iTop\Form\Field\LabelField;
-use \Combodo\iTop\Form\Field\StringField;
-use \Combodo\iTop\Form\Field\TextAreaField;
-use \Combodo\iTop\Form\Field\SelectField;
-use \Combodo\iTop\Form\Field\RadioField;
-use \Combodo\iTop\Form\Field\CheckboxField;
-use \Combodo\iTop\Form\Validator\IntegerValidator;
-use \Combodo\iTop\Form\Validator\NotEmptyValidator;
-use \Combodo\iTop\Renderer\Bootstrap\BsFormRenderer;
 
 /**
  * Description of objectformmanager
@@ -110,6 +100,12 @@ class ObjectFormManager extends FormManager
 			$oFormManager->SetActionRulesToken($aJson['formactionrulestoken']);
 		}
 
+		// Retrieving form properties
+		if (isset($aJson['formproperties']))
+		{
+			$oFormManager->SetFormProperties($aJson['formproperties']);
+		}
+
 		// Retrieving callback urls
 		if (!isset($aJson['formcallbacks']))
 		{
@@ -215,6 +211,10 @@ class ObjectFormManager extends FormManager
 	 */
 	public function SetFormProperties($aFormProperties)
 	{
+//		echo '<pre>';
+//		print_r($aFormProperties);
+//		echo '</pre>';
+//		die();
 		$this->aFormProperties = $aFormProperties;
 		return $this;
 	}
@@ -256,6 +256,7 @@ class ObjectFormManager extends FormManager
 			$aJson['formobject_id'] = $this->oObject->GetKey();
 		$aJson['formmode'] = $this->sMode;
 		$aJson['formactionrulestoken'] = $this->sActionRulesToken;
+		$aJson['formproperties'] = $this->aFormProperties;
 
 		return $aJson;
 	}
@@ -433,7 +434,7 @@ class ObjectFormManager extends FormManager
 			$oAttDef = MetaModel::GetAttributeDef(get_class($this->oObject), $sAttCode);
 			
 			// TODO : Make AttributeDefinition::MakeFormField() for all kind of fields
-			if (in_array(get_class($oAttDef), array('AttributeString', 'AttributeText', 'AttributeLongText', 'AttributeCaseLog', 'AttributeHTML', 'AttributeFriendlyName', 'AttributeEnum', 'AttributeExternalKey', 'AttributeCustomFields', 'AttributeLinkedSet', 'AttributeLinkedSetIndirect', 'AttributeDate', 'AttributeDateTime')))
+			if (in_array(get_class($oAttDef), array('AttributeString', 'AttributeEmailAddress', 'AttributeText', 'AttributeLongText', 'AttributeCaseLog', 'AttributeHTML', 'AttributeFriendlyName', 'AttributeEnum', 'AttributeExternalKey', 'AttributeCustomFields', 'AttributeLinkedSet', 'AttributeLinkedSetIndirect', 'AttributeDate', 'AttributeDateTime')))
 			{
 				$oField = $oAttDef->MakeFormField($this->oObject);
 				
@@ -631,10 +632,20 @@ class ObjectFormManager extends FormManager
 			// The try catch is essentially to start a MySQL transaction in order to ensure that all or none objects are persisted when creating an object with links
 			try
 			{
+				$sObjectClass = get_class($this->oObject);
+
 				// Starting transaction
 				CMDBSource::Query('START TRANSACTION');
+				// Forcing allowed writing on the object if necessary. This is used in some particular cases.
+				$bAllowWrite = ($sObjectClass === 'Person' && $this->oObject->GetKey() == UserRights::GetContactId());
+				if ($bAllowWrite)
+				{
+					$this->oObject->AllowWrite(true);
+				}
+
 				// Writing object to DB
 				$bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified());
+				$bWasModified = $this->oObject->IsModified();
 				$this->oObject->DBWrite();
 				// Finalizing images link to object, otherwise it will be cleaned by the GC
 				InlineImage::FinalizeInlineImages($this->oObject);
@@ -655,7 +666,7 @@ class ObjectFormManager extends FormManager
 					$sTriggersQuery = $this->oApp['combodo.portal.instance.conf']['properties']['triggers_query'];
 					if ($sTriggersQuery !== null)
 					{
-						$aParentClasses = MetaModel::EnumParentClasses(get_class($this->oObject), ENUM_PARENT_CLASSES_ALL);
+						$aParentClasses = MetaModel::EnumParentClasses($sObjectClass, ENUM_PARENT_CLASSES_ALL);
 						$oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL($sTriggersQuery), array(), array('parent_classes' => $aParentClasses));
 						while ($oTrigger = $oTriggerSet->Fetch())
 						{
@@ -668,7 +679,10 @@ class ObjectFormManager extends FormManager
 				// Ending transaction with a commit as everything was fine
 				CMDBSource::Query('COMMIT');
 
-				$aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved')));
+				if ($bWasModified)
+				{
+					$aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved')));
+				}
 			}
 			catch (Exception $e)
 			{
@@ -810,7 +824,11 @@ class ObjectFormManager extends FormManager
 			}
 		}
 		// Then we build and update form
-		$this->SetFormProperties($aFormProperties);
+		// - We update form properties only we don't have any yet. This is a fallback for cases when form properties where not among the JSON data
+		if ($this->GetFormProperties() === null)
+		{
+			$this->SetFormProperties($aFormProperties);
+		}
 		$this->Build();
 	}
 

+ 179 - 0
datamodels/2.x/itop-portal-base/portal/src/forms/passwordformmanager.class.inc.php

@@ -0,0 +1,179 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>
+
+namespace Combodo\iTop\Portal\Form;
+
+use \Exception;
+use \CMDBSource;
+use \Dict;
+use \UserRights;
+use \Combodo\iTop\Form\FormManager;
+use \Combodo\iTop\Form\Form;
+use \Combodo\iTop\Form\Field\HiddenField;
+use \Combodo\iTop\Form\Field\PasswordField;
+
+/**
+ * Description of passwordformmanager
+ *
+ * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
+ */
+class PasswordFormManager extends FormManager
+{
+	const FORM_TYPE = 'change_password';
+
+	public function Build()
+	{
+		// Building the form
+		$oForm = new Form('change_password');
+
+		// Adding hidden field with form type
+		$oField = new HiddenField('form_type');
+		$oField->SetCurrentValue('change_password');
+		$oForm->AddField($oField);
+
+		// Adding old password field
+		$oField = new PasswordField('old_password');
+		$oField->SetMandatory(true)
+			->SetLabel(Dict::S('UI:Login:OldPasswordPrompt'));
+		$oForm->AddField($oField);
+		// Adding new password field
+		$oField = new PasswordField('new_password');
+		$oField->SetMandatory(true)
+			->SetLabel(Dict::S('Brick:Portal:UserProfile:Password:ChoosePassword'));
+		$oForm->AddField($oField);
+		// Adding confirm password field
+		$oField = new PasswordField('confirm_password');
+		$oField->SetMandatory(true)
+			->SetLabel(Dict::S('Brick:Portal:UserProfile:Password:ConfirmPassword'));
+		$oForm->AddField($oField);
+
+		$oForm->Finalize();
+		$this->oForm = $oForm;
+		$this->oRenderer->SetForm($this->oForm);
+	}
+
+	/**
+	 * Validates the form and returns an array with the validation status and the messages.
+	 * If the form is valid, creates/updates the object.
+	 *
+	 * eg :
+	 *  array(
+	 * 	  'status' => true|false
+	 * 	  'messages' => array(
+	 * 		  'errors' => array()
+	 * 	)
+	 *
+	 * @param array $aArgs
+	 * @return array
+	 */
+	public function OnSubmit($aArgs = null)
+	{
+		$aData = array(
+			'valid' => true,
+			'messages' => array(
+				'success' => array(),
+				'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this.
+				'error' => array()
+			)
+		);
+
+		// Update object and form
+		$this->OnUpdate($aArgs);
+
+		// Check if form valid
+		if ($this->oForm->Validate())
+		{
+			// The try catch is essentially to start a MySQL transaction
+			try
+			{
+				// Updating password
+				$sAuthUser = $_SESSION['auth_user'];
+				$sOldPassword = $this->oForm->GetField('old_password')->GetCurrentValue();
+				$sNewPassword = $this->oForm->GetField('new_password')->GetCurrentValue();
+				$sConfirmPassword = $this->oForm->GetField('confirm_password')->GetCurrentValue();
+				
+				if ($sOldPassword !== '' && $sNewPassword !== '' && $sConfirmPassword !== '')
+				{
+					if (!UserRights::CanChangePassword())
+					{
+						$aData['valid'] = false;
+						$aData['messages']['error'] += array('_main' => array(Dict::S('Brick:Portal:UserProfile:Password:CantChangeContactAdministrator')));
+					}
+					else if (!UserRights::CheckCredentials($sAuthUser, $sOldPassword))
+					{
+						$aData['valid'] = false;
+						$aData['messages']['error'] += array('old_password' => array(Dict::S('UI:Login:IncorrectOldPassword')));
+					}
+					else if ($sNewPassword !== $sConfirmPassword)
+					{
+						$aData['valid'] = false;
+						$aData['messages']['error'] += array('confirm_password' => array(Dict::S('UI:Login:RetypePwdDoesNotMatch')));
+					}
+					else if (!UserRights::ChangePassword($sOldPassword, $sNewPassword))
+					{
+						$aData['valid'] = false;
+						$aData['messages']['error'] += array('confirm_password' => array(Dict::S('Brick:Portal:UserProfile:Password:CantChangeForUnknownReason')));
+					}
+					else
+					{
+						$aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved')));
+					}
+				}
+			}
+			catch (Exception $e)
+			{
+				$aData['valid'] = false;
+				$aData['messages']['error'] += array('_main' => array($e->getMessage()));
+			}
+		}
+		else
+		{
+			// Handle errors
+			$aData['valid'] = false;
+			$aData['messages']['error'] += $this->oForm->GetErrorMessages();
+		}
+		
+		return $aData;
+	}
+
+	public function OnUpdate($aArgs = null)
+	{
+
+		// We build the form
+		$this->Build();
+
+		// Then we update it with new values
+		if (is_array($aArgs))
+		{
+			if (isset($aArgs['currentValues']))
+			{
+				foreach ($aArgs['currentValues'] as $sPreferenceName => $value)
+				{
+					$this->oForm->GetField($sPreferenceName)->SetCurrentValue($value);
+				}
+			}
+		}
+	}
+
+	public function OnCancel($aArgs = null)
+	{
+		
+	}
+
+}

+ 169 - 0
datamodels/2.x/itop-portal-base/portal/src/forms/preferencesformmanager.class.inc.php

@@ -0,0 +1,169 @@
+<?php
+
+// 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 <http://www.gnu.org/licenses/>
+
+namespace Combodo\iTop\Portal\Form;
+
+use \Exception;
+use \CMDBSource;
+use \Dict;
+use \UserRights;
+use \Combodo\iTop\Form\FormManager;
+use \Combodo\iTop\Form\Form;
+use \Combodo\iTop\Form\Field\HiddenField;
+use \Combodo\iTop\Form\Field\SelectField;
+
+/**
+ * Description of preferencesformmanager
+ *
+ * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
+ */
+class PreferencesFormManager extends FormManager
+{
+	const FORM_TYPE = 'preferences';
+
+	public function Build()
+	{
+		// Building the form
+		$oForm = new Form('preferences');
+
+		// Adding hidden field with form type
+		$oField = new HiddenField('form_type');
+		$oField->SetCurrentValue('preferences');
+		$oForm->AddField($oField);
+
+		// Adding language field
+		$oField = new SelectField('language');
+		$oField->SetMandatory(true)
+			->SetLabel(Dict::S('UI:Favorites:SelectYourLanguage'))
+			->SetCurrentValue(Dict::GetUserLanguage())
+			->SetStartsWithNullChoice(false);
+		// - Preparing choices
+		foreach (Dict::GetLanguages() as $sCode => $aLanguage)
+		{
+			$oField->AddChoice($sCode, $aLanguage['description'] . ' (' . $aLanguage['localized_description'] . ')');
+		}
+		// - Adding to form
+		$oForm->AddField($oField);
+
+		$oForm->Finalize();
+		$this->oForm = $oForm;
+		$this->oRenderer->SetForm($this->oForm);
+	}
+
+	/**
+	 * Validates the form and returns an array with the validation status and the messages.
+	 * If the form is valid, creates/updates the object.
+	 *
+	 * eg :
+	 *  array(
+	 * 	  'status' => true|false
+	 * 	  'messages' => array(
+	 * 		  'errors' => array()
+	 * 	)
+	 *
+	 * @param array $aArgs
+	 * @return array
+	 */
+	public function OnSubmit($aArgs = null)
+	{
+		$aData = array(
+			'valid' => true,
+			'messages' => array(
+				'success' => array(),
+				'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this.
+				'error' => array()
+			)
+		);
+
+		// Update object and form
+		$this->OnUpdate($aArgs);
+
+		// Check if form valid
+		if ($this->oForm->Validate())
+		{
+			// The try catch is essentially to start a MySQL transaction
+			try
+			{
+				// Starting transaction
+				CMDBSource::Query('START TRANSACTION');
+				$iFieldChanged = 0;
+
+				// Updating user
+				$oCurUser = UserRights::GetUserObject();
+				// - Language
+				$sLanguage = $this->oForm->GetField('language')->GetCurrentValue();
+				if (($sLanguage !== null) && ($oCurUser->Get('language') !== $sLanguage))
+				{
+					$oCurUser->Set('language', $sLanguage);
+					$iFieldChanged++;
+				}
+				
+				// Updating only if preferences changed
+				if ($iFieldChanged > 0)
+				{
+					$oCurUser->DBUpdate();
+					$aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved')));
+				}
+
+				// Ending transaction with a commit as everything was fine
+				CMDBSource::Query('COMMIT');
+			}
+			catch (Exception $e)
+			{
+				// End transaction with a rollback as something failed
+				CMDBSource::Query('ROLLBACK');
+				$aData['valid'] = false;
+				$aData['messages']['error'] += array('_main' => array($e->getMessage()));
+			}
+		}
+		else
+		{
+			// Handle errors
+			$aData['valid'] = false;
+			$aData['messages']['error'] += $this->oForm->GetErrorMessages();
+		}
+		
+		return $aData;
+	}
+
+	public function OnUpdate($aArgs = null)
+	{
+
+		// We build the form
+		$this->Build();
+
+		// Then we update it with new values
+		if (is_array($aArgs))
+		{
+			if (isset($aArgs['currentValues']))
+			{
+				foreach ($aArgs['currentValues'] as $sPreferenceName => $value)
+				{
+					$this->oForm->GetField($sPreferenceName)->SetCurrentValue($value);
+				}
+			}
+		}
+	}
+
+	public function OnCancel($aArgs = null)
+	{
+		
+	}
+
+}

+ 18 - 1
datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php

@@ -123,6 +123,23 @@ class ApplicationHelper
 	}
 
 	/**
+	 * Loads form managers from the base portal
+	 *
+	 * @param string $sScannedDir Directory to load the managers from
+	 * @throws \Exception
+	 */
+	static function LoadFormManagers($sScannedDir = null)
+	{
+		if ($sScannedDir === null)
+		{
+			$sScannedDir = __DIR__ . '/../forms';
+		}
+
+		// Loading form managers from base portal (those from modules have already been loaded by module.xxx.php files)
+		self::LoadClasses($sScannedDir, 'formmanager.class.inc.php', 'brick');
+	}
+
+	/**
 	 * Registers routes in the Silex Application from all declared Router classes
 	 *
 	 * @param \Silex\Application $oApp
@@ -581,7 +598,7 @@ class ApplicationHelper
 //						$oApp['combodo.portal.instance.routes'] = $aRoutes;
 //					}
 					// Checking brick security
-					if ($oBrick->IsGrantedForProfiles(UserRights::ListProfiles()))
+					if ($oBrick->GetActive() && $oBrick->IsGrantedForProfiles(UserRights::ListProfiles()))
 					{
 						$aPortalConf['bricks'][] = $oBrick;
 						$aPortalConf['bricks_total_width'] += $oBrick->GetWidth();

+ 113 - 35
datamodels/2.x/itop-portal-base/portal/src/views/bricks/user-profile/layout.html.twig

@@ -2,13 +2,16 @@
 {# Browse brick base layout #}
 {% extends 'itop-portal-base/portal/src/views/bricks/layout.html.twig' %}
 
+{% set oContactForm = forms.contact %}
+{% set oPreferencesForm = forms.preferences %}
+{% set oPasswordForm = forms.password %}
 
 {% block pMainHeaderTitle %}
 	{{ oBrick.GetTitle()|dict_s }}
 {% endblock %}
 
 {% block pMainContentHolder%}
-	<form class="">
+	<div id="user-profile-wrapper">
 		<div class="row">
 			<div class="col-sm-6">
 				<div class="panel panel-default">
@@ -16,30 +19,17 @@
 						<h3 class="panel-title">{{ 'Brick:Portal:UserProfile:PersonalInformations:Title'|dict_s }}</h3>
 					</div>
 					<div class="panel-body">
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Nom</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="Lajarige" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Prénom</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="Guillaume" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Email</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="guillaume.lajarige@combodo.com" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Téléphone</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="0625540067" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Organisation</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="Combodo" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Site</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="Siège" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Fonction</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="Ingénieur R&D" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">Manager</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="John Doe" class="form-control" maxlength="255" /></div>
-						</div>
+						<form id="{{ oContactForm.id }}" class="" method="POST" action="{{ oContactForm.renderer.GetEndpoint()|raw }}">
+							<input type="hidden" name="transaction_id" value="{{ oContactForm.transaction_id }}" />
+							<div class="form_alerts">
+								<div class="alert alert-success" role="alert" style="display: none;"></div>
+								<div class="alert alert-warning" role="alert" style="display: none;"></div>
+								<div class="alert alert-error alert-danger" role="alert" style="display: none;"></div>
+							</div>
+							<div class="form_fields">
+								{{ oContactForm.renderer.GetBaseLayout()|raw }}
+							</div>
+						</form>
 					</div>
 				</div>
 			</div>
@@ -60,9 +50,16 @@
 						<h3 class="panel-title">{{ 'Class:appUserPreferences/Attribute:preferences'|dict_s }}</h3>
 					</div>
 					<div class="panel-body">
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">{{ 'UI:FavoriteLanguage'|dict_s }}</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="Français" class="form-control" maxlength="255" /></div>
-						</div>
+						<form id="{{ oPreferencesForm.id }}" class="" method="POST" action="{{ oPreferencesForm.renderer.GetEndpoint()|raw }}">
+							<div class="form_alerts">
+								<div class="alert alert-success" role="alert" style="display: none;"></div>
+								<div class="alert alert-warning" role="alert" style="display: none;"></div>
+								<div class="alert alert-error alert-danger" role="alert" style="display: none;"></div>
+							</div>
+							<div class="form_fields">
+								{{ oPreferencesForm.renderer.GetBaseLayout()|raw }}
+							</div>
+						</form>
 					</div>
 				</div>
 				<div class="panel panel-default">
@@ -70,15 +67,96 @@
 						<h3 class="panel-title">{{ 'Brick:Portal:UserProfile:Password:Title'|dict_s }}</h3>
 					</div>
 					<div class="panel-body">
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">{{ 'Brick:Portal:UserProfile:Password:ChoosePassword'|dict_s }}</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="" class="form-control" maxlength="255" /></div>
-						</div>
-						<div data-field-id="reason" data-form-path="aa" class="portal_form_field form_field">
-							<div class="form-group form_mandatory"><label for="field_reason_572c5888e6378" class="control-label">{{ 'Brick:Portal:UserProfile:Password:ConfirmPassword'|dict_s }}</label><input type="text" id="field_reason_572c5888e6378" name="reason" value="" class="form-control" maxlength="255" /></div>
-						</div>
+						{% if oPasswordForm is not null %}
+							<form id="{{ oPasswordForm.id }}" class="" method="POST" action="{{ oPasswordForm.renderer.GetEndpoint()|raw }}">
+								<div class="form_alerts">
+									<div class="alert alert-success" role="alert" style="display: none;"></div>
+									<div class="alert alert-warning" role="alert" style="display: none;"></div>
+									<div class="alert alert-error alert-danger" role="alert" style="display: none;"></div>
+								</div>
+								<div class="form_fields">
+									{{ oPasswordForm.renderer.GetBaseLayout()|raw }}
+								</div>
+							</form>
+						{% else %}
+							pas le droit
+						{% endif %}
 					</div>
 				</div>
 			</div>
+		</div>						
+		<div class="form_buttons">
+			<div class="form_btn_regular">
+				<input class="btn btn-primary form_btn_submit" type="submit" value="{{ 'Portal:Button:Submit'|dict_s }}">
+			</div>
 		</div>
-	</form>
+	</div>
+{% endblock %}
+
+{% block pPageReadyScripts %}
+	{{ parent() }}
+	
+	// Personal informations form
+	var oContactFormFieldSet = $('#{{ oContactForm.id }} > .form_fields').field_set({{ oContactForm.fieldset|json_encode()|raw }});
+	$('#{{ oContactForm.id }}').portal_form_handler({
+		formmanager_class: "{{ oContactForm.formmanager_class|escape('js') }}",
+		formmanager_data: {{ oContactForm.formmanager_data|json_encode()|raw }},
+		field_set: oContactFormFieldSet,
+		endpoint: "{{ oContactForm.renderer.GetEndpoint()|raw }}"
+	});
+	
+	// Preferences form
+	var oPreferencesFormFieldSet = $('#{{ oPreferencesForm.id }} > .form_fields').field_set({{ oPreferencesForm.fieldset|json_encode()|raw }});
+	$('#{{ oPreferencesForm.id }}').portal_form_handler({
+		formmanager_class: "{{ oPreferencesForm.formmanager_class|escape('js') }}",
+		formmanager_data: {{ oPreferencesForm.formmanager_data|json_encode()|raw }},
+		field_set: oPreferencesFormFieldSet,
+		endpoint: "{{ oPreferencesForm.renderer.GetEndpoint()|raw }}"
+	});
+	
+	{% if oPasswordForm is not null %}
+		// Password form
+		var oPasswordFormFieldSet = $('#{{ oPasswordForm.id }} > .form_fields').field_set({{ oPasswordForm.fieldset|json_encode()|raw }});
+		$('#{{ oPasswordForm.id }}').portal_form_handler({
+			formmanager_class: "{{ oPasswordForm.formmanager_class|escape('js') }}",
+			formmanager_data: {{ oPasswordForm.formmanager_data|json_encode()|raw }},
+			field_set: oPasswordFormFieldSet,
+			endpoint: "{{ oPasswordForm.renderer.GetEndpoint()|raw }}"
+		});
+	{% endif %}
+	
+	// Submit button
+	$('#user-profile-wrapper .form_buttons .form_btn_submit').off('click').on('click', function(oEvent){
+		oEvent.preventDefault();
+		
+		// Resetting feedback
+		$('#user-profile-wrapper .form_alerts .alert').hide();
+		$('#user-profile-wrapper .form_alerts .alert > p').remove();
+		$('#user-profile-wrapper .form_field').removeClass('has-error');
+		$('#user-profile-wrapper .form_field .help-block > p').remove();
+		
+		// Submiting contact form through AJAX
+		//if($('#{{ oContactForm.id }} .field_set').field_set('hasTouchedFields'))
+		//{
+			$('#{{ oContactForm.id }}').portal_form_handler('submit', oEvent);
+		//}
+		
+		// Submiting preferences form through AJAX
+		//if($('#{{ oPreferencesForm.id }} .field_set').field_set('hasTouchedFields'))
+		//{
+			$('#{{ oPreferencesForm.id }}').portal_form_handler('submit', oEvent);
+		//}
+		
+		{% if oPasswordForm is not null %}
+			// Submiting password form through AJAX
+			// Only if fields are filled
+			$('#{{ oPasswordForm.id }} :password').each(function(iIndex, oElem){
+				if($(oElem).val() !== '')
+				{
+					$('#{{ oPasswordForm.id }}').portal_form_handler('submit', oEvent);
+					return false;
+				}
+			});
+		{% endif %}
+	});
 {% endblock %}

+ 5 - 4
datamodels/2.x/itop-portal-base/portal/web/index.php

@@ -39,15 +39,15 @@ require_once __DIR__ . '/../src/providers/scopevalidatorserviceprovider.class.in
 require_once __DIR__ . '/../src/helpers/scopevalidatorhelper.class.inc.php';
 require_once __DIR__ . '/../src/helpers/securityhelper.class.inc.php';
 require_once __DIR__ . '/../src/helpers/applicationhelper.class.inc.php';
-// Forms
-require_once __DIR__ . '/../src/forms/objectformmanager.class.inc.php';
 
-use \Exception;
-use \Symfony\Component\HttpFoundation\Response;
 use \Combodo\iTop\Portal\Helper\ApplicationHelper;
 
 // Checking user rights and prompt if needed
 LoginWebPage::DoLoginEx(PORTAL_ID);
+if (UserRights::GetContactId() == 0)
+{
+	die(Dict::S('Portal:ErrorNoContactForThisUser'));
+}
 
 // Initializing Silex framework
 $oApp = new Silex\Application();
@@ -81,6 +81,7 @@ ApplicationHelper::LoadControllers();
 ApplicationHelper::LoadRouters();
 ApplicationHelper::RegisterRoutes($oApp);
 ApplicationHelper::LoadBricks();
+ApplicationHelper::LoadFormManagers();
 ApplicationHelper::RegisterTwigExtensions($oApp);
 
 // Loading portal configuration from the module design

+ 5 - 1
datamodels/2.x/itop-portal-base/portal/web/js/portal_form_handler.js

@@ -81,7 +81,7 @@ $(function()
 							var oValidation = oData.form.validation;
 							
 							// First we build the form
-							me.options.field_set.field_set('option', 'fields_list', oData.form.fields_list);
+							me.options.field_set.field_set('option', 'fields_list', oData.form.fieldset.fields_list);
 							me.options.field_set.field_set('option', 'is_valid', oValidation.valid);
 							me.options.field_set.field_set('buildForm');
 
@@ -299,6 +299,10 @@ $(function()
 		{
 			$('#page_overlay').fadeOut(200);
 		},
+		submit: function(oEvent)
+		{
+			this._onSubmitClick(oEvent);
+		},
 		getAttachmentIds: function()
 		{
 			var me = this;

+ 10 - 6
js/field_set.js

@@ -231,11 +231,6 @@ $(function()
 			}
 			return this.options.is_valid;
 		},
-		// Debug helper
-		showOptions: function()
-		{
-			return this.options;
-		},
 		_loadCssFile: function(url)
 		{
 			if (!$('link[href="' + url + '"]').length)
@@ -322,6 +317,15 @@ $(function()
 			this.options.style_element.text(this.buildData.style_code);
 			
 			eval(this.options.script_element.text());
-		}
+		},
+		hasTouchedFields: function()
+		{
+			return (this.options.touched_fields.length > 0);
+		},
+		// Debug helper
+		showOptions: function()
+		{
+			return this.options;
+		},
 	});
 });

+ 1 - 1
js/form_field.js

@@ -78,7 +78,7 @@ $(function()
 			var value = null;
 			
 			this.element.find(':input').each(function(iIndex, oElem){
-				if($(oElem).is(':hidden') || $(oElem).is(':text') || $(oElem).is('textarea'))
+				if($(oElem).is(':hidden') || $(oElem).is(':text') || $(oElem).is(':password') || $(oElem).is('textarea'))
 				{
 					value = $(oElem).val();
 				}