Skip to content

Commit f347ad4

Browse files
committed
Added test cases for #357
1 parent e506dc9 commit f347ad4

File tree

4 files changed

+231
-15
lines changed

4 files changed

+231
-15
lines changed

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.google.common.base.Stopwatch
66
import grails.transaction.Transactional
77

88
import java.util.concurrent.TimeUnit
9+
import java.util.concurrent.atomic.AtomicInteger
910
import java.util.regex.Pattern
1011
import java.util.zip.ZipOutputStream
1112
import java.util.zip.ZipEntry
@@ -107,7 +108,13 @@ class ExportService {
107108

108109
Map results = [:]
109110
if (project.requiredNumberOfTranscriptions > 1) {
110-
results = valuesMap
111+
if (valuesMap) {
112+
results = valuesMap
113+
} else {
114+
// return empty map to allow the export of task fields even though there are no transcription fields
115+
// this behaviour is consistent with the single transcription task
116+
results = [(EMPTY_TRANSCRIPTIONID): [:]]
117+
}
111118
}
112119
else {
113120
results << getTranscribedAndUploadedFields(task, valuesMap)
@@ -195,6 +202,7 @@ class ExportService {
195202

196203
int threadPoolSize = grailsApplication.config.exportCSVThreadPoolSize ?: THREAD_POOL
197204
GParsPool.withPool threadPoolSize, {
205+
final AtomicInteger numberOfTasks = new AtomicInteger(0)
198206
taskList.eachParallel { Task task ->
199207
def sw2 = Stopwatch.createUnstarted()
200208
def sw3 = Stopwatch.createUnstarted()
@@ -229,8 +237,12 @@ class ExportService {
229237
def elapsed = sw2.elapsed(MILLISECONDS)
230238
if (elapsed > 50) log.debug("Got column values in {}ms", elapsed)
231239
writer.writeNext(values as String[])
240+
//valuesList.add(values as String[])
232241
}
242+
numberOfTasks.addAndGet(1)
243+
//writer.writeAll(valuesList)
233244
}
245+
log.info ("Wrote {} tasks", numberOfTasks.toString())
234246
}
235247

236248
log.debug("Wrote all tasks in {}ms", sw.elapsed(MILLISECONDS))
@@ -292,6 +304,7 @@ class ExportService {
292304

293305
int threadPoolSize = grailsApplication.config.exportCSVThreadPoolSize ?: THREAD_POOL
294306
GParsPool.withPool threadPoolSize, {
307+
final AtomicInteger numberOfTasks = new AtomicInteger(0)
295308
taskList.eachParallel { task ->
296309
Map toExport = getTranscriptionsToExport(project, task, valueMap[task.id])
297310
toExport.each { transcriptionId, transcriptionValueMap ->
@@ -309,7 +322,9 @@ class ExportService {
309322
writer.writeNext(values)
310323
}
311324
}
325+
numberOfTasks.addAndGet(1)
312326
}
327+
log.info ("Wrote {} tasks", numberOfTasks.toString())
313328
}
314329
writer.flush();
315330
zipStream.closeEntry();

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

+27-11
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,38 @@ class TaskService {
146146
Task.findAllByProjectAndIsFullyTranscribed(project, true, params)
147147
}
148148

149+
/***
150+
* Obtain Fully Transcribed tasks and the corresponding transcriptions for the project (eager fetching)
151+
* Note: if there are 2000 fully transcribed tasks and 4000 transcriptions (2 transcriptions per task), this should return 2000 rows of tasks.
152+
*/
149153
List getFullyTranscribedTasksAndTranscriptions(Project projectInstance, Map params) {
150-
Task.createCriteria().list (params) {
151-
eq 'project', projectInstance
152-
eq 'isFullyTranscribed', true
153-
fetchMode 'transcriptions', FetchMode.JOIN
154-
}
154+
Task.executeQuery("""
155+
select t from Task t
156+
left outer join fetch t.transcriptions
157+
where t.project = :projectInstance
158+
and t.isFullyTranscribed = true
159+
order by t.id
160+
""", [projectInstance: projectInstance], params)
155161
}
156162

157-
List getValidTranscribedTasks(Project project, Map params) {
158-
Task.createCriteria().list (params) {
159-
eq 'project', project
160-
eq 'isValid', true
161-
fetchMode 'transcriptions', FetchMode.JOIN
162-
}
163+
/***
164+
* Obtain Fully Validated tasks and the corresponding transcriptions for the project (eager fetching)
165+
* Note: if there are 2000 validated tasks and 4000 transcriptions (2 transcriptions per task), this should return 2000 rows of tasks.
166+
*/
167+
List getValidTranscribedTasks(Project projectInstance, Map params) {
168+
Task.executeQuery("""
169+
select t from Task t
170+
left outer join fetch t.transcriptions
171+
where t.project = :projectInstance
172+
and t.isValid = true
173+
order by t.id
174+
""", [projectInstance: projectInstance], params)
163175
}
164176

177+
/***
178+
* Obtain all tasks and the corresponding transcriptions for the project (eager fetching)
179+
* Note: if there are 2000 tasks and 4000 transcriptions (2 transcriptions per task), this should return 2000 rows of tasks.
180+
*/
165181
List getAllTasksAndTranscriptionsIfExists(Project projectInstance, Map params) {
166182
Task.executeQuery("""
167183
select t from Task t

grails-app/views/task/exportOptionsFragment.gsp

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
$("#btnExportTasks").click(function (e) {
3636
e.preventDefault();
3737
var format = $("input:radio[name='optionsExport']:checked").val();
38-
var url = "${createLink(controller:'project', action:'exportCSV', id: projectId, params:[validated: exportCriteria == 'validated', transcribed: exportCriteria=='transcribed'])}&exportFormat=" + format;
38+
var url = "${createLink(controller:'project', action:'exportCSV', id: projectId, params:[validated: exportCriteria == 'validated', transcribed: exportCriteria=='transcribed']).encodeAsJavaScript()}&exportFormat=" + format;
3939
window.location = url;
4040
});
4141

src/test/groovy/au/org/ala/volunteer/ExportServiceSpec.groovy

+187-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class ExportServiceSpec extends Specification {
2727
def setup() {
2828
fieldService = Mock(FieldService)
2929
service.fieldService = fieldService
30+
grailsApplication.config.exportCSVThreadPoolSize = 1
31+
service.grailsApplication = grailsApplication
3032
taskService = Mock(TaskService)
3133
service.taskService = taskService
3234
response = new GrailsMockHttpServletResponse()
@@ -48,8 +50,8 @@ class ExportServiceSpec extends Specification {
4850
mockDomain(Project, [project])
4951
}
5052

51-
private Task createTask() {
52-
Task task = new Task(transcriptions: new HashSet(), project:project)
53+
private Task createTask(String externalIdentifier = '') {
54+
Task task = new Task(transcriptions: new HashSet(), project:project, externalIdentifier: externalIdentifier)
5355
project.tasks.add(task)
5456
mockDomain(Task, [task])
5557

@@ -71,6 +73,189 @@ class ExportServiceSpec extends Specification {
7173
fields
7274
}
7375

76+
def "Test non parrallel writes is working for larger tasks"() {
77+
setup:
78+
project.transcriptionsPerTask = 2
79+
String userA = 'userA'
80+
String userB = 'userB'
81+
82+
List allFields = new ArrayList()
83+
84+
int numOfTasks = 100
85+
for (int i= 1; i <= numOfTasks; i++) {
86+
Task task = createTask("image${i}.jpq")
87+
List fields1 = transcribeTask(task, [[name:"scientificName", value:"Magpie"], [name:"individualCount", value:"10"]], userA)
88+
allFields.addAll(fields1)
89+
List fields2 = transcribeTask(task, [[name:"scientificName", value:"Crow"], [name:"individualCount", value:"5"]], userB)
90+
allFields.addAll(fields2)
91+
}
92+
93+
List<Task> taskList = project.tasks as List
94+
List<String> fieldNames = taskOrTranscriptionFields + ["scientificName", "individualCount"]
95+
96+
when:
97+
service.export_default(project, taskList, fieldNames, allFields, response)
98+
List results = new CSVMapReader(new StringReader(response.text)).readAll()
99+
100+
then:
101+
1 * fieldService.getMaxRecordIndexByFieldForProject(project) >> [['scientificName', 0], ['individualCount', 0]]
102+
1 * taskService.getUserMapFromTaskList(taskList) >> [(userA):[displayName:userA], (userB):[displayName:userB]]
103+
104+
and:
105+
results.size() == 200
106+
for (int i= 1; i <= numOfTasks; i++) {
107+
results.findAll { it.externalIdentifier == "image${i}.jpq" }.size() == 2
108+
}
109+
results.findAll{it.transcriberID == userA}.size() == numOfTasks
110+
results.findAll{it.transcriberID == userB}.size() == numOfTasks
111+
}
112+
113+
def "Test parrallel writes is working for larger tasks"() {
114+
setup:
115+
grailsApplication.config.exportCSVThreadPoolSize = 10
116+
service.grailsApplication = grailsApplication
117+
118+
project.transcriptionsPerTask = 2
119+
String userA = 'userA'
120+
String userB = 'userB'
121+
122+
List allFields = new ArrayList()
123+
124+
int numOfTasks = 100
125+
for (int i= 1; i <= numOfTasks; i++) {
126+
Task task = createTask("image${i}.jpq")
127+
List fields1 = transcribeTask(task, [[name:"scientificName", value:"Magpie"], [name:"individualCount", value:"10"]], userA)
128+
allFields.addAll(fields1)
129+
List fields2 = transcribeTask(task, [[name:"scientificName", value:"Crow"], [name:"individualCount", value:"5"]], userB)
130+
allFields.addAll(fields2)
131+
}
132+
133+
List<Task> taskList = project.tasks as List
134+
List<String> fieldNames = taskOrTranscriptionFields + ["scientificName", "individualCount"]
135+
136+
when:
137+
service.export_default(project, taskList, fieldNames, allFields, response)
138+
List results = new CSVMapReader(new StringReader(response.text)).readAll()
139+
140+
then:
141+
1 * fieldService.getMaxRecordIndexByFieldForProject(project) >> [['scientificName', 0], ['individualCount', 0]]
142+
1 * taskService.getUserMapFromTaskList(taskList) >> [(userA):[displayName:userA], (userB):[displayName:userB]]
143+
144+
and:
145+
results.size() == 200
146+
for (int i= 1; i <= numOfTasks; i++) {
147+
results.findAll { it.externalIdentifier == "image${i}.jpq" }.size() == 2
148+
}
149+
results.findAll{it.transcriberID == userA}.size() == 100
150+
results.findAll{it.transcriberID == userB}.size() == 100
151+
}
152+
153+
def "All project transcription tasks data can be exported in CSV form for multiple transcription project"() {
154+
setup:
155+
project.transcriptionsPerTask = 2
156+
String userA = 'userA'
157+
String userB = 'userB'
158+
Task birdTask = createTask()
159+
Task kangarooTask = createTask()
160+
List birdFields1 = transcribeTask(birdTask, [[name:"scientificName", value:"Magpie"], [name:"individualCount", value:"10"]], userA)
161+
List birdFields2 = transcribeTask(birdTask, [[name:"scientificName", value:"Crow"], [name:"individualCount", value:"5"]], userB)
162+
List kangarooFields1 = transcribeTask(kangarooTask, [[name:"scientificName", value:"Red Kangaroo"], [name:"individualCount", value:"2"]], userA)
163+
List kangarooFields2 = transcribeTask(kangarooTask, [[name:"scientificName", value:"Red Kangaroo"], [name:"individualCount", value:"2"]], userB)
164+
165+
List<Task> taskList = project.tasks as List
166+
List<String> fieldNames = taskOrTranscriptionFields + ["scientificName", "individualCount"]
167+
168+
when:
169+
service.export_default(project, taskList, fieldNames, birdFields1 + birdFields2 + kangarooFields1 + kangarooFields2, response)
170+
List results = new CSVMapReader(new StringReader(response.text)).readAll()
171+
172+
then:
173+
1 * fieldService.getMaxRecordIndexByFieldForProject(project) >> [['scientificName', 0], ['individualCount', 0]]
174+
1 * taskService.getUserMapFromTaskList(taskList) >> [(userA):[displayName:userA], (userB):[displayName:userB]]
175+
176+
and:
177+
results.size() == 4 // not counting headers
178+
results.findAll{it.transcriberID == userA && it.taskID == birdTask.id.toString() && it.scientificName == 'Magpie'}.size() == 1
179+
results.findAll{it.transcriberID == userA && it.taskID == kangarooTask.id.toString() && it.scientificName == 'Red Kangaroo'}.size() == 1
180+
results.findAll{it.transcriberID == userB && it.taskID == birdTask.id.toString() && it.scientificName == 'Crow'}.size() == 1
181+
results.findAll{it.transcriberID == userB && it.taskID == kangarooTask.id.toString() && it.scientificName == 'Red Kangaroo'}.size() == 1
182+
}
183+
184+
def "Partially transcribed project tasks data can be exported in CSV form for multiple transcription project"() {
185+
setup:
186+
project.transcriptionsPerTask = 2
187+
String userA = 'userA'
188+
String userB = 'userB'
189+
Task birdTask = createTask()
190+
Task kangarooTask = createTask()
191+
List birdFields1 = transcribeTask(birdTask, [[name:"scientificName", value:"Magpie"], [name:"individualCount", value:"10"]], userA)
192+
List kangarooFields1 = transcribeTask(kangarooTask, [[name:"scientificName", value:"Red Kangaroo"], [name:"individualCount", value:"2"]], userB)
193+
194+
List<Task> taskList = project.tasks as List
195+
List<String> fieldNames = taskOrTranscriptionFields + ["scientificName", "individualCount"]
196+
197+
when:
198+
service.export_default(project, taskList, fieldNames, birdFields1 + kangarooFields1, response)
199+
List results = new CSVMapReader(new StringReader(response.text)).readAll()
200+
201+
then:
202+
1 * fieldService.getMaxRecordIndexByFieldForProject(project) >> [['scientificName', 0], ['individualCount', 0]]
203+
1 * taskService.getUserMapFromTaskList(taskList) >> [(userA):[displayName:userA], (userB):[displayName:userB]]
204+
205+
and:
206+
results.size() == 2 //not counting headers
207+
results.findAll{it.transcriberID == userA && it.taskID == birdTask.id.toString() && it.scientificName == 'Magpie'}.size() == 1
208+
results.findAll{it.transcriberID == userB && it.taskID == kangarooTask.id.toString() && it.scientificName == 'Red Kangaroo'}.size() == 1
209+
}
210+
211+
def "For multiple transcription project with tasks that have not been transcribed, data can be exported in CSV"() {
212+
setup:
213+
project.transcriptionsPerTask = 2
214+
String macpieImage = 'macpieImage.jpg'
215+
String kangarooImage = 'kangarooImage.jpg'
216+
Task birdTask = createTask(macpieImage)
217+
Task kangarooTask = createTask(kangarooImage)
218+
219+
List<Task> taskList = project.tasks as List
220+
List<String> fieldNames = taskOrTranscriptionFields
221+
222+
when:
223+
service.export_default(project, taskList, fieldNames, [], response)
224+
List results = new CSVMapReader(new StringReader(response.text)).readAll()
225+
226+
then:
227+
1 * fieldService.getMaxRecordIndexByFieldForProject(project) >> []
228+
1 * taskService.getUserMapFromTaskList(taskList) >> [:]
229+
230+
and:
231+
results.size() == 2
232+
results.find{it.taskID == kangarooTask.id.toString()}.externalIdentifier == kangarooImage
233+
results.find{it.taskID == birdTask.id.toString()}.externalIdentifier == macpieImage
234+
}
235+
236+
def "For single transcription project with tasks that have not been transcribed, data can be exported in CSV"() {
237+
setup:
238+
project.transcriptionsPerTask = 1
239+
String macpieImage = 'macpieImage.jpg'
240+
Task birdTask = createTask(macpieImage)
241+
242+
List<Task> taskList = project.tasks as List
243+
List<String> fieldNames = taskOrTranscriptionFields
244+
245+
when:
246+
service.export_default(project, taskList, fieldNames, [], response)
247+
List results = new CSVMapReader(new StringReader(response.text)).readAll()
248+
249+
then:
250+
1 * fieldService.getMaxRecordIndexByFieldForProject(project) >> []
251+
1 * taskService.getUserMapFromTaskList(taskList) >> [:]
252+
253+
and:
254+
results.size() == 1
255+
results[0].externalIdentifier == macpieImage
256+
}
257+
258+
74259
def "All project task data can be exported in CSV form for single transcription projects"() {
75260
setup:
76261
String userId = '1234'

0 commit comments

Comments
 (0)