Merge pull request #962 from nasa/csv-export-update-751

[Timeline] Updates to CSV Export
This commit is contained in:
Andrew Henry 2016-05-27 14:55:59 -07:00
commit 8f9308de01
15 changed files with 196 additions and 45 deletions

View File

@ -91,7 +91,12 @@ define([
"name": "Export Timeline as CSV",
"category": "contextual",
"implementation": ExportTimelineAsCSVAction,
"depends": ["exportService", "notificationService"]
"depends": [
"$log",
"exportService",
"notificationService",
"resources[]"
]
}
],
"constants": [

View File

@ -27,11 +27,15 @@ define([], function () {
* in a domain object's composition.
* @param {number} index the zero-based index of the composition
* element associated with this column
* @param idMap an object containing key value pairs, where keys
* are domain object identifiers and values are whatever
* should appear in CSV output in their place
* @constructor
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function CompositionColumn(index) {
function CompositionColumn(index, idMap) {
this.index = index;
this.idMap = idMap;
}
CompositionColumn.prototype.name = function () {
@ -41,7 +45,9 @@ define([], function () {
CompositionColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(),
composition = model.composition || [];
return (composition[this.index]) || "";
return composition.length > this.index ?
this.idMap[composition[this.index]] : "";
};
return CompositionColumn;

View File

@ -27,14 +27,23 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) {
*
* @param exportService the service used to perform the CSV export
* @param notificationService the service used to show notifications
* @param {Array} resources an array of `resources` extensions
* @param context the Action's context
* @implements {Action}
* @constructor
* @memberof {platform/features/timeline}
*/
function ExportTimelineAsCSVAction(exportService, notificationService, context) {
function ExportTimelineAsCSVAction(
$log,
exportService,
notificationService,
resources,
context
) {
this.$log = $log;
this.task = new ExportTimelineAsCSVTask(
exportService,
resources,
context.domainObject
);
this.notificationService = notificationService;
@ -45,13 +54,15 @@ define(["./ExportTimelineAsCSVTask"], function (ExportTimelineAsCSVTask) {
notification = notificationService.notify({
title: "Exporting CSV",
unknownProgress: true
});
}),
$log = this.$log;
return this.task.run()
.then(function () {
notification.dismiss();
})
.catch(function () {
.catch(function (err) {
$log.warn(err);
notification.dismiss();
notificationService.error("Error exporting CSV");
});

View File

@ -35,11 +35,13 @@ define([
* @constructor
* @memberof {platform/features/timeline}
* @param exportService the service used to export as CSV
* @param resources the `resources` extension category
* @param {DomainObject} domainObject the timeline being exported
*/
function ExportTimelineAsCSVTask(exportService, domainObject) {
function ExportTimelineAsCSVTask(exportService, resources, domainObject) {
this.domainObject = domainObject;
this.exportService = exportService;
this.resources = resources;
}
/**
@ -50,9 +52,10 @@ define([
*/
ExportTimelineAsCSVTask.prototype.run = function () {
var exportService = this.exportService;
var resources = this.resources;
function doExport(objects) {
var exporter = new TimelineColumnizer(objects),
var exporter = new TimelineColumnizer(objects, resources),
options = { headers: exporter.headers() };
return exporter.rows().then(function (rows) {
return exportService.exportCSV(rows, options);

View File

@ -23,19 +23,23 @@
define([], function () {
/**
* A column showing domain object identifiers.
* A column showing identifying domain objects.
* @constructor
* @param idMap an object containing key value pairs, where keys
* are domain object identifiers and values are whatever
* should appear in CSV output in their place
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function IdColumn() {
function IdColumn(idMap) {
this.idMap = idMap;
}
IdColumn.prototype.name = function () {
return "Identifier";
return "Index";
};
IdColumn.prototype.value = function (domainObject) {
return domainObject.getId();
return this.idMap[domainObject.getId()];
};
return IdColumn;

View File

@ -27,10 +27,14 @@ define([], function () {
* @constructor
* @param {number} index the zero-based index of the composition
* element associated with this column
* @param idMap an object containing key value pairs, where keys
* are domain object identifiers and values are whatever
* should appear in CSV output in their place
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function ModeColumn(index) {
function ModeColumn(index, idMap) {
this.index = index;
this.idMap = idMap;
}
ModeColumn.prototype.name = function () {
@ -39,8 +43,9 @@ define([], function () {
ModeColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(),
composition = (model.relationships || {}).modes || [];
return (composition[this.index]) || "";
modes = (model.relationships || {}).modes || [];
return modes.length > this.index ?
this.idMap[modes[this.index]] : "";
};
return ModeColumn;

View File

@ -25,13 +25,15 @@ define([
"./ModeColumn",
"./CompositionColumn",
"./MetadataColumn",
"./TimespanColumn"
"./TimespanColumn",
"./UtilizationColumn"
], function (
IdColumn,
ModeColumn,
CompositionColumn,
MetadataColumn,
TimespanColumn
TimespanColumn,
UtilizationColumn
) {
/**
@ -63,15 +65,17 @@ define([
*
* @param {DomainObject[]} domainObjects the objects to include
* in the exported data
* @param {Array} resources an array of `resources` extensions
* @constructor
* @memberof {platform/features/timeline}
*/
function TimelineColumnizer(domainObjects) {
function TimelineColumnizer(domainObjects, resources) {
var maxComposition = 0,
maxRelationships = 0,
columnNames = {},
columns = [],
foundTimespan = false,
idMap,
i;
function addMetadataProperty(property) {
@ -82,7 +86,12 @@ define([
}
}
columns.push(new IdColumn());
idMap = domainObjects.reduce(function (map, domainObject, index) {
map[domainObject.getId()] = index + 1;
return map;
}, {});
columns.push(new IdColumn(idMap));
domainObjects.forEach(function (domainObject) {
var model = domainObject.getModel(),
@ -113,12 +122,16 @@ define([
columns.push(new TimespanColumn(false));
}
resources.forEach(function (resource) {
columns.push(new UtilizationColumn(resource));
});
for (i = 0; i < maxComposition; i += 1) {
columns.push(new CompositionColumn(i));
columns.push(new CompositionColumn(i, idMap));
}
for (i = 0; i < maxRelationships; i += 1) {
columns.push(new ModeColumn(i));
columns.push(new ModeColumn(i, idMap));
}
this.domainObjects = domainObjects;

View File

@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2009-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* A column showing utilization costs associated with activities.
* @constructor
* @param {string} key the key for the particular cost
* @implements {platform/features/timeline.TimelineCSVColumn}
*/
function UtilizationColumn(resource) {
this.resource = resource;
}
UtilizationColumn.prototype.name = function () {
var units = {
"Kbps": "Kb",
"watts": "watt-seconds"
}[this.resource.units] || "unknown units";
return this.resource.name + " (" + units + ")";
};
UtilizationColumn.prototype.value = function (domainObject) {
var resource = this.resource;
function getCost(utilization) {
var seconds = (utilization.end - utilization.start) / 1000;
return seconds * utilization.value;
}
function getUtilizationValue(utilizations) {
utilizations = utilizations.filter(function (utilization) {
return utilization.key === resource.key;
});
if (utilizations.length === 0) {
return "";
}
return utilizations.map(getCost).reduce(function (a, b) {
return a + b;
}, 0);
}
return domainObject.hasCapability('utilization') ?
domainObject.getCapability('utilization').internal()
.then(getUtilizationValue) :
"";
};
return UtilizationColumn;
});

View File

@ -193,6 +193,13 @@ define(
* @returns {Promise.<string[]>} a promise for resource identifiers
*/
resources: promiseResourceKeys,
/**
* Get the resource utilization associated with this object
* directly, not including any resource utilization associated
* with contained objects.
* @returns {Promise.<Array>}
*/
internal: promiseInternalUtilization,
/**
* Get the resource utilization associated with this
* object. Results are not sorted. This requires looking

View File

@ -23,13 +23,20 @@
define(
['../../src/actions/CompositionColumn'],
function (CompositionColumn) {
var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f'];
describe("CompositionColumn", function () {
var testIndex,
testIdMap,
column;
beforeEach(function () {
testIndex = 3;
column = new CompositionColumn(testIndex);
testIdMap = TEST_IDS.reduce(function (map, id, index) {
map[id] = index;
return map;
}, {});
column = new CompositionColumn(testIndex, testIdMap);
});
it("includes a one-based index in its name", function () {
@ -46,15 +53,13 @@ define(
'domainObject',
['getId', 'getModel', 'getCapability']
);
testModel = {
composition: ['a', 'b', 'c', 'd', 'e', 'f']
};
testModel = { composition: TEST_IDS };
mockDomainObject.getModel.andReturn(testModel);
});
it("returns a corresponding identifier", function () {
it("returns a corresponding value from the map", function () {
expect(column.value(mockDomainObject))
.toEqual(testModel.composition[testIndex]);
.toEqual(testIdMap[testModel.composition[testIndex]]);
});
it("returns nothing when composition is exceeded", function () {

View File

@ -24,7 +24,8 @@ define(
['../../src/actions/ExportTimelineAsCSVAction'],
function (ExportTimelineAsCSVAction) {
describe("ExportTimelineAsCSVAction", function () {
var mockExportService,
var mockLog,
mockExportService,
mockNotificationService,
mockNotification,
mockDomainObject,
@ -39,6 +40,13 @@ define(
['getId', 'getModel', 'getCapability', 'hasCapability']
);
mockType = jasmine.createSpyObj('type', ['instanceOf']);
mockLog = jasmine.createSpyObj('$log', [
'warn',
'error',
'info',
'debug'
]);
mockExportService = jasmine.createSpyObj(
'exportService',
['exportCSV']
@ -63,8 +71,10 @@ define(
testContext = { domainObject: mockDomainObject };
action = new ExportTimelineAsCSVAction(
mockLog,
mockExportService,
mockNotificationService,
[],
testContext
);
});
@ -129,8 +139,11 @@ define(
});
describe("and an error occurs", function () {
var testError;
beforeEach(function () {
testPromise.reject();
testError = { someProperty: "some value" };
testPromise.reject(testError);
waitsFor(function () {
return mockCallback.calls.length > 0;
});
@ -145,6 +158,10 @@ define(
expect(mockNotificationService.error)
.toHaveBeenCalledWith(jasmine.any(String));
});
it("logs the root cause", function () {
expect(mockLog.warn).toHaveBeenCalledWith(testError);
});
});
});
});

View File

@ -52,6 +52,7 @@ define(
task = new ExportTimelineAsCSVTask(
mockExportService,
[],
mockDomainObject
);
});

View File

@ -24,10 +24,12 @@ define(
['../../src/actions/IdColumn'],
function (IdColumn) {
describe("IdColumn", function () {
var column;
var testIdMap,
column;
beforeEach(function () {
column = new IdColumn();
testIdMap = { "foo": "bar" };
column = new IdColumn(testIdMap);
});
it("has a name", function () {
@ -47,9 +49,9 @@ define(
mockDomainObject.getId.andReturn(testId);
});
it("provides a domain object's identifier", function () {
it("provides a value mapped from domain object's identifier", function () {
expect(column.value(mockDomainObject))
.toEqual(testId);
.toEqual(testIdMap[testId]);
});
});

View File

@ -23,13 +23,20 @@
define(
['../../src/actions/ModeColumn'],
function (ModeColumn) {
var TEST_IDS = ['a', 'b', 'c', 'd', 'e', 'f'];
describe("ModeColumn", function () {
var testIndex,
testIdMap,
column;
beforeEach(function () {
testIndex = 3;
column = new ModeColumn(testIndex);
testIdMap = TEST_IDS.reduce(function (map, id, index) {
map[id] = index;
return map;
}, {});
column = new ModeColumn(testIndex, testIdMap);
});
it("includes a one-based index in its name", function () {
@ -48,15 +55,15 @@ define(
);
testModel = {
relationships: {
modes: ['a', 'b', 'c', 'd', 'e', 'f']
modes: TEST_IDS
}
};
mockDomainObject.getModel.andReturn(testModel);
});
it("returns a corresponding identifier", function () {
it("returns a corresponding value from the map", function () {
expect(column.value(mockDomainObject))
.toEqual(testModel.relationships.modes[testIndex]);
.toEqual(testIdMap[testModel.relationships.modes[testIndex]]);
});
it("returns nothing when relationships are exceeded", function () {

View File

@ -75,7 +75,7 @@ define(
return c === 'metadata' && testMetadata;
});
exporter = new TimelineColumnizer(mockDomainObjects);
exporter = new TimelineColumnizer(mockDomainObjects, []);
});
describe("rows", function () {
@ -94,13 +94,6 @@ define(
it("include one row per domain object", function () {
expect(rows.length).toEqual(mockDomainObjects.length);
});
it("includes identifiers for each domain object", function () {
rows.forEach(function (row, index) {
var id = mockDomainObjects[index].getId();
expect(row.indexOf(id)).not.toEqual(-1);
});
});
});
describe("headers", function () {