ソースを参照

New attribute: ImageAttribute

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4129 a333f486-631f-4898-b8df-5754b55c2be0
romainq 9 年 前
コミット
f0bb65f409

+ 61 - 1
application/cmdbabstract.class.inc.php

@@ -1869,7 +1869,42 @@ EOF
 					$sHTMLValue .= "<span id=\"name_$iInputId\">".htmlentities($sFileName, ENT_QUOTES, 'UTF-8')."</span><br/>\n";
 					$sHTMLValue .= "<input title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}[fcontents]\" type=\"file\" id=\"file_$iId\" onChange=\"UpdateFileName('$iId', this.value)\"/>&nbsp;{$sValidationSpan}{$sReloadSpan}\n";
 				break;
-				
+
+				case 'Image':
+					$aEventsList[] ='validate';
+					$aEventsList[] ='change';
+					$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/edit_image.js');
+					$oDocument = $value; // Value is an ormDocument object
+					$sDefaultUrl = $oAttDef->Get('default_image');
+					if (is_object($oDocument) && !$oDocument->IsEmpty())
+					{
+						$sUrl = 'data:'.$oDocument->GetMimeType().';base64,'.base64_encode($oDocument->GetData());
+					}
+					else
+					{
+						$sUrl = $sDefaultUrl;
+					}
+
+					$sHTMLValue = "<div id=\"edit_$iInputId\" class=\"edit-image\"></div>";
+					$sHTMLValue .= "&nbsp;{$sValidationSpan}{$sReloadSpan}\n";
+
+					$aEditImage = array(
+						'input_name' => 'attr_'.$sFieldPrefix.$sAttCode.$sNameSuffix,
+						'max_file_size' => utils::ConvertToBytes(ini_get('upload_max_filesize')),
+						'max_width_px' => $oAttDef->Get('display_max_width'),
+						'max_height_px' => $oAttDef->Get('display_max_height'),
+						'current_image_url' => $sUrl,
+						'default_image_url' => $sDefaultUrl,
+						'labels' => array(
+							'reset_button' => htmlentities(Dict::S('UI:Button:ResetImage'), ENT_QUOTES, 'UTF-8'),
+							'remove_button' => htmlentities(Dict::S('UI:Button:RemoveImage'), ENT_QUOTES, 'UTF-8'),
+							'upload_button' => $sHelpText
+						)
+					);
+					$sEditImageOptions = json_encode($aEditImage);
+					$oPage->add_ready_script("$('#edit_$iInputId').edit_image($sEditImageOptions);");
+					break;
+
 				case 'StopWatch':
 					$sHTMLValue = "The edition of a stopwatch is not allowed!!!";
 				break;
@@ -3008,6 +3043,23 @@ EOF
 					$this->Set($sAttCode, $oDocument);
 				}
 			}
