1 | /** |
---|
2 | * SimpleWizardController Controler |
---|
3 | * |
---|
4 | * Description of my controller |
---|
5 | * |
---|
6 | * @author your email (+name?) |
---|
7 | * @since 2010mmdd |
---|
8 | * @package ??? |
---|
9 | * |
---|
10 | * Revision information: |
---|
11 | * $Rev: 1838 $ |
---|
12 | * $Author: s.h.sikkema@gmail.com $ |
---|
13 | * $Date: 2011-05-11 16:16:26 +0000 (wo, 11 mei 2011) $ |
---|
14 | */ |
---|
15 | package dbnp.studycapturing |
---|
16 | |
---|
17 | import org.apache.poi.ss.usermodel.DataFormatter |
---|
18 | import org.dbnp.gdt.* |
---|
19 | import grails.plugins.springsecurity.Secured |
---|
20 | import dbnp.authentication.SecUser |
---|
21 | import dbnp.importer.ImportCell |
---|
22 | import dbnp.importer.ImportRecord |
---|
23 | import dbnp.importer.MappingColumn |
---|
24 | import org.hibernate.SessionFactory |
---|
25 | import org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass; |
---|
26 | |
---|
27 | @Secured(['IS_AUTHENTICATED_REMEMBERED']) |
---|
28 | class SimpleWizardController extends StudyWizardController { |
---|
29 | def authenticationService |
---|
30 | def fileService |
---|
31 | def importerService |
---|
32 | def gdtService = new GdtService() |
---|
33 | def sessionFactory |
---|
34 | |
---|
35 | /** |
---|
36 | * index closure |
---|
37 | */ |
---|
38 | def index = { |
---|
39 | // if( params.id ) |
---|
40 | // redirect( action: "simpleWizard", id: params.id ); |
---|
41 | // else |
---|
42 | // redirect( action: "simpleWizard" ); |
---|
43 | redirect action: 'simpleWizard', params: params |
---|
44 | } |
---|
45 | |
---|
46 | def simpleWizardFlow = { |
---|
47 | entry { |
---|
48 | action{ |
---|
49 | flow.study = getStudyFromRequest( params ) |
---|
50 | if (!flow.study) retrievalError() |
---|
51 | |
---|
52 | flow.inferDesign = params.inferDesign |
---|
53 | // Search for studies |
---|
54 | flow.studies = Study.giveWritableStudies( authenticationService.getLoggedInUser(), 100 ) |
---|
55 | } |
---|
56 | on("retrievalError").to "handleError" |
---|
57 | on("success").to "study" |
---|
58 | } |
---|
59 | |
---|
60 | study { |
---|
61 | on("next") { |
---|
62 | handleStudy( flow.study, params ) |
---|
63 | if( !validateObject( flow.study ) ) |
---|
64 | error() |
---|
65 | }.to "decisionState" |
---|
66 | on("open") { |
---|
67 | // Send the user to the URL of the simple wizard in order |
---|
68 | // to avoid code duplication for loading the study |
---|
69 | if( params.study ) { |
---|
70 | flow.openStudyId = params.study |
---|
71 | } else { |
---|
72 | flash.error = "No study selected"; |
---|
73 | return error(); |
---|
74 | } |
---|
75 | }.to "openStudy" |
---|
76 | on("refresh") { handleStudy( flow.study, params ); }.to "study" |
---|
77 | on( "save" ) { |
---|
78 | handleStudy( flow.study, params ); |
---|
79 | if( !validateObject( flow.study ) ) |
---|
80 | return error() |
---|
81 | |
---|
82 | if( flow.study.save( flush: true ) ) { |
---|
83 | flash.message = "Your study is succesfully saved."; |
---|
84 | } else { |
---|
85 | flash.error = "An error occurred while saving your study: <br />" |
---|
86 | flow.study.getErrors().each { flash.error += it.toString() + "<br />"} |
---|
87 | } |
---|
88 | success() |
---|
89 | }.to "study" |
---|
90 | } |
---|
91 | |
---|
92 | openStudy { |
---|
93 | redirect( action: "simpleWizard", id: flow.openStudyId ); |
---|
94 | } |
---|
95 | |
---|
96 | decisionState { |
---|
97 | action { |
---|
98 | // Create data in the flow |
---|
99 | flow.templates = [ |
---|
100 | 'Subject': Template.findAllByEntity( Subject.class ), |
---|
101 | 'Event': Template.findAllByEntity( Event.class ), |
---|
102 | 'SamplingEvent': Template.findAllByEntity( SamplingEvent.class ), |
---|
103 | 'Sample': Template.findAllByEntity( Sample.class ) |
---|
104 | ]; |
---|
105 | |
---|
106 | flow.encodedEntity = [ |
---|
107 | 'Subject': gdtService.encryptEntity( Subject.class.name ), |
---|
108 | 'Event': gdtService.encryptEntity( Event.class.name ), |
---|
109 | 'SamplingEvent': gdtService.encryptEntity( SamplingEvent.class.name ), |
---|
110 | 'Sample': gdtService.encryptEntity( Sample.class.name ) |
---|
111 | ] |
---|
112 | |
---|
113 | if (flow.study.samples) |
---|
114 | checkStudySimplicity(flow.study) ? existingSamples() : complexStudy() |
---|
115 | else |
---|
116 | samples() |
---|
117 | } |
---|
118 | on ("existingSamples").to "startExistingSamples" |
---|
119 | on ("complexStudy").to "complexStudy" |
---|
120 | on ("samples").to "samples" |
---|
121 | } |
---|
122 | |
---|
123 | startExistingSamples { |
---|
124 | action { |
---|
125 | def records = importerService.getRecords( flow.study ); |
---|
126 | flow.records = records |
---|
127 | flow.templateCombinations = records.templateCombination.unique() |
---|
128 | success(); |
---|
129 | } |
---|
130 | on( "success" ).to "existingSamples" |
---|
131 | } |
---|
132 | |
---|
133 | existingSamples { |
---|
134 | on("next") { |
---|
135 | handleExistingSamples( flow.study, params, flow ) ? success() : error() |
---|
136 | }.to "startAssays" |
---|
137 | on( "save" ) { |
---|
138 | if( !handleExistingSamples( flow.study, params, flow ) ) |
---|
139 | return error() |
---|
140 | |
---|
141 | if( flow.study.save( flush: true ) ) { |
---|
142 | flash.message = "Your study is succesfully saved."; |
---|
143 | } else { |
---|
144 | flash.error = "An error occurred while saving your study: <br />" |
---|
145 | flow.study.getErrors().each { flash.error += it.toString() + "<br />"} |
---|
146 | } |
---|
147 | success() |
---|
148 | }.to "existingSamples" |
---|
149 | on("previous").to "study" |
---|
150 | on("refresh") { |
---|
151 | if( !handleExistingSamples( flow.study, params, flow ) ) |
---|
152 | return error() |
---|
153 | |
---|
154 | // Refresh the templates, since the template editor has been opened. |
---|
155 | flow.templates = [ |
---|
156 | 'Subject': Template.findAllByEntity( Subject.class ).collect { it.refresh(); return it }, |
---|
157 | 'Event': Template.findAllByEntity( Event.class ).collect { it.refresh(); return it }, |
---|
158 | 'SamplingEvent': Template.findAllByEntity( SamplingEvent.class ).collect { it.refresh(); return it }, |
---|
159 | 'Sample': Template.findAllByEntity( Sample.class ).collect { it.refresh(); return it } |
---|
160 | ]; |
---|
161 | }.to "existingSamples" |
---|
162 | on("update") { |
---|
163 | handleExistingSamples( flow.study, params, flow ) ? success() : error() |
---|
164 | }.to "samples" |
---|
165 | |
---|
166 | on("skip").to "startAssays" |
---|
167 | } |
---|
168 | |
---|
169 | complexStudy { |
---|
170 | on("save").to "save" |
---|
171 | on("previous").to "study" |
---|
172 | } |
---|
173 | |
---|
174 | samples { |
---|
175 | on("next") { |
---|
176 | if( !handleSamples( flow.study, params, flow ) ) |
---|
177 | return error(); |
---|
178 | |
---|
179 | // Add domain fields for all entities |
---|
180 | flow.domainFields = [:] |
---|
181 | |
---|
182 | flow.templates.each { |
---|
183 | if( it.value ) { |
---|
184 | flow.domainFields[ it.key ] = it.value[0].entity.giveDomainFields(); |
---|
185 | } |
---|
186 | } |
---|
187 | |
---|
188 | //println flow.sampleForm.template |
---|
189 | }.to "columns" |
---|
190 | on("refresh") { |
---|
191 | def filename = params.get( 'importfile' ); |
---|
192 | |
---|
193 | handleSampleForm( flow.study, params, flow ) |
---|
194 | |
---|
195 | // Handle 'existing*' in front of the filename. This is put in front to make a distinction between |
---|
196 | // an already uploaded file test.txt (maybe moved to some other directory) and a newly uploaded file test.txt |
---|
197 | // still being in the temporary directory. |
---|
198 | // This import step doesn't have to make that distinction, since all files remain in the temporary directory. |
---|
199 | if( filename == 'existing*' ) |
---|
200 | filename = ''; |
---|
201 | else if( filename[0..8] == 'existing*' ) |
---|
202 | filename = filename[9..-1] |
---|
203 | |
---|
204 | flow.sampleForm.importFile = filename |
---|
205 | |
---|
206 | // Refresh the templates, since the template editor has been opened. |
---|
207 | flow.templates = [ |
---|
208 | 'Subject': Template.findAllByEntity( Subject.class ).collect { it.refresh(); return it }, |
---|
209 | 'Event': Template.findAllByEntity( Event.class ).collect { it.refresh(); return it }, |
---|
210 | 'SamplingEvent': Template.findAllByEntity( SamplingEvent.class ).collect { it.refresh(); return it }, |
---|
211 | 'Sample': Template.findAllByEntity( Sample.class ).collect { it.refresh(); return it } |
---|
212 | ]; |
---|
213 | }.to "samples" |
---|
214 | on("previous").to "returnFromSamples" |
---|
215 | on("study").to "study" |
---|
216 | on("skip").to "startAssays" |
---|
217 | } |
---|
218 | |
---|
219 | returnFromSamples { |
---|
220 | action { |
---|
221 | flow.study.samples ? existingSamples() : study(); |
---|
222 | } |
---|
223 | on( "existingSamples" ).to "startExistingSamples" |
---|
224 | on( "study" ).to "study" |
---|
225 | } |
---|
226 | |
---|
227 | columns { |
---|
228 | on( "next" ) { |
---|
229 | flow.editImportedData = params.get( 'editAfterwards' ) ? true : false; |
---|
230 | handleColumns( flow.study, params, flow ) ? success() : error() |
---|
231 | }.to "checkImportedEntities" |
---|
232 | on( "previous" ).to "samples" |
---|
233 | } |
---|
234 | |
---|
235 | checkImportedEntities { |
---|
236 | action { |
---|
237 | // Only continue to the next page if the information entered is correct |
---|
238 | if( flow.editImportedData || flow.imported.numInvalidEntities > 0 ) { |
---|
239 | missingFields(); |
---|
240 | } else { |
---|
241 | // The import of the excel file has finished. Now delete the excelfile |
---|
242 | if( flow.excel.filename ) |
---|
243 | fileService.delete( flow.excel.filename ); |
---|
244 | |
---|
245 | flow.sampleForm = null |
---|
246 | |
---|
247 | assays(); |
---|
248 | } |
---|
249 | } |
---|
250 | on( "missingFields" ).to "missingFields" |
---|
251 | on( "assays" ).to "startAssays" |
---|
252 | } |
---|
253 | |
---|
254 | missingFields { |
---|
255 | on( "refresh" ) { |
---|
256 | handleMissingFields( flow.study, params, flow ); |
---|
257 | success(); |
---|
258 | }.to "missingFields" |
---|
259 | on( "next" ) { |
---|
260 | if( !handleMissingFields( flow.study, params, flow ) ) { |
---|
261 | return error(); |
---|
262 | } |
---|
263 | |
---|
264 | // The import of the excel file has finished. Now delete the excelfile |
---|
265 | if( flow.excel.filename ) |
---|
266 | fileService.delete( flow.excel.filename ); |
---|
267 | |
---|
268 | flow.sampleForm = null |
---|
269 | |
---|
270 | success(); |
---|
271 | }.to "startAssays" |
---|
272 | on( "previous" ) { |
---|
273 | // The user goes back to the previous page, so the already imported entities |
---|
274 | // (of which some gave an error) should be removed again. |
---|
275 | // Add all samples |
---|
276 | flow.imported.data.each { record -> |
---|
277 | record.each { entity -> |
---|
278 | if( entity ) { |
---|
279 | switch( entity.class ) { |
---|
280 | case Sample: flow.study.removeFromSamples( entity ); break; |
---|
281 | case Subject: flow.study.removeFromSubjects( entity ); break; |
---|
282 | case Event: flow.study.removeFromEvents( entity ); break; |
---|
283 | case SamplingEvent: flow.study.removeFromSamplingEvents( entity ); break; |
---|
284 | } |
---|
285 | } |
---|
286 | } |
---|
287 | } |
---|
288 | |
---|
289 | success(); |
---|
290 | }.to "columns" |
---|
291 | } |
---|
292 | |
---|
293 | startAssays { |
---|
294 | action { |
---|
295 | if( !flow.assay ) { |
---|
296 | if( flow.study.assays ) { |
---|
297 | flow.assay = flow.study.assays[ 0 ] |
---|
298 | //println "Existing assay: " + flow.assay |
---|
299 | } else { |
---|
300 | flow.assay = new Assay( parent: flow.study ); |
---|
301 | } |
---|
302 | } |
---|
303 | success(); |
---|
304 | } |
---|
305 | on( "success" ).to "assays" |
---|
306 | } |
---|
307 | |
---|
308 | assays { |
---|
309 | on( "next" ) { |
---|
310 | handleAssays( flow.assay, params, flow ); |
---|
311 | if( flow.assay.template && !validateObject( flow.assay ) ) |
---|
312 | error(); |
---|
313 | |
---|
314 | }.to "overview" |
---|
315 | on( "skip" ) { |
---|
316 | // In case the user has created an assay before he clicked 'skip', it should only be kept if it |
---|
317 | // existed before this step |
---|
318 | if( flow.assay != null && !flow.assay.id ) { |
---|
319 | flow.remove( "assay" ) |
---|
320 | } |
---|
321 | |
---|
322 | }.to "overview" |
---|
323 | on( "previous" ).to "returnFromAssays" |
---|
324 | on( "save" ) { |
---|
325 | handleAssays( flow.assay, params, flow ); |
---|
326 | if( flow.assay.template && !validateObject( flow.assay ) ) |
---|
327 | error(); |
---|
328 | |
---|
329 | if( saveStudyToDatabase( flow ) ) { |
---|
330 | flash.message = "Your study is succesfully saved."; |
---|
331 | } else { |
---|
332 | flash.error = "An error occurred while saving your study: <br />" |
---|
333 | flow.study.getErrors().each { flash.error += it.toString() + "<br />"} |
---|
334 | return error(); |
---|
335 | } |
---|
336 | }.to "assays" |
---|
337 | on("refresh") { |
---|
338 | handleAssays( flow.assay, params, flow ); |
---|
339 | |
---|
340 | flow.assay?.template?.refresh() |
---|
341 | success() |
---|
342 | }.to "assays" |
---|
343 | } |
---|
344 | |
---|
345 | returnFromAssays { |
---|
346 | action { |
---|
347 | flow.study.samples ? existingSamples() : samples(); |
---|
348 | } |
---|
349 | on( "existingSamples" ).to "existingSamples" |
---|
350 | on( "samples" ).to "samples" |
---|
351 | } |
---|
352 | |
---|
353 | overview { |
---|
354 | on( "save" ).to "saveStudy" |
---|
355 | on( "previous" ).to "startAssays" |
---|
356 | } |
---|
357 | saveStudy { |
---|
358 | action { |
---|
359 | if( saveStudyToDatabase( flow ) ) { |
---|
360 | flash.message = "Your study is succesfully saved."; |
---|
361 | finish(); |
---|
362 | } else { |
---|
363 | flash.error = "An error occurred while saving your study: <br />" |
---|
364 | flow.study.getErrors().each { flash.error += it.toString() + "<br />"} |
---|
365 | overview(); |
---|
366 | } |
---|
367 | } |
---|
368 | on( "finish" ).to "finish" |
---|
369 | on( "overview" ).to "overview" |
---|
370 | } |
---|
371 | |
---|
372 | finish() |
---|
373 | |
---|
374 | handleError{ |
---|
375 | redirect action: "errorPage" |
---|
376 | } |
---|
377 | } |
---|
378 | |
---|
379 | /** |
---|
380 | * Saves the study with assay |
---|
381 | * |
---|
382 | * @param flow |
---|
383 | * @return true on success, false otherwise |
---|
384 | */ |
---|
385 | protected boolean saveStudyToDatabase( def flow ) { |
---|
386 | // Save the assay to the study |
---|
387 | if( flow.assay && flow.assay.template && !flow.study.assays?.contains( flow.assay ) ) { |
---|
388 | flow.study.addToAssays( flow.assay ); |
---|
389 | } |
---|
390 | |
---|
391 | if( flow.study.save( flush: true ) ) { |
---|
392 | // Make sure all samples are attached to all assays |
---|
393 | flow.study.assays.each { assay -> |
---|
394 | def l = []+ assay.samples; |
---|
395 | l.each { sample -> |
---|
396 | if( sample ) |
---|
397 | assay.removeFromSamples( sample ); |
---|
398 | } |
---|
399 | assay.samples?.clear(); |
---|
400 | |
---|
401 | flow.study.samples.each { sample -> |
---|
402 | assay.addToSamples( sample ) |
---|
403 | } |
---|
404 | } |
---|
405 | |
---|
406 | return true; |
---|
407 | |
---|
408 | } else { |
---|
409 | // Remove the assay from the study again, since it is still available |
---|
410 | // in the session |
---|
411 | if( flow.assay ) { |
---|
412 | flow.study.removeFromAssays( flow.assay ); |
---|
413 | flow.assay.parent = flow.study; |
---|
414 | } |
---|
415 | |
---|
416 | return false; |
---|
417 | } |
---|
418 | } |
---|
419 | |
---|
420 | /** |
---|
421 | * Retrieves the required study from the database or return an empty Study object if |
---|
422 | * no id is given |
---|
423 | * |
---|
424 | * @param params Request parameters with params.id being the ID of the study to be retrieved |
---|
425 | * @return A study from the database or an empty study if no id was given |
---|
426 | */ |
---|
427 | protected Study getStudyFromRequest( def params ) { |
---|
428 | int id = params.int( "id" ); |
---|
429 | |
---|
430 | if( !id ) { |
---|
431 | return new Study( title: "New study", owner: authenticationService.getLoggedInUser() ); |
---|
432 | } |
---|
433 | |
---|
434 | Study s = Study.get( id ); |
---|
435 | |
---|
436 | if( !s ) { |
---|
437 | flash.error = "No study found with given id"; |
---|
438 | return null; |
---|
439 | } |
---|
440 | if( !s.canWrite( authenticationService.getLoggedInUser() ) ) { |
---|
441 | flash.error = "No authorization to edit this study." |
---|
442 | return null; |
---|
443 | } |
---|
444 | |
---|
445 | return s |
---|
446 | } |
---|
447 | |
---|
448 | /** |
---|
449 | * Handles study input |
---|
450 | * @param study Study to update |
---|
451 | * @param params Request parameter map |
---|
452 | * @return True if everything went OK, false otherwise. An error message is put in flash.error |
---|
453 | */ |
---|
454 | def handleStudy( study, params ) { |
---|
455 | // did the study template change? |
---|
456 | if (params.get('template') && study.template?.name != params.get('template')) { |
---|
457 | // set the template |
---|
458 | study.template = Template.findByName(params.remove('template')) |
---|
459 | } |
---|
460 | |
---|
461 | // does the study have a template set? |
---|
462 | if (study.template && study.template instanceof Template) { |
---|
463 | // yes, iterate through template fields |
---|
464 | study.giveFields().each() { |
---|
465 | // and set their values |
---|
466 | study.setFieldValue(it.name, params.get(it.escapedName())) |
---|
467 | } |
---|
468 | } |
---|
469 | |
---|
470 | // handle public checkbox |
---|
471 | if (params.get("publicstudy")) { |
---|
472 | study.publicstudy = params.get("publicstudy") |
---|
473 | } |
---|
474 | |
---|
475 | // handle publications |
---|
476 | handleStudyPublications(study, params) |
---|
477 | |
---|
478 | // handle contacts |
---|
479 | handleStudyContacts(study, params) |
---|
480 | |
---|
481 | // handle users (readers, writers) |
---|
482 | handleStudyUsers(study, params, 'readers') |
---|
483 | handleStudyUsers(study, params, 'writers') |
---|
484 | |
---|
485 | return true |
---|
486 | } |
---|
487 | |
---|
488 | /** |
---|
489 | * Handles the editing of existing samples |
---|
490 | * @param study Study to update |
---|
491 | * @param params Request parameter map |
---|
492 | * @return True if everything went OK, false otherwise. An error message is put in flash.error |
---|
493 | */ |
---|
494 | def handleExistingSamples( study, params, flow ) { |
---|
495 | flash.validationErrors = []; |
---|
496 | |
---|
497 | def errors = false; |
---|
498 | |
---|
499 | // iterate through objects; set field values and validate the object |
---|
500 | def eventgroups = study.samples.parentEventGroup.findAll { it } |
---|
501 | def events; |
---|
502 | if( !eventgroups ) |
---|
503 | events = [] |
---|
504 | else |
---|
505 | events = eventgroups.events?.getAt(0); |
---|
506 | |
---|
507 | def objects = [ |
---|
508 | 'Subject': study.samples.parentSubject.findAll { it }, |
---|
509 | 'SamplingEvent': study.samples.parentEvent.findAll { it }, |
---|
510 | 'Event': events.flatten().findAll { it }, |
---|
511 | 'Sample': study.samples |
---|
512 | ]; |
---|
513 | objects.each { |
---|
514 | def type = it.key; |
---|
515 | def entities = it.value; |
---|
516 | |
---|
517 | entities.each { entity -> |
---|
518 | // iterate through entity fields |
---|
519 | entity.giveFields().each() { field -> |
---|
520 | def value = params.get( type.toLowerCase() + '_' + entity.getIdentifier() + '_' + field.escapedName()) |
---|
521 | |
---|
522 | // set field value; name cannot be set to an empty value |
---|
523 | if (field.name != 'name' || value) { |
---|
524 | log.info "setting "+field.name+" to "+value |
---|
525 | entity.setFieldValue(field.name, value) |
---|
526 | } |
---|
527 | } |
---|
528 | |
---|
529 | // has the template changed? |
---|
530 | def templateName = params.get(type.toLowerCase() + '_' + entity.getIdentifier() + '_template') |
---|
531 | if (templateName && entity.template?.name != templateName) { |
---|
532 | entity.template = Template.findByName(templateName) |
---|
533 | } |
---|
534 | |
---|
535 | // validate sample |
---|
536 | if (!entity.validate()) { |
---|
537 | errors = true; |
---|
538 | |
---|
539 | def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ] |
---|
540 | getHumanReadableErrors( entity ).each { |
---|
541 | flash.validationErrors << [ key: it.key, value: "(" + entityName + ") " + it.value ]; |
---|
542 | } |
---|
543 | } |
---|
544 | } |
---|
545 | } |
---|
546 | |
---|
547 | return !errors |
---|
548 | } |
---|
549 | |
---|
550 | /** |
---|
551 | * Handles the upload of sample data |
---|
552 | * @param study Study to update |
---|
553 | * @param params Request parameter map |
---|
554 | * @return True if everything went OK, false otherwise. An error message is put in flash.error |
---|
555 | */ |
---|
556 | def handleSamples( study, params, flow ) { |
---|
557 | def filename = params.get( 'importfile' ); |
---|
558 | |
---|
559 | // Handle 'existing*' in front of the filename. This is put in front to make a distinction between |
---|
560 | // an already uploaded file test.txt (maybe moved to some other directory) and a newly uploaded file test.txt |
---|
561 | // still being in the temporary directory. |
---|
562 | // This import step doesn't have to make that distinction, since all files remain in the temporary directory. |
---|
563 | if( filename == 'existing*' ) |
---|
564 | filename = ''; |
---|
565 | else if( filename[0..8] == 'existing*' ) |
---|
566 | filename = filename[9..-1] |
---|
567 | |
---|
568 | handleSampleForm( study, params, flow ); |
---|
569 | |
---|
570 | // Check whether the template exists |
---|
571 | if (!flow.sampleForm.template.Sample ){ |
---|
572 | log.error ".simple study wizard not all fields are filled in (sample template) " |
---|
573 | flash.error = "No template was chosen. Please choose a template for the samples you provided." |
---|
574 | return false |
---|
575 | } |
---|
576 | |
---|
577 | // These fields have been removed from the form, so will always contain |
---|
578 | // their default value. The code however remains like this for future use. |
---|
579 | int sheetIndex = (params.int( 'sheetindex' ) ?: 1 ) |
---|
580 | int dataMatrixStart = (params.int( 'datamatrix_start' ) ?: 2 ) |
---|
581 | int headerRow = (params.int( 'headerrow' ) ?: 1 ) |
---|
582 | |
---|
583 | flow.sampleForm.sheetIndex = sheetIndex; |
---|
584 | flow.sampleForm.dataMatrixStart = dataMatrixStart |
---|
585 | flow.sampleForm.headerRow = headerRow |
---|
586 | flow.sampleForm.importFile = filename |
---|
587 | |
---|
588 | def importedfile = fileService.get( filename ) |
---|
589 | def workbook |
---|
590 | if (importedfile.exists()) { |
---|
591 | try { |
---|
592 | workbook = importerService.getWorkbook(new FileInputStream(importedfile)) |
---|
593 | } catch (Exception e) { |
---|
594 | log.error ".simple study wizard could not load file: " + e |
---|
595 | flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples."; |
---|
596 | return false |
---|
597 | } |
---|
598 | } else { |
---|
599 | log.error ".simple study wizard no file given"; |
---|
600 | flash.error = "No file was given. Please provide an excel file for entering samples."; |
---|
601 | return false; |
---|
602 | } |
---|
603 | |
---|
604 | if( !workbook ) { |
---|
605 | log.error ".simple study wizard could not load file into a workbook" |
---|
606 | flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples."; |
---|
607 | return false |
---|
608 | } |
---|
609 | |
---|
610 | def selectedentities = [] |
---|
611 | |
---|
612 | if( !excelChecks( workbook, sheetIndex, headerRow, dataMatrixStart ) ) |
---|
613 | return false; |
---|
614 | |
---|
615 | // Get the header from the Excel file using the arguments given in the first step of the wizard |
---|
616 | def importerHeader; |
---|
617 | def importerDataMatrix; |
---|
618 | |
---|
619 | try { |
---|
620 | importerHeader = importerService.getHeader(workbook, |
---|
621 | sheetIndex - 1, // 0 == first sheet |
---|
622 | headerRow, // 1 == first row :s |
---|
623 | dataMatrixStart - 1, // 0 == first row |
---|
624 | Sample.class) |
---|
625 | |
---|
626 | importerDataMatrix = importerService.getDatamatrix( |
---|
627 | workbook, |
---|
628 | importerHeader, |
---|
629 | sheetIndex - 1, // 0 == first sheet |
---|
630 | dataMatrixStart - 1, // 0 == first row |
---|
631 | 5) |
---|
632 | } catch( Exception e ) { |
---|
633 | // An error occurred while reading the excel file. |
---|
634 | log.error ".simple study wizard error while reading the excel file"; |
---|
635 | e.printStackTrace(); |
---|
636 | |
---|
637 | // Show a message to the user |
---|
638 | flash.error = "An error occurred while reading the excel file. Have you provided the right sheet number and row numbers. Contact your system administrator if this problem persists."; |
---|
639 | return false; |
---|
640 | } |
---|
641 | |
---|
642 | // Match excel columns with template fields |
---|
643 | def fieldNames = []; |
---|
644 | flow.sampleForm.template.each { template -> |
---|
645 | if( template.value ) { |
---|
646 | def fields = template.value.entity.giveDomainFields() + template.value.getFields(); |
---|
647 | fields.each { field -> |
---|
648 | if( !field.entity ) |
---|
649 | field.entity = template.value.entity |
---|
650 | |
---|
651 | fieldNames << field |
---|
652 | } |
---|
653 | } |
---|
654 | } |
---|
655 | importerHeader.each { mc -> |
---|
656 | def bestfit = importerService.mostSimilar( mc.name, fieldNames, 0.8); |
---|
657 | if( bestfit ) { |
---|
658 | // Remove this fit from the list |
---|
659 | fieldNames.remove( bestfit ); |
---|
660 | |
---|
661 | mc.entityclass = bestfit.entity |
---|
662 | mc.property = bestfit.name |
---|
663 | } |
---|
664 | } |
---|
665 | |
---|
666 | // Save read excel data into session |
---|
667 | def dataMatrix = []; |
---|
668 | def df = new DataFormatter(); |
---|
669 | importerDataMatrix.each { |
---|
670 | dataMatrix << it.collect{ it ? df.formatCellValue(it) : "" } |
---|
671 | } |
---|
672 | |
---|
673 | flow.excel = [ |
---|
674 | filename: filename, |
---|
675 | sheetIndex: sheetIndex, |
---|
676 | dataMatrixStart: dataMatrixStart, |
---|
677 | headerRow: headerRow, |
---|
678 | data: [ |
---|
679 | header: importerHeader, |
---|
680 | dataMatrix: dataMatrix |
---|
681 | ] |
---|
682 | ] |
---|
683 | |
---|
684 | return true |
---|
685 | } |
---|
686 | |
---|
687 | /** |
---|
688 | * Copies data from the submitted sample form to the flow |
---|
689 | * @param study |
---|
690 | * @param params |
---|
691 | * @param flow |
---|
692 | * @return |
---|
693 | */ |
---|
694 | protected def handleSampleForm( study, params, flow ) { |
---|
695 | def sampleTemplateId = params.long( 'sample_template_id' ) |
---|
696 | def subjectTemplateId = params.long( 'subject_template_id' ) |
---|
697 | def eventTemplateId = params.long( 'event_template_id' ) |
---|
698 | def samplingEventTemplateId = params.long( 'samplingEvent_template_id' ) |
---|
699 | |
---|
700 | // Save form data in session |
---|
701 | if( !flow.sampleForm ) |
---|
702 | flow.sampleForm = [:] |
---|
703 | |
---|
704 | flow.sampleForm.templateId = [ |
---|
705 | 'Subject': subjectTemplateId, |
---|
706 | 'Event': eventTemplateId, |
---|
707 | 'SamplingEvent': samplingEventTemplateId, |
---|
708 | 'Sample': sampleTemplateId |
---|
709 | ]; |
---|
710 | flow.sampleForm.template = [ |
---|
711 | 'Subject': subjectTemplateId ? Template.get( subjectTemplateId ) : null, |
---|
712 | 'Event': eventTemplateId ? Template.get( eventTemplateId ) : null, |
---|
713 | 'SamplingEvent': samplingEventTemplateId ? Template.get( samplingEventTemplateId ) : null, |
---|
714 | 'Sample': sampleTemplateId ? Template.get( sampleTemplateId ) : null |
---|
715 | ]; |
---|
716 | } |
---|
717 | |
---|
718 | /** |
---|
719 | * Handles the matching of template fields with excel columns by the user |
---|
720 | * @param study Study to update |
---|
721 | * @param params Request parameter map |
---|
722 | * @return True if everything went OK, false otherwise. An error message is put in flash.error |
---|
723 | * The field session.simpleWizard.imported.numInvalidEntities reflects the number of |
---|
724 | * entities that have errors, and should be fixed before saving. The errors for those entities |
---|
725 | * are saved into session.simpleWizard.imported.errors |
---|
726 | */ |
---|
727 | def handleColumns( study, params, flow ) { |
---|
728 | // Find actual Template object from the chosen template name |
---|
729 | def templates = [:]; |
---|
730 | flow.sampleForm.templateId.each { |
---|
731 | templates[ it.key ] = it.value ? Template.get( it.value ) : null; |
---|
732 | } |
---|
733 | |
---|
734 | def headers = flow.excel.data.header; |
---|
735 | |
---|
736 | if( !params.matches ) { |
---|
737 | log.error( ".simple study wizard no column matches given" ); |
---|
738 | flash.error = "No column matches given"; |
---|
739 | return false; |
---|
740 | } |
---|
741 | |
---|
742 | // Retrieve the chosen matches from the request parameters and put them into |
---|
743 | // the headers-structure, for later reference |
---|
744 | params.matches.index.each { columnindex, value -> |
---|
745 | // Determine the entity and property by splitting it |
---|
746 | def parts = value.toString().tokenize( "||" ); |
---|
747 | |
---|
748 | def property |
---|
749 | def entityName |
---|
750 | if( parts.size() > 1 ) { |
---|
751 | property = parts[ 1 ]; |
---|
752 | entityName = "dbnp.studycapturing." + parts[ 0 ]; |
---|
753 | } else if( parts.size() == 1 ) { |
---|
754 | property = parts[ 0 ]; |
---|
755 | entityName = headers[columnindex.toInteger()].entityclass.getName(); |
---|
756 | } |
---|
757 | |
---|
758 | // Create an actual class instance of the selected entity with the selected template |
---|
759 | // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities |
---|
760 | def entityClass = Class.forName( entityName, true, this.getClass().getClassLoader()) |
---|
761 | def entityObj = entityClass.newInstance(template: templates[ entityName[entityName.lastIndexOf( '.' ) + 1..-1] ]) |
---|
762 | |
---|
763 | headers[ columnindex.toInteger() ].entityclass = entityClass |
---|
764 | |
---|
765 | // Store the selected property for this column into the column map for the ImporterService |
---|
766 | headers[columnindex.toInteger()].property = property |
---|
767 | |
---|
768 | // Look up the template field type of the target TemplateField and store it also in the map |
---|
769 | headers[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property) |
---|
770 | |
---|
771 | // Is a "Don't import" property assigned to the column? |
---|
772 | headers[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false |
---|
773 | |
---|
774 | //if it's an identifier set the mapping column true or false |
---|
775 | entityClass.giveDomainFields().each { |
---|
776 | headers[columnindex.toInteger()].identifier = ( it.preferredIdentifier && (it.name == property) ) |
---|
777 | } |
---|
778 | } |
---|
779 | |
---|
780 | // Import the workbook and store the table with entity records and store the failed cells |
---|
781 | //println "Importing samples for study " + study + " (" + study.id + ")"; |
---|
782 | |
---|
783 | def importedfile = fileService.get( flow.excel.filename ) |
---|
784 | def workbook |
---|
785 | if (importedfile.exists()) { |
---|
786 | try { |
---|
787 | workbook = importerService.getWorkbook(new FileInputStream(importedfile)) |
---|
788 | } catch (Exception e) { |
---|
789 | log.error ".simple study wizard could not load file: " + e |
---|
790 | flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples."; |
---|
791 | return false |
---|
792 | } |
---|
793 | } else { |
---|
794 | log.error ".simple study wizard no file given"; |
---|
795 | flash.error = "No file was given. Please provide an excel file for entering samples."; |
---|
796 | return false; |
---|
797 | } |
---|
798 | |
---|
799 | if( !workbook ) { |
---|
800 | log.error ".simple study wizard could not load file into a workbook" |
---|
801 | flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples."; |
---|
802 | return false |
---|
803 | } |
---|
804 | |
---|
805 | def imported = importerService.importOrUpdateDataBySampleIdentifier(templates, |
---|
806 | workbook, |
---|
807 | flow.excel.sheetIndex - 1, |
---|
808 | flow.excel.dataMatrixStart - 1, |
---|
809 | flow.excel.data.header, |
---|
810 | flow.study, |
---|
811 | true // Also create entities for which no data is imported but where templates were chosen |
---|
812 | ); |
---|
813 | |
---|
814 | def table = imported.table |
---|
815 | def failedcells = imported.failedCells |
---|
816 | |
---|
817 | flow.imported = [ |
---|
818 | data: table, |
---|
819 | failedCells: failedcells |
---|
820 | ]; |
---|
821 | |
---|
822 | // loop through all entities to validate them and add them to failedcells if an error occurs |
---|
823 | def numInvalidEntities = 0; |
---|
824 | def errors = []; |
---|
825 | |
---|
826 | if (flow.inferDesign) { |
---|
827 | |
---|
828 | println 'Entered infer design...' |
---|
829 | |
---|
830 | // find the indices of the classes of interest in the records |
---|
831 | def sampleIdx = table[0].findIndexOf{it.class.name == 'dbnp.studycapturing.Sample'} |
---|
832 | def samplingEventIdx = table[0].findIndexOf{it.class.name == 'dbnp.studycapturing.SamplingEvent'} |
---|
833 | def subjectIdx = table[0].findIndexOf{it.class.name == 'dbnp.studycapturing.Subject'} |
---|
834 | |
---|
835 | // Check for duplicate samples |
---|
836 | def samples = table.collect{it[sampleIdx]} |
---|
837 | |
---|
838 | def uniques = [] as Set |
---|
839 | def duplicates = [] as Set |
---|
840 | |
---|
841 | // this approach separates the unique from the duplicate entries |
---|
842 | samples*.name.each { |
---|
843 | uniques.add(it) || duplicates.add(it) |
---|
844 | } |
---|
845 | |
---|
846 | duplicates.each{ duplicateName -> |
---|
847 | samples.findAll{it.name == duplicateName}.each{ sample -> |
---|
848 | numInvalidEntities++ |
---|
849 | failedcells = addNonValidatingCells(failedcells, sample, flow) |
---|
850 | errors += "(Sample) duplicate name: $duplicateName" |
---|
851 | } |
---|
852 | } |
---|
853 | |
---|
854 | // A closure that returns a sub list of entities from a list that have |
---|
855 | // unique values of a property indicated by propertyName |
---|
856 | def uniqueEntitiesByProperty = { entities, propertyName -> |
---|
857 | |
---|
858 | entities*."$propertyName".unique().collect { uniquePropertyValue -> |
---|
859 | |
---|
860 | entities.find{ it."$propertyName" == uniquePropertyValue } |
---|
861 | |
---|
862 | } |
---|
863 | } |
---|
864 | |
---|
865 | def addToCollectionIfNonexistent = { parent, collectionName, entity, propertyName -> |
---|
866 | |
---|
867 | if (!parent[collectionName].find{it[propertyName] == entity[propertyName]}) |
---|
868 | parent."addTo${collectionName.capitalize()}" entity |
---|
869 | |
---|
870 | } |
---|
871 | |
---|
872 | // collect unique subjects and sampling events from table |
---|
873 | def uniqueSubjects = |
---|
874 | uniqueEntitiesByProperty(table.collect{it[subjectIdx]}, 'name') |
---|
875 | uniqueSubjects.each{ |
---|
876 | addToCollectionIfNonexistent study, 'subjects', it, 'name' |
---|
877 | it.species = Term.findByName('Homo sapiens') |
---|
878 | } |
---|
879 | |
---|
880 | def uniqueSamplingEvents = |
---|
881 | uniqueEntitiesByProperty(table.collect{it[samplingEventIdx]}, 'startTime') |
---|
882 | uniqueSamplingEvents.each{ |
---|
883 | it.setFieldValue( 'sampleTemplate', flow.sampleForm.template.Sample.name ) |
---|
884 | } |
---|
885 | |
---|
886 | // create an event group for each unique sampling event (not much of a group, is it ...) |
---|
887 | def eventGroups = uniqueSamplingEvents.collect{ |
---|
888 | |
---|
889 | def eventGroupName = "Sampling_${it.sampleTemplate.name}_${new RelTime(it.startTime).toString()}" |
---|
890 | |
---|
891 | def eventGroup = study.eventGroups.find{it.name == eventGroupName} ?: //EventGroup.findByParentAndName(study, eventGroupName) ?: |
---|
892 | new EventGroup(name: eventGroupName) |
---|
893 | |
---|
894 | eventGroup.addToSamplingEvents it |
---|
895 | |
---|
896 | if (!study.eventGroups.find{it == eventGroup}) |
---|
897 | study.addToEventGroups eventGroup |
---|
898 | |
---|
899 | if (!it.parent) study.addToSamplingEvents it |
---|
900 | |
---|
901 | println eventGroup.name |
---|
902 | |
---|
903 | eventGroup |
---|
904 | |
---|
905 | } |
---|
906 | |
---|
907 | table.each{ record -> |
---|
908 | |
---|
909 | Sample sample = record[sampleIdx] |
---|
910 | |
---|
911 | // gather all sample related entities |
---|
912 | def correspondingSamplingEvent = uniqueSamplingEvents.find {it.startTime == record[samplingEventIdx].startTime} |
---|
913 | def correspondingSubject = uniqueSubjects.find{it.name == record[subjectIdx].name} |
---|
914 | def correspondingEventGroup = eventGroups.find{correspondingSamplingEvent in it.samplingEvents} |
---|
915 | |
---|
916 | addToCollectionIfNonexistent correspondingSamplingEvent, 'samples', sample, 'name' |
---|
917 | |
---|
918 | sample.parentSubject = correspondingSubject |
---|
919 | |
---|
920 | correspondingEventGroup.addToSamplingEvents correspondingSamplingEvent |
---|
921 | |
---|
922 | if (!correspondingEventGroup.subjects.find{it.name == correspondingSubject.name}) |
---|
923 | correspondingEventGroup.addToSubjects correspondingSubject |
---|
924 | |
---|
925 | addToCollectionIfNonexistent study, 'samples', sample, 'name' |
---|
926 | |
---|
927 | } |
---|
928 | |
---|
929 | } else { |
---|
930 | // Add all samples |
---|
931 | table.each { record -> |
---|
932 | record.each { entity -> |
---|
933 | if( entity ) { |
---|
934 | // Determine entity class and add a parent. Add the entity to the study |
---|
935 | def preferredIdentifier = importerService.givePreferredIdentifier( entity.class ); |
---|
936 | def equalClosure = { it.getIdentifier() == entity.getIdentifier() } |
---|
937 | def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ] |
---|
938 | |
---|
939 | entity.parent = study |
---|
940 | |
---|
941 | switch( entity.class ) { |
---|
942 | case Sample: |
---|
943 | if( !study.samples?.find( equalClosure ) ) { |
---|
944 | study.addToSamples( entity ); |
---|
945 | } |
---|
946 | |
---|
947 | // If an eventgroup is created, add it to the study |
---|
948 | // The eventgroup must have a unique name, but the user shouldn't be bothered with it |
---|
949 | // Add 'group ' + samplename and it that is not unique, add a number to it |
---|
950 | if( entity.parentEventGroup ) { |
---|
951 | study.addToEventGroups( entity.parentEventGroup ) |
---|
952 | |
---|
953 | entity.parentEventGroup.name = "Group " + entity.name |
---|
954 | while( !entity.parentEventGroup.validate() ) { |
---|
955 | //entity.parentEventGroup.getErrors().each { println it } |
---|
956 | entity.parentEventGroup.name += "" + Math.floor( Math.random() * 100 ) |
---|
957 | } |
---|
958 | } |
---|
959 | |
---|
960 | break; |
---|
961 | case Subject: |
---|
962 | if( !study.samples?.find( equalClosure ) ) { |
---|
963 | |
---|
964 | if( preferredIdentifier ) { |
---|
965 | // Subjects without a name should just be called 'subject' |
---|
966 | if( !entity.getFieldValue( preferredIdentifier.name ) ) |
---|
967 | entity.setFieldValue( preferredIdentifier.name, "Subject" ); |
---|
968 | |
---|
969 | // Subjects should have unique names; if the user has entered the same name multiple times, |
---|
970 | // the subject will be renamed |
---|
971 | def baseName = entity.getFieldValue( preferredIdentifier.name ) |
---|
972 | def counter = 2; |
---|
973 | |
---|
974 | while( study.subjects?.find { it.getFieldValue( preferredIdentifier.name ) == entity.getFieldValue( preferredIdentifier.name ) } ) { |
---|
975 | entity.setFieldValue( preferredIdentifier.name, baseName + " (" + counter++ + ")" ) |
---|
976 | } |
---|
977 | } |
---|
978 | |
---|
979 | study.addToSubjects( entity ); |
---|
980 | |
---|
981 | } |
---|
982 | |
---|
983 | break; |
---|
984 | case Event: |
---|
985 | if( !study.events?.find( equalClosure ) ) { |
---|
986 | study.addToEvents( entity ); |
---|
987 | } |
---|
988 | break; |
---|
989 | case SamplingEvent: |
---|
990 | // Sampling events have a 'sampleTemplate' value, which should be filled by the |
---|
991 | // template that is chosen for samples. |
---|
992 | if( !entity.getFieldValue( 'sampleTemplate' ) ) { |
---|
993 | entity.setFieldValue( 'sampleTemplate', flow.sampleForm.template.Sample.name ) |
---|
994 | } |
---|
995 | |
---|
996 | if( !study.samplingEvents?.find( equalClosure ) ) { |
---|
997 | study.addToSamplingEvents( entity ); |
---|
998 | } |
---|
999 | break; |
---|
1000 | } |
---|
1001 | |
---|
1002 | if (!entity.validate()) { |
---|
1003 | numInvalidEntities++; |
---|
1004 | |
---|
1005 | // Add this field to the list of failed cells, in order to give the user feedback |
---|
1006 | failedcells = addNonValidatingCells( failedcells, entity, flow ) |
---|
1007 | |
---|
1008 | // Also create a full list of errors |
---|
1009 | def currentErrors = getHumanReadableErrors( entity ) |
---|
1010 | if( currentErrors ) { |
---|
1011 | currentErrors.each { |
---|
1012 | errors += "(" + entityName + ") " + it.value; |
---|
1013 | } |
---|
1014 | } |
---|
1015 | } |
---|
1016 | } |
---|
1017 | } |
---|
1018 | } |
---|
1019 | } |
---|
1020 | |
---|
1021 | flow.imported.numInvalidEntities = numInvalidEntities + failedcells?.size(); |
---|
1022 | flow.imported.errors = errors; |
---|
1023 | |
---|
1024 | return true |
---|
1025 | } |
---|
1026 | |
---|
1027 | /** |
---|
1028 | * Handles the update of the edited fields by the user |
---|
1029 | * @param study Study to update |
---|
1030 | * @param params Request parameter map |
---|
1031 | * @return True if everything went OK, false otherwise. An error message is put in flash.error. |
---|
1032 | * The field session.simpleWizard.imported.numInvalidEntities reflects the number of |
---|
1033 | * entities that still have errors, and should be fixed before saving. The errors for those entities |
---|
1034 | * are saved into session.simpleWizard.imported.errors |
---|
1035 | */ |
---|
1036 | def handleMissingFields( study, params, flow ) { |
---|
1037 | def numInvalidEntities = 0; |
---|
1038 | def errors = []; |
---|
1039 | |
---|
1040 | // Check which fields failed previously |
---|
1041 | def failedCells = flow.imported.failedCells |
---|
1042 | def newFailedCells = []; |
---|
1043 | |
---|
1044 | flow.imported.data.each { table -> |
---|
1045 | table.each { entity -> |
---|
1046 | def invalidFields = 0 |
---|
1047 | def failed = new ImportRecord(); |
---|
1048 | def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ] |
---|
1049 | |
---|
1050 | |
---|
1051 | // Set the fields for this entity by retrieving values from the params |
---|
1052 | entity.giveFields().each { field -> |
---|
1053 | def fieldName = importerService.getFieldNameInTableEditor( entity, field ); |
---|
1054 | |
---|
1055 | if( params[ fieldName ] == "#invalidterm" ) { |
---|
1056 | // If the value '#invalidterm' is chosen, the user hasn't fixed anything, so this field is still incorrect |
---|
1057 | invalidFields++; |
---|
1058 | |
---|
1059 | // store the mapping column and value which failed |
---|
1060 | def identifier = entityName.toLowerCase() + "_" + entity.getIdentifier() + "_" + fieldName |
---|
1061 | def mcInstance = new MappingColumn() |
---|
1062 | failed.addToImportcells(new ImportCell(mappingcolumn: mcInstance, value: params[ fieldName ], entityidentifier: identifier)) |
---|
1063 | } else { |
---|
1064 | if( field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM || field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST ) { |
---|
1065 | // If this field is an ontologyterm field or a stringlist field, the value has changed, so remove the field from |
---|
1066 | // the failedCells list |
---|
1067 | importerService.removeFailedCell( failedCells, entity, field ) |
---|
1068 | } |
---|
1069 | |
---|
1070 | // Update the field, regardless of the type of field |
---|
1071 | entity.setFieldValue(field.name, params[ fieldName ] ) |
---|
1072 | } |
---|
1073 | } |
---|
1074 | |
---|
1075 | // Try to validate the entity now all fields have been set. If it fails, return an error |
---|
1076 | if (!entity.validate() || invalidFields) { |
---|
1077 | numInvalidEntities++; |
---|
1078 | |
---|
1079 | // Add this field to the list of failed cells, in order to give the user feedback |
---|
1080 | failedCells = addNonValidatingCellsToImportRecord( failed, entity, flow ) |
---|
1081 | |
---|
1082 | // Also create a full list of errors |
---|
1083 | def currentErrors = getHumanReadableErrors( entity ) |
---|
1084 | if( currentErrors ) { |
---|
1085 | currentErrors.each { |
---|
1086 | errors += "(" + entityName + ") " + it.value; |
---|
1087 | } |
---|
1088 | } |
---|
1089 | |
---|
1090 | newFailedCells << failed; |
---|
1091 | } else { |
---|
1092 | importerService.removeFailedCell( failedCells, entity ) |
---|
1093 | } |
---|
1094 | } // end of record |
---|
1095 | } // end of table |
---|
1096 | |
---|
1097 | flow.imported.failedCells = newFailedCells |
---|
1098 | flow.imported.numInvalidEntities = numInvalidEntities; |
---|
1099 | flow.imported.errors = errors; |
---|
1100 | |
---|
1101 | return numInvalidEntities == 0 |
---|
1102 | } |
---|
1103 | |
---|
1104 | /** |
---|
1105 | * Handles assay input |
---|
1106 | * @param study Study to update |
---|
1107 | * @param params Request parameter map |
---|
1108 | * @return True if everything went OK, false otherwise. An error message is put in flash.error |
---|
1109 | */ |
---|
1110 | def handleAssays( assay, params, flow ) { |
---|
1111 | // did the study template change? |
---|
1112 | if (params.get('template') && assay.template?.name != params.get('template')) { |
---|
1113 | // set the template |
---|
1114 | assay.template = Template.findByName(params.remove('template')) |
---|
1115 | } |
---|
1116 | |
---|
1117 | // does the study have a template set? |
---|
1118 | if (assay.template && assay.template instanceof Template) { |
---|
1119 | // yes, iterate through template fields |
---|
1120 | assay.giveFields().each() { |
---|
1121 | // and set their values |
---|
1122 | assay.setFieldValue(it.name, params.get(it.escapedName())) |
---|
1123 | } |
---|
1124 | } |
---|
1125 | |
---|
1126 | return true |
---|
1127 | } |
---|
1128 | |
---|
1129 | |
---|
1130 | /** |
---|
1131 | * Checks whether the given study is simple enough to be edited using this controller. |
---|
1132 | * |
---|
1133 | * The study is simple enough if the samples, subjects, events and samplingEvents can be |
---|
1134 | * edited as a flat table. That is: |
---|
1135 | * - Every subject belongs to 0 or 1 eventgroup |
---|
1136 | * - Every eventgroup belongs to 0 or 1 sample |
---|
1137 | * - Every eventgroup has 0 or 1 subjects, 0 or 1 event and 0 or 1 samplingEvents |
---|
1138 | * - If a sample belongs to an eventgroup: |
---|
1139 | * - If that eventgroup has a samplingEvent, that same samplingEvent must also be |
---|
1140 | * the sampling event that generated this sample |
---|
1141 | * - If that eventgroup has a subject, that same subject must also be the subject |
---|
1142 | * from whom the sample was taken |
---|
1143 | * |
---|
1144 | * @param study Study to check |
---|
1145 | * @return True if the study can be edited by this controller, false otherwise |
---|
1146 | */ |
---|
1147 | def checkStudySimplicity( study ) { |
---|
1148 | def simplicity = true; |
---|
1149 | |
---|
1150 | if( !study ) |
---|
1151 | return false |
---|
1152 | |
---|
1153 | if( study.eventGroups ) { |
---|
1154 | study.eventGroups.each { eventGroup -> |
---|
1155 | // Check for simplicity of eventgroups: only 0 or 1 subject, 0 or 1 event and 0 or 1 samplingEvent |
---|
1156 | if( eventGroup.subjects?.size() > 1 || eventGroup.events?.size() > 1 || eventGroup.samplingEvents?.size() > 1 ) { |
---|
1157 | flash.message = "One or more eventgroups contain multiple subjects or events." |
---|
1158 | simplicity = false; |
---|
1159 | } |
---|
1160 | |
---|
1161 | // Check whether this eventgroup only belongs to (max) 1 sample |
---|
1162 | def numSamples = 0; |
---|
1163 | study.samples.each { sample -> |
---|
1164 | // If no id is given for the eventGroup, it has been entered in this wizard, but |
---|
1165 | // not yet saved. In that case, it is always OK |
---|
1166 | if( eventGroup.id && sample.parentEventGroup?.id == eventGroup.id ) |
---|
1167 | numSamples++; |
---|
1168 | } |
---|
1169 | |
---|
1170 | if( numSamples > 1 ) { |
---|
1171 | flash.message = "One or more eventgroups belong to multiple samples." |
---|
1172 | simplicity = false; |
---|
1173 | } |
---|
1174 | } |
---|
1175 | |
---|
1176 | if( !simplicity ) return false; |
---|
1177 | |
---|
1178 | // Check whether subject only belong to zero or one event group |
---|
1179 | if( study.subjects ) { |
---|
1180 | study.subjects.each { subject -> |
---|
1181 | def numEventGroups = 0 |
---|
1182 | study.eventGroups.each { eventGroup -> |
---|
1183 | // If no id is given for the subject, it has been entered in this wizard, but |
---|
1184 | // not yet saved. In that case, it is always OK |
---|
1185 | if( subject.id && eventGroup.subjects && eventGroup.subjects.toList()[0]?.id == subject.id ) |
---|
1186 | numEventGroups++ |
---|
1187 | } |
---|
1188 | |
---|
1189 | if( numEventGroups > 1 ) { |
---|
1190 | flash.message = "One or more subjects belong to multiple eventgroups." |
---|
1191 | simplicity = false; |
---|
1192 | } |
---|
1193 | } |
---|
1194 | } |
---|
1195 | |
---|
1196 | if( !simplicity ) return false; |
---|
1197 | |
---|
1198 | // Check whether the samples that belong to an eventgroup have the right parentObjects |
---|
1199 | study.samples.each { sample -> |
---|
1200 | if( sample.parentEventGroup ) { |
---|
1201 | // If no id is given for the subject, it has been entered in this wizard, but |
---|
1202 | // not yet saved. In that case, it is always OK |
---|
1203 | if( sample.parentSubject && sample.parentSubject.id) { |
---|
1204 | if( !sample.parentEventGroup.subjects || sample.parentEventGroup.subjects.toList()[0]?.id != sample.parentSubject.id ) { |
---|
1205 | flash.message = "The structure of the eventgroups of one or more samples is too complex" |
---|
1206 | simplicity = false; |
---|
1207 | } |
---|
1208 | } |
---|
1209 | |
---|
1210 | // If no id is given for the sampling event, it has been entered in this wizard, but |
---|
1211 | // not yet saved. In that case, it is always OK |
---|
1212 | if( sample.parentEvent && sample.parentEvent.id) { |
---|
1213 | if( !sample.parentEventGroup.samplingEvents || sample.parentEventGroup.samplingEvents.toList()[0]?.id != sample.parentEvent.id ) { |
---|
1214 | flash.message = "The structure of the eventgroups of one or more samples is too complex" |
---|
1215 | simplicity = false; |
---|
1216 | } |
---|
1217 | } |
---|
1218 | } |
---|
1219 | } |
---|
1220 | |
---|
1221 | if( !simplicity ) return false; |
---|
1222 | } |
---|
1223 | |
---|
1224 | return simplicity; |
---|
1225 | } |
---|
1226 | |
---|
1227 | |
---|
1228 | /** |
---|
1229 | * Adds all fields of this entity that have given an error when validating to the failedcells list |
---|
1230 | * @param failedcells Current list of ImportRecords |
---|
1231 | * @param entity Entity to check. The entity must have been validated before |
---|
1232 | * @return Updated list of ImportRecords |
---|
1233 | */ |
---|
1234 | protected def addNonValidatingCells( failedcells, entity, flow ) { |
---|
1235 | // Add this entity and the fields with an error to the failedCells list |
---|
1236 | ImportRecord failedRecord = addNonValidatingCellsToImportRecord( new ImportRecord(), entity, flow ); |
---|
1237 | |
---|
1238 | failedcells.add( failedRecord ); |
---|
1239 | |
---|
1240 | return failedcells |
---|
1241 | } |
---|
1242 | |
---|
1243 | /** |
---|
1244 | * Adds all fields of this entity that have given an error when validating to the failedcells list |
---|
1245 | * @param failedcells Current list of ImportRecords |
---|
1246 | * @param entity Entity to check. The entity must have been validated before |
---|
1247 | * @return Updated list of ImportRecords |
---|
1248 | */ |
---|
1249 | protected def addNonValidatingCellsToImportRecord( failedRecord, entity, flow ) { |
---|
1250 | entity.getErrors().getFieldErrors().each { error -> |
---|
1251 | String field = error.getField(); |
---|
1252 | |
---|
1253 | def mc = importerService.findMappingColumn( flow.excel.data.header, field ); |
---|
1254 | def mcInstance = new MappingColumn( name: field, entityClass: Sample.class, index: -1, property: field.toLowerCase(), templateFieldType: entity.giveFieldType( field ) ); |
---|
1255 | |
---|
1256 | // Create a clone of the mapping column |
---|
1257 | if( mc ) { |
---|
1258 | mcInstance.properties = mc.properties |
---|
1259 | } |
---|
1260 | |
---|
1261 | failedRecord.addToImportcells( new ImportCell(mappingcolumn: mcInstance, value: error.getRejectedValue(), entityidentifier: importerService.getFieldNameInTableEditor( entity, field ) ) ) |
---|
1262 | } |
---|
1263 | |
---|
1264 | return failedRecord |
---|
1265 | } |
---|
1266 | |
---|
1267 | |
---|
1268 | /** |
---|
1269 | * Checks an excel workbook whether the given sheetindex and rownumbers are correct |
---|
1270 | * @param workbook Excel workbook to read |
---|
1271 | * @param sheetIndex 1-based sheet index for the sheet to read (1=first sheet) |
---|
1272 | * @param headerRow 1-based row number for the header row (1=first row) |
---|
1273 | * @param dataMatrixStart 1-based row number for the first data row (1=first row) |
---|
1274 | * @return True if the sheet index and row numbers are correct. |
---|
1275 | */ |
---|
1276 | protected boolean excelChecks( def workbook, int sheetIndex, int headerRow, int dataMatrixStart ) { |
---|
1277 | // Perform some basic checks on the excel file. These checks should be performed by the importerservice |
---|
1278 | // in a perfect scenario. |
---|
1279 | if( sheetIndex > workbook.getNumberOfSheets() ) { |
---|
1280 | log.error ".simple study wizard Sheet index is too high: " + sheetIndex + " / " + workbook.getNumberOfSheets(); |
---|
1281 | flash.error = "Your excel sheet contains too few excel sheets. The provided excel sheet has only " + workbook.getNumberOfSheets() + " sheet(s)."; |
---|
1282 | return false |
---|
1283 | } |
---|
1284 | |
---|
1285 | def sheet = workbook.getSheetAt(sheetIndex - 1); |
---|
1286 | def firstRowNum = sheet.getFirstRowNum(); |
---|
1287 | def lastRowNum = sheet.getLastRowNum(); |
---|
1288 | def numRows = lastRowNum - firstRowNum + 1; |
---|
1289 | |
---|
1290 | if( headerRow > numRows ) { |
---|
1291 | log.error ".simple study wizard Header row number is incorrect: " + headerRow + " / " + numRows; |
---|
1292 | flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below"; |
---|
1293 | return false |
---|
1294 | } |
---|
1295 | |
---|
1296 | if( dataMatrixStart > numRows ) { |
---|
1297 | log.error ".simple study wizard Data row number is incorrect: " + dataMatrixStart + " / " + numRows; |
---|
1298 | flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below"; |
---|
1299 | return false |
---|
1300 | } |
---|
1301 | |
---|
1302 | return true; |
---|
1303 | } |
---|
1304 | |
---|
1305 | /** |
---|
1306 | * Validates an object and puts human readable errors in validationErrors variable |
---|
1307 | * @param entity Entity to validate |
---|
1308 | * @return True iff the entity validates, false otherwise |
---|
1309 | */ |
---|
1310 | protected boolean validateObject( def entity ) { |
---|
1311 | if( !entity.validate() ) { |
---|
1312 | flash.validationErrors = getHumanReadableErrors( entity ) |
---|
1313 | return false; |
---|
1314 | } |
---|
1315 | return true; |
---|
1316 | } |
---|
1317 | |
---|
1318 | /** |
---|
1319 | * transform domain class validation errors into a human readable |
---|
1320 | * linked hash map |
---|
1321 | * @param object validated domain class |
---|
1322 | * @return object linkedHashMap |
---|
1323 | */ |
---|
1324 | def getHumanReadableErrors(object) { |
---|
1325 | def errors = [:] |
---|
1326 | object.errors.getAllErrors().each() { error -> |
---|
1327 | // error.codes.each() { code -> println code } |
---|
1328 | |
---|
1329 | // generally speaking g.message(...) should work, |
---|
1330 | // however it fails in some steps of the wizard |
---|
1331 | // (add event, add assay, etc) so g is not always |
---|
1332 | // availably. Using our own instance of the |
---|
1333 | // validationTagLib instead so it is always |
---|
1334 | // available to us |
---|
1335 | errors[error.getArguments()[0]] = validationTagLib.message(error: error) |
---|
1336 | } |
---|
1337 | |
---|
1338 | return errors |
---|
1339 | } |
---|
1340 | } |
---|