IEEE.org     |     IEEE Xplore Digital Library     |     IEEE Standards     |     IEEE Spectrum     |     More Sites

Commit 4aae4fa7 authored by jedjas12's avatar jedjas12
Browse files

Merge branch 'testingLeafletMap' of...

Merge branch 'testingLeafletMap' of github.com:my-conservation-life/my-conservation-life into testingLeafletMap

because I said so # the commit.
parents 77555244 d57643ae
......@@ -80,8 +80,8 @@ describe('GET assetTypes', () => {
await teardown();
});
it('gets all asset types', () => {
return request(app)
it('gets all asset types', async () => {
await request(app)
.get('/api/v1/assetTypes')
.expect(200)
.then((res) => {
......@@ -110,9 +110,10 @@ describe('POST assetPropTypes', () => {
await teardown();
});
it('gets all asset property types', () => {
return request(app)
it('gets all asset property types', async () => {
await request(app)
.post('/api/v1/assetPropTypes')
.send({'assetTypeID' : '1'})
.expect(200)
.then((res) => {
expect(res.body.rows).toEqual(
......@@ -122,7 +123,7 @@ describe('POST assetPropTypes', () => {
expect.objectContaining(EXPECTED_ASSET_TYPE3)
])
);
expect(res.body.rows).toHaveLength(3);
expect(res.body.rows).toHaveLength(1);
});
});
});
......@@ -140,9 +141,10 @@ describe('POST assetPropsByTypeID', () => {
await teardown();
});
it('gets all asset property types', () => {
return request(app)
it('gets all asset property types', async () => {
await request(app)
.post('/api/v1/assetPropsByTypeID')
.send({'assetTypeID' : '1'})
.expect(200)
.then((res) => {
expect(res.body.rows).toEqual(
......
......@@ -11,7 +11,7 @@ const DISTANCE_ENDPOINT = `${GEOMETRY_ENDPOINT}/distance`;
const POLYGON_ENDPOINT = `${GEOMETRY_ENDPOINT}/polygon`;
describe('GET assets/geometrySearch/envelope', () => {
describe('POST assets/geometrySearch/envelope', () => {
beforeAll(async () => {
jest.setTimeout(30000);
await setup();
......@@ -35,7 +35,7 @@ describe('GET assets/geometrySearch/envelope', () => {
});
await request(app)
.get(ENVELOPE_ENDPOINT + `?${envelopeQuery}`)
.post(ENVELOPE_ENDPOINT + `?${envelopeQuery}`)
.expect(200);
});
......@@ -51,7 +51,7 @@ describe('GET assets/geometrySearch/envelope', () => {
});
await request(app)
.get(ENVELOPE_ENDPOINT + `?${envelopeQuery}`)
.post(ENVELOPE_ENDPOINT + `?${envelopeQuery}`)
.expect(200)
.then((res) => {
expect(res.body).toHaveLength(1);
......@@ -62,7 +62,7 @@ describe('GET assets/geometrySearch/envelope', () => {
});
});
describe('GET assets/geometrySearch/distance', () => {
describe('POST assets/geometrySearch/distance', () => {
beforeAll(async () => {
jest.setTimeout(30000);
await setup();
......@@ -85,12 +85,12 @@ describe('GET assets/geometrySearch/distance', () => {
});
await request(app)
.get(DISTANCE_ENDPOINT + `?${distanceQuery}`)
.post(DISTANCE_ENDPOINT + `?${distanceQuery}`)
.expect(200);
});
});
describe('GET assets/geometrySearch/polygon', () => {
describe('POST assets/geometrySearch/polygon', () => {
// Helper function
let pack = (lat, lon) => { return { latitude: lat, longitude: lon}; };
......@@ -114,7 +114,7 @@ describe('GET assets/geometrySearch/polygon', () => {
const validBody = { coordinates : [pack('13.3', '33'), pack('12', '44'), pack('12', '3')]};
await request(app)
.get(POLYGON_ENDPOINT)
.post(POLYGON_ENDPOINT)
.send(validBody)
.expect(200);
});
......
......@@ -4,7 +4,7 @@ const { setup, teardown, loadSQL } = require('../setup');
const ENDPOINT = '/api/v1/assets/properties/temporalSearch';
describe('GET assets/properties/temporalSearch', () => {
describe('POST assets/properties/temporalSearch', () => {
const polygon_search = {
'geometry': {
......@@ -42,14 +42,14 @@ describe('GET assets/properties/temporalSearch', () => {
it('returns HTTP 200 with "Polygon" search', async () => {
await request(app)
.get(ENDPOINT)
.post(ENDPOINT)
.send(polygon_search)
.expect(200);
});
it('returns HTTP 200 with "Circle" search', async () => {
await request(app)
.get(ENDPOINT)
.post(ENDPOINT)
.send(circle_search)
.expect(200);
});
......
......@@ -10,4 +10,4 @@ const createTestAsset = async (latitude, longitude, projectId = 1) =>
[projectId, longitude, latitude]
);
module.exports = { createTestAsset };
\ No newline at end of file
module.exports = { createTestAsset };
......@@ -35,11 +35,7 @@ describe('assetDefinitions.controller.getAssetPropsByTypeID', () => {
let data;
beforeEach(() => {
req = {
'body': {
}
};
req = { valid : { assetTypeID: 1 } };
res = {
json: jest.fn()
......@@ -65,7 +61,7 @@ describe('assetDefinitions.controller.getAssetPropsTypes', () => {
let data;
beforeEach(() => {
req = {};
req = { valid : { assetTypeID: 1 } };
res = {
json: jest.fn()
......
......@@ -28,14 +28,22 @@ describe('assets.controller.find', () => {
it('accesses DB and sends JSON response when no project_id is provided', async () => {
await find(req, res, next);
expect(assetsDb.find).toHaveBeenCalledWith(undefined, undefined, undefined);
expect(assetsDb.find).toHaveBeenCalledWith(undefined, undefined, undefined, undefined);
expect(res.json).toHaveBeenCalledWith(EXPECTED_ASSETS);
});
it('accesses DB and sends JSON response when project_id is provided', async () => {
req.valid['project_id'] = 2;
await find(req, res, next);
expect(assetsDb.find).toHaveBeenCalledWith(undefined, 2, undefined);
expect(assetsDb.find).toHaveBeenCalledWith(undefined, 2, undefined, undefined);
expect(res.json).toHaveBeenCalledWith(EXPECTED_ASSETS);
});
it('accesses DB and sends JSON response when project_id is provided', async () => {
const valid_donor_codes = ['abc123', 'cc1211'];
req.valid['donor_code'] = valid_donor_codes;
await find(req, res, next);
expect(assetsDb.find).toHaveBeenCalledWith(undefined, undefined, undefined, valid_donor_codes);
expect(res.json).toHaveBeenCalledWith(EXPECTED_ASSETS);
});
......@@ -53,4 +61,4 @@ describe('assets.controller.find', () => {
expect(next).toHaveBeenCalledWith(DB_ERROR);
expect(res.json).not.toHaveBeenCalled();
});
});
\ No newline at end of file
});
......@@ -4,9 +4,10 @@ const find = async (req, res, next) => {
const sponsorId = req.valid.sponsor_id;
const projectId = req.valid.project_id;
const assetId = req.valid.asset_type_id;
const donorCodes = req.valid.donor_code;
try {
const assets = await assetsDb.find(sponsorId, projectId, assetId);
const assets = await assetsDb.find(sponsorId, projectId, assetId, donorCodes);
res.json(assets);
} catch (error) {
next(error);
......
......@@ -184,7 +184,6 @@ describe('assetDefinitions.db.storeCSV', () => {
it('finds all asset types in the database', async () => {
const actualRows = await findAssetTypes();
expect(actualRows).toEqual(rows);
expect(query).toHaveBeenCalledWith(expect.stringContaining('asset_type'));
});
});
......@@ -203,7 +202,7 @@ describe('assetDefinitions.db.storeCSV', () => {
it('finds all properties associated with the asset type ID given', async () => {
const actualRows = await findAssetPropTypes(assetTypeId);
expect(actualRows).toEqual(rows);
expect(actualRows.rows).toEqual(rows);
});
});
......
......@@ -29,6 +29,15 @@ describe('assets.db.find', () => {
expect(query.mock.calls[0][1]).toEqual([54]);
});
it('executes correct DB query when donor_code is specified', async () => {
const valid_donor_codes = ['abc123', 'xyz987'];
await find(undefined, undefined, undefined, valid_donor_codes);
expect(query).toHaveBeenCalledTimes(1);
expect(query.mock.calls[0][0]).toEqual(expect.stringContaining('AND donor_code = ANY'));
expect(query.mock.calls[0][1]).toEqual(expect.arrayContaining([valid_donor_codes]));
});
it('returns an array of asset rows', async () => {
rows = [
{ id: 1, project_id: 1, latitude: 2, longitude: 3 },
......@@ -46,4 +55,4 @@ describe('assets.db.find', () => {
await expect(find()).rejects.toThrow();
});
});
\ No newline at end of file
});
......@@ -46,7 +46,14 @@ const findAssetTypes = async () => {
*/
const findAssetPropTypes = async (assetTypeID) => {
let query = PROPERTIES_QUERY;
query = query + ' WHERE asset_type_id=$1 ORDER BY id';
query = query + `
WHERE
asset_type_id = $1
AND
is_private = false
ORDER BY
id
`;
const params = [assetTypeID];
......@@ -60,16 +67,20 @@ const findAssetPropTypes = async (assetTypeID) => {
const findAssetPropsByTypeID = async (assetTypeID) => {
let query = `
SELECT
asset.id as id, asset_property.value as value, asset_property.property_id as property_id
asset.id AS id,
ST_X(asset.location) AS longitude,
ST_Y(asset.location) AS latitude,
asset_property.value AS value,
asset_property.property_id AS property_id,
property.is_private AS is_private
FROM
asset
INNER JOIN
asset_property
ON
asset_property.asset_id=asset.id
asset
JOIN asset_property ON asset_property.asset_id = asset.id
JOIN property ON asset_property.property_id = property.id
WHERE
asset_type_id = $1
asset.asset_type_id = $1
AND
property.is_private = false
ORDER BY
property_id
`;
......@@ -280,6 +291,28 @@ const updateAssetProperty = async(client, assetId, propertyId, newValue) => {
return client.query(query, values);
};
/**
* Updates the location of an asset by encoding longitude and latitude coordinates
* @param {*} client Node Postgres client
* @param {Number} assetId ID of the asset whose location is to be added/updated
* @param {Number} longitude Longitude of the asset
* @param {Number} latitude Latitude of the asset
*/
const addLocation = async(client, assetId, longitude, latitude) => {
// Generate the SQL command
const query = `
UPDATE asset
SET location=ST_MakePoint($1, $2)
WHERE id=$3
`;
// Generate the values to subsitute into the SQL command
const values = [longitude, latitude, assetId];
// Execute the SQL command
return client.query(query, values);
};
/**
* Stores contents of CSV into the DB
*
......@@ -349,7 +382,7 @@ const storeCSV = async(assetTypeId, csvJson) => {
}
for (const propertyName in asset) {
if (propertyName !== 'asset_id') {
if (propertyName !== 'asset_id' && propertyName !== 'longitude' && propertyName !== 'latitude') {
// Throw an error if CSV contains a header that is not associated with the selected asset type
if (!(propertyName in properties)) {
throw 'The selected CSV file either contains an empty column, is missing a header, or contains a property that is not being tracked (' + propertyName + ')';
......@@ -365,19 +398,29 @@ const storeCSV = async(assetTypeId, csvJson) => {
// Throw an error if a row fails to contain a value for a property that is required
if (value === '' && propertyIsRequired) {
throw 'The selected CSV file is missing a required value (' + propertyName + ', ' + JSON.stringify(asset) + ')';
}
else if (assetProperties.length > 0) {
} else if (value === '') {
continue;
} else if (assetProperties.length > 0) {
if (assetProperties[0].value !== value) {
await addAssetPropertyToHistory(client, assetId, propertyId, value, date);
await updateAssetProperty(client, assetId, propertyId, value);
}
}
else {
} else {
await addAssetPropertyToHistory(client, assetId, propertyId, value, date);
await createAssetProperty(client, assetId, propertyId, value);
}
}
}
// Add location of asset to the DB
if (!('latitude' in properties) || !('longitude' in properties)) {
throw 'The selected CSV file is missing a latitude and/or longitude column';
} else {
let longitude = parseFloat(properties['longitude']);
let latitude = parseFloat(properties['latitude']);
await addLocation(client, assetId, longitude, latitude);
}
}
await utils.db.commitTransaction(client);
}
......
......@@ -8,7 +8,8 @@ const QUERY_FIND = `
asset_type.name AS asset_type,
asset_type.description AS asset_description,
ST_Y(asset.location) AS latitude,
ST_X(asset.location) AS longitude
ST_X(asset.location) AS longitude,
asset.donor_code AS donor_code
FROM
asset
JOIN project ON asset.project_id = project.id
......@@ -22,25 +23,37 @@ const QUERY_FIND = `
*
* Returns all assets across all projects unless projectId is given.
*
* @param {number} sponsorId - The ID of the sponsor
* @param {number} [projectId] - (assumes isValidDbInteger(projectId)): optional: filter assets by this Project ID
* @param {number} assetType - the ID of the asset type
* @param {Array} donorCodes - a list of donor codes that the asset could have
* @returns {object[]|undefined} array of assets with fields (id, project_id, latitude, and longitude), or undefined if projectId is invalid.
* @throws error if the DB query failed to execute
*/
const find = async (sponsorId, projectId, assetType) => {
const find = async (sponsorId, projectId, assetType, donorCodes = undefined) => {
let query = QUERY_FIND;
let values = [];
if ((typeof sponsorId !== 'undefined') & (sponsorId > 0)) {
if ((typeof sponsorId !== 'undefined') && (sponsorId > 0)) {
values.push(sponsorId);
query = query + `AND sponsor_id = $${values.length}` + ' ';
}
if ((typeof projectId !== 'undefined') & (projectId > 0)) {
if ((typeof projectId !== 'undefined') && (projectId > 0)) {
values.push(projectId);
query = query + `AND project_id = $${values.length}` + ' ';
}
if ((typeof assetType !== 'undefined') & (assetType > 0)) {
if ((typeof assetType !== 'undefined') && (assetType > 0)) {
values.push(assetType);
query = query + `AND asset_type_id = $${values.length}` + ' ';
}
if ((typeof donorCodes !== 'undefined') && (donorCodes.length > 0)) {
values.push(donorCodes);
query = query + `AND donor_code = ANY ($${values.length})` + ' ';
}
const result = await global.dbPool.query(query, values);
return result.rows;
};
......
......@@ -685,3 +685,29 @@ describe('validate.param.params', () => {
expect(result).toBe(undefined);
});
});
describe('validate.type.donorCode', () => {
it('parses a single donor code and returns an array', () => {
const donor_code = 'FF00ABC';
const result = type.donorCode(donor_code);
expect(result.isSuccess()).toBeTruthy();
expect(result.value).toEqual(expect.arrayContaining([donor_code]));
});
it('parses an array of donor codes', () => {
const donor_code = ['FF00ABC', 'AABB1122'];
const result = type.donorCode(donor_code);
expect(result.isSuccess()).toBeTruthy();
expect(result.value).toEqual(expect.arrayContaining(donor_code));
});
it('returns a ParseResult.failure() if the donor code is Undefined', () => {
const result = type.donorCode(undefined);
expect(result.isFailure()).toBeTruthy();
});
it('returns a ParseResult.failure() if the donor code is an empty array', () => {
const result = type.donorCode([]);
expect(result.isFailure()).toBeTruthy();
});
});
......@@ -17,7 +17,7 @@ const {
} = require('../controllers');
// Geometry Searches
router.get(
router.post(
'/assets/geometrySearch/envelope',
validate(param.query, 'minimumLatitude', type.latitude, true),
validate(param.query, 'minimumLongitude', type.longitude, true),
......@@ -27,7 +27,7 @@ router.get(
);
// Geometry Searches
router.get(
router.post(
'/assets/geometrySearch/distance',
validate(param.query, 'latitude', type.latitude, true),
validate(param.query, 'longitude', type.longitude, true),
......@@ -35,13 +35,13 @@ router.get(
geometrySearch.distanceFind
);
router.get(
router.post(
'/assets/geometrySearch/polygon',
validate(param.body, 'coordinates', type.coordinates, true),
geometrySearch.polygonFind
);
router.get(
router.post(
'/assets/properties/temporalSearch',
validate(param.body, 'asset_id', type.id),
validate(param.body, 'sponsor', type.sponsorName),
......@@ -53,6 +53,16 @@ router.get(
temporal.temporalSearch
);
// donor code search
router.post(
'/assets/donor',
validate(param.body, 'sponsor_id', type.id),
validate(param.body, 'project_id', type.id),
validate(param.body, 'asset_type_id', type.id),
validate(param.body, 'donor_code', type.donorCode),
assets.find
);
// Assets
router.get(
'/assets',
......
......@@ -539,6 +539,31 @@ const parseProject = (project) => {
return ParseResult.success(project);
};
/**
* Validates a donor code or a list of donor codes
*
* @param {*} donor_codes - the donor code string or an array of donor codes
* @returns {ParseResult} a successful ParseResult if its a valid donor code
*/
const parseDonorCode = (donor_codes) => {
if (parseString(donor_codes)) {
return ParseResult.success([donor_codes]);
} else if (Array.isArray(donor_codes) && donor_codes.length != 0) {
var allStrings = true;
for (const code in donor_codes) {
if (!parseString(code)) {
allStrings = false;
break;
}
}
if (allStrings) {
return ParseResult.success(donor_codes);
}
}
return ParseResult.failure('donor_code must be a string or an array of strings.');
};
module.exports = {
validate,
......@@ -554,6 +579,7 @@ module.exports = {
assetTypeName: parseAssetTypeName,
coordinates: parseCoordinates,
date: parseDate,
donorCode: parseDonorCode,
geometry: parseGeometry,
latitude: parseLatitude,
longitude: parseLongitude,
......
{
"/assets/properties/temporalSearch": {
"get": {
"post": {
"requestBody": {
"description": "Gets an array of assets' historical properties.",
"content": {
......
......@@ -10,4 +10,4 @@ VALUES
INSERT INTO asset (project_id, asset_type_id, location)
SELECT 3, 5, ST_MakePoint(random_between(37.90, 38.78), random_between(-16.80, -16.15))
FROM generate_series(1, 30);
\ No newline at end of file
FROM generate_series(1, 30);
......@@ -5,8 +5,8 @@ CREATE EXTENSION IF NOT EXISTS postgis;
-- PostgreSQL database dump
--
-- Dumped from database version 11.6 (Ubuntu 11.6-1.pgdg16.04+1)
-- Dumped by pg_dump version 11.2
-- Dumped from database version 11.7 (Ubuntu 11.7-2.pgdg16.04+1)
-- Dumped by pg_dump version 12.1 (Ubuntu 12.1-1.pgdg18.04+1)
ALTER TABLE IF EXISTS ONLY "public"."property" DROP CONSTRAINT IF EXISTS "property_data_type_fkey";
ALTER TABLE IF EXISTS ONLY "public"."property" DROP CONSTRAINT IF EXISTS "property_asset_type_id_fkey";
......@@ -74,7 +74,8 @@ CREATE TABLE "public"."asset" (
"id" bigint NOT NULL,
"project_id" integer,
"asset_type_id" integer NOT NULL,
"location" "public"."geometry"(Point)
"location" "public"."geometry"(Point),
"donor_code" "text"
);
......
<template>
<lightning:layout horizontalalign="space">
<lightning:layoutItem size="10">
<lightning-combobox
name="datasource"
label="Export data to CSV"
value={value}
placeholder={value}
options={combo_options}
onchange={handleChange}
></lightning-combobox>
</lightning:layoutItem>
<br/>
<lightning:layoutItem size="2">
<lightning-button variant="success" label="Download" onclick={download}></lightning-button>
</lightning:layoutItem>
</lightning:layout>
<lightning-card title="Export CSV">
<div class="slds-m-around_medium">
<div class="slds-col slds-medium-size_3-of-3 slds-small-size_1-of-1">
<lightning:layout horizontalalign="space">
<lightning:layoutItem size="10">
<lightning-combobox
name="datasource"
label="Export data to CSV"