+			elseif ($oAttDef->GetEditClass() == 'Image')
+			{
+				// There should be an uploaded file with the named attr_<attCode>
+				if ($value['remove'])
+				{
+					$this->Set($sAttCode, null);
+				}
+				else
+				{
+					$oDocument = $value['fcontents'];
+					if (!$oDocument->IsEmpty())
+					{
+						// A new file has been uploaded
+						$this->Set($sAttCode, $oDocument);
+					}
+				}
+			}
 			elseif ($oAttDef->GetEditClass() == 'One Way Password')
 			{
 				// Check if the password was typed/changed
@@ -3143,6 +3195,14 @@ EOF
 			{
 				$value = array('fcontents' => utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents'));
 			}
+			elseif ($oAttDef->GetEditClass() == 'Image')
+			{
+				$oImage = utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents');
+				$aSize = utils::GetImageSize($oImage->GetData());
+				$oImage = utils::ResizeImageToFit($oImage, $aSize[0], $aSize[1], $oAttDef->Get('storage_max_width'), $oAttDef->Get('storage_max_height'));
+				$aOtherData = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", array(), 'raw_data');
+				$value = array('fcontents' => $oImage, 'remove' => $aOtherData['remove']);
+			}
 			elseif ($oAttDef->GetEditClass() == 'RedundancySetting')
 			{
 				$value = $oAttDef->ReadValueFromPostedForm($sFormPrefix);

+ 104 - 0
application/utils.inc.php

@@ -1223,4 +1223,108 @@ class utils
 		}
 		return $sCssRelPath;
 	}
+	
+	
+	static public function GetImageSize($sImageData)
+	{
+		if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher
+		{
+			$aRet = @getimagesizefromstring($sImageData);
+		}
+		else if(ini_get('allow_url_fopen'))
+		{
+			// work around to avoid creating a tmp file
+			$sUri = 'data://application/octet-stream;base64,'.base64_encode($sImageData);
+			$aRet = @getimagesize($sUri);
+		}
+		else
+		{
+			// Damned, need to create a tmp file
+			$sTempFile = tempnam(SetupUtils::GetTmpDir(), 'img-');
+			@file_put_contents($sTempFile, $sImageData);
+			$aRet = @getimagesize($sTempFile);
+			@unlink($sTempFile);
+		}
+		return $aRet;
+	}
+
+	/**
+	 * Resize an image attachment so that it fits in the given dimensions
+	 * @param ormDocument $oImage The original image stored as an ormDocument
+	 * @param int $iWidth Image's original width
+	 * @param int $iHeight Image's original height
+	 * @param int $iMaxImageWidth Maximum width for the resized image
+	 * @param int $iMaxImageHeight Maximum height for the resized image
+	 * @return ormDocument The resampled image
+	 */
+	public static function ResizeImageToFit(ormDocument $oImage, $iWidth, $iHeight, $iMaxImageWidth, $iMaxImageHeight)
+	{
+		if (($iWidth <= $iMaxImageWidth) && ($iHeight <= $iMaxImageHeight))
+		{
+			return $oImage;
+		}
+		switch($oImage->GetMimeType())
+		{
+			case 'image/gif':
+			case 'image/jpeg':
+			case 'image/png':
+			$img = @imagecreatefromstring($oImage->GetData());
+			break;
+			
+			default:
+			// Unsupported image type, return the image as-is
+			//throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used.");
+			return $oImage;
+		}
+		if ($img === false)
+		{
+			//throw new Exception("Warning: corrupted image: '".$oImage->GetFileName()." / ".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used.");
+			return $oImage;
+		}
+		else
+		{
+			// Let's scale the image, preserving the transparency for GIFs and PNGs
+			
+			$fScale = min($iMaxImageWidth / $iWidth, $iMaxImageHeight / $iHeight);
+
+			$iNewWidth = $iWidth * $fScale;
+			$iNewHeight = $iHeight * $fScale;
+			
+			$new = imagecreatetruecolor($iNewWidth, $iNewHeight);
+			
+			// Preserve transparency
+			if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png"))
+			{
+				imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127));
+				imagealphablending($new, false);
+				imagesavealpha($new, true);
+			}
+			
+			imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight);
+			
+			ob_start();
+			switch ($oImage->GetMimeType())
+			{
+				case 'image/gif':
+				imagegif($new); // send image to output buffer
+				break;
+				
+				case 'image/jpeg':
+				imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality
+				break;
+				 
+				case 'image/png':
+				imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression
+				break;
+			}
+			$oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName());
+			@ob_end_clean();
+			
+			imagedestroy($img);
+			imagedestroy($new);
+							
+			return $oResampledImage;
+		}
+				
+	}	
 }

+ 14 - 0
application/wizardhelper.class.inc.php

@@ -109,6 +109,20 @@ class WizardHelper
 						$oObj->Set($sAttCode, $oDocument);
 					}
 				}
