diff --git a/README.md b/README.md
index da4009f2..56cc1aa0 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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]
diff --git a/build.gradle b/build.gradle
index 29ecddb8..505ea347 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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"
diff --git a/gradle.properties b/gradle.properties
index eee60b5a..9b8dc973 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -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
diff --git a/grails-app/assets/javascripts/transcribe/wildlifespotter.js b/grails-app/assets/javascripts/transcribe/wildlifespotter.js
index f2d4ae08..dcb64066 100644
--- a/grails-app/assets/javascripts/transcribe/wildlifespotter.js
+++ b/grails-app/assets/javascripts/transcribe/wildlifespotter.js
@@ -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() {
@@ -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();
}
@@ -50,7 +74,9 @@ 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"; });
@@ -58,6 +84,7 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
$(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();
@@ -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();
@@ -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');
@@ -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');
@@ -313,6 +355,7 @@ 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');
@@ -320,12 +363,19 @@ function wildlifespotter(wsParams, imagePrefix, recordValues, placeholders) {
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) {
@@ -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() {
@@ -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("
Invalid selection " + _.pluck(validationResults.errorList, 'message').join(' ') + " ");
};
diff --git a/grails-app/controllers/au/org/ala/volunteer/LeaderBoardController.groovy b/grails-app/controllers/au/org/ala/volunteer/LeaderBoardController.groovy
index 192b1d8a..a6d9a3b0 100644
--- a/grails-app/controllers/au/org/ala/volunteer/LeaderBoardController.groovy
+++ b/grails-app/controllers/au/org/ala/volunteer/LeaderBoardController.groovy
@@ -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)
}
diff --git a/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy b/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy
index a65de8c1..0d63e346 100644
--- a/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy
+++ b/grails-app/controllers/au/org/ala/volunteer/ProjectController.groovy
@@ -37,6 +37,7 @@ class ProjectController {
def authService
def groovyPageRenderer
def templateService
+ def settingsService
Closure jooqContext
/**
@@ -63,6 +64,8 @@ class ProjectController {
} else {
// project info
List userIds = taskService.getUserIdsAndCountsForProject(projectInstance, new HashMap())
+ 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"
@@ -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
diff --git a/grails-app/controllers/au/org/ala/volunteer/TaskController.groovy b/grails-app/controllers/au/org/ala/volunteer/TaskController.groovy
index 3630456d..6099c3c8 100644
--- a/grails-app/controllers/au/org/ala/volunteer/TaskController.groovy
+++ b/grails-app/controllers/au/org/ala/volunteer/TaskController.groovy
@@ -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 {
diff --git a/grails-app/services/au/org/ala/volunteer/ExportService.groovy b/grails-app/services/au/org/ala/volunteer/ExportService.groovy
index 104ad132..713213f0 100644
--- a/grails-app/services/au/org/ala/volunteer/ExportService.groovy
+++ b/grails-app/services/au/org/ala/volunteer/ExportService.groovy
@@ -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
@@ -83,7 +84,7 @@ class ExportService {
return result
}
- def export_zipFile = { Project project, taskList, fieldNames, fieldList, response ->
+ def export_zipFile = { Project project, List taskList, ArrayList fieldNames, List fieldList, HttpServletResponse response ->
def sw = Stopwatch.createStarted()
def databaseFieldNames = fieldService.getMaxRecordIndexByFieldForProject(project)
log.debug("Got databaseFieldNames in {}ms", sw.elapsed(MILLISECONDS))
@@ -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, repeatingFields)
}
private Map getTranscriptionsToExport(Project project, Task task, Map valuesMap) {
@@ -253,10 +254,11 @@ class ExportService {
writer.close()
}
- private void zipExport(Project project, taskList, List fieldNames, fieldList, response, List datasetCategories, List otherRepeatingFields) {
+ private void zipExport(Project project, List taskList, ArrayList fieldNames, List fieldList, HttpServletResponse response, List datasetCategories, List otherRepeatingFields) {
def valueMap = fieldListToMultiMap(fieldList)
def sw = Stopwatch.createStarted()
def datasetCategoryFields = [:]
+
if (datasetCategories) {
datasetCategories.each { category ->
// Work out which fields are a repeating group...
@@ -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
@@ -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]))
@@ -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]
@@ -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])
@@ -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()
diff --git a/grails-app/services/au/org/ala/volunteer/LeaderBoardService.groovy b/grails-app/services/au/org/ala/volunteer/LeaderBoardService.groovy
index 4ebf2ff4..5bc90c38 100644
--- a/grails-app/services/au/org/ala/volunteer/LeaderBoardService.groovy
+++ b/grails-app/services/au/org/ala/volunteer/LeaderBoardService.groovy
@@ -79,6 +79,7 @@ class LeaderBoardService {
def today = todaysDate
def ineligibleUsers = settingsService.getSetting(SettingDefinition.IneligibleLeaderBoardUsers)
+ log.debug("LeaderBoardService ineligible users: ${ineligibleUsers}")
def headingPrefix = "Top 20 volunteers for "
def heading = headingPrefix + category?.toString()?.toTitleCase()
@@ -155,46 +156,69 @@ class LeaderBoardService {
}
List getTopNForPeriod(Date startDate, Date endDate, int count, List institutionList, List ineligibleUsers = [], def pt = null) {
+ log.debug("getTopNForPeriod: ${[startDate: startDate, endDate: endDate, count: count, institutionList: institutionList, ineligibleUsers: ineligibleUsers, pt: pt]}")
// Get a map of users who transcribed tasks during this period, along with the count
def scoreMap = getUserMapForPeriod(startDate, endDate, ActivityType.Transcribed, institutionList, ineligibleUsers, pt)
+ log.debug("Score Map: ${scoreMap}")
// Get a map of user who validated tasks during this periodn, along with the count
def validatedMap = getUserMapForPeriod(startDate, endDate, ActivityType.Validated, institutionList, ineligibleUsers, pt)
+ log.debug("Validate Map: ${validatedMap}")
return mergeScores(validatedMap, scoreMap, count, ineligibleUsers)
}
+ /**
+ * Merge the validated score map into the transcribed score map, forming a total activity score for the superset of users.
+ * Will omit ineligible users (saved in Admin/Inelgible Users).
+ * @param validatedMap Map of users validation scores
+ * @param scoreMap Map of users transcribe scores
+ * @param count the total number of combined users to list
+ * @param ineligibleUsers List of users to be omitted from the leaderboard
+ * @return the complete combined list with user details.
+ */
private List mergeScores(LinkedHashMap validatedMap, LinkedHashMap scoreMap, int count, def ineligibleUsers) {
- // merge the validated map into the transcribed map, forming a total activity score for the superset of users
+ log.debug("Ineligible users: ${ineligibleUsers}")
+
+ // combine the transcribed count with the validated count for that user.
validatedMap.each { kvp ->
+ if (!scoreMap[kvp.key]) {
+ scoreMap[kvp.key] = 0
+ }
+ scoreMap[kvp.key] += kvp.value
+ }
+
+ // Parse the combined scoremap for ineligible users
+ scoreMap.each {kvp ->
// If the user is excluded, set their score to -1.
if (ineligibleUsers?.size() > 0 && ineligibleUsers?.contains(kvp.key)) {
scoreMap[kvp.key] = -1
- } else {
- // if there exists a validator who is not a transcriber, set the transcription count to 0
- if (!scoreMap[kvp.key]) {
- scoreMap[kvp.key] = 0
- }
-
- // combine the transcribed count with the validated count for that user.
- scoreMap[kvp.key] += kvp.value
+ log.debug("Ineligbile user [${kvp.key}] score: ${scoreMap[kvp.key]}")
}
}
+ // Sort and clip the top n volunteers
scoreMap = scoreMap.sort { a, b -> b.value <=> a.value }
if (scoreMap.size() > count) {
scoreMap = scoreMap.take(count)
}
+ log.debug("Final Score Map: ${scoreMap}")
- // Flatten the map into a list for easy sorting, so we can slice off the top N
+ // Flatten the map into a list with user details
def list = []
def userDetails = userService.detailsForUserIds(scoreMap.keySet() as List).collectEntries { [(it.userId): it] }
scoreMap.each { kvp ->
- def user = User.findByUserId(kvp.key)
- def details = userDetails[kvp.key]
- if (user) {
- list << [name: details?.displayName, email: details?.email, score: kvp?.value ?: 0, userId: user?.id]
+ // Only include if they have a positive score (ineligible users will have -1)
+ if (kvp.value > 0) {
+ def user = User.findByUserId(kvp.key)
+ def details = userDetails[kvp.key]
+ if (user) {
+ list << [name: details?.displayName, email: details?.email, score: kvp?.value ?: 0, userId: user?.id]
+ log.debug("Adding user details for ${[name: details?.displayName, email: details?.email, score: kvp?.value ?: 0, userId: user?.id]}")
+ } else {
+ log.warn("Failed to find user with key: ${kvp.key}")
+ }
} else {
- log.warn("Failed to find user with key: ${kvp.key}")
+ log.debug("Omitting ${kvp.key} from leaderboard list")
}
}
diff --git a/grails-app/services/au/org/ala/volunteer/TaskService.groovy b/grails-app/services/au/org/ala/volunteer/TaskService.groovy
index 0c3c8baf..032c064c 100644
--- a/grails-app/services/au/org/ala/volunteer/TaskService.groovy
+++ b/grails-app/services/au/org/ala/volunteer/TaskService.groovy
@@ -73,13 +73,14 @@ class TaskService {
* @return Map of project id -> count
*/
Map getProjectTaskTranscribedCounts(boolean activeOnly = false) {
- def projectTaskCounts = Task.executeQuery(
- """select t.project.id as projectId, count(t) as taskCount
- from Task t
- where
- exists (from Field as f where f.task = t)
- ${activeOnly ? 'and t.project.inactive != true' : ''}
- group by t.project.id""")
+ def querySelect = """
+ select t.project.id as projectId, count(t) as taskCount
+ from Task t
+ where exists (from Field as f where f.task = t) """.stripIndent()
+ def activeClause = "and t.project.inactive != true"
+ def query = querySelect + (activeOnly ? activeClause : "") + " group by t.project.id"
+
+ def projectTaskCounts = Task.executeQuery(query)
projectTaskCounts.toMap()
}
@@ -87,15 +88,15 @@ class TaskService {
* @return Map of project id -> count
*/
Map getProjectTaskFullyTranscribedCounts(boolean activeOnly = false) {
- def projectTaskCounts = Task.executeQuery(
- """select t.project.id as projectId, count(t) as taskCount
- from Transcription t
- ${activeOnly ? 'where t.project.inactive != true' : ''}
- group by t.project.id""")
+ def querySelect = """select t.project.id as projectId, count(t) as taskCount
+ from Transcription t """.stripIndent()
+ def activeClause = "where t.project.inactive != true"
+ def query = querySelect + (activeOnly ? activeClause : "") + " group by t.project.id"
+
+ def projectTaskCounts = Task.executeQuery(query)
projectTaskCounts.toMap()
}
-
Map getProjectDates() {
def dates = Task.executeQuery(
"""select t.project.id as projectId, min(trans.dateFullyTranscribed), max(trans.dateFullyTranscribed), min(t.dateFullyValidated), max(t.dateFullyValidated)
@@ -110,7 +111,6 @@ class TaskService {
map
}
-
/**
*
* @param project
diff --git a/grails-app/services/au/org/ala/volunteer/ValidationService.groovy b/grails-app/services/au/org/ala/volunteer/ValidationService.groovy
index 39db53de..b9be8d55 100644
--- a/grails-app/services/au/org/ala/volunteer/ValidationService.groovy
+++ b/grails-app/services/au/org/ala/volunteer/ValidationService.groovy
@@ -128,7 +128,13 @@ class ValidationService {
*/
private boolean fieldsMatch(Transcription t1, Transcription t2) {
if (t1?.project?.projectType?.name == ProjectType.PROJECT_TYPE_CAMERATRAP) {
- return fieldsMatch(t1, t2, CAMERATRAP_EXCLUDED_FIELDS)
+ // If validation type is set to species only, add individual count to the list of excluded fields
+ def ctExcludedFields = CAMERATRAP_EXCLUDED_FIELDS
+ def viewParams = t1?.project?.template?.viewParams
+ if (viewParams && AutoValidationType.fromString(viewParams?.autoValidationType as String) == AutoValidationType.speciesOnly) {
+ ctExcludedFields.add(DarwinCoreField.individualCount.name())
+ }
+ return fieldsMatch(t1, t2, ctExcludedFields)
} else {
return fieldsMatch(t1, t2, EXCLUDED_FIELDS)
}
diff --git a/grails-app/views/admin/index.gsp b/grails-app/views/admin/index.gsp
index d1a167c0..a07eb938 100644
--- a/grails-app/views/admin/index.gsp
+++ b/grails-app/views/admin/index.gsp
@@ -4,6 +4,19 @@
+
+
+
@@ -31,123 +44,123 @@
- Manage Institutions
Manage Institutions
- Manage Expeditions
Download expedition images and remove from server, clone and edit expedition.
- Create new Expedition
Create a new ${message(code: 'default.application.name')} Expedition
- Templates
Manage expedition templates and their fields
- Bulk manage Picklists
Allows modification to the values held in various picklists
- Validation Rules
Manage transcription validation rules
- Configure Front Page
Configure the appearance of the front page
- Configure Landing Page
Configure the appearance of a landing page
- Configure Honour Board
+ Manage Leaderboard
Configure the appearance of the Honour Board
- Stats
Various Statistics (Experimental!)
- Tutorial Files
Manage tutorial files
- Tools
Tools
- User List
List of all User accounts on DigiVol
- Users Opted-Out
List of all Users who have opted-out of receiving Institution Messages
- Manage Institution Admins
Manage Institution Admins
- Manage User Roles
Manage User Roles, such as validators and forum moderators.
- Institution Messages
Create and send messages to volunteers of your institutions and expeditions.
- Manage Badges
Manage Achievements
- Manage Tags
Manage Expedition Tags
- Advanced Settings
Advanced Settings
@@ -157,13 +170,13 @@
Admin reports
- User report
- Current users
- Expedition Summary Report
diff --git a/grails-app/views/layouts/_profileDropDown.gsp b/grails-app/views/layouts/_profileDropDown.gsp
index 306b01ee..e53d280b 100644
--- a/grails-app/views/layouts/_profileDropDown.gsp
+++ b/grails-app/views/layouts/_profileDropDown.gsp
@@ -26,7 +26,7 @@
diff --git a/grails-app/views/leaderBoardAdmin/index.gsp b/grails-app/views/leaderBoardAdmin/index.gsp
index 04e33ff8..2da2edda 100644
--- a/grails-app/views/leaderBoardAdmin/index.gsp
+++ b/grails-app/views/leaderBoardAdmin/index.gsp
@@ -89,7 +89,7 @@
$("#add-user").val('');
})
- .fail(function() { alert("Couldn't add user")});
+ .fail(function() { alert("Couldn't add user"); });
return null;
}, 'displayName');
@@ -111,7 +111,7 @@
p.remove();
$("#add-user").val('');
})
- .fail(alert("Couldn't remove user"))
+ .fail(function() { alert("Couldn't remove user"); })
.always(hideSpinner);
}
diff --git a/grails-app/views/project/index.gsp b/grails-app/views/project/index.gsp
index b080022f..33b2b502 100644
--- a/grails-app/views/project/index.gsp
+++ b/grails-app/views/project/index.gsp
@@ -277,7 +277,14 @@
url: "${createLink(controller: 'task', action: 'details')}/" + id,
success: function (data) {
- var content = "Catalogue No.: " + data.cat + " Taxon: " + data.name + " Transcribed by: " + data.transcriber + "
";
+ var content = "Task: " + data.id + "
";
+
+ content += "File: " + data.filename + " ";
+
+
+ content += "File: " + data.filename + " ";
+
+ content += "Transcribed by: " + data.transcriber + "
";
infowindow.close();
infowindow.setContent(content);
infowindow.open(map, marker);
diff --git a/grails-app/views/project/manage.gsp b/grails-app/views/project/manage.gsp
index 8b08eddd..4e2ebc06 100644
--- a/grails-app/views/project/manage.gsp
+++ b/grails-app/views/project/manage.gsp
@@ -185,9 +185,7 @@
-%{-- --}%
-%{-- --}%
@@ -197,10 +195,11 @@
-%{-- --}%
+
+
@@ -263,6 +262,16 @@ jQuery(function($) {
}
});
+ $(".export-project").click(function(e) {
+ e.preventDefault();
+ var projectId = $(this).parents("[projectId]").attr("projectId");
+ var options = {
+ title:'Export all tasks',
+ url:"${createLink(controller: "task", action: "exportOptionsFragment", params: [exportCriteria: 'all']).encodeAsJavaScript()}&projectId=" + projectId
+ };
+ bvp.showModal(options);
+ });
+
$(".clone-project").click(function(e) {
e.preventDefault();
var oldProjectId = $(this).parents("[projectId]").attr("projectId");
diff --git a/grails-app/views/task/exportOptionsFragment.gsp b/grails-app/views/task/exportOptionsFragment.gsp
index ef5a34d3..6032d0f1 100644
--- a/grails-app/views/task/exportOptionsFragment.gsp
+++ b/grails-app/views/task/exportOptionsFragment.gsp
@@ -1,20 +1,27 @@
-
Select an export format
+
Select an export format