Skip to content

Commit

Permalink
Merge branch 'release/6.1.8'
Browse files Browse the repository at this point in the history
  • Loading branch information
cdausmus committed Mar 1, 2024
2 parents 7237d05 + e6e60d1 commit ab39ddb
Show file tree
Hide file tree
Showing 27 changed files with 469 additions and 224 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# DigiVol [![Build Status](https://travis-ci.org/AtlasOfLivingAustralia/volunteer-portal.svg?branch=develop)](https://travis-ci.org/AtlasOfLivingAustralia/volunteer-portal)

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

## Running

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

## Contributing

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

For git flow operations you may like to use the `git-flow` command line tools. Either install [Atlassian SourceTree]
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ plugins {
id "com.dorongold.task-tree" version "2.1.1"
}

version "6.1.7"
version "6.1.8"
group "au.org.ala"
description "Digivol application"

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ springBootVersion=2.7.9
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M
alaAuthVersion=6.0.3
alaAuthVersion=6.2.0

#grailsWrapperVersion=1.0.0
gradleWrapperVersion=5.0
Expand Down
90 changes: 71 additions & 19 deletions grails-app/assets/javascripts/transcribe/wildlifespotter.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {

var selectedIndicies = {};

// Default save button to disabled until a selection has been made.
$('#btnSave').attr('disabled', 'disabled');

// selection
$('#ct-container').on('click', '.ws-selector', function() {
var $this = $(this);
var index = $this.closest('[data-item-index]').data('item-index');
toggleIndex(index);
var validationtype = $this.data('validationType');
toggleIndex(index, validationtype);
});

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

function toggleIndex(index) {
$('input[name=recordValues\\.0\\.noAnimalsVisible]').change(function() {
checkCheckboxValues();
});

$('input[name=recordValues\\.0\\.problemWithImage]').change(function() {
checkCheckboxValues();
});

function checkCheckboxValues() {
var q1 = $('input[name=recordValues\\.0\\.noAnimalsVisible]:checked').val();
var q2 = $('input[name=recordValues\\.0\\.problemWithImage]:checked').val();
if (!q1 && !q2) {
$('#btnSave').attr('disabled', 'disabled');
} else {
$('#btnSave').removeAttr('disabled');
}
}

function toggleIndex(index, validationType = "speciesWithCount") {
if (selectedIndicies.hasOwnProperty(index)) {
deselectIndex(index);
} else {
selectIndex(index);
selectIndex(index, validationType);
}
}

function selectIndex(index) {
selectedIndicies[index] = { count: 1, notes: '', editorOpen: false};
function selectIndex(index, validationType = "speciesWithCount") {
var count = 0;
if (validationType === "speciesOnly") count = 1;
selectedIndicies[index] = { count: count, notes: '', editorOpen: false, init: true};
syncSelections();
}

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

function syncSelections() {
var usKeys = _.chain(selectedIndicies).keys().filter(function(idx) { return selectedIndicies[idx].count > 0; });
var usKeys = _.chain(selectedIndicies).keys().filter(function(idx) {
return selectedIndicies[idx].count >= 0;
});
var dataItemIndexes = usKeys.map(function(v,i,l) { return "[data-item-index='"+v+"']"});
var wsSelectionIndicator = dataItemIndexes.map(function(v,i,l) { return v + " .ws-selected"; });
var wsSelectorIndicator = dataItemIndexes.map(function(v,i,l) { return v + " .ws-selector"; });
$(wsSelectionIndicator.value().join(", ")).addClass('selected');
$(wsSelectorIndicator.value().join(", ")).attr('aria-selected', 'true');
$('[data-item-index]:not('+ dataItemIndexes.value().join(',') + ') .ws-selected').removeClass('selected').attr('aria-selected', 'false');
$('[data-item-index]:not('+ dataItemIndexes.value().join(',') + ') .ws-selector').attr('aria-selected', 'false');

var length = usKeys.value().length;
if (length == 0) {
hideSelectionPanel();
Expand All @@ -83,17 +110,13 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
return {
index: v,
name: wsParams.animals[v].vernacularName,
options: _([1,2,3,4,5,6,7,8,9,10]).map(function(opt,i) {
return {
val: opt,
selected: selectedIndicies[v].count == opt ? 'selected' : '',
isSelected: selectedIndicies[v].count == opt ? 'true' : 'false'
};
}),
curval: selectedIndicies[v].count,
comment: selectedIndicies[v].comment
};

}).sortBy(function(o) { return o.index; }).value()
};

mu.replaceTemplate(parent, 'status-detail-list-template', templateObj);

parent.show();
Expand All @@ -110,14 +133,20 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
generateFormFields();
});