+				else if ( $oAttDef->GetEditClass() == 'Image' )
+				{
+					if ($bReadUploadedFiles)
+					{
+						$oDocument = utils::ReadPostedDocument('attr_'.$sAttCode, 'fcontents');
+						$oObj->Set($sAttCode, $oDocument);
+					}
+					else
+					{
+						// Create a new empty document, just for displaying the file name
+						$oDocument = new ormDocument(null, '', $value);
+						$oObj->Set($sAttCode, $oDocument);
+					}
+				}
 				else if (($oAttDef->IsExternalKey()) && (!empty($value)) && ($value > 0) )
 				{
 					// For external keys: load the target object so that external fields

+ 64 - 2
core/attributedef.class.inc.php

@@ -4880,7 +4880,7 @@ class AttributeBlob extends AttributeDefinition
 		//	 (temporary tables created on disk)
 		//	 We will have to remove the blobs from the list of attributes when doing the select
 		//	 then the use of Get() should finalize the load
-		if ($value instanceOf ormDocument)
+		if ($value instanceOf ormDocument && !$value->IsEmpty())
 		{
 			$aValues = array();
 			$aValues[$this->GetCode().'_data'] = $value->GetData();
@@ -4946,7 +4946,17 @@ class AttributeBlob extends AttributeDefinition
 	
 	public function GetAsXML($value, $oHostObject = null, $bLocalize = true)
 	{
-		return ''; // Not exportable in XML, or as CDATA + some subtags ??
+		$sRet = '';
+		if (is_object($value))
+		{
+			if (!$value->IsEmpty())
+			{
+				$sRet = '<mimetype>'.$value->GetMimeType().'</mimetype>';
+				$sRet .= '<filename>'.$value->GetFileName().'</filename>';
+				$sRet .= '<data>'.base64_encode($value->GetData()).'</data>';
+			}
+		}
+		return $sRet;
 	}
 
 	/**
@@ -4999,6 +5009,58 @@ class AttributeBlob extends AttributeDefinition
 }
 
 /**
+ * An image is a specific type of document, it is stored as several columns in the database
+ *
+ * @package	 iTopORM
+ */
+class AttributeImage extends AttributeBlob
+{
+	public function GetEditClass() {return "Image";}
+
+	// Facilitate things: allow administrators to upload a document
+	// from a CSV by specifying its path/URL
+	public function MakeRealValue($proposedValue, $oHostObj)
+	{
+		if (!is_object($proposedValue))
+		{
+			if (file_exists($proposedValue) && UserRights::IsAdministrator())
+			{
+				$sContent = file_get_contents($proposedValue);
+				$sExtension = strtolower(pathinfo($proposedValue, PATHINFO_EXTENSION));
+				$sMimeType = "application/x-octet-stream";
+				$aKnownExtensions = array(
+					'jpg' => 'image/jpeg',
+					'jpeg' => 'image/jpeg',
+					'gif' => 'image/gif',
+					'png' => 'image/png'
+				);
+
+				if (!array_key_exists($sExtension, $aKnownExtensions) && extension_loaded('fileinfo'))
+				{
+					$finfo = new finfo(FILEINFO_MIME);
+					$sMimeType = $finfo->file($proposedValue);
+				}
+				return new ormDocument($sContent, $sMimeType);
+			}
+		}
+		return $proposedValue;
+	}
+
+	public function GetAsHTML($value, $oHostObject = null, $bLocalize = true)
+	{
+		$iMaxWidthPx = $this->Get('display_max_width');
+		$iMaxHeightPx = $this->Get('display_max_height');
+		$sUrl = $this->Get('default_image');
+		$sRet = '<img src="'.$sUrl.'" style="max-width: '.$iMaxWidthPx.'px; max-height: '.$iMaxHeightPx.'px">';
+		if (is_object($value) && !$value->IsEmpty())
+		{
+			$sUrl = $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode());
+			$sRet = '<img src="'.$sUrl.'" style="max-width: '.$iMaxWidthPx.'px; max-height: '.$iMaxHeightPx.'px">';
+		}
+		return '<div class="view-image" style="width: '.$iMaxWidthPx.'px; height: '.$iMaxHeightPx.'px;"><span class="helper-middle"></span>'.$sRet.'</div>';
+	}
+}
+/**
  * A stop watch is an ormStopWatch object, it is stored as several columns in the database  
  *
  * @package	 iTopORM

+ 13 - 5
core/cmdbchangeop.class.inc.php

@@ -355,11 +355,19 @@ class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute
 				$sAttName = $this->Get('attcode');
 			}
 			$oPrevDoc = $this->Get('prevdata');
-			$sDocView = $oPrevDoc->GetAsHtml();
-			$sDocView .= "<br/>".Dict::Format('UI:OpenDocumentInNewWindow_',$oPrevDoc->GetDisplayLink(get_class($this), $this->GetKey(), 'prevdata')).", \n";
-			$sDocView .= Dict::Format('UI:DownloadDocument_', $oPrevDoc->GetDownloadLink(get_class($this), $this->GetKey(), 'prevdata'))."\n";
-			//$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata');
-			$sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sDocView);
+			if ($oPrevDoc->IsEmpty())
+			{
+				$sPrevious = '';
+				$sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevious);
+			}
+			else
+			{
+				$sDocView = $oPrevDoc->GetAsHtml();
+				$sDocView .= "<br/>".Dict::Format('UI:OpenDocumentInNewWindow_', $oPrevDoc->GetDisplayLink(get_class($this), $this->GetKey(), 'prevdata')).", \n";
+				$sDocView .= Dict::Format('UI:DownloadDocument_', $oPrevDoc->GetDownloadLink(get_class($this), $this->GetKey(), 'prevdata'))."\n";
+				//$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata');
+				$sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sDocView);
+			}
 		}
 		return $sResult;
 	}

+ 49 - 0
core/pdfbulkexport.class.inc.php

@@ -187,6 +187,55 @@ EOF
 		return $sPDF;
 	}
 
+	protected function GetValue($oObj, $sAttCode)
+	{
+		switch($sAttCode)
+		{
+			case 'id':
+				$sRet = parent::GetValue($oObj, $sAttCode);
+				break;
+
+			default:
+				$value = $oObj->Get($sAttCode);
+				if ($value instanceof ormDocument)
+				{
+					$oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);
+					if ($oAttDef instanceof AttributeImage)
+					{
+						// To limit the image size in the PDF output, we have to enforce the size as height/width because max-width/max-height have no effect
+						//
+						list($iWidth, $iHeight) = utils::GetImageSize($value->GetData());
+						$iMaxWidthPx = min(48, $oAttDef->Get('display_max_width'));
+						$iMaxHeightPx = min(48, $oAttDef->Get('display_max_height'));
+
+						$fScale = min($iMaxWidthPx / $iWidth, $iMaxHeightPx / $iHeight);
+						$iNewWidth = $iWidth * $fScale;
+						$iNewHeight = $iHeight * $fScale;
+						if ($value->IsEmpty())
+						{
+							$sUrl = $oAttDef->Get('default_image');
+							$sRet = '<img src="'.$sUrl.'" style="width: '.$iNewWidth.'px; height: '.$iNewHeight.'px">';
+						}
+						else
+						{
+							$sUrl = 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData());
+							$sRet = '<img src="'.$sUrl.'" style="width: '.$iNewWidth.'px; height: '.$iNewHeight.'px">';
+						}
+						$sRet = '<div class="view-image">'.$sRet.'</div>';
+					}
+					else
+					{
+						$sRet = parent::GetValue($oObj, $sAttCode);
+					}
+				}
+				else
+				{
+					$sRet = parent::GetValue($oObj, $sAttCode);
+				}
+		}
+		return $sRet;
+	}
+
 	public function GetSupportedFormats()
 	{
 		return array('pdf' => Dict::S('Core:BulkExport:PDFFormat'));

+ 4 - 0
core/spreadsheetbulkexport.class.inc.php

@@ -143,6 +143,10 @@ EOF
 				{
 					$sRet = $value->GetTimeSpent();
 				}
+				elseif ($value instanceof ormDocument)
+				{
+					$sRet = '';
+				}
 				elseif ($oAttDef instanceof AttributeString)
 				{
 					$sRet = $oObj->GetAsHTML($sAttCode);

+ 88 - 0
css/light-grey.css

@@ -107,6 +107,78 @@ table.listResults td {
 }
 
 
+table.listResults td .view-image {
+  width: inherit !important;
+  height: inherit !important;
+}
+
+table.listResults td .view-image img {
+  max-width: 48px !important;
+  max-height: 48px !important;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+
+.edit-image .view-image {
+  display: inline-block;
+}
+.edit-image .view-image.dirty.compat {
+  background-image: url("ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png");
+}
+.edit-image .view-image.dirty.compat img {
+  opacity: 0.3;
+}
+
+.edit-image .edit-buttons {
+  display: inline-block;
+  vertical-align: top;
+  margin-top: 4px;
+  margin-left: 3px;
+}
+.edit-image .edit-buttons .button {
+  cursor: pointer;
+  margin-bottom: 3px;
+  padding: 2px;
+  background-color: #e87c1e;
+}
+.edit-image .edit-buttons .button.disabled {
+  cursor: default;
+  background-color: #555555;
+  opacity: 0.3;
+}
+.edit-image .edit-buttons .button .ui-icon {
+  background-image: url("ui-lightness/images/ui-icons_ffffff_256x240.png");
+}
+
+.edit-image .file-input {
+  display: block;
+}
+
+
+/*
+ * Center the image both horizontally and vertically, withing a box which size is fixed (depends on the attribute definition)
+ */
+.details .view-image {
+  text-align: center;
+  padding: 2px;
+  border: 2px solid #dddddd;
+  border-radius: 6px;
+}
+
+.details .view-image img {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.details .view-image .helper-middle {
+  display: inline-block;
+  height: 100%;
+  vertical-align: middle;
+}
+
+
 table.listContainer {
   border: 0;
   padding: 0;
@@ -2057,6 +2129,22 @@ table.export_parameters td {
 }
 
 
+/*
+ * Format for the PDF output
+ */
+.table_preview .view-image {
+  width: inherit !important;
+  height: inherit !important;
+  text-align: center;
+}
+
+.table_preview .view-image img {
+  max-width: 48px !important;
+  max-height: 48px !important;
+  display: inline-block;
+}
+
+
 .graph_zoom {
   display: inline-block;
   float: right;

+ 93 - 4
css/light-grey.scss

@@ -90,6 +90,83 @@ table.listResults td {
 	padding: 2px;
 }
 
+table.listResults td .view-image {
+  // Counteract the forced dimensions (usefull for displaying in the details view)
+  width: inherit !important;
+  height: inherit !important;
+  img {
+    max-width: 48px !important;
+    max-height: 48px !important;
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+.edit-image {
+  .view-image {
+	display: inline-block;
+
+	&.dirty {
+	  // The image will be modified when saving the changes
+
+	  &.compat {
+		// Browser not supporting FileReader
+		background-image: url("ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png");
+		img {
+		  opacity: 0.3;
+		}
+	  }
+	}
+  }
+
+  .edit-buttons {
+	display: inline-block;
+	vertical-align: top;
+	margin-top: 4px;
+	margin-left: 3px;
+
+	.button {
+	  cursor: pointer;
+	  margin-bottom: 3px;
+	  padding: 2px;
+	  background-color: $highlight-color;
+
+	  &.disabled {
+		cursor: default;
+		background-color: $grey-color;
+		opacity: 0.3;
+	  }
+	  .ui-icon {
+		background-image: url("ui-lightness/images/ui-icons_ffffff_256x240.png");
+	  }
+	}
+  }
+
+  .file-input {
+	display: block;
+  }
+}
+
+/* Center the image both horizontally and vertically, withing a box which size is fixed (depends on the attribute definition) */
+.details .view-image {
+  text-align: center;
+  padding: 2px;
+  border: 2px solid #DDDDDD;
+  border-radius: 6px;
+
+  img {
+	display: inline-block;
+	vertical-align: middle;
+  }
+  .helper-middle {
+	// Helper to center the image (requires a span dedicated to this)
+	display: inline-block;
+	height: 100%;
+	vertical-align: middle;
+  }
+}
+
 table.listContainer {
     border: 0;
 	padding: 0;
@@ -1520,6 +1597,18 @@ table.export_parameters td {
 .table_preview div.text-preview {
 	white-space: pre-wrap;
 }
+/* Format for the PDF output */
+.table_preview .view-image {
+  // Counteract the forced dimensions (usefull for displaying in the details view)
+  width: inherit !important;
+  height: inherit !important;
+  text-align: center;
+  img {
+	max-width: 48px !important;
+	max-height: 48px !important;
+	display: inline-block;
+  }
+}
 .graph_zoom {
 	display: inline-block;
 	float: right;
@@ -1585,10 +1674,10 @@ span.refresh-button {
 	display: block;
 }
 .case-log-history-entry-end {
-	display: none;	
+	display: none;
 }
 .expanded .case-log-history-entry-end {
-	display: inline;	
+	display: inline;
 }
 .case-log-history-entry-more {
 	display: inline;
@@ -1603,7 +1692,7 @@ span.refresh-button {
 	vertical-align: bottom;
 }
 .printable-tab .case-log-history-entry-end {
-	display: inline;	
+	display: inline;
 }
 .printable-tab .case-log-history-entry-more {
 	display: none;
@@ -1776,4 +1865,4 @@ span.refresh-button {
 }
 .mfp-close {
 	cursor: pointer !important;
-}
+}

ファイルの差分が大きいため隠しています
+ 0 - 0
datamodels/2.x/itop-config-mgmt/data.sample.persons.xml


+ 16 - 0
datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml

@@ -458,6 +458,14 @@
         </reconciliation>
       </properties>
       <fields>
+        <field id="picture" xsi:type="AttributeImage">
+          <display_max_width>96</display_max_width>
+          <display_max_height>96</display_max_height>
+          <storage_max_width>128</storage_max_width>
+          <storage_max_height>128</storage_max_height>
+          <default_image>images/silhouette.png</default_image>
+          <is_null_allowed>true</is_null_allowed>
+        </field>
         <field id="first_name" xsi:type="AttributeString">
           <sql>first_name</sql>
           <default_value/>
@@ -568,6 +576,14 @@
             <item id="col:col2">
               <rank>50</rank>
               <items>
+                <item id="fieldset:Person:personal_info">
+                  <rank>5</rank>
+                  <items>
+                    <item id="picture">
+                      <rank>10</rank>
+                    </item>
+                  </items>
+                </item>
                 <item id="fieldset:Person:notifiy">
                   <rank>10</rank>
                   <items>

+ 3 - 0
datamodels/2.x/itop-config-mgmt/en.dict.itop-config-mgmt.php

@@ -201,6 +201,8 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Class:Person/Attribute:tickets_list+' => 'All the tickets this person is the caller',
 	'Class:Person/Attribute:manager_id_friendlyname' => 'Manager friendly name',
 	'Class:Person/Attribute:manager_id_friendlyname+' => '',
+	'Class:Person/Attribute:picture' => 'Picture',
+	'Class:Person/Attribute:picture+' => '',
 ));
 
 //
@@ -1880,6 +1882,7 @@ Dict::Add('EN US', 'English', 'English', array(
 'Server:otherinfo' => 'Other information',
 'Server:power' => 'Power supply',
 'Person:info' => 'General information',
+'Person:personal_info' => 'Personal information',
 'Person:notifiy' => 'Notification',
 'Class:Subnet/Tab:IPUsage' => 'IP Usage',
 'Class:Subnet/Tab:IPUsage-explain' => 'Interfaces having an IP in the range: <em>%1$s</em> to <em>%2$s</em>',

+ 3 - 0
datamodels/2.x/itop-config-mgmt/fr.dict.itop-config-mgmt.php

@@ -146,6 +146,8 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'Class:Person/Attribute:tickets_list+' => '',
 	'Class:Person/Attribute:manager_id_friendlyname' => 'Manager friendly name',
 	'Class:Person/Attribute:manager_id_friendlyname+' => '',
+	'Class:Person/Attribute:picture' => 'Photo',
+	'Class:Person/Attribute:picture+' => '',
 ));
 
 //
@@ -1850,6 +1852,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
 'Server:otherinfo' => 'Autres informations',
 'Server:power' => 'Alimentation électrique',
 'Person:info' => 'Informations générales',
+'Person:personal_info' => 'Informations personnelles',
 'Person:notifiy' => 'Notification',
 'Class:Subnet/Tab:IPUsage' => 'IP utilisées',
 'Class:Subnet/Tab:IPUsage-explain' => 'Interfaces ayant une IP dans la plage: <em>%1$s</em> à <em>%2$s</em>',

BIN
datamodels/2.x/itop-config-mgmt/images/silhouette.png


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

@@ -424,6 +424,25 @@ class ApplicationHelper
 		}
 
 		$oApp['combodo.current_user'] = $oUser;
+
+		$sUrl = $oApp['combodo.portal.base.absolute_url'].'img/user-profile-default-256px.png';
+		$oContact = UserRights::GetContactObject();
+		if ($oContact)
+		{
+			if (MetaModel::IsValidAttCode(get_class($oContact), 'picture'))
+			{
+				$oImage = $oContact->Get('picture');
+				if (is_object($oImage) && !$oImage->IsEmpty())
+				{
+					$sUrl = $oImage->GetDownloadURL(get_class($oContact), $oContact->GetKey(), 'picture');
+				}
+				else
+				{
+					$sUrl = MetaModel::GetAttributeDef(get_class($oContact), 'picture')->Get('default_image');
+				}
+			}
+		}
+		$oApp['combodo.current_user_img'] = $sUrl;
 	}
 
 	/**

+ 1 - 2
datamodels/2.x/itop-portal-base/portal/src/views/layout.html.twig

@@ -5,8 +5,7 @@
 	{% set bUserConnected = true %}
 	{% set sUserFullname = app['combodo.current_user'].Get('first_name') ~ ' ' ~ app['combodo.current_user'].Get('last_name') %}
 	{% set sUserEmail = app['combodo.current_user'].Get('email') %}
-	{% set sUserPhotoUrl = app['combodo.portal.base.absolute_url'] ~ 'img/user-profile-default-256px.png' %}
-	{% set sUserPhotoUrl = 'https://scontent-fra3-1.xx.fbcdn.net/v/t1.0-9/11050099_10153305298138954_7206181025917413544_n.jpg?oh=728b8e7b1f073b81a2e6b43858c795f8&oe=57E2B0D3' %}
+	{% set sUserPhotoUrl = app['combodo.current_user_img'] %}
 {% else %}
 	{% set bUserConnected = false %}
 	{% set sUserFullname = '' %}

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

@@ -1326,5 +1326,6 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.',
 	
 	'UI:ToggleFullScreen' => 'Toggle Maximize / Minimize',
+	'UI:Button:ResetImage' => 'Recover the previous image',
+	'UI:Button:RemoveImage' => 'Remove the image',
 ));
-?>

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

@@ -1168,4 +1168,6 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:NoInlineImage' => 'Il n\'y a aucune image de disponible sur le serveur. Utilisez le bouton "Parcourir" (ci-dessus) pour sélectionner une image sur votre ordinateur et la télécharger sur le serveur.',
 	
 	'UI:ToggleFullScreen' => 'Agrandir / Minimiser',
+	'UI:Button:ResetImage' => 'Récupérer l\'image initiale',
+	'UI:Button:RemoveImage' => 'Supprimer l\'image',
 ));

+ 153 - 0
js/edit_image.js

@@ -0,0 +1,153 @@
+// jQuery UI style "widget" for editing an image (file upload)
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// graph
+//
+$(function()
+{
+	// the widget definition, where "itop" is the namespace,
+	// "dashboard" the widget name
+	$.widget( "itop.edit_image",
+		{
+			// default options
+			options: {
+				input_name: '_image_input_',
+				max_file_size: 0,
+				max_width_px: 32,
+				max_height_px: 32,
+				current_image_url: '',
+				default_image_url: '',
+				labels: {
+					reset_button: 'Reset',
+					remove_button: 'Remove',
+					upload_button: 'Upload'
+				}
+			},
+
+			// the constructor
+			_create: function () {
+				var me = this;
+				me.bLoadedEmpty = (me.options.current_image_url == '');
+
+				var sMarkup = '';
+				sMarkup += '<input type="hidden" id="do_remove_' + me.options.input_name + '" name="' + me.options.input_name + '[remove]" value="0"/>';
+				sMarkup += '<div id="preview_' + me.options.input_name + '" class="view-image" style="width: ' + me.options.max_width_px + 'px; height: ' + me.options.max_height_px + 'px;">';
+				sMarkup += '<span class="helper-middle"></span>';
+				sMarkup += '<img src="' + me.options.current_image_url + '" data-original-src="' + me.options.current_image_url + '" data-default-src="' + me.options.default_image_url + '" style="max-width: ' + me.options.max_width_px + 'px; max-height: ' + me.options.max_height_px + 'px">';
+				sMarkup += '</div>';
+				sMarkup += '<div id="buttons_' + me.options.input_name + '" class="edit-buttons">';
+				sMarkup += '<div title="' + me.options.labels.reset_button + '" id="reset_' + me.options.input_name + '" class="button disabled"><div class="ui-icon ui-icon-arrowreturnthick-1-w"></div></div>';
+
+				var sDisabled = me.bLoadedEmpty ? 'disabled' : '';
+				var sLoadedDisabled = me.bLoadedEmpty ? 'yes' : 'no';
+				sMarkup += '<div title="' + me.options.labels.remove_button + '" id="remove_' + me.options.input_name + '" data-loaded-disabled="' + sLoadedDisabled + '" class="button ' + sDisabled + '"><div class="ui-icon ui-icon-trash"></div></div>';
+				sMarkup += '</div>';
+
+				sMarkup += '<input type="hidden" name="MAX_FILE_SIZE" value="'+me.options.max_file_size+'" />';
+				sMarkup += '<input class="file-input" title="' + me.options.labels.upload_button + '" name="' + me.options.input_name + '[fcontents]" type="file" id="file_' + me.options.input_name + '" />';
+
+				this.element
+					.addClass('edit-image')
+					.append(sMarkup);
+
+				$('#file_' + me.options.input_name).change(function () {
+
+					$('#do_remove_' + me.options.input_name).val('0');
+
+					me.previewImage(this, '#preview_' + me.options.input_name + ' img');
+
+					var oImage = $('#preview_' + me.options.input_name + ' img');
+					oImage.closest('.view-image').addClass('dirty');
+
+					$('#reset_' + me.options.input_name).removeClass('disabled');
+					$('#remove_' + me.options.input_name).removeClass('disabled');
+				});
+				$('#reset_' + me.options.input_name).click(function () {
+
+					if ($(this).hasClass('disabled')) return;
+
+					$('#do_remove_' + me.options.input_name).val('0');
+
+					// Restore the image
+					var oImage = $('#preview_' + me.options.input_name + ' img');
+					oImage.attr('src', oImage.attr('data-original-src'));
+					oImage.closest('.view-image').removeClass('dirty').removeClass('compat');
+
+					// Reset the file input without losing events bound to it
+					var oInput = $('#file_' + me.options.input_name);
+					oInput.replaceWith(oInput.val('').clone(true));
+
+					$('#reset_' + me.options.input_name).addClass('disabled');
+					var oRemoveBtn = $('#remove_' + me.options.input_name);
+					if (oRemoveBtn.attr('data-loaded-disabled') == 'yes') {
+						oRemoveBtn.addClass('disabled');
+					}
+					else {
+						oRemoveBtn.removeClass('disabled');
+					}
+				});
+				$('#remove_' + me.options.input_name).click(function () {
+
+					if ($(this).hasClass('disabled')) return;
+
+					$('#do_remove_' + me.options.input_name).val('1');
+
+					// Restore the default image
+					var oImage = $('#preview_' + me.options.input_name + ' img');
+					oImage.attr('src', oImage.attr('data-default-src'));
+					oImage.closest('.view-image')
+						.removeClass('compat')
+						.addClass('dirty');
+
+					// Reset the file input without losing events bound to it
+					var oInput = $('#file_' + me.options.input_name);
+					oInput.replaceWith(oInput.val('').clone(true));
+
+					var oRemoveBtn = $('#remove_' + me.options.input_name);
+					if (oRemoveBtn.attr('data-loaded-disabled') == 'yes') {
+						$('#reset_' + me.options.input_name).addClass('disabled');
+					}
+					else {
+						$('#reset_' + me.options.input_name).removeClass('disabled');
+					}
+					oRemoveBtn.addClass('disabled');
+				});
+			},
+			// called when created, and later when changing options
+			_refresh: function () {
+			},
+			// events bound via _bind are removed automatically
+			// revert other modifications here
+			_destroy: function () {
+				this.element.removeClass('edit-image');
+			},
+			// _setOptions is called with a hash of all options that are changing
+			_setOptions: function () {
+				this._superApply(arguments);
+			},
+			// _setOption is called for each individual option that is changing
+			_setOption: function (key, value) {
+				this._superApply(arguments);
+			},
+			previewImage: function (input, sImageSelector) {
+				if (input.files && input.files[0]) {
+					if (window.FileReader) {
+						var reader = new FileReader();
+
+						reader.onload = function (e) {
+							$(sImageSelector).attr('src', e.target.result);
+						}
+
+						reader.readAsDataURL(input.files[0]);
+					}
+					else {
+						$(sImageSelector).closest('.view-image').addClass('compat');
+					}
+				}
+				else {
+					$(sImageSelector).closest('.view-image').addClass('compat');
+				}
+			}
+	});
+});

+ 14 - 0
setup/compiler.class.inc.php

@@ -1158,6 +1158,20 @@ EOF
 					$aParameters['is_null_allowed'] = $this->GetPropBoolean($oField, 'is_null_allowed', false);
 					$aParameters['depends_on'] = $sDependencies;
 				}
+				elseif ($sAttType == 'AttributeImage')
+				{
+					$aParameters['is_null_allowed'] = $this->GetPropBoolean($oField, 'is_null_allowed', false);
+					$aParameters['depends_on'] = $sDependencies;
+					$aParameters['display_max_width'] = $this->GetPropNumber($oField, 'display_max_width', 128);
+					$aParameters['display_max_height'] = $this->GetPropNumber($oField, 'display_max_height', 128);
+					$aParameters['storage_max_width'] = $this->GetPropNumber($oField, 'storage_max_width', 256);
+					$aParameters['storage_max_height'] = $this->GetPropNumber($oField, 'storage_max_height', 256);
+
+					if (($sDefault = $oField->GetChildText('default_image')) && (strlen($sDefault) > 0))
+					{
+						$aParameters['default_image'] = "utils::GetAbsoluteUrlModulesRoot().'$sModuleRelativeDir/$sDefault'";
+					}
+				}
 				elseif ($sAttType == 'AttributeStopWatch')
 				{
 					$oStates = $oField->GetUniqueElement('states');

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません