### Description The existing implementation for exporting playout logs to a CSV file incorporates a very simplified CSV format. Some aspects of the complete [RFC](https://www.rfc-editor.org/rfc/rfc4180) are missing, such as escaping of quotes, and quoting of fields that contain certain characters. This is problematic for common office spreadsheet tools, and practically, anything else. Many radio stations rely on this functionality to work well for exporting playout data, for example, in order to compile data for reporting requirements. **This is a new feature**: The changes in this PR add quoting of fields containing a comma, as well as those containing a CR/LF. It also escapes quotes by doubling them. I'm not sure it makes CSVexport.js completely RFC 4180 compliant, but it is much closer than it was. **I have updated the documentation to reflect these changes**: I don't think there are any documentation changes necessary; this is probably expected behavior for anyone trying to use the CSV exporter. ### Testing Notes **What I did:** To validate this, I did a clean install of Debian, cloned from the official libretime repo, and applied the code as a patch to the installer. I then proceeded with the install and then loaded a database from a running system (so that I had some playout data to test with). I then performed the playout history export and examined the resulting CSV file and after seeing previously problematic fields properly quoted, was convinced it looked the way I expected. I loaded the csv file into Libreoffice Calc and did not see any errors. **How you can replicate my testing:** See "What I did" above, basically run the patch after cloning the installer from the repo. You could also apply the changes to a running system by applying the patch to the file: /usr/share/libretime/legacy/public/js/libs/CSVexport.js Be sure to clear your browser cache and do a hard reload of the web interface, before re-testing. ### **Links** Closes: #2477
115 lines
3.6 KiB
JavaScript
115 lines
3.6 KiB
JavaScript
/**
|
|
@namespace Converts JSON to CSV.
|
|
|
|
Compress with: http://jscompress.com/
|
|
*/
|
|
(function (window) {
|
|
"use strict";
|
|
/**
|
|
Default constructor
|
|
*/
|
|
var _CSV = function (JSONData) {
|
|
if (typeof JSONData === 'undefined')
|
|
return;
|
|
|
|
var csvData = typeof JSONData != 'object' ? JSON.parse(settings.JSONData) : JSONData,
|
|
csvHeaders,
|
|
csvEncoding = 'data:text/csv;charset=utf-8,',
|
|
csvOutput = "",
|
|
csvRows = [],
|
|
BREAK = '\r\n', // Use CRLF for line breaks
|
|
DELIMITER = ',',
|
|
FILENAME = "export.csv";
|
|
|
|
// Get and Write the headers
|
|
csvHeaders = Object.keys(csvData[0]);
|
|
csvOutput += csvHeaders.join(DELIMITER) + BREAK;
|
|
|
|
for (var i = 0; i < csvData.length; i++) {
|
|
var rowElements = [];
|
|
for (var k = 0; k < csvHeaders.length; k++) {
|
|
var cell = csvData[i][csvHeaders[k]];
|
|
if (typeof cell === 'string') {
|
|
if (cell.includes('"')) {
|
|
cell = '"' + cell.replace(/"/g, '""') + '"'; // Escape double quotes by doubling them
|
|
} else if (cell.includes(DELIMITER) || cell.includes('\r') || cell.includes('\n')) {
|
|
cell = '"' + cell + '"'; // Enclose in double quotes if it contains commas, CR, or LF
|
|
}
|
|
}
|
|
rowElements.push(cell);
|
|
} // Write the row array based on the headers
|
|
csvRows.push(rowElements.join(DELIMITER));
|
|
}
|
|
|
|
csvOutput += csvRows.join(BREAK);
|
|
|
|
// Initiate Download
|
|
var a = document.createElement("a");
|
|
|
|
if (navigator.msSaveBlob) { // IE10
|
|
navigator.msSaveBlob(new Blob([csvOutput], { type: "text/csv" }), FILENAME);
|
|
} else if ('download' in a) { //html5 A[download]
|
|
a.href = csvEncoding + encodeURIComponent(csvOutput);
|
|
a.download = FILENAME;
|
|
document.body.appendChild(a);
|
|
setTimeout(function() {
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}, 66);
|
|
} else if (document.execCommand) { // Other version of IE
|
|
var oWin = window.open("about:blank", "_blank");
|
|
oWin.document.write(csvOutput);
|
|
oWin.document.close();
|
|
oWin.document.execCommand('SaveAs', true, FILENAME);
|
|
oWin.close();
|
|
} else {
|
|
alert("Support for your specific browser hasn't been created yet, please check back later.");
|
|
}
|
|
};
|
|
|
|
window.CSVExport = _CSV;
|
|
|
|
})(window);
|
|
|
|
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
|
|
if (!Object.keys) {
|
|
Object.keys = (function() {
|
|
'use strict';
|
|
var hasOwnProperty = Object.prototype.hasOwnProperty,
|
|
hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
|
|
dontEnums = [
|
|
'toString',
|
|
'toLocaleString',
|
|
'valueOf',
|
|
'hasOwnProperty',
|
|
'isPrototypeOf',
|
|
'propertyIsEnumerable',
|
|
'constructor'
|
|
],
|
|
dontEnumsLength = dontEnums.length;
|
|
|
|
return function(obj) {
|
|
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
|
|
throw new TypeError('Object.keys called on non-object');
|
|
}
|
|
|
|
var result = [], prop, i;
|
|
|
|
for (prop in obj) {
|
|
if (hasOwnProperty.call(obj, prop)) {
|
|
result.push(prop);
|
|
}
|
|
}
|
|
|
|
if (hasDontEnumBug) {
|
|
for (i = 0; i < dontEnumsLength; i++) {
|
|
if (hasOwnProperty.call(obj, dontEnums[i])) {
|
|
result.push(dontEnums[i]);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}());
|
|
}
|