Browse Source

User portal: enable the creation of Incident tickets (ITIL + requires a change in the configuration file -see the readme file)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2959 a333f486-631f-4898-b8df-5754b55c2be0
romainq 11 years ago
parent
commit
fd76e8223a

+ 9 - 0
core/config.class.inc.php

@@ -667,6 +667,15 @@ class Config
 			'source_of_value' => '',
 			'show_in_conf_sample' => false,
 		),
+		'portal_tickets' => array(
+			'type' => 'string',
+			'description' => 'CSV list of classes supported in the portal',
+			// examples... not used
+			'default' => 'UserRequest',
+			'value' => 'UserRequest',
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),
 	);
 
 	public function IsProperty($sPropCode)

+ 8 - 7
datamodels/1.x/itop-request-mgmt-1.0.0/datamodel.itop-request-mgmt.xml

@@ -7,15 +7,16 @@
     <constant id="PORTAL_VALIDATE_SERVICECATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT Service AS s JOIN SLA AS sla ON sla.service_id=s.id JOIN lnkContractToSLA AS ln ON ln.sla_id=sla.id JOIN CustomerContract AS cc ON ln.contract_id=cc.id WHERE cc.org_id = :org_id AND s.id = :id]]></constant>
     <constant id="PORTAL_VALIDATE_SERVICESUBCATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT ServiceSubcategory AS Sub JOIN Service AS Svc ON Sub.service_id = Svc.id WHERE Sub.id=:id]]></constant>
     <constant id="PORTAL_ALL_PARAMS" xsi:type="string" _delta="define"><![CDATA[from_service_id,org_id,caller_id,service_id,servicesubcategory_id,title,description,impact,urgency,workgroup_id,moreinfo,caller_id,start_date,end_date,duration,impact_duration]]></constant>
-    <constant id="PORTAL_ATTCODE_LOG" xsi:type="string" _delta="define"><![CDATA[ticket_log]]></constant>
-    <constant id="PORTAL_ATTCODE_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_commment]]></constant>
-    <constant id="PORTAL_REQUEST_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency,workgroup_id,ticket_log]]></constant>
-    <constant id="PORTAL_ATTCODE_TYPE" xsi:type="string" _delta="define"><![CDATA[]]></constant>
     <constant id="PORTAL_SET_TYPE_FROM" xsi:type="string" _delta="define"><![CDATA[]]></constant>
-    <constant id="PORTAL_TICKETS_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
+    <constant id="PORTAL_TYPE_TO_CLASS" xsi:type="string" _delta="define"><![CDATA[]]></constant>
+    <constant id="PORTAL_USERREQUEST_PUBLIC_LOG" xsi:type="string" _delta="define"><![CDATA[ticket_log]]></constant>
+    <constant id="PORTAL_USERREQUEST_USER_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_commment]]></constant>
+    <constant id="PORTAL_USERREQUEST_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency,workgroup_id,ticket_log]]></constant>
+    <constant id="PORTAL_USERREQUEST_TYPE" xsi:type="string" _delta="define"><![CDATA[]]></constant>
+    <constant id="PORTAL_USERREQUEST_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
     <constant id="PORTAL_TICKETS_SEARCH_CRITERIA" xsi:type="string" _delta="define"><![CDATA[ref,start_date,close_date,service_id,caller_id]]></constant>
-    <constant id="PORTAL_TICKETS_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
-    <constant id="PORTAL_TICKET_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
+    <constant id="PORTAL_USERREQUEST_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
+    <constant id="PORTAL_USERREQUEST_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
   </constants>
   <classes>
     <class id="UserRequest" _delta="define">

+ 6 - 6
datamodels/1.x/itop-welcome-itil/main.itop-welcome-itil.php

@@ -71,16 +71,16 @@ class MyPortalURLMaker implements iDBObjectURLMaker
 {
 	public static function MakeObjectURL($sClass, $iId)
 	{
-		switch($sClass)
+		if (strpos(MetaModel::GetConfig()->Get('portal_tickets'), $sClass) !== false)
 		{
-		case 'UserRequest':
 			$sAbsoluteUrl = utils::GetAbsoluteUrlAppRoot();
 			$sUrl = "{$sAbsoluteUrl}portal/index.php?operation=details&class=$sClass&id=$iId";
-			return $sUrl;
-
-		default:
-			return '';
 		}
+		else
+		{
+			$sUrl = '';
+		}
+		return $sUrl;
 	}
 }
 

+ 17 - 9
datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml

@@ -3,19 +3,27 @@
   <constants>
     <constant id="PORTAL_POWER_USER_PROFILE" xsi:type="string" _delta="define"><![CDATA[Portal power user]]></constant>
     <constant id="PORTAL_SERVICECATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT Service AS s JOIN lnkCustomerContractToService AS l1 ON l1.service_id=s.id JOIN CustomerContract AS cc ON l1.customercontract_id=cc.id WHERE cc.org_id = :org_id AND s.status != 'obsolete']]></constant>
-    <constant id="PORTAL_SERVICE_SUBCATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT ServiceSubcategory WHERE service_id = :svc_id AND request_type="service_request" AND ServiceSubcategory.status != "obsolete"]]></constant>
+    <constant id="PORTAL_SERVICE_SUBCATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT ServiceSubcategory WHERE service_id = :svc_id AND ServiceSubcategory.status != "obsolete"]]></constant>
     <constant id="PORTAL_VALIDATE_SERVICECATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT Service AS s JOIN lnkCustomerContractToService AS l1 ON l1.service_id=s.id JOIN CustomerContract AS cc ON l1.customercontract_id=cc.id WHERE cc.org_id = :org_id AND s.id = :id AND s.status != 'obsolete']]></constant>
     <constant id="PORTAL_VALIDATE_SERVICESUBCATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT ServiceSubcategory AS Sub JOIN Service AS Svc ON Sub.service_id = Svc.id WHERE Sub.id=:id AND Sub.status != 'obsolete']]></constant>
     <constant id="PORTAL_ALL_PARAMS" xsi:type="string" _delta="define"><![CDATA[from_service_id,org_id,caller_id,service_id,servicesubcategory_id,title,description,impact,emergency,moreinfo,caller_id,start_date,end_date,duration,impact_duration]]></constant>
