diff --git a/_Events.groovy b/_Events.groovy index 1171a73a..fdd9cf30 100644 --- a/_Events.groovy +++ b/_Events.groovy @@ -28,7 +28,7 @@ def build(String baseDir) { 'angular-ui-bootstrap/dist/ui-bootstrap-csp.css', 'bootbox/dist/bootbox.min.js', 'jquery/dist/jquery.min.js', 'ng-file-upload/dist/ng-file-upload.js', 'ngbootbox/dist/ngBootbox.min.js', 'bootstrap/dist/', 'leaflet/dist/', 'leaflet-draw/dist/', 'leaflet-measure/dist/', 'proj4/dist/proj4.js', - 'proj4leaflet/src/proj4leaflet.js', 'lz-string/libs/lz-string.min.js'] + 'proj4leaflet/src/proj4leaflet.js', 'lz-string/libs/lz-string.min.js', 'angular-sanitize/angular-sanitize.min.js'] files.each { name -> def dst = new File(baseDir + '/grails-app/assets/node_modules/' + name) dst.getParentFile().mkdirs() diff --git a/build.gradle b/build.gradle index 07b2cbaf..b8ff40e8 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } } -version "2.1.2-SNAPSHOT" +version "2.2.0-SNAPSHOT" group "au.org.ala" apply plugin:"eclipse" diff --git a/gradle.properties b/gradle.properties index 00c1447e..0c2cf77d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,4 @@ webdriverBinariesVersion=2.6 #chromeDriverVersion=2.45.0 geckodriverVersion=0.24.0 seleniumSafariDriverVersion=3.14.0 -alaSecurityLibsVersion=6.2.0 +alaSecurityLibsVersion=6.3.0 diff --git a/grails-app/assets/compile/spAppModules.js b/grails-app/assets/compile/spAppModules.js index 0ae47478..8cf7beb7 100644 --- a/grails-app/assets/compile/spAppModules.js +++ b/grails-app/assets/compile/spAppModules.js @@ -1,2 +1,2 @@ /* Do not edit. This file built at compile by _Events.groovy */ -$spAppModules = ["tool-add-layer-service","tool-export-checklist-service","sandbox-service","lists-service","layers-service","http-service","colour-service","i18n-service","tool-export-sample-service","species-auto-complete-service","logger-service","menu-service","doi-service","tool-export-doi-service","layout-service","tool-add-facet-service","url-params-service","popup-service","facet-auto-complete-service","tool-export-area-service","gaz-auto-complete-service","layer-distances-service","keep-alive-service","tool-area-report-service","poi-service","bie-service","event-service","predefined-layer-lists-service","sessions-service","tool-add-species-service","tools-service","predefined-areas-service","tool-export-map-service","biocache-service","flickr-service","workflow-service","map-service","select-area-directive","facet-editor-directive","select-date-range-directive","i18n-directive","sp-percent-directive","sp-options-directive","sp-legend-directive","layer-list-upload-directive","lists-list-directive","species-auto-complete-directive","envelope-directive","select-multiple-area-directive","nearest-locality-directive","sp-menu-directive","sandbox-list-directive","http-ui-directive","draw-area-directive","workflow-annotation-directive","point-comparison-directive","species-options-directive","leaflet-quick-links-directive","doi-auto-complete-directive","google-places-directive","lifeform-select-directive","playback-directive","layer-auto-complete-directive","layer-list-select-directive","select-facet-directive","select-species-directive","workflow-item-directive","area-list-select-directive","sp-map-directive","select-layers-directive","biocache-chart-directive","gaz-auto-complete-directive","modal-iframe-instance-ctrl","species-info-ctrl","facet-ctrl","workflow-ctrl","add-d-o-i-ctrl","tabulate-ctrl","layout-ctrl","area-report-ctrl","add-w-m-s-ctrl","tool-ctrl","leaflet-map-controller","sand-box-ctrl","sessions-ctrl","create-species-list-ctrl","csv-ctrl","facet-editor-modal-ctrl","add-area-ctrl","log-ctrl","analysis-ctrl"]; \ No newline at end of file +$spAppModules = ["tool-add-layer-service","tool-export-checklist-service","sandbox-service","lists-service","layers-service","http-service","colour-service","i18n-service","tool-export-sample-service","species-auto-complete-service","logger-service","menu-service","doi-service","tool-export-doi-service","layout-service","tool-add-facet-service","url-params-service","popup-service","facet-auto-complete-service","tool-export-area-service","gaz-auto-complete-service","layer-distances-service","keep-alive-service","tool-area-report-service","poi-service","bie-service","event-service","predefined-layer-lists-service","sessions-service","tool-add-species-service","tools-service","predefined-areas-service","tool-export-map-service","biocache-service","flickr-service","workflow-service","map-service","select-area-directive","facet-editor-directive","select-date-range-directive","i18n-directive","sp-percent-directive","sp-options-directive","sp-legend-directive","layer-list-upload-directive","lists-list-directive","species-auto-complete-directive","envelope-directive","select-multiple-area-directive","nearest-locality-directive","sp-menu-directive","sandbox-list-directive","http-ui-directive","draw-area-directive","workflow-annotation-directive","point-comparison-directive","species-options-directive","leaflet-quick-links-directive","doi-auto-complete-directive","google-places-directive","lifeform-select-directive","playback-directive","layer-auto-complete-directive","layer-list-select-directive","select-facet-directive","select-species-directive","workflow-item-directive","area-list-select-directive","sp-map-directive","select-layers-directive","biocache-chart-directive","gaz-auto-complete-directive","modal-iframe-instance-ctrl","species-info-ctrl","facet-ctrl","workflow-ctrl","add-d-o-i-ctrl","tabulate-ctrl","layout-ctrl","area-report-ctrl","add-w-m-s-ctrl","tool-ctrl","leaflet-map-controller","add-points-ctrl","sand-box-ctrl","sessions-ctrl","create-species-list-ctrl","csv-ctrl","facet-editor-modal-ctrl","add-area-ctrl","log-ctrl","analysis-ctrl"]; \ No newline at end of file diff --git a/grails-app/assets/javascripts/application.js b/grails-app/assets/javascripts/application.js index 6f061e36..66f24b31 100644 --- a/grails-app/assets/javascripts/application.js +++ b/grails-app/assets/javascripts/application.js @@ -33,6 +33,7 @@ //= require angular-slider/slider.js //= require angular-sortable/sortable.js //= require ng-file-upload/dist/ng-file-upload.min.js +//= require angular-sanitize/angular-sanitize.min.js //bootbox //= require ngbootbox/dist/ngBootbox.min.js diff --git a/grails-app/assets/javascripts/spApp/controller/addPointsCtrl.js b/grails-app/assets/javascripts/spApp/controller/addPointsCtrl.js new file mode 100644 index 00000000..d83e2fe7 --- /dev/null +++ b/grails-app/assets/javascripts/spApp/controller/addPointsCtrl.js @@ -0,0 +1,305 @@ +(function (angular) { + 'use strict'; + /** + * @memberof spApp + * @ngdoc controller + * @name AddAPointsCtrl + * @description + * Add points to the map using spatial-service's sandbox services + */ + angular.module('add-points-ctrl', ['map-service', 'layers-service', 'predefined-areas-service', 'ngSanitize']) + .controller('AddPointsCtrl', ['LayoutService', '$scope', 'MapService', '$timeout', 'LayersService', + '$uibModalInstance', 'PredefinedAreasService', 'BiocacheService', 'LoggerService', 'data', + function (LayoutService, $scope, MapService, $timeout, LayersService, $uibModalInstance, PredefinedAreasService, BiocacheService, LoggerService, inputData) { + + $scope.inputData = inputData; + + $scope.step = 'default'; + $scope.method = 'existing'; + + $scope.errorMsg = ''; + + $scope.datasetName = 'my dataset name'; + $scope.message = ''; + $scope.status = 'queued'; + $scope.statusUrl = ''; + $scope.dataResourceUid = ''; + + $scope.maxFileSize = $SH.maxUploadSize; + + $scope.uploadingFile = false; + $scope.uploadProgress = 0; + + $scope.file = null; + + $scope.uploadsList = []; + $scope.searchUploads = ''; + $scope.sortType = 'date'; + $scope.sortReverse = true; + + $scope.instructions = $i18n(551, "Select a CSV or zipped CSV file."); + + LayoutService.addToSave($scope); + + $scope.init = function () { + // get a list of all prior uploads + if ($SH.userId) { + // find existing old sandbox uploads + if ($SH.sandboxServiceUrl && $SH.sandboxUrl) { + BiocacheService.userUploads($SH.userId, $SH.sandboxServiceUrl).then(function (data) { + + if (data.totalRecords === 0) { + return; + } + + // add bs and ws to each item + var items = data.facetResults[0].fieldResult; + items.forEach(function (item) { + item.bs = $SH.sandboxServiceUrl; + item.ws = $SH.sandboxUrl; + // get dataset_name and last_load_date + BiocacheService.searchForOccurrences({ + qid: item.fq, // skip qid registration for this one-off query + bs: item.bs, + ws: item.ws + }, [], 0, 0, 'dataset_name,last_processed_date').then(function (data) { + if (data.totalRecords > 0) { + // handle facets returning in a different order + var order = data.facetResults[0].fieldName === 'dataset_name' ? 0 : 1; + item.label = data.facetResults[order === 0 ? 0 : 1].fieldResult[0].label; + item.date = data.facetResults[order === 0 ? 1 : 0].fieldResult[0].label; + + // format the date so that it is sortable. It is currently a string, e.g. "2010-11-01T00:00:00Z" + item.date = new Date(item.date).toISOString().slice(0, 10); + + item.addedToMap = false; + + item.old = true; + + $scope.uploadsList.push(item); + } + }); + }); + }); + } + + // find existing spatial-service sandbox uploads + BiocacheService.userUploads($SH.userId, $SH.sandboxSpatialServiceUrl).then(function (data) { + if (data.totalRecords === 0) { + return; + } + + // add bs and ws to each item + var items = data.facetResults[0].fieldResult; + items.forEach(function (item) { + item.bs = $SH.sandboxSpatialServiceUrl; + item.ws = $SH.sandboxSpatialUiUrl; + // get dataset_name and last_load_date + BiocacheService.searchForOccurrences({ + qid: item.fq, // skip qid registration for this one-off query + bs: item.bs, + ws: item.ws + }, [], 0, 0, 'datasetName,lastProcessedDate').then(function (data) { + if (data.totalRecords > 0) { + // handle facets returning in a different order + var order = data.facetResults[0].fieldName === 'datasetName' ? 0 : 1; + item.label = data.facetResults[order === 0 ? 0 : 1].fieldResult[0].label; + item.date = data.facetResults[order === 0 ? 1 : 0].fieldResult[0].label; + + // format the date so that it is sortable. It is currently a string, e.g. "2010-11-01T00:00:00Z" + item.date = new Date(item.date).toISOString().slice(0, 10); + + item.addedToMap = false; + + item.old = false; + + $scope.uploadsList.push(item); + } + }); + }); + }); + } + } + + $scope.uploadFile = function (newFiles) { + + if (newFiles == null || newFiles.length == 0) { + return + } + + var file = newFiles[0] + + if (file.$error) { + if (file.$errorMessages.maxSize) { + bootbox.alert($i18n(476, "The uploaded file is too large. Max file size:") + " " + Math.floor($scope.maxFileSize / 1024 / 1024) + "MB"); + return + } + } + + $scope.file = file; + + // remove file extension and add date/time + var dateTime = new Date().toLocaleString(); + var newName = file.name.replace(/\.[^/.]+$/, "") + " " + dateTime; + $scope.datasetName = newName.substring(0, 200); // limit to 200 characters + }; + + $scope.checkStatus = function() { + LayersService.getSandboxUploadStatus($scope.statusUrl).then(function (data) { + $scope.status = data.status; + $scope.message = data.message; + + if ($scope.status === 'running') { + $timeout(function () { + $scope.checkStatus(); + }, 3000); // wait 3 seconds before checking status + } else if ($scope.status === 'finished') { + // successful + } + }, function (error) { + if (!error.handled) { + $scope.status = 'error'; + if (error.data.error) { + $scope.message = error.data.error; + } else { + $scope.message = "status code: " + error.status; + } + } + }); + } + + // currently removing the auto close step, so that the user can read the last status message + $scope.addToMapAndClose = function () { + var q = { + q: ['dataResourceUid:"' + $scope.dataResourceUid + '"'], + name: $scope.datasetName, + bs: $SH.sandboxSpatialServiceUrl, + ws: $SH.sandboxSpatialUiUrl + }; + + if (!$scope.logged) { + $scope.logged = true + + LoggerService.log("Create", "Points", {query: q, name: $scope.datasetName}) + } + + BiocacheService.newLayer(q, undefined, q.name).then(function (data) { + if (data != null) { + MapService.add(data); + } + $scope.$close(); + }); + }; + + $scope.ok = function () { + if ($scope.errorMsg || $scope.status === 'error') { + $scope.$close(); + } else if ($scope.status === 'finished') { + $scope.addToMapAndClose(); + } else { + $scope.step = 'uploading'; + $scope.uploadingFile = true; + + LayersService.uploadSandboxFile($scope.file, $scope.datasetName, $scope.file.name).then(function (response) { + if (response.data.error) { + $scope.status = 'error'; + $scope.uploadingFile = false; + $scope.errorMsg = response.data.error; + return + } else { + $scope.status = 'starting'; + $scope.statusUrl = response.data.statusUrl; + $scope.message = response.data.message; + $scope.dataResourceUid = response.data.dataResourceUid; + + $timeout(function () { + $scope.checkStatus(); + }, 3000); // wait 3 seconds before checking status + } + + $scope.uploadingFile = false; + }, function (error) { + $scope.errorMsg = "Unexpected error."; + if (!error.handled) { + if (error.status == 500) { + $scope.errorMsg = "Unexpected error: the uploaded file may be broken or unrecognised."; + } else { + if (error.data.error) { + $scope.errorMsg = error.data.error; + } else { + $scope.errorMsg = $i18n(540,"An error occurred. Please try again and if the same error occurs, send an email to support@ala.org.au and include the URL to this page, the error message and what steps you performed that triggered this error."); + } + } + } + $scope.uploadingFile = false; + + }, function (evt) { + $scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total); + }); + } + } + + $scope.isDisabled = function () { + if ($scope.step === 'uploading') { + return $scope.status !== 'finished' && $scope.status !== 'error' + } else { + return $scope.file == null || $scope.uploadingFile; + } + } + + $scope.isLoggedIn = $scope.isLoggedIn = $SH.userId !== undefined && $SH.userId !== null && $SH.userId.length > 0; + $scope.isNotLoggedIn = !$scope.isLoggedIn; + + $scope.addToMap = function (item) { + item.addedToMap = true; + + BiocacheService.registerLayer(item.bs, item.ws, [item.fq], undefined, undefined, true, true, item.label).then(function (data) { + if (data != null) { + MapService.add(data); + } + }); + } + + $scope.delete = function (item) { + bootbox.confirm("Are you sure you want to delete \"" + item.label + "\?", function (result) { + if (result) { + // extract id from item.fq with the content `data_resource_uid:"39632cdd-4e1f-41d8-922a-c09a68270b2d"` + var dataResourceUid = item.fq.split('"')[1]; + + LayersService.deleteSandboxUpload(dataResourceUid).then(function (data) { + if (data != null) { + $scope.uploadsList = $scope.uploadsList.filter(function (i) { + return i.fq !== item.fq; + }); + } + }); + } + }); + } + + $scope.download = function (item) { + var url = item.ws + "/occurrences/search?q=" + item.fq; + var a = document.createElement('a'); + a.target = '_blank'; + a.href = url; + a.click(); + } + + $scope.searchFilter = function(item) { + if (!$scope.searchUploads) { + return true; + } + var searchText = $scope.searchUploads.toLowerCase(); + return item.label.toLowerCase().includes(searchText); + }; + + $scope.showFinished = function() { + return st + } + + $scope.init(); + + + }]) + +}(angular)); diff --git a/grails-app/assets/javascripts/spApp/service/biocacheService.js b/grails-app/assets/javascripts/spApp/service/biocacheService.js index 17452443..30bcd3d3 100644 --- a/grails-app/assets/javascripts/spApp/service/biocacheService.js +++ b/grails-app/assets/javascripts/spApp/service/biocacheService.js @@ -388,7 +388,7 @@ * @param {List} fqs (Optional) additional fq terms * @param {Integer} pageSize (Optional) page size (default=1) * @param {Integer} offset (Optional) offset (default=0) - * @param {Boolean} facet (Optional) include server default facets (default=false) + * @param {String} facets (Optional) comma delimited facets (flimit is -1) * @returns {Promise(Map)} search results * * @example @@ -418,8 +418,8 @@ * "activeFacetMap": {} * } */ - searchForOccurrences: function (query, fqs, pageSize, offset, facet) { - facet = facet || false; + searchForOccurrences: function (query, fqs, pageSize, offset, facets) { + facets = facets || ""; pageSize = pageSize === undefined ? 1 : pageSize; offset = offset || 0; var fqList = (fqs === undefined ? '' : '&fq=' + this.joinAndEncode(fqs)); @@ -427,13 +427,27 @@ if (response == null) { return {} } - return $http.get(query.bs + "/occurrences/search?facet=" + facet + "&pageSize=" + pageSize + "&startIndex=" + offset + "&q=" + response.qid + fqList + '&sort=id', _httpDescription('searchForOccurrences')).then(function (response) { + return $http.get(query.bs + "/occurrences/search?facets=" + facets + "&pageSize=" + pageSize + "&startIndex=" + offset + "&q=" + response.qid + fqList + '&sort=id', _httpDescription('searchForOccurrences')).then(function (response) { if (response.data !== undefined) { return response.data; } }) }) }, + /** + * Facet search for dataResourceUid for a given userId. + * + * @param userId user's id used to upload data resources + * @param sandboxServiceUrl biocache-service URL for the sandbox + * @returns {*} + */ + userUploads: function (userId, sandboxServiceUrl) { + return $http.get(sandboxServiceUrl + "/occurrences/search?facets=data_resource_uid&flimit=-1&pageSize=0&q=user_id:" + userId, _httpDescription('userUploads')).then(function (response) { + if (response.data !== undefined) { + return response.data; + } + }) + }, /** * Get facet list * @memberof BiocacheService diff --git a/grails-app/assets/javascripts/spApp/service/layersService.js b/grails-app/assets/javascripts/spApp/service/layersService.js index 9961e79d..e4154194 100644 --- a/grails-app/assets/javascripts/spApp/service/layersService.js +++ b/grails-app/assets/javascripts/spApp/service/layersService.js @@ -556,7 +556,59 @@ } } return result; + }, + /** + * Upload an sandbox file. zip (csv) or csv + * @memberof LayersService + * @param {File} file + * @param {string} datasetName name of the dataset + * @returns {Promise(Map)} in progress uploaded csv info + */ + uploadSandboxFile: function (file, datasetName) { + var uploadURL = $SH.baseUrl + "/sandbox?name=" + encodeURI(datasetName); + + file.upload = Upload.upload({ + url: uploadURL, + data: {file: file} + }); + + return file.upload; + }, + /** + * Get sandbox list + * @memberof LayersService + * @param {string} userId + * @returns {Promise(List)} list of sandbox uploads + */ + listSandbox: function (userId) { + var urlProxy = $SH.sandboxBiocacheServiceUrl + "/occurrences/search?q=userId:" + userId + "&pageSize=0&facets=raw_datasetName"; + return $http.get(urlProxy, _httpDescription('listSandbox', {})).then(function (response) { + return response.data; + }); + }, + /** + * Get sandbox upload status + * @memberof LayersService + * @param {string} statusUrl + * @returns {Promise(Map)} status of the upload + */ + getSandboxUploadStatus: function (statusUrl) { + return $http.get(statusUrl, _httpDescription('getUploadStatus')).then(function (response) { + return response.data; + }); + }, + /** + * Delete a sandbox upload, by dataResourceUid. Only applies to the spatial-service sandbox. + * @memberof LayersService + * @param {string} dataResourceUid + * @returns {Promise(Map)} status of the delete + */ + deleteSandboxUpload: function (dataResourceUid) { + return $http.delete($SH.baseUrl + "/sandbox?id=" + dataResourceUid, _httpDescription('deleteSandboxUpload')).then(function (response) { + return response.data; + }); } + } return thiz; diff --git a/grails-app/assets/javascripts/spApp/templates/addPointsContent.tpl.htm b/grails-app/assets/javascripts/spApp/templates/addPointsContent.tpl.htm new file mode 100644 index 00000000..acc811ad --- /dev/null +++ b/grails-app/assets/javascripts/spApp/templates/addPointsContent.tpl.htm @@ -0,0 +1,163 @@ +
Select a CSV or zipped CSV file. Please note that for the records in a data set to be effectively discoverable and able to be mapped it needs to meet a minimum set of standards.
\ +Data needs to include, at a minimum:
\ +scientificName
/vernacularName
to identify the organismeventDate
to indicate when the occurrence happeneddecimalLatitude
and decimalLongitude
to indicate where the occurrence happenedcatalogNumber
or occurrenceID
, with a unique value for each recordAlso note:
\ +