diff --git a/.travis.yml b/.travis.yml index 970c1d556..412cbf192 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ branches: only: - master - dev + - develop - /^feature\/.*$/ - /^hotfix\/.*$/ before_cache: diff --git a/build.gradle b/build.gradle index 5c2063302..a8fa642b7 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { plugins { id 'war' } -version "5.2" +version "5.2.1-SNAPSHOT" group "au.org.ala" @@ -107,7 +107,7 @@ dependencies { if (!inplace) { compile "org.grails.plugins:ala-map-plugin:3.0" - compile "org.grails.plugins:ecodata-client-plugin:3.0.5" + compile "org.grails.plugins:ecodata-client-plugin:3.1" } } diff --git a/grails-app/assets/javascripts/chartjsManager.js b/grails-app/assets/javascripts/chartjsManager.js index e4df65361..2c4aa885f 100644 --- a/grails-app/assets/javascripts/chartjsManager.js +++ b/grails-app/assets/javascripts/chartjsManager.js @@ -11,7 +11,14 @@ function ChartjsManagerViewModel() { * Whether to show the list of charts. */ self.chartjsListShow = ko.computed(function () { - return self.chartjsList().length > 0; + var ifChartjsListShow = self.chartjsList().length > 0 + if(ifChartjsListShow){ + $('#chartGraphTab').show() + } + else { + $('#chartGraphTab').hide() + } + return ifChartjsListShow; }); /** diff --git a/grails-app/assets/javascripts/facets.js b/grails-app/assets/javascripts/facets.js index 6cee83454..99b3a5d62 100644 --- a/grails-app/assets/javascripts/facets.js +++ b/grails-app/assets/javascripts/facets.js @@ -926,6 +926,9 @@ function decodeCamelCase(text) { var result = text.replace( /([A-Z])/g, " $1" ); return result.charAt(0).toUpperCase() + result.slice(1); // capitalize the first letter - as an example. } + else{ + return text + } } function cleanName(text) { diff --git a/grails-app/assets/javascripts/projectActivities.js b/grails-app/assets/javascripts/projectActivities.js index fd90a36be..0a424abf8 100644 --- a/grails-app/assets/javascripts/projectActivities.js +++ b/grails-app/assets/javascripts/projectActivities.js @@ -230,12 +230,8 @@ var ProjectActivitiesSettingsViewModel = function (pActivitiesVM, placeHolder) { }; self.saveForm = function () { - return self.genericUpdate("form"); - }; - - self.saveSpecies = function () { if(self.current().areSpeciesValid()) { - self.genericUpdate("species"); + self.genericUpdate("form"); } else { showAlert("All species field(s) must be configured before saving.", "alert-error", self.placeHolder); } diff --git a/grails-app/assets/javascripts/projectActivity.js b/grails-app/assets/javascripts/projectActivity.js index ef5afaf05..430b0eb89 100644 --- a/grails-app/assets/javascripts/projectActivity.js +++ b/grails-app/assets/javascripts/projectActivity.js @@ -687,6 +687,9 @@ var ProjectActivity = function (params) { if (by == "form") { jsData = {}; jsData.pActivityFormName = self.pActivityFormName(); + jsData.speciesFields = $.map(self.speciesFields(), function (obj, i) { + return obj.asJson(); + }); } else if (by == "info") { var ignore = self.ignore.concat(['current', @@ -698,13 +701,6 @@ var ProjectActivity = function (params) { jsData = ko.mapping.toJS(self, {ignore: ignore}); jsData.endDate = moment(self.endDate(), 'YYYY-MM-DDThh:mm:ssZ').isValid() ? self.endDate() : ""; } - else if (by == "species") { - jsData = {}; - - jsData.speciesFields = $.map(self.speciesFields(), function (obj, i) { - return obj.asJson(); - }); - } else if (by == "sites") { jsData = {}; var sites = []; diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index af2df53d8..70effb7b0 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -512,4 +512,8 @@ if(!map.overlays) { ] } -settings.surveyMethods="fielddata.survey.methods" \ No newline at end of file +settings.surveyMethods="fielddata.survey.methods" +if (!app.file.script.path) { + app.file.script.path = "/data/biocollect/scripts" +} +script.read.extensions.list = ['js','min.js','png'] \ No newline at end of file diff --git a/grails-app/controllers/au/org/ala/biocollect/BioActivityController.groovy b/grails-app/controllers/au/org/ala/biocollect/BioActivityController.groovy index 779be0632..a9fe7e84e 100644 --- a/grails-app/controllers/au/org/ala/biocollect/BioActivityController.groovy +++ b/grails-app/controllers/au/org/ala/biocollect/BioActivityController.groovy @@ -2,6 +2,7 @@ package au.org.ala.biocollect import au.org.ala.biocollect.merit.* import au.org.ala.biocollect.merit.hub.HubSettings +import au.org.ala.ecodata.forms.ActivityFormService import au.org.ala.web.AuthService import au.org.ala.web.UserDetails import grails.converters.JSON @@ -33,6 +34,7 @@ class BioActivityController { SettingService settingService AuthService authService UtilService utilService + ActivityFormService activityFormService static int MAX_FLIMIT = 500 static allowedMethods = ['bulkDelete': 'POST', bulkRelease: 'POST', bulkEmbargo: 'POST'] @@ -883,10 +885,8 @@ class BioActivityController { } def addOutputModel(model) { - model.metaModel = metadataService.getActivityModel(model.activity.type) - model.outputModels = model.metaModel?.outputs?.collectEntries { - [it, metadataService.getDataModelFromOutputName(it)] - } + model.putAll(activityFormService.getActivityAndOutputMetadata(model.activity.type)) + model } def defaultData diff --git a/grails-app/controllers/au/org/ala/biocollect/DownloadController.groovy b/grails-app/controllers/au/org/ala/biocollect/DownloadController.groovy index 62b407dfb..2610a7220 100644 --- a/grails-app/controllers/au/org/ala/biocollect/DownloadController.groovy +++ b/grails-app/controllers/au/org/ala/biocollect/DownloadController.groovy @@ -3,6 +3,8 @@ package au.org.ala.biocollect import au.org.ala.biocollect.merit.CommonService import au.org.ala.biocollect.merit.UserService import au.org.ala.biocollect.merit.WebService +import org.apache.commons.io.FilenameUtils + class DownloadController { WebService webService @@ -38,4 +40,50 @@ class DownloadController { } } } + + /*** + * This method is used to read custom Js files under /data/scripts directory. + * @return + */ + def getScriptFile() { + if (params.filename && params.hub && params.model) { + log.debug("Script name: " + params.filename) + log.debug("Hub: " + params.hub) + log.debug("Model: " + params.model) + String filename = FilenameUtils.getName(params.filename) + + if (filename != params.filename) { + response.status = 404 + return + } + + def extension = FilenameUtils.getExtension(params.filename)?.toLowerCase() + if (extension && !grailsApplication.config.script.read.extensions.list.contains(extension)){ + response.status = 404 + return + } + + String path = "${grailsApplication.config.app.file.script.path}${File.separator}${params.hub}${File.separator}${params.model}${File.separator}${params.filename}" + log.debug("Script path: " + path) + + File file = new File(path) + + if (!file.exists()) { + response.status = 404 + return null + } + + if(extension == 'js' || extension == 'min.js') { + response.setContentType('text/javascript') + } + response.outputStream << new FileInputStream(file) + response.outputStream.flush() + + return null + + } else { + response.status = 400 + render status:400, text: 'filename, hub or model is missing' + } + } } diff --git a/grails-app/controllers/au/org/ala/biocollect/UrlMappings.groovy b/grails-app/controllers/au/org/ala/biocollect/UrlMappings.groovy index 9fe47e21d..de87d83a4 100644 --- a/grails-app/controllers/au/org/ala/biocollect/UrlMappings.groovy +++ b/grails-app/controllers/au/org/ala/biocollect/UrlMappings.groovy @@ -110,6 +110,7 @@ class UrlMappings { } "/download/file"(controller: "download", action: [GET: "file"]) "/download/$id"(controller: "download", action: [GET: "downloadProjectDataFile"]) + "/download/getScriptFile"(controller: "download", action: [GET: "getScriptFile"]) "500"(controller:'error', action:'response500') "404"(controller:'error', action:'response404') diff --git a/grails-app/controllers/au/org/ala/biocollect/merit/ProxyController.groovy b/grails-app/controllers/au/org/ala/biocollect/merit/ProxyController.groovy index b3655c969..9e2333b85 100644 --- a/grails-app/controllers/au/org/ala/biocollect/merit/ProxyController.groovy +++ b/grails-app/controllers/au/org/ala/biocollect/merit/ProxyController.groovy @@ -127,13 +127,17 @@ class ProxyController { def excelOutputTemplate() { String url = "${grailsApplication.config.ecodata.service.url}/metadata/excelOutputTemplate" String expandList = params.expandList?:"" + String listName = params.listName?:null if (params.data) { webService.proxyPostRequest(response, url, [listName:params.listName, type:params.type, data:params.data, editMode:params.editMode, allowExtraRows:params.allowExtraRows, autosizeColumns:false, expandList: expandList]) } else { - url += "?type=${params.type?.encodeAsURL()}&listName=${params.listName?.encodeAsURL()}&listName=${expandList?.encodeAsURL()}" + url += "?type=${params.type?.encodeAsURL()}&expandList=${expandList?.encodeAsURL()}" + if(listName){ + url += "&listName=${params.listName.encodeAsURL()}" + } webService.proxyGetRequest(response, url) } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 4a2856321..09bfc1cec 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -691,7 +691,7 @@ project.myrecords.title=My sightings project.userrecords.title=Sightings of allrecords.title=All records myrecords.title=My records -record.create.title=Record a sighting +record.create.title=Create a record record.edit.title=Edit a sighting record.view.title=View a sighting record.edit.map.latLon=Latitude/longitude diff --git a/grails-app/services/au/org/ala/biocollect/merit/FormSpeciesFieldParserService.groovy b/grails-app/services/au/org/ala/biocollect/merit/FormSpeciesFieldParserService.groovy index 4b880561d..ab83089e7 100644 --- a/grails-app/services/au/org/ala/biocollect/merit/FormSpeciesFieldParserService.groovy +++ b/grails-app/services/au/org/ala/biocollect/merit/FormSpeciesFieldParserService.groovy @@ -1,7 +1,10 @@ package au.org.ala.biocollect.merit +import au.org.ala.ecodata.forms.ActivityFormService + class FormSpeciesFieldParserService { MetadataService metadataService + ActivityFormService activityFormService /** * Get the list of species fields, for the specified survey, grouped by outputs @@ -13,15 +16,7 @@ class FormSpeciesFieldParserService { * @return the list of species fields */ Map getSpeciesFieldsForSurvey(String id) { - def model = [:] - - // the activity meta-model - model.metaModel = metadataService.getActivityModel(id) - // the array of output models - model.outputModels = model.metaModel?.outputs?.collectEntries { - [it, metadataService.getDataModelFromOutputName(it)] - } - + def model = activityFormService.getActivityAndOutputMetadata(id) def fields = [] diff --git a/grails-app/services/au/org/ala/biocollect/merit/MetadataService.groovy b/grails-app/services/au/org/ala/biocollect/merit/MetadataService.groovy index 3d90f43d4..f886732ef 100644 --- a/grails-app/services/au/org/ala/biocollect/merit/MetadataService.groovy +++ b/grails-app/services/au/org/ala/biocollect/merit/MetadataService.groovy @@ -205,7 +205,7 @@ class MetadataService { def clearEcodataCache() { - webService.get(grailsApplication.config.ecodata.service.url + "/admin/clearMetadataCache") + webService.doGet(grailsApplication.config.ecodata.service.url + "/admin/clearMetadataCache" , null) } def outputTypesList() { diff --git a/grails-app/services/au/org/ala/biocollect/merit/SpeciesService.groovy b/grails-app/services/au/org/ala/biocollect/merit/SpeciesService.groovy index 10aa82428..caac84e60 100644 --- a/grails-app/services/au/org/ala/biocollect/merit/SpeciesService.groovy +++ b/grails-app/services/au/org/ala/biocollect/merit/SpeciesService.groovy @@ -154,30 +154,14 @@ class SpeciesService { def searchSpeciesList(String sort = 'listName', Integer max = 100, Integer offset = 0, String guid = null, String order = "asc", String searchTerm = null) { def list - String url = "${grailsApplication.config.lists.baseURL}/ws/speciesList?sort=${sort}&max=${max}&offset=${offset}&order=${order}" + String url = "${grailsApplication.config.lists.baseURL}/ws/speciesList?sort=${sort}&max=${max}&offset=${offset}&order=${order}&q=${searchTerm?:''}" - if (!guid & !searchTerm) { + if (!guid) { list = webService.getJson(url) } else { - if (guid) { - // Search List by species in the list - url = "${url}&items=createAlias:items&items.guid=eq:${guid}" - list = webService.getJson(url) - } - if (searchTerm) { - // Search list by list name - def searchList = getAllSpeciesList() - def toLowerSearchTerm = searchTerm.toLowerCase() - // remove list that don't match the name - searchList.lists.removeAll { !(it.listName.toLowerCase() =~ /\A${toLowerSearchTerm}/) } - if (list) { - list.lists.addAll(searchList.lists) - } else { - list = searchList - } - list.lists.unique() - list.listCount = list.lists.size() - } + // Search List by species in the list + url = "${url}&items=createAlias:items&items.guid=eq:${guid}" + list = webService.getJson(url) } list diff --git a/grails-app/views/projectActivity/_species.gsp b/grails-app/views/projectActivity/_species.gsp index 8da254ddb..daf5355ce 100644 --- a/grails-app/views/projectActivity/_species.gsp +++ b/grails-app/views/projectActivity/_species.gsp @@ -85,7 +85,7 @@
+ data-bind="click: $parent.saveForm">Save
diff --git a/src/test/groovy/au/org/ala/biocollect/DownloadControllerSpec.groovy b/src/test/groovy/au/org/ala/biocollect/DownloadControllerSpec.groovy new file mode 100644 index 000000000..3b5e0694d --- /dev/null +++ b/src/test/groovy/au/org/ala/biocollect/DownloadControllerSpec.groovy @@ -0,0 +1,84 @@ +package au.org.ala.biocollect + +import grails.test.mixin.TestFor +import org.apache.http.HttpStatus +import spock.lang.Specification + +@TestFor(DownloadController) +class DownloadControllerSpec extends Specification { + + File scriptsPath + File temp + File hubPath + File modelPath + + void setup() { + + temp = File.createTempDir("tmp", "") + scriptsPath = new File(temp, "scripts") + scriptsPath.mkdir() + hubPath = new File(scriptsPath, "tempHub") + hubPath.mkdir() + modelPath = new File(hubPath, "tempModel") + modelPath.mkdir() + + controller.grailsApplication.config.app.file.script.path = scriptsPath.getAbsolutePath() + + // Setup three files, one that should be accessible, and others that should not + File validFile = new File(modelPath, "validFile.js") + validFile.createNewFile() + + File jsonFile = new File(modelPath, "validFile.json") + jsonFile.createNewFile() + + File privateFile = new File(temp, "privateFile.js") + privateFile.createNewFile() + } + + void "Files can be retrieved from the temporary scripts directory by filename"() { + when: + params.hub = "tempHub" + params.filename = "validFile.js" + params.model = "tempModel" + controller.getScriptFile() + + then: + response.contentType == "text/javascript" + response.status == HttpStatus.SC_OK + } + + void "If a file doesn't exist, a 404 will be returned"() { + when: + params.hub = "tempHub" + params.filename = "missingFile.js" + params.model = "tempModel" + controller.getScriptFile() + + then: + response.status == HttpStatus.SC_NOT_FOUND + } + + void "A file cannot be retrieved from outside the designated scripts directory"() { + when: + params.hub = "tempHub" + params.filename = "../../../privateFile.js" + params.model = "tempModel" + controller.getScriptFile() + + then: + new File(modelPath, params.filename).exists() + response.status == HttpStatus.SC_NOT_FOUND + } + + void "Tyring to read a file with extension which are not allowed to read"() { + when: + params.hub = "tempHub" + params.filename = "validFile.json" + params.model = "tempModel" + controller.getScriptFile() + + then: + new File(modelPath, params.filename).exists() + response.status == HttpStatus.SC_NOT_FOUND + } +} diff --git a/src/test/groovy/au/org/ala/biocollect/merit/MetadataServiceSpec.groovy b/src/test/groovy/au/org/ala/biocollect/merit/MetadataServiceSpec.groovy new file mode 100644 index 000000000..0be45e21f --- /dev/null +++ b/src/test/groovy/au/org/ala/biocollect/merit/MetadataServiceSpec.groovy @@ -0,0 +1,34 @@ +package au.org.ala.biocollect.merit + +import au.org.ala.biocollect.merit.ActivityService +import grails.test.mixin.TestFor +import spock.lang.Specification + +/** + * Tests for the MetadataServiceSpec + */ +@TestFor(MetadataService) +public class MetadataServiceSpec extends Specification { + + def webServiceStub = Mock(WebService) + + void setup() { + service.webService = webServiceStub + service.grailsApplication = grailsApplication + grailsApplication.config.ecodata.service.url = "http://test" + } + + def "clear metadata cache"() { + given: + def response = [resp: "done", statusCode: "200"] + + when: + def methodResponse = service.clearEcodataCache() + + then: + 1 * webServiceStub.doGet('http://test/admin/clearMetadataCache', null) >> response + methodResponse.resp == "done" + methodResponse.statusCode == "200" + + } +} diff --git a/src/test/groovy/au/org/ala/biocollect/merit/ProxyControllerSpec.groovy b/src/test/groovy/au/org/ala/biocollect/merit/ProxyControllerSpec.groovy new file mode 100644 index 000000000..4e692d5eb --- /dev/null +++ b/src/test/groovy/au/org/ala/biocollect/merit/ProxyControllerSpec.groovy @@ -0,0 +1,46 @@ +package au.org.ala.biocollect.merit + +import grails.test.mixin.TestFor +import org.apache.http.HttpStatus +import spock.lang.Specification + +import javax.servlet.http.HttpServletResponse + +/** + * Tests the ProxyController class. + */ +@TestFor(ProxyController) +class ProxyControllerSpec extends Specification { + + def webServiceStub = Mock(WebService) + + def setup() { + controller.webService = webServiceStub + controller.grailsApplication.config.ecodata.service.url = "http://test" + } + + + def "Get an excel template that can be used to populate a table of data in an output form"() { + when: + params.expandList = 'true' + params.listName = 'test' + params.type = 'test' + controller.excelOutputTemplate() + + then: + 1 * webServiceStub.proxyGetRequest(response, "http://test/metadata/excelOutputTemplate?type=test&expandList=true&listName=test") + response.status == HttpStatus.SC_OK + } + + def "Get an excel template without listName"() { + when: + params.expandList = 'true' + params.listName = null + params.type = 'test' + controller.excelOutputTemplate() + + then: + 1 * webServiceStub.proxyGetRequest(response, "http://test/metadata/excelOutputTemplate?type=test&expandList=true") + response.status == HttpStatus.SC_OK + } +} diff --git a/src/test/groovy/au/org/ala/biocollect/merit/SpeciesServiceSpec.groovy b/src/test/groovy/au/org/ala/biocollect/merit/SpeciesServiceSpec.groovy new file mode 100644 index 000000000..12571a7ce --- /dev/null +++ b/src/test/groovy/au/org/ala/biocollect/merit/SpeciesServiceSpec.groovy @@ -0,0 +1,62 @@ +package au.org.ala.biocollect.merit + +import grails.test.mixin.TestFor +import grails.test.mixin.TestMixin +import grails.test.mixin.services.ServiceUnitTestMixin +import spock.lang.Specification + +/* + * Copyright (C) 2021 Atlas of Living Australia + * All Rights Reserved. + * + * The contents of this file are subject to the Mozilla Public + * License Version 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Created by Temi on 3/8/21. + */ + +@TestFor(SpeciesService) +@TestMixin(ServiceUnitTestMixin) +class SpeciesServiceSpec extends Specification { + def setup() { + service.grailsApplication = [ + "config": [ + "lists": [ + "baseURL": "xyz" + ] + ] + ] + + service.webService = Mock(WebService) + } + + void "should call list tool with empty search term" () { + String searchTerm = null + String url = "xyz/ws/speciesList?sort=listName&max=10&offset=0&order=asc&q=" + + when: + service.searchSpeciesList('listName', 10, 0, null, 'asc', searchTerm) + + then: + 1 * service.webService.getJson(url) >> [:] + } + + void "should call list tool with passed search term"() { + String searchTerm = 'abc' + String url = "xyz/ws/speciesList?sort=listName&max=10&offset=0&order=asc&q=abc" + + when: + service.searchSpeciesList('listName', 10, 0, null, 'asc', searchTerm) + + then: + 1 * service.webService.getJson(url) >> [:] + + } +} diff --git a/src/test/js/spec/ProjectActivitySpec.js b/src/test/js/spec/ProjectActivitySpec.js index b606865da..0fe47a580 100644 --- a/src/test/js/spec/ProjectActivitySpec.js +++ b/src/test/js/spec/ProjectActivitySpec.js @@ -46,10 +46,11 @@ describe("ProjectActivityViewModel Spec", function () { expect(pActivity.status()).toEqual("active"); }); - it("species asJS('species') should return a map with species and speciesFields", function () { + it("species asJS('form') should return a map with form name, species and speciesFields", function () { var params = { pActivity: { + pActivityFormName: "speciesForm1", speciesFields: [ { label: "species field 1", @@ -76,7 +77,7 @@ describe("ProjectActivityViewModel Spec", function () { } var speciesVM = new ProjectActivity(params); - expect(speciesVM.asJS("species")).toEqual(params.pActivity); + expect(speciesVM.asJS("form")).toEqual(params.pActivity); }); it("surveySiteOption change should clear selected sites and draw options", function () {