瀏覽代碼

Plugin API - alpha version

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1124 a333f486-631f-4898-b8df-5754b55c2be0
romainq 14 年之前
父節點
當前提交
40fdf26072

+ 49 - 0
application/applicationextension.inc.php

@@ -0,0 +1,49 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program 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 General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+/**
+ * Class iPlugin
+ * Management of application plugin 
+ *
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
+ */
+
+interface iApplicationUIExtension
+{
+	public function OnDisplayProperties($oObject, WebPage $oPage, $bEditMode = false);
+	public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false);
+	public function OnFormSubmit($oObject, $sFormPrefix = '');
+
+	public function EnumUsedAttributes($oObject); // Not yet implemented
+	public function GetIcon($oObject); // Not yet implemented
+	public function GetHilightClass($oObject);
+
+	public function EnumAllowedActions(DBObjectSet $oSet);
+}
+
+interface iApplicationObjectExtension
+{
+	public function OnCheckToWrite($oObject);
+	public function OnCheckToDelete($oObject);
+	public function OnDBUpdate($oObject, $oChange = null);
+	public function OnDBInsert($oObject, $oChange = null);
+	public function OnDBDelete($oObject, $oChange = null);
+}
+
+?>

+ 135 - 2
application/cmdbabstract.class.inc.php

@@ -32,6 +32,7 @@ define('HILIGHT_CLASS_OK', 'green');
 define('HILIGHT_CLASS_NONE', '');
 
 require_once(APPROOT.'/core/cmdbobject.class.inc.php');
+require_once(APPROOT.'/application/applicationextension.inc.php');
 require_once(APPROOT.'/application/utils.inc.php');
 require_once(APPROOT.'/application/applicationcontext.class.inc.php');
 require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
@@ -75,7 +76,7 @@ interface iDisplay
 abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 {
 	protected $m_iFormId; // The ID of the form used to edit the object (when in edition mode !)
-	
+
 	public static function GetUIPage()
 	{
 		return '../pages/UI.php';
@@ -163,6 +164,11 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 	function DisplayBareProperties(WebPage $oPage, $bEditMode = false)
 	{
 		$oPage->add($this->GetBareProperties($oPage, $bEditMode));		
+
+		foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnDisplayProperties($this, $oPage, $bEditMode);
+		}
 	}
 
 	function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
@@ -306,6 +312,11 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 				$oBlock->Display($oPage, 'notifications', array());
 			}
 		}
+
+		foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnDisplayRelations($this, $oPage, $bEditMode);
+		}
 	}
 
 	function GetBareProperties(WebPage $oPage, $bEditMode = false)
@@ -1772,6 +1783,34 @@ EOF
 		}
 	}
 	
