<commit_before>package dbnp.studycapturing

import dbnp.data.*

// Grails convertors is imported in order to create JSON objects
import grails.converters.*
import grails.plugins.springsecurity.Secured
import dbnp.authentication.AuthenticationService
import dbnp.authentication.SecUser


/**
 * Wizard Controler
 *
 * The wizard controller handles the handeling of pages and data flow
 * through the study capturing wizard.
 *
 * This controller is only accessible by logged in users.
 *
 * @author <NAME>
 * @since 20100107
 * @package studycapturing
 *
 * Revision information:
 * $Rev$
 * $Author$
 * $Date$
 */
@Secured(['IS_AUTHENTICATED_REMEMBERED'])
class WizardController {
	def authenticationService

	/**
	 * index method, redirect to the webflow
	 * @void
	 */
	def index = {
		def jump = [:]

		// allow quickjumps to:
		//	edit a study	: /wizard?jump=edit&id=1
		//	create a study	: /wizard?jump=create
		if (params.get('jump')) {
			switch (params.get('jump')) {
				case 'create':
					jump = [
					    action: 'create'
					]
					break
				case 'edit':
					jump = [
					    action	: 'edit',
						id		: params.get('id')
					]
					break
				default:
					break
			}
		}

		// store in session
		session.jump = jump

		/**
		 * Do you believe it in your head?
		 * I can go with the flow
		 * Don't say it doesn't matter (with the flow) matter anymore
		 * I can go with the flow (I can go)
		 * Do you believe it in your head?
		 */
		redirect(action: 'pages')
	}

