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
15 changed files with 196 additions and 45 deletions

View File

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

View File

@@ -27,11 +27,15 @@ define([], function () {
* in a domain object's composition. * in a domain object's composition.
* @param {number} index the zero-based index of the composition * @param {number} index the zero-based index of the composition
* element associated with this column * 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 * @constructor
* @implements {platform/features/timeline.TimelineCSVColumn} * @implements {platform/features/timeline.TimelineCSVColumn}
*/ */
function CompositionColumn(index) { function CompositionColumn(index, idMap) {
this.index = index; this.index = index;
this.idMap = idMap;
} }
CompositionColumn.prototype.name = function () { CompositionColumn.prototype.name = function () {
@@ -41,7 +45,9 @@ define([], function () {
CompositionColumn.prototype.value = function (domainObject) { CompositionColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(), var model = domainObject.getModel(),
composition = model.composition || []; composition = model.composition || [];
return (composition[this.index]) || "";
return composition.length > this.index ?
this.idMap[composition[this.index]] : "";
}; };
return CompositionColumn; return CompositionColumn;

View File

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

View File

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

View File

@@ -23,19 +23,23 @@
define([], function () { define([], function () {
/** /**
* A column showing domain object identifiers. * A column showing identifying domain objects.
* @constructor * @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} * @implements {platform/features/timeline.TimelineCSVColumn}
*/ */
function IdColumn() { function IdColumn(idMap) {
this.idMap = idMap;
} }
IdColumn.prototype.name = function () { IdColumn.prototype.name = function () {
return "Identifier"; return "Index";
}; };
IdColumn.prototype.value = function (domainObject) { IdColumn.prototype.value = function (domainObject) {
return domainObject.getId(); return this.idMap[domainObject.getId()];
}; };
return IdColumn; return IdColumn;

View File

@@ -27,10 +27,14 @@ define([], function () {
* @constructor * @constructor
* @param {number} index the zero-based index of the composition * @param {number} index the zero-based index of the composition
* element associated with this column * 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} * @implements {platform/features/timeline.TimelineCSVColumn}
*/ */
function ModeColumn(index) { function ModeColumn(index, idMap) {
this.index = index; this.index = index;
this.idMap = idMap;
} }
ModeColumn.prototype.name = function () { ModeColumn.prototype.name = function () {
@@ -39,8 +43,9 @@ define([], function () {
ModeColumn.prototype.value = function (domainObject) { ModeColumn.prototype.value = function (domainObject) {
var model = domainObject.getModel(), var model = domainObject.getModel(),
composition = (model.relationships || {}).modes || []; modes = (model.relationships || {}).modes || [];
return (composition[this.index]) || ""; return modes.length > this.index ?
this.idMap[modes[this.index]] : "";
}; };
return ModeColumn; return ModeColumn;

View File

@@ -25,13 +25,15 @@ define([
"./ModeColumn", "./ModeColumn",
"./CompositionColumn", "./CompositionColumn",
"./MetadataColumn", "./MetadataColumn",
"./TimespanColumn" "./TimespanColumn",
"./UtilizationColumn"
], function ( ], function (
IdColumn, IdColumn,
ModeColumn, ModeColumn,
CompositionColumn, CompositionColumn,
MetadataColumn, MetadataColumn,
TimespanColumn TimespanColumn,
UtilizationColumn
) { ) {
/** /**
@@ -63,15 +65,17 @@ define([
* *
* @param {DomainObject[]} domainObjects the objects to include * @param {DomainObject[]} domainObjects the objects to include
* in the exported data * in the exported data
* @param {Array} resources an array of `resources` extensions
* @constructor * @constructor
* @memberof {platform/features/timeline} * @memberof {platform/features/timeline}
*/ */
function TimelineColumnizer(domainObjects) { function TimelineColumnizer(domainObjects, resources) {
var maxComposition = 0, var maxComposition = 0,
maxRelationships = 0, maxRelationships = 0,
columnNames = {}, columnNames = {},
columns = [], columns = [],
foundTimespan = false, foundTimespan = false,
idMap,
i; i;
function addMetadataProperty(property) { 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) { domainObjects.forEach(function (domainObject) {
var model = domainObject.getModel(), var model = domainObject.getModel(),
@@ -113,12 +122,16 @@ define([
columns.push(new TimespanColumn(false)); columns.push(new TimespanColumn(false));
} }
resources.forEach(function (resource) {
columns.push(new UtilizationColumn(resource));
});
for (i = 0; i < maxComposition; i += 1) { 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) { for (i = 0; i < maxRelationships; i += 1) {
columns.push(new ModeColumn(i)); columns.push(new ModeColumn(i, idMap));
} }
this.domainObjects = domainObjects; 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 * @returns {Promise.<string[]>} a promise for resource identifiers
*/ */
resources: promiseResourceKeys, 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 * Get the resource utilization associated with this
* object. Results are not sorted. This requires looking * object. Results are not sorted. This requires looking

View File

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

View File

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

View File

@@ -24,10 +24,12 @@ define(
['../../src/actions/IdColumn'], ['../../src/actions/IdColumn'],
function (IdColumn) { function (IdColumn) {
describe("IdColumn", function () { describe("IdColumn", function () {
var column; var testIdMap,
column;
beforeEach(function () { beforeEach(function () {
column = new IdColumn(); testIdMap = { "foo": "bar" };
column = new IdColumn(testIdMap);
}); });
it("has a name", function () { it("has a name", function () {
@@ -47,9 +49,9 @@ define(
mockDomainObject.getId.andReturn(testId); 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)) expect(column.value(mockDomainObject))
.toEqual(testId); .toEqual(testIdMap[testId]);
}); });
}); });

View File

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

View File

@@ -75,7 +75,7 @@ define(
return c === 'metadata' && testMetadata; return c === 'metadata' && testMetadata;
}); });
exporter = new TimelineColumnizer(mockDomainObjects); exporter = new TimelineColumnizer(mockDomainObjects, []);
}); });
describe("rows", function () { describe("rows", function () {
@@ -94,13 +94,6 @@ define(
it("include one row per domain object", function () { it("include one row per domain object", function () {
expect(rows.length).toEqual(mockDomainObjects.length); 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 () { describe("headers", function () {