+	// $m_highlightComparison[previous][new] => next value
+	protected static $m_highlightComparison = array(
+		HILIGHT_CLASS_CRITICAL => array(
+			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
+			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_CRITICAL,
+			HILIGHT_CLASS_OK => HILIGHT_CLASS_CRITICAL,
+			HILIGHT_CLASS_NONE => HILIGHT_CLASS_CRITICAL,
+		),
+		HILIGHT_CLASS_WARNING => array(
+			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
+			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_WARNING,
+			HILIGHT_CLASS_OK => HILIGHT_CLASS_WARNING,
+			HILIGHT_CLASS_NONE => HILIGHT_CLASS_WARNING,
+		),
+		HILIGHT_CLASS_OK => array(
+			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
+			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_WARNING,
+			HILIGHT_CLASS_OK => HILIGHT_CLASS_OK,
+			HILIGHT_CLASS_NONE => HILIGHT_CLASS_OK,
+		),
+		HILIGHT_CLASS_NONE => array(
+			HILIGHT_CLASS_CRITICAL => HILIGHT_CLASS_CRITICAL,
+			HILIGHT_CLASS_WARNING => HILIGHT_CLASS_WARNING,
+			HILIGHT_CLASS_OK => HILIGHT_CLASS_OK,
+			HILIGHT_CLASS_NONE => HILIGHT_CLASS_NONE,
+		),
+	);
+	
 	/**
 	 * This function returns a 'hilight' CSS class, used to hilight a given row in a table
 	 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL,
@@ -1784,7 +1823,15 @@ EOF
 	{
 		// Possible return values are:
 		// HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE	
-		return HILIGHT_CLASS_NONE; // Not hilighted by default
+		$current = HILIGHT_CLASS_NONE; // Not hilighted by default
+
+		// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$new = $oExtensionInstance->GetHilightClass($this);
+			@$current = self::$m_highlightComparison[$current][$new];
+		}
+		return $current;
 	}
 	
 	/**
@@ -1959,5 +2006,91 @@ EOF
 			}
 		}
 	}
+
+	protected function DBInsertTracked_Internal($bDoNotReload = false)
+	{
+		$res = parent::DBInsertTracked_Internal($bDoNotReload);
+
+		// Invoke extensions after insertion (the object must exist, have an id, etc.)
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnDBInsert($this, self::$m_oCurrChange);
+		}
+
+		return $res;
+	}
+
+	protected function DBCloneTracked_Internal($newKey = null)
+	{
+		$oNewObj = parent::DBCloneTracked_Internal($newKey);
+
+		// Invoke extensions after insertion (the object must exist, have an id, etc.)
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnDBInsert($oNewObj, self::$m_oCurrChange);
+		}
+		return $oNewObj;
+	}
+
+	protected function DBUpdateTracked_Internal()
+	{
+		$res = parent::DBUpdateTracked_Internal();
+
+		// Invoke extensions after the update (could be before)
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnDBUpdate($this, self::$m_oCurrChange);
+		}
+		return $res;
+	}
+
+	protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues)
+	{
+		// Todo - invoke the extension
+		return parent::BulkUpdateTracked_Internal($oFilter, $aValues);
+	}
+
+	protected function DBDeleteTracked_Internal()
+	{
+		// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$oExtensionInstance->OnDBDelete($this, self::$m_oCurrChange);
+		}
+
+		return parent::DBDeleteTracked_Internal();
+	}
+
+	protected static function BulkDeleteTracked_Internal(DBObjectSearch $oFilter)
+	{
+		// Todo - invoke the extension
+		return parent::BulkDeleteTracked_Internal($oFilter);
+	}
+
+	public function DoCheckToWrite()
+	{
+		parent::DoCheckToWrite();
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$aNewIssues = $oExtensionInstance->OnCheckToWrite($this);
+			if (count($aNewIssues) > 0)
+			{
+				$this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues);
+			}
+		}
+	}
+
+	protected function DoCheckToDelete()
+	{
+		parent::DoCheckToDelete();
+		foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
+		{
+			$aNewIssues = $oExtensionInstance->OnCheckToDelete($this);
+			if (count($aNewIssues) > 0)
+			{
+				$this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues);
+			}
+		}
+	}
 }
 ?>

+ 18 - 0
application/displayblock.class.inc.php

@@ -1059,6 +1059,15 @@ class MenuBlock extends DisplayBlock
 				if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Add'), 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&addObjects=true&$sContext"); }
 				if ($bIsModifyAllowed) { $aActions[] = array ('label' => Dict::S('UI:Menu:Manage'), 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&sContext"); }
 			}
+			$this->AddMenuSeparator($aActions);
+			foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
+			{
+				$oSet->Rewind();
+				foreach($oExtensionInstance->EnumAllowedActions($oSet) as $sLabel => $sUrl)
+				{
+					$aActions[] = array ('label' => $sLabel, 'url' => $sUrl);
+				}
+			}
 			break;
 			
 			default:
@@ -1089,6 +1098,15 @@ class MenuBlock extends DisplayBlock
 				$aActions[] = array ('label' => Dict::S('UI:Menu:EMail'), 'url' => "mailto:?subject=".$oSet->GetFilter()->__DescribeHTML()."&body=".urlencode("$sUrl?operation=search&filter=$sFilter&$sContext"));
 				$aActions[] = array ('label' => Dict::S('UI:Menu:CSVExport'), 'url' => "../pages/$sUIPage?operation=search&filter=$sFilter&format=csv&$sContext");
 			}
+			$this->AddMenuSeparator($aActions);
+			foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
+			{
+				$oSet->Rewind();
+				foreach($oExtensionInstance->EnumAllowedActions($oSet) as $sLabel => $sUrl)
+				{
+					$aActions[] = array ('label' => $sLabel, 'url' => $sUrl);
+				}
+			}
 		}
 		$sHtml .= "<div class=\"itop_popup\"><ul>\n<li>".Dict::S('UI:Menu:Actions')."\n<ul>\n";
 		foreach ($aActions as $aAction)

+ 25 - 8
core/dbobject.class.php

@@ -46,6 +46,8 @@ abstract class DBObject
 	private $m_bCheckStatus = null; // Means: the object has been verified and is consistent with integrity rules
 													//        if null, then the check has to be performed again to know the status
 	protected $m_aCheckIssues = null;
+	protected $m_aDeleteIssues = null;
+
 	protected $m_aAsArgs = null; // The current object as a standard argument (cache)
 
 	private $m_bFullyLoaded = false; // Compound objects can be partially loaded
@@ -786,11 +788,14 @@ abstract class DBObject
 
 	// check if it is allowed to delete the existing object from the database
 	// a displayable error is returned
-	public function CheckToDelete()
+	protected function DoCheckToDelete()
 	{
-		return true;
+		$this->m_aDeleteIssues = array(); // Ok
 	}
 
+  	// final public function CheckToDelete() - THE EQUIVALENT OF CheckToWrite IS NOT AVAILABLE
+  	// Todo - split the "DeleteObject()" function (UI.php) and move the generic part in cmdbAbstractObject, etc. 
+
 	protected function ListChangedValues(array $aProposal)
 	{
 		$aDelta = array();
@@ -1346,15 +1351,24 @@ abstract class DBObject
 	 */	 	
 	public function GetDeletionScheme(&$aDeletedObjs, &$aResetedObjs, $aVisited = array())
 	{
-		if (array_key_exists(get_class($this), $aVisited))
+		$sClass = get_class($this);
+		$iThisId = $this->GetKey();
+
+		if (array_key_exists($sClass, $aVisited))
 		{
-			if (in_array($this->GetKey(), $aVisited[get_class($this)]))
+			if (in_array($iThisId, $aVisited[$sClass]))
 			{
 				return;
 			}
 		}
-		$aVisited[get_class($this)] = $this->GetKey();
+		$aVisited[$sClass] = $iThisId;
 
+		$aDeletedObjs[$sClass][$iThisId]['to_delete'] = $this;
+		$aDeletedObjs[$sClass][$iThisId]['auto_delete'] = true;
+		// Check the node itself
+		$this->DoCheckToDelete();
+		$aDeletedObjs[$sClass][$iThisId]['issues'] = $this->m_aDeleteIssues;
+	
 		$aDependentObjects = $this->GetReferencingObjects();
 		foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes)
 		{
@@ -1397,12 +1411,15 @@ abstract class DBObject
 						{
 							// First time we find the given object in the list
 							// (and most likely case is that no other occurence will be found)
-							$aDeletedObjs[$sRemoteClass][$iId]['to_delete'] = $oDependentObj;
-							$aDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = ($iDeletePropagationOption == DEL_AUTO);
-							// Recursively inspect this object
 							if ($iDeletePropagationOption == DEL_AUTO)
 							{
+							// Recursively inspect this object
 								$oDependentObj->GetDeletionScheme($aDeletedObjs, $aResetedObjs, $aVisited);
+							}
+							else
+							{
+								$aDeletedObjs[$sRemoteClass][$iId]['to_delete'] = $oDependentObj;
+								$aDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = false;
 							} 
 						}
 					}

