Skip to content

Commit

Permalink
Fix #1919. Implemented WFS download plugin (#1942)
Browse files Browse the repository at this point in the history
Fix #1919. Implemented WFS download plugin

This plugin provides the functionalities to export data from WFS in multiple formats.
Can be configured specifying the list of allowed fomats.
 - Fix #1919 - Add formats to the plugin
 - Add query filterObj selector
 - If the WFSPlugin is missing, the feature grid will continue working as usual, waiting for the new implementation
  • Loading branch information
offtherailz authored Jun 21, 2017
1 parent fb784bf commit a790299
Show file tree
Hide file tree
Showing 22 changed files with 603 additions and 2 deletions.
1 change: 1 addition & 0 deletions docma-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
"web/client/plugins/Search.jsx",
"web/client/plugins/SearchServicesConfig.jsx",
"web/client/plugins/TOC.jsx",
"web/client/plugins/WFSDownload.jsx",
"web/client/plugins/ZoomIn.jsx",
"web/client/plugins/ZoomOut.jsx"
]
Expand Down
39 changes: 39 additions & 0 deletions web/client/actions/__tests__/wfsdownload-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

var expect = require('expect');
var {
DOWNLOAD_FEATURES,
DOWNLOAD_OPTIONS_CHANGE,
DOWNLOAD_FINISHED,
downloadFeatures,
onDownloadOptionChange,
onDownloadFinished
} = require('../wfsdownload');

describe('Test correctness of the wfsdownload actions', () => {
it('test downloadFeatures action', () => {
let {type, url, filterObj, downloadOptions} = downloadFeatures("url", "filterObj", "downloadOptions");
expect(type).toBe(DOWNLOAD_FEATURES);
expect(url).toBe("url");
expect(filterObj).toBe("filterObj");
expect(downloadOptions).toBe("downloadOptions");
});
it('test onDownloadOptionChange action', () => {
let {type, key, value} = onDownloadOptionChange("key", "value");
expect(type).toBe(DOWNLOAD_OPTIONS_CHANGE);
expect(key).toBe("key");
expect(value).toBe("value");
});
it('test onDownloadFinished action', () => {
let {type} = onDownloadFinished();
expect(type).toBe(DOWNLOAD_FINISHED);
});


});
57 changes: 57 additions & 0 deletions web/client/actions/wfsdownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

const DOWNLOAD_FEATURES = "WFSDOWNLOAD::DOWNLOAD_FEATURES";
const DOWNLOAD_FINISHED = "WFSDOWNLOAD::DOWNLOAD_FINISHED";
const DOWNLOAD_OPTIONS_CHANGE = "WFSDOWNLOAD::FORMAT_SELECTED";

/**
* Actions for WFS Download
* @memberof actions
* @name wfsdownload
* @type {Object}
*/
module.exports = {
DOWNLOAD_FEATURES,
DOWNLOAD_OPTIONS_CHANGE,
DOWNLOAD_FINISHED,
/**
* action to download features
* @memberof actions.wfsdownload
* @param {string} url the URL of WFSDownload
* @param {object} filterObj the object that represent the WFS filterObj
* @param {object} downloadOptions download options e.g. `{singlePage: true|false, selectedFormat: "csv"}`
* @return {action} The action of type `DOWNLOAD_FEATURES`
*/
downloadFeatures: (url, filterObj, downloadOptions) => ({
type: DOWNLOAD_FEATURES,
url,
filterObj,
downloadOptions
}),
/**
* action for change download options
* @memberof actions.wfsdownload
* @param {string} key the value key to change. e.g. selectedFormat
* @param {string|boolean} value the value of the option
* @return {action} the action of type `DOWNLOAD_OPTIONS_CHANGE`
*/
onDownloadOptionChange: (key, value) => ({
type: DOWNLOAD_OPTIONS_CHANGE,
key,
value
}),
/**
* action that notifies the end of the downloadOptions
* @memberof actions.wfsdownload
* @return {action} action of type `DOWNLOAD_FINISHED`
*/
onDownloadFinished: () => ({
type: DOWNLOAD_FINISHED
})
};
75 changes: 75 additions & 0 deletions web/client/components/data/download/DownloadDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const React = require('react');
const {Button, Glyphicon} = require('react-bootstrap');
const Spinner = require('react-spinkit');