	/**
	 * WebFlow definition
	 * @see http://grails.org/WebFlow
	 * @void
	 */
	def pagesFlow = {
		// start the flow
		onStart {
			log.info ".entering onStart"

			// define flow variables
			flow.page = 0
			flow.pages = [
				//[title: 'Templates'],			// templates
				[title: 'Start'],				// load or create a study
				[title: 'Subjects'],			// subjects
				[title: 'Events'],				// events and event grouping
				//[title: 'Event Groups'],		// groups
				[title: 'Samples'],				// samples
				[title: 'Assays'],				// assays
				//[title: 'Assay Groups'],		// assays
				[title: 'Confirmation'],		// confirmation page
				[title: 'Done']					// finish page
			]
			flow.jump = session.jump

			// since this controller was refactored it's technically
			// safe to enable quicksave throughout the application.
			// However, we must keep an eye on the quality of the
			// studies as this change makes it easier to create dummy
			// studies which will create garbage in out database.
			flow.quickSave = true
			success()
		}

		// render the main wizard page which immediately
		// triggers the 'next' action (hence, the main
		// page dynamically renders the study template
		// and makes the flow jump to the study logic)
		mainPage {
			render(view: "/wizard/index")
			onRender {
				log.info ".entering mainPage"

				flow.page = 1
				success()
			}
			on("next").to "handleJump"
		}

		// handle the jump parameter
		//
		// I came to get down [2x]
		// So get out your seats and jump around
		// Jump around [3x]
		// Jump up Jump up and get down
		// Jump [18x]
		handleJump {
			action {
				log.info ".entering handleJump"

				if (flow.jump && flow.jump.action == 'edit') {
					if (flow.jump.id) {
						// load study
						if (this.loadStudy(flow, flash, [studyid:flow.jump.id],authenticationService.getLoggedInUser())) {
							toStudyPage()
						} else {
							toStartPage()
						}
					} else {
						toModifyPage()
					}
				} else if (flow.jump && flow.jump.action == 'create') {
					toStudyPage()
				} else {
					toStartPage()
				}
			}
			on("toStartPage").to "start"
			on("toStudyPage").to "study"
			on("toModifyPage").to "modify"
		}

		// create or modify a study
		start {
			render(view: "_start")
			onRender {
				log.info ".entering start"

				flow.page = 1
				success()
			}
			on("next") {
				// clean the flow scope
				flow.remove('study')

				// set 'quicksave' variable to false
				flow.quickSave = false
			}.to "study"
			on("modify").to "modify"
			on("import").to "redirectToImport"
		}

		// redirect to the import wizard
		redirectToImport {
			render(view: "_redirect")
			onRender {
				log.info ".entering redirectToImport"
				flash.uri = "/importer/index"
			}
			on("next").to "start"
		}

		// load a study to modify
		modify {
			render(view: "_modify")
			onRender {
				log.info ".entering modify"

				flow.page = 1
				flash.cancel = true
				success()
			}
			on("cancel") {
				flow.remove('study')

				success()
			}.to "start"
			on("next") {
				// load study
				if (this.loadStudy(flow, flash, params, authenticationService.getLoggedInUser())) {
					success()
				} else {
					error()
				}
			}.to "study"
		}

		// render and handle the study page
		study {
			render(view: "_study")
			onRender {
				log.info ".entering study"

				flow.page = 1
				success()
			}
			on("refresh") {
				// handle form data
				studyPage(flow, flash, params)

				// force refresh of the template
				if (flow.study.template && flow.study.template instanceof Template) {
					flow.study.template.refresh()
				}

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "study"
            on("switchTemplate") {
				// handle form data
				studyPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "study"
			on("previous") {
				// handle form data
				studyPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "start"
			on("next") {
				studyPage(flow, flash, params) ? success() : error()
			}.to "subjects"
			on("quickSave") {
				studyPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// render and handle subjects page
		subjects {
			render(view: "_subjects")
			onRender {
				log.info ".entering subjects"

				flow.page = 2

				if (!flash.values || !flash.values.addNumber) flash.values = [addNumber:1]

				success()
			}
			on("refresh") {
				// remember the params in the flash scope
				flash.values = params

				// refresh templates
				if (flow.study.subjects) {
					flow.study.giveSubjectTemplates().each() {
						it.refresh()
					}
				}

				success()
			}.to "subjects"
			on("add") {
				// handle form data
				addSubjects(flow, flash, params) ? success() : error()
			}.to "subjects"
			on("delete") {
				// handle form data
				subjectPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// remove subject
				def subjectToRemove = flow.study.subjects.find { it.identifier == (params.get('do') as int) }
				if (subjectToRemove) {
					flow.study.deleteSubject( subjectToRemove )
				}
			}.to "subjects"
			on("previous") {
				// handle form data
				subjectPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "study"
			on("next") {
				// handle form data
				subjectPage(flow, flash, params) ? success() : error()
			}.to "events"
			on("quickSave") {
				// handle form data
				subjectPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// render events page
		events {
			render(view: "_events")
			onRender {
				log.info ".entering events"

				flow.page = 3

				// add initial eventGroup to study
				if (!flow.study.eventGroups?.size()) {
					flow.study.addToEventGroups(
						new EventGroup(name: 'Group 1')
					)
				}

				success()
			}
			on("clear") {
				// remove all events
				(flow.study.events + flow.study.samplingEvents).each() { event ->
					if (event instanceof SamplingEvent) {
						flow.study.deleteSamplingEvent( event )
					} else {
						flow.study.deleteEvent( event )
					}
				}

				success()
			}.to "events"
			on("switchTemplate") {
				// handle form data
				eventPage(flow, flash, params)

				// get template
				def type	= params.get('eventType')
				def template= Template.findByName( params.get( type + 'Template' ) )

				// change template and/or instance?
				if (!flow.event || (flow.event instanceof Event && type == "sample") || (flow.event instanceof SamplingEvent && type == "event")) {
					// create new instance
					flow.event = (type == "event") ? new Event(template: template) : new SamplingEvent(template: template)
				} else {
					flow.event.template = template
				}

				// reset errors
				flash.wizardErrors = [:]
				success()

			}.to "events"
			on("refresh") {
				// handle form data
				eventPage(flow, flash, params)

				// refresh templates
				flow.study.giveEventTemplates().each() {
					it.refresh()
				}

				// refresh event template
				if (flow.event?.template) flow.event.template.refresh()

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "events"
			on("add") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// add event to study
				if (flow.event instanceof SamplingEvent) {
					flow.study.addToSamplingEvents( flow.event )
				} else {
					flow.study.addToEvents( flow.event )
				}

				// validate event
				if (flow.event.validate()) {
					// remove event from the flowscope
					flow.remove('event')

					success()
				} else {
					// event does not validate
					// remove from study
					if (flow.event instanceof SamplingEvent) {
						flow.study.removeFromSamplingEvents( flow.event )
					} else {
						flow.study.removeFromEvents( flow.event )
					}

					// append errors
					this.appendErrors(flow.event, flash.wizardErrors)
					error()
				}
			}.to "events"
			on("deleteEvent") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// find matching (sampling) event
				def event 			= flow.study.events.find { it.getIdentifier() == (params.get('do') as int) }
				def samplingEvent	= flow.study.samplingEvents.find { it.getIdentifier() == (params.get('do') as int) }

				// perform delete
				if (event) flow.study.deleteEvent( event )
				if (samplingEvent) flow.study.deleteSamplingEvent( samplingEvent )
			}.to "events"
			on("addEventGroup") {
				// handle form data
				eventPage(flow, flash, params)

				// set work variables
				def groupName = 'Group '
				def tempGroupIterator = 1
				def tempGroupName = groupName + tempGroupIterator

				// make sure group name is unique
				if (flow.study.eventGroups) {
					while (flow.study.eventGroups.find { it.name == tempGroupName }) {
						tempGroupIterator++
						tempGroupName = groupName + tempGroupIterator
					}
				}
				groupName = tempGroupName

				// add a new eventGroup
				flow.study.addToEventGroups(
					new EventGroup(
						name	: groupName
					)
				)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "events"
			on("deleteEventGroup") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// remove eventGroup
				def eventGroupToRemove = flow.study.eventGroups.find { it.getIdentifier() == (params.get('do') as int) }
				if (eventGroupToRemove) {
					println flow.study.deleteEventGroup( eventGroupToRemove )
				}
			}.to "events"
			on("previous") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "subjects"
			on("next") {
				// handle form data
				eventPage(flow, flash, params) ? success() : error()
			}.to "eventsNext"
			on("quickSave") {
				// handle form data
				eventPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// decide to show a warning page or not
		eventsNext {
			action {
				log.info ".entering eventsNext"

				def assigned = false

				// check if all sampling events are in an eventGroup
				flow.study.samplingEvents.each() { samplingEvent ->
					// iterate through eventGroups
					flow.study.eventGroups.each() { eventGroup ->
						if ( eventGroup.samplingEvents.find { it.equals(samplingEvent) } ) {
							assigned = true
						}
					}
				}

				if (assigned) {
					toGroupsPage()
				} else {
					toWarningPage()
				}
			}
			on("toWarningPage").to "unassignedSamplingEventWarning"
			on("toGroupsPage").to "groups"
		}

		// warning page for unassigned samplingEvent
		unassignedSamplingEventWarning {
			render(view: "_unassigned_samplingEvent_warning")
			onRender {
				log.info ".entering unassignedSamplingEventsWarning"

				flow.page = 3
				success()
			}
			on("next").to "groups"
			on("previous").to "events"
		}

		// groups page
		groups {
			render(view: "_groups")
			onRender {
				log.info ".entering groups"

				flow.page = 3
				success()
			}
			on("previous") {
				// handle form data
				groupPage(flow, flash, params) ? success() : error()
			}.to "events"
			on("next") {
				// handle form data
				groupPage(flow, flash, params) ? success() : error()
			}.to "samples"
			on("quickSave") {
				// handle form data
				groupPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// sample 'previous' page with warning
		samplePrevious {
			render(view: "_samples_previous_warning")
			onRender {
				log.info ".entering samplePrevious"

				flow.page = 4
			}
			on("next").to "samples"
			on("previous").to "groups"
		}

		// samples page
		samples {
			render(view: "_samples")
			onRender {
				log.info ".entering samples"

				flow.page = 4
				success()
			}
			on("switchTemplate") {
				// handle form data
				samplePage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				succes()
			}.to "samples"
			on("refresh") {
				// handle samples
				samplePage(flow, flash, params)

				// refresh all sample templates
				flow.study.giveSampleTemplates().each() {
					it.refresh()
				}

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samples"
			on("regenerate") {
				// handle samples
				samplePage(flow, flash, params)

				// remove all samples from the study
				flow.study.samples.findAll{true}.each() { sample ->
					flow.study.removeFromSamples(sample)
				}

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samples"
			on("previous") {
				// handle samples
				samplePage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samplePrevious"
			on("next") {
				// handle form data
				samplePage(flow, flash, params) ? success() : error()
			}.to "assays"
			on("quickSave") {
				// handle form data
				samplePage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// assays page
		assays {
			render(view: "_assays")
			onRender {
				log.info ".entering assays"

				flow.page = 5
			}
			on("refresh") {
				// handle form data
				assayPage(flow, flash, params)

				// force refresh of the template
				if (flow.assay && flow.assay.template && flow.assay.template instanceof Template) {
					flow.assay.template.refresh()
				}

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "assays"
            on("switchTemplate") {
				// handle form data
	            assayPage(flow, flash, params)

	            // find assay template
	            def template = Template.findByName(params.get('template'))
	            if (flow.assay) {
		            // set template
		            flow.assay.template = template
		            if (template) {
			            flow.assay.setFieldValue(
				            'externalAssayID',
				            this.ucwords(flow.study.code).replaceAll("([ ]{1,})", "") + '_' + this.ucwords(template.name).replaceAll("([ ]{1,})", "")
			            )
		            }
	            } else {
		            // create a new assay instance
		            flow.assay = new Assay(template: template)
		            if (template) {
			            flow.assay.setFieldValue(
				            'externalAssayID',
				            this.ucwords(flow.study.code).replaceAll("([ ]{1,})", "") + '_' + this.ucwords(template.name).replaceAll("([ ]{1,})", "")
			            )
		            }
	            }

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "assays"
			on("add") {
				// handle form data
				assayPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// add assay to study
				flow.study.addToAssays( flow.assay )

				// validate assay
				if (flow.assay.validate()) {
					// remove assay from the flowscope
					flow.remove('assay')
					success()
				} else {
					// assay does not validate
					// remove from study
					flow.study.removeFromAssays( flow.assay )

					// append errors
					this.appendErrors(flow.assay, flash.wizardErrors)
					error()
				}
			}.to "assays"
			on("deleteAssay") {
				println params

				// handle form data
				assayPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// find this assay
				def assay = flow.study.assays.find { it.getIdentifier() == (params.get('do') as int) }

				// perform delete
				if (assay) flow.study.deleteAssay( assay )
			}.to "assays"
			on("previous") {
				// handle form data
				assayPage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samples"
			on("next") {
				// handle form data
				assayPage(flow, flash, params) ? success() : error()
			}.to "assayNext"
			on("quickSave") {
				// handle form data
				assayPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// assayNext
		assayNext {
			action {
				log.info ".entering assayNext"

				// have we got samples and assays?
				if (flow.study.assays && flow.study.samples) {
					// yes, go to the group page
					toAssayGroups()
				} else {
					// no need to show the group page as
					// there's nothing to group
					toConfirm()
				}
			}
			on("toAssayGroups").to "assayGroups"
			on("toConfirm").to "confirm"
		}

		// assay grouping page
		assayGroups {
			render(view: "_assay_groups")
			onRender {
				log.info ".entering assayGroups"

				flow.page = 5
			}
			on("previous") {
				// handle form data
				assayGroupPage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "assays"
			on("next") {
				// handle form data
				assayGroupPage(flow, flash, params) ? success() : error()
			}.to "confirm"
			on("quickSave") {
				// handle form data
				assayGroupPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// confirm Previous
		confirmPrevious {
			action {
				log.info ".entering confirmPrevious"

				// have we got samples and assays?
				if (flow.study.assays && flow.study.samples) {
					// yes, go to the group page
					toAssayGroups()
				} else {
					// no need to show the group page as
					// there's nothing to group
					toAssays()
				}
			}
			on("toAssayGroups").to "assayGroups"
			on("toAssays").to "assays"
		}

		// confirmation
		confirm {
			render(view: "_confirmation")
			onRender {
				log.info ".entering confirm"

				flow.page = 6
			}
			on("toStudy").to "study"
			on("toSubjects").to "subjects"
			on("toEvents").to "events"
			on("toGroups").to "groups"
			on("toSamples").to "samples"
			on("toAssays").to "assays"
			on("toAssayGroups").to "assayGroups"
			on("previous").to "confirmPrevious"
			on("next").to "waitForSave"
			on("quickSave").to "waitForSave"
		}

		waitForSave {
			render(view: "_wait")
			onRender {
				log.info ".entering waitForSave"

				flow.page = 7
			}
			on("next").to "save"
		}

		// store all study data
		save {
			action {
				log.info ".entering save"

				flash.wizardErrors = [:]

				// persist data to the database
				try {
					// save study
					log.info ".saving study"

					// Make sure the owner of the study is set right
					flow.study.owner = authenticationService.getLoggedInUser()

					if (!flow.study.save(flush:true)) {
						this.appendErrors(flow.study, flash.wizardErrors)
						throw new Exception('error saving study')
					}
					log.info ".saved study "+flow.study+" (id: "+flow.study.id+")"

					success()
				} catch (Exception e) {
					// rollback
					this.appendErrorMap(['exception': e.toString() + ', see log for stacktrace' ], flash.wizardErrors)

					// stacktrace in flash scope
					flash.debug = e.getStackTrace()

					error()
				}
			}
			on("error").to "error"
			on(Exception).to "error"
			on("success").to "done"
		}

		// error storing data
		error {
			render(view: "_error")
			onRender {
				log.info ".entering error"

				flow.page = 6
			}
			on("next").to "waitForSave"
			on("previous").to "samples"
		}

		// render finish page
		done {
			render(view: "_done")
			onRender {
				log.info ".entering done"

				flow.page = 7
			}
			onEnd {
				// clean flow scope
				flow.clear()
			}
		}
	}

	/**
	 * load a study
	 * @param Map LocalAttributeMap (the flow scope)
	 * @param Map localAttributeMap (the flash scope)
	 * @param Map GrailsParameterMap (the flow parameters = form data)
	 * @returns boolean
	 */
	def loadStudy(flow, flash, params, user) {
		flash.wizardErrors = new LinkedHashMap()
		
		// load study
		try {
			// load study
			def study = (params.studyid) ? Study.findById( params.studyid ) : Study.findByTitle( params.study )

			// Check whether the user is allowed to edit this study. If it is not allowed
			// the used should had never seen a link to this page, so he should never get
			// here. That's why we just return false
            if (!study.canWrite(user)){
 				return false
			}

			// Grails tends to dynamically load objects. This is a nice
			// feature, but can cause problems (see ticket #223) as objects
			// get instantiated multiple times which causes the POST variable
			// to have a different 'identity' as the actual instances. The
			// workaround is to 'touch' them here so they get instantiated:
			study.hasMany.each { name, type ->
				// dynamically instantiate all identity classes
				if (type.toString() =~ "dbnp.studycapturing") {
					study.getProperty( name ).each { }
				}
			}

			// store study in the flowscope
			flow.study = study

			// set 'quicksave' variable
			flow.quickSave = true

			return true
		} catch (Exception e) {
			// rollback
			this.appendErrorMap(['exception': e.getMessage() + ', see log for stacktrace'], flash.wizardErrors)

			return false<commit_msg>- improved in-code comment<commit_after>package dbnp.studycapturing

import dbnp.data.*

// Grails convertors is imported in order to create JSON objects
import grails.converters.*
import grails.plugins.springsecurity.Secured
import dbnp.authentication.AuthenticationService
import dbnp.authentication.SecUser


/**
 * Wizard Controler
 *
 * The wizard controller handles the handeling of pages and data flow
 * through the study capturing wizard.
 *
 * This controller is only accessible by logged in users.
 *
 * @author <NAME>
 * @since 20100107
 * @package studycapturing
 *
 * Revision information:
 * $Rev$
 * $Author$
 * $Date$
 */
@Secured(['IS_AUTHENTICATED_REMEMBERED'])
class WizardController {
	def authenticationService

	/**
	 * index method, redirect to the webflow
	 * @void
	 */
	def index = {
		def jump = [:]

		// allow quickjumps to:
		//	edit a study	: /wizard?jump=edit&id=1
		//	create a study	: /wizard?jump=create
		if (params.get('jump')) {
			switch (params.get('jump')) {
				case 'create':
					jump = [
					    action: 'create'
					]
					break
				case 'edit':
					jump = [
					    action	: 'edit',
						id		: params.get('id')
					]
					break
				default:
					break
			}
		}

		// store in session
		session.jump = jump

		/**
		 * Do you believe it in your head?
		 * I can go with the flow
		 * Don't say it doesn't matter (with the flow) matter anymore
		 * I can go with the flow (I can go)
		 * Do you believe it in your head?
		 */
		redirect(action: 'pages')
	}

	/**
	 * WebFlow definition
	 * @see http://grails.org/WebFlow
	 * @void
	 */
	def pagesFlow = {
		// start the flow
		onStart {
			log.info ".entering onStart"

			// define flow variables
			flow.page = 0
			flow.pages = [
				//[title: 'Templates'],			// templates
				[title: 'Start'],				// load or create a study
				[title: 'Subjects'],			// subjects
				[title: 'Events'],				// events and event grouping
				//[title: 'Event Groups'],		// groups
				[title: 'Samples'],				// samples
				[title: 'Assays'],				// assays
				//[title: 'Assay Groups'],		// assays
				[title: 'Confirmation'],		// confirmation page
				[title: 'Done']					// finish page
			]
			flow.jump = session.jump

			// since this controller was refactored it's technically
			// safe to enable quicksave throughout the application.
			// However, we must keep an eye on the quality of the
			// studies as this change makes it easier to create dummy
			// studies which will create garbage in out database.
			flow.quickSave = true
			success()
		}

		// render the main wizard page which immediately
		// triggers the 'next' action (hence, the main
		// page dynamically renders the study template
		// and makes the flow jump to the study logic)
		mainPage {
			render(view: "/wizard/index")
			onRender {
				log.info ".entering mainPage"

				flow.page = 1
				success()
			}
			on("next").to "handleJump"
		}

		// handle the jump parameter
		//
		// I came to get down [2x]
		// So get out your seats and jump around
		// Jump around [3x]
		// Jump up Jump up and get down
		// Jump [18x]
		handleJump {
			action {
				log.info ".entering handleJump"

				if (flow.jump && flow.jump.action == 'edit') {
					if (flow.jump.id) {
						// load study
						if (this.loadStudy(flow, flash, [studyid:flow.jump.id],authenticationService.getLoggedInUser())) {
							toStudyPage()
						} else {
							toStartPage()
						}
					} else {
						toModifyPage()
					}
				} else if (flow.jump && flow.jump.action == 'create') {
					toStudyPage()
				} else {
					toStartPage()
				}
			}
			on("toStartPage").to "start"
			on("toStudyPage").to "study"
			on("toModifyPage").to "modify"
		}

		// create or modify a study
		start {
			render(view: "_start")
			onRender {
				log.info ".entering start"

				flow.page = 1
				success()
			}
			on("next") {
				// clean the flow scope
				flow.remove('study')

				// set 'quicksave' variable to false
				flow.quickSave = false
			}.to "study"
			on("modify").to "modify"
			on("import").to "redirectToImport"
		}

		// redirect to the import wizard
		redirectToImport {
			render(view: "_redirect")
			onRender {
				log.info ".entering redirectToImport"
				flash.uri = "/importer/index"
			}
			on("next").to "start"
		}

		// load a study to modify
		modify {
			render(view: "_modify")
			onRender {
				log.info ".entering modify"

				flow.page = 1
				flash.cancel = true
				success()
			}
			on("cancel") {
				flow.remove('study')

				success()
			}.to "start"
			on("next") {
				// load study
				if (this.loadStudy(flow, flash, params, authenticationService.getLoggedInUser())) {
					success()
				} else {
					error()
				}
			}.to "study"
		}

		// render and handle the study page
		study {
			render(view: "_study")
			onRender {
				log.info ".entering study"

				flow.page = 1
				success()
			}
			on("refresh") {
				// handle form data
				studyPage(flow, flash, params)

				// force refresh of the template
				if (flow.study.template && flow.study.template instanceof Template) {
					flow.study.template.refresh()
				}

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "study"
            on("switchTemplate") {
				// handle form data
				studyPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "study"
			on("previous") {
				// handle form data
				studyPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "start"
			on("next") {
				studyPage(flow, flash, params) ? success() : error()
			}.to "subjects"
			on("quickSave") {
				studyPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// render and handle subjects page
		subjects {
			render(view: "_subjects")
			onRender {
				log.info ".entering subjects"

				flow.page = 2

				if (!flash.values || !flash.values.addNumber) flash.values = [addNumber:1]

				success()
			}
			on("refresh") {
				// remember the params in the flash scope
				flash.values = params

				// refresh templates
				if (flow.study.subjects) {
					flow.study.giveSubjectTemplates().each() {
						it.refresh()
					}
				}

				success()
			}.to "subjects"
			on("add") {
				// handle form data
				addSubjects(flow, flash, params) ? success() : error()
			}.to "subjects"
			on("delete") {
				// handle form data
				subjectPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// remove subject
				def subjectToRemove = flow.study.subjects.find { it.identifier == (params.get('do') as int) }
				if (subjectToRemove) {
					flow.study.deleteSubject( subjectToRemove )
				}
			}.to "subjects"
			on("previous") {
				// handle form data
				subjectPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "study"
			on("next") {
				// handle form data
				subjectPage(flow, flash, params) ? success() : error()
			}.to "events"
			on("quickSave") {
				// handle form data
				subjectPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// render events page
		events {
			render(view: "_events")
			onRender {
				log.info ".entering events"

				flow.page = 3

				// add initial eventGroup to study
				if (!flow.study.eventGroups?.size()) {
					flow.study.addToEventGroups(
						new EventGroup(name: 'Group 1')
					)
				}

				success()
			}
			on("clear") {
				// remove all events
				(flow.study.events + flow.study.samplingEvents).each() { event ->
					if (event instanceof SamplingEvent) {
						flow.study.deleteSamplingEvent( event )
					} else {
						flow.study.deleteEvent( event )
					}
				}

				success()
			}.to "events"
			on("switchTemplate") {
				// handle form data
				eventPage(flow, flash, params)

				// get template
				def type	= params.get('eventType')
				def template= Template.findByName( params.get( type + 'Template' ) )

				// change template and/or instance?
				if (!flow.event || (flow.event instanceof Event && type == "sample") || (flow.event instanceof SamplingEvent && type == "event")) {
					// create new instance
					flow.event = (type == "event") ? new Event(template: template) : new SamplingEvent(template: template)
				} else {
					flow.event.template = template
				}

				// reset errors
				flash.wizardErrors = [:]
				success()

			}.to "events"
			on("refresh") {
				// handle form data
				eventPage(flow, flash, params)

				// refresh templates
				flow.study.giveEventTemplates().each() {
					it.refresh()
				}

				// refresh event template
				if (flow.event?.template) flow.event.template.refresh()

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "events"
			on("add") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// add event to study
				if (flow.event instanceof SamplingEvent) {
					flow.study.addToSamplingEvents( flow.event )
				} else {
					flow.study.addToEvents( flow.event )
				}

				// validate event
				if (flow.event.validate()) {
					// remove event from the flowscope
					flow.remove('event')

					success()
				} else {
					// event does not validate
					// remove from study
					if (flow.event instanceof SamplingEvent) {
						flow.study.removeFromSamplingEvents( flow.event )
					} else {
						flow.study.removeFromEvents( flow.event )
					}

					// append errors
					this.appendErrors(flow.event, flash.wizardErrors)
					error()
				}
			}.to "events"
			on("deleteEvent") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// find matching (sampling) event
				def event 			= flow.study.events.find { it.getIdentifier() == (params.get('do') as int) }
				def samplingEvent	= flow.study.samplingEvents.find { it.getIdentifier() == (params.get('do') as int) }

				// perform delete
				if (event) flow.study.deleteEvent( event )
				if (samplingEvent) flow.study.deleteSamplingEvent( samplingEvent )
			}.to "events"
			on("addEventGroup") {
				// handle form data
				eventPage(flow, flash, params)

				// set work variables
				def groupName = 'Group '
				def tempGroupIterator = 1
				def tempGroupName = groupName + tempGroupIterator

				// make sure group name is unique
				if (flow.study.eventGroups) {
					while (flow.study.eventGroups.find { it.name == tempGroupName }) {
						tempGroupIterator++
						tempGroupName = groupName + tempGroupIterator
					}
				}
				groupName = tempGroupName

				// add a new eventGroup
				flow.study.addToEventGroups(
					new EventGroup(
						name	: groupName
					)
				)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "events"
			on("deleteEventGroup") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// remove eventGroup
				def eventGroupToRemove = flow.study.eventGroups.find { it.getIdentifier() == (params.get('do') as int) }
				if (eventGroupToRemove) {
					println flow.study.deleteEventGroup( eventGroupToRemove )
				}
			}.to "events"
			on("previous") {
				// handle form data
				eventPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "subjects"
			on("next") {
				// handle form data
				eventPage(flow, flash, params) ? success() : error()
			}.to "eventsNext"
			on("quickSave") {
				// handle form data
				eventPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// decide to show a warning page or not
		eventsNext {
			action {
				log.info ".entering eventsNext"

				def assigned = false

				// check if all sampling events are in an eventGroup
				flow.study.samplingEvents.each() { samplingEvent ->
					// iterate through eventGroups
					flow.study.eventGroups.each() { eventGroup ->
						if ( eventGroup.samplingEvents.find { it.equals(samplingEvent) } ) {
							assigned = true
						}
					}
				}

				if (assigned) {
					toGroupsPage()
				} else {
					toWarningPage()
				}
			}
			on("toWarningPage").to "unassignedSamplingEventWarning"
			on("toGroupsPage").to "groups"
		}

		// warning page for unassigned samplingEvent
		unassignedSamplingEventWarning {
			render(view: "_unassigned_samplingEvent_warning")
			onRender {
				log.info ".entering unassignedSamplingEventsWarning"

				flow.page = 3
				success()
			}
			on("next").to "groups"
			on("previous").to "events"
		}

		// groups page
		groups {
			render(view: "_groups")
			onRender {
				log.info ".entering groups"

				flow.page = 3
				success()
			}
			on("previous") {
				// handle form data
				groupPage(flow, flash, params) ? success() : error()
			}.to "events"
			on("next") {
				// handle form data
				groupPage(flow, flash, params) ? success() : error()
			}.to "samples"
			on("quickSave") {
				// handle form data
				groupPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// sample 'previous' page with warning
		samplePrevious {
			render(view: "_samples_previous_warning")
			onRender {
				log.info ".entering samplePrevious"

				flow.page = 4
			}
			on("next").to "samples"
			on("previous").to "groups"
		}

		// samples page
		samples {
			render(view: "_samples")
			onRender {
				log.info ".entering samples"

				flow.page = 4
				success()
			}
			on("switchTemplate") {
				// handle form data
				samplePage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				succes()
			}.to "samples"
			on("refresh") {
				// handle samples
				samplePage(flow, flash, params)

				// refresh all sample templates
				flow.study.giveSampleTemplates().each() {
					it.refresh()
				}

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samples"
			on("regenerate") {
				// handle samples
				samplePage(flow, flash, params)

				// remove all samples from the study
				flow.study.samples.findAll{true}.each() { sample ->
					flow.study.removeFromSamples(sample)
				}

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samples"
			on("previous") {
				// handle samples
				samplePage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samplePrevious"
			on("next") {
				// handle form data
				samplePage(flow, flash, params) ? success() : error()
			}.to "assays"
			on("quickSave") {
				// handle form data
				samplePage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// assays page
		assays {
			render(view: "_assays")
			onRender {
				log.info ".entering assays"

				flow.page = 5
			}
			on("refresh") {
				// handle form data
				assayPage(flow, flash, params)

				// force refresh of the template
				if (flow.assay && flow.assay.template && flow.assay.template instanceof Template) {
					flow.assay.template.refresh()
				}

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "assays"
            on("switchTemplate") {
				// handle form data
	            assayPage(flow, flash, params)

	            // find assay template
	            def template = Template.findByName(params.get('template'))
	            if (flow.assay) {
		            // set template
		            flow.assay.template = template
		            if (template) {
			            flow.assay.setFieldValue(
				            'externalAssayID',
				            this.ucwords(flow.study.code).replaceAll("([ ]{1,})", "") + '_' + this.ucwords(template.name).replaceAll("([ ]{1,})", "")
			            )
		            }
	            } else {
		            // create a new assay instance
		            flow.assay = new Assay(template: template)
		            if (template) {
			            flow.assay.setFieldValue(
				            'externalAssayID',
				            this.ucwords(flow.study.code).replaceAll("([ ]{1,})", "") + '_' + this.ucwords(template.name).replaceAll("([ ]{1,})", "")
			            )
		            }
	            }

				// reset errors
				flash.wizardErrors = [:]
				success()
			}.to "assays"
			on("add") {
				// handle form data
				assayPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// add assay to study
				flow.study.addToAssays( flow.assay )

				// validate assay
				if (flow.assay.validate()) {
					// remove assay from the flowscope
					flow.remove('assay')
					success()
				} else {
					// assay does not validate
					// remove from study
					flow.study.removeFromAssays( flow.assay )

					// append errors
					this.appendErrors(flow.assay, flash.wizardErrors)
					error()
				}
			}.to "assays"
			on("deleteAssay") {
				println params

				// handle form data
				assayPage(flow, flash, params)

				// reset errors
				flash.wizardErrors = [:]

				// find this assay
				def assay = flow.study.assays.find { it.getIdentifier() == (params.get('do') as int) }

				// perform delete
				if (assay) flow.study.deleteAssay( assay )
			}.to "assays"
			on("previous") {
				// handle form data
				assayPage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "samples"
			on("next") {
				// handle form data
				assayPage(flow, flash, params) ? success() : error()
			}.to "assayNext"
			on("quickSave") {
				// handle form data
				assayPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// assayNext
		assayNext {
			action {
				log.info ".entering assayNext"

				// have we got samples and assays?
				if (flow.study.assays && flow.study.samples) {
					// yes, go to the group page
					toAssayGroups()
				} else {
					// no need to show the group page as
					// there's nothing to group
					toConfirm()
				}
			}
			on("toAssayGroups").to "assayGroups"
			on("toConfirm").to "confirm"
		}

		// assay grouping page
		assayGroups {
			render(view: "_assay_groups")
			onRender {
				log.info ".entering assayGroups"

				flow.page = 5
			}
			on("previous") {
				// handle form data
				assayGroupPage(flow, flash, params)

				// ignore errors
				flash.wizardErrors = [:]

				success()
			}.to "assays"
			on("next") {
				// handle form data
				assayGroupPage(flow, flash, params) ? success() : error()
			}.to "confirm"
			on("quickSave") {
				// handle form data
				assayGroupPage(flow, flash, params) ? success() : error()
			}.to "waitForSave"
		}

		// confirm Previous
		confirmPrevious {
			action {
				log.info ".entering confirmPrevious"

				// have we got samples and assays?
				if (flow.study.assays && flow.study.samples) {
					// yes, go to the group page
					toAssayGroups()
				} else {
					// no need to show the group page as
					// there's nothing to group
					toAssays()
				}
			}
			on("toAssayGroups").to "assayGroups"
			on("toAssays").to "assays"
		}

		// confirmation
		confirm {
			render(view: "_confirmation")
			onRender {
				log.info ".entering confirm"

				flow.page = 6
			}
			on("toStudy").to "study"
			on("toSubjects").to "subjects"
			on("toEvents").to "events"
			on("toGroups").to "groups"
			on("toSamples").to "samples"
			on("toAssays").to "assays"
			on("toAssayGroups").to "assayGroups"
			on("previous").to "confirmPrevious"
			on("next").to "waitForSave"
			on("quickSave").to "waitForSave"
		}

		waitForSave {
			render(view: "_wait")
			onRender {
				log.info ".entering waitForSave"

				flow.page = 7
			}
			on("next").to "save"
		}

		// store all study data
		save {
			action {
				log.info ".entering save"

				flash.wizardErrors = [:]

				// persist data to the database
				try {
					// save study
					log.info ".saving study"

					// Make sure the owner of the study is set right
					flow.study.owner = authenticationService.getLoggedInUser()

					if (!flow.study.save(flush:true)) {
						this.appendErrors(flow.study, flash.wizardErrors)
						throw new Exception('error saving study')
					}
					log.info ".saved study "+flow.study+" (id: "+flow.study.id+")"

					success()
				} catch (Exception e) {
					// rollback
					this.appendErrorMap(['exception': e.toString() + ', see log for stacktrace' ], flash.wizardErrors)

					// stacktrace in flash scope
					flash.debug = e.getStackTrace()

					error()
				}
			}
			on("error").to "error"
			on(Exception).to "error"
			on("success").to "done"
		}

		// error storing data
		error {
			render(view: "_error")
			onRender {
				log.info ".entering error"

				flow.page = 6
			}
			on("next").to "waitForSave"
			on("previous").to "samples"
		}

		// render finish page
		done {
			render(view: "_done")
			onRender {
				log.info ".entering done"

				flow.page = 7
			}
			onEnd {
				// clean flow scope
				flow.clear()
			}
		}
	}

	/**
	 * load a study
	 * @param Map LocalAttributeMap (the flow scope)
	 * @param Map localAttributeMap (the flash scope)
	 * @param Map GrailsParameterMap (the flow parameters = form data)
	 * @returns boolean
	 */
	def loadStudy(flow, flash, params, user) {
		flash.wizardErrors = new LinkedHashMap()
		
		// load study
		try {
			// load study
			def study = (params.studyid) ? Study.findById( params.studyid ) : Study.findByTitle( params.study )

			// Check whether the user is allowed to edit this study. If it is not allowed
			// the used should had never seen a link to this page, so he should never get
			// here. That's why we just return false
            if (!study.canWrite(user)){
 				return false
			}

			// Grails tends to lazily initialize objects. While in theory this
			// is a nice feature, it does not work well with our complex setup
			// using the Identity class to match POST variables with object
			// instances. This lazy initialization caused two issues:
			// #147 and #223, and both are now resolved by forcing objects to
			// be instantiated / initialized when a study is loaded from the
			// database
			study.hasMany.each { name, type ->
				// dynamically instantiate all identity classes
				if (type.toString() =~ "dbnp.studycapturing") {
					study.getProperty( name ).each { }
				}
			}

			// store study in the flowscope
			flow.study = study

			// set 'quicksave' variable
			flow.quickSave = true

			return true
		} catch (Exception e) {
			// rollback
			this.appendErrorMap(['exception': e.getMessage() + ', see log for stacktrace'], flash.wizardErrors)

			return false
{'fraction_non_alphanumeric': 0.13366103292707562, 'fraction_numerical': 0.0018629497452472335, 'mean_word_length': 2.338378540386633, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7150820', 'n_tokens_mistral': 18501, 'n_tokens_neox': 13678, 'n_words': 3724}