+ 40 - 0
core/metamodel.class.php

@@ -3444,6 +3444,30 @@ abstract class MetaModel
 	public static function Startup($sConfigFile, $bModelOnly = false)
 	{
 		self::LoadConfig($sConfigFile);
+
+		$aInterfaces = array('iApplicationUIExtension', 'iApplicationObjectExtension');
+		foreach($aInterfaces as $sInterface)
+		{
+			self::$m_aExtensionClasses[$sInterface] = array();
+		}
+
+		foreach(get_declared_classes() as $sPHPClass)
+		{
+			$oRefClass = new ReflectionClass($sPHPClass);
+			$oExtensionInstance = null;
+			foreach($aInterfaces as $sInterface)
+			{
+				if ($oRefClass->implementsInterface($sInterface))
+				{
+					if (is_null($oExtensionInstance))
+					{
+						$oExtensionInstance = new $sPHPClass;
+					}
+					self::$m_aExtensionClasses[$sInterface][] = $oExtensionInstance;
+				}
+			}
+		}
+
 		if ($bModelOnly) return;
 
 		CMDBSource::SelectDB(self::$m_sDBName);
@@ -3570,6 +3594,8 @@ abstract class MetaModel
 		return self::$m_oConfig;
 	}
 
+	protected static $m_aExtensionClasses = array();
+
 	protected static $m_aPlugins = array();
 	public static function RegisterPlugin($sType, $sName, $aInitCallSpec = array())
 	{
@@ -3960,6 +3986,20 @@ abstract class MetaModel
 		return str_replace($aSearches, $aReplacements, $aInput);
 	}
 