const Dialog = require('../../misc/Dialog');
const Message = require('../../I18N/Message');
const DownloadOptions = require('./DownloadOptions');

const DownloadDialog = React.createClass({
propTypes: {
filterObj: React.PropTypes.object,
closeGlyph: React.PropTypes.string,
url: React.PropTypes.string,
onMount: React.PropTypes.func,
onUnmount: React.PropTypes.func,
enabled: React.PropTypes.bool,
loading: React.PropTypes.bool,
onClose: React.PropTypes.func,
onExport: React.PropTypes.func,
onDownloadOptionChange: React.PropTypes.func,
downloadOptions: React.PropTypes.object,
formats: React.PropTypes.array
},
getDefaultProps() {
return {
onMount: () => {},
onUnmount: () => {},
onExport: () => {},
onClose: () => {},
onDownloadOptionChange: () => {},
closeGlyph: "1-close",
formats: [
{name: "csv", label: "csv"},
{name: "shape-zip", label: "shape-zip"}
]
};
},
componentDidMount() {
this.props.onMount();
},
componentWillUnmount() {
this.props.onUnmount();
},
onClose() {
this.props.onClose();
},
renderIcon() {
return this.props.loading ? <div style={{"float": "left"}}><Spinner spinnerName="circle" noFadeIn/></div> : <Glyphicon glyph="download" />;
},
render() {
return (<Dialog id="mapstore-export" style={{display: this.props.enabled ? "block" : "none"}}>
<span role="header">
<span className="about-panel-title"><Message msgId="wfsdownload.title" /></span>
<button onClick={this.props.onClose} className="settings-panel-close close">{this.props.closeGlyph ? <Glyphicon glyph={this.props.closeGlyph}/> : <span>×</span>}</button>
</span>
<div role="body">
<DownloadOptions
downloadOptions={this.props.downloadOptions}
onChange={this.props.onDownloadOptionChange}
formats={this.props.formats}/>
</div>
<div role="footer">
<Button
bsStyle="primary"
className="download-button"
disabled={!this.props.downloadOptions.selectedFormat || this.props.loading}
onClick={() => this.props.onExport(this.props.url, this.props.filterObj, this.props.downloadOptions)}>
{this.renderIcon()} <Message msgId="wfsdownload.export" />
</Button>
</div>
</Dialog>);
}
});