$('#ct-container').on('change', 'select.numAnimals', function() {
//$('#ct-container').on('change', 'select.numAnimals', function() {
$('#ct-container').on('change', 'input.numAnimals', function() {
var $this = $(this);
var idx = $this.closest('[data-item-index]').data('item-index');
var count = $this.val();
// console.log("value change: " + count);
selectedIndicies[idx].count = parseInt(count);
generateFormFields();
});

$('.input-group-btn-vertical').click(function() {
$('#ct-container').trigger("change");
});

$('#ct-container').on('click', '.editCommentButton', function() {
var $this = $(this);
var idx = $this.closest('[data-item-index]').data('item-index');
Expand All @@ -128,6 +157,19 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
selectedIndicies[idx].editorOpen = true;
});

$("#ct-container").on('keydown', '.numAnimals', function(e) {
// Allow: backspace, delete, tab, escape, enter and .
if ($.inArray(e.keyCode, [46, 8, 9, 27, 13, 110, 190]) !== -1 || (e.keyCode === 65 && e.ctrlKey === true) || (e.keyCode >= 35 && e.keyCode <= 40)) {
// console.log("key " + e.keyCode + " allowed");
return;
}

if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) {
// console.log("key " + e.keyCode + " not allowed");
e.preventDefault();
}
});

$('#ct-container').on('click', '.saveCommentButton', function() {
var $this = $(this);
var idx = $this.closest('[data-item-index]').data('item-index');
Expand Down Expand Up @@ -313,19 +355,27 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
function generateFormFields() {
var $ctFields = $('#ct-fields');
$ctFields.empty();
var enableSubmit = true;
if (_.keys(selectedIndicies).length > 0) {
$('input[name=recordValues\\.0\\.noAnimalsVisible]').removeAttr('checked');
$('input[name=recordValues\\.0\\.problemWithImage]').removeAttr('checked');
if (recordValues && recordValues[0]) {
delete recordValues[0].noAnimalsVisible;
delete recordValues[0].problemWithImage;
}
_.each(selectedIndicies, function(value, key, list) {
if (value.count === 0) enableSubmit = false;
});
} else {
if (recordValues && recordValues[0]) {
delete recordValues[0].vernacularName;
delete recordValues[0].scientificName;
delete recordValues[0].individualCount;
}

var q1 = $('input[name=recordValues\\.0\\.noAnimalsVisible]:checked').val();
var q2 = $('input[name=recordValues\\.0\\.problemWithImage]:checked').val();
if (!q1 && !q2) enableSubmit = false;
}
var i = 0;
_.each(selectedIndicies, function (value, key, list) {
Expand All @@ -335,6 +385,10 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
mu.appendTemplate($ctFields, 'input-template', {id: 'recordValues.' + i + '.comment', value: value.comment});
++i;
});

// console.log("Enable submit button? " + enableSubmit);
if (enableSubmit) $('#btnSave').removeAttr('disabled');
else $('#btnSave').attr('disabled', 'disabled');
}

function syncRecordValues() {
Expand Down Expand Up @@ -371,12 +425,10 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
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" });
}
});
transcribeValidation.setErrorRenderFunctions(function (errorList) {
},
function() {
});

submitRequiresConfirmation = true;
transcribeValidation.setErrorRenderFunctions(function (errorList) {}, function() {});
var submitRequiresConfirmation = true;

postValidationFunction = function(validationResults) {
if (validationResults.errorList.length > 0) bootbox.alert("<h3>Invalid selection</h3><ul><li>" + _.pluck(validationResults.errorList, 'message').join('</li><li>') + "</li>");
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class LeaderBoardController {
def category = params.category as LeaderBoardCategory

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

leaderBoardService.topList(category, institution)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ProjectController {
def authService
def groovyPageRenderer
def templateService
def settingsService
Closure<DSLContext> jooqContext

/**
Expand All @@ -63,6 +64,8 @@ class ProjectController {
} else {
// project info
List userIds = taskService.getUserIdsAndCountsForProject(projectInstance, new HashMap<String, Object>())
def ineligible = settingsService.getSetting(SettingDefinition.IneligibleLeaderBoardUsers) ?: []
log.debug("Ineligible users: ${ineligible}")
def expedition = grailsApplication.config.getProperty("expedition", List.class)
def roles = [] // List of Map
// copy expedition data structure to "roles" & add "members"
Expand All @@ -78,7 +81,7 @@ class ProjectController {
def count = it[1]
def assigned = false
def user = User.findByUserId(userId)
if (user) {
if (user && !ineligible.contains(userId)) {
roles.eachWithIndex { role, i ->
if (count >= role.threshold && role.members.size() < role.max && !assigned) {
// assign role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ class TaskController {
def jsonObj = [:]
jsonObj.put("cat", recordValues?.get(0)?.catalogNumber)
jsonObj.put("name", recordValues?.get(0)?.scientificName)
jsonObj.put("id", taskInstance.id)
jsonObj.put("filename", taskInstance.externalIdentifier)

List transcribers = []
taskInstance.transcriptions.each {
Expand Down
16 changes: 10 additions & 6 deletions grails-app/services/au/org/ala/volunteer/ExportService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import grails.gorm.transactions.Transactional
import org.apache.commons.lang.SerializationUtils
import org.jooq.tools.StringUtils

import javax.servlet.http.HttpServletResponse
import java.util.concurrent.atomic.AtomicInteger
import java.util.regex.Pattern
import java.util.zip.ZipOutputStream
Expand Down Expand Up @@ -83,7 +84,7 @@ class ExportService {
return result
}

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

zipExport(project, taskList, fieldNames, fieldList, response, ["dataset"], repeatingFields)
zipExport(project, taskList, fieldNames, fieldList, response, ["dataset"] as List<FieldCategory>, repeatingFields)
}

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

private void zipExport(Project project, taskList, List fieldNames, fieldList, response, List<FieldCategory> datasetCategories, List<String> otherRepeatingFields) {
private void zipExport(Project project, List<Task> taskList, ArrayList<String> fieldNames, List fieldList, HttpServletResponse response, List<FieldCategory> datasetCategories, List<String> otherRepeatingFields) {
def valueMap = fieldListToMultiMap(fieldList)
def sw = Stopwatch.createStarted()
def datasetCategoryFields = [:]

if (datasetCategories) {
datasetCategories.each { category ->
// Work out which fields are a repeating group...
Expand Down Expand Up @@ -291,7 +293,7 @@ class ExportService {
// Prepare the response for a zip file - use the project name as a basis of the filename
def filename = "Project-" + (cleanFilename(project.featuredLabel) ?: project.id) + "-DwC"

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

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

CSVWriter writer = new CSVWriter(outputwriter);

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

Expand Down Expand Up @@ -336,6 +338,7 @@ class ExportService {

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

zipStream.putNextEntry(new ZipEntry("associatedMedia.csv"))
zipStream.putNextEntry(new ZipEntry("associatedMedia-" + new Date().getTime() + ".csv"))
exportMultimedia(taskList, writer);
writer.flush();
zipStream.closeEntry()
Expand Down
Loading

0 comments on commit ab39ddb

Please sign in to comment.