Skip to content

Commit ab39ddb

Browse files
committed
Merge branch 'release/6.1.8'
2 parents 7237d05 + e6e60d1 commit ab39ddb

27 files changed

+469
-224
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# DigiVol [![Build Status](https://travis-ci.org/AtlasOfLivingAustralia/volunteer-portal.svg?branch=develop)](https://travis-ci.org/AtlasOfLivingAustralia/volunteer-portal)
22

33
The [Atlas of Living Australia], in collaboration with the [Australian Museum], developed [DigiVol]
4-
to harness the power of online volunteers (also known as crowdsourcing) to digitise biodiversity data that is locked up
5-
in biodiversity collections, field notebooks and survey sheets.
4+
to harness the power of online volunteers (also known as crowdsourcing) to digitise biodiversity data that is locked
5+
up in biodiversity collections, field notebooks and survey sheets.
66

77
## Running
88

@@ -33,7 +33,7 @@ ansible-playbook -i inventories/vagrant --user vagrant --private-key ~/.vagrant.
3333

3434
## Contributing
3535

36-
DigiVol is a [Grails] v3.2.4 based web application. It requires [PostgreSQL] for data storage. Development follows the
36+
DigiVol is a [Grails] v5.3 based web application. It requires [PostgreSQL] v15 for data storage. Development follows the
3737
[git flow] workflow.
3838

3939
For git flow operations you may like to use the `git-flow` command line tools. Either install [Atlassian SourceTree]

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ plugins {
2424
id "com.dorongold.task-tree" version "2.1.1"
2525
}
2626

27-
version "6.1.7"
27+
version "6.1.8"
2828
group "au.org.ala"
2929
description "Digivol application"
3030

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ springBootVersion=2.7.9
66
org.gradle.daemon=true
77
org.gradle.parallel=true
88
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M
9-
alaAuthVersion=6.0.3
9+
alaAuthVersion=6.2.0
1010

1111
#grailsWrapperVersion=1.0.0
1212
gradleWrapperVersion=5.0

grails-app/assets/javascripts/transcribe/wildlifespotter.js

+71-19
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
1818

1919
var selectedIndicies = {};
2020

21+
// Default save button to disabled until a selection has been made.
22+
$('#btnSave').attr('disabled', 'disabled');
23+
2124
// selection
2225
$('#ct-container').on('click', '.ws-selector', function() {
2326
var $this = $(this);
2427
var index = $this.closest('[data-item-index]').data('item-index');
25-
toggleIndex(index);
28+
var validationtype = $this.data('validationType');
29+
toggleIndex(index, validationtype);
2630
});
2731

2832
$('#ct-container').on('click', '.animalDelete', function() {
@@ -31,16 +35,36 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
3135
deselectIndex(index);
3236
});
3337

