From 09ffd10ec2be5910c02598b58e987e2940e6e7e1 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 1 Apr 2015 17:59:40 +1100 Subject: [PATCH 01/12] Prepare for next development iteration --- application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application.properties b/application.properties index 6fe3b3539..ef434ecbe 100644 --- a/application.properties +++ b/application.properties @@ -6,4 +6,4 @@ app.buildProfile=development app.grails.version=2.3.11 app.name=volunteer-portal app.servlet.version=2.5 -app.version=2.1.0 +app.version=2.1.1-SNAPSHOT From 867d21370993f0d306889b1c5554c9d709095ba6 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Tue, 7 Apr 2015 17:04:56 +1000 Subject: [PATCH 02/12] #91 Fix user lookup. Update to latest version of ala-auth and ala-bootstrap2 plugins (nee ala-web-theme). Add bulk lookup path to config to workaround issue in ala-auth plugin --- grails-app/conf/BuildConfig.groovy | 4 +++- grails-app/conf/Config.groovy | 11 ++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 257f3402e..20c0c9c3b 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -47,7 +47,9 @@ grails.project.dependency.resolution = { runtime ":quartz:1.0.1" runtime ":tiny-mce:3.4.9" runtime ":webxml:1.4.1" - runtime ":ala-web-theme:0.8.5" + //runtime ":ala-web-theme:0.8.5" + runtime ":ala-auth:1.2" + runtime ":ala-bootstrap2:1.4" runtime ":lesscss-resources:1.3.3" compile ':scaffolding:2.0.3' runtime ':database-migration:1.4.0' diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy index 531a7e6d6..b8badfae9 100644 --- a/grails-app/conf/Config.groovy +++ b/grails-app/conf/Config.groovy @@ -129,15 +129,8 @@ expedition = [ ] -achievements = [ - [ name: 'tenth_transcription', label:"10th transcription", description:'Submit ten transcription tasks for validation', icon: 'images/achievements/bronze_lens.png' ], - [ name: 'hundredth_transcription', label:"100th transcription", description:'Submit one hundred transcription tasks for validation', icon: 'images/achievements/silver_telescope.png' ], - [ name: 'fivehundredth_transcription', label:"500th transcription", description:'Submit five hundred transcription tasks for validation', icon: 'images/achievements/gold_microscope.png' ], - [ name: 'three_projects', label:"Three expeditions", description:'Transcribe tasks across three different expeditions', icon: 'images/achievements/bronze_net.png' ], - [ name: 'five_projects', label:"Five expeditions", description:'Transcribe tasks across five different expeditions', icon: 'images/achievements/silver_binoculars.png' ], - [ name: 'seven_projects', label:"Seven expeditions", description:'Transcribe tasks across seven different expeditions', icon: 'images/achievements/gold_telescope.png' ], - -] +// TODO Remove this after ala-auth plugin is updated +userDetailsById.bulkPath = 'getUserDetailsFromIdList' volunteer.defaultProjectId = 6306 viewedTask.timeout = 2 * 60 * 60 * 1000 From 47d7876c87758f58671d5c5b97bbcb1c61280017 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 8 Apr 2015 11:18:04 +1000 Subject: [PATCH 03/12] #87 Wrong forum names when posting a reply --- grails-app/views/forum/postMessage.gsp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grails-app/views/forum/postMessage.gsp b/grails-app/views/forum/postMessage.gsp index 198d3cbc1..0b2adb7b3 100644 --- a/grails-app/views/forum/postMessage.gsp +++ b/grails-app/views/forum/postMessage.gsp @@ -83,10 +83,10 @@

Conversation history:

-
" style="border: 1px solid #a9a9a9; margin: 3px; padding: 3px; background: white"> +
" style="border: 1px solid #a9a9a9; margin: 3px; padding: 3px; background: white">
- On ${formatDate(date: reply.date, format: 'dd MMM yyyy')} at ${formatDate(date: reply.date, format: 'HH:mm:ss')} wrote: + On ${formatDate(date: reply.date, format: 'dd MMM yyyy')} at ${formatDate(date: reply.date, format: 'HH:mm:ss')} wrote:
${reply.text}
From 5be0a758ba187f4a10cce00bd555a9a370ce1d31 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 8 Apr 2015 12:21:20 +1000 Subject: [PATCH 04/12] #92 Revert change to ALA Bootstrap plugin, add environment and build.date to front page from Grails metadata sources --- grails-app/conf/BuildConfig.groovy | 4 +--- grails-app/conf/Config.groovy | 1 + .../au/org/ala/volunteer/VolunteerTagLib.groovy | 11 +++++++++++ grails-app/views/admin/index.gsp | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 20c0c9c3b..257f3402e 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -47,9 +47,7 @@ grails.project.dependency.resolution = { runtime ":quartz:1.0.1" runtime ":tiny-mce:3.4.9" runtime ":webxml:1.4.1" - //runtime ":ala-web-theme:0.8.5" - runtime ":ala-auth:1.2" - runtime ":ala-bootstrap2:1.4" + runtime ":ala-web-theme:0.8.5" runtime ":lesscss-resources:1.3.3" compile ':scaffolding:2.0.3' runtime ':database-migration:1.4.0' diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy index b8badfae9..9e8218dc2 100644 --- a/grails-app/conf/Config.groovy +++ b/grails-app/conf/Config.groovy @@ -130,6 +130,7 @@ expedition = [ ] // TODO Remove this after ala-auth plugin is updated +userDetails.url = "https://auth.ala.org.au/userdetails/userDetails/" userDetailsById.bulkPath = 'getUserDetailsFromIdList' volunteer.defaultProjectId = 6306 diff --git a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy index 53157572e..287a27393 100644 --- a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy +++ b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy @@ -6,6 +6,8 @@ import groovy.time.TimeCategory import au.org.ala.cas.util.AuthenticationCookieUtils import groovy.xml.MarkupBuilder +import java.text.DateFormat + class VolunteerTagLib { static namespace = 'cl' @@ -722,4 +724,13 @@ class VolunteerTagLib { def achievementsEnabled = { attrs -> settingsService.getSetting(SettingDefinition.EnableMyNotebook) && settingsService.getSetting(SettingDefinition.EnableAchievementCalculations) } + + def buildDate = { attrs -> + def bd = grailsApplication.metadata['build.date'] + if (bd) { + DateFormat.getDateInstance(DateFormat.MEDIUM).format(bd) + } else { + "" + } + } } \ No newline at end of file diff --git a/grails-app/views/admin/index.gsp b/grails-app/views/admin/index.gsp index fecb3b2d7..1d4d3676e 100644 --- a/grails-app/views/admin/index.gsp +++ b/grails-app/views/admin/index.gsp @@ -1,4 +1,4 @@ -<%@ page import="au.org.ala.volunteer.Project" %> +<%@ page import="java.text.DateFormat; au.org.ala.volunteer.Project" %> @@ -9,7 +9,7 @@ - Version ${grailsApplication.metadata['app.version']} (built ${grailsApplication.metadata['app.buildDate']} ${grailsApplication.metadata['app.buildProfile']} sha: ${grailsApplication.metadata['environment.TRAVIS_COMMIT']}) + Version ${grailsApplication.metadata['app.version']} (built  ${grails.util.Environment.current} sha: ${grailsApplication.metadata['environment.TRAVIS_COMMIT']}) From 15c26f88a6eabd6086d8a3532e919d4158e2a890 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 8 Apr 2015 12:32:55 +1000 Subject: [PATCH 05/12] #92 sadly build.date is a String and not a Date --- grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy index 287a27393..a348084d6 100644 --- a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy +++ b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy @@ -728,7 +728,7 @@ class VolunteerTagLib { def buildDate = { attrs -> def bd = grailsApplication.metadata['build.date'] if (bd) { - DateFormat.getDateInstance(DateFormat.MEDIUM).format(bd) + bd } else { "" } From de1d39a423622e5bc4496b2ac3771e1277d47d34 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 8 Apr 2015 14:07:12 +1000 Subject: [PATCH 06/12] #92 better date format for buildDate --- .../au/org/ala/volunteer/VolunteerTagLib.groovy | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy index a348084d6..7e3655053 100644 --- a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy +++ b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy @@ -2,11 +2,13 @@ package au.org.ala.volunteer import grails.converters.JSON import grails.util.Environment +import grails.util.Metadata import groovy.time.TimeCategory import au.org.ala.cas.util.AuthenticationCookieUtils import groovy.xml.MarkupBuilder import java.text.DateFormat +import java.text.SimpleDateFormat class VolunteerTagLib { @@ -21,7 +23,7 @@ class VolunteerTagLib { def authService def achievementService - static returnObjectForTags = ['emailForUserId', 'displayNameForUserId', 'achievementBadgeBase', 'newAchievements', 'achievementsEnabled'] + static returnObjectForTags = ['emailForUserId', 'displayNameForUserId', 'achievementBadgeBase', 'newAchievements', 'achievementsEnabled', 'buildDate'] def isLoggedIn = { attrs, body -> @@ -726,11 +728,13 @@ class VolunteerTagLib { } def buildDate = { attrs -> - def bd = grailsApplication.metadata['build.date'] + def bd = Metadata.current['build.date'] + log.info("Build Date type is ${bd?.class?.name}") + def df = new SimpleDateFormat('MMM d, yyyy') if (bd) { - bd + df.format(new SimpleDateFormat('EEE MMM dd HH:mm:ss zzz yyyy').parse(bd)) } else { - "" + df.format(new Date()) } } } \ No newline at end of file From 9bdbf49405ac84adf474573dacac846716f1d69d Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 8 Apr 2015 18:16:09 +1000 Subject: [PATCH 07/12] #93 Picklist limit too small: Allow uploading of CSV files into picklists to work around Tomcat max POST param size --- .../ala/volunteer/PicklistController.groovy | 8 ++- .../org/ala/volunteer/PicklistService.groovy | 6 +- grails-app/views/picklist/manage.gsp | 68 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/grails-app/controllers/au/org/ala/volunteer/PicklistController.groovy b/grails-app/controllers/au/org/ala/volunteer/PicklistController.groovy index 475a5a2ef..ff22f09f2 100644 --- a/grails-app/controllers/au/org/ala/volunteer/PicklistController.groovy +++ b/grails-app/controllers/au/org/ala/volunteer/PicklistController.groovy @@ -21,7 +21,13 @@ class PicklistController { } def uploadCsvData = { - picklistService.replaceItems(Long.parseLong(params.picklistId), params.picklist, params.institutionCode) + picklistService.replaceItems(Long.parseLong(params.picklistId), params.picklist.toCsvReader(), params.institutionCode) + redirect(action: "manage") + } + + def uploadCsvFile() { + def f = request.getFile('picklistFile') + picklistService.replaceItems(Long.parseLong(params.picklistId), f.inputStream.toCsvReader(['charset':'UTF-8']), params.institutionCode) redirect(action: "manage") } diff --git a/grails-app/services/au/org/ala/volunteer/PicklistService.groovy b/grails-app/services/au/org/ala/volunteer/PicklistService.groovy index 3ceaf7a8f..ea728a26f 100644 --- a/grails-app/services/au/org/ala/volunteer/PicklistService.groovy +++ b/grails-app/services/au/org/ala/volunteer/PicklistService.groovy @@ -1,5 +1,7 @@ package au.org.ala.volunteer +import au.com.bytecode.opencsv.CSVReader +import org.grails.plugins.csv.CSVReaderUtils import org.hibernate.FlushMode class PicklistService { @@ -33,7 +35,7 @@ class PicklistService { } } - def replaceItems(long picklistId, String csvdata, String institutionCode) { + def replaceItems(long picklistId, CSVReader csvdata, String institutionCode) { def picklist = Picklist.get(picklistId) // First delete the existing items... if (picklist) { @@ -50,7 +52,7 @@ class PicklistService { int rowsProcessed = 0; try { sessionFactory.currentSession.setFlushMode(FlushMode.MANUAL) - csvdata.eachCsvLine { tokens -> + csvdata.eachLine { tokens -> def value = tokens[0] def m = pattern.matcher(value) if (m.find()) { diff --git a/grails-app/views/picklist/manage.gsp b/grails-app/views/picklist/manage.gsp index 4c1aec91a..c64d7e2cf 100644 --- a/grails-app/views/picklist/manage.gsp +++ b/grails-app/views/picklist/manage.gsp @@ -24,7 +24,7 @@ -
+
+ + + $(document).ready(function() { @@ -71,7 +107,7 @@ onClose: function() { if (bvp.newCollectionCode) { // Add item to list... - var select = $("#institutionCode"); + var select = $("#institutionCode, #upInstitutionCode"); select.append( $('').val(bvp.newCollectionCode).html(bvp.newCollectionCode) ); @@ -81,8 +117,34 @@ }); }); + $('#upload-picklist-file').click(function(e) { + var modal = $('#picklistModal'); + var form = $('#picklist-form'); + modal.find('#upPicklistId').val(form.find('#picklistId').val()); + modal.find('#upInstitutionCode').val(form.find('#institutionCode').val()); + }); + + var maxSize = ${grailsApplication.config.bvp.maxPostSize ?: 2097152}; + $('#picklist').change(function(e) { + var pl = $('#upload-picklist-button'); + var disabled = pl.prop('disabled'); + var shouldDisable = byteLength($(e.target).val()) > maxSize; + if (disabled != shouldDisable) pl.prop('disabled', shouldDisable); + }) }); + function byteLength(str) { + // returns the byte length of an utf8 string + var s = str.length; + for (var i=str.length-1; i>=0; i--) { + var code = str.charCodeAt(i); + if (code > 0x7f && code <= 0x7ff) s++; + else if (code > 0x7ff && code <= 0xffff) s+=2; + if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate + } + return s; + } + From 9a26272a3911f057aa415248fa17b9be496b0bfe Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 9 Apr 2015 14:52:20 +1000 Subject: [PATCH 08/12] #93 Add index to picklist_id and institution_code for faster access to large picklists --- grails-app/domain/au/org/ala/volunteer/PicklistItem.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grails-app/domain/au/org/ala/volunteer/PicklistItem.groovy b/grails-app/domain/au/org/ala/volunteer/PicklistItem.groovy index 397df07d5..cdd9204a4 100644 --- a/grails-app/domain/au/org/ala/volunteer/PicklistItem.groovy +++ b/grails-app/domain/au/org/ala/volunteer/PicklistItem.groovy @@ -9,6 +9,8 @@ class PicklistItem implements Serializable { static mapping = { version false + picklist index: 'picklist_item_picklist_id_institution_code_idx' + institutionCode index: 'picklist_item_picklist_id_institution_code_idx' } static constraints = { From f3c6707b23a983841a8194acc3ed0f6c087f354b Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Fri, 10 Apr 2015 15:48:57 +1000 Subject: [PATCH 09/12] #85 User report; Start writing column headers straight away, batch queries to speed up processing time --- .../org/ala/volunteer/AjaxController.groovy | 124 ++++++++++++++---- .../ala/volunteer/CSVHeadingsWriter.groovy | 123 +++++++++++++++++ 2 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 src/groovy/au/org/ala/volunteer/CSVHeadingsWriter.groovy diff --git a/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy b/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy index f9e968454..7eef95e46 100644 --- a/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy +++ b/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy @@ -1,6 +1,7 @@ package au.org.ala.volunteer import au.org.ala.volunteer.collectory.CollectoryProviderDto +import com.google.common.base.Stopwatch import com.google.common.collect.ImmutableMap import com.google.common.collect.Sets import com.google.gson.Gson @@ -14,6 +15,8 @@ import java.sql.ResultSet import org.grails.plugins.csv.CSVWriter import org.grails.plugins.csv.CSVWriterColumnsBuilder +import static grails.async.Promises.* + class AjaxController { def taskService @@ -27,6 +30,7 @@ class AjaxController { def authService def settingsService def achievementService + def sessionFactory static responseFormats = ['json', 'xml'] @@ -90,46 +94,114 @@ class AjaxController { return; } - def report = [] - def users = User.list() - def serviceResults = [:] - try { - serviceResults = authService.getUserDetailsById(users*.userId) - } catch (Exception e) { - log.warn("couldn't get user details from web service", e) + // Pre-create the writer and writer the headings straight away to prevent a read timeout. + def writer + if (params.wt && params.wt == 'csv') { + def nodata = params.nodata ?: 'nodata' + + response.addHeader("Content-type", "text/plain") + + writer = new CSVHeadingsWriter((Writer) response.writer, { + 'user_id' { it[0] } + 'display_name' { it[1] } + 'transcribed_count' { it[2] } + 'validated_count' { it[3] } + 'last_activity' { it[4] ?: nodata } + 'projects_count' { it[5] } + 'volunteer_since' { it[6] } + }) + writer.writeHeadings() + response.flushBuffer() + } + + + def asyncCounts = Task.async.withStatelessSession { + def sw1 = new Stopwatch().start() + def vs = (Task.withCriteria { + projections { + groupProperty('fullyValidatedBy') + count('id') + } + }).collectEntries { [(it[0]): it[1]] } + def ts = (Task.withCriteria { + projections { + groupProperty('fullyTranscribedBy') + count('id') + } + }).collectEntries { [(it[0]): it[1]] } + sw1.stop() + log.info("UserReport counts took ${sw1.toString()}") + [vs: vs, ts: ts] } - for (User user : users) { - def transcribedCount = Task.countByFullyTranscribedBy(user.userId) - def validatedCount = Task.countByFullyValidatedBy(user.userId) - def lastActivity = ViewedTask.executeQuery("select to_timestamp(max(vt.lastView)/1000) from ViewedTask vt where vt.userId = :userId", [userId: user.userId])[0] + def asyncLastActivities = ViewedTask.async.withStatelessSession { + def sw2 = new Stopwatch().start() + def lastActivities = ViewedTask.executeQuery("select vt.userId, to_timestamp(max(vt.lastView)/1000) from ViewedTask vt group by vt.userId").collectEntries { [(it[0]): it[1]] } + sw2.stop() + log.info("UserReport viewedTasks took ${sw2.toString()}") + lastActivities + } - def projectCount = ViewedTask.executeQuery("select distinct t.project from Task t where t.fullyTranscribedBy = :userId", [userId: user.userId]).size() + def asyncProjectCounts = Task.async.withStatelessSession { + def sw4 = new Stopwatch().start() + def projectCounts = Task.executeQuery("select t.fullyTranscribedBy, count(distinct t.project) from Task t group by t.fullyTranscribedBy ").collectEntries { [(it[0]): it[1]] } + sw4.stop() + log.info("UserReport projectCounts took ${sw4.toString()}") + projectCounts + } - //def props = userService.detailsForUserId(user.userId) - def serviceResult = serviceResults?.users?.get(user.userId) + def sw3 = new Stopwatch().start() + def asyncUserDetails = User.async.list().then { users -> + def serviceResults = [:] + try { + serviceResults = authService.getUserDetailsById(users*.userId) + } catch (Exception e) { + log.warn("couldn't get user details from web service", e) + } + sw3.stop() + log.info("UserReport user details took ${sw3.toString()}") + + [users: users, results: serviceResults] + } + + def asyncResults = waitAll(asyncCounts, asyncLastActivities, asyncProjectCounts, asyncUserDetails) + + // transform raw results into map(id -> count) + def validateds = asyncResults[0].vs + def transcribeds = asyncResults[0].ts + + def lastActivities = asyncResults[1] + def projectCounts = asyncResults[2] + + def users = asyncResults[3].users + def serviceResults = asyncResults[3].results + + def report = [] + + def sw5 = new Stopwatch().start() + for (User user: users) { + def id = user.userId + def transcribedCount = transcribeds[id] ?: 0 + def validatedCount = validateds[id] ?: 0 + def lastActivity = lastActivities[id] + def projectCount = projectCounts[id]?: 0 + + def serviceResult = serviceResults?.users?.get(id) report.add([serviceResult?.userName ?: user.email, serviceResult?.displayName ?: user.displayName, transcribedCount, validatedCount, lastActivity, projectCount, user.created]) } + sw5.stop() + log.info("UserReport generate report took ${sw5}") + sw5.reset().start() // Sort by the transcribed count report.sort({ row1, row2 -> row2[2] - row1[2]}) + sw5.stop() + log.info("UserReport sort took ${sw5.toString()}") - def nodata = params.nodata ?: 'nodata' if (params.wt && params.wt == 'csv') { - response.addHeader("Content-type", "text/plain") - - def writer = new CSVWriter((Writer) response.writer, { - 'user_id' { it[0] } - 'display_name' { it[1] } - 'transcribed_count' { it[2] } - 'validated_count' { it[3] } - 'last_activity' { it[4] ?: nodata } - 'projects_count' { it[5] } - 'volunteer_since' { it[6] } - }) for (def row : report) { writer << row } diff --git a/src/groovy/au/org/ala/volunteer/CSVHeadingsWriter.groovy b/src/groovy/au/org/ala/volunteer/CSVHeadingsWriter.groovy new file mode 100644 index 000000000..1b4cf5868 --- /dev/null +++ b/src/groovy/au/org/ala/volunteer/CSVHeadingsWriter.groovy @@ -0,0 +1,123 @@ +package au.org.ala.volunteer + +import org.grails.plugins.csv.CSVWriter +import org.grails.plugins.csv.CSVWriterColumnsBuilder + +/** + * Writes CSV content to a given writer, using a definition DSL. + * + * Examples: + * + * def sw = new StringWriter() + * def b = new CSVBuilder(sw) { + * col1 { it.val1 } + * col2 { it.val2 } + * } + * b << [val1: 'a', val2: 'b'] + * b << [val1: 'c', val2: 'd'] + * + * assert b.writer.toString() == '''"col1","col2" + * "a",b" + * "c","d"''' + * + * This class is NOT threadsafe. + * + * This class is a copy of org.grails.plugins.csv.CSVWriter with the writeHeadings made public. + * + * @author Luke Daley + * + * + */ +class CSVHeadingsWriter { + + private columns = [:] + + final writer + + private cachedQuote + private cachedQuoteEscape + private cachedQuoteReplace + private cachedValueSeperator + private cachedRowSeperator + + private producers + private lastProducer + + private headingsWritten = false + + CSVHeadingsWriter(Writer writer, Closure definition) { + this.writer = writer + + columns = CSVWriterColumnsBuilder.build(definition) + + // do these once incase subclasses are reading from config etc. + cachedQuote = this.quote + cachedQuoteEscape = this.quoteEscape + cachedQuoteReplace = this.quoteEscape + this.quote + cachedValueSeperator = this.valueSeperator + cachedRowSeperator = this.rowSeperator + + producers = columns.values().toList() + lastProducer = producers.last() + } + + def leftShift(row) { + write(row) + this + } + + def write(row) { + if (!headingsWritten) { + writeHeadings() + } + + writer << this.@cachedRowSeperator + for (producer in this.@producers) { + writeValue(producer(row).toString()) + if (!producer.is(this.@lastProducer)) { + writer << this.@cachedValueSeperator + } + } + + writer + } + + def writeAll(Collection rows) { + for (row in rows) { + write(row) + } + writer + } + + def writeHeadings() { + columns.eachWithIndex { column, i -> + writeValue(column.key) + if (i != (columns.size() - 1)) { + writer << this.@cachedValueSeperator + } + } + headingsWritten = true + } + + protected writeValue(String value) { + writer << this.@cachedQuote + writer << value.replace(this.@cachedQuote, this.@cachedQuoteReplace) + writer << this.@cachedQuote + } + + protected getQuote() { + '"' + } + + protected getQuoteEscape() { + '"' + } + + protected getValueSeperator() { + "," + } + + protected getRowSeperator() { + "\n" + } +} From 848ca3af187aa9e1956ddc1f65590e48dd6f06d9 Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Tue, 14 Apr 2015 11:29:20 +1000 Subject: [PATCH 10/12] Tweak log levels --- .../au/org/ala/volunteer/AjaxController.groovy | 14 +++++++------- .../au/org/ala/volunteer/VolunteerTagLib.groovy | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy b/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy index 7eef95e46..47cd60e8b 100644 --- a/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy +++ b/grails-app/controllers/au/org/ala/volunteer/AjaxController.groovy @@ -95,7 +95,7 @@ class AjaxController { } - // Pre-create the writer and writer the headings straight away to prevent a read timeout. + // Pre-create the writer and write the headings straight away to prevent a read timeout. def writer if (params.wt && params.wt == 'csv') { def nodata = params.nodata ?: 'nodata' @@ -131,7 +131,7 @@ class AjaxController { } }).collectEntries { [(it[0]): it[1]] } sw1.stop() - log.info("UserReport counts took ${sw1.toString()}") + log.debug("UserReport counts took ${sw1.toString()}") [vs: vs, ts: ts] } @@ -139,7 +139,7 @@ class AjaxController { def sw2 = new Stopwatch().start() def lastActivities = ViewedTask.executeQuery("select vt.userId, to_timestamp(max(vt.lastView)/1000) from ViewedTask vt group by vt.userId").collectEntries { [(it[0]): it[1]] } sw2.stop() - log.info("UserReport viewedTasks took ${sw2.toString()}") + log.debug("UserReport viewedTasks took ${sw2.toString()}") lastActivities } @@ -147,7 +147,7 @@ class AjaxController { def sw4 = new Stopwatch().start() def projectCounts = Task.executeQuery("select t.fullyTranscribedBy, count(distinct t.project) from Task t group by t.fullyTranscribedBy ").collectEntries { [(it[0]): it[1]] } sw4.stop() - log.info("UserReport projectCounts took ${sw4.toString()}") + log.debug("UserReport projectCounts took ${sw4.toString()}") projectCounts } @@ -160,7 +160,7 @@ class AjaxController { log.warn("couldn't get user details from web service", e) } sw3.stop() - log.info("UserReport user details took ${sw3.toString()}") + log.debug("UserReport user details took ${sw3.toString()}") [users: users, results: serviceResults] } @@ -191,13 +191,13 @@ class AjaxController { report.add([serviceResult?.userName ?: user.email, serviceResult?.displayName ?: user.displayName, transcribedCount, validatedCount, lastActivity, projectCount, user.created]) } sw5.stop() - log.info("UserReport generate report took ${sw5}") + log.debug("UserReport generate report took ${sw5}") sw5.reset().start() // Sort by the transcribed count report.sort({ row1, row2 -> row2[2] - row1[2]}) sw5.stop() - log.info("UserReport sort took ${sw5.toString()}") + log.debug("UserReport sort took ${sw5.toString()}") if (params.wt && params.wt == 'csv') { diff --git a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy index 7e3655053..0c0a15ed3 100644 --- a/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy +++ b/grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy @@ -729,7 +729,7 @@ class VolunteerTagLib { def buildDate = { attrs -> def bd = Metadata.current['build.date'] - log.info("Build Date type is ${bd?.class?.name}") + log.debug("Build Date type is ${bd?.class?.name}") def df = new SimpleDateFormat('MMM d, yyyy') if (bd) { df.format(new SimpleDateFormat('EEE MMM dd HH:mm:ss zzz yyyy').parse(bd)) From b3fca7eef27460186e06bd3ef81ea470738d3f3b Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Wed, 15 Apr 2015 16:38:39 +1000 Subject: [PATCH 11/12] #81 Help bubbles go off screen --- .../org/ala/volunteer/TranscribeTagLib.groovy | 18 ++++++++++++------ .../_aerialObservationsTranscribe.gsp | 2 +- .../views/transcribe/_journalTranscribe.gsp | 2 +- .../transcribe/_threeColumnLabelTranscribe.gsp | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/grails-app/taglib/au/org/ala/volunteer/TranscribeTagLib.groovy b/grails-app/taglib/au/org/ala/volunteer/TranscribeTagLib.groovy index 11b44f844..f361eb7a5 100644 --- a/grails-app/taglib/au/org/ala/volunteer/TranscribeTagLib.groovy +++ b/grails-app/taglib/au/org/ala/volunteer/TranscribeTagLib.groovy @@ -66,6 +66,8 @@ class TranscribeTagLib { * @attr labelClass * @attr valueClass * @attr field Optional, if the template already has the field object, no need to look up from it's name. + * @attr helpTargetPosition Optional, the target position for the qtip help pop up + * @attr helpTooltipPosition Optional, the tooltip position for the qtip help pop up */ def renderFieldBootstrap = { attrs, body -> @@ -75,6 +77,8 @@ class TranscribeTagLib { def valueClass = attrs.valueClass ?: "span12" def rowClass = attrs.rowClass ?: "row-fluid" def recordIdx = attrs.recordIdx ?: 0 + def helpTargetPosition = attrs.helpTargetPosition + def helpTooltipPosition = attrs.helpTooltipPosition if (!task) { return @@ -87,7 +91,7 @@ class TranscribeTagLib { } def mb = new MarkupBuilder(out) - renderFieldBootstrapImpl(mb, field, task, recordValues, recordIdx, labelClass, valueClass, attrs, rowClass) + renderFieldBootstrapImpl(mb, field, task, recordValues, recordIdx, labelClass, valueClass, attrs, rowClass, helpTargetPosition, helpTooltipPosition) } private String getFieldLabel(TemplateField field) { @@ -98,7 +102,7 @@ class TranscribeTagLib { } } - private void renderFieldBootstrapImpl(MarkupBuilder mb, TemplateField field, Task task, recordValues, int recordIdx, String labelClass, String valueClass, Map attrs, String rowClass = "row-fluid") { + private void renderFieldBootstrapImpl(MarkupBuilder mb, TemplateField field, Task task, recordValues, int recordIdx, String labelClass, String valueClass, Map attrs, String rowClass = "row-fluid", String helpTargetPosition = null, String helpTooltipPosition = null) { if (!task || !field) { return @@ -134,7 +138,7 @@ class TranscribeTagLib { mkp.yieldUnescaped(widgetHtml) } div(class:'span2') { - renderFieldHelp(mb, field) + renderFieldHelp(mb, field, helpTargetPosition, helpTooltipPosition) } } } @@ -518,13 +522,15 @@ class TranscribeTagLib { def fieldHelp = { attrs, body -> def field = attrs.field as TemplateField - renderFieldHelp(new MarkupBuilder(out), field) + def tooltipPosition = attrs.tooltipPosition + def targetPosition = attrs.targetPosition + renderFieldHelp(new MarkupBuilder(out), field, targetPosition, tooltipPosition) } - private renderFieldHelp(MarkupBuilder mb, TemplateField field) { + private renderFieldHelp(MarkupBuilder mb, TemplateField field, String targetPosition = null, String tooltipPosition = null) { if (field && field.helpText) { def helpText = markdownService.markdown(field.helpText) - mb.a(href:'#', class:'fieldHelp', title:helpText, tabindex: "-1") { + mb.a(href:'#', class:'fieldHelp', title:helpText, tabindex: "-1", targetPosition: targetPosition, tooltipPosition: tooltipPosition) { span(class:'help-container') { mkp.yieldUnescaped(' ') } diff --git a/grails-app/views/transcribe/_aerialObservationsTranscribe.gsp b/grails-app/views/transcribe/_aerialObservationsTranscribe.gsp index 7b655446b..3060565a6 100644 --- a/grails-app/views/transcribe/_aerialObservationsTranscribe.gsp +++ b/grails-app/views/transcribe/_aerialObservationsTranscribe.gsp @@ -49,7 +49,7 @@
- +
diff --git a/grails-app/views/transcribe/_journalTranscribe.gsp b/grails-app/views/transcribe/_journalTranscribe.gsp index 0728fa0d0..f737c4c5f 100644 --- a/grails-app/views/transcribe/_journalTranscribe.gsp +++ b/grails-app/views/transcribe/_journalTranscribe.gsp @@ -46,7 +46,7 @@
${nextSectionNumber()}. ${allTextField?.label ?: "Transcribe All Text"} -   +   Shrink
diff --git a/grails-app/views/transcribe/_threeColumnLabelTranscribe.gsp b/grails-app/views/transcribe/_threeColumnLabelTranscribe.gsp index f389f3921..dfa28f7fc 100644 --- a/grails-app/views/transcribe/_threeColumnLabelTranscribe.gsp +++ b/grails-app/views/transcribe/_threeColumnLabelTranscribe.gsp @@ -56,7 +56,7 @@
- + ${nextSectionNumber()}. ${allTextField?.label ?: "Transcribe All Text"} @@ -75,7 +75,7 @@
-   +  
From 2bc866561e3f48b66d2e7f5dfa6ce2a2dad46e7f Mon Sep 17 00:00:00 2001 From: Simon Bear Date: Thu, 16 Apr 2015 18:14:21 +1000 Subject: [PATCH 12/12] Release version 2.1.1 --- application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application.properties b/application.properties index ef434ecbe..4f64808fa 100644 --- a/application.properties +++ b/application.properties @@ -6,4 +6,4 @@ app.buildProfile=development app.grails.version=2.3.11 app.name=volunteer-portal app.servlet.version=2.5 -app.version=2.1.1-SNAPSHOT +app.version=2.1.1