+	/**
+	 * Returns an array of classes implementing the given interface
+	 */
+	public static function EnumPlugins($sInterface)
+	{
+		if (array_key_exists($sInterface, self::$m_aExtensionClasses))
+		{
+			return self::$m_aExtensionClasses[$sInterface];
+		}
+		else
+		{
+			return array();
+		}
+	}
 } // class MetaModel
 
 

+ 3 - 2
dictionaries/de.dictionary.itop.ui.php

@@ -653,6 +653,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'UI:Title:BulkDeletionOf_Count_ObjectsOf_Class' => 'Massenlöschung von %1$d Objekten der %2$s',
 	'UI:Delete:NotAllowedToDelete' => 'Sie sind nicht berechtigt, dieses Objekt zu löschen.',
 	'UI:Delete:NotAllowedToUpdate_Fields' => 'Sie sind nicht berechtigt, die folgenden Felder zu aktualisieren: %1$s',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => 'Dieses Objekt konnte nicht gelöscht werden, da der derzeitige Benutzer nicht die notwendigen Rechte dazu besitzt.',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => 'Dieses Objekt konnte nicht gelöscht werden, da zuerst dazu einige manuelle Operationen durchgeführt werden müssen.',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s im Auftrag von %2$s',
@@ -664,8 +665,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s gelöscht.',
 	'UI:Delete:ConfirmDeletionOf_Name' => 'Löschung von %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => 'Löschung von %1$d Objekten der Klasse %2$s',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Sollte automatisch gelöscht werden, aber Sie sind nicht berechtigt, dies zu tun',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Muss automatisch gelöscht werden, aber Sie sind nicht berechtigt, dieses Objekt zu löschen. Bitte kontaktieren Sie Ihren Anwendungs-Administrator',
+//	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Sollte automatisch gelöscht werden, aber Sie sind nicht berechtigt, dies zu tun',
+//	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Muss automatisch gelöscht werden, aber Sie sind nicht berechtigt, dieses Objekt zu löschen. Bitte kontaktieren Sie Ihren Anwendungs-Administrator',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Wird automatisch gelöscht',
 	'UI:Delete:MustBeDeletedManually' => 'Muss manuell gelöscht werden',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Sollte automatisch aktualisiert werden, aber: %1$s',

+ 6 - 2
dictionaries/dictionary.itop.ui.php