34-
function toggleIndex(index) {
38+
$('input[name=recordValues\\.0\\.noAnimalsVisible]').change(function() {
39+
checkCheckboxValues();
40+
});
41+
42+
$('input[name=recordValues\\.0\\.problemWithImage]').change(function() {
43+
checkCheckboxValues();
44+
});
45+
46+
function checkCheckboxValues() {
47+
var q1 = $('input[name=recordValues\\.0\\.noAnimalsVisible]:checked').val();
48+
var q2 = $('input[name=recordValues\\.0\\.problemWithImage]:checked').val();
49+
if (!q1 && !q2) {
50+
$('#btnSave').attr('disabled', 'disabled');
51+
} else {
52+
$('#btnSave').removeAttr('disabled');
53+
}
54+
}
55+
56+
function toggleIndex(index, validationType = "speciesWithCount") {
3557
if (selectedIndicies.hasOwnProperty(index)) {
3658
deselectIndex(index);
3759
} else {
38-
selectIndex(index);
60+
selectIndex(index, validationType);
3961
}
4062
}
4163

42-
function selectIndex(index) {
43-
selectedIndicies[index] = { count: 1, notes: '', editorOpen: false};
64+
function selectIndex(index, validationType = "speciesWithCount") {
65+
var count = 0;
66+
if (validationType === "speciesOnly") count = 1;
67+
selectedIndicies[index] = { count: count, notes: '', editorOpen: false, init: true};
4468
syncSelections();
4569
}
4670

@@ -50,14 +74,17 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
5074
}
5175

5276
function syncSelections() {
53-
var usKeys = _.chain(selectedIndicies).keys().filter(function(idx) { return selectedIndicies[idx].count > 0; });
77+
var usKeys = _.chain(selectedIndicies).keys().filter(function(idx) {
78+
return selectedIndicies[idx].count >= 0;
79+
});
5480
var dataItemIndexes = usKeys.map(function(v,i,l) { return "[data-item-index='"+v+"']"});
5581
var wsSelectionIndicator = dataItemIndexes.map(function(v,i,l) { return v + " .ws-selected"; });
5682
var wsSelectorIndicator = dataItemIndexes.map(function(v,i,l) { return v + " .ws-selector"; });
5783
$(wsSelectionIndicator.value().join(", ")).addClass('selected');
5884
$(wsSelectorIndicator.value().join(", ")).attr('aria-selected', 'true');
5985
$('[data-item-index]:not('+ dataItemIndexes.value().join(',') + ') .ws-selected').removeClass('selected').attr('aria-selected', 'false');
6086
$('[data-item-index]:not('+ dataItemIndexes.value().join(',') + ') .ws-selector').attr('aria-selected', 'false');
87+
6188
var length = usKeys.value().length;
6289
if (length == 0) {
6390
hideSelectionPanel();
@@ -83,17 +110,13 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
83110
return {
84111
index: v,
85112
name: wsParams.animals[v].vernacularName,
86-
options: _([1,2,3,4,5,6,7,8,9,10]).map(function(opt,i) {
87-
return {
88-
val: opt,
89-
selected: selectedIndicies[v].count == opt ? 'selected' : '',
90-
isSelected: selectedIndicies[v].count == opt ? 'true' : 'false'
91-
};
92-
}),
113+
curval: selectedIndicies[v].count,
93114
comment: selectedIndicies[v].comment
94115
};
116+
95117
}).sortBy(function(o) { return o.index; }).value()
96118
};
119+
97120
mu.replaceTemplate(parent, 'status-detail-list-template', templateObj);
98121

99122
parent.show();
@@ -110,14 +133,20 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
110133
generateFormFields();
111134
});
112135

