From 8c02af8e4cc00b9d1626b76b814dbe51780a9a66 Mon Sep 17 00:00:00 2001 From: Chris Dunstall Date: Thu, 7 Apr 2022 17:28:33 +1000 Subject: [PATCH 1/7] Increment version number for next development iteration --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 62bdbac1..597ba2d9 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { id "com.virgo47.ClasspathJar" version "1.0.0" } -version "6.1.0" +version "6.1.1-SNAPSHOT" group "au.org.ala" description "Digivol application" From 0730e58b638152314bd50889b8c6881bacf5d733 Mon Sep 17 00:00:00 2001 From: Chris Dunstall Date: Sat, 23 Apr 2022 13:45:49 +1000 Subject: [PATCH 2/7] #481 Fixes and updates after peer review audio-template-config.js - Updated require statements wildlifespotter-template-config.js - Updated require statements ImageController.groovy - Removed unnecessary code ProjectController.groovy - Cleaned up duplicated code TemplateController.groovy - Removed dependency on h2.org library audioTemplateConfig.gsp - created import from duplicated code _taskSummary.gsp - Updated for specific version of wavesurfer JS. --- .../javascripts/audio-template-config.js | 2 +- .../assets/javascripts/template-config.js | 510 +++++++++++++++++ .../wildlifespotter-template-config.js | 2 +- .../org/ala/volunteer/ImageController.groovy | 15 +- .../ala/volunteer/ProjectController.groovy | 22 +- .../ala/volunteer/TemplateController.groovy | 3 +- .../ala/volunteer/ProjectToolsService.groovy | 4 +- grails-app/views/forum/_taskSummary.gsp | 2 +- grails-app/views/task/showDetails.gsp | 2 +- .../views/template/audioTemplateConfig.gsp | 525 +----------------- .../views/template/wildlifeTemplateConfig.gsp | 460 +-------------- 11 files changed, 553 insertions(+), 994 deletions(-) create mode 100644 grails-app/assets/javascripts/template-config.js diff --git a/grails-app/assets/javascripts/audio-template-config.js b/grails-app/assets/javascripts/audio-template-config.js index 3020da6c..0982025c 100644 --- a/grails-app/assets/javascripts/audio-template-config.js +++ b/grails-app/assets/javascripts/audio-template-config.js @@ -3,5 +3,5 @@ //= require compile/csv/csv.js //= require compile/soundmanager2/soundmanager2.js //= require inline-audio-player.js -//=require_self + diff --git a/grails-app/assets/javascripts/template-config.js b/grails-app/assets/javascripts/template-config.js new file mode 100644 index 00000000..bd225fda --- /dev/null +++ b/grails-app/assets/javascripts/template-config.js @@ -0,0 +1,510 @@ +var templateId = T_CONF.templateId; //${templateInstance.id}; +var viewParams = T_CONF.viewParams; //; +var wstc = angular.module('wildlifespottertemplateconfig', ['ngAnimate', 'ngFileUpload']); + +function TemplateConfigController($http, $log, $timeout, $window, Upload) { + var self = this; + self.model = _.defaults(viewParams, { animals: [], categories:[]}); + + function initCategoryUiStatus() { + self.categoryUiStatus = []; + for (var i = 0; i < self.model.categories.length; ++i) { + self.categoryUiStatus.push({minimized: true}); + } + } + + function initAnimalUiStatus() { + self.animalUiStatus = []; + for (var i = 0; i < self.model.animals.length; ++i) { + self.animalUiStatus.push({minimized: true}); + } + } + + function ensureAnimalArray() { + if (angular.isUndefined(self.model.animals) || self.model.animals === null) { + self.model.animals = []; + } + } + + initCategoryUiStatus(); + initAnimalUiStatus(); + + self.addCategory = function() { + self.model.categories.push({name: '', entries: []}); + self.categoryUiStatus.push({minimized: false}); + }; + + self.removeCategory = function(index) { + self.model.categories.splice(index, 1); + self.categoryUiStatus.splice(index, 1); + }; + + self.addEntry = function(c) { + c.entries.push({name: '', hash: ''}); + }; + + self.removeEntry = function(c, $index) { + c.entries.splice($index, 1); + }; + + self.addAnimal = function() { + ensureAnimalArray(); + self.model.animals.push({vernacularName: '', scientificName: '', description: '', categories: {}, images: [], audio: []}); + self.animalUiStatus.push({minimized: false}); + }; + + self.removeAnimal = function($index) { + self.model.animals.splice($index, 1); + self.animalUiStatus.splice($index, 1); + }; + + self.addBlankImage = function(a) { + ensureImagesArray(a); + a.images.push({hash: ''}); + }; + + function ensureImagesArray(a) { + if (angular.isUndefined(a.images) || a.images === null) { + a.images = []; + } + } + + function ensureAudioArray(a) { + if (angular.isUndefined(a.audio) || a.audio === null) { + a.audio = []; + } + } + + self.removeImage = function(a, $index) { + a.images.splice($index, 1); + }; + + self.removeAudio = function(a, $index) { + a.audio.splice($index, 1); + }; + + self.addManyFiles = function(animal, category, $files, fileType) { + for (var i = 0; i < $files.length; i++) { + var file = $files[i]; + var hashable; + if (category) { + hashable = {name: $files[i].name, hash: ''}; + category.entries.push(hashable); + } else { + hashable = {hash: ''}; + if (fileType === 'image') { + ensureImagesArray(animal); + animal.images.push(hashable); + } else if (fileType === 'audio') { + ensureAudioArray(animal); + animal.audio.push(hashable); + } + } + + uploadFile(hashable, file, fileType); + } + }; + + self.addManyImages = function(animal, category, $files) { + self.addManyFiles(animal, category, $files, 'image'); + }; + + self.addManyAudio = function(animal, category, $files) { + self.addManyFiles(animal, category, $files, 'audio'); + }; + + self.addFile = function(entryOrAnimalArray, $index, $file, fileType) { + var entryOrAnimal = entryOrAnimalArray[$index]; + if (!$file) { + entryOrAnimal.hash = ''; + return; + } + + uploadFile(entryOrAnimal, $file[0], fileType); + }; + + self.addImage = function(entryOrAnimalArray, $index, $file) { + self.addFile(entryOrAnimalArray, $index, $file, 'image'); + }; + + self.addAudio = function(entryOrAnimalArray, $index, $file) { + self.addFile(entryOrAnimalArray, $index, $file, 'audio'); + }; + + function uploadFile(hashable, file, type) { + var data; + if ("name" in hashable) { + data = {entry: file}; + } else { + data = {animal: file}; + } + var name = file.name; + var submitUrl = T_CONF.submitUrl; //""; + if (type !== undefined && type === 'audio') { + submitUrl = T_CONF.audioSubmitUrl; //""; + } + + Upload.upload({ + url: submitUrl, + data: data + }).then(function (resp) { + $log.debug('Success ' + name + ' uploaded. Response: ' + JSON.stringify(resp.data)); + hashable.hash = resp.data.hash; + hashable.ext = resp.data.format; + }, function (resp) { + bootbox.alert("Image upload failed"); + $log.error('Error status: ' + resp.status); + }, function (evt) { + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); + $log.info('progress: ' + progressPercentage + '% ' + name); + }); + } + + function uploadImage(hashable, file) { + uploadFile(hashable, file, 'image'); + } + + function uploadAudio(hashable, file) { + uploadFile(hashable, file, 'audio'); + } + + self.moveCategoryUp = function(index) { + self.moveUp(self.model.categories, index); + self.moveUp(self.categoryUiStatus, index); + }; + + self.moveCategoryDown = function(index) { + self.moveDown(self.model.categories, index); + self.moveDown(self.categoryUiStatus, index); + }; + + self.moveAnimalUp = function(index) { + self.moveUp(self.model.animals, index); + self.moveUp(self.animalUiStatus, index); + }; + + self.moveAnimalDown = function(index) { + self.moveDown(self.model.animals, index); + self.moveDown(self.animalUiStatus, index); + }; + + self.moveUp = function(a, $index) { + if ($index <= 0) { + return; + } + var a1 = a[$index]; + a[$index] = a[$index - 1]; + a[$index - 1] = a1; + }; + + self.moveDown = function(a, $index) { + if ($index >= (a.length - 1)) { + return; + } + var a1 = a[$index]; + a[$index] = a[$index + 1]; + a[$index + 1] = a1; + }; + + self.sortAnimals = function() { + self.minimizeAll(self.animalUiStatus); + + self.model.animals.sort(function(a,b) { + var nameA = self.fullName(a).toUpperCase(); + var nameB = self.fullName(b).toUpperCase(); + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + + // names must be equal + return 0; + }); + }; + + self.minimizeAll = function(array) { + for (var i = 0; i < array.length; ++i) { + array[i].minimized = true; + } + }; + + var imageUrlTemplate = T_CONF.imageUrlTemplate; //""; + + var audioUrlTemplate = T_CONF.audioUrlTemplate; //""; + + self.entryUrl = function(e) { + var url = ""; + if (!angular.isUndefined(e.hash) && e.hash !== "") { + url = imageUrlTemplate.replace("{{name}}", e.hash).replace("{{width}}", "100").replace("{{height}}", "100").replace("{{format}}", "png"); + } + return url; + }; + + self.imageUrl = function(i) { + var url = ""; + if (!angular.isUndefined(i.hash) && i.hash !== "") { + url = imageUrlTemplate.replace("{{name}}", i.hash).replace("{{width}}", "150").replace("{{height}}", "150").replace("{{format}}", i.ext); + } + return url; + }; + + self.audioUrl = function(i) { + var url = audioUrlTemplate.replace("{{name}}", i.hash).replace("{{format}}", i.ext); + return url; + } + + self.fullName = function(a) { + if (a.vernacularName && a.scientificName) { + return a.vernacularName + " (" + a.scientificName+")"; + } else if (a.vernacularName) { + return a.vernacularName; + } else if (a.scientificName) { + return a.scientificName; + } else { + return ''; + } + }; + + self.categoryChange = function(cat) { + var oldValue = cat.prevName; + var newValue= cat.name; + var model = self.model; + if (!(typeof oldValue === 'undefined')) { + for (var i = 0; i < model.animals.length; ++i) { + var animal = model.animals[i]; + if (oldValue in animal.categories) { + animal.categories[newValue] = animal.categories[oldValue]; + delete animal.categories[oldValue]; + } + } + } + cat.prevName = cat.name; + }; + + self.entryChange = function(cat, entry) { + var oldValue = entry.prevName; + var newValue= entry.name; + var model = self.model; + if (!(typeof oldValue === 'undefined')) { + for (var i = 0; i < model.animals.length; ++i) { + var animal = model.animals[i]; + if (animal.categories[cat.name] === oldValue) { + animal.categories[cat.name] = newValue; + } + } + } + entry.prevName = entry.name; + }; + + self.uploadCategoryJSON = function($file) { + var reader = new FileReader(); + + reader.onload = function(e) { + // Render thumbnail. + var categories = JSON.parse(e.target.result); + console.log(categories); + + // TODO sanity check + if (!Array.isArray(categories)) { + bootbox.alert("Uploaded file is not an array"); + return; + } + + var catDefaults = { name: '', entries: [] }; + var entryDefaults = { hash: '', name: '' }; + + categories = categories.map(function(v,i,l) { + var cat = _.defaults(v, catDefaults); + cat.entries = cat.entries.map(function(v2,i2,l2) { + return _.defaults(v2, entryDefaults); + }); + return cat; + }); + + self.model.categories = categories; + }; + + // Read in the image file as a data URL. + reader.readAsText($file); + }; + + self.downloadCategoryJSON = function($file) { + var data = JSON.stringify(angular.copy(self.model.categories).map(function(v,i,l) { + var cat = _.omit(v, 'prevName'); + cat.entries = v.entries.map(function(v2,i2,l2) { + return _.omit(v2, 'prevName'); + }); + return cat; + }), null, 2); + var fileName = "" + templateId + "-categories.json"; + downloadFile(data, 'application/json', fileName); + }; + + self.uploadAnimalCSV = function($file) { + CSV.fetch({ + file: $file + }).done(function(dataset) { + $log.info(dataset); + + var normalisedFields = _.map(dataset.fields, function(v,i) { + var key = findCategory(v); + if (!key) bootbox.alert("Unknown category/column: " + v); + return key || v; + }); + + var defaultCats = _.chain(self.model.categories).pluck('name').map(function(v) { return [v, null] }).object().value(); + var catNames = _.chain(self.model.categories).pluck('name').value(); + var animalDefaults = { + vernacularName: '', + scientificName: '', + description: '', + images: [], + audio: [], + categories: defaultCats + }; + + var animals = _.map(dataset.records, function(v,i,l) { + var row = _.chain(v).map(function(v2,i2) { + var field = normalisedFields[i2]; + return [field, v2]; + }).value(); + + var animal = _.chain(row).filter(function(v2,i2) { + return _.contains(['vernacularName','scientificName','description','images'], v2[0]); + }).object().value(); + + if (animal.images) animal.images = animal.images.split(',').map(function (s,i,l) { return {hash: s.trim()}; }); + else animal.images = []; + + var categories = _.chain(row).filter(function(v2,i2) { + var field = v2[0]; + return !_.contains(['vernacularName','scientificName','description','images'], field) && _.contains(catNames, field); + }).map(function(v2,i2) { + var field = v2[0]; + var origValue = v2[1]; + var value = findCatEntry(field, origValue); + if (origValue && !value) { + bootbox.alert("Can't find a matching value for row " + (i+2) + ", vernacular name " + + animal.vernacularName + ", category " + field + ", value of " + origValue ); + } + return [ field, value ]; + }).object().value(); + + animal.categories = _.defaults(categories, defaultCats); + + animal = _.defaults(animal, animalDefaults); + if (animal.vernacularName == null) animal.vernacularName = ''; + if (animal.scientificName == null) animal.scientificName = ''; + if (animal.description == null) animal.description = ''; + + return animal; + }); + + $log.info(animals); + + self.model.animals = animals; + initAnimalUiStatus(); + + }).fail(function(e) { + bootbox.alert("Couldn't read CSV:" + e); + }); + }; + + function findCategory(s) { + var S = s.toUpperCase(); + var vn = 'vernacularName', sn = 'scientificName', desc = 'description', images = 'images'; + if (S === vn.toUpperCase()) return vn; + if (S === sn.toUpperCase()) return sn; + if (S === desc.toUpperCase()) return desc; + if (S === images.toUpperCase()) return images; + for (var i = 0; i < self.model.categories.length; ++i) { + var c = self.model.categories[i]; + var name = (c.name || '').toUpperCase(); + if (name === S) { + return c.name; + } + } + return null; + } + + function findCatEntry(cat, value) { + var V = (value || '').toUpperCase(); + var c = _.findWhere(self.model.categories, { name: cat }); + + if (!c) return null; + + for (var i = 0; i < c.entries.length; ++i) { + var e = c.entries[i]; + var name = (e.name || '').toUpperCase(); + if (name === V) { + return e.name; + } + } + + return null; + } + + self.downloadAnimalCSV = function() { + var fields = ["vernacularName","scientificName","description"].concat(_.pluck(self.model.categories, "name"),["images"]); + var records = self.model.animals.map(function(v,i,l) { + var start = [v.vernacularName, v.scientificName, v.description]; + var cats = self.model.categories.map(function(v2,i2,l2) { + return v.categories[v2.name]; + }); + var images = (v.images || []).map(function(v2,i2,l2) { return v2.hash}).join(','); + return start.concat(cats,[images]); + }); + var data = CSV.serialize([fields].concat(records)); + var fileName = "" + templateId + "-animals.csv"; + downloadFile(data, 'text/csv', fileName); + }; + + function downloadFile(data, contentType, fileName) { + var file = new Blob([data], {type: contentType}); + var a = document.createElement("a"); + document.body.appendChild(a); + var fileURL = $window.URL.createObjectURL(file); + a.href = fileURL; + a.download = fileName; + a.click(); + document.body.removeChild(a); + $timeout(function() { + $log.info("Revoking " + fileName); + $window.URL.revokeObjectURL(fileURL); + }, 60 * 1000, false); + } + + function filterModel() { + var results = _.chain(self.model.animals).zip(self.animalUiStatus).filter(function(e,i,l) { + var animal = e[0]; + var result = animal.scientificName || animal.vernacularName || animal.description || (animal.images && animal.images.length && animal.images.some(function(v) { return !!v.hash})); + return result; + }).unzip().value(); + self.model.animals = results[0]; + self.animalUiStatus = results[1]; + + results = _.chain(self.model.categories).zip(self.categoryUiStatus).filter(function(e,i,l) { + var cat = e[0]; + var result = cat.name || (cat.entries && cat.entries.length && cat.entries.every(function(entry) { + return !!entry.name || !!entry.hash; + })); + return result; + }).unzip().value(); + self.model.categories = results[0]; + self.categoryUiStatus = results[1]; + } + + self.save = function() { + filterModel(); + var p = $http.post(T_CONF.saveTemplateUrl /*""*/, self.model); + p.then(function(response) { + bootbox.alert("Saved!"); + }, function(response) { + bootbox.alert("Couldn't save WildlifeSpotter config"); + }); + }; +} + +wstc.controller('TemplateConfigController', TemplateConfigController); \ No newline at end of file diff --git a/grails-app/assets/javascripts/wildlifespotter-template-config.js b/grails-app/assets/javascripts/wildlifespotter-template-config.js index e1c674c5..dfc5e09d 100644 --- a/grails-app/assets/javascripts/wildlifespotter-template-config.js +++ b/grails-app/assets/javascripts/wildlifespotter-template-config.js @@ -1,4 +1,4 @@ //= require angular/ng-file-upload //= require underscore //= require compile/csv/csv.js -//=require_self + diff --git a/grails-app/controllers/au/org/ala/volunteer/ImageController.groovy b/grails-app/controllers/au/org/ala/volunteer/ImageController.groovy index 45ada47f..1b7fa42a 100644 --- a/grails-app/controllers/au/org/ala/volunteer/ImageController.groovy +++ b/grails-app/controllers/au/org/ala/volunteer/ImageController.groovy @@ -86,24 +86,11 @@ class ImageController { def imagesHome = grailsApplication.config.getProperty('images.home') File result = new File("$imagesHome${File.separator}$encodedPrefix", "${encodedName}.${format}") - if (result.exists()) { - sendImage(result, contentType(format)) - return - } - - File original = findImage(encodedPrefix, encodedName) - if (!original) { + if (!result.exists()) { response.sendError(SC_NOT_FOUND) return } - def originalImage = ImageIO.read(original) - if (!originalImage) { - log.warn("${original.path} could not be read as an image") - render([error: "${original.path} could not be read as an image"] as JSON, status: 500) - return - } - originalImage.flush() sendImage(result, contentType(format)) } diff --git a/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy b/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy index 56529e78..b755a9d7 100644 --- a/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy +++ b/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy @@ -400,7 +400,7 @@ class ProjectController { bindData(project, params) if (params.institutionId) { - Institution institution = Institution.get(params.long('institutionId') as Long) + Institution institution = Institution.get(params.long('institutionId') as long) if (institution) { project.institution = institution } else { @@ -410,11 +410,8 @@ class ProjectController { } if (params.template) { - Template newTemplate = Template.get(params.long('template')) - ProjectType newProjectType = (params.projectType) ? ProjectType.get(params.long('projectType')) : project.projectType - log.debug("Project Type: ${project.projectType}, Template view name ${newTemplate.viewName}") - if ((newProjectType.name == ProjectType.PROJECT_TYPE_AUDIO && !newTemplate.viewName.contains("audio")) || - (newProjectType.name != ProjectType.PROJECT_TYPE_AUDIO && newTemplate.viewName.contains("audio"))) { + if (!isValidTemplateView(((params.projectType) ? ProjectType.get(params.long('projectType') as long) : project.projectType), + Template.get(params.long('template') as long).viewName as String)) { project.errors.rejectValue("template", "project.template.notcompatible", "Template is not compatible with expedition type.") } @@ -766,11 +763,8 @@ class ProjectController { } if (params.template) { - Template newTemplate = Template.get(params.long('template')) - ProjectType newProjectType = (params.projectType) ? ProjectType.get(params.long('projectType')) : project.projectType - log.debug("Project Type: ${project.projectType}, Template view name ${newTemplate.viewName}") - if ((newProjectType.name == ProjectType.PROJECT_TYPE_AUDIO && !newTemplate.viewName.contains("audio")) || - (newProjectType.name != ProjectType.PROJECT_TYPE_AUDIO && newTemplate.viewName.contains("audio"))) { + if (!isValidTemplateView(((params.projectType) ? ProjectType.get(params.long('projectType')) : project.projectType), + Template.get(params.long('template')).viewName)) { project.errors.rejectValue("template", "project.template.notcompatible", "Template is not compatible with expedition type.") return false @@ -805,6 +799,12 @@ class ProjectController { return false } + private isValidTemplateView(ProjectType projectType, String viewName) { + // log.debug("[isValidTemplateView]: ${(projectType.name == ProjectType.PROJECT_TYPE_AUDIO && viewName.contains("audio"))}") + return (projectType.name == ProjectType.PROJECT_TYPE_AUDIO && viewName.contains("audio") || + projectType.name != ProjectType.PROJECT_TYPE_AUDIO && !viewName.contains("audio")) + } + private def generateActivationNotification(Project project) { def message = groovyPageRenderer.render(view: '/project/projectActivationNotification', model: [projectName: project.name]) projectService.emailNotification(project, message, ProjectService.NOTIFICATION_TYPE_ACTIVATION) diff --git a/grails-app/controllers/au/org/ala/volunteer/TemplateController.groovy b/grails-app/controllers/au/org/ala/volunteer/TemplateController.groovy index 1b4e0df0..14f4a577 100644 --- a/grails-app/controllers/au/org/ala/volunteer/TemplateController.groovy +++ b/grails-app/controllers/au/org/ala/volunteer/TemplateController.groovy @@ -5,7 +5,6 @@ import com.google.common.hash.HashCode import grails.converters.JSON import grails.transaction.Transactional import org.apache.commons.io.FilenameUtils -import org.h2.util.StringUtils import org.springframework.dao.DataIntegrityViolationException import org.springframework.web.multipart.MultipartFile @@ -632,7 +631,7 @@ class TemplateController { MultipartFile upload = request.getFile('animal') ?: request.getFile('entry') def fileType = "wildlifespotter" - if (!StringUtils.isNullOrEmpty(params.fileType as String) && params.fileType == "audio") { + if (!Strings.isNullOrEmpty(params.fileType as String) && params.fileType == "audio") { fileType = "audiotranscribe" } diff --git a/grails-app/services/au/org/ala/volunteer/ProjectToolsService.groovy b/grails-app/services/au/org/ala/volunteer/ProjectToolsService.groovy index 2b5c6ee4..ea185649 100644 --- a/grails-app/services/au/org/ala/volunteer/ProjectToolsService.groovy +++ b/grails-app/services/au/org/ala/volunteer/ProjectToolsService.groovy @@ -1,6 +1,6 @@ package au.org.ala.volunteer -import org.h2.util.StringUtils +import com.google.common.base.Strings import org.hibernate.FlushMode class ProjectToolsService { @@ -53,7 +53,7 @@ class ProjectToolsService { def targetField = fieldList.find { it.name.equals(keyField) && it.recordIdx == field.recordIdx } boolean isCandidate = false if (targetField) { - if (StringUtils.isNullOrEmpty(targetField.value?.trim()) || targetField.value.contains("[Ljava.lang.String;")) { + if (Strings.isNullOrEmpty(targetField.value?.trim()) || targetField.value.contains("[Ljava.lang.String;")) { isCandidate = true } } else { diff --git a/grails-app/views/forum/_taskSummary.gsp b/grails-app/views/forum/_taskSummary.gsp index d6cfa661..396d2e40 100644 --- a/grails-app/views/forum/_taskSummary.gsp +++ b/grails-app/views/forum/_taskSummary.gsp @@ -48,7 +48,7 @@ %{----}% - + $(document).ready(function () { setupPanZoom(); diff --git a/grails-app/views/task/showDetails.gsp b/grails-app/views/task/showDetails.gsp index 2971b87c..bb865751 100644 --- a/grails-app/views/task/showDetails.gsp +++ b/grails-app/views/task/showDetails.gsp @@ -211,7 +211,7 @@ - + diff --git a/grails-app/views/template/audioTemplateConfig.gsp b/grails-app/views/template/audioTemplateConfig.gsp index 1c312e70..7d34bce3 100644 --- a/grails-app/views/template/audioTemplateConfig.gsp +++ b/grails-app/views/template/audioTemplateConfig.gsp @@ -291,521 +291,20 @@ - - - - var templateId = ${templateInstance.id}; - var viewParams = ; - var wstc = angular.module('wildlifespottertemplateconfig', ['ngAnimate', 'ngFileUpload']); - - function TemplateConfigController($http, $log, $timeout, $window, Upload) { - var self = this; - self.model = _.defaults(viewParams, { animals: [], categories:[]}); - - function initCategoryUiStatus() { - self.categoryUiStatus = []; - for (var i = 0; i < self.model.categories.length; ++i) { - self.categoryUiStatus.push({minimized: true}); - } - } - - function initAnimalUiStatus() { - self.animalUiStatus = []; - for (var i = 0; i < self.model.animals.length; ++i) { - self.animalUiStatus.push({minimized: true}); - } - } - - function ensureAnimalArray() { - if (angular.isUndefined(self.model.animals) || self.model.animals === null) { - self.model.animals = []; - } - }; - - initCategoryUiStatus(); - initAnimalUiStatus(); - - self.addCategory = function() { - self.model.categories.push({name: '', entries: []}); - self.categoryUiStatus.push({minimized: false}); - }; - - self.removeCategory = function(index) { - self.model.categories.splice(index, 1); - self.categoryUiStatus.splice(index, 1); - }; - - self.addEntry = function(c) { - c.entries.push({name: '', hash: ''}); - }; - - self.removeEntry = function(c, $index) { - c.entries.splice($index, 1); - }; - - self.addAnimal = function() { - ensureAnimalArray(); - self.model.animals.push({vernacularName: '', scientificName: '', description: '', categories: {}, images: [], audio: []}); - self.animalUiStatus.push({minimized: false}); - }; - - self.removeAnimal = function($index) { - self.model.animals.splice($index, 1); - self.animalUiStatus.splice($index, 1); - }; - - self.addBlankImage = function(a) { - ensureImagesArray(a); - a.images.push({hash: ''}); - }; - - function ensureImagesArray(a) { - if (angular.isUndefined(a.images) || a.images === null) { - a.images = []; - } - }; - - function ensureAudioArray(a) { - if (angular.isUndefined(a.audio) || a.audio === null) { - a.audio = []; - } - }; - - self.removeImage = function(a, $index) { - a.images.splice($index, 1); - }; - - self.removeAudio = function(a, $index) { - a.audio.splice($index, 1); - }; - - self.addManyFiles = function(animal, category, $files, fileType) { - for (var i = 0; i < $files.length; i++) { - var file = $files[i]; - var hashable; - if (category) { - hashable = {name: $files[i].name, hash: ''}; - category.entries.push(hashable); - } else { - hashable = {hash: ''}; - if (fileType === 'image') { - ensureImagesArray(animal); - animal.images.push(hashable); - } else if (fileType === 'audio') { - ensureAudioArray(animal); - animal.audio.push(hashable); - } - } - - uploadFile(hashable, file, fileType); - } - }; - - self.addManyImages = function(animal, category, $files) { - self.addManyFiles(animal, category, $files, 'image'); - }; - - self.addManyAudio = function(animal, category, $files) { - self.addManyFiles(animal, category, $files, 'audio'); - }; - - self.addFile = function(entryOrAnimalArray, $index, $file, fileType) { - var entryOrAnimal = entryOrAnimalArray[$index]; - if (!$file) { - entryOrAnimal.hash = ''; - return; - } - - uploadFile(entryOrAnimal, $file[0], fileType); - }; - - self.addImage = function(entryOrAnimalArray, $index, $file) { - self.addFile(entryOrAnimalArray, $index, $file, 'image'); - }; - - self.addAudio = function(entryOrAnimalArray, $index, $file) { - self.addFile(entryOrAnimalArray, $index, $file, 'audio'); - }; - - function uploadFile(hashable, file, type) { - var data; - if ("name" in hashable) { - data = {entry: file}; - } else { - data = {animal: file}; - } - var name = file.name; - var submitUrl = ""; - if (type !== undefined && type === 'audio') { - submitUrl = ""; - } - - Upload.upload({ - url: submitUrl, - data: data - }).then(function (resp) { - $log.debug('Success ' + name + ' uploaded. Response: ' + JSON.stringify(resp.data)); - hashable.hash = resp.data.hash; - hashable.ext = resp.data.format; - }, function (resp) { - bootbox.alert("Image upload failed"); - $log.error('Error status: ' + resp.status); - }, function (evt) { - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); - $log.info('progress: ' + progressPercentage + '% ' + name); - }); - }; - - function uploadImage(hashable, file) { - uploadFile(hashable, file, 'image'); - }; - - function uploadAudio(hashable, file) { - uploadFile(hashable, file, 'audio'); - }; - - self.moveCategoryUp = function(index) { - self.moveUp(self.model.categories, index); - self.moveUp(self.categoryUiStatus, index); - }; - - self.moveCategoryDown = function(index) { - self.moveDown(self.model.categories, index); - self.moveDown(self.categoryUiStatus, index); - }; - - self.moveAnimalUp = function(index) { - self.moveUp(self.model.animals, index); - self.moveUp(self.animalUiStatus, index); - }; - - self.moveAnimalDown = function(index) { - self.moveDown(self.model.animals, index); - self.moveDown(self.animalUiStatus, index); - }; - - self.moveUp = function(a, $index) { - if ($index <= 0) { - return; - } - var a1 = a[$index]; - a[$index] = a[$index - 1]; - a[$index - 1] = a1; - }; - - self.moveDown = function(a, $index) { - if ($index >= (a.length - 1)) { - return; - } - var a1 = a[$index]; - a[$index] = a[$index + 1]; - a[$index + 1] = a1; - }; - - self.sortAnimals = function() { - self.minimizeAll(self.animalUiStatus); - - self.model.animals.sort(function(a,b) { - var nameA = self.fullName(a).toUpperCase(); - var nameB = self.fullName(b).toUpperCase(); - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - - // names must be equal - return 0; - }); - }; - - self.minimizeAll = function(array) { - for (var i = 0; i < array.length; ++i) { - array[i].minimized = true; - } - }; - - var imageUrlTemplate = ""; - - var audioUrlTemplate = ""; - - self.entryUrl = function(e) { - var url = ""; - if (!angular.isUndefined(e.hash) && e.hash !== "") { - url = imageUrlTemplate.replace("{{name}}", e.hash).replace("{{width}}", "100").replace("{{height}}", "100").replace("{{format}}", "png"); - } - return url; - }; - - self.imageUrl = function(i) { - var url = ""; - if (!angular.isUndefined(i.hash) && i.hash !== "") { - url = imageUrlTemplate.replace("{{name}}", i.hash).replace("{{width}}", "150").replace("{{height}}", "150").replace("{{format}}", i.ext); - } - return url; - }; - - self.audioUrl = function(i) { - var url = audioUrlTemplate.replace("{{name}}", i.hash).replace("{{format}}", i.ext); - return url; - } - - self.fullName = function(a) { - if (a.vernacularName && a.scientificName) { - return a.vernacularName + " (" + a.scientificName+")"; - } else if (a.vernacularName) { - return a.vernacularName; - } else if (a.scientificName) { - return a.scientificName; - } else { - return ''; - } - }; - - self.categoryChange = function(cat) { - var oldValue = cat.prevName; - var newValue= cat.name; - var model = self.model; - if (!(typeof oldValue === 'undefined')) { - for (var i = 0; i < model.animals.length; ++i) { - var animal = model.animals[i]; - if (oldValue in animal.categories) { - animal.categories[newValue] = animal.categories[oldValue]; - delete animal.categories[oldValue]; - } - } - } - cat.prevName = cat.name; - }; - - self.entryChange = function(cat, entry) { - var oldValue = entry.prevName; - var newValue= entry.name; - var model = self.model; - if (!(typeof oldValue === 'undefined')) { - for (var i = 0; i < model.animals.length; ++i) { - var animal = model.animals[i]; - if (animal.categories[cat.name] === oldValue) { - animal.categories[cat.name] = newValue; - } - } - } - entry.prevName = entry.name; - }; - - self.uploadCategoryJSON = function($file) { - var reader = new FileReader(); - - reader.onload = function(e) { - // Render thumbnail. - var categories = JSON.parse(e.target.result); - console.log(categories); - - // TODO sanity check - if (!Array.isArray(categories)) { - bootbox.alert("Uploaded file is not an array"); - return; - } - - var catDefaults = { name: '', entries: [] }; - var entryDefaults = { hash: '', name: '' }; - - categories = categories.map(function(v,i,l) { - var cat = _.defaults(v, catDefaults); - cat.entries = cat.entries.map(function(v2,i2,l2) { - return _.defaults(v2, entryDefaults); - }); - return cat; - }); - - self.model.categories = categories; - }; - - // Read in the image file as a data URL. - reader.readAsText($file); - }; - - self.downloadCategoryJSON = function($file) { - var data = JSON.stringify(angular.copy(self.model.categories).map(function(v,i,l) { - var cat = _.omit(v, 'prevName'); - cat.entries = v.entries.map(function(v2,i2,l2) { - return _.omit(v2, 'prevName'); - }); - return cat; - }), null, 2); - var fileName = "" + templateId + "-categories.json"; - downloadFile(data, 'application/json', fileName); - }; - - self.uploadAnimalCSV = function($file) { - CSV.fetch({ - file: $file - }).done(function(dataset) { - $log.info(dataset); - - var normalisedFields = _.map(dataset.fields, function(v,i) { - var key = findCategory(v); - if (!key) bootbox.alert("Unknown category/column: " + v); - return key || v; - }); - - var defaultCats = _.chain(self.model.categories).pluck('name').map(function(v) { return [v, null] }).object().value(); - var catNames = _.chain(self.model.categories).pluck('name').value(); - var animalDefaults = { - vernacularName: '', - scientificName: '', - description: '', - images: [], - audio: [], - categories: defaultCats - }; - - var animals = _.map(dataset.records, function(v,i,l) { - var row = _.chain(v).map(function(v2,i2) { - var field = normalisedFields[i2]; - return [field, v2]; - }).value(); - - var animal = _.chain(row).filter(function(v2,i2) { - return _.contains(['vernacularName','scientificName','description','images'], v2[0]); - }).object().value(); - - if (animal.images) animal.images = animal.images.split(',').map(function (s,i,l) { return {hash: s.trim()}; }); - else animal.images = []; - - var categories = _.chain(row).filter(function(v2,i2) { - var field = v2[0]; - return !_.contains(['vernacularName','scientificName','description','images'], field) && _.contains(catNames, field); - }).map(function(v2,i2) { - var field = v2[0]; - var origValue = v2[1]; - var value = findCatEntry(field, origValue); - if (origValue && !value) { - bootbox.alert("Can't find a matching value for row " + (i+2) + ", vernacular name " + - animal.vernacularName + ", category " + field + ", value of " + origValue ); - } - return [ field, value ]; - }).object().value(); - - animal.categories = _.defaults(categories, defaultCats); - - animal = _.defaults(animal, animalDefaults); - if (animal.vernacularName == null) animal.vernacularName = ''; - if (animal.scientificName == null) animal.scientificName = ''; - if (animal.description == null) animal.description = ''; - - return animal; - }); - - $log.info(animals); - - self.model.animals = animals; - initAnimalUiStatus(); - - }).fail(function(e) { - bootbox.alert("Couldn't read CSV:" + e); - }); - }; - - function findCategory(s) { - var S = s.toUpperCase(); - var vn = 'vernacularName', sn = 'scientificName', desc = 'description', images = 'images'; - if (S === vn.toUpperCase()) return vn; - if (S === sn.toUpperCase()) return sn; - if (S === desc.toUpperCase()) return desc; - if (S === images.toUpperCase()) return images; - for (var i = 0; i < self.model.categories.length; ++i) { - var c = self.model.categories[i]; - var name = (c.name || '').toUpperCase(); - if (name === S) { - return c.name; - } - } - return null; - } - - function findCatEntry(cat, value) { - var V = (value || '').toUpperCase(); - var c = _.findWhere(self.model.categories, { name: cat }); - - if (!c) return null; - - for (var i = 0; i < c.entries.length; ++i) { - var e = c.entries[i]; - var name = (e.name || '').toUpperCase(); - if (name === V) { - return e.name; - } - } - - return null; - } - - self.downloadAnimalCSV = function() { - var fields = ["vernacularName","scientificName","description"].concat(_.pluck(self.model.categories, "name"),["images"]); - var records = self.model.animals.map(function(v,i,l) { - var start = [v.vernacularName, v.scientificName, v.description]; - var cats = self.model.categories.map(function(v2,i2,l2) { - return v.categories[v2.name]; - }); - var images = (v.images || []).map(function(v2,i2,l2) { return v2.hash}).join(','); - return start.concat(cats,[images]); - }); - var data = CSV.serialize([fields].concat(records)); - var fileName = "" + templateId + "-animals.csv"; - downloadFile(data, 'text/csv', fileName); - }; - - function downloadFile(data, contentType, fileName) { - var file = new Blob([data], {type: contentType}); - var a = document.createElement("a"); - document.body.appendChild(a); - var fileURL = $window.URL.createObjectURL(file); - a.href = fileURL; - a.download = fileName; - a.click(); - document.body.removeChild(a); - $timeout(function() { - $log.info("Revoking " + fileName); - $window.URL.revokeObjectURL(fileURL); - }, 60 * 1000, false); - } - - function filterModel() { - var results = _.chain(self.model.animals).zip(self.animalUiStatus).filter(function(e,i,l) { - var animal = e[0]; - var result = animal.scientificName || animal.vernacularName || animal.description || (animal.images && animal.images.length && animal.images.some(function(v) { return !!v.hash})); - return result; - }).unzip().value(); - self.model.animals = results[0]; - self.animalUiStatus = results[1]; - - results = _.chain(self.model.categories).zip(self.categoryUiStatus).filter(function(e,i,l) { - var cat = e[0]; - var result = cat.name || (cat.entries && cat.entries.length && cat.entries.every(function(entry) { - return !!entry.name || !!entry.hash; - })); - return result; - }).unzip().value(); - self.model.categories = results[0]; - self.categoryUiStatus = results[1]; - } + var T_CONF = { + templateId:${templateInstance.id}, + viewParams:, + submitUrl: "", + audioSubmitUrl: "", + imageUrlTemplate: "", + audioUrlTemplate: "", + saveTemplateUrl: "" + }; + - self.save = function() { - filterModel(); - var p = $http.post("", self.model); - p.then(function(response) { - bootbox.alert("Saved!"); - }, function(response) { - bootbox.alert("Couldn't save WildlifeSpotter config"); - }); - }; - } + + - wstc.controller('TemplateConfigController', TemplateConfigController); - diff --git a/grails-app/views/template/wildlifeTemplateConfig.gsp b/grails-app/views/template/wildlifeTemplateConfig.gsp index ec8bfa9e..90df7f98 100644 --- a/grails-app/views/template/wildlifeTemplateConfig.gsp +++ b/grails-app/views/template/wildlifeTemplateConfig.gsp @@ -243,457 +243,21 @@ - - var templateId = ${templateInstance.id}; - var viewParams = ; - var wstc = angular.module('wildlifespottertemplateconfig', ['ngAnimate', 'ngFileUpload']); - - function TemplateConfigController($http, $log, $timeout, $window, Upload) { - var self = this; - self.model = _.defaults(viewParams, { animals: [], categories:[]}); - - function initCategoryUiStatus() { - self.categoryUiStatus = []; - for (var i = 0; i < self.model.categories.length; ++i) { - self.categoryUiStatus.push({minimized: true}); - } - } - - function initAnimalUiStatus() { - self.animalUiStatus = []; - for (var i = 0; i < self.model.animals.length; ++i) { - self.animalUiStatus.push({minimized: true}); - } - } - - initCategoryUiStatus(); - initAnimalUiStatus(); - - - self.addCategory = function() { - self.model.categories.push({name: '', entries: []}); - self.categoryUiStatus.push({minimized: false}); - }; - - self.removeCategory = function(index) { - self.model.categories.splice(index, 1); - self.categoryUiStatus.splice(index, 1); - }; - - self.addEntry = function(c) { - c.entries.push({name: '', hash: ''}); - }; - - self.removeEntry = function(c, $index) { - c.entries.splice($index, 1); - }; - - self.addAnimal = function() { - self.model.animals.push({vernacularName: '', scientificName: '', description: '', categories: {}, images: []}); - self.animalUiStatus.push({minimized: false}); - }; - - self.removeAnimal = function($index) { - self.model.animals.splice($index, 1); - self.animalUiStatus.splice($index, 1); - }; - - self.addBlankImage = function(a) { - ensureImagesArray(a); - a.images.push({hash: ''}); - }; - - function ensureImagesArray(a) { - if (angular.isUndefined(a.images) || a.images === null) { - a.images = []; - } - } - - self.removeImage = function(a, $index) { - a.images.splice($index, 1); - }; - - self.addManyImages = function(animal, category, $files) { - for (var i = 0; i < $files.length; i++) { - var file = $files[i]; - var hashable; - if (category) { - hashable = {name: $files[i].name, hash: ''}; - category.entries.push(hashable); - } else { - hashable = {hash: ''}; - ensureImagesArray(hashable); - animal.images.push(hashable); - } - - uploadImage(hashable, file); - } - }; - - self.addImage = function(entryOrAnimalArray, $index, $file) { - var entryOrAnimal = entryOrAnimalArray[$index]; - if (!$file) { - entryOrAnimal.hash = ''; - return; - } - - uploadImage(entryOrAnimal, $file[0]); - }; - - function uploadImage(hashable, file) { - var data; - if ("name" in hashable) { - data = {entry: file}; - } else { - data = {animal: file}; - } - var name = file.name; - - Upload.upload({ - url: "", - data: data - }).then(function (resp) { - $log.debug('Success ' + name + ' uploaded. Response: ' + JSON.stringify(resp.data)); - hashable.hash = resp.data.hash; - hashable.ext = resp.data.format; - }, function (resp) { - bootbox.alert("Image upload failed"); - $log.error('Error status: ' + resp.status); - }, function (evt) { - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); - $log.info('progress: ' + progressPercentage + '% ' + name); - }); - } - - self.moveCategoryUp = function(index) { - self.moveUp(self.model.categories, index); - self.moveUp(self.categoryUiStatus, index); - }; - - self.moveCategoryDown = function(index) { - self.moveDown(self.model.categories, index); - self.moveDown(self.categoryUiStatus, index); - }; - - self.moveAnimalUp = function(index) { - self.moveUp(self.model.animals, index); - self.moveUp(self.animalUiStatus, index); - }; - - self.moveAnimalDown = function(index) { - self.moveDown(self.model.animals, index); - self.moveDown(self.animalUiStatus, index); - }; - - self.moveUp = function(a, $index) { - if ($index <= 0) { - return; - } - var a1 = a[$index]; - a[$index] = a[$index - 1]; - a[$index - 1] = a1; - }; - - self.moveDown = function(a, $index) { - if ($index >= (a.length - 1)) { - return; - } - var a1 = a[$index]; - a[$index] = a[$index + 1]; - a[$index + 1] = a1; - }; - - self.sortAnimals = function() { - self.minimizeAll(self.animalUiStatus); - - self.model.animals.sort(function(a,b) { - var nameA = self.fullName(a).toUpperCase(); - var nameB = self.fullName(b).toUpperCase(); - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - - // names must be equal - return 0; - }); - }; - - self.minimizeAll = function(array) { - for (var i = 0; i < array.length; ++i) { - array[i].minimized = true; - } - }; - - var imageUrlTemplate = ""; - - self.entryUrl = function(e) { - var url = imageUrlTemplate.replace("{{name}}", e.hash).replace("{{width}}", "100").replace("{{height}}", "100").replace("{{format}}", "png"); - return url; - }; - - self.imageUrl = function(i) { - var url = imageUrlTemplate.replace("{{name}}", i.hash).replace("{{width}}", "150").replace("{{height}}", "150").replace("{{format}}", "jpg"); - return url; - }; - - self.fullName = function(a) { - if (a.vernacularName && a.scientificName) { - return a.vernacularName + " (" + a.scientificName+")"; - } else if (a.vernacularName) { - return a.vernacularName; - } else if (a.scientificName) { - return a.scientificName; - } else { - return ''; - } - }; - - self.categoryChange = function(cat) { - var oldValue = cat.prevName; - var newValue= cat.name; - var model = self.model; - if (!(typeof oldValue === 'undefined')) { - for (var i = 0; i < model.animals.length; ++i) { - var animal = model.animals[i]; - if (oldValue in animal.categories) { - animal.categories[newValue] = animal.categories[oldValue]; - delete animal.categories[oldValue]; - } - } - } - cat.prevName = cat.name; - }; - - self.entryChange = function(cat, entry) { - var oldValue = entry.prevName; - var newValue= entry.name; - var model = self.model; - if (!(typeof oldValue === 'undefined')) { - for (var i = 0; i < model.animals.length; ++i) { - var animal = model.animals[i]; - if (animal.categories[cat.name] === oldValue) { - animal.categories[cat.name] = newValue; - } - } - } - entry.prevName = entry.name; - }; - - self.uploadCategoryJSON = function($file) { - var reader = new FileReader(); - - reader.onload = function(e) { - // Render thumbnail. - var categories = JSON.parse(e.target.result); - console.log(categories); - - // TODO sanity check - if (!Array.isArray(categories)) { - bootbox.alert("Uploaded file is not an array"); - return; - } - - var catDefaults = { name: '', entries: [] }; - var entryDefaults = { hash: '', name: '' }; - - categories = categories.map(function(v,i,l) { - var cat = _.defaults(v, catDefaults); - cat.entries = cat.entries.map(function(v2,i2,l2) { - return _.defaults(v2, entryDefaults); - }); - return cat; - }); - - self.model.categories = categories; - }; - - // Read in the image file as a data URL. - reader.readAsText($file); - }; - - self.downloadCategoryJSON = function($file) { - var data = JSON.stringify(angular.copy(self.model.categories).map(function(v,i,l) { - var cat = _.omit(v, 'prevName'); - cat.entries = v.entries.map(function(v2,i2,l2) { - return _.omit(v2, 'prevName'); - }); - return cat; - }), null, 2); - var fileName = "" + templateId + "-categories.json"; - downloadFile(data, 'application/json', fileName); - }; - - self.uploadAnimalCSV = function($file) { - CSV.fetch({ - file: $file - }).done(function(dataset) { - $log.info(dataset); - - var normalisedFields = _.map(dataset.fields, function(v,i) { - var key = findCategory(v); - if (!key) bootbox.alert("Unknown category/column: " + v); - return key || v; - }); - - var defaultCats = _.chain(self.model.categories).pluck('name').map(function(v) { return [v, null] }).object().value(); - var catNames = _.chain(self.model.categories).pluck('name').value(); - var animalDefaults = { - vernacularName: '', - scientificName: '', - description: '', - images: [], - categories: defaultCats - }; - - var animals = _.map(dataset.records, function(v,i,l) { - var row = _.chain(v).map(function(v2,i2) { - var field = normalisedFields[i2]; - return [field, v2]; - }).value(); - - var animal = _.chain(row).filter(function(v2,i2) { - return _.contains(['vernacularName','scientificName','description','images'], v2[0]); - }).object().value(); - - if (animal.images) animal.images = animal.images.split(',').map(function (s,i,l) { return {hash: s.trim()}; }); - else animal.images = []; - - var categories = _.chain(row).filter(function(v2,i2) { - var field = v2[0]; - return !_.contains(['vernacularName','scientificName','description','images'], field) && _.contains(catNames, field); - }).map(function(v2,i2) { - var field = v2[0]; - var origValue = v2[1]; - var value = findCatEntry(field, origValue); - if (origValue && !value) { - bootbox.alert("Can't find a matching value for row " + (i+2) + ", vernacular name " + - animal.vernacularName + ", category " + field + ", value of " + origValue ); - } - return [ field, value ]; - }).object().value(); - - animal.categories = _.defaults(categories, defaultCats); - - animal = _.defaults(animal, animalDefaults); - if (animal.vernacularName == null) animal.vernacularName = ''; - if (animal.scientificName == null) animal.scientificName = ''; - if (animal.description == null) animal.description = ''; - - return animal; - }); - - $log.info(animals); - - self.model.animals = animals; - initAnimalUiStatus(); - - }).fail(function(e) { - bootbox.alert("Couldn't read CSV:" + e); - }); - }; - - function findCategory(s) { - var S = s.toUpperCase(); - var vn = 'vernacularName', sn = 'scientificName', desc = 'description', images = 'images'; - if (S === vn.toUpperCase()) return vn; - if (S === sn.toUpperCase()) return sn; - if (S === desc.toUpperCase()) return desc; - if (S === images.toUpperCase()) return images; - for (var i = 0; i < self.model.categories.length; ++i) { - var c = self.model.categories[i]; - var name = (c.name || '').toUpperCase(); - if (name === S) { - return c.name; - } - } - return null; - } - - function findCatEntry(cat, value) { - var V = (value || '').toUpperCase(); - var c = _.findWhere(self.model.categories, { name: cat }); - - if (!c) return null; - - for (var i = 0; i < c.entries.length; ++i) { - var e = c.entries[i]; - var name = (e.name || '').toUpperCase(); - if (name === V) { - return e.name; - } - } - - return null; - } - - self.downloadAnimalCSV = function() { - var fields = ["vernacularName","scientificName","description"].concat(_.pluck(self.model.categories, "name"),["images"]); - var records = self.model.animals.map(function(v,i,l) { - var start = [v.vernacularName, v.scientificName, v.description]; - var cats = self.model.categories.map(function(v2,i2,l2) { - return v.categories[v2.name]; - }); - var images = (v.images || []).map(function(v2,i2,l2) { return v2.hash}).join(','); - return start.concat(cats,[images]); - }); - var data = CSV.serialize([fields].concat(records)); - var fileName = "" + templateId + "-animals.csv"; - downloadFile(data, 'text/csv', fileName); - }; - - function downloadFile(data, contentType, fileName) { - var file = new Blob([data], {type: contentType}); - var a = document.createElement("a"); - document.body.appendChild(a); - var fileURL = $window.URL.createObjectURL(file); - a.href = fileURL; - a.download = fileName; - a.click(); - document.body.removeChild(a); - $timeout(function() { - $log.info("Revoking " + fileName); - $window.URL.revokeObjectURL(fileURL); - }, 60 * 1000, false); - } - - function filterModel() { - var results = _.chain(self.model.animals).zip(self.animalUiStatus).filter(function(e,i,l) { - var animal = e[0]; - var result = animal.scientificName || animal.vernacularName || animal.description || (animal.images && animal.images.length && animal.images.some(function(v) { return !!v.hash})); - return result; - }).unzip().value(); - self.model.animals = results[0]; - self.animalUiStatus = results[1]; - - results = _.chain(self.model.categories).zip(self.categoryUiStatus).filter(function(e,i,l) { - var cat = e[0]; - var result = cat.name || (cat.entries && cat.entries.length && cat.entries.every(function(entry) { - return !!entry.name || !!entry.hash; - })); - return result; - }).unzip().value(); - self.model.categories = results[0]; - self.categoryUiStatus = results[1]; - } + var T_CONF = { + templateId:${templateInstance.id}, + viewParams:, + submitUrl: "", + audioSubmitUrl: "", + imageUrlTemplate: "", + audioUrlTemplate: "", + saveTemplateUrl: "" + }; + - self.save = function() { - filterModel(); - var p = $http.post("", self.model); - p.then(function(response) { - bootbox.alert("Saved!"); - }, function(response) { - bootbox.alert("Couldn't save WildlifeSpotter config"); - }); - }; - } + + - wstc.controller('TemplateConfigController', TemplateConfigController); - From bca57330a30c4852be50d7f6a1be3502e4edda80 Mon Sep 17 00:00:00 2001 From: Chris Dunstall Date: Sat, 23 Apr 2022 16:05:39 +1000 Subject: [PATCH 3/7] #496 Code cleanup post peer review --- .../au/org/ala/volunteer/UserService.groovy | 18 ++---------- grails-app/views/task/showDetails.gsp | 1 - .../templateViews/audioTranscribe.gsp | 11 -------- .../templateViews/cameratrapTranscribe.gsp | 4 --- .../wildlifespotterTranscribe.gsp | 4 --- src/main/resources/digivol-ehcache.xml | 28 +++++++++++++++++++ 6 files changed, 31 insertions(+), 35 deletions(-) diff --git a/grails-app/services/au/org/ala/volunteer/UserService.groovy b/grails-app/services/au/org/ala/volunteer/UserService.groovy index b2427dfb..7b5732a8 100644 --- a/grails-app/services/au/org/ala/volunteer/UserService.groovy +++ b/grails-app/services/au/org/ala/volunteer/UserService.groovy @@ -6,6 +6,7 @@ import au.org.ala.userdetails.UserDetailsClient import au.org.ala.web.UserDetails import com.google.common.base.Stopwatch import com.google.common.base.Strings +import grails.plugin.cache.Cacheable import grails.transaction.NotTransactional import grails.transaction.Transactional import grails.web.servlet.mvc.GrailsParameterMap @@ -323,25 +324,11 @@ class UserService { // - If the provided project matches the role's project, this is a project-level role - return true. log.debug("Checking if user has validator role") def user = User.findByUserId(userId) -// if (user) { -// def validatorRole = Role.findByNameIlike(BVPRole.VALIDATOR) -// def role = user.userRoles.find { -// it.role.id == validatorRole.id && ((it.institution == null && it.project == null) || -// projectId == null || -// (it.institution != null && it.institution?.id == projectInstitutionId) || -// it.project?.id == projectId) -// } -// if (role) { -// // a role exists for the current user and the specified project/institution (or the user has a role with a null project and null institution -// // indicating that they can validate tasks from any project and or institution) -// return true -// } -// } -// return false return userHasValidatorRole(user, projectId, projectInstitutionId) } + @Cacheable(value = 'UserHasValidator', key = "(#user?.id?.toString()?:'-1') + (#projectId?:'-1') + (#projectInstitutionId?:'-1')") boolean userHasValidatorRole(User user, Long projectId, Long projectInstitutionId = null) { if (user) { @@ -374,6 +361,7 @@ class UserService { * @param role the role to query * @return true of the user has the role, false if not. */ + @Cacheable(value = 'UserHasCasRole', key = "(#user?.id?.toString()?:'-1') + (#role?:'-1')") boolean hasCasRole(User user, String role) { if (!user) return false def serviceResults = [:] diff --git a/grails-app/views/task/showDetails.gsp b/grails-app/views/task/showDetails.gsp index bb865751..6bc32b62 100644 --- a/grails-app/views/task/showDetails.gsp +++ b/grails-app/views/task/showDetails.gsp @@ -163,7 +163,6 @@ -%{-- --}% diff --git a/grails-app/views/transcribe/templateViews/audioTranscribe.gsp b/grails-app/views/transcribe/templateViews/audioTranscribe.gsp index 48ea7991..a93308a1 100644 --- a/grails-app/views/transcribe/templateViews/audioTranscribe.gsp +++ b/grails-app/views/transcribe/templateViews/audioTranscribe.gsp @@ -57,10 +57,6 @@ class="btn btn-success bvp-submit-button ${validator ? '' : 'hidden'}"> ${message(code: 'default.button.validate.label', default: 'Submit validation')} -%{-- --}% @@ -237,13 +233,6 @@
{{name}} ({{scientificName}})
-%{-- --}% -%{-- --}% -%{-- --}%
diff --git a/grails-app/views/transcribe/templateViews/cameratrapTranscribe.gsp b/grails-app/views/transcribe/templateViews/cameratrapTranscribe.gsp index 5a894cb9..eeb638ac 100644 --- a/grails-app/views/transcribe/templateViews/cameratrapTranscribe.gsp +++ b/grails-app/views/transcribe/templateViews/cameratrapTranscribe.gsp @@ -295,10 +295,6 @@ class="btn btn-success btn-lg bvp-submit-button ${validator ? '' : 'hidden'}"> ${message(code: 'default.button.validate.label', default: 'Submit validation')} -%{-- --}% diff --git a/grails-app/views/transcribe/templateViews/wildlifespotterTranscribe.gsp b/grails-app/views/transcribe/templateViews/wildlifespotterTranscribe.gsp index 071e55dc..1f1a28dc 100644 --- a/grails-app/views/transcribe/templateViews/wildlifespotterTranscribe.gsp +++ b/grails-app/views/transcribe/templateViews/wildlifespotterTranscribe.gsp @@ -61,10 +61,6 @@ class="btn btn-success bvp-submit-button ${validator ? '' : 'hidden'}"> ${message(code: 'default.button.validate.label', default: 'Submit validation')} -%{-- --}% diff --git a/src/main/resources/digivol-ehcache.xml b/src/main/resources/digivol-ehcache.xml index 171ec36d..c46c3c65 100644 --- a/src/main/resources/digivol-ehcache.xml +++ b/src/main/resources/digivol-ehcache.xml @@ -285,6 +285,34 @@ + + + + Date: Sat, 23 Apr 2022 16:14:29 +1000 Subject: [PATCH 4/7] #409 Set background save and timeout values for production --- grails-app/views/layouts/digivol-task.gsp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grails-app/views/layouts/digivol-task.gsp b/grails-app/views/layouts/digivol-task.gsp index 43e4c1a0..7b571fa6 100644 --- a/grails-app/views/layouts/digivol-task.gsp +++ b/grails-app/views/layouts/digivol-task.gsp @@ -735,7 +735,7 @@ $(document).ready(function() { // prompt user to save if page has been open for too long - var taskLockTimeout = 2 * 60; // Seconds ####### ---> SET TO 90 FOR RELEASE + var taskLockTimeout = 90 * 60; // 90 mins in Seconds setPageTimeoutTimer(); function setPageTimeoutTimer() { @@ -969,7 +969,7 @@ } $(document).ready(function() { - var bgSaveTimer = 2 * 60; // SET TO 15 FOR RELEASE + var bgSaveTimer = 15 * 60; // 15 minutes in seconds var timerInitial = 0; From a40c74482fa75b8ac4c383d2df7f6abfdfff64ee Mon Sep 17 00:00:00 2001 From: Chris Dunstall Date: Mon, 25 Apr 2022 11:28:44 +1000 Subject: [PATCH 5/7] #492 Updated query to protect against SQL injection --- .../org/ala/volunteer/VolunteerStatsService.groovy | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/grails-app/services/au/org/ala/volunteer/VolunteerStatsService.groovy b/grails-app/services/au/org/ala/volunteer/VolunteerStatsService.groovy index 192aa3df..1d80f131 100644 --- a/grails-app/services/au/org/ala/volunteer/VolunteerStatsService.groovy +++ b/grails-app/services/au/org/ala/volunteer/VolunteerStatsService.groovy @@ -396,11 +396,17 @@ class VolunteerStatsService { def parameters = [:] if (tags?.size() > 0) { - def tagList = tags.join("','") + def tagParams = tags.withIndex().collectEntries { tag, index -> + [('tag' + index): tag] + } + log.debug("Tag string: ${tagParams}") + labelJoin = """\ join project_labels on (project_labels.project_id = project.id) - join label on (label.id = project_labels.label_id and label.value in ('${tagList}')) """ - log.debug("tagList: ${tagList}") + join label on (label.id = project_labels.label_id and label.value in (${tagParams.keySet().collect { ':' + it }.join(',')})) """ + + parameters.putAll(tagParams) + log.debug("labelJoin: ${labelJoin}") } From 2e042b2b2ef7785c10a2b945c7a1568e7cc751ee Mon Sep 17 00:00:00 2001 From: Chris Dunstall Date: Mon, 2 May 2022 12:09:30 +1000 Subject: [PATCH 6/7] #481 refactored template config code to work with grails/angular --- grails-app/assets/javascripts/template-config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grails-app/assets/javascripts/template-config.js b/grails-app/assets/javascripts/template-config.js index bd225fda..8ed93c71 100644 --- a/grails-app/assets/javascripts/template-config.js +++ b/grails-app/assets/javascripts/template-config.js @@ -2,7 +2,7 @@ var templateId = T_CONF.templateId; //${templateInstance.id}; var viewParams = T_CONF.viewParams; //; var wstc = angular.module('wildlifespottertemplateconfig', ['ngAnimate', 'ngFileUpload']); -function TemplateConfigController($http, $log, $timeout, $window, Upload) { +var TemplateConfigController = ['$http', '$log', '$timeout', '$window', 'Upload', function ($http, $log, $timeout, $window, Upload) { var self = this; self.model = _.defaults(viewParams, { animals: [], categories:[]}); @@ -505,6 +505,6 @@ function TemplateConfigController($http, $log, $timeout, $window, Upload) { bootbox.alert("Couldn't save WildlifeSpotter config"); }); }; -} +}]; wstc.controller('TemplateConfigController', TemplateConfigController); \ No newline at end of file From 838dd4692d7b3aee48bd473e66a5c6f9872789b3 Mon Sep 17 00:00:00 2001 From: Chris Dunstall Date: Tue, 3 May 2022 10:24:00 +1000 Subject: [PATCH 7/7] Set version number for release --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 597ba2d9..46ed3506 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { id "com.virgo47.ClasspathJar" version "1.0.0" } -version "6.1.1-SNAPSHOT" +version "6.1.1" group "au.org.ala" description "Digivol application"