@@ -623,8 +623,11 @@ Dict::Add('EN US', 'English', 'English', array(
 	'UI:Delete:NotAllowedToDelete' => 'You are not allowed to delete this object',
 	'UI:Delete:NotAllowedToUpdate_Fields' => 'You are not allowed to update the following field(s): %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => 'This object could not be deleted because the current user do not have sufficient rights',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => 'This object could not be deleted because some manual operations must be performed prior to that',
+	'UI:Error:CannotDeleteBecauseManualOpNeeded' => 'This object could not be deleted because some manual operations must be performed prior to that',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s on behalf of %2$s',
+	'UI:Delete:Deleted' => 'deleted',
 	'UI:Delete:AutomaticallyDeleted' => 'automatically deleted',
 	'UI:Delete:AutomaticResetOf_Fields' => 'automatic reset of field(s): %1$s',
 	'UI:Delete:CleaningUpRefencesTo_Object' => 'Cleaning up all references to %1$s...',
@@ -633,8 +636,9 @@ Dict::Add('EN US', 'English', 'English', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s deleted.',
 	'UI:Delete:ConfirmDeletionOf_Name' => 'Deletion of %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => 'Deletion of %1$d objects of class %2$s',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Should be automaticaly deleted, but you are not allowed to do so',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Must be deleted manually - but you are not allowed to delete this object, please contact your application admin',
+	'UI:Delete:CannotDeleteBecause' => 'Could not be deleted: %1$s',
+	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Should be automaticaly deleted, but this is not feasible: %1$s',
+	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Must be deleted manually, but this is not feasible: %1$s',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Will be automaticaly deleted',
 	'UI:Delete:MustBeDeletedManually' => 'Must be deleted manually',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Should be automatically updated, but: %1$s',

+ 2 - 2
dictionaries/es_cr.dictionary.itop.ui.php

@@ -628,8 +628,8 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s borrado.',
 	'UI:Delete:ConfirmDeletionOf_Name' => 'Borrado de %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => 'Borrado de %1$d objetos de al clase %2$s',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Beberia ser eliminado automaticamente, pero usted no esta autorizado para hacerlo',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Debe ser borrado manualmente - pero usted no esta autorizado para borrar este objeto, por favor contacte al administrador de la aplicacion',
+//	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Beberia ser eliminado automaticamente, pero usted no esta autorizado para hacerlo',
+//	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Debe ser borrado manualmente - pero usted no esta autorizado para borrar este objeto, por favor contacte al administrador de la aplicacion',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Sera borrado automaticamente',
 	'UI:Delete:MustBeDeletedManually' => 'Debe ser borrado manualmente',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Debe ser actualizado automaticamente, pero: %1$s',

+ 5 - 2
dictionaries/fr.dictionary.itop.ui.php

@@ -626,9 +626,11 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'UI:Title:BulkDeletionOf_Count_ObjectsOf_Class' => 'Suppression massive de %1$d objets de type %2$s',
 	'UI:Delete:NotAllowedToDelete' => 'Vous n\'êtes pas autorisé à supprimer cet objet',
 	'UI:Delete:NotAllowedToUpdate_Fields' => 'Vous n\'êtes pas autorisé à mettre à jour les champs suivants : %1$s',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => 'Cet objet ne peut pas être supprimé car l\'utilisateur courant n\'a pas les droits nécessaires.',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => 'Cet objet ne peut pas être supprimé, des opérations manuelles sont nécessaire avant sa suppression.',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s pour %2$s',
+	'UI:Delete:Deleted' => 'supprimé',
 	'UI:Delete:AutomaticallyDeleted' => 'supprimé automatiquement',
 	'UI:Delete:AutomaticResetOf_Fields' => 'mise à jour automatique des champ(s): %1$s',
 	'UI:Delete:CleaningUpRefencesTo_Object' => 'Suppression de toutes les références vers %1$s...',
@@ -637,8 +639,9 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'UI:Delete:_Name_Class_Deleted' => ' %2$s %1$s supprimé.',
 	'UI:Delete:ConfirmDeletionOf_Name' => 'Suppression de %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => 'Suppression de %1$d objets de type %2$s',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Devrait être supprimé automatiquement, mais vous n\'avez pas le droit d\'effectuer cette opération.',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Doit être supprimé manuellement - mais vous n\'avez pas le droit de supprimer cet objet, contactez votre administrateur pour régler ce problème',
+	'UI:Delete:CannotDeleteBecause' => 'Ne peux pas être supprimé: %1$s',
+	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Devrait être supprimé automatiquement, mais cela n\'est pas possible: %1$s',
+	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Doit être supprimé manuellement, mais cela n\'est pas possible: %1$s',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Sera supprimé automatiquement',
 	'UI:Delete:MustBeDeletedManually' => 'Doit être supprimé manuellement',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Devrait être mis à jour automatiquement, mais: %1$s',

+ 3 - 2
dictionaries/pt_br.dictionary.itop.ui.php

@@ -623,6 +623,7 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
 	'UI:Title:BulkDeletionOf_Count_ObjectsOf_Class' => 'Bulk deletion of %1$d objects of class %2$s',
 	'UI:Delete:NotAllowedToDelete' => 'Permissão negado para eliminar este objeto',
 	'UI:Delete:NotAllowedToUpdate_Fields' => 'Permissão negado para atualizar o(s) seguinte(s) campo(s): %1$s',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => 'Este objeto não pode ser apagado pelo usu&aacute;rio não ter direitos administrativos',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => 'This object could not be deleted because some manual operations must be performed prior to that',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s em nome de %2$s',
@@ -634,8 +635,8 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s eliminado.',
 	'UI:Delete:ConfirmDeletionOf_Name' => 'Eliminação de %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => 'Eliminação do objeto %1$d da classe %2$s',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Should be automaticaly deleted, but you are not allowed to do so',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Must be deleted manually - but you are not allowed to delete this object, please contact your application admin',
+//	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Should be automaticaly deleted, but this is not feasible: %1$s',
+//	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Must be deleted manually, but this is not feasible: %1$s',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Ser&aacute; automaticamente exclu&iacute;do',
 	'UI:Delete:MustBeDeletedManually' => 'Deve ser exclu&iacute;do manualmente',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Devem ser atualizados automaticamente, mas: %1$s',

+ 3 - 2
dictionaries/ru.dictionary.itop.ui.php

@@ -609,6 +609,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 	'UI:Title:BulkDeletionOf_Count_ObjectsOf_Class' => 'Пакетное удаление %1$d объектов класса %2$s',
 	'UI:Delete:NotAllowedToDelete' => 'Вы не можете удалить этот объект',
 	'UI:Delete:NotAllowedToUpdate_Fields' => 'Вы не можете обновить следующее(ие) поле(я): %1$s',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => 'Этот объект не может быть удален, потому что текущий пользователь не имеет достаточных прав',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => 'Этот объект не может быть удален, потому что некоторые ручные операции должны быть выполнены до этого',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s от имени %2$s',
@@ -620,8 +621,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s удалено.',
 	'UI:Delete:ConfirmDeletionOf_Name' => 'Удаление %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => 'Удаление %1$d объектов класса %2$s',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Должно быть автоматичски удалено, но вы не можете это сделать',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Необходимо удалить вручную - но вы не можете удалить этот объект, свяжитесь с администратором вашего приложения',
+//	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Должно быть автоматичски удалено, но вы не можете это сделать',
+//	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Необходимо удалить вручную - но вы не можете удалить этот объект, свяжитесь с администратором вашего приложения',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Будет удалено автоматически',
 	'UI:Delete:MustBeDeletedManually' => 'Необходимо удалить вручную',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Должно быть автоматически обновлено, но: %1$s',

+ 3 - 2
dictionaries/tr.dictionary.itop.ui.php

@@ -608,6 +608,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
 	'UI:Title:BulkDeletionOf_Count_ObjectsOf_Class' => '%2$s sınıfına ait çoklu %1$d nesne silimi',
 	'UI:Delete:NotAllowedToDelete' => 'Bu nesneyi silmek için yetkiniz yok',
 	'UI:Delete:NotAllowedToUpdate_Fields' => '%1$s alanlarını güncellemek için yetkiniz yok',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => 'Nesne yetersiz yetki nedeniyle silinemedi',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => 'Bu nesneyi silmek için öncelikli dışarıdan yapılması gereken işlemler var',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s on behalf of %2$s',
@@ -619,8 +620,8 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s silindi.',
 	'UI:Delete:ConfirmDeletionOf_Name' => '%1$s\'in silimi',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => '%2$s sınıfına ait %1$d nesnelerinin silimi ',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => 'Otomatik silimesini mi istiyorsunuz, ancak buna yetkiniz yok',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => 'Manuel silinmeli - ancak bu nesneyi silmeye yetkiniz yok, lütfen sistem yöneticisiyle irtibata geçiniz.',
+//	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => 'Otomatik silimesini mi istiyorsunuz, ancak buna yetkiniz yok',
+//	'UI:Delete:MustBeDeletedManuallyButNotPossible' => 'Manuel silinmeli - ancak bu nesneyi silmeye yetkiniz yok, lütfen sistem yöneticisiyle irtibata geçiniz.',
 	'UI:Delete:WillBeDeletedAutomatically' => 'Otomatik olarak silinecek',
 	'UI:Delete:MustBeDeletedManually' => 'Manuel silinmeli',
 	'UI:Delete:CannotUpdateBecause_Issue' => 'Otomatik güncellenmeli, ancak: %1$s',

+ 3 - 2
dictionaries/zh.dictionary.itop.ui.php

@@ -607,6 +607,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
 	'UI:Title:BulkDeletionOf_Count_ObjectsOf_Class' => '批量删除 %1$d 个 %2$s 类的对象',
 	'UI:Delete:NotAllowedToDelete' => '您未被允许删除该对象',
 	'UI:Delete:NotAllowedToUpdate_Fields' => '您未被允许更新下述栏目: %1$s',
+	'UI:Error:CannotDeleteBecause' => 'This object could not be deleted because: %1$s',
 	'UI:Error:NotEnoughRightsToDelete' => '该对象不能被删除, 因为当前用户没有足够权限',
 	'UI:Error:CannotDeleteBecauseOfDepencies' => '该对象不能被删除, 因为一些手工操作必须事先完成',
 	'UI:Archive_User_OnBehalfOf_User' => '%1$s on behalf of %2$s',
@@ -618,8 +619,8 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
 	'UI:Delete:_Name_Class_Deleted' => '%1$s - %2$s 删除了.',
 	'UI:Delete:ConfirmDeletionOf_Name' => '删除 %1$s',
 	'UI:Delete:ConfirmDeletionOf_Count_ObjectsOf_Class' => '删除 %2$s 类的 %1$d 个对象',
-	'UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed' => '应该自动删除, 但您未被允许这样做',
-	'UI:Delete:MustBeDeletedManuallyButNotAllowed' => '必须手工删除 - 但您未被允许删除该对象, 请联系您的应用程序系统管理员',
+//	'UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible' => '应该自动删除, 但您未被允许这样做',
+//	'UI:Delete:MustBeDeletedManuallyButNotPossible' => '必须手工删除 - 但您未被允许删除该对象, 请联系您的应用程序系统管理员',
 	'UI:Delete:WillBeDeletedAutomatically' => '将被自动删除',
 	'UI:Delete:MustBeDeletedManually' => '必须手工删除',
 	'UI:Delete:CannotUpdateBecause_Issue' => '应该被自动更新, 但是: %1$s',

+ 73 - 18
pages/UI.php

@@ -28,12 +28,22 @@
  */
 function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 {
-	$bFoundManual = false;
 	$bFoundStopper = false;
+	$bFoundManuelOp = false; // A manual operation is needed
+	$bFoundManualDel = false; // Manual deletion (or change an ext key) is needed
+	$bFoundSecurityIssue = false; // Some operation not allowed to the end-user
+
 	$iTotalDelete = 0; // count of object that must be deleted
 	$iTotalReset = 0; // count of object for which an ext key will be reset (to 0)
 	$aTotalDeletedObjs = array();
 	$aTotalResetedObjs = array();
+
+	$aRequestedByUser = array();
+	foreach($aObjects as $oObj)
+	{
+		$aRequestedByUser[] = $oObj->GetKey();
+	}
+	
 	foreach($aObjects as $oObj)
 	{
 		// Evaluate the impact on the DB integrity
@@ -50,26 +60,36 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 			foreach ($aDeletes as $iId => $aData)
 			{
 				$oToDelete = $aData['to_delete'];
+				$aTotalDeletedObjs[$sRemoteClass][$iId]['to_delete'] = $oToDelete;
+				if (in_array($iId, $aRequestedByUser))
+				{
+					$aTotalDeletedObjs[$sRemoteClass][$iId]['requested_by_user'] = true;
+				}
 				$bDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, DBObjectSet::FromObject($oToDelete));
 				$aTotalDeletedObjs[$sRemoteClass][$iId]['auto_delete'] = $aData['auto_delete'];
 				if (!$bDeleteAllowed)
 				{
 					$aTotalDeletedObjs[$sRemoteClass][$iId]['issue'] = Dict::S('UI:Delete:NotAllowedToDelete');
 					$bFoundStopper = true;
+					$bFoundSecurityIssue = true;
 				}
-				else
+				elseif (isset($aData['issues']) && (count($aData['issues']) > 0))
 				{
-					$aTotalDeletedObjs[$sRemoteClass][$iId]['to_delete'] = $oToDelete;
+					$sIssueDesc = implode(', ', $aData['issues']);
+					$aTotalDeletedObjs[$sRemoteClass][$iId]['issue'] = $sIssueDesc;
+					$bFoundStopper = true;
+					$bFoundManuelOp = true;
 				}
 	
 				$bAutoDel = $aData['auto_delete'];
 				if (!$bAutoDel)
 				{
-					$bFoundManual = true;
+					$bFoundStopper = true;
+					$bFoundManualDel = true;
 				}
 			}
 		}
-		
+
 		foreach ($aResetedObjs as $sRemoteClass => $aToReset)
 		{
 			$iTotalReset += count($aToReset);
@@ -84,6 +104,7 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 					if (!$bUpdateAllowed)
 					{
 						$bFoundStopper = true;
+						$bFoundSecurityIssue = true;
 						$aForbiddenKeys[] = $aRemoteAttDef->GetLabel();
 					}
 					$aExtKeyLabels[] = $aRemoteAttDef->GetLabel();
@@ -117,11 +138,15 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 			$oP->add("<h1>".Dict::Format('UI:Title:BulkDeletionOf_Count_ObjectsOf_Class', count($aObjects), MetaModel::GetName($sClass))."</h1>\n");		
 		}
 		// Security - do not allow the user to force a forbidden delete by the mean of page arguments...
-		if ($bFoundStopper)
+		if ($bFoundSecurityIssue)
 		{
 			throw new CoreException(Dict::S('UI:Error:NotEnoughRightsToDelete'));
 		}
-		if ($bFoundManual)
+		if ($bFoundManuelOp)
+		{
+			throw new CoreException(Dict::S('UI:Error:CannotDeleteBecauseManualOpNeeded'));
+		}
+		if ($bFoundManualDel)
 		{
 			throw new CoreException(Dict::S('UI:Error:CannotDeleteBecauseOfDepencies'));
 		}
@@ -143,10 +168,18 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 			{
 				$oToDelete = $aData['to_delete'];
 
+				if (isset($aData['requested_by_user']))
+				{
+					$sMessage = Dict::S('UI:Delete:Deleted');
+				}
+				else
+				{
+					$sMessage = Dict::S('UI:Delete:AutomaticallyDeleted');
+				}
 				$aDisplayData[] = array(
 					'class' => MetaModel::GetName(get_class($oToDelete)),
 					'object' => $oToDelete->GetHyperLink(),
-					'consequence' => Dict::S('UI:Delete:AutomaticallyDeleted'),
+					'consequence' => $sMessage,
 				);
 
 				$oToDelete->DBDeleteTracked($oMyChange);
@@ -194,11 +227,11 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 			$oP->table($aDisplayConfig, $aDisplayData);
 		}
 
+		// Final report
 		foreach($aObjects as $oObj)
 		{
 			$sName = $oObj->GetName();
 			$sClassLabel = MetaModel::GetName(get_class($oObj));
-			$oObj->DBDeleteTracked($oMyChange);
 			$oP->add("<h1>".Dict::Format('UI:Delete:_Name_Class_Deleted', $sName, $sClassLabel)."</h1>\n");
 		}
 	}
@@ -226,18 +259,32 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 				{
 					if ($bAutoDel)
 					{
-						$sConsequence = Dict::S('UI:Delete:ShouldBeDeletedAtomaticallyButNotAllowed');
+						if (isset($aData['requested_by_user']))
+						{
+							$sConsequence = Dict::Format('UI:Delete:CannotDeleteBecause', $aData['issue']);
+						}
+						else
+						{
+							$sConsequence = Dict::Format('UI:Delete:ShouldBeDeletedAtomaticallyButNotPossible', $aData['issue']);
+						}
 					}
 					else
 					{
-						$sConsequence = Dict::S('UI:Delete:MustBeDeletedManuallyButNotAllowed');
+						$sConsequence = Dict::Format('UI:Delete:MustBeDeletedManuallyButNotPossible', $aData['issue']);
 					}
 				}
 				else
 				{
 					if ($bAutoDel)
 					{
-						$sConsequence = Dict::S('UI:Delete:WillBeDeletedAutomatically');
+						if (isset($aData['requested_by_user']))
+						{
+	                  $sConsequence = ''; // not applicable
+						}
+						else
+						{
+							$sConsequence = Dict::S('UI:Delete:WillBeDeletedAutomatically');
+						}
 					}
 					else
 					{
@@ -272,19 +319,23 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 			}
 		}
 
-		if ($iTotalTargets > 0)
+      $iInducedDeletions = $iTotalTargets - count($aObjects);
+		if ($iInducedDeletions > 0)
 		{
 			if (count($aObjects) == 1)
 			{
 				$oObj = $aObjects[0];
-				$oP->p(Dict::Format('UI:Delete:Count_Objects/LinksReferencing_Object', $iTotalTargets, $oObj->GetName()));
+				$oP->p(Dict::Format('UI:Delete:Count_Objects/LinksReferencing_Object', $iInducedDeletions, $oObj->GetName()));
 			}
 			else
 			{
-				$oP->p(Dict::Format('UI:Delete:Count_Objects/LinksReferencingTheObjects', $iTotalTargets));
+				$oP->p(Dict::Format('UI:Delete:Count_Objects/LinksReferencingTheObjects', $iInducedDeletions));
 			}
 			$oP->p(Dict::S('UI:Delete:ReferencesMustBeDeletedToEnsureIntegrity'));
+		}
 
+		if (($iInducedDeletions > 0) || $bFoundStopper)
+		{
 			$aDisplayConfig = array();
 			$aDisplayConfig['class'] = array('label' => 'Class', 'description' => '');
 			$aDisplayConfig['object'] = array('label' => 'Object', 'description' => '');
@@ -292,13 +343,17 @@ function DeleteObjects(WebPage $oP, $sClass, $aObjects, $bDeleteConfirmed)
 			$oP->table($aDisplayConfig, $aDisplayData);
 		}
 
-		if ($iTotalTargets > 0 && ($bFoundManual || $bFoundStopper))
+		if ($bFoundStopper)
 		{
-			if ($bFoundStopper)
+			if ($bFoundSecurityIssue)
 			{
 				$oP->p(Dict::S('UI:Delete:SorryDeletionNotAllowed'));
 			}
-			elseif ($bFoundManual)
+			elseif ($bFoundManualDel)
+			{
+				$oP->p(Dict::S('UI:Delete:PleaseDoTheManualOperations'));
+			}
+			else // $bFoundManualOp
 			{
 				$oP->p(Dict::S('UI:Delete:PleaseDoTheManualOperations'));
 			}