113-
$('#ct-container').on('change', 'select.numAnimals', function() {
136+
//$('#ct-container').on('change', 'select.numAnimals', function() {
137+
$('#ct-container').on('change', 'input.numAnimals', function() {
114138
var $this = $(this);
115139
var idx = $this.closest('[data-item-index]').data('item-index');
116140
var count = $this.val();
141+
// console.log("value change: " + count);
117142
selectedIndicies[idx].count = parseInt(count);
118143
generateFormFields();
119144
});
120145

146+
$('.input-group-btn-vertical').click(function() {
147+
$('#ct-container').trigger("change");
148+
});
149+
121150
$('#ct-container').on('click', '.editCommentButton', function() {
122151
var $this = $(this);
123152
var idx = $this.closest('[data-item-index]').data('item-index');
@@ -128,6 +157,19 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
128157
selectedIndicies[idx].editorOpen = true;
129158
});
130159

160+
$("#ct-container").on('keydown', '.numAnimals', function(e) {
161+
// Allow: backspace, delete, tab, escape, enter and .
162+
if ($.inArray(e.keyCode, [46, 8, 9, 27, 13, 110, 190]) !== -1 || (e.keyCode === 65 && e.ctrlKey === true) || (e.keyCode >= 35 && e.keyCode <= 40)) {
163+
// console.log("key " + e.keyCode + " allowed");
164+
return;
165+
}
166+
167+
if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) {
168+
// console.log("key " + e.keyCode + " not allowed");
169+
e.preventDefault();
170+
}
171+
});
172+
131173
$('#ct-container').on('click', '.saveCommentButton', function() {
132174
var $this = $(this);
133175
var idx = $this.closest('[data-item-index]').data('item-index');
@@ -313,19 +355,27 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
313355
function generateFormFields() {
314356
var $ctFields = $('#ct-fields');
315357
$ctFields.empty();
358+
var enableSubmit = true;
316359
if (_.keys(selectedIndicies).length > 0) {
317360
$('input[name=recordValues\\.0\\.noAnimalsVisible]').removeAttr('checked');
318361
$('input[name=recordValues\\.0\\.problemWithImage]').removeAttr('checked');
319362
if (recordValues && recordValues[0]) {
320363
delete recordValues[0].noAnimalsVisible;
321364
delete recordValues[0].problemWithImage;
322365
}
366+
_.each(selectedIndicies, function(value, key, list) {
367+
if (value.count === 0) enableSubmit = false;
368+
});
323369
} else {
324370
if (recordValues && recordValues[0]) {
325371
delete recordValues[0].vernacularName;
326372
delete recordValues[0].scientificName;
327373
delete recordValues[0].individualCount;
328374
}
375+
376+
var q1 = $('input[name=recordValues\\.0\\.noAnimalsVisible]:checked').val();
377+
var q2 = $('input[name=recordValues\\.0\\.problemWithImage]:checked').val();
378+
if (!q1 && !q2) enableSubmit = false;
329379
}
330380
var i = 0;
331381
_.each(selectedIndicies, function (value, key, list) {
@@ -335,6 +385,10 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
335385
mu.appendTemplate($ctFields, 'input-template', {id: 'recordValues.' + i + '.comment', value: value.comment});
336386
++i;
337387
});
388+
389+
// console.log("Enable submit button? " + enableSubmit);
390+
if (enableSubmit) $('#btnSave').removeAttr('disabled');
391+
else $('#btnSave').attr('disabled', 'disabled');
338392
}
339393

340394
function syncRecordValues() {
@@ -371,12 +425,10 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
371425
errorList.push({element: null, message: "You must either indicate that there are no animals, there's a problem with the image or select at least one animal before you can submit", type: "Error" });
372426
}
373427
});
374-
transcribeValidation.setErrorRenderFunctions(function (errorList) {
375-
},
376-
function() {
377-
});
378428

379-
submitRequiresConfirmation = true;
429+
transcribeValidation.setErrorRenderFunctions(function (errorList) {}, function() {});
430+
var submitRequiresConfirmation = true;
431+
380432
postValidationFunction = function(validationResults) {
381433
if (validationResults.errorList.length > 0) bootbox.alert("<h3>Invalid selection</h3><ul><li>" + _.pluck(validationResults.errorList, 'message').join('</li><li>') + "</li>");
382434
};

grails-app/controllers/au/org/ala/volunteer/LeaderBoardController.groovy

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class LeaderBoardController {
3636
def category = params.category as LeaderBoardCategory
3737

3838
def institution = Institution.get(params.int("institutionId"))
39+
log.debug("Leaderboard top list")
3940

4041
leaderBoardService.topList(category, institution)
4142
}

grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ProjectController {
3737
def authService
3838
def groovyPageRenderer
3939
def templateService
40+
def settingsService
4041
Closure<DSLContext> jooqContext
4142

4243
/**
@@ -63,6 +64,8 @@ class ProjectController {
6364
} else {
6465
// project info
6566
List userIds = taskService.getUserIdsAndCountsForProject(projectInstance, new HashMap<String, Object>())
67+
def ineligible = settingsService.getSetting(SettingDefinition.IneligibleLeaderBoardUsers) ?: []
68+
log.debug("Ineligible users: ${ineligible}")
6669
def expedition = grailsApplication.config.getProperty("expedition", List.class)
6770
def roles = [] // List of Map
6871
// copy expedition data structure to "roles" & add "members"
@@ -78,7 +81,7 @@ class ProjectController {
7881
def count = it[1]
7982
def assigned = false
8083
def user = User.findByUserId(userId)
81-
if (user) {
84+
if (user && !ineligible.contains(userId)) {
8285
roles.eachWithIndex { role, i ->
8386
if (count >= role.threshold && role.members.size() < role.max && !assigned) {
8487
// assign role

grails-app/controllers/au/org/ala/volunteer/TaskController.groovy

+2
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ class TaskController {
174174
def jsonObj = [:]
175175
jsonObj.put("cat", recordValues?.get(0)?.catalogNumber)
176176
jsonObj.put("name", recordValues?.get(0)?.scientificName)
177+
jsonObj.put("id", taskInstance.id)
178+
jsonObj.put("filename", taskInstance.externalIdentifier)
177179

178180
List transcribers = []
179181
taskInstance.transcriptions.each {

grails-app/services/au/org/ala/volunteer/ExportService.groovy

+10-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import grails.gorm.transactions.Transactional
77
import org.apache.commons.lang.SerializationUtils
88
import org.jooq.tools.StringUtils
99

10+
import javax.servlet.http.HttpServletResponse
1011
import java.util.concurrent.atomic.AtomicInteger
1112
import java.util.regex.Pattern
1213
import java.util.zip.ZipOutputStream
@@ -83,7 +84,7 @@ class ExportService {
8384
return result
8485
}
8586

86-
def export_zipFile = { Project project, taskList, fieldNames, fieldList, response ->
87+
def export_zipFile = { Project project, List<Task> taskList, ArrayList<String> fieldNames, List fieldList, HttpServletResponse response ->
8788
def sw = Stopwatch.createStarted()
8889
def databaseFieldNames = fieldService.getMaxRecordIndexByFieldForProject(project)
8990
log.debug("Got databaseFieldNames in {}ms", sw.elapsed(MILLISECONDS))
@@ -102,7 +103,7 @@ class ExportService {
102103
log.debug("Generated repeating fields in {}ms", sw.elapsed(MILLISECONDS))
103104
sw.reset().start()
104105

105-
zipExport(project, taskList, fieldNames, fieldList, response, ["dataset"], repeatingFields)
106+
zipExport(project, taskList, fieldNames, fieldList, response, ["dataset"] as List<FieldCategory>, repeatingFields)
106107
}
107108

108109
private Map<Transcription, Map> getTranscriptionsToExport(Project project, Task task, Map valuesMap) {
@@ -253,10 +254,11 @@ class ExportService {
253254
writer.close()
254255
}
255256

256-
private void zipExport(Project project, taskList, List fieldNames, fieldList, response, List<FieldCategory> datasetCategories, List<String> otherRepeatingFields) {
257+
private void zipExport(Project project, List<Task> taskList, ArrayList<String> fieldNames, List fieldList, HttpServletResponse response, List<FieldCategory> datasetCategories, List<String> otherRepeatingFields) {
257258
def valueMap = fieldListToMultiMap(fieldList)
258259
def sw = Stopwatch.createStarted()
259260
def datasetCategoryFields = [:]
261+
260262
if (datasetCategories) {
261263
datasetCategories.each { category ->
262264
// Work out which fields are a repeating group...
@@ -291,7 +293,7 @@ class ExportService {
291293
// Prepare the response for a zip file - use the project name as a basis of the filename
292294
def filename = "Project-" + (cleanFilename(project.featuredLabel) ?: project.id) + "-DwC"
293295

294-
response.setHeader("Content-Disposition", "attachment;filename=" + filename +".zip");
296+
response.setHeader("Content-Disposition", "attachment;filename=" + filename + "-" + new Date().getTime() + ".zip");
295297
response.setContentType("application/zip");
296298

297299
// First up write out the main tasks file -all the remaining fields are single value only
@@ -301,7 +303,7 @@ class ExportService {
301303

302304
CSVWriter writer = new CSVWriter(outputwriter);
303305

304-
zipStream.putNextEntry(new ZipEntry("tasks.csv"));
306+
zipStream.putNextEntry(new ZipEntry("tasks-" + new Date().getTime() + ".csv"));
305307
// write header line (field names)
306308
writer.writeNext((String[]) fieldNames.toArray(new String[0]))
307309

@@ -336,6 +338,7 @@ class ExportService {
336338

337339
// now for each repeating field category...
338340
if (datasetCategoryFields) {
341+
log.debug("DatasetCategory Fields: ${datasetCategoryFields}")
339342
datasetCategoryFields.keySet().each { category ->
340343
// Dataset files...
341344
def dataSetFieldNames = datasetCategoryFields[category]
@@ -349,6 +352,7 @@ class ExportService {
349352
}
350353
// Now for the other repeating fields...
351354
if (otherRepeatingFields) {
355+
log.debug("Other Repeating Fields: ${otherRepeatingFields}")
352356
otherRepeatingFields.each {
353357
zipStream.putNextEntry(new ZipEntry("${it}.csv"))
354358
exportDataSet(project, taskList, valueMap, writer, [it])
@@ -362,7 +366,7 @@ class ExportService {
362366
// Export multimedia as 'associatedMedia'. There may be more than one piece of multimedia per task
363367
// so we do it in a separate file...
364368

365-
zipStream.putNextEntry(new ZipEntry("associatedMedia.csv"))
369+
zipStream.putNextEntry(new ZipEntry("associatedMedia-" + new Date().getTime() + ".csv"))
366370
exportMultimedia(taskList, writer);
367371
writer.flush();
368372
zipStream.closeEntry()

0 commit comments

Comments
 (0)