module.exports = DownloadDialog;
49 changes: 49 additions & 0 deletions web/client/components/data/download/DownloadOptions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const Select = require('react-select');
const {Checkbox} = require('react-bootstrap');
const {get, head} = require('lodash');
const Message = require('../../I18N/Message');
/**
* Download Options Form. Shows a selector of the options to perform a WFS download
* @memberof components.data.download
* @name DownloadOptions
* @class
* @prop {object} downloadOptions the options to set. e.g. `{singlePage: true|false, selectedFormat: "csv"}`
* @prop {array} formats the selectable format options.
* @prop {function} onChange the function to trigger when some option changes
*/
module.exports = React.createClass({
propTypes: {
downloadOptions: React.PropTypes.object,
formats: React.PropTypes.array,
onChange: React.PropTypes.func
},
getSelectedFormat() {
return get(this.props, "downloadOptions.selectedFormat") || get(head(this.props.formats), "value");
},
getDefaultProps() {
return {
downloadOptions: {}
};
},
render() {
return (<form>
<label><Message msgId="wfsdownload.format" /></label>
<Select
clearable={false}
value={this.getSelectedFormat()}
onChange={(sel) => this.props.onChange("selectedFormat", sel.value)}
options={this.props.formats.map(f => ({value: f.name, label: f.label || f.name}))} />
<Checkbox checked={this.props.downloadOptions.singlePage} onChange={() => this.props.onChange("singlePage", !this.props.downloadOptions.singlePage ) }>
<Message msgId="wfsdownload.downloadonlycurrentpage" />
</Checkbox>
</form>);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
var React = require('react');
var ReactDOM = require('react-dom');
var DownloadDialog = require('../DownloadDialog');
var expect = require('expect');
const spyOn = expect.spyOn;
const TestUtils = require('react-addons-test-utils');

describe('Test for DownloadOptions component', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});
// test DEFAULTS
it('render with defaults', () => {
const cmp = ReactDOM.render(<DownloadDialog/>, document.getElementById("container"));
expect(cmp).toExist();
});
it('render with enabled', () => {
const cmp = ReactDOM.render(<DownloadDialog enabled={true} formats={[{name: "test"}]}/>, document.getElementById("container"));
expect(cmp).toExist();
expect(TestUtils.scryRenderedDOMComponentsWithClass(cmp, "Select-value-label")).toExist();
});
it('export event', () => {
const events = {
onExport: () => {}
};
spyOn(events, "onExport");
ReactDOM.render(<DownloadDialog onExport={events.onExport} downloadOptions={{selectedFormat: "test"}} formats={[{name: "test"}]}/>, document.getElementById("container"));
const btn = document.querySelector('button.download-button');
btn.click();
expect(events.onExport).toHaveBeenCalled();

});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
var React = require('react');
var ReactDOM = require('react-dom');
var DownloadOptions = require('../DownloadOptions');
var expect = require('expect');
const spyOn = expect.spyOn;
const TestUtils = require('react-addons-test-utils');

describe('Test for DownloadOptions component', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});
// test DEFAULTS
it('render with defaults', () => {
const cmp = ReactDOM.render(<DownloadOptions/>, document.getElementById("container"));
expect(cmp).toExist();
});
it('render with element selected', () => {
const cmp = ReactDOM.render(<DownloadOptions downloadOptions={{selectedFormat: "test"}} formats={[{name: "test"}]}/>, document.getElementById("container"));
expect(cmp).toExist();
expect(TestUtils.scryRenderedDOMComponentsWithClass(cmp, "Select-value-label")).toExist();
});
it('singlePage checkbox events', () => {
const events = {
onChange: () => {}
};
spyOn(events, "onChange");
ReactDOM.render(<DownloadOptions onChange={events.onChange} downloadOptions={{selectedFormat: "test"}} formats={[{name: "test"}]}/>, document.getElementById("container"));
const check = document.querySelector('input[type=checkbox]');
check.click();
expect(events.onChange).toHaveBeenCalled();

});
});
5 changes: 4 additions & 1 deletion web/client/components/data/featuregrid/DockedFeatureGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ const DockedFeatureGrid = React.createClass({
dockSize: React.PropTypes.number,
minDockSize: React.PropTypes.number,
maxDockSize: React.PropTypes.number,
setDockSize: React.PropTypes.func
setDockSize: React.PropTypes.func,
exportAction: React.PropTypes.func,
exportEnabled: React.PropTypes.bool
},
contextTypes: {
messages: React.PropTypes.object
Expand Down Expand Up @@ -251,6 +253,7 @@ const DockedFeatureGrid = React.createClass({
height: "100%"
}}>
<FeatureGrid
exportAction={this.props.exportEnabled && this.props.exportAction}
useIcons={true}
tools={[<Button onClick={this.props.onBackToSearch} ><Glyphicon glyph="arrow-left" /><I18N.Message msgId="featuregrid.backtosearch"/></Button>]}
key={"search-results-" + (this.state && this.state.searchN)}
Expand Down
Empty file.
Loading

0 comments on commit a790299

Please sign in to comment.