-    <constant id="PORTAL_ATTCODE_LOG" xsi:type="string" _delta="define"><![CDATA[public_log]]></constant>
-    <constant id="PORTAL_ATTCODE_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_comment]]></constant>
-    <constant id="PORTAL_REQUEST_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency]]></constant>
-    <constant id="PORTAL_ATTCODE_TYPE" xsi:type="string" _delta="define"><![CDATA[]]></constant>
-    <constant id="PORTAL_SET_TYPE_FROM" xsi:type="string" _delta="define"><![CDATA[]]></constant>
-    <constant id="PORTAL_TICKETS_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
+    <constant id="PORTAL_SET_TYPE_FROM" xsi:type="string" _delta="define"><![CDATA[request_type]]></constant>
+    <constant id="PORTAL_TYPE_TO_CLASS" xsi:type="string" _delta="define"><![CDATA[{"service_request":"UserRequest","incident":"Incident"}]]></constant>
     <constant id="PORTAL_TICKETS_SEARCH_CRITERIA" xsi:type="string" _delta="define"><![CDATA[ref,start_date,close_date,service_id,caller_id]]></constant>
-    <constant id="PORTAL_TICKETS_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
-    <constant id="PORTAL_TICKET_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
+    <constant id="PORTAL_USERREQUEST_PUBLIC_LOG" xsi:type="string" _delta="define"><![CDATA[public_log]]></constant>
+    <constant id="PORTAL_USERREQUEST_USER_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_comment]]></constant>
+    <constant id="PORTAL_USERREQUEST_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency]]></constant>
+    <constant id="PORTAL_USERREQUEST_TYPE" xsi:type="string" _delta="define"><![CDATA[]]></constant>
+    <constant id="PORTAL_USERREQUEST_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
+    <constant id="PORTAL_USERREQUEST_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
+    <constant id="PORTAL_USERREQUEST_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
+    <constant id="PORTAL_INCIDENT_PUBLIC_LOG" xsi:type="string" _delta="define"><![CDATA[public_log]]></constant>
+    <constant id="PORTAL_INCIDENT_USER_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_comment]]></constant>
+    <constant id="PORTAL_INCIDENT_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency]]></constant>
+    <constant id="PORTAL_INCIDENT_TYPE" xsi:type="string" _delta="define"><![CDATA[]]></constant>
+    <constant id="PORTAL_INCIDENT_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
+    <constant id="PORTAL_INCIDENT_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
+    <constant id="PORTAL_INCIDENT_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
   </constants>
   <classes>
     <class id="UserRequest" _delta="define">

+ 8 - 7
datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml

@@ -7,15 +7,16 @@
     <constant id="PORTAL_VALIDATE_SERVICECATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT Service AS s JOIN lnkCustomerContractToService AS l1 ON l1.service_id=s.id JOIN CustomerContract AS cc ON l1.customercontract_id=cc.id WHERE cc.org_id = :org_id AND s.id = :id AND s.status != 'obsolete']]></constant>
     <constant id="PORTAL_VALIDATE_SERVICESUBCATEGORY_QUERY" xsi:type="string" _delta="define"><![CDATA[SELECT ServiceSubcategory AS Sub JOIN Service AS Svc ON Sub.service_id = Svc.id WHERE Sub.id=:id AND Sub.status != 'obsolete']]></constant>
     <constant id="PORTAL_ALL_PARAMS" xsi:type="string" _delta="define"><![CDATA[from_service_id,org_id,caller_id,service_id,servicesubcategory_id,title,description,impact,emergency,moreinfo,caller_id,start_date,end_date,duration,impact_duration]]></constant>
-    <constant id="PORTAL_ATTCODE_LOG" xsi:type="string" _delta="define"><![CDATA[public_log]]></constant>
-    <constant id="PORTAL_ATTCODE_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_comment]]></constant>
-    <constant id="PORTAL_REQUEST_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency,public_log]]></constant>
-    <constant id="PORTAL_ATTCODE_TYPE" xsi:type="string" _delta="define"><![CDATA[request_type]]></constant>
     <constant id="PORTAL_SET_TYPE_FROM" xsi:type="string" _delta="define"><![CDATA[request_type]]></constant>
-    <constant id="PORTAL_TICKETS_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
+    <constant id="PORTAL_TYPE_TO_CLASS" xsi:type="string" _delta="define"><![CDATA[]]></constant>
+    <constant id="PORTAL_USERREQUEST_PUBLIC_LOG" xsi:type="string" _delta="define"><![CDATA[public_log]]></constant>
+    <constant id="PORTAL_USERREQUEST_USER_COMMENT" xsi:type="string" _delta="define"><![CDATA[user_comment]]></constant>
+    <constant id="PORTAL_USERREQUEST_FORM_ATTRIBUTES" xsi:type="string" _delta="define"><![CDATA[title,description,impact,urgency,public_log]]></constant>
+    <constant id="PORTAL_USERREQUEST_TYPE" xsi:type="string" _delta="define"><![CDATA[request_type]]></constant>
+    <constant id="PORTAL_USERREQUEST_LIST_ZLIST" xsi:type="string" _delta="define"><![CDATA[finalclass,title,start_date,status,servicesubcategory_id,priority,caller_id]]></constant>
     <constant id="PORTAL_TICKETS_SEARCH_CRITERIA" xsi:type="string" _delta="define"><![CDATA[ref,start_date,close_date,service_id,caller_id]]></constant>
