ajax.csvimport.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. <?php
  2. // Copyright (C) 2010-2012 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Specific to the interactive csv import
  20. *
  21. * @copyright Copyright (C) 2010-2012 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. */
  24. require_once('../approot.inc.php');
  25. require_once(APPROOT.'/application/application.inc.php');
  26. require_once(APPROOT.'/application/webpage.class.inc.php');
  27. require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
  28. require_once(APPROOT.'/application/wizardhelper.class.inc.php');
  29. require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
  30. require_once(APPROOT.'/application/csvpage.class.inc.php');
  31. /**
  32. * Determines if the name of the field to be mapped correspond
  33. * to the name of an external key or an Id of the given class
  34. * @param string $sClassName The name of the class
  35. * @param string $sFieldCode The attribute code of the field , or empty if no match
  36. * @return bool true if the field corresponds to an id/External key, false otherwise
  37. */
  38. function IsIdField($sClassName, $sFieldCode)
  39. {
  40. $bResult = false;
  41. if (!empty($sFieldCode))
  42. {
  43. if ($sFieldCode == 'id')
  44. {
  45. $bResult = true;
  46. }
  47. else if (strpos($sFieldCode, '->') === false)
  48. {
  49. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sFieldCode);
  50. $bResult = $oAttDef->IsExternalKey();
  51. }
  52. }
  53. return $bResult;
  54. }
  55. /**
  56. * Get all the fields xxx->yyy based on the field xxx which is an external key
  57. * @param string $sExtKeyAttCode Attribute code of the external key
  58. * @param AttributeDefinition $oExtKeyAttDef Attribute definition of the external key
  59. * @param bool $bAdvanced True if advanced mode
  60. * @return Ash List of codes=>display name: xxx->yyy where yyy are the reconciliation keys for the object xxx
  61. */
  62. function GetMappingsForExtKey($sAttCode, AttributeDefinition $oExtKeyAttDef, $bAdvanced)
  63. {
  64. $aResult = array();
  65. $sTargetClass = $oExtKeyAttDef->GetTargetClass();
  66. foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sTargetAttCode => $oTargetAttDef)
  67. {
  68. if (MetaModel::IsReconcKey($sTargetClass, $sTargetAttCode))
  69. {
  70. $bExtKey = $oTargetAttDef->IsExternalKey();
  71. $sSuffix = '';
  72. if ($bExtKey)
  73. {
  74. $sSuffix = '->id';
  75. }
  76. if ($bAdvanced || !$bExtKey)
  77. {
  78. // When not in advanced mode do not allow to use reconciliation keys (on external keys) if they are themselves external keys !
  79. $aResult[$sAttCode.'->'.$sTargetAttCode] = $oExtKeyAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel().$sSuffix;
  80. }
  81. }
  82. }
  83. return $aResult;
  84. }
  85. /**
  86. * Helper function to build the mapping drop-down list for a field
  87. * Spec: Possible choices are "writable" fields in this class plus external fields that are listed as reconciliation keys
  88. * for any class pointed to by an external key in the current class.
  89. * If not in advanced mode, all "id" fields (id and external keys) must be mapped to ":none:" (i.e -- ignore this field --)
  90. * External fields that do not correspond to a reconciliation key must be mapped to ":none:"
  91. * Otherwise, if a field equals either the 'code' or the 'label' (translated) of a field, then it's mapped automatically
  92. * @param string $sClassName Name of the class used for the mapping
  93. * @param string $sFieldName Name of the field, as it comes from the data file (header line)
  94. * @param integer $iFieldIndex Number of the field in the sequence
  95. * @param bool $bAdvancedMode Whether or not advanced mode was chosen
  96. * @param string $sDefaultChoice If set, this will be the item selected by default
  97. * @return string The HTML code corresponding to the drop-down list for this field
  98. */
  99. function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMode, $sDefaultChoice)
  100. {
  101. $aChoices = array('' => Dict::S('UI:CSVImport:MappingSelectOne'));
  102. $aChoices[':none:'] = Dict::S('UI:CSVImport:MappingNotApplicable');
  103. $sFieldCode = ''; // Code of the attribute, if there is a match
  104. $aMatches = array();
  105. if (preg_match('/^(.+)\*$/', $sFieldName, $aMatches))
  106. {
  107. // Remove any trailing "star" character.
  108. // A star character at the end can be used to indicate a mandatory field
  109. $sFieldName = $aMatches[1];
  110. }
  111. else if (preg_match('/^(.+)\*->(.+)$/', $sFieldName, $aMatches))
  112. {
  113. // Remove any trailing "star" character before the arrow (->)
  114. // A star character at the end can be used to indicate a mandatory field
  115. $sFieldName = $aMatches[1].'->'.$aMatches[2];
  116. }
  117. if (($sFieldName == 'id') || ($sFieldName == Dict::S('UI:CSVImport:idField')))
  118. {
  119. $sFieldCode = 'id';
  120. }
  121. if ($bAdvancedMode)
  122. {
  123. $aChoices['id'] = Dict::S('UI:CSVImport:idField');
  124. }
  125. foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
  126. {
  127. $sStar = '';
  128. if ($oAttDef->IsExternalKey())
  129. {
  130. if (($sFieldName == $oAttDef->GetLabel()) || ($sFieldName == $sAttCode))
  131. {
  132. $sFieldCode = $sAttCode;
  133. }
  134. if ($bAdvancedMode)
  135. {
  136. $aChoices[$sAttCode] = $oAttDef->GetLabel();
  137. }
  138. $oExtKeyAttDef = MetaModel::GetAttributeDef($sClassName, $oAttDef->GetKeyAttCode());
  139. if (!$oExtKeyAttDef->IsNullAllowed())
  140. {
  141. $sStar = '*';
  142. }
  143. // Get fields of the external class that are considered as reconciliation keys
  144. $sTargetClass = $oAttDef->GetTargetClass();
  145. foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sTargetAttCode => $oTargetAttDef)
  146. {
  147. // Note: Could not use "MetaModel::GetFriendlyNameAttributeCode($sTargetClass) === $sTargetAttCode" as it would return empty because the friendlyname is composite.
  148. if (MetaModel::IsReconcKey($sTargetClass, $sTargetAttCode) || ($oTargetAttDef instanceof AttributeFriendlyName))
  149. {
  150. $bExtKey = $oTargetAttDef->IsExternalKey();
  151. $aSignatures = array();
  152. $aSignatures[] = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel();
  153. $aSignatures[] = $sAttCode.'->'.$sTargetAttCode;
  154. if ($bExtKey)
  155. {
  156. $aSignatures[] = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel().'->id';
  157. $aSignatures[] = $sAttCode.'->'.$sTargetAttCode.'->id';
  158. }
  159. if ($bAdvancedMode || !$bExtKey)
  160. {
  161. // When not in advanced mode do not allow to use reconciliation keys (on external keys) if they are themselves external keys !
  162. $aChoices[$sAttCode.'->'.$sTargetAttCode] = MetaModel::GetLabel($sClassName, $sAttCode.'->'.$sTargetAttCode, true);
  163. foreach ($aSignatures as $sSignature)
  164. {
  165. if (strcasecmp($sFieldName, $sSignature) == 0)
  166. {
  167. $sFieldCode = $sAttCode.'->'.$sTargetAttCode;
  168. }
  169. }
  170. }
  171. }
  172. }
  173. }
  174. else if (
  175. ($oAttDef->IsWritable() && (!$oAttDef->IsLinkset() || ($bAdvancedMode && $oAttDef->IsIndirect())))
  176. || ($oAttDef instanceof AttributeFriendlyName)
  177. )
  178. {
  179. $aChoices[$sAttCode] = MetaModel::GetLabel($sClassName, $sAttCode, true);
  180. if ( ($sFieldName == $oAttDef->GetLabel()) || ($sFieldName == $sAttCode))
  181. {
  182. $sFieldCode = $sAttCode;
  183. }
  184. }
  185. }
  186. asort($aChoices);
  187. $sHtml = "<select id=\"mapping_{$iFieldIndex}\" name=\"field[$iFieldIndex]\">\n";
  188. $bIsIdField = IsIdField($sClassName, $sFieldCode);
  189. foreach($aChoices as $sAttCode => $sLabel)
  190. {
  191. $sSelected = '';
  192. if ($bIsIdField && (!$bAdvancedMode)) // When not in advanced mode, ID are mapped to n/a
  193. {
  194. if ($sAttCode == ':none:')
  195. {
  196. $sSelected = ' selected';
  197. }
  198. }
  199. else if (empty($sFieldCode) && (strpos($sFieldName, '->') !== false))
  200. {
  201. if ($sAttCode == ':none:')
  202. {
  203. $sSelected = ' selected';
  204. }
  205. }
  206. else if (is_null($sDefaultChoice) && ($sFieldCode == $sAttCode))
  207. {
  208. $sSelected = ' selected';
  209. }
  210. else if (!is_null($sDefaultChoice) && ($sDefaultChoice == $sAttCode))
  211. {
  212. $sSelected = ' selected';
  213. }
  214. $sHtml .= "<option value=\"$sAttCode\"$sSelected>$sLabel</option>\n";
  215. }
  216. $sHtml .= "</select>\n";
  217. return $sHtml;
  218. }
  219. try
  220. {
  221. require_once(APPROOT.'/application/startup.inc.php');
  222. require_once(APPROOT.'/application/loginwebpage.class.inc.php');
  223. LoginWebPage::DoLogin(); // Check user rights and prompt if needed
  224. $sOperation = utils::ReadParam('operation', '');
  225. switch($sOperation)
  226. {
  227. case 'parser_preview':
  228. $oPage = new ajax_page("");
  229. $oPage->no_cache();
  230. $oPage->SetContentType('text/html');
  231. $sSeparator = utils::ReadParam('separator', ',', false, 'raw_data');
  232. if ($sSeparator == 'tab') $sSeparator = "\t";
  233. $sTextQualifier = utils::ReadParam('qualifier', '"', false, 'raw_data');
  234. $iLinesToSkip = utils::ReadParam('do_skip_lines', 0);
  235. $bFirstLineAsHeader = utils::ReadParam('header_line', true);
  236. $sEncoding = utils::ReadParam('encoding', 'UTF-8');
  237. $sData = stripslashes(utils::ReadParam('csvdata', true, false, 'raw_data'));
  238. $oCSVParser = new CSVParser($sData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop'));
  239. $iMaxIndex= 10; // Display maximum 10 lines for the preview
  240. $aData = $oCSVParser->ToArray($iLinesToSkip, null, $iMaxIndex);
  241. $iTarget = count($aData);
  242. if ($iTarget == 0)
  243. {
  244. $oPage->p(Dict::S('UI:CSVImport:NoData'));
  245. }
  246. else
  247. {
  248. $sMaxLen = (strlen(''.$iTarget) < 3) ? 3 : strlen(''.$iTarget); // Pad line numbers to the appropriate number of chars, but at least 3
  249. $sFormat = '%0'.$sMaxLen.'d';
  250. $oPage->p("<h3>".Dict::S('UI:Title:DataPreview')."</h3>\n");
  251. $oPage->p("<div style=\"overflow-y:auto\" class=\"white\">\n");
  252. $oPage->add("<table cellspacing=\"0\" style=\"overflow-y:auto\">");
  253. $index = 1;
  254. foreach($aData as $aRow)
  255. {
  256. $sCSSClass = 'csv_row'.($index % 2);
  257. if ( ($bFirstLineAsHeader) && ($index == 1))
  258. {
  259. $oPage->add("<tr class=\"$sCSSClass\"><td style=\"border-left:#999 3px solid;padding-right:10px;padding-left:10px;\">".sprintf($sFormat, $index)."</td>");
  260. foreach ($aRow as $sCell)
  261. {
  262. $oPage->add('<th>'.htmlentities($sCell, ENT_QUOTES, 'UTF-8').'</th>');
  263. }
  264. $oPage->add("</tr>\n");
  265. $iNbCols = count($aRow);
  266. }
  267. else
  268. {
  269. if ($index == 1) $iNbCols = count($aRow);
  270. $oPage->add("<tr class=\"$sCSSClass\"><td style=\"border-left:#999 3px solid;padding-right:10px;padding-left:10px;\">".sprintf($sFormat, $index)."</td>");
  271. foreach ($aRow as $sCell)
  272. {
  273. $oPage->add('<td>'.htmlentities($sCell, ENT_QUOTES, 'UTF-8').'</td>');
  274. }
  275. $oPage->add("</tr>\n");
  276. }
  277. $index++;
  278. if ($index > $iMaxIndex) break;
  279. }
  280. $oPage->add("</table>\n");
  281. $oPage->add("</div>\n");
  282. if($iNbCols == 1)
  283. {
  284. $oPage->p('<img src="../images/error.png">&nbsp;'.Dict::S('UI:CSVImport:ErrorOnlyOneColumn'));
  285. }
  286. else
  287. {
  288. $oPage->p('&nbsp;');
  289. }
  290. }
  291. break;
  292. case 'display_mapping_form':
  293. $oPage = new ajax_page("");
  294. $oPage->no_cache();
  295. $oPage->SetContentType('text/html');
  296. $sSeparator = utils::ReadParam('separator', ',', false, 'raw_data');
  297. $sTextQualifier = utils::ReadParam('qualifier', '"', false, 'raw_data');
  298. $iLinesToSkip = utils::ReadParam('do_skip_lines', 0);
  299. $bFirstLineAsHeader = utils::ReadParam('header_line', false);
  300. $sData = stripslashes(utils::ReadParam('csvdata', '', false, 'raw_data'));
  301. $sClassName = utils::ReadParam('class_name', '');
  302. $bAdvanced = utils::ReadParam('advanced', false);
  303. $sEncoding = utils::ReadParam('encoding', 'UTF-8');
  304. $sInitFieldMapping = utils::ReadParam('init_field_mapping', '', false, 'raw_data');
  305. $sInitSearchField = utils::ReadParam('init_search_field', '', false, 'raw_data');
  306. $aInitFieldMapping = empty($sInitFieldMapping) ? array() : json_decode($sInitFieldMapping, true);
  307. $aInitSearchField = empty($sInitSearchField) ? array() : json_decode($sInitSearchField, true);
  308. $oCSVParser = new CSVParser($sData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop'));
  309. $aData = $oCSVParser->ToArray($iLinesToSkip, null, 3 /* Max: 1 header line + 2 lines of sample data */);
  310. $iTarget = count($aData);
  311. if ($iTarget == 0)
  312. {
  313. $oPage->p(Dict::S('UI:CSVImport:NoData'));
  314. }
  315. else
  316. {
  317. $oPage->add("<table>");
  318. $aFirstLine = $aData[0]; // Use the first row to determine the number of columns
  319. $iStartLine = 0;
  320. $iNbColumns = count($aFirstLine);
  321. if ($bFirstLineAsHeader)
  322. {
  323. $iStartLine = 1;
  324. foreach($aFirstLine as $sField)
  325. {
  326. $aHeader[] = $sField;
  327. }
  328. }
  329. else
  330. {
  331. // Build some conventional name for the fields: field1...fieldn
  332. $index= 1;
  333. foreach($aFirstLine as $sField)
  334. {
  335. $aHeader[] = Dict::Format('UI:CSVImport:FieldName', $index);
  336. $index++;
  337. }
  338. }
  339. $oPage->add("<table>\n");
  340. $oPage->add('<tr>');
  341. $oPage->add('<th>'.Dict::S('UI:CSVImport:HeaderFields').'</th><th>'.Dict::S('UI:CSVImport:HeaderMappings').'</th><th>&nbsp;</th><th>'.Dict::S('UI:CSVImport:HeaderSearch').'</th><th>'.Dict::S('UI:CSVImport:DataLine1').'</th><th>'.Dict::S('UI:CSVImport:DataLine2').'</th>');
  342. $oPage->add('</tr>');
  343. $index = 1;
  344. foreach($aHeader as $sField)
  345. {
  346. $sDefaultChoice = null;
  347. if (isset($aInitFieldMapping[$index]))
  348. {
  349. $sDefaultChoice = $aInitFieldMapping[$index];
  350. }
  351. $oPage->add('<tr>');
  352. $oPage->add("<th>$sField</th>");
  353. $oPage->add('<td>'.GetMappingForField($sClassName, $sField, $index, $bAdvanced, $sDefaultChoice).'</td>');
  354. $oPage->add('<td>&nbsp;</td>');
  355. $oPage->add('<td><input id="search_'.$index.'" type="checkbox" name="search_field['.$index.']" value="1" /></td>');
  356. $oPage->add('<td>'.(isset($aData[$iStartLine][$index-1]) ? htmlentities($aData[$iStartLine][$index-1], ENT_QUOTES, 'UTF-8') : '&nbsp;').'</td>');
  357. $oPage->add('<td>'.(isset($aData[$iStartLine+1][$index-1]) ? htmlentities($aData[$iStartLine+1][$index-1], ENT_QUOTES, 'UTF-8') : '&nbsp;').'</td>');
  358. $oPage->add('</tr>');
  359. $index++;
  360. }
  361. $oPage->add("</table>\n");
  362. if (empty($sInitSearchField))
  363. {
  364. // Propose a reconciliation scheme
  365. //
  366. $aReconciliationKeys = MetaModel::GetReconcKeys($sClassName);
  367. $aMoreReconciliationKeys = array(); // Store: key => void to automatically remove duplicates
  368. foreach($aReconciliationKeys as $sAttCode)
  369. {
  370. if (!MetaModel::IsValidAttCode($sClassName, $sAttCode)) continue;
  371. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCode);
  372. if ($oAttDef->IsExternalKey())
  373. {
  374. // An external key is specified as a reconciliation key: this means that all the reconciliation
  375. // keys of this class are proposed to identify the target object
  376. $aMoreReconciliationKeys = array_merge($aMoreReconciliationKeys, GetMappingsForExtKey($sAttCode, $oAttDef, $bAdvanced));
  377. }
  378. elseif($oAttDef->IsExternalField())
  379. {
  380. // An external field is specified as a reconciliation key, translate the field into a field on the target class
  381. // since external fields are not writable, and thus never appears in the mapping form
  382. $sKeyAttCode = $oAttDef->GetKeyAttCode();
  383. $sTargetAttCode = $oAttDef->GetExtAttCode();
  384. $aMoreReconciliationKeys[$sKeyAttCode.'->'.$sTargetAttCode] = '';
  385. }
  386. }
  387. $sDefaultKeys = '"'.implode('", "',array_merge($aReconciliationKeys, array_keys($aMoreReconciliationKeys))).'"';
  388. }
  389. else
  390. {
  391. // The reconciliation scheme is given (navigating back in the wizard)
  392. //
  393. $aDefaultKeys = array();
  394. foreach ($aInitSearchField as $iSearchField => $void)
  395. {
  396. $sAttCodeEx = $aInitFieldMapping[$iSearchField];
  397. $aDefaultKeys[] = $sAttCodeEx;
  398. }
  399. $sDefaultKeys = '"'.implode('", "', $aDefaultKeys).'"';
  400. }
  401. // Read only attributes (will be forced to "search")
  402. $aReadOnlyKeys = array();
  403. foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
  404. {
  405. if(!$oAttDef->IsWritable())
  406. {
  407. $aReadOnlyKeys[] = $sAttCode;
  408. }
  409. }
  410. $sReadOnlyKeys = '"'.implode('", "', $aReadOnlyKeys).'"';
  411. $oPage->add_ready_script(
  412. <<<EOF
  413. $('select[name^=field]').change( DoCheckMapping );
  414. aDefaultKeys = new Array($sDefaultKeys);
  415. aReadOnlyKeys = new Array($sReadOnlyKeys);
  416. DoCheckMapping();
  417. EOF
  418. );
  419. }
  420. break;
  421. case 'get_csv_template':
  422. $sClassName = utils::ReadParam('class_name');
  423. $sFormat = utils::ReadParam('format', 'csv');
  424. if (MetaModel::IsValidClass($sClassName))
  425. {
  426. $oSearch = new DBObjectSearch($sClassName);
  427. $oSearch->AddCondition('id', 0, '='); // Make sure we create an empty set
  428. $oSet = new CMDBObjectSet($oSearch);
  429. $sResult = cmdbAbstractObject::GetSetAsCSV($oSet, array('showMandatoryFields' => true));
  430. $sClassDisplayName = MetaModel::GetName($sClassName);
  431. $sDisposition = utils::ReadParam('disposition', 'inline');
  432. if ($sDisposition == 'attachment')
  433. {
  434. switch($sFormat)
  435. {
  436. case 'xlsx':
  437. $oPage = new ajax_page("");
  438. $oPage->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  439. $oPage->SetContentDisposition('attachment', $sClassDisplayName.'.xlsx');
  440. require_once(APPROOT.'/application/excelexporter.class.inc.php');
  441. $writer = new XLSXWriter();
  442. $writer->setAuthor(UserRights::GetUserFriendlyName());
  443. $aHeaders = array( 0 => explode(',', $sResult)); // comma is the default separator
  444. $writer->writeSheet($aHeaders, $sClassDisplayName, array());
  445. $oPage->add($writer->writeToString());
  446. break;
  447. case 'csv':
  448. default:
  449. $oPage = new CSVPage("");
  450. $oPage->add_header("Content-type: text/csv; charset=utf-8");
  451. $oPage->add_header("Content-disposition: attachment; filename=\"{$sClassDisplayName}.csv\"");
  452. $oPage->no_cache();
  453. $oPage->add($sResult);
  454. }
  455. }
  456. else
  457. {
  458. $oPage = new ajax_page("");
  459. $oPage->no_cache();
  460. $oPage->add('<p style="text-align:center">');
  461. $oPage->add('<div style="display:inline-block;margin:0.5em;"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&class_name='.$sClassName.'"><img border="0" src="../images/csv.png"><br/>'.$sClassDisplayName.'.csv</a></div>');
  462. $oPage->add('<div style="display:inline-block;margin:0.5em;"><a style="text-decoration:none" href="'.utils::GetAbsoluteUrlAppRoot().'pages/ajax.csvimport.php?operation=get_csv_template&disposition=attachment&format=xlsx&class_name='.$sClassName.'"><img border="0" src="../images/xlsx.png"><br/>'.$sClassDisplayName.'.xlsx</a></div>');
  463. $oPage->add('</p>');
  464. $oPage->add('<p><textarea rows="5" cols="100">'.$sResult.'</textarea></p>');
  465. }
  466. }
  467. else
  468. {
  469. $oPage = new ajax_page("Class $sClassName is not a valid class !");
  470. }
  471. break;
  472. }
  473. $oPage->output();
  474. }
  475. catch (Exception $e)
  476. {
  477. IssueLog::Error($e->getMessage());
  478. }
  479. ?>