-    <constant id="PORTAL_TICKETS_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
-    <constant id="PORTAL_TICKET_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
+    <constant id="PORTAL_USERREQUEST_CLOSED_ZLIST" xsi:type="string" _delta="define"><![CDATA[title,start_date,close_date,servicesubcategory_id]]></constant>
+    <constant id="PORTAL_USERREQUEST_DETAILS_ZLIST" xsi:type="string" _delta="define"><![CDATA[{"col:left":["ref","caller_id","servicesubcategory_id","title","description","solution"],"col:right":["status","priority","start_date","resolution_date","last_update","agent_id"]}]]></constant>
   </constants>
   <classes>
     <class id="UserRequest" _delta="define">

+ 6 - 6
datamodels/2.x/itop-welcome-itil/main.itop-welcome-itil.php

@@ -74,16 +74,16 @@ class MyPortalURLMaker implements iDBObjectURLMaker
 {
 	public static function MakeObjectURL($sClass, $iId)
 	{
-		switch($sClass)
+		if (strpos(MetaModel::GetConfig()->Get('portal_tickets'), $sClass) !== false)
 		{
-		case 'UserRequest':
 			$sAbsoluteUrl = utils::GetAbsoluteUrlAppRoot();
 			$sUrl = "{$sAbsoluteUrl}portal/index.php?operation=details&class=$sClass&id=$iId";
-			return $sUrl;
-
-		default:
-			return '';
 		}
+		else
+		{
+			$sUrl = '';
+		}
+		return $sUrl;
 	}
 }
 

+ 262 - 107
portal/index.php

@@ -27,6 +27,91 @@ require_once(APPROOT.'/application/application.inc.php');
 require_once(APPROOT.'/application/nicewebpage.class.inc.php');
 require_once(APPROOT.'/application/wizardhelper.class.inc.php');
 
+
+/**
+ * Helper to determine the supported types of tickets
+ */
+function GetTicketClasses()
+{
+	$aClasses = array();
+	foreach (explode(',', MetaModel::GetConfig()->Get('portal_tickets')) as $sRawClass)
+	{
+		$sRawClass = trim($sRawClass);
+		if (!MetaModel::IsValidClass($sRawClass))
+		{
+			throw new Exception("Class '$sRawClass' is not a valid class, please review your configuration (portal_tickets)");
+		}
+		if (!MetaModel::IsParentClass('Ticket', $sRawClass))
+		{
+			throw new Exception("Class '$sRawClass' does not inherit from Ticket, please review your configuration (portal_tickets)");
+		}
+		$aClasses[] = $sRawClass;
+	}
+	return $aClasses;
+} 
+
+/**
+ * Helper to get the relevant constant 
+ */
+function GetConstant($sClass, $sName)
+{
+	$sConstName = 'PORTAL_'.strtoupper($sClass).'_'.$sName;
+	if (defined($sConstName))
+	{
+		return constant($sConstName);
+	}
+	else
+	{
+		throw new Exception("Missing portal constant '$sConstName'");
+	}
+}
+
+/**
+ * Helper to determine the ticket class given the service subcategory
+ */
+function ComputeClass($iSubSvcId)
+{
+	$aClasses = GetTicketClasses();
+	if ((PORTAL_SET_TYPE_FROM == '') || (PORTAL_TYPE_TO_CLASS == ''))
+	{
+		// return the first enabled class
+		$sClass = reset($aClasses);
+	}
+	else
+	{
+		$oServiceSubcat = MetaModel::GetObject('ServiceSubcategory', $iSubSvcId, true, true /* allow all data*/);
+		$sTicketType = $oServiceSubcat->Get(PORTAL_SET_TYPE_FROM);
+		$aMapping = json_decode(PORTAL_TYPE_TO_CLASS, true);
+		if (!array_key_exists($sTicketType, $aMapping))
+		{
+			throw new Exception("Ticket type '$sTicketType' not found in the mapping (".implode(', ', array_keys($aMapping))."). Please contact your administrator.");
+		}
+		$sClass = $aMapping[$sTicketType];
+		if (!in_array($sClass, $aClasses))
+		{
+			throw new Exception("Service subcategory #$iSubSvcId has a ticket type ($sClass) that is not known by the portal, please contact your administrator.");
+		}
+	}
+	return $sClass;
+}
+
+/**
+ * Helper to limit the service categories depending on the current settings
+ */
+function RestrictSubcategories(&$oSearch)
+{
+	$aMapping = json_decode(PORTAL_TYPE_TO_CLASS, true);
+	foreach($aMapping as $sTicketType => $sClass)
+	{
+		if (!in_array($sClass, GetTicketClasses()))
+		{
+			// Exclude this value for the result set
+			$oSearch->AddCondition(PORTAL_SET_TYPE_FROM, $sTicketType, '!=');
+		}
+	}
+}
+ 
+
 /**
  * Displays the portal main menu
  * @param WebPage $oP The current web page
@@ -147,6 +232,7 @@ function SelectServiceSubCategory($oP, $oUserOrg, $iSvcId = null)
 	$iDefaultWizNext = 2;
 
 	$oSearch = DBObjectSearch::FromOQL(PORTAL_SERVICE_SUBCATEGORY_QUERY);
+	RestrictSubcategories($oSearch);
 	$oSearch->AllowAllData(); // In case the user has the rights on his org only
 	$oSet = new CMDBObjectSet($oSearch, array(), array('svc_id' => $iSvcId, 'org_id' => $oUserOrg->GetKey()));
 	if ($oSet->Count() == 1)
@@ -225,7 +311,17 @@ function SelectRequestTemplate($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null
 	$iDefaultTemplate = isset($aParameters['template_id']) ? $aParameters['template_id'] : 0;
 	if (MetaModel::IsValidClass('Template'))
 	{
-		$oSearch = DBObjectSearch::FromOQL(REQUEST_TEMPLATE_QUERY);
+		$sClass = ComputeClass($aParameters['servicesubcategory_id']);
+		try
+		{
+			$sOql = GetConstant($sClass, 'TEMPLATE_QUERY');
+		}
+		catch(Exception $e)
+		{
+			// Backward compatibility
+			$sOql = REQUEST_TEMPLATE_QUERY;
+		}
+		$oSearch = DBObjectSearch::FromOQL($sOql);
 		$oSearch->AllowAllData();
 		$oSet = new CMDBObjectSet($oSearch, array(), array(
 			'service_id' => $aParameters['service_id'],
@@ -293,7 +389,7 @@ function SelectRequestTemplate($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null
 }
 
 /**
- * Displays the form for the final step of the UserRequest creation
+ * Displays the form for the final step of the ticket creation
  * @param WebPage $oP The current web page for the form output
  * @param Organization $oUserOrg The organization of the current user
  * @param integer $iSvcId The identifier of the service (fall through when there is only one service)
@@ -303,12 +399,6 @@ function SelectRequestTemplate($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null
  */
 function RequestCreationForm($oP, $oUserOrg, $iSvcId = null, $iSubSvcId = null, $iTemplateId = null)
 {
-		$oP->add_script(
-<<<EOF
-		// Create the object once at the beginning of the page...
-		var oWizardHelper = new WizardHelper('UserRequest', '');
-EOF
-);
 	$aParameters = $oP->ReadAllParams(PORTAL_ALL_PARAMS.',template_id');
 	if ($iSvcId != null)
 	{
@@ -323,9 +413,6 @@ EOF
 		$aParameters['template_id'] = $iTemplateId;
 	}
 	
-	// Example: $aList = array('title', 'description', 'impact', 'emergency');
-	$aList = explode(',', PORTAL_REQUEST_FORM_ATTRIBUTES);
-
 	$sDescription = '';
 	if (isset($aParameters['template_id']) && ($aParameters['template_id'] != 0))
 	{
@@ -352,17 +439,21 @@ EOF
 	$oServiceSubCategory = MetaModel::GetObject('ServiceSubcategory', $aParameters['servicesubcategory_id'], false, true /* allow all data*/);
 	if (is_object($oServiceCategory) && is_object($oServiceSubCategory))
 	{
-		$oRequest = new UserRequest();
+		$sClass = ComputeClass($oServiceSubCategory->GetKey());
+		$oRequest = MetaModel::NewObject($sClass);
 		$oRequest->Set('org_id', $oUserOrg->GetKey());
 		$oRequest->Set('caller_id', UserRights::GetContactId());
 		$oRequest->Set('service_id', $aParameters['service_id']);
 		$oRequest->Set('servicesubcategory_id', $aParameters['servicesubcategory_id']);
-		
-		$oAttDef = MetaModel::GetAttributeDef('UserRequest', 'service_id');
+
+		$oAttDef = MetaModel::GetAttributeDef($sClass, 'service_id');
 		$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $oServiceCategory->GetName());
-		$oAttDef = MetaModel::GetAttributeDef('UserRequest', 'servicesubcategory_id');
+
+		$oAttDef = MetaModel::GetAttributeDef($sClass, 'servicesubcategory_id');
 		$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $oServiceSubCategory->GetName());
 
+		$aList = explode(',', GetConstant($sClass, 'FORM_ATTRIBUTES'));
+
 		$iFlags = 0;
 		foreach($aList as $sAttCode)
 		{
@@ -377,7 +468,7 @@ EOF
 		foreach($aList as $sAttCode)
 		{
 			$value = '';
-			$oAttDef = MetaModel::GetAttributeDef(get_class($oRequest), $sAttCode);
+			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
 			$iFlags = $oRequest->GetAttributeFlags($sAttCode);
 			if (isset($aParameters[$sAttCode]))
 			{
@@ -387,11 +478,9 @@ EOF
 				
 			$sInputId = 'attr_'.$sAttCode;
 			$aFieldsMap[$sAttCode] = $sInputId;
-			$sValue = "<span id=\"field_{$sInputId}\">".$oRequest->GetFormElementForField($oP, get_class($oRequest), $sAttCode, $oAttDef, $value, '', 'attr_'.$sAttCode, '', $iFlags, $aArgs).'</span>';
+			$sValue = "<span id=\"field_{$sInputId}\">".$oRequest->GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $value, '', 'attr_'.$sAttCode, '', $iFlags, $aArgs).'</span>';
 			$aDetails[] = array('label' => $oAttDef->GetLabel(), 'value' => $sValue);
 		}
-//		The log must be requested in the constant PORTAL_REQUEST_FORM_ATTRIBUTES
-//		$aDetails[] = array('label' => MetaModel::GetLabel('UserRequest', PORTAL_ATTCODE_LOG), 'value' => '<textarea id="attr_moreinfo" class="resizable ui-resizable" cols="40" rows="8" name="attr_moreinfo" title="" style="margin: 0px; resize: none; position: static; display: block; height: 145px; width: 339px;">'.$sDescription.'</textarea>');
 
 		if (!empty($aTemplateFields))
 		{
@@ -399,7 +488,7 @@ EOF
 			{
 				if (!in_array($sAttCode, $aList))
 				{
-					$sValue = $oField->GetFormElement($oP, get_class($oRequest));
+					$sValue = $oField->GetFormElement($oP, $sClass);
 					if ($oField->Get('input_type') == 'hidden')
 					{
 						$aHidden[] = $sValue;
@@ -412,6 +501,13 @@ EOF
 			}
 		}
 
+		$oP->add_script(
+<<<EOF
+// Create the object once at the beginning of the page...
+	var oWizardHelper = new WizardHelper('$sClass', '');
+EOF
+);
+
 		$oP->add_linked_script("../js/json.js");
 		$oP->add_linked_script("../js/forms-json-utils.js");
 		$oP->add_linked_script("../js/wizardhelper.js");
@@ -422,13 +518,28 @@ EOF
 		$oP->add("<div class=\"wizContainer\" id=\"form_request_description\">\n");
 		$oP->add("<h1 id=\"title_request_form\">".Dict::S('Portal:DescriptionOfTheRequest')."</h1>\n");
 		$oP->WizardFormStart('request_form', 4);
-		//$oP->add("<table>\n");
+
 		$oP->details($aDetails);
 
+		// Add hidden fields for known values, enabling dependant attributes to be computed correctly
+		//
+		foreach($oRequest->ListChanges() as $sAttCode => $value)
+		{
+			if (!in_array($sAttCode, $aList))
+			{
+				$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+				if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
+				{
+					$sValue = htmlentities($oRequest->Get($sAttCode), ENT_QUOTES, 'UTF-8');
+					$oP->add("<input type=\"hidden\" id=\"attr_$sAttCode\" name=\"attr_$sAttCode\" value=\"$sValue\">");
+					$aFieldsMap[$sAttCode] = 'attr_'.$sAttCode;
+				}
+			}
+		}
+
 		$oAttPlugin = new AttachmentPlugIn();
 		$oAttPlugin->OnDisplayRelations($oRequest, $oP, true /* edit */);
 
-		$oP->DumpHiddenParams($aParameters, $aList);
 		$oP->add("<input type=\"hidden\" name=\"operation\" value=\"create_request\">");
 		$oP->WizardFormButtons(BUTTON_BACK | BUTTON_FINISH | BUTTON_CANCEL); //Back button automatically discarded if on the first page
 		$oP->WizardFormEnd();
@@ -457,7 +568,7 @@ EOF
 }
 
 /**
- * Validate the parameters and create the UserRequest object (based on the page's POSTed parameters)
+ * Validate the parameters and create the ticket object (based on the page's POSTed parameters)
  * @param WebPage $oP The current web page for the  output
  * @param Organization $oUserOrg The organization of the current user
  * @return void
@@ -487,6 +598,7 @@ function DoCreateRequest($oP, $oUserOrg)
 	
 	// 2) Service Subcategory
 	$oSearch = DBObjectSearch::FromOQL(PORTAL_VALIDATE_SERVICESUBCATEGORY_QUERY);
+	RestrictSubcategories($oSearch);
 	$oSearch->AllowAllData(); // In case the user has the rights on his org only
 	$oSet = new CMDBObjectSet($oSearch, array(), array('service_id' => $aParameters['service_id'], 'id' =>$aParameters['servicesubcategory_id'],'org_id' => $oUserOrg->GetKey() ));
 	if ($oSet->Count() != 1)
@@ -496,27 +608,28 @@ function DoCreateRequest($oP, $oUserOrg)
 	}
 	$oServiceSubCategory = $oSet->Fetch();
 
-	$oRequest = new UserRequest();
+	$sClass = ComputeClass($oServiceSubCategory->GetKey());
+	$oRequest = MetaModel::NewObject($sClass);
 	$oRequest->Set('org_id', $oUserOrg->GetKey());
 	$oRequest->Set('caller_id', UserRights::GetContactId());
-	$aList = array('service_id', 'servicesubcategory_id', 'title', 'description', 'impact');
 	$oRequest->UpdateObjectFromPostedForm();
 	if (isset($aParameters['moreinfo']))
 	{
 		// There is a template, insert it into the description
-		$oRequest->Set(PORTAL_ATTCODE_LOG, $aParameters['moreinfo']);
+		$sLogAttCode = GetConstant($sClass, 'PUBLIC_LOG');
+		$oRequest->Set($sLogAttCode, $aParameters['moreinfo']);
 	}
 
-	if ((PORTAL_ATTCODE_TYPE != '') && (PORTAL_SET_TYPE_FROM != ''))
+	$sTypeAttCode = GetConstant($sClass, 'TYPE');
+	if (($sTypeAttCode != '') && (PORTAL_SET_TYPE_FROM != ''))
 	{
-		$oRequest->Set(PORTAL_ATTCODE_TYPE, $oServiceSubCategory->Get(PORTAL_SET_TYPE_FROM));
+		$oRequest->Set($sTypeAttCode, $oServiceSubCategory->Get(PORTAL_SET_TYPE_FROM));
 	}
-	if (MetaModel::IsValidAttCode('UserRequest', 'origin'))
+	if (MetaModel::IsValidAttCode($sClass, 'origin'))
 	{
 		$oRequest->Set('origin', 'portal');
 	}
 
-	/////$oP->DoUpdateObjectFromPostedForm($oObj);
 	$oAttPlugin = new AttachmentPlugIn();
 	$oAttPlugin->OnFormSubmit($oRequest);
 
@@ -526,7 +639,8 @@ function DoCreateRequest($oP, $oUserOrg)
 		if (isset($aParameters['template_id']))
 		{
 			$oTemplate = MetaModel::GetObject('Template', $aParameters['template_id']);
-			$oRequest->Set('public_log', $oTemplate->GetPostedValuesAsText($oRequest)."\n");
+			$sLogAttCode = GetConstant($sClass, 'PUBLIC_LOG');
+			$oRequest->Set($sLogAttCode, $oTemplate->GetPostedValuesAsText($oRequest)."\n");
 			$oRequest->DBInsertNoReload();
 			$oTemplate->RecordExtraDataFromPostedForm($oRequest);
 		}
@@ -534,7 +648,7 @@ function DoCreateRequest($oP, $oUserOrg)
 		{
 			$oRequest->DBInsertNoReload();
 		}
-		$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oRequest->GetName(), MetaModel::GetName(get_class($oRequest)))."</h1>\n");
+		$oP->add("<h1>".Dict::Format('UI:Title:Object_Of_Class_Created', $oRequest->GetName(), MetaModel::GetName($sClass))."</h1>\n");
 
 		//DisplayObject($oP, $oRequest, $oUserOrg);
 		ShowOngoingTickets($oP);
@@ -580,6 +694,47 @@ function CreateRequest(WebPage $oP, Organization $oUserOrg)
 }
 
 /**
+ * Helper to display lists (UserRequest, Incident, etc.)
+ * Adjust the presentation depending on the following cases:
+ * - no item at all
+ * - items of one class only
+ * - items of several classes    
+ */ 
+function DisplayRequestLists(WebPage $oP, $aClassToSet)
+{
+	$iNotEmpty = 0; // Count of types for which there are some items to display
+	foreach ($aClassToSet as $sClass => $oSet)
+	{
+		if ($oSet->Count() > 0)
+		{
+			$iNotEmpty++;
+		}
+	}
+	if ($iNotEmpty == 0)
+	{
+		$oP->p(Dict::S('Portal:NoOpenRequest'));
+	}
+	else
+	{
+		foreach ($aClassToSet as $sClass => $oSet)
+		{
+			if ($iNotEmpty > 1)
+			{
+				// Differentiate the sublists
+				$oP->add("<h2>".MetaModel::GetName($sClass)."</h2>\n");
+			}
+			if ($oSet->Count() > 0)
+			{
+				$sZList = GetConstant($sClass, 'LIST_ZLIST');
+				$aZList =  explode(',', $sZList);
+				$oP->DisplaySet($oSet, $aZList, Dict::S('Portal:NoOpenRequest'));
+			}
+		}
+	}
+}
+
+
+/**
  * Lists all the currently opened User Requests for the current user
  * @param WebPage $oP The current web page
  * @return void
@@ -588,16 +743,19 @@ function ListOpenRequests(WebPage $oP)
 {
 	$oUserOrg = GetUserOrg();
 
-	$sOQL = 'SELECT UserRequest WHERE org_id = :org_id AND status NOT IN ("closed", "resolved")';
-	$oSearch = DBObjectSearch::FromOQL($sOQL);
-	$iUser = UserRights::GetContactId();
-	if ($iUser > 0 && !IsPowerUser())
+	$aClassToSet = array();
+	foreach (GetTicketClasses() as $sClass)
 	{
-		$oSearch->AddCondition('caller_id', $iUser);
+		$sOQL = "SELECT $sClass WHERE org_id = :org_id AND status NOT IN ('closed', 'resolved')";
+		$oSearch = DBObjectSearch::FromOQL($sOQL);
+		$iUser = UserRights::GetContactId();
+		if ($iUser > 0 && !IsPowerUser())
+		{
+			$oSearch->AddCondition('caller_id', $iUser);
+		}
+		$aClassToSet[$sClass] = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
 	}
-	$oSet = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
-	$aZList =  explode(',', PORTAL_TICKETS_LIST_ZLIST);
-	$oP->DisplaySet($oSet, $aZList, Dict::S('Portal:NoOpenRequest'));
+	DisplayRequestLists($oP, $aClassToSet);
 }
 
 /**
@@ -609,16 +767,19 @@ function ListResolvedRequests(WebPage $oP)
 {
 	$oUserOrg = GetUserOrg();
 
-	$sOQL = 'SELECT UserRequest WHERE org_id = :org_id AND status = "resolved"';
-	$oSearch = DBObjectSearch::FromOQL($sOQL);
-	$iUser = UserRights::GetContactId();
-	if ($iUser > 0 && !IsPowerUser())
+	$aClassToSet = array();
+	foreach (GetTicketClasses() as $sClass)
 	{
-		$oSearch->AddCondition('caller_id', $iUser);
+		$sOQL = "SELECT $sClass WHERE org_id = :org_id AND status = 'resolved'";
+		$oSearch = DBObjectSearch::FromOQL($sOQL);
+		$iUser = UserRights::GetContactId();
+		if ($iUser > 0 && !IsPowerUser())
+		{
+			$oSearch->AddCondition('caller_id', $iUser);
+		}
+		$aClassToSet[$sClass] = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
 	}
-	$oSet = new CMDBObjectSet($oSearch, array(), array('org_id' => $oUserOrg->GetKey()));
-	$aZList =  explode(',', PORTAL_TICKETS_LIST_ZLIST);
-	$oP->DisplaySet($oSet, $aZList, Dict::S('Portal:NoOpenRequest'));
+	DisplayRequestLists($oP, $aClassToSet);
 }
 
 /**
@@ -629,28 +790,32 @@ function ListResolvedRequests(WebPage $oP)
 function ListClosedTickets(WebPage $oP)
 {
 	$aAttSpecs = explode(',', PORTAL_TICKETS_SEARCH_CRITERIA);
-	$aZList =  explode(',', PORTAL_TICKETS_CLOSED_ZLIST);
-
-	$oP->DisplaySearchForm('UserRequest', $aAttSpecs, array('operation' => 'show_closed'), 'search_', false /* => not closed */);
+	$aClasses = GetTicketClasses();
+	$sMainClass = reset($aClasses);
+	$oP->DisplaySearchForm($sMainClass, $aAttSpecs, array('operation' => 'show_closed'), 'search_', false /* => not closed */);
 
 	$oUserOrg = GetUserOrg();
 
-	// UserRequest
-	$oSearch = $oP->PostedParamsToFilter('UserRequest', $aAttSpecs, 'search_');
-	if(is_null($oSearch))
-	{
-		$oSearch = new DBObjectSearch('UserRequest');
-	}
-	$oSearch->AddCondition('org_id', $oUserOrg->GetKey());
-	$oSearch->AddCondition('status', 'closed');
-	$iUser = UserRights::GetContactId();
-	if ($iUser > 0 && !IsPowerUser())
+	$oP->add("<h1>".Dict::S('Portal:ClosedRequests')."</h1>\n");
+
+	$aClassToSet = array();
+	foreach (GetTicketClasses() as $sClass)
 	{
-		$oSearch->AddCondition('caller_id', $iUser);
+		$oSearch = $oP->PostedParamsToFilter($sClass, $aAttSpecs, 'search_');
+		if(is_null($oSearch))
+		{
+			$oSearch = new DBObjectSearch($sClass);
+		}
+		$oSearch->AddCondition('org_id', $oUserOrg->GetKey());
+		$oSearch->AddCondition('status', 'closed');
+		$iUser = UserRights::GetContactId();
+		if ($iUser > 0 && !IsPowerUser())
+		{
+			$oSearch->AddCondition('caller_id', $iUser);
+		}
+		$aClassToSet[$sClass] = new CMDBObjectSet($oSearch);
 	}
-	$oSet1 = new CMDBObjectSet($oSearch);
-	$oP->add("<h1>".Dict::S('Portal:ClosedRequests')."</h1>\n");
-	$oP->DisplaySet($oSet1, $aZList, Dict::S('Portal:NoClosedRequest'));
+	DisplayRequestLists($oP, $aClassToSet);
 }
 
 
@@ -663,13 +828,12 @@ function ListClosedTickets(WebPage $oP)
  */
 function DisplayObject($oP, $oObj, $oUserOrg)
 {
-	switch(get_class($oObj))
+	if (in_array(get_class($oObj), GetTicketClasses()))
 	{
-		case 'UserRequest':
 		ShowDetailsRequest($oP, $oObj);
-		break;
-
-		default:
+	}
+	else
+	{
 		throw new Exception("The class ".get_class($oObj)." is not handled through the portal");
 	}
 }
@@ -683,6 +847,8 @@ function DisplayObject($oP, $oObj, $oUserOrg)
 function ShowDetailsRequest(WebPage $oP, $oObj)
 {	
 	$sClass = get_class($oObj);
+	$sLogAttCode = GetConstant($sClass, 'PUBLIC_LOG');
+	$sUserCommentAttCode = GetConstant($sClass, 'USER_COMMENT');
 
 	$bIsEscalateButton = false;
 	$bIsReopenButton = false;
@@ -698,7 +864,7 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
 			case 'frozen':
 			case 'pending':
 			$aEditAtt = array(
-				PORTAL_ATTCODE_LOG => '????'
+				$sLogAttCode => '????'
 			);
 			$bEditAttachments = true;
 			// disabled - $bIsEscalateButton = true;
@@ -707,7 +873,7 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
 			case 'escalated_tto':
 			case 'escalated_ttr':
 			$aEditAtt = array(
-				PORTAL_ATTCODE_LOG => '????'
+				$sLogAttCode => '????'
 			);
 			$bEditAttachments = true;
 			break;
@@ -717,10 +883,10 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
 			if (array_key_exists('ev_reopen', MetaModel::EnumStimuli($sClass)))
 			{
 				$bIsReopenButton = true;
-				MakeStimulusForm($oP, $oObj, 'ev_reopen', array(PORTAL_ATTCODE_LOG));
+				MakeStimulusForm($oP, $oObj, 'ev_reopen', array($sLogAttCode));
 			}
 			$bIsCloseButton = true;
-			MakeStimulusForm($oP, $oObj, 'ev_close', array('user_satisfaction', PORTAL_ATTCODE_COMMENT));
+			MakeStimulusForm($oP, $oObj, 'ev_close', array('user_satisfaction', $sUserCommentAttCode));
 			break;
 	
 			case 'closed':
@@ -733,22 +899,13 @@ function ShowDetailsRequest(WebPage $oP, $oObj)
 // REFACTORISER LA MISE EN FORME
 	$oP->add("<h1 id=\"title_request_details\">".$oObj->GetIcon()."&nbsp;".Dict::Format('Portal:TitleRequestDetailsFor_Request', $oObj->GetName())."</h1>\n");
 
-	switch($sClass)
-	{
-		case 'UserRequest':
-		$aAttList = json_decode(PORTAL_TICKET_DETAILS_ZLIST, true);
+	$aAttList = json_decode(GetConstant($sClass, 'DETAILS_ZLIST'), true);
 
-		switch($oObj->GetState())
-		{
-			case 'closed':
-			$aAttList['centered'][] = 'user_satisfaction';
-			$aAttList['centered'][] = PORTAL_ATTCODE_COMMENT;
-		}
-		break;
-
-		default:
-		array('col:left'=> array('ref','service_id','servicesubcategory_id','title','description'),'col:right'=> array('status','start_date'));
-		break;
+	switch($oObj->GetState())
+	{
+		case 'closed':
+		$aAttList['centered'][] = 'user_satisfaction';
+		$aAttList['centered'][] = $sUserCommentAttCode;
 	}
 
 	// Remove the edited attribute from the shown attributes
@@ -841,7 +998,7 @@ EOF
 	}
 	foreach($aEditFields as $sAttCode => $aFieldSpec)
 	{
-		if ($sAttCode == PORTAL_ATTCODE_LOG)
+		if ($sAttCode == $sLogAttCode)
 		{
 			// Skip, the public log will be displayed below the buttons
 			continue;
@@ -881,17 +1038,17 @@ EOF
 
 	$oP->add('<tr>');
 	$oP->add('<td colspan="2" style="vertical-align:top;">');
-	if (isset($aEditFields[PORTAL_ATTCODE_LOG]))
+	if (isset($aEditFields[$sLogAttCode]))
 	{
 		$oP->add("<div class=\"edit_item\">");
-		$oP->add('<h1>'.$aEditFields[PORTAL_ATTCODE_LOG]['label'].'</h1>');
-		$oP->add($aEditFields[PORTAL_ATTCODE_LOG]['value']);
+		$oP->add('<h1>'.$aEditFields[$sLogAttCode]['label'].'</h1>');
+		$oP->add($aEditFields[$sLogAttCode]['value']);
 		$oP->add('</div>');
 	}
 	else
 	{
-		$oP->add('<h1>'.MetaModel::GetLabel($sClass, PORTAL_ATTCODE_LOG).'</h1>');
-		$oP->add($oObj->GetAsHTML(PORTAL_ATTCODE_LOG));
+		$oP->add('<h1>'.MetaModel::GetLabel($sClass, $sLogAttCode).'</h1>');
+		$oP->add($oObj->GetAsHTML($sLogAttCode));
 	}
 	$oP->add('</td>');
 	$oP->add('</tr>');
@@ -1031,7 +1188,9 @@ try
 
    ApplicationContext::SetUrlMakerClass('MyPortalURLMaker');
 
-	if (!class_exists('UserRequest'))
+	$aClasses = explode(',', MetaModel::GetConfig()->Get('portal_tickets'));
+	$sMainClass = trim(reset($aClasses));
+	if (!class_exists($sMainClass))
 	{
 		$oP = new WebPage(Dict::S('Portal:Title'));
 		$oP->p(dict::Format('Portal:NoRequestMgmt', UserRights::GetUserFriendlyName()));
@@ -1074,7 +1233,7 @@ try
 				case 'details':
 				$oP->set_title(Dict::S('Portal:TitleDetailsFor_Request'));
 				DisplayMainMenu($oP);
-				$oObj = $oP->FindObjectFromArgs(array('UserRequest'));
+				$oObj = $oP->FindObjectFromArgs(GetTicketClasses());
 				DisplayObject($oP, $oObj, $oUserOrg);
 				break;
 				
@@ -1083,16 +1242,12 @@ try
 				DisplayMainMenu($oP);
 				if (!MetaModel::DBIsReadOnly())
 				{
-					$oObj = $oP->FindObjectFromArgs(array('UserRequest'));
-					switch(get_class($oObj))
-					{
-					case 'UserRequest':
-						$aAttList = array(PORTAL_ATTCODE_LOG, 'user_satisfaction', PORTAL_ATTCODE_COMMENT);
-						break;
-		
-					default:
-						throw new Exception("Implementation issue: unexpected class '".get_class($oObj)."'");
-					}
+					$oObj = $oP->FindObjectFromArgs(GetTicketClasses());
+					$aAttList = array(
+						GetConstant(get_class($oObj), 'PUBLIC_LOG'),
+						'user_satisfaction',
+						GetConstant(get_class($oObj), 'USER_COMMENT')
+					);
 					try
 					{
 						$oP->DoUpdateObjectFromPostedForm($oObj, $aAttList);

+ 69 - 0
portal/readme.txt

@@ -0,0 +1,69 @@
+
+--- Customization of the portal
+
+This is the way it is working now and is highly subject to change...
+
+
+Configuration (itop-config.php)
+===============================
+portal_tickets: CSV value to specify which ticket classes are enabled (default to 'UserRequest') 
+
+
+Common constants (XML)
+======================
+PORTAL_POWER_USER_PROFILE: Name of the profile that determines who can see the ticket of her organization (not only the tickets she is caller for)
+PORTAL_SERVICECATEGORY_QUERY: OQL to list the services (parameters available: org_id)
+PORTAL_SERVICE_SUBCATEGORY_QUERY: OQL to list the service subcategories (parameters available: org_id, svc_id)
+PORTAL_VALIDATE_SERVICECATEGORY_QUERY: OQL to check the service again (security against malicious HTTP POSTs)
+PORTAL_VALIDATE_SERVICESUBCATEGORY_QUERY: OQL to check the service again (security against malicious HTTP POSTs)
+PORTAL_ALL_PARAMS: parameters that the wizard will kindly propagate through its pages (mixing should not be a problem, default value could be cleaned a little...)
+PORTAL_SET_TYPE_FROM: attribute of the class ServiceSubcategory determining the request type
+PORTAL_TYPE_TO_CLASS: optional mapping from the request types to ticket classes
+PORTAL_TICKETS_SEARCH_CRITERIA: list of search criteria for closed tickets
+
+
+Caution: Hardcoded stuff
+========================
+Classes Service and ServiceSubcategory
+A user can update a ticket (new/assigned)
+A user can close a ticket (resolved) (user_satisfaction is hardcoded though user_comment is not)
+
+
+Constants depending on the class of ticket
+==========================================
+For each ticket class enabled, you will have to define these constants:
+
+PORTAL_<TICKET-CLASS>_PUBLIC_LOG: name of the public log attribute
+PORTAL_<TICKET-CLASS>_USER_COMMENT: name of the user comment attribute (legacy, used to be user_commmmment)
+PORTAL_<TICKET-CLASS>_FORM_ATTRIBUTES: attributes proposed to the end-user in the edition form
+PORTAL_<TICKET-CLASS>_TYPE: optional attribute to be set with the value of "request type"
+PORTAL_<TICKET-CLASS>_LIST_ZLIST: list of attribute displayed in the lists (opened and resolved)
+PORTAL_<TICKET-CLASS>_CLOSED_ZLIST: list of attribute displayed in the list of closed tickets
+PORTAL_<TICKET-CLASS>_DETAILS_ZLIST: selection and presentation of attributes in the page that shows their details
+
+
+How to add a type of ticket (example: Incident)
+===============================================
+1) Add it to the list of supported tickets classes: itop-config.php/portal_tickets
+2) Define PORTAL_SET_TYPE_FROM (if not already done) as the attribute of ServiceSubcategory, that will define the request type, depending on the user selection
+3) Map the different values of this request type (in class ServiceSubcategory) to the supported ticket classes
+YOU MUST MAKE SURE THAT ANY OF THE VALUE HAS A MAPPING SO AS TO EXCLUDE SUBCATEGORIES IF THE CORRESPONDING CLASS ARE NOT ENABLED IN THE CONFIG.
+4) Make sure that the queries PORTAL_SERVICE_SUBCATEGORY_QUERY and PORTAL_VALIDATE_SERVICESUBCATEGORY_QUERY will not exclude the expected type
+5) Define the various constants for this class (PORTAL_<MY-CLASS>_XXXX).
+6) Adjust PORTAL_TICKETS_SEARCH_CRITERIA. Those criteria are common to all types of tickets. Giving too many criteria can lead to confusion.
+7) Test, test and re-test!!!
+
+
+How to copy the request type to the ticket
+==========================================
+1) Define PORTAL_SET_TYPE_FROM (if not already done) as the attribute of ServiceSubcategory, that will define the request type, depending on the user selection
+2) Define PORTAL_<TICKET-CLASS>_TYPE as the tiket attribute code to which the request type will be copied as is. There is no mapping.
+
+
+Behavior of the lists when handling several types of tickets
+============================================================
+There are three lists: opened tickets, resolved tickets and closed tickets.
+The following explanation applies to any of those lists.
+ * If no item has been found, one single message is displayed (no request of this category).
+ * If a number of items of only one category have been found, the list is displayed as is.
+ * Otherwise, there are several types of tickets to display. Each sub-list is preceeded by the name of the corresponding class.