Explorar o código

Merge of tecant and hypoAfrica branches, crfVisitNew is based solely on crfSetup, crfData and others, so heavy code reuse and modular structure, cutting 1000 lines from original crfVisit

Andrej Studen %!s(int64=2) %!d(string=hai) anos
pai
achega
37b9fd46ca

+ 26 - 0
views/participantPortal.html

@@ -0,0 +1,26 @@
+<table cellspacing="2" cellpadding="5" border="0">
+<tr><td>Version: </td><td><strong id="version">0.0</strong></td></tr>
+</table>
+
+<div id="formDiv">
+</div>
+
+<div id="debugDiv" style="display:none">
+	<h3>Debug notes</h3>
+	<textarea cols="95" rows="5" id="formStatus">
+	</textarea>
+</div>
+
+<script type "text/javascript">
+window.onload=loadScripts;
+
+function loadScripts(){
+  LABKEY.requiresScript(["crfTecant/participantPortal.js"],init);
+}
+
+function init(){
+   console.log('Here participantPortal');
+   let action=function(){ participantPortal.generateFormArray();};
+   participantPortal.init(action);
+}
+</script>

+ 48 - 0
views/visitNew.html

@@ -0,0 +1,48 @@
+<h1 id="formTitle">Title</h1>
+
+<table cellspacing="2" cellpadding="5" border="0" id="staticTable">
+<tr><td>CRF ID: </td><td><strong id="crfRefId">1583163135258</strong></td></tr>
+</table>
+
+<form name="visitForm" id="visitForm">
+</form>
+
+<div id="submitDiv"/>
+
+
+<div id="errorDiv" style="display:none">
+<textarea id="errorTxt" cols="95" rows="10"></textarea>
+</div>
+
+<div id="debugDiv" style="display:block"/>
+
+
+<script type="text/javascript">
+
+window.onload = loadScripts;
+
+function loadScripts(){
+   let action=init;
+   LABKEY.requiresScript(["crf/crfVisitNew.js"],action);
+}
+
+function init(){
+		
+	let searchParams = new URLSearchParams(window.location.search);
+	
+	//update this to pick crfRef from url
+	let crfRef=searchParams.get('entryId');
+	//let formSetupQuery=searchParams.get('formSetupQuery');	
+	document.getElementById("crfRefId").innerHTML=crfRef;
+   crfVisit.crfRef=crfRef;
+   crfVisit.masterForm="visitForm";
+
+
+	crfVisit.formId=searchParams.get("formId");
+	crfVisit.role=searchParams.get('role');	
+	
+	crfVisit.clear();
+
+	crfVisit.generateMasterForm();
+}
+</script>

+ 10 - 0
views/visitNew.view.xml

@@ -0,0 +1,10 @@
+<view xmlns="http://labkey.org/data/xml/view" title="CRF Form">
+	<dependencies>
+		<!--local copy of pdfkit, version 0.10.0-->
+		<!--https://github.com/devongovett/pdfkit/releases/download/v0.10.0/pdfkit.standalone.js-->
+		<!--local copy of blob-stream, version 0.1.3-->
+		<!--https://github.com/devongovett/blob-stream/releases/download/v0.1.3/blob-stream.js-->
+
+ </dependencies>
+</view>
+<!-- need to restart labkey to add new files -->

+ 422 - 0
web/crf/crfData.js

@@ -0,0 +1,422 @@
+//not tested yet.
+//to use, add crfTecant/crfData.js to requiresScript and in the call-back, run init
+//will work with crfSetup as setup object
+
+var crfData={};
+
+crfData.init=
+function(cb=null){
+   this.print('[crfData:init]');
+   let that=this;
+   let action=function(){that.afterScripts(cb);};
+   LABKEY.requiresScript(["crf/runQuery.js","crf/variableList.js"],action);
+}
+
+crfData.afterScripts=
+function(cb=null){
+   if (cb) cb();
+}
+
+crfData.setSetup=
+function(setup){
+   this.setup=setup;
+}
+
+crfData.getContainer=
+function(label){
+   return this.setup.getContainer(label);
+}
+
+crfData.print=
+function(msg){
+   console.log(msg);
+}
+
+//getters
+crfData.getSnapshotObject=
+function(){
+   if (!("dataQueriesSnapshot" in this))
+      this.dataQueriesSnapshot=new Object();
+   return this.dataQueriesSnapshot;
+}
+
+
+crfData.getQuerySnapshot=
+function(queryName){
+   //check whether queryName is in snapshotObject?
+   return this.getSnapshotObject()[queryName];
+}
+
+crfData.getLayoutObject=
+function(){
+   if (!("dataQueriesLayout" in this))
+      this.dataQueriesLayout=new Object();
+   return this.dataQueriesLayout;
+}
+
+crfData.getQueryLayout=
+function(queryName){
+   //check whether queryName is in snapshotObject?
+   return this.getLayoutObject()[queryName];
+}
+
+crfData.getLookupObject=
+function(){
+   if (!("lookup" in this))
+      this.lookup=new Object();
+   return this.lookup;
+}
+
+crfData.getLookup=
+function(queryName){
+   let x=this.getLookupObject();
+   if (queryName in x) return x[queryName];
+   return null;
+}
+
+crfData.getRegistration=
+function(){
+   let regQueryPars=variableList.parseVariables(this.setup.getSettings('registrationQuery'));
+   let query=regQueryPars['query'];
+   return this.getQuerySnapshot(query).rows;
+}
+
+crfData.getCrfEntry=
+function(){
+   return this.getQuerySnapshot('crfEntry').rows[0];
+}
+
+crfData.getActiveQueries=
+function(){
+   if (!("activeQueries" in this))
+      this.activeQueries=new Object();
+   return this.activeQueries;
+}
+
+crfData.getActiveQuery=
+function(queryName){
+   let aq=this.getActiveQueries();
+   if (queryName in aq) return aq[queryName];
+   return null;
+}
+
+crfData.getRegistrationMap=
+function(value=null){
+   let rows=this.getRegistration();
+   let qMap=new Object();
+   let key='Key';
+   if (!value) value='participantStudyId';
+   for (let i=0;i<rows.length;i++){
+      qMap[rows[i][key]]=rows[i][value];
+   }
+   return qMap;
+}
+
+crfData.setDataLayout=
+function(formId,role,cb){
+   let fName='[setDataLayout]';
+   this.print(fName);
+	let rowsSetup=this.setup.selectFormSetupRows(formId);
+   let queryArray=new Array();
+	let dS=this.getLayoutObject();//reference only
+	let qMap=this.setup.getMap('inputLists');
+   let qMapInverse=this.setup.invertMap(qMap);
+   this.clearActiveQueries();
+	//config.formConfig.lookup=new Object();
+	for (let i=0;i<rowsSetup.length;i++){
+		let entry=rowsSetup[i];
+		//skip review rows
+		if (entry['showFlag']=='REVIEW')
+			continue;
+      let accessMode=role+'Mode';
+		//skipField
+		if (entry[accessMode]=="NONE") continue;
+
+		let queryId=entry['queryName'];
+		let q=qMap[queryId];
+		queryArray.push(runQuery.makeQuery(dS,'data',q,q,[]));
+      this.addActiveQuery(crfSetup.getEntryMap('inputLists')[queryId]);
+      this.print(fName+' adding '+q);
+		if (entry['showQuery']!="NONE"){
+			let sq=entry['showQuery'];
+		   queryArray.push(runQuery.makeQuery(dS,'data',sq,sq,[]));
+         //need inverse of qMap
+         let sQueryId=qMapInverse[sq];
+         this.addActiveQuery(crfSetup.getEntryMap('inputLists')[sQueryId]);
+         this.print(fName+' adding '+sq);
+			
+		}
+   }
+	//always add reviews
+   let q='reviewComments';
+   queryArray.push(runQuery.makeQuery(dS,'data',q,q,[]));
+   let rQueryId=qMapInverse[q];
+   this.addActiveQuery(crfSetup.getEntryMap('inputLists')[rQueryId]);
+	
+   //debug
+   this.print("List of datasets in form : ");
+	for (f in this.getActiveQueries()){
+      let entry=this.getActiveQuery(f);
+      this.print("\t"+f+" ID: "+entry['Key']+' title '+entry['title']);
+   }
+
+   let that=this;
+   let action=function(){that.processLayout(cb);};
+   runQuery.getDataFromQueries(this,queryArray,action);
+}
+
+//this happens after the for loop, so all dataQueries objects are set
+crfData.processLayout=
+function(cb=null){
+   let fName='[processLayout]';
+   let qList=this.getActiveQueries();
+   //for layouts
+   let queryArray=new Array();
+   let targetObject=this.getLookupObject();
+   let lookupSet=new Object();
+   for (let qId in qList){
+      let entry=this.getActiveQuery(qId);
+      let q=entry['queryName'];
+      let qobject=this.getQueryLayout(q);
+	   this.print(fName+" inspecting layout for "+q+" "+qobject);
+	   qobject.fields=qobject.metaData.fields;
+	   qobject.title=entry['title'];
+
+      //check for lookups
+	   for (let f in qobject.fields){
+		   //anything else is simple but lookup
+		   let field=qobject.fields[f];
+		   if (!("lookup" in field)) continue;
+         let lookup=field.lookup;
+         let qObject=this.getLookup(lookup.queryName);
+         if (qObject) continue;
+         //add to list
+         let qName=lookup.queryName;
+         let qCode=qName+':'+lookup.keyColumn+':'+lookup.displayColumn;
+         let e=runQuery.makeQuery(targetObject,'data',qName,qCode,[]);
+         //adjust minor settings
+         if (lookup.containerPath) e.containerPath=lookup.containerPath;
+         e.schemaName=lookup.schemaName;
+         e.columns=lookup.keyColumn+','+lookup.displayColumn;
+         lookupSet[qCode]=e;
+         this.print(fName+' inserting '+qCode);
+      }
+   }
+   for (let x in lookupSet){
+      queryArray.push(lookupSet[x]);
+      this.print(fName+' adding '+x);
+      for (let v in lookupSet[x]){
+         this.print(fName+' value ['+v+'] '+lookupSet[x][v]);
+      }
+   }
+   //this.print(fName+' print '+targetObject.print);
+   let that=this;
+   let action=function(){that.processLookup(cb);};
+   this.print(fName+' getDataFromQueries');
+   runQuery.getDataFromQueries(this,queryArray,action);
+   this.print(fName+' getDataFromQueries done');
+}
+
+crfData.processLookup=
+function(cb=null){
+   let fName="[processLookup]";
+
+   let obj=this.getLookupObject();
+   for (let q in obj){
+	   this.print(fName+" "+q);
+      let a=q.split(':');
+      if (a.length<3) continue;
+      let lookupName=a[0];
+      let key=a[1];
+      let val=a[2];
+      obj[lookupName]=new Object();
+      this.print(fName+' adding ['+lookupName+'] '+key+'/'+val);
+      let lObject=obj[lookupName];
+
+	   lObject.LUT=new Array();//key to value
+	   lObject.ValToKey=new Array();//value to key
+	   lObject.keyColumn=key
+	   lObject.displayColumn=val;
+      
+      let qRows=obj[q].rows;
+	   for (let i=0;i<qRows.length;i++){
+         let r=qRows[i];
+         this.print(fName+' LUT ['+r[key]+'] '+r[val]);
+		   lObject.LUT[r[key]]=r[val];
+		   lObject.ValToKey[r[val]]=r[key];
+	   }
+   }
+	if (cb) cb();
+}
+
+crfData.setData=
+function(crfRef,cb=null, schemaName=null){
+	fName='[setData]';
+	//let crfMatch=this.getCRFref();
+	//let parentCrf=config.formConfig.crfEntry['parentCrf'];
+	//if (parentCrf!=undefined) crfMatch=parentCrf;
+
+	this.print(fName+' form crf ['+crfRef+'] ');
+
+   let queryArray=new Array();
+   let targetObject=this.getSnapshotObject();
+	//collect data and execute callback cb for queries in cb.queryList
+   let qList=this.getActiveQueries();
+	for (let qId in qList){
+      let entry=qList[qId];
+      let q=entry['queryName'];
+		let filters=[LABKEY.Filter.create("crfRef",crfRef)];
+      let fieldName=q;
+      if (schemaName=='study') fieldName=q+'Study';
+      queryArray.push(runQuery.makeQuery(targetObject,'data',q,fieldName,filters,schemaName));
+
+	}
+   runQuery.getDataFromQueries(this,queryArray,cb);
+}
+
+crfData.setDataForQuery=
+function(queryName,crfRef=null,cb=null,schemaName='lists',crfField='crfRef'){
+   let queryArray=new Array();
+   let targetObject=this.getSnapshotObject();
+	
+   let filters=[LABKEY.Filter.create(crfField,crfRef)];
+   let fieldName=q;
+   if (schemaName=='study') fieldName=q+'Study';
+   queryArray.push(runQuery.makeQuery(targetObject,'data',q,fieldName,filters,schemaName));
+   runQuery.getDataFromQueries(this,queryArray,cb);
+}
+
+crfData.uploadData=
+function(id,crfRef,cb=null){
+   let fName='[uploadData['+id+']'+crfRef+']';
+   this.print(fName);
+   let qList=this.getActiveQueries();
+   let modArray=new Array();
+   let schemaName='study';
+   let containerName='data';
+   for (let qId in qList){
+
+      let entry=qList[qId];
+      let q=entry['queryName'];
+
+      this.print(fName+' working on '+q);
+      
+      //determine rows that need to be updated form querySnapshot
+      let studyRows=this.getQuerySnapshot(q+'Study').rows;
+      let listRows=this.getQuerySnapshot(q).rows;
+      this.print(fName+' studies '+studyRows.length+' lists '+listRows.length);
+      //rows to be UPDATED (already in dataset)
+      let modRows=new Array();
+      for (let i=0;i<studyRows.length;i++){
+         let entry=studyRows[i];
+         //
+         if (! (i<listRows.length) ) continue;
+         let entryList=listRows[i];
+		   //keeps study only variables (ParticipantId, SequenceNum)
+		   for (let f in entryList) {
+			   entry[f]=entryList[f];
+			   this.print(fName+" copying ["+f+"]: "+entry[f]+"/"+entryList[f]);
+		   }
+         modRows.push(entry);
+      }
+
+      //rows to be INSERTED 
+      let insRows=new Array();
+      let participantField=crfSetup.getRows('studyData')[0]["SubjectColumnName"];
+      for (let i=studyRows.length;i<listRows.length;i++){
+         let entry=listRows[i];
+         //make sure you have the participantField right
+         //
+         entry[participantField]=id;
+         entry.crfRef=crfRef;;
+         entry.SequenceNum=crfRef;
+         entry.SequenceNum=entry.SequenceNum % 1000000000;
+		
+         if (listRows.length>1){
+            entry.SequenceNum+=i/100;
+         }
+         this.print( "Adding sequence number "+entry.SequenceNum);
+         insRows.push(entry);
+      }
+
+      if (modRows.length>0)
+         modArray.push(runQuery.makeModification('update',containerName,schemaName,q,modRows));
+      //determine rows that need to be inserted from querySnapshot
+      if (insRows.length>0)
+         modArray.push(runQuery.makeModification('insert',containerName,schemaName,q,insRows));
+
+   }
+   //do the whole batch
+   runQuery.modifyDataFromQueries(this,modArray,cb);
+}
+
+crfData.removeData=
+function(cb=null){
+   let qList=this.getActiveQueries();
+   let modArray=new Array();
+   for (let qId in qList){
+      let entry=qList[qId];
+      let q=entry['queryName'];
+      
+      //determine rows that need to be updated form querySnapshot
+      let studyRows=this.getQuerySnapshot(q+'Study').rows;
+      if (studyRows.length>0)
+         modArray.push(runQuery.makeModification('delete','data','study',q,studyRows));
+
+      let listRows=this.getQuerySnapshot(q).rows;
+      if (listRows.length>0)
+         modArray.push(runQuery.makeModification('delete','data','lists',q,listRows));
+   }
+   //do the whole batch
+   runQuery.modifyDataFromQueries(this,modArray,cb);
+}
+
+
+
+crfData.setRegistration=
+function(cb=null){
+      let regQueryPars=variableList.parseVariables(this.setup.getSettings('registrationQuery'));
+   let q=regQueryPars['query'];
+   let queryArray=new Array();   
+   let targetObject=this.getSnapshotObject();
+   queryArray.push(runQuery.makeQuery(targetObject,'data',q,q,[]));
+   runQuery.getDataFromQueries(this,queryArray,cb);
+}
+
+crfData.setCrfEntry=
+function(crfRef,cb=null){
+   let q='crfEntry';
+   let queryArray=new Array();   
+   let targetObject=this.getSnapshotObject();
+	let filters=[LABKEY.Filter.create('entryId',crfRef)];
+   queryArray.push(runQuery.makeQuery(targetObject,'data',q,q,filters));
+   runQuery.getDataFromQueries(this,queryArray,cb);
+}
+
+crfData.createCrfStatus=
+function(crfEntry){
+   let crfStatus=new Object();
+   crfStatus.entryId=crfEntry.entryId;
+   crfStatus.submissionDate=new Date();
+   crfStatus.FormStatus=crfEntry.FormStatus;
+   crfStatus.User=crfEntry.UserId;
+   crfStatus.Form=crfEntry.Form;
+   return crfStatus;
+}
+
+crfData.addActiveQuery=
+function(entry){
+   let aq=this.getActiveQueries();
+   let qName=entry['queryName'];
+   if (qName in aq) return;
+   aq[qName]=entry;
+   return;
+}
+
+crfData.clearActiveQueries=
+function(){
+   let aq=this.getActiveQueries();
+   for (q in aq){
+      delete aq[q];
+   }
+}

+ 41 - 0
web/crf/crfHTML.css

@@ -0,0 +1,41 @@
+table {margin-bottom:20px;table-layout:fixed; border-collapse:collapse; border-spacing:10px}
+table.t1 {width:400px; border:1px solid black}
+table.t1 th {border:1px solid black;padding:4px;background-color:#e0e0e0}
+table.t1 td {text-align:center}
+table.t2 {width:800px; border:1px solid black;}
+table.t2 th {border:1px solid black;padding:4px;background-color:#e0e0e0}
+table.t2 td {border:1px solid black; text-align:center}
+
+div.d1 {text-align:center; width=400px; background-color:#e0e0e0;
+        font-size:      20px; margin-bottom:20px}
+
+.box{
+width:120px;
+height:120px;
+}
+
+.gold{ background-color: gold; }
+.red{ background-color: darkred; }
+.green {background-color: green;}
+.orange {background-color: orange;}
+.blue {background-color: steelblue;}
+.brown {background-color: tan;}
+.empty { background-color: #dddddd;
+      border-style: dashed;
+}
+
+.large{
+	font-size: 30px;
+}
+
+.medium{
+   font-size: 24px;
+}
+
+.center{
+	text-align: center;
+}
+
+.stretch {
+   padding: 50px;
+}

+ 177 - 0
web/crf/crfHTML.js

@@ -0,0 +1,177 @@
+var crfHTML={};
+
+crfHTML.print=
+function(msg){
+   console.log(msg);
+}
+
+crfHTML.init=
+function(cb=null){
+   LABKEY.requiresCss("crf/crfHTML.css");
+   this.print('CSS loaded');
+   if (cb) cb();
+}
+
+crfHTML.getElement=
+function(id){
+   return document.getElementById(id);
+}
+
+crfHTML.append=
+function(element,id=null,el=null){
+   if (id) document.getElementById(id).appendChild(element);
+   if (el) el.appendChild(element);
+}
+
+crfHTML.addStyle=
+function(el,style){
+   el.classList.add(style);
+}
+ 
+crfHTML.createSelect=
+function(qMap,id=null,el=null){
+   let fName='[makeSelect]';
+   let input=document.createElement('select');
+   this.addSelectOptions(input,qMap);
+   this.append(input,id,el);
+   return input;
+}
+
+crfHTML.createTable=
+function(id=null,el=null,style=null){
+   let table=document.createElement('table');
+   this.append(table,id,el);
+   if (style) this.addStyle(style);
+   return table;
+}
+
+crfHTML.createBox=
+function(id=null,el=null){
+   let fbox=document.createElement('div');
+   fbox.classList.add("box");
+   this.append(fbox,id,el);
+   return fbox;
+}
+
+crfHTML.createParagraph=
+function(text,id=null,el=null){
+   let fp=document.createElement("p");
+   fp.innerHTML=text;
+   fp.classList.add("center");
+   this.append(fp,id,el);
+   return fp;
+}
+
+crfHTML.createTblHeader=
+function(id=null,el=null){
+   let element=document.createElement('th');
+   this.append(element,id,el);
+   return element;
+}
+
+crfHTML.createButton=
+function(id=null,el=null){
+   let button=document.createElement('input');
+   button.type='button';
+   this.append(button,id,el);
+   return button;
+}
+
+crfHTML.createTextNode=
+function(text,id=null,el=null){
+	let tNode=document.createTextNode(text);
+   this.append(tNode,id,el);
+   return tNode;
+}
+
+crfHTML.createDiv=
+function(divId=null,id=null,el=null){
+   let div=document.createElement('div');
+   if (divId) div.id=divId;
+   this.append(div,id,el);
+   return div;
+}
+
+crfHTML.createTextArea=
+function(id=null,el=null){
+   let area=document.createElement('textarea');
+   this.append(area,id,el);
+   return area;
+}
+
+crfHTML.createLabel=
+function(label,id=null,el=null){
+   let x=document.createElement('label');
+   x.innerText=label;
+   this.append(x,id,el);
+   return x;
+}
+
+crfHTML.createDate=
+function(id=null,el=null){
+   let x=document.createElement('input');
+   x.type='date';
+   this.append(x,id,el);
+   return x;
+}
+
+crfHTML.createTextInput=
+function(id=null,el=null){
+   let x=document.createElement('input');
+   x.type='text';
+   this.append(x,id,el);
+   return x;
+}
+
+crfHTML.createFileInput=
+function(id=null,el=null){
+   let x=document.createElement('input');
+   x.type='file';
+   this.append(x,id,el);
+   return x;
+}
+
+crfHTML.createCheckbox=
+function(id=null,el=null){
+   let x=document.createElement('input');
+   x.type='checkbox';
+   this.append(x,id,el);
+   return x;
+}
+
+crfHTML.clear=
+function(el){
+   while (el.hasChildNodes()){
+      el.removeChild(el.lastChild);
+   }
+}
+
+crfHTML.clearOptions=
+function(input){
+   while(input.options.length) input.remove(0);
+}
+
+crfHTML.addSelectOptions=
+function(input,qMap){
+   this.clearOptions(input);
+   let opt = document.createElement("option");
+	opt.text = "<Select>";
+	opt.value = -1;
+	input.options[0] = opt;
+	this.print(fName+": Adding <Select>");
+	
+
+	//add other, label them with LUT
+	for (let v in qMap) {
+		this.print(fName+': populating '+v+': '+qMap[v]);
+
+		let opt = document.createElement("option");
+		opt.text = qMap[v];
+		opt.value = v;
+		input.options[input.options.length] = opt;
+		
+	}
+	input.selectedIndex=0;	
+}
+
+

+ 459 - 0
web/crf/crfManager.js

@@ -0,0 +1,459 @@
+//global config variable
+const config=new Object();
+
+function print(msg){
+	config.document.getElementById(config.debugArea).value+="\n"+msg;
+}
+
+function clear(){
+	config.document.getElementById(config.debugArea).value="";
+}
+
+function doNothing(){
+	print('doNothing called');
+}
+
+
+function generateDescription(){
+	//loop over all forms
+	//read the setup
+	print('Generate description');
+	setFormConfig();
+	
+}
+
+function setContainer(label,container){
+	if (!(config.formConfig.hasOwnProperty('container'))){
+		config.formConfig.container=new Array();
+	}
+	config.formConfig.container[label]=container;
+}
+
+function getContainer(label){
+	return config.formConfig.container[label];
+}
+
+function makeQuery(containerName,queryName,fieldName,filterArray){
+	let e=new Object();
+	e.containerName=containerName;
+	e.queryName=queryName;
+	e.fieldName=fieldName;
+	e.filterArray=filterArray;
+	return e;
+}
+
+
+
+function getDataFromQueries(queryArray,cb){
+	//queryArray should contain elements with
+	//- fieldName to set the data variable
+	//- containerName to select container (data,config,CRF)
+	//- queryName to select query
+	//- filterArray to perform filtering, empty array works
+	//- callback cb to be called with no arguments
+	//
+	afterQuery(new Object(),-1,queryArray,cb);
+}
+
+
+function afterQuery(data,id,queryArray,cb){
+	//queryArray should contain elements with
+	//- fieldName to set the data variable
+	//- containerName to select container (data,config,CRF)
+	//- queryName to select query
+	//- filterArray to perform filtering, empty array works
+	//- callback cb to be called with no arguments
+	//
+	//it should be called with id -1.
+	//
+	print('afterQuery['+id+'/'+queryArray.length+']: ');
+
+	if (id>-1){
+		let fieldName=queryArray[id].fieldName;
+		print('afterQuery['+fieldName+']: '+data.rows.length);
+		config.formConfig[fieldName]=data;
+	}
+	id+=1;
+	if (id==queryArray.length) {
+		cb();
+		return;
+	}
+
+
+	let e=queryArray[id];
+	let qconfig=new Object();
+	qconfig.containerPath=getContainer(e.containerName);
+	qconfig.schemaName="lists";
+	if ("schemaName" in e){
+		print('afterQuery: schemaName='+e.schemaName);
+		qconfig.schemaName=e.schemaName;
+	}
+
+	if ("columns" in e){
+		print('afterQuery: columns='+e.columns);
+		qconfig.columns=e.columns;
+	}
+	qconfig.queryName=e.queryName;
+	//this should point to configuration container
+	//don't filter -> so we can pick up other forms (say registration) later on
+	//qconfig.filterArray=[LABKEY.Filter.create('Key',config.formId)];
+	if ("filterArray" in e)
+		qconfig.filterArray=e.filterArray;
+	
+	//qconfig.filterArray=[LABKEY.Filter.create('formStatus',1)]
+	qconfig.success=function(data){afterQuery(data,id,queryArray,cb);};
+	qconfig.failure=doNothing;
+	LABKEY.Query.selectRows(qconfig);
+
+}
+
+function setFormConfig(){
+
+	
+
+	//add object to store form related data
+	config.formConfig=new Object();
+
+	config.formConfig.softwareVersion='0.0.1';
+	let debug=true;
+
+	if (debug)
+		print("setFormConfig");	
+	
+	//set containers for data and configuration
+
+	//TODO: set this from a query
+	//
+	
+	setContainer('data',LABKEY.ActionURL.getContainer());
+	setContainer('config',LABKEY.ActionURL.getContainer());
+	setContainer('CRF',LABKEY.ActionURL.getContainer());
+
+	let selectRows=new Object();
+	//this is local data
+	selectRows.containerPath=getContainer('CRF');
+	selectRows.schemaName='lists';
+	selectRows.queryName='crfSettings';
+	//store form related data to this object
+	selectRows.success=afterSettings;
+	LABKEY.Query.selectRows(selectRows);
+
+}
+
+function afterSettings(data){
+
+	config.formConfig.settings=new Array();
+	for (let i=0;i<data.rows.length;i++){
+		let n=data.rows[i]['name'];
+		let v=data.rows[i]['value'];
+		config.formConfig.settings[n]=v;
+	}
+
+	let st=config.formConfig.settings;
+	print('afterSettings');
+	for (let k in st){
+		print('\t'+k+'='+st[k]);
+	}
+
+	//if ('dataContainer' in st){
+	//	setContainer('data',st['dataContainer']);
+	//}
+	let vname='configContainer';
+	if (vname in st){
+		setContainer('config',st[vname]);
+	}
+	print('Config: '+getContainer('config'));
+	print('Data: '+getContainer('data'));
+	collectData();
+}
+
+
+function collectData(){
+
+	let queryArray=new Array();
+	//users
+	queryArray.push(makeQuery('CRF','users','userData',[]));
+	queryArray[queryArray.length-1].schemaName='core';
+	
+	//Forms	
+	queryArray.push(makeQuery('config','Forms','formData',[]));
+	//FormSetup	
+	queryArray.push(makeQuery('config','FormSetup','formSetup',[]));
+	//inputLists
+	queryArray.push(makeQuery('config','inputLists','inputLists',[]));
+	//
+
+	print('running getDataFromQueries');
+	getDataFromQueries(queryArray,fcontinue);
+}
+
+function findName(listId){
+	let frows=config.formConfig.inputLists.rows;
+	for (let i=0;i<frows.length;i++){
+		if (frows[i]['Key']!=listId) continue;
+		return frows[i]['queryName'];
+	}
+}
+
+function fcontinue(){
+	print('loadedData');
+	let queryArray=new Array();
+	
+	let frows=config.formConfig.formSetup.rows;
+	for (let i=0;i<frows.length;i++){
+		let listId=frows[i]['queryName'];
+		//skip forms only
+		let showFlag=frows[i]['showFlag'];
+		let showQuery=frows[i]['showQuery'];
+		if (showFlag=='REVIEW') continue;
+		let listName=findName(listId);
+		print(listName);
+		queryArray.push(makeQuery('data',listName,listName,[]));
+		if (showFlag=='NONE') continue;
+		queryArray.push(makeQuery('data',showQuery,showQuery,[]));
+	}
+
+	getDataFromQueries(queryArray,fcontinue1);
+
+}
+
+function getList(formId){
+	let fList=new Array();
+	let frows=config.formConfig.formSetup.rows;
+	for (let i=0;i<frows.length;i++){
+		if (frows[i]['formName']!=formId) continue;
+		let listId=frows[i]['queryName'];
+		let showFlag=frows[i]['showFlag'];
+		let showQuery=frows[i]['showQuery'];
+		if (showFlag=='REVIEW') continue;
+		let fObj=new Object();
+		fObj['queryName']=findName(listId);
+		fObj['title']=frows[i]['title'];
+		fList.push(fObj);
+		if (showFlag=='NONE') continue;
+		let fObj1=new Object();
+		fObj1['queryName']=showQuery;
+		fObj1['title']=frows[i]['title']+' (details)';
+		fList.push(fObj1);
+	}
+	return fList;
+}
+
+function printField(field){
+	let name=field['name'];
+	if (name=='Key') return;
+	if (name=='crfRef') return;
+	let type=field['type'];
+	let qName='';
+	if ('lookup' in field){
+		qName=field.lookup.queryName;
+	}
+	print(name+' '+type+'/'+qName);
+	printPDF(field);
+}
+
+function addLookup(lookupList,field){
+	if ('lookup' in field){
+		lookupList.add(field.lookup.queryName);
+	}
+}
+
+function printFields(lookupList,listName){
+	let fields=config.formConfig[listName].metaData.fields;
+	//print('getFields '+listName+': '+fields.length);
+	for (f in fields){
+		printField(fields[f]);
+		addLookup(lookupList,fields[f]);		
+		//printPDF(fields[f]);
+	}
+}
+
+function fcontinue1(){
+	printLayout();
+}
+
+function printData(){
+	let frows=config.formConfig.formData.rows;
+	let lookupList=new Set();
+	for (let i=0;i<frows.length;i++){
+		let formId=frows[i]['Key'];
+		print(frows[i]['formName']);
+		printTitlePDF(20,frows[i]['formName']);
+		let fList=getList(formId);
+		for (let j=0;j<fList.length;j++){
+			print(fList[j]['queryName']);
+			printTitlePDF(16,fList[j]['title']);
+			printFields(lookupList,fList[j]['queryName']);
+		}
+	}
+	print('all done');
+	let queryArray=new Array();
+	for (let item of lookupList){
+		queryArray.push(makeQuery('data',item,item,[]));
+	}
+	let cb=function(){printLookup(lookupList);};
+	getDataFromQueries(queryArray,cb);
+}
+
+function printQuery(queryName){
+	printTitlePDF(16,queryName);
+	print(queryName);
+	let frows=config.formConfig[queryName].rows;
+	print('rows: '+frows);
+	let fields=config.formConfig[queryName].metaData.fields;
+	let field=undefined;
+	for (f in fields){
+		if (fields[f].name=='Key') continue;
+		field=fields[f];
+		break;
+	}
+	for (let i=0;i<frows.length;i++){
+		printPDFEntry(field,frows[i]);
+	}
+}
+
+function printLookup(lookupList){
+
+	printTitlePDF(20,'Enumerators');
+	for (let item of lookupList){
+		printQuery(item);
+	}
+	config.doc.end();
+
+}
+
+function checkBlob(){
+	print("checkBlob: "+config.blob);
+	if (config.blob) {
+		clearInterval(config.blobInterval);
+		config.a.href = config.window.URL.createObjectURL(config.blob);
+		print("HREF: "+config.a.href);
+		config.a.download = 'test.pdf';
+		config.a.click();
+		config.window.URL.revokeObjectURL(config.a.href);
+	}
+	config.count=config.count+1;
+	print("Eval: "+config.count);
+	if (config.count>100){
+		clearInterval(config.blobInterval);
+	}
+
+}
+
+
+function printLayout(){
+
+	config.doc=new PDFDocument();
+	//config.doc.end();
+	let stream = config.doc.pipe(blobStream()).on("finish",function(){
+			config.blob=stream.toBlob("application/pdf");});
+	
+	print("BLob: "+config.blob);
+	config.a = config.document.createElement("a");
+	config.document.body.appendChild(config.a);
+	config.a.innerHTML="Download PDF";
+	config.a.style = "display: none";
+	config.count=0;
+	//run until blob is set
+	config.blobInterval=setInterval(checkBlob,1000);
+
+	//pick data from crfForm list
+        print("Printing form");
+	printData();
+}
+
+function printTitlePDF(fontSize,title){
+	config.doc.y+=10;
+	config.doc.font('Courier-Bold').fontSize(fontSize).text(title);
+}
+
+function printPDF(field){
+	//object field should have a name, type, caption
+	//entry should have field.name
+	//lookup is null or has a lookup table LUT 
+	//for value v of entry[field.name]
+	//
+	//the total width of a A4 page is 598 px, 
+	//left margin is 72. With a right margin of 50,
+	//the total available with is 476 px.
+	
+	let w=476;
+	let spacing=25;
+	let w1=(w-spacing)*0.5;
+	let fontSize=14;	
+	
+	print('printPDF: entry['+field.name);
+	print('printPDF: field type:'+field.type);
+
+	//measure text
+	let label=field.caption;
+	let opt={width:w1};
+	config.doc.fontSize(fontSize);
+	
+	//for more eloquent display the height of the text
+	//can be measured prior to output
+	//use currentLineHeight to scale height
+	//let lineH=config.doc.currentLineHeight(1);
+	//let h=config.doc.heightOfString(label,opt)/lineH;
+
+
+	//print label
+	config.doc.font('Courier').text(label,opt);
+	
+	//align last row of description w/ first row of value
+	config.doc.moveUp();
+
+	//store x value for later use
+	let tx=config.doc.x;
+	let ty=config.doc.y;
+
+	//shift for value output
+	config.doc.x+=w1+spacing;
+	let v=field.type;
+	if ('lookup' in field){
+		v+='/'+field.lookup.queryName;
+	}
+	print('v: '+v);
+	config.doc.font('Courier-Bold').text(v,opt);
+
+	//restore x value
+	config.doc.x=tx;
+	
+}
+
+function printPDFEntry(field,entry){
+	//object field should have a name, type, caption
+	//entry should have field.name
+	//lookup is null or has a lookup table LUT 
+	//for value v of entry[field.name]
+	//
+	//the total width of a A4 page is 598 px, 
+	//left margin is 72. With a right margin of 50,
+	//the total available with is 476 px.
+	
+	let w=476;
+	let spacing=25;
+	let w1=(w-spacing)*0.5;
+	let fontSize=14;	
+	
+	print('printPDF: entry['+field.name);
+	print('printPDF: field type:'+field.type);
+
+	//measure text
+	let label=entry[field.name];
+	let opt={width:w1};
+	config.doc.fontSize(fontSize);
+	
+	//for more eloquent display the height of the text
+	//can be measured prior to output
+	//use currentLineHeight to scale height
+	//let lineH=config.doc.currentLineHeight(1);
+	//let h=config.doc.heightOfString(label,opt)/lineH;
+
+
+	//print label
+	config.doc.font('Courier').text(label,opt);
+	
+	
+}
+

+ 182 - 0
web/crf/crfPrint.js

@@ -0,0 +1,182 @@
+var crfPrint={};
+
+//printing section
+//
+crfPrint.set=
+function(parentClass){
+   this.parent=parentClass;
+}
+
+crfPrint.checkBlob=
+function(){
+	this.parent.print("checkBlob: "+this.blob);
+	if (this.blob) {
+		clearInterval(this.blobInterval);
+		this.a.href = this.parent.config.window.URL.createObjectURL(this.blob);
+		this.parent.print("HREF: "+this.a.href);
+		this.a.download = 'test.pdf';
+		this.a.click();
+		this.parent.config.window.URL.revokeObjectURL(this.a.href);
+	}
+	this.count=this.count+1;
+	this.parent.print("Eval: "+this.count);
+	if (this.count>100){
+		clearInterval(this.blobInterval);
+	}
+
+}
+
+crfPrint.printForm=
+function(){
+   let that=this;
+   let action=function(){that.afterScripts();};
+   LABKEY.Utils.requiresScript(["crf/blob-stream.js","crf/pdfkit.standalone.js"],action);
+}
+
+crfPrint.afterScripts=
+function(){
+   let config=this.parent.config;
+	this.doc=new PDFDocument();
+   let that=this;
+	//config.doc.end();
+   let action=function(){that.blob=that.stream.toBlob("application/pdf");};
+	this.stream = this.doc.pipe(blobStream()).on("finish",action);
+	
+	this.parent.print("BLob: "+this.blob);
+	this.a = this.parent.config.document.createElement("a");
+	this.parent.config.document.body.appendChild(this.a);
+	this.a.innerHTML="Download PDF";
+	this.a.style = "display: none";
+	this.count=0;
+	//run until blob is set
+   let iAction=function(){that.checkBlob();}
+   this.blobInterval=setInterval(iAction,1000);
+
+	//pick data from crfForm list
+   this.parent.print("Printing form");
+	this.printHeader();
+
+   let foo=function(){that.formatPrintData();};
+	this.parent.setData(foo);
+}
+
+crfPrint.printHeader=
+function(){
+   let config=this.parent.config;
+	this.doc.fontSize(25).text(config.formConfig.form['formName']);
+	this.doc.moveDown();
+	let crfEntry=config.formConfig.crfEntry;
+	let site=config.formConfig.currentSite;
+	let val=new Object();
+	let user=config.formConfig.user;
+	val['A']={o:crfEntry,f:'EudraCTNumber',t:'Eudra CT Number'};
+	val['B']={o:crfEntry,f:'StudyCoordinator',t:'Study Coordinator'};
+	val['C']={o:crfEntry,f:'StudySponsor',t:'Study Sponsor'};
+	val['D']={o:site,f:'siteName',t:'Site'};
+	val['E']={o:site,f:'sitePhone',t:'Phone'};
+	val['F']={o:user,f:'DisplayName',t:'Investigator'};
+
+	for (let f in val){
+		this.parent.print('Printing for '+f);
+		let e=val[f];
+		let entry=new Object();
+		entry[f]=e.o[e.f];
+		this.printPDF(entry,
+			{name:f,caption:e.t,type:'string'},null);
+	}
+	this.doc.moveDown();
+}
+
+crfPrint.formatPrintData=
+function(){
+   let config=this.parent.config;
+	qS=this.parent.getQueryList();
+	for (let q in qS){
+		this.parent.print('Setting up '+q);
+		let qData=this.parent.getQuerySnapshot(q);
+      let qLayout=this.parent.getQueryLayout(q);
+		this.parent.print('Number of rows: '+qData.rows.length);
+		if (qData.rows.length>0){
+			this.doc.fontSize(20).text(qLayout.title);
+		}
+      let fields=qLayout.fields;
+		for (let i=0;i<qData.rows.length;i++){
+			let entry=qData.rows[i];
+		   for (let f in fields){
+				let field=fields[f];
+				let lookup=null;
+				if (field.lookup){
+					lookup=this.parent.getLookup(field.lookup.queryName);
+				}
+				if (field.hidden) continue;
+				this.printPDF(entry,field,lookup);
+			}
+		}
+		this.doc.moveDown();
+	}
+	this.parent.print("All done");
+   this.doc.end();
+}
+
+crfPrint.printPDF=
+function(entry,field,lookup){
+	//object field should have a name, type, caption
+	//entry should have field.name
+	//lookup is null or has a lookup table LUT 
+	//for value v of entry[field.name]
+	//
+	//the total width of a A4 page is 598 px, 
+	//left margin is 72. With a right margin of 50,
+	//the total available with is 476 px.
+	
+	let w=476;
+	let spacing=25;
+	let w1=(w-spacing)*0.5;
+	let fontSize=14;	
+	
+	this.parent.print('printPDF: entry['+field.name+']='+entry[field.name]);
+	let v=entry[field.name];
+	if (lookup!=null){
+		v=lookup.LUT[v];
+	}
+	this.parent.print('printPDF: field type:'+field.type);
+	if (field.type=="date"){
+		let d=new Date(v);
+		v=d.getDate()+'/'+(d.getMonth()+1)+'/'+d.getFullYear();
+	}	
+	if (v===null) v=' / ';
+	if (v===undefined) v=' / ';
+
+	//measure text
+	let label=field.caption;
+	let opt={width:w1};
+	this.doc.fontSize(fontSize);
+	
+	//for more eloquent display the height of the text
+	//can be measured prior to output
+	//use currentLineHeight to scale height
+	//let lineH=config.doc.currentLineHeight(1);
+	//let h=config.doc.heightOfString(label,opt)/lineH;
+
+
+	//print label
+	this.doc.font('Courier').text(label,opt);
+	
+	//align last row of description w/ first row of value
+	this.doc.moveUp();
+
+	//store x value for later use
+	let tx=this.doc.x;
+	let ty=this.doc.y;
+
+	//shift for value output
+	this.doc.x+=w1+spacing;
+	
+	this.doc.font('Courier-Bold').text(v,opt);
+
+	//restore x value
+	this.doc.x=tx;
+	
+}
+
+

+ 465 - 0
web/crf/crfReviewSection.js

@@ -0,0 +1,465 @@
+//loadFile is in fileManager.js
+var crfReviewSection={};
+
+crfReviewSection.set=
+function(parentClass){
+   if ("parent" in this) 
+      return;
+   this.parent=parentClass;
+}
+
+crfReviewSection.generateErrorMessage=
+function (id,listName,msg){
+	this.parent.print('generateErrorMessage:');
+	let eid=listName+"_errorMsg";
+	let el=config.document.getElementById(eid);
+	if (el===null){
+		el=config.document.createElement("p");
+		config.document.getElementById(id).appendChild(el);
+	}
+	el.innerHTML=msg;
+}
+
+crfReviewSection.clearErrorMessage=
+function(listName){
+	let eid=listName+"_errorMsg";
+	let el=config.document.getElementById(eid);
+	if (el===null) return;
+	el.remove();
+}
+
+
+
+
+crfReviewSection.generateSection=
+function(listName,id,callback){
+   let that=this;
+   let action=function(){that.fcontinue(listName,id,callback);};
+   LABKEY.requiresScript(["crf/fileManager.js"],action);
+}
+
+crfReviewSection.fcontinue=
+function(listName,id,callback){
+	//callback should be generateReviewSectionCB and it takes no arguments
+	this.parent.print("generateReviewSection");
+   let config=this.parent.config;
+	//need base path
+
+
+	config.loadFileConfig=new Object();
+	
+	
+	config.loadFileConfig.cb=callback;
+	config.loadFileConfig.id=id;
+	config.loadFileConfig.url=fileManager.getBasePath()+'/@files/reportSetup/'+listName+'.json';
+	fileManager.loadFile();
+	//load file and continue in the next function
+}
+
+crfReviewSection.getParticipantCode=
+function(pid){
+
+	let filters=[LABKEY.Filter.create("crfRef",this.parent.getCRFref())];
+   let config=this.parent.config;
+	let mfId=config.formConfig.form['masterQuery'];
+   let queryName=config.formConfig.queryMap[mfId];
+   let that=this;
+	pid.afterId=function(id){that.setParticipantCode(id);};
+	pid.participantField=config.formConfig.studyData["SubjectColumnName"];
+	let cb=function(data){that.afterRegistration(pid,data);}
+   //untested
+   this.parent.selectRows('lists',queryName,filters,cb,this.parent.getContainer('data'));
+}
+
+crfReviewSection.visitCodeFromVisitId=
+function(visitId){
+	if (visitId<0) return "NONE";
+	let project=this.parent.getContainer('data');
+this.parent.print('visitCodeFromVisitId: '+project.search('retro'));
+	if (project.search('retro')>-1)
+		visitId-=1;
+	return 'VISIT_'+visitId.toString();
+}
+
+crfReviewSection.replaceSlash=
+function(x){
+	return x.replace(/\//,'_');
+}
+
+crfReviewSection.setParticipantCode=
+function(pid){
+	let fName='[setParticipantCode]';
+	let rows=pid.registration.rows;
+   let config=this.parent.config;
+	//pick from study
+	let participantField=config.formConfig.studyData["SubjectColumnName"];
+	if (rows.length==1){
+		this.parent.print(fName+': '+rows[0][participantField]+'/'+rows[0].visitId);
+		let visitCode=this.visitCodeFromVisitId(rows[0].visitId);
+		this.parent.print('setParticipantCode: '+pid.participantId+'/'+visitCode);
+		pid.participantCode=this.replaceSlash(pid.participantId);
+		pid.visitCode=visitCode;
+	}
+	this.generateReviewSection2(pid);
+}
+
+crfReviewSection.CB=
+function(){
+   let config=this.parent.config;
+	let listName=config.loadFileConfig.listName;
+	let id=config.loadFileConfig.id;
+
+	this.parent.clearErrorMessage(listName);
+
+	let pid=new Object();
+	pid.participantCode="NONE";
+	pid.visitCode="NONE";
+	this.getParticipantCode(pid);
+	this.parent.print('Get participant code sent');
+	//involves database search, continue after callback
+}
+
+crfReviewSection.getValueFromElement=
+function(id,defaultValue){
+   let config=this.parent.config;
+	let e=config.document.getElementById(id);
+	if (e!=null){
+		defaultValue=e.innerHTML;
+	}
+	return defaultValue;
+}
+
+crfReviewSection.pickParticipantCodeFromPage=
+function(){
+	let pid=new Object();
+	pid.participantCode=this.getValueFromElement("participantCode","NIX-LJU-D2002-IRAE-A000");
+	pid.visitCode=this.getValueFromElement("visitCode","VISIT_1");
+	this.generateReviewSection2(pid);
+}
+
+crfReviewSection.patternReplace=
+function(src,replacements,values){
+
+	for (rep in replacements){
+		let txt1=src.replace(new RegExp(rep),values[replacements[rep]]);
+		src=txt1;
+	}
+	return src;
+
+}
+
+crfReviewSection.plotImage=
+function(cell,k,row,rowVariable,obj,pid){
+   let config=this.parent.config;
+	let baseDir=this.patternReplace(obj.imageDir,obj.replacements,pid);
+	this.parent.print('Base dir: '+pid.basePath);
+	pid[obj.variable]=obj.values[k];
+	cell.id=pid[obj.variable]+"_"+rowVariable+pid[rowVariable];
+	let img=null;
+	let imgId=cell.id+'_img_';
+	img=config.document.getElementById(imgId);
+	if (img===null){
+		img=config.document.createElement('img');
+		img.id=imgId;
+		cell.appendChild(img);
+	}
+	let imgSrc=patternReplace(obj.file,obj.replacements,pid);
+   this.parent.print('Image: '+imgSrc);
+	let imagePath=pid.basePath+'/'+baseDir+'/'+imgSrc;
+			
+	img.src=imagePath;
+	img.width="300";
+
+	
+}
+
+crfReviewSection.showReport=
+function(cell,k,row,rowVariable,obj,pid){
+
+	cell.width="300px";
+	cell.id='report_'+obj.values[k]+"_"+rowVariable+pid[rowVariable];
+	let reportConfig=new Object();
+	reportConfig.partName="Report";
+	reportConfig.renderTo=cell.id;
+	//reportConfig.showFrame=false;
+	//reportConfig.width="300";
+	reportConfig.frame="none";
+	reportConfig.partConfig=new Object();
+	reportConfig.partConfig.width="300";
+	reportConfig.partConfig.title="R Report";
+	reportConfig.partConfig.reportName=obj.values[k];
+	for (f in obj.parameters){
+		reportConfig.partConfig[f]=pid[f];
+	}
+	reportConfig.partConfig.showSection="myscatterplot";
+	let reportWebPartRenderer = new LABKEY.WebPart(reportConfig);
+   this.parent.print('Render to: '+reportConfig.renderTo);
+	reportWebPartRenderer.render();
+}
+
+crfReviewSection.showProbability=
+function(cell,k,row,rowSetup,j,obj,pid){
+   this.parent.print('showProbability: '+rowSetup);
+	let rowVariable=rowSetup.variable;	
+	cell.id='prob_'+obj.values[k]+"_"+rowVariable+pid[rowVariable];
+
+	let probDensity=new Object();
+	probDensity.mean=rowSetup.mean[j];
+	probDensity.sigma=rowSetup.sigma[j];
+
+   this.parent.print('showProbability: mean '+probDensity.mean+' sigma '+probDensity.sigma);
+
+
+	probDensity.func=obj.values[k];
+	probDensity.organCode=pid.organCode;
+	pid[obj.variable]=rowSetup[obj.variable][j];
+	probDensity.percentile=pid.percentile;
+	let selectRows=new Object();
+	selectRows.queryName=obj.queryName;
+	selectRows.schemaName="study";
+	selectRows.filterArray=[];
+	selectRows.containerPath=getContainer('data');
+	for (let f in obj.filters){
+		selectRows.filterArray.push(
+				LABKEY.Filter.create(f,pid[obj.filters[f]]));
+	   this.parent.print('Filter ['+f+']: '+pid[obj.filters[f]]);
+	}
+	selectRows.success=function(data){ 
+		this.drawProbability(data,cell,obj,pid,probDensity);}
+	LABKEY.Query.selectRows(selectRows);
+}
+
+crfReviewSection.erf=
+function(x){
+	let fx=[0,0.02,0.04,0.06,0.08,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,
+		1,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2,
+		2.1,2.2,2.3,2.4,2.5,3,3.5];
+	let fy=[0,0.222702589,0.328626759,0.428392355,0.520499878,
+		0.603856091,0.677801194,0.742100965,0.796908212,
+	 	0.842700793,0.880205070,0.910313978,0.934007945,
+		0.952285120,0.966105146,0.976348383,
+		0.983790459,0.989090502,0.992790429,0.995322265,
+		0.997020533,0.998137154,0.998856823,0.999311486,
+		0.999593048,0.999977910,0.999999257];
+	let n=32;
+	let i0=n-1;
+
+	for (let i=1;i<n;i++){
+		if (Math.abs(x)>fx[i]) continue;
+		i0=i-1;
+		break;
+	}
+	let fval=1;
+	if (i0<n-1){
+		//interpolate
+		let y1=fy[i0+1];
+		let y0=fy[i0];
+		let x1=fx[i0+1];
+		let x0=fx[i0];
+		fval=y0+(y1-y0)/(x1-x0)*(Math.abs(x)-x0);
+	}
+   this.parent.print('Erf: '+fval);
+	if (x<0) return -fval;
+	return fval;
+}
+
+crfReviewSection.setLine=
+function(fbox,name,value,fontSize){
+	let fpId=fbox.id+name;
+	let fp=config.document.getElementById(fpId);
+	if (fp===null){
+		fp=config.document.createElement("p");
+		fp.id=fpId;
+		fbox.appendChild(fp);
+	}
+	fp.classList.add("center");
+	fp.style.textAlign="center";
+	fp.style.fontSize=fontSize;
+	fp.innerText=value;
+}	
+
+crfReviewSection.drawProbability=
+function(data,cell,obj,pid,probDensity){
+this.parent.print('drawProbability');
+	if (data.rows.length!=1){
+	   this.parent.print("drawProbability row length mismatch: "+data.rows.length);
+		return;
+	}
+	//possible mismatch; I assume the dataset will have a field called value
+	let val=data.rows[0].value;
+
+	let prob=0;
+	let fz=-100;
+
+	if (probDensity.func=="gaus"){
+		fz=(val-probDensity.mean)/probDensity.sigma/Math.sqrt(2);
+		prob=0.5+0.5*erf(fz);
+	}
+	let color="red";
+	let fzx=fz*Math.sqrt(2);
+   this.parent.print('drawProbability '+fzx);
+
+	for (let i=1;i<obj.intervals.n;i++){
+		if (fzx>obj.intervals.zlimits[i]) continue;
+		color=obj.intervals.colors[i-1];
+		break;
+	}
+	
+	let fboxId=cell.id+'_fbox_';
+	let fbox=config.document.getElementById(fboxId);
+	if (fbox===null){
+		fbox=config.document.createElement("div");
+		fbox.id=fboxId;
+		cell.appendChild(fbox);
+	}
+	fbox.style.backgroundColor=color;
+	fbox.style.width="180px";
+	fbox.style.height="180px";
+	
+
+   this.parent.print('organCode '+probDensity.organCode);
+	let organName="Lung";
+
+	if (probDensity.organCode==4){
+		organName="Thyroid";
+	}
+	if (probDensity.organCode==5){
+		organName="Bowel";
+	}
+
+	this.setLine(fbox,'_fp4_',organName,"16px");
+	this.setLine(fbox,'_fp_',val.toPrecision(3),"25px");
+	this.setLine(fbox,'_fp1_',"SUV("+probDensity.percentile+"%)","16px");
+	this.setLine(fbox,'_fp2_',fzx.toPrecision(3),"25px");
+	this.setLine(fbox,'_fp3_',"z-value","16px");
+
+
+}
+crfReviewSection.generateReviewSection2=
+function(pid){ 
+	let config=this.parent.config;
+	let listName=config.loadFileConfig.listName;
+	let id=config.loadFileConfig.id;
+	
+   this.parent.print('generateReviewSection2: '+pid.participantCode+'/'+
+		pid.visitCode);
+	if (pid.participantCode=="NONE" || pid.visitCode=="NONE"){
+		this.generateErrorMessage(id,listName,
+			"ParticipantId/visitId not set");
+		return;
+	}
+	
+
+   this.parent.print('JSON: '+config.loadFileConfig.json);
+
+	let json=config.loadFileConfig.json;
+	let nrows=json.rows.values.length;
+	let ncol=json.columns.length;
+
+	pid.basePath=fileManager.getBasePath()+"/@files";
+	
+	
+	let el=config.document.getElementById(id);
+	let tableId=id+'_Table';
+	let table=config.document.getElementById(tableId);
+	if (table==null){
+		table=config.document.createElement('table');
+		table.id=tableId;
+		el.appendChild(table);
+	}
+	table.style.tableLayout="fixed";
+	table.style.columnWidth="300px";
+	
+	for (let i=0;i<nrows;i++){
+		pid[json.rows.variable]=json.rows.values[i];
+		//let organ=organs[i];
+		let row=null;
+		if (i<table.rows.length)
+			row=table.rows[i];
+		else
+			row=table.insertRow();
+
+		let ic=0;
+		for (let j=0;j<ncol;j++){
+			let obj=json.columns[j];
+			let nv=obj.values.length;
+			for (let k=0;k<nv;k++){
+				let cell=null;
+				if (ic<row.cells.length)
+					cell=row.cells[ic];
+				else
+					cell=row.insertCell();
+				if (obj.display=="image") 
+					this.plotImage(cell,k,row,json.rows.variable,obj,pid);
+				if (obj.display=="report") 
+					this.showReport(cell,k,row,json.rows.variable,obj,pid);
+				if (obj.display=="probability"){ 
+					this.showProbability(cell,k,row,json.rows,i,obj,pid);
+				}	
+				ic++;
+			}
+
+
+		}
+		
+
+	}
+}
+
+///>>>>>>>>>>>>>>end of reviewSection(REPORT)
+
+crfReviewSection.afterRegistration=
+function(data,fc){
+	let fName='[afterRegistration/'+data.queryName+']';
+   this.parent.print(fName+": rows:"+data.rows.length);
+	fc.registration=data;
+	let registrationData=fc.registration;
+	this.parent.clearErr();
+	if (registrationData.rows.length!=1){
+		let msg=fName+": ERROR: Found "+registrationData.rows.length;
+		msg+=" registration entries for crfrefid "+this.parent.getCRFref();
+	   this.parent.print(msg);
+		fc.afterId(fc);
+		return;
+	}
+   this.parent.print(fName+'registration participant field: '+fc.participantField);
+	fc.participantId=registrationData.rows[0][fc.participantField];
+	//could be a lookup field (particularly for studies)
+   this.parent.print('ID: '+fc.participantId);	
+	let fields=registrationData.metaData.fields;
+	let field="NONE";
+	for (f in fields){
+		if (fields[f]["name"]==fc.participantField)
+			field=fields[f];
+	}
+	if ("lookup" in field){
+		let pid=fc.participantId;
+	   this.parent.print("Using lookup for participantId: "+pid);
+		let lookup=field["lookup"];
+   	this.parent.print("Lookup: ["+lookup.schemaName+','+lookup.queryName+']');
+
+      //load lookup
+      let that=this;
+		let cb=function(data){that.afterRegistrationLookup(data,lookup.displayColumn,fc)};
+		let filters=[LABKEY.Filter.create(lookup.keyColumn,pid)];
+      this.parent.selectRows(lookup.schemaName,lookup.queryName,filters,cb,lookup.containerPath);
+
+	}
+	else{
+		//afterParticipantId(configUpload);
+		fc.afterId(fc);
+	}
+}
+
+crfReviewSection.afterRegistrationLookup=
+function(data,displayColumn,fc){
+   this.parent.print("afterRegistrationLookup");
+	let entry=data.rows[0];
+	fc.participantId=entry[displayColumn];
+   this.parent.print('Setting to '+fc.participantId);
+	fc.afterId(fc);
+	//afterParticipantId(configUpload);
+}
+
+
+

+ 454 - 0
web/crf/crfSetup.js

@@ -0,0 +1,454 @@
+var crfSetup={};
+
+crfSetup.print=
+function(msg){
+   console.log(msg);
+}
+
+crfSetup.init=
+function(cb=null){
+   let fName="[crfSetup:init]";
+   this.print(fName);
+   let that=this;
+   let action=function(){that.afterScripts(cb);}
+   LABKEY.requiresScript(["crf/runQuery.js"],action);
+}
+
+crfSetup.afterScripts=
+function(cb=null){
+   if (cb) cb();
+}
+
+crfSetup.setContainer=
+function(label,container){
+	if (!(this.hasOwnProperty('container'))){
+		this.container=new Array();
+	}
+	this.container[label]=container;
+}
+
+crfSetup.getContainer=
+function(label){
+	return this.container[label];
+}
+
+crfSetup.getSettings=
+function(variable){
+   if (variable in this.settings){
+      return this.settings[variable];
+   }
+   return null;
+}
+
+crfSetup.getRows=
+function(objectName){
+   if (objectName in this)
+      return this[objectName].rows;
+   return new Array();
+}
+
+crfSetup.getMaps=
+function(){
+   return this.getObject('maps');
+}
+
+crfSetup.getEntryMaps=
+function(){
+   return this.getObject('entryMaps');
+}
+
+crfSetup.getMap=
+function(queryName){
+   let maps=this.getMaps();
+   if (!(queryName in maps))
+      this.parseMap(queryName);
+   return maps[queryName];
+
+}
+
+crfSetup.getEntryMap=
+function(queryName){
+   let entryMaps=this.getEntryMaps();
+   if (!(queryName in entryMaps))
+      this.parseEntryMap(queryName);
+   return entryMaps[queryName];
+}
+
+crfSetup.getAdditionalDataObject=
+function(){
+   return this.getObject('additionalData');
+}
+
+crfSetup.getAdditionalData=
+function(queryName){
+   let adObject=this.getAdditionalDataObject();
+	if (queryName in adObject){
+		this.print(fName+': Returning preset value');
+		return adObject[queryName];
+	}
+   return null;
+}
+
+
+crfSetup.getTargetStatus=
+function(action){
+   return this.getObject('targetStatus')[action];
+}
+
+crfSetup.getTargetRecipient=
+function(action){
+   return this.getObject('targetRecipient')[action];
+}
+
+crfSetup.getActionSettings=
+function(action){
+   return this.getObject('actionSettings')[action];
+}
+
+
+crfSetup.getObject=
+function(name){
+   if (!(name in this))
+      this[name]=new Object;
+   return this[name];
+}
+
+crfSetup.addObject=
+function(masterObject,objectName,object){
+   let obj=this.getObject(masterObject);
+   obj[objectName]=object;
+}
+
+crfSetup.invertMap=
+function(qMap){
+   let qInverseMap=new Object();
+   for (let q in qMap){
+      qInverseMap[qMap[q]]=q;
+   }
+   return qInverseMap;
+}
+
+crfSetup.setContainers=
+function(cb=null){
+
+   this.setContainer('data',LABKEY.ActionURL.getContainer());
+	this.setContainer('config',LABKEY.ActionURL.getContainer());
+	this.setContainer('CRF',LABKEY.ActionURL.getContainer());
+   let selectRows=new Object();
+	//this is local data
+	selectRows.containerPath=this.getContainer('CRF');
+	selectRows.schemaName='lists';
+	selectRows.queryName='crfSettings';
+	//store form related data to this object
+   let that=this;
+	selectRows.success=function(data){that.parseSettings(data,cb);};
+	LABKEY.Query.selectRows(selectRows);
+}
+
+crfSetup.parseSettings=
+function(data,cb){
+   let fName="[parseSettings]";
+	this.settings=new Array();
+	for (let i=0;i<data.rows.length;i++){
+		let n=data.rows[i]['name'];
+		let v=data.rows[i]['value'];
+		this.settings[n]=v;
+	}
+
+	this.print(fName);
+	for (let k in this.settings){
+		this.print(fName+'\t'+k+'='+this.settings[k]);
+	}
+
+	//if ('dataContainer' in st){
+	//	setContainer('data',st['dataContainer']);
+	//}
+	let vname='configContainer';
+	if (vname in this.settings){
+		this.setContainer('config',this.settings[vname]);
+	}
+	this.print(fName+' config: '+this.getContainer('config'));
+	this.print(fName+' data: '+this.getContainer('data'));
+
+   if (cb) cb();
+
+}
+
+crfSetup.parseSetup=
+function(cb=null){
+	//setup queryArray
+	let queryArray=new Array();
+
+   //targetObject
+   let targetObject=this;
+
+   //static variables
+	queryArray.push(runQuery.makeQuery(targetObject,'data','crfStaticVariables','crfStaticVariables',[]));
+	//Forms 
+	queryArray.push(runQuery.makeQuery(targetObject,'config','Forms','dataForms',[]));
+   //also formData
+	//users
+	queryArray.push(runQuery.makeQuery(targetObject,'data','users','users',[]));
+	queryArray[queryArray.length-1].schemaName='core';
+   //also userData
+	//inputLists
+	queryArray.push(runQuery.makeQuery(targetObject,'config','inputLists','inputLists',[]));
+	//crfEditors
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfEditors','crfEditors',[]));
+   //crfEditorData
+	//crfMonitors
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfMonitors','crfMonitors',[]));
+   //crfMonitorData
+	//crfSponsors
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfSponsors','crfSponsors',[]));
+   //crfSponsorData
+	//crfManagers
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfManagers','crfManagers',[]));
+	//FormStatus
+   let statusFilter=[];
+   if ("formStatus" in this)
+      statusFilter.push(LABKEY.Filter.create('Key',this.formStatus));
+	queryArray.push(runQuery.makeQuery(targetObject,'config','FormStatus','formStatus',statusFilter));
+   //crfButtons
+	let statusButtonFilter=[];
+   if ("formStatus" in this)
+	   statusButtonFilter.push(LABKEY.Filter.create('sourceFormStatus',this.formStatus));
+	queryArray.push(
+		runQuery.makeQuery(targetObject,'config','crfButtons','crfButtons',statusButtonFilter));
+	//site
+	queryArray.push(runQuery.makeQuery(targetObject,'config','site','siteData',[]));
+	//crfEntry
+	queryArray.push(runQuery.makeQuery(targetObject,'data','crfEntry','crfEntries',[]));
+   //specialFields 
+   queryArray.push(runQuery.makeQuery(targetObject,'data','specialFields','specialFields',[]));
+	//FormSetup	
+	queryArray.push(runQuery.makeQuery(targetObject,'config','FormSetup','formSetup',[]));
+	//generateConfig
+   queryArray.push(
+		runQuery.makeQuery(targetObject,'config','generateConfig','generateConfigData',[]));	
+
+   //parentCrf
+   if ("parentCrf" in this){
+		let crfFilter=LABKEY.Filter.create('entryId',this.parentCrf);
+		queryArray.push(runQuery.makeQuery(targetObject,'data','crfEntry','parentCrfData',[crfFilter]));	
+	}
+
+
+   let that=this;
+   let action=function(){that.addStudyProperties(cb);};
+	runQuery.getDataFromQueries(this,queryArray,action);
+}
+
+crfSetup.addStudyProperties=
+function(cb){
+   //setup queryArray
+	let queryArray=new Array();
+   let targetObject=this;
+	
+	queryArray.push(runQuery.makeQuery(targetObject,'data','StudyProperties','studyData',[]));
+   //also studyDataAll1
+	let e=queryArray[queryArray.length-1];
+	e.schemaName='study';
+   let columnModel="";
+	let varRows=this.getRows('crfStaticVariables');
+	for (let i=0;i<varRows.length;i++){
+      if (i>0) columnModel+=',';
+      columnModel+=varRows[i]['staticVariable'];
+   }
+	e.columns=columnModel;
+   let that=this;
+   //let action=function(){that.fcontinue();};
+   //let action=function(){that.parseQueryMap(cb);};
+   let action=cb;
+   runQuery.getDataFromQueries(this,queryArray,action);
+
+}
+
+
+crfSetup.selectFormSetupRows=
+function(formId){
+	let formSetupRows=new Array();
+   let config=this.config;
+	let allRows=this.getRows("formSetup");
+	for (let i=0;i<allRows.length;i++){
+		let formEntry=allRows[i];
+		if (formEntry.formName==formId)
+			formSetupRows.push(formEntry);
+	}
+	return formSetupRows;
+}
+
+crfSetup.findSetupRow=
+function(sectionId){
+   let key=sectionId.replace('section','');
+   return this.getEntryMap('formSetup')[key];
+}
+
+
+crfSetup.parseMap=
+function(queryName){
+   let fName='[parseMap/'+queryName+']';
+   let key="Key";
+   let value="value";
+   if (queryName=="inputLists")
+      value="queryName";
+   if (queryName=="users"){
+      key="UserId";
+      value="DisplayName";
+   }
+   if (queryName=='dataForms')
+      value='formName';
+   if (queryName=='formStatus')
+      value='formStatus';
+
+   this.print(fName);
+   let rows=this.getRows(queryName);
+   this.maps[queryName]=new Object();
+   let qMap=this.maps[queryName];
+   for (let i=0;i<rows.length;i++){
+      let r=rows[i];
+      qMap[r[key]]=r[value];
+      //this.print(fName+' ['+r[key]+'] '+r[value]);
+   }
+   
+}
+
+crfSetup.parseEntryMap=
+function(queryName){
+   //queryMap can be a combination of queryName:key where key is an override of standard keys given below
+   let fName='[parseEntryMap/'+queryName+']';
+   let tA=queryName.split(':');
+   let q=tA[0];
+   let rows=this.getRows(q);
+   this.entryMaps[queryName]=new Object();
+   let qMap=this.entryMaps[queryName];
+   let key='Key';
+   if (q=='users') key='UserId';
+   if (q=='siteData') key='siteNumber';
+   if (tA.length>1) key=tA[1];
+   for (let i=0;i<rows.length;i++){
+      let r=rows[i];
+      qMap[r[key]]=r;
+      this.print(fName+' ['+r[key]+'] '+r);
+   }
+}
+ 
+crfSetup.printMap=
+function(queryName){
+   let fName='[printMap]';
+   let qMap=this.getMap(queryName);
+   for (let x in qMap){
+      this.print(fName+' ['+x+'] '+qMap[x]);
+   }
+}
+
+crfSetup.setAdditionalData=
+function(crfRef,formId){
+   let formRows=this.selectFormSetupRows(formId);
+   for (let i=0;i<formRows.length;i++){
+      this.setAdditionalDataEntry(crfRef,formRows[i]);
+   }
+}
+
+crfSetup.setAdditionalDataEntry=
+function(crfRef,formSetupEntry){
+   //return information on additional data associated with the form
+   //additionalData is a sub-list with multiple entries per patient/visit
+   
+   //argument is the row of the formSetup setup list
+	let queryName=this.getMap('inputLists')[formSetupEntry['queryName']];
+	let fName='[getAdditionalData/'+queryName+']';
+	this.print(fName);
+
+   
+   //additionalData holds a reference to all queries already parsed
+   //this helps in reducing number of calls to the database (I assume)a
+
+   let adObject=this.getAdditionalDataObject();
+
+   //first time we see this query, so we have to do the setup
+	this.print(fName+': generating');
+	adObject[queryName]=new Object();
+	
+   //takes address, so further changes will be to the newly created object
+   //in fact, ad is just a short alias of the long variable name on the right
+	let ad=adObject[queryName];
+
+   //no additional data
+	if (formSetupEntry["showFlag"]==="NONE") {
+		this.print(fName+": empty");
+		return ad;
+	}
+
+   //use showFlag to setup report section of the CRF list
+	if (formSetupEntry["showFlag"]==="REVIEW") {
+		//abuse additionalData to signal different segment
+		this.print(fName+": generateReport");
+		ad.isReview=true;
+		return ad;
+	}
+
+   //setup the additionalData memory object
+	this.print(fName+': setting values');
+	ad.showFlag=formSetupEntry["showFlag"];
+	ad.showFlagValue=formSetupEntry["showFlagValue"];
+	ad.queryName=formSetupEntry["showQuery"];
+
+   //for data queries, limit to present CRF only
+	ad.filters=new Object();
+	ad.filters['crfRef']=crfRef;
+
+   //compose a long debug message
+	let msg=fName+": flag "+ad.showFlag;
+	msg+=" value "+ad.showFlagValue;
+	msg+=" query "+ad.queryName;
+	this.print(msg);
+
+	return ad;	
+}
+
+crfSetup.findTitle=
+function(queryId){
+   let entry=this.getEntryMap('inputLists')[queryId];
+   if (entry) 
+      return entry['title'];
+   return "NONE";
+}
+
+crfSetup.parseButtons=
+function(){
+	let rows=this.getRows('crfButtons');
+
+	for (let i=0; i<rows.length; i++){
+		let action=rows[i].action;//String
+      let tstatus=rows[i].targetFormStatus;
+      let trecip=rows[i].targetRecipient;
+   	this.addTargetStatus(action,tstatus);
+		this.addTargetRecipient(action,trecip);
+		//allow for settings to be promoted with each action (and potentially parsed and acted upon)
+		//config.formConfig.actionSettings[action]=undefined;
+		let aSet=rows[i].actionSettings;
+		if (aSet){
+			this.addActionSettings(action,variableList.parseVariables(aSet));
+			variableList.printVariables(this,this.getActionSettings(action));
+		}
+
+	}
+}
+
+
+crfSetup.addTargetStatus=
+function(action,tstatus){
+   this.addObject('targetStatus',action,tstatus);
+}
+
+crfSetup.addTargetRecipient=
+function(action,x){
+   this.addObject('targetRecipient',action,x);
+}
+
+crfSetup.addActionSettings=
+function(action,x){
+   this.addObject('actionSettings',action,x);
+}
+

+ 2238 - 0
web/crf/crfVisitNew.js

@@ -0,0 +1,2238 @@
+var crfVisit={};
+
+//crfVisit.config=new Object();
+
+crfVisit.setDebug=
+function(debug=null){
+   if (debug){
+      this.print=function(msg){debug.this.print(msg);};
+      this.clear=function(){debug.clear();}
+      return;
+   }
+   //provide default functions if not debug object is available
+   this.print=function(msg){console.log(msg);}
+   this.clear=function(){;}
+}
+
+crfVisit.setDebug();
+
+
+crfVisit.init=
+function(cb=null){
+   let that=this;
+   let action=function(){that.scriptsLoaded(cb);};
+   let dependencies=new Array();
+   dependencies.push('crf/runQuery.js');
+   dependencies.push("crf/crfReviewSection.js");
+   dependencies.push("crf/participantIdManager.js");
+   dependencies.push("crf/variableList.js");
+   dependencies.push("crf/webdav.js");
+   dependencies.push("crf/crfPrint.js");
+   dependencies.push("crf/crfSetup.js");
+   dependencies.push("crf/crfData.js");
+   dependencies.push("crf/crfHTML.js");
+   dependencies.push("crf/generateRegistration.js");
+   LABKEY.Utils.requiresScript(dependencies,action);
+}
+
+crfVisit.scriptsLoaded=
+function(cb=null){
+   participantIdManager.set(crfSetup,crfData);
+   webdav.set(this);
+   crfReviewSection.set(this);
+   crfPrint.set(this);
+   crfData.setSetup(crfSetup);
+   crfHTML.init();
+   generateRegistration.init();
+   let initRegistration=function(){generateRegistration.init(cb);};
+   let initIdManager=function(){participantIdManager.init(initRegistration);};
+   let action=function(){crfData.init(initIdManager)};
+   crfSetup.init(action);
+}
+
+
+crfVisit.getContainer=
+function(label){
+	return crfSetup.getContainer(label);
+}
+
+crfVisit.getCrfRefFirst=
+function(){
+	//crfRef is part of html call and gets stored in the page
+	return this.crfRef;
+}
+
+crfVisit.getCrfRef=
+function (){
+	//'crfRefId'
+	return crfData.getCrfEntry()['entryId'];
+}
+
+crfVisit.getCrfRefData=
+function(){
+	let parentCrf=crfData.getCrfEntry()['parentCrf'];
+	if (parentCrf!=undefined) return parentCrf;
+	return this.getCrfRef();
+}
+
+crfVisit.onFailure=
+function(errorInfo, options, responseObj){
+	
+	if (errorInfo && errorInfo.exception)
+		alert("Failure: " + errorInfo.exception);
+	else
+		alert("Failure: " + responseObj.statusText);
+}
+
+crfVisit.doNothing=
+function (){
+	this.print('doNothing called');
+}
+
+crfVisit.getIdManager=
+function(){
+   if (!("idManager" in this)){
+      participantIdManager.masterForm=this.masterForm;
+      this.idManager=participantIdManager.getObject();
+   }
+   return this.idManager;
+}
+
+crfVisit.getSetupObject=
+function(){
+   if (!("setups" in this))
+      this.setups=new Object();
+   return this.setups;
+}
+
+crfVisit.getStoredSetup=
+function(sectionId){
+   let sObj=this.getSetupObject();
+   if (sectionId in sObj) return sObj[sectionId];
+   return null;
+}
+
+crfVisit.addSetup=
+function(sectionId,setup){
+   let sObj=this.getSetupObject();
+   sObj[sectionId]=setup;
+}
+   
+crfVisit.makeSetup=
+function(sectionId,listName){
+	//generate setup object whcih should contain fields:
+	//readonlyFlag - whether the dataset is writeable
+	//filters - selection fields that allow creation of LABKEY.Filter.create()
+	//getInputId - formating of unique ids for html elements
+   //
+
+   let fName='[Setup]';
+	this.print(fName+' '+sectionId+'/'+listName);
+   let setup=new Object();
+	setup.queryName=listName;
+	setup.readonlyFlag=function(vName){return false};
+	setup.filters=new Object();
+	setup.filters['crfRef']=this.getCrfRef();
+	setup.getInputId=function(vName){return sectionId+"_"+vName;}
+   setup.sectionId=sectionId;
+	setup.isReview=false;
+   this.addSetup(sectionId,setup);
+   setup.setVariables=new Object();
+   return setup;
+	
+}
+
+crfVisit.makeFullAccessSetup=
+function(sectionId,listName){
+	//addApply - whether a submit/Save button is generated
+	let setup=this.makeSetup(sectionId,listName);
+	setup.addApply="Save";
+	return setup;
+
+}
+
+crfVisit.makeReadonlySetup=
+function(sectionId,listName){
+   let setup=this.makeSetup(sectionId,listName);
+	//see definition of setup object above, change readonly flag
+	setup.readonlyFlag=function(vName){return true};
+	return setup;
+}
+
+crfVisit.getSetup=
+function(sectionId,listName,writeAccess=true){
+	//change to section granulated permission of type EDIT, COMMENT, READ
+	//let formStatus=config.formConfig.formStatus;
+	//equivalent to READ
+   let setup=this.getStoredSetup(sectionId);
+   if (setup) return setup;
+
+
+	if (!writeAccess)
+	//if (formStatus=="Submitted")
+		return this.makeReadonlySetup(sectionId,listName);
+	//if (formStatus=="Approved")
+	//	return readonlySetup(listName);
+	return this.makeFullAccessSetup(sectionId,listName);
+}
+
+crfVisit.generateSection=
+function(formSetupEntry){
+   let that=this;
+	let listName=crfSetup.getMap('inputLists')[formSetupEntry['queryName']];
+   let sectionId="section"+formSetupEntry['Key'];
+   //if (!listName) is for debugSection
+   if (!listName){
+      listName="debugSection";
+   }
+	let fName='[generateSection/'+listName+']';
+	let sectionTitle=formSetupEntry['title'];	
+	let accessModeColumn=this.role+'Mode';
+	let accessMode=formSetupEntry[accessModeColumn];
+	//this will fix it for later use as well
+	this.print(fName+' title '+sectionTitle);
+
+	let tb=crfHTML.createTable(this.masterForm);
+	tb.className='t2';
+	let row=tb.insertRow();
+	let cell=crfHTML.createTblHeader(null,row);
+	cell.setAttribute("colspan","4");
+	cell.style.fontSize="20px";
+	cell.style.textAlign="center";
+   crfHTML.createTextNode(sectionTitle,null,cell);
+	cell=row.insertCell();
+	let input=crfHTML.createButton(null,cell);
+	input.value="Show";
+	input.id="toggle"+sectionId+"VisbilityButton";
+	input.onclick=function(){that.toggleVisibility(sectionId,input.id)};
+
+	let div=crfHTML.createDiv(sectionId,this.masterForm);
+	div.style.display="none";
+
+   //here divert for debugArea
+   if (listName=="debugSection"){
+      let debugArea=crfHTML.createTextArea(null,div);
+      debugArea.rows=10;
+      debugArea.cols=95;
+      debugArea.id=this.debugId;
+      return;
+   }
+
+	let additionalData=crfSetup.getAdditionalData(listName);
+	let divTable=crfHTML.createDiv(sectionId+"Table",null,div);
+	
+	if ("showFlag" in additionalData) {
+		additionalData.divName=sectionId+"SubDiv";
+		additionalData.divQueryName=sectionId+"SubDivList";
+
+		let div1=crfHTML.createDiv(additionalData.divName,null,div);
+		div1.style.display="none";
+		let div2=crfHTML.createDiv(additionalData.divQueryName,null,div1);
+
+	}
+	this.print(fName+" generate master table");
+
+	let writeMode=accessMode=="EDIT";	
+	let setup=this.getSetup(sectionId,listName,writeMode);
+   setup.setVariables=variableList.parseVariables(formSetupEntry['variableDefinition']);
+	
+
+   if	("isReview" in additionalData){
+      crfReviewSection.set(this);
+      let action=function(){crfReviewSection.CB();};
+		crfReviewSection.generateSection(listName,div.id,action);
+		return;	
+	}
+	//master table is unique per visit
+
+	
+	setup.unique=true;
+	this.generateTable(listName,divTable.id,additionalData,setup);
+	
+	this.print("generate master table: done");
+
+	let generateSubTable=true;
+	//generateSubTable equivalent to read/write access to section
+	if (accessMode != "EDIT")
+		generateSubTable=false;
+	
+	if (! ("showFlag" in additionalData) ) generateSubTable=false;
+	
+	if (generateSubTable){
+		let qName=additionalData.queryName;
+		let dName=additionalData.divName;
+		
+      let subsectionId='sub'+sectionId;
+		let xsetup=this.makeFullAccessSetup(subsectionId,qName);
+		//only set master query for additionalData
+		xsetup.masterQuery=listName;
+		//if (readonly) setup=readonlySetup(config);
+      xsetup.subTable=true;
+      this.generateTable(qName,dName,additionalData,xsetup);
+		//generateTable(formSetupEntry,qName,dName,additionalData,setup);
+	}
+
+	this.print("generate review");
+
+	let divReviewList=crfHTML.createDiv(sectionId+"ReviewList",null,div);
+	let divReview=crfHTML.createDiv(sectionId+"Review",null,div);
+
+
+	//assume we already have listId (content of config.setupQueryName is listId)
+	//we need listName also
+	//qconfig.queryName=config.setupQueryName;
+	this.generateReview(divReview.id,divReviewList.id,listName,accessMode);
+
+	if (accessMode!='GENERATE') return;
+	this.print('Adding generate button');	
+	//add generateButton
+	let divGenerateButton=crfHTML.createDiv(listName+"GenerateButton",null,div);
+	this.print('Adding generate button completed to here');	
+   let cb=function(){that.onGenerateQuery(listName);};
+	this.generateButton(divGenerateButton.id,'Generate','Generate '+listName,'onGenerateQuery',cb);
+	this.print(fName+' adding generate button completed');	
+}
+
+crfVisit.generateReview=
+function(divReviewId,divReviewListId, listName, accessMode){
+   let qMapInvert=crfSetup.invertMap(crfSetup.getMap('inputLists'));
+	let listId=qMapInvert[listName]
+
+	//listId is a number->should it be queryName?
+	
+   let fName='[generateReview]';
+   this.print(fName+" list "+listId+'/'+listName);
+	let reviewSetup=new Object();
+   reviewSetup.setVariables=new Object();
+	reviewSetup.readonlyFlag=function(vName){
+		if (vName=="queryName") return true; 
+		if (vName=="queryname") return true; 
+		if (vName=="ModifiedBy") return true;
+		return false;};
+	reviewSetup.addApply="Add Review";
+   reviewSetup.reviewTable=true;
+
+	let generateTableFlag=true;
+	let formStatus=crfData.getCrfEntry()['FormStatus'];
+	//COMMENTS allowed or not
+	//three levels of access: EDIT, COMMENT, READ
+	if (accessMode == "READ"){
+	//if (formStatus == "Approved" ){
+		delete reviewSetup.addApply;
+		reviewSetup.readonlyFlag=function(vName){return false;}
+		generateTableFlag=false;
+	}
+	
+	reviewSetup.filters=new Object();
+	reviewSetup.filters["crfRef"]=this.crfRef;
+   if (crfData.getCrfEntry()['parentCrf']){
+      let parentCrf=crfData.getCrfEntry()['parentCrf'];
+      reviewSetup.filters["crfRef"]=this.crfRef+";"+parentCrf;
+   }
+ 	reviewSetup.filters["queryName"]=listId;//entry in reviewComments list is queryname, all in small caps
+	//needs listName, in argument
+	
+	reviewSetup.getInputId=function(vName){return listName+"_add"+vName};
+	reviewSetup.divReviewListId=divReviewListId;
+	reviewSetup.isReview=true;	
+   this.addSetup(divReviewId,reviewSetup);
+
+   let msg="Review: divId: "+divReviewId;
+   msg+=" inputId: "+reviewSetup.getInputId;
+   this.print(msg);
+	
+	this.updateListDisplay(divReviewListId,"reviewComments",reviewSetup.filters,true);
+
+	if (! generateTableFlag) return;
+
+	
+   this.generateTable("reviewComments",divReviewId,new Object(),reviewSetup);
+}	
+
+//>>>>>>>>>>trigger visibility of additional lists
+
+crfVisit.setListVisibility=
+function(input,setup,readonlyFlag){
+	let fName="[setListVisibility/"+setup.queryName+"]";
+	this.print(fName);
+	let additionalData=crfSetup.getAdditionalData(setup.queryName);
+	
+	let x = crfHTML.getElement(additionalData.divName);
+	this.print(fName+": Div: "+x);
+	x.style.display="none";
+
+	let sText;
+	if (readonlyFlag) sText=input.innerText;
+	else sText=input.options[input.selectedIndex].text;
+			
+	this.print(fName+": Selected option text: "+sText);
+
+	if (sText == additionalData.showFlagValue){
+		let filters=new Object();
+		if ("filters" in additionalData) filters=additionalData.filters;
+		x.style.display = "block";
+		this.updateListDisplay(additionalData.divQueryName,
+			additionalData.queryName,filters,readonlyFlag);
+	}
+}
+
+//>>have list refresh when data is added (not optimal yet)
+//
+
+crfVisit.updateListDisplay=
+function(divName,queryName,filters,readonlyFlag){
+	//use Labkey.QueryWebPart to show list
+
+	let fName="[updateListDisplay]";
+
+   this.print(fName+": UpdateListDisplay: Query - "+queryName
+      +" div - "+divName);
+
+	if (divName=="NONE") return;
+
+	let crfRef=this.getCrfRef();
+	let div=crfHTML.getElement(divName);
+
+   this.print(fName+": generating WebPart: "+queryName);
+	
+	var qconfig=new Object();
+	qconfig.renderTo=divName;
+	//point to data container
+	qconfig.containerPath=this.getContainer('data');
+	qconfig.schemaName='lists'; 
+	qconfig.queryName=queryName;
+	qconfig.buttonBarPosition='top';
+	qconfig.filters=[];
+	for (f in filters){
+      let fType=LABKEY.Filter.Types.EQUAL;
+      this.print(fName+' filter ['+f+'] '+filters[f]+'/'+typeof(filters[f])+' ['+fType+']');
+     
+      if (variableList.isFilterList(filters[f])){
+         fType=LABKEY.Filter.Types.IN;
+      }
+		qconfig.filters.push(LABKEY.Filter.create(f, filters[f],fType));
+	}
+   let that=this;
+	qconfig.success=function(data){that.updateSuccess(data);};
+	qconfig.failure=function(errorInfo,options,responseObj){that.onFailure(errorInfo,options,responseObj);};
+	//show only print button
+	if (readonlyFlag){
+		qconfig.buttonBar=new Object();
+		qconfig.buttonBar.items=["print"];
+	}
+
+	LABKEY.QueryWebPart(qconfig);
+	
+}
+
+crfVisit.updateSuccess=
+function(data){
+	this.print("Update success");
+}
+
+//TODO: this should trigger a data refresh on section, ie populateData(field)
+crfVisit.toggleVisibility=
+function(sectionId,buttonName){
+	let fName='[toggleVisibility/'+sectionId+']';
+	this.print(fName);
+	let x = crfHTML.getElement(sectionId);
+	if (x.style.display === "none") {
+		//exclude non data sections (like debug)...
+		this.print(fName+': issuing setData(populateSection)');
+    		x.style.display = "block";
+		crfHTML.getElement(buttonName).value="Hide";
+      let that=this;
+		let cb=function(){that.populateSection(sectionId);};
+		crfData.setData(this.crfRef,cb);
+
+  	} else {
+    		x.style.display = "none";
+		   crfHTML.getElement(buttonName).value="Show";
+
+  	}
+}
+
+crfVisit.generateButton=
+function(divName,caption,label,callbackLabel,callback=null){
+	this.print("generateButtonX");
+	
+	let tb=crfHTML.createTable(divName);
+	tb.className="t2";
+	
+	let r1=tb.insertRow();
+   let th=crfHTML.createTblHeader(null,r1);
+	th.innerHTML=caption;
+	//*!*
+	let c2=r1.insertCell();
+	let i1=crfHTML.createButton(null,c2);
+	i1.value=label;
+	i1.style.fontSize="20px";
+   let that=this;
+   if (callback)
+      i1.onclick=callback;
+   else
+	   i1.onclick=function(){that[callbackLabel]();};
+	i1.id='button_'+callbackLabel;
+
+	let c1=r1.insertCell();
+	c1.setAttribute("colspan","1");
+	//this is only for saveReview?
+	c1.id=divName+'_reportField';
+	//c1.id=config.submitReportId;
+	
+}
+
+crfVisit.generateSubQuery=
+function(input, setup, readonlyFlag){
+	let fName="[generateSubQuery]";
+	if (setup.isReview) return;
+
+	if (!(setup.queryName in crfSetup.getAdditionalDataObject())){
+		this.print(fName+': no additionalData entry (probably a subquery)');
+		return;
+	}
+
+	let additionalData=crfSetup.getAdditionalData(setup.queryName);
+	if (!("showFlag" in additionalData))
+		return;
+
+	this.print(fName);
+		
+	let expId=setup.getInputId(additionalData.showFlag);
+	if (expId!=input.id) {
+		this.print(fName+": ignoring field "+input.id+"/"+expId);
+		return;
+	}
+
+	this.print(fName+": Setting onChange to "+input.id);
+	if (readonlyFlag)
+      return;
+
+   let that=this;
+	input.onchange=function(){that.setListVisibility(input,setup,readonlyFlag)};
+}
+
+
+//>>populate fields
+//
+//
+//split to field generation and field population
+//
+crfVisit.addFieldRow=
+function(tb,field,setup,additionalData){
+
+	let fName="[addFieldRow/"+setup.queryName+':'+field.name+']';
+
+	let vName=field.name;
+	let vType=field.type;
+	let isLookup=("lookup" in field);
+	this.print(fName+": ["+vName+"/"+vType+'/'+isLookup+"]");
+
+	let row=tb.insertRow();
+	let cell=crfHTML.createTblHeader(null,row);
+   cell.style.width='300px';
+	
+	let text = crfHTML.createTextNode(field.shortCaption,null,cell);
+		
+	
+	let input=null;
+   let colSpan="3";
+	let cell1=row.insertCell();
+	cell1.colSpan=colSpan;
+	let readonlyFlag=setup.readonlyFlag(vName);
+
+
+	//set the html input object
+	while (1){
+
+		if (readonlyFlag){
+         input=crfHTML.createLabel('Loading',null,cell1);
+			break;
+		}
+	
+
+		//lookup
+		if (isLookup){
+	      let lookup=field["lookup"];
+	      //get all values from config.formConfig.lookup[X]
+	      let lObject=crfData.getLookup(lookup.queryName);
+         input = crfHTML.createSelect(lObject.LUT,null,cell1);
+
+			break;
+		}
+
+		//date
+	   if (vType=="date"){
+         input = crfHTML.createDate(null,cell1);
+			break;
+		}
+
+		//string
+		if (vType=="string"){
+			//we have to make sure UNDEF is carried to below
+			//since we are adapting file to either show
+			//current file or allow user to select a file
+			//
+			//TODO change this so one can always select file
+			//but also show the selected file
+
+			if(vName.search("reviewComment")>-1){
+				input = crfHTML.createTextArea(null,cell1);
+				input.cols="65";
+				input.rows="5";
+				break;
+			}
+
+			input=crfHTML.createTextInput(null,cell1);
+			
+			if (vName.search('_file_')<0) break;
+			cell1.setAttribute('colspan',"1");
+			let cell2=row.insertCell();
+			cell2.setAttribute('colspan',"2");
+			let input1=crfHTML.createFileInput(null,cell2);
+			input1.id=setup.getInputId(vName)+'_file_';
+			break;
+				
+		}
+
+
+		if (vType=="float"){
+			input = crfHTML.createTextInput(null,cell1);
+			break;
+		}	
+		
+		
+		if (vType=="boolean"){
+			input = crfHTML.createCheckbox(null,cell1);
+			this.print("Creating checkbox");
+			break;
+		}
+		break;
+	}
+	
+   input.id=setup.getInputId(vName);
+   this.print(fName+': adding element '+input.id);
+   this.print(fName+': listing element '+crfHTML.getElement(input.id));
+	
+
+	//connect associated list
+	this.generateSubQuery(input,setup,readonlyFlag);	
+
+	if (readonlyFlag) {
+		this.print(fName+': exiting(readonlyFlag)');
+		return;
+	}
+	
+}
+
+crfVisit.addSpecialFieldRows=
+function(tb,specFieldSetup,setup){
+   //tb is the table, specFieldSetup is a row from the table where special fields are being setup
+   //the first column is fieldUID, which is a colon joined amalgation of queryName:fieldName
+   let fieldUID=specFieldSetup["fieldUID"];
+   let x=fieldUID.split(':');
+   let fieldName=x[1];
+   let fName="[addSpecialFieldRow/"+fieldUID+"]";
+   let q=variableList.parseVariables(specFieldSetup['actionParameters']);
+   this.print(fName);
+   let type=specFieldSetup['actionType'];
+   this.print(fName+' type '+type);
+   if (type=='textArea' || type=="textAreaFromVariableDefinition"){
+      let row=tb.insertRow();
+      let cell1=row.insertCell();
+      cell1.colSpan="4";
+      cell1.style.textAlign="justify";
+      cell1.style.padding="10px";
+      cell1.style.backgroundColor="#e0e0e0";
+      cell1.innerText=q['description'];
+      if (type!='textAreaFromVariableDefinition') return;
+      let varName=q['varName'];
+      //get the value. But sometimes and particularly in this case, there are two rows for the same query
+      //one should rely on formSetup variable definition
+      let value=setup.setVariables[varName];
+      let code=q['pattern'];
+      code=code.replace(varName,value);
+      this.print(fName+' using ['+varName+'] '+value+' pattern '+q['pattern']+' code '+code);
+      cell1.innerText=q[code];
+   
+   }
+   //copyCrfEntry in populateSpecialField
+   if (specFieldSetup['actionType']=='generationObject'){
+      //only in EDIT mode!!
+      let ro=setup.readonlyFlag(fieldName);
+      if (ro) return;
+      generateRegistration.set(this);
+      q['setup']=setup;
+      let gc=generateRegistration.getObject(q,setup.getInputId(fieldName));
+      let that=this;
+      let action=function(){that.doNothing();};
+      if ('mailRecipient' in q){
+         gc.callback=function(data){that.sendEmail(data,q['mailRecipient'],action,q['subject']);};
+      }
+      else 
+         gc.callback=function(data){that.doNothing();};
+      if ("addData" in q){
+         vars=q["addData"].split(',');
+         gc.addData=new Array();
+         for (let v in vars){
+            let s=vars[v]
+            //variable name can be written as A/B where A is the name in addData and B is the variable name in crfEntry
+            //useful for mocking up crfId from daughter crf-s such as registration
+            let sArray=s.split('/');
+            let sTarget=sArray[0];
+            let sSource=sArray[sArray.length-1];
+            gc.addData[sTarget]=crfData.getCrfEntry()[sSource];
+            this.print(fName+" addData ["+sTarget+"]: "+gc.addData[sTarget]);
+         }
+      }
+      let row=tb.insertRow();
+      let cell=crfHTML.createTblHeader(null,row);
+      crfHTML.createTextNode("Automatic ID generator",null,cell);
+      let cell1=row.insertCell();
+      cell1.colSpan="3";
+      let b=crfHTML.createButton(null,cell1);
+      b.id="generateIdButton";
+      b.onclick=function(){generateRegistration.execute(gc);};
+      b.value="Generate ID";
+
+   }
+}
+
+crfVisit.populateFieldRow=		
+function(entry,field,setup){
+	this.populateField(entry,field,setup);
+	this.populateSubQuery(entry,field,setup);
+   this.populateSpecialFields(entry,field,setup);
+}
+
+crfVisit.populateSubQuery=
+function(entry,field,setup){
+	let fName='[populateSubQuery/'+setup.queryName+':'+field.name+']';
+	if (setup.isReview) return;
+	
+	if (!(setup.queryName in crfSetup.getAdditionalDataObject())){
+		let msg=fName+': no additionalData entry for '+setup.queryName;
+		msg+=' (probably a subquery)';
+		this.print(msg);
+		return;
+	}
+	//find if field is connected to a sub array
+	//find queryName
+	//
+	let additionalData=crfSetup.getAdditionalData(setup.queryName);
+	this.print(fName);
+	//let flag=additionalData.showFlag;
+	
+	if (!("showFlag" in additionalData)) return;
+	let eId=setup.getInputId(additionalData.showFlag);
+	let id=setup.getInputId(field.name);
+	
+	if (eId!=id) {
+		this.print(fName+": ignoring field "+id+"/"+eId);
+		return;
+	}
+	
+	this.print(fName+': id '+id);
+	//hard to estimate readonlyFlag
+	//
+	let input=crfHTML.getElement(id);
+	let eType=input.nodeName.toLowerCase();
+	let readonlyFlag=eType!="select";
+	this.setListVisibility(input,setup,readonlyFlag);
+
+}
+
+crfVisit.clearField=
+function(field,setup){
+   let foo=new Object();
+   this.populateField(foo,field,setup);
+}
+
+crfVisit.populateField=
+function(entry,field,setup){
+
+	let vName=field.name;
+	let fName='[populateFieldName/'+vName+']';
+
+	let varValue="UNDEF";
+
+	//if (vName in setup.filters) varValue=setup.filters[vName];
+	if (vName in entry) varValue=entry[vName];
+	//if part of the filter, set it to value
+	if (vName in setup.filters) varValue=setup.filters[vName];
+	
+	let isLookup=("lookup" in field);
+	
+	this.print(fName+' v='+varValue+'/'+isLookup+' ['+
+		setup.getInputId(field.name)+']');
+	
+	let vType=field.type;
+	let id=setup.getInputId(vName);
+	let input=crfHTML.getElement(id);
+
+		
+	//date
+	if (vType=="date"){
+		if (varValue=="UNDEF") varValue=new Date();
+		else varValue=new Date(varValue);
+	}
+	
+	//lookup for readonly
+	if (isLookup && varValue!="UNDEF"){
+		let lookup=field["lookup"];
+		//get all values from config.formConfig.lookup[X]
+		let lObject=crfData.getLookup(lookup.queryName);
+		varValue=lObject.LUT[varValue];
+	}
+
+	this.print('Element: '+id+'/'+input);
+	//figure out the element type
+	let eType=input.nodeName.toLowerCase();
+	this.print('Element type: '+eType);
+
+	//change varValue for printing
+	if (varValue=="UNDEF") varValue="";
+	//HTMLTextArea, createElement(textArea)
+	if (eType==="textarea"){
+		input.value=varValue;
+		return;
+	}
+	//Text, createTextNode
+	if (eType==="#text"){
+		input.nodeValue=varValue;
+		return;
+	}
+	//HTMLLabelElement, createElement('label')
+	if (eType==="label"){
+		input.innerText=varValue;
+		return;
+	}
+
+	//HTMLSelectElement, createElement('select')
+	if (eType==="select"){
+		input.selectedIndex=0;
+		for (let i=0;i<input.options.length;i++){
+			let v=input.options[i].text;
+			if (v!=varValue) continue;
+			input.selectedIndex=i;
+			break;
+		}
+		return;
+	}
+
+	if (eType!="input"){
+		this.print('Unknown type: '+eType+' encountered, igonring');
+		return;
+	}
+	
+	//HTMLInputElement
+	let type=input.type;
+
+	if (type=="date"){
+		input.valueAsDate=varValue;
+		return;
+	}
+	//string,float
+	if (type=="text"){
+		input.value=varValue;
+		return;
+	}
+	//boolean
+	if (type=="checkbox"){
+		input.checked=varValue;
+		return;
+	}
+	this.print('Unknown input type: '+type+'. Ignoring.');
+}
+
+crfVisit.populateSpecialFields=
+function(entry,field,setup){
+   let fName='[populateSpecialFields]';
+
+   let fieldUID=setup.queryName+':'+field.name;
+   let specialFields=crfSetup.getEntryMap('specialFields:fieldUID');
+
+   if (!(fieldUID in specialFields)) return;
+
+   let specFieldSetup=specialFields[fieldUID];
+   //q is not used by copyCrfEntry, keeping it here for future reference
+   let q=variableList.parseVariables(specFieldSetup['actionParameters']);
+   let type=specFieldSetup['actionType'];
+   if (type=='copyCrfEntry'){
+      let el=crfHTML.getElement(setup.getInputId(field.name));
+      let varName=field.name;
+      if ("varName" in q)  varName=q["varName"];
+      let id=crfData.getCrfEntry()[varName];
+      el.value=id;
+      this.print(fName+' specialFields ['+field.name+'] '+id+'/'+el.value);
+   }
+
+
+}
+
+crfVisit.populateTable=
+function(listName,writeMode,setup){
+//function populateTable(formSetupEntry){
+	//let listName=config.formConfig.queryMap[formSetupEntry['queryName']];
+	//let accessMode=config.formConfig.operator+'Mode';
+	//let writeMode=formSetupEntry[accessMode]=='EDIT';
+
+	let fName='[populateTable/'+listName+']';
+
+   //should contain formSetup key
+   
+
+	
+	//data snapshot
+	let fQuery=crfData.getQuerySnapshot(listName);
+   let queryLayout=crfData.getQueryLayout(listName);
+
+	//here I assume that listName was parsed during setDataLayout and setData 
+	//so that rows was set (even if they are empty)
+	this.print(fName+"]: nrows "+fQuery.rows.length);
+	
+	let entry=this.selectEntry(fQuery.rows,setup);
+	
+   if (!entry) entry=new Object();
+	let fields=queryLayout.fields;
+		
+	for (f in fields){	
+		let field=fields[f];
+		//each field is a new row
+		this.print(fName+": Adding field: "+f+'/'+field.name+' hidden: '+field.hidden+' type:'+field.type);
+		if (field.hidden) continue;
+		if (field.name=="crfRef") continue;
+      if (field.name in setup.setVariables) continue;
+		this.populateFieldRow(entry,field,setup);
+		
+	}
+
+}
+
+crfVisit.generateTable=
+function(listName,divName,additionalData,setup){
+	let fName="[generateTable/"+listName+"]";	
+	this.print(fName);
+
+	//is listName and setup.queryName a duplicate of the same value
+	this.print(fName+': setup.queryName '+setup.queryName);	
+	//assume data is set in config.formConfig.dataQueries[data.queryName].rows;
+   let populateData=true;
+   if ("subTable" in setup){
+      this.print(fName+" is subTable");
+      populateData=false;
+   }
+
+	let entry=new Object();
+
+
+
+	//data snapshot
+	let fQuerySnapshot=crfData.getQuerySnapshot(listName);
+   let queryLayout=crfData.getQueryLayout(listName);
+	//here I assume that listName was parsed during setDataLayout and setData 
+	//so that rows was set (even if they are empty)
+	this.print(fName+": Nrows "+fQuerySnapshot.rows.length);
+	
+	if (fQuerySnapshot.rows.length>0)
+		entry=fQuerySnapshot.rows[0];
+
+   
+   if ("reviewTable" in setup){
+      entry['reviewComment']='';
+      delete entry["ModifiedBy"];
+   }
+	
+	let tb=crfHTML.createTable(divName);
+	tb.className="t2";
+
+	//this are the fields (probably constant)
+	let fields=queryLayout.fields;
+		
+	for (f in fields){
+		let field=fields[f];
+      let fieldUID=listName+":"+field.name;
+		//each field is a new row
+		this.print(fName+": Adding field: "+f+'/'+field.name+' ('+fieldUID+').');
+      //unique name
+		if (field.hidden) continue;
+		if (field.name=="crfRef") continue;
+      if (field.name in setup.setVariables) continue;
+		this.addFieldRow(tb,field,setup,additionalData);
+		if (populateData) this.populateFieldRow(entry,field,setup);
+      let specialFields=crfSetup.getEntryMap('specialFields:fieldUID');
+      if (fieldUID in specialFields){
+         let specFieldSetup=specialFields[fieldUID];
+         this.addSpecialFieldRows(tb,specFieldSetup,setup);
+      }
+
+		
+	}
+   //finish of if apply button is not required
+	if (!("addApply" in setup)) {
+		this.print(fName+"populateTable: done");
+		return;
+	}
+	
+	let row=tb.insertRow();
+
+	let th=crfHTML.createTblHeader(null,row);
+	th.innerHTML=setup.addApply; 
+	let cell=row.insertCell();
+	//cell.setAttribute("colspan","2");
+	let input=crfHTML.createButton(null,cell);
+	input.value=setup.addApply;
+	let cell1=row.insertCell();
+	cell1.setAttribute("colspan","2");
+	cell1.id=setup.getInputId("rerviewLastSave");
+	cell1.innerHTML="No recent update";
+	//saveReview is a generic name for saving content of the html page to a list entry
+   let that=this;
+	input.onclick=function(){that.saveReview(listName,cell1.id,setup)};
+}	
+
+crfVisit.setEntryFromElement=
+function(entry,elementId, field){
+   //set value to entry from element using representation (field) from labkey
+   //
+   //
+   
+   let fName='setEntryFromElement';
+
+   let el=crfHTML.getElement(elementId);
+				
+   if (!el) {
+      this.print(fName+" element: "+elementId+" not found");
+      return;
+   }
+   this.print(fName+" element: "+elementId);
+      
+
+   let vName=field.name;
+   let vType=field.type;
+
+   let eType=el.nodeName.toLowerCase();
+
+   if (eType==="select"){
+      entry[vName]=el.options[el.selectedIndex].value;
+      return;
+   }
+
+   if (eType==="td"){
+      entry[vName]=el.innerText;
+      return;
+   }
+   
+   if (vType=="date"){
+      let date=el.valueAsDate;
+      if (!date) return;
+
+      date.setUTCHours(12);
+      entry[vName]=date.toString();
+      this.print(fName+" setting date to "+entry[vName]);
+      return;
+   }
+
+   if (vType=="string"){
+      entry[vName]=el.value;
+      
+      if (vName.search('_file_')<0) 
+         return;
+      
+      //upload file
+      let id1=elementId+'_file_';
+      let input1=crfHTML.getElement(id1);
+      this.print(fName+' attachment field: '+input1.value);
+      //entry[vName]=el.files[0].stream();
+      let ctx=new Object();
+      ctx['dirName']='consent';
+      ctx['ID']=entry['crfRef'];
+      //should point to data container
+      ctx['project']=getContainer('data');
+      //need ID->crf!
+      //assume crfRef will get set before this
+      //element is encountered
+      this.uploadFile(input1,ctx);
+      let fv=el.value;
+      let suf=fv.split('.').pop();
+      entry[vName]=entry['crfRef']+'.'+suf;
+      return;
+      
+   }	
+   if (vType=="float" || vType=="int"){
+      entry[vName]=el.value;
+      
+      if (vName=="queryName") {
+         this.print(fName+' parsing queryName: '+el.innerText);
+         let qMapInverse=crfSetup.invertMap(crfSetup.getMap('inputLists'));
+         entry[vName]=qMapInverse[el.innerText];
+         //use queryMap lookup
+      }
+      return;
+   }	
+   if (vType=="boolean"){
+      entry[vName]=el.checked;
+      return;
+	}
+   return;
+}
+
+crfVisit.selectEntry=
+function(fRows,setup){
+   let fName='[selectEntry]';
+
+   if (!("unique" in setup)) return null;
+   if (fRows.length==0) return null;
+
+   keys=Object.keys(setup.setVariables);
+   
+   if (keys.length==0)
+      return fRows[0];
+   
+   for (let i=0;i<fRows.length;i++){
+      for (let v in setup.setVariables){
+         this.print(fName+' row '+i+' ['+v+'] '+fRows[i][v]+'/'+setup.setVariables[v]);
+         if (fRows[i][v]==setup.setVariables[v]){
+            this.print(fName+' using '+i);
+            return fRows[i];
+         }
+      }
+   }
+   return null;
+
+}
+
+crfVisit.saveReview=
+function(queryName,elementId,setup){
+	//loads any queryName
+
+	let debug=true;
+   let fName='[saveReview/'+queryName+']';
+	this.print(fName+" elementId "+elementId);
+
+
+   let unique=("unique" in setup);
+	
+   //data snapshot
+	let fQuerySnapshot=crfData.getQuerySnapshot(queryName);
+   let nRows=fQuerySnapshot.rows.length;
+   
+   //data layout 
+	let queryLayout=crfData.getQueryLayout(queryName);
+
+   //determine mode based on entry uniqueness and presence of data
+   let entry=this.selectEntry(fQuerySnapshot.rows,setup);
+   let mode='update';
+
+   if (!entry){
+      entry=new Object();
+      mode='insert';
+   }
+   this.print(fName+' unique '+unique+' mode '+mode+' nRows '+nRows);
+
+	entry.crfRef=this.getCrfRefData();
+
+	this.print(fName+" set crfRef="+entry.crfRef);
+
+
+	let fields=queryLayout.fields;
+	for (f in fields){
+
+		let field=fields[f];
+		this.print(fName+" saveReview field: "+field.name);
+		if (field.hidden) continue;
+      if (field.name in setup.setVariables){
+         entry[field.name]=setup.setVariables[field.name];
+         continue;
+      }
+		
+		let vName=field.name;
+		let vType=field.type;
+
+		this.print(fName+" vType: "+vType);
+		
+		if (vName=="crfRef") continue;
+		//need to save queryName for reviewComments
+		
+		let eId=setup.getInputId(vName);
+      //copy values from form to entry
+      this.setEntryFromElement(entry,eId,field,setup);
+
+      //clear field value
+      if (!unique) this.clearField(field,setup);
+
+	}
+   let that=this;
+	let action=function(data){that.updateLastSavedFlag(data,setup,elementId)};
+
+   runQuery.modifyRows(mode,'lists',queryName,[entry],action,crfSetup.getContainer('data'));
+
+}
+
+crfVisit.updateLastSavedFlag=
+function(data,setup,elementId){
+   let fName='[updateLastSavedFlag]';
+   this.print(fName+" update last saved flag to "+elementId);
+	let el=crfHTML.getElement(elementId);
+	let dt=new Date();
+	el.innerHTML="Last saved "+dt.toString();
+	if (data.queryName=="reviewComments"){
+		this.updateListDisplay(setup.divReviewListId,"reviewComments",setup.filters,true);
+	}	
+	//refresh stored data!
+	let writeMode=!setup.readonlyFlag();
+   let that=this;
+   let cb=function(){that.populateTable(data.queryName,writeMode,setup);};
+	if ("unique" in setup)
+		crfData.setData(this.crfRef,cb);
+	if ("masterQuery" in setup){
+		let ad=crfSetup.getAdditionalData(setup.masterQuery);
+		this.print('Updating list display: '+setup.queryName+'/'+ad.queryName);
+		this.updateListDisplay(ad.divQueryName,ad.queryName,ad.filters,false);
+   }
+}
+
+//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+//generic form status switch statementes
+
+crfVisit.changeFormStatusAndNotify=
+function(actionName){
+   let fName='[changeStatusAndNotify]';
+   this.print(fName);
+   let targetStatus=crfSetup.getTargetStatus(actionName);
+   let targetRecipient=crfSetup.getTargetRecipient(actionName);
+   let actionSettings=crfSetup.getActionSettings(actionName);
+   let action=new Object();
+   action.name=actionName;
+
+   let finalStep=function(){that.redirect();};
+	if (variableList.hasVariable(actionSettings,"finalStep")){
+      this.print(fName+' adjusting finalStep');
+		//set to doNothing to remain on submit window
+		if (actionSettings.finalStep=="doNothing"){
+			finalStep=function(){that.doNothing();};
+		}
+	}
+   this.print(fName+' action '+actionName+' targetStatus '+targetStatus);
+   let that=this;	
+   action.cb=function(data){that.sendEmail(data,targetRecipient,finalStep,actionName);}
+   this.updateFlag(targetStatus,action);//Approved
+}
+
+crfVisit.updateFlag=
+function(flag,action){
+	let fName='[updateFlag]';
+
+	let entry=crfData.getCrfEntry();
+	entry.FormStatus=flag;
+	let uId=LABKEY.Security.currentUser.id;
+	entry[this.role]=uId;
+
+	this.print(fName+': Form: '+entry.Form);
+	this.print(fName+": set form status to "+entry.FormStatus);
+
+   let that=this;	
+	let cb=function(data){that.completeWithFlag(data,action);};
+   runQuery.modifyRows('update','lists','crfEntry',[entry],cb,crfSetup.getContainer('data'));
+		
+}
+
+
+crfVisit.completeWithFlag=
+function(data,action){
+	let fName='[completeWithFlag]';
+	this.print(fName+': nrows '+data.rows.length);
+
+	let fentry=data.rows[0];
+	this.print(fName+': form status '+fentry.FormStatus);
+	this.print(fName+': form '+fentry.Form);
+
+	let crfStatus=crfData.createCrfStatus(fentry);
+	crfStatus.operator=this.role;
+	crfStatus.action=action.name;
+   let that=this;
+   let cb=function(){that.doNothing();};
+   if (action.cb) cb=action.cb;
+
+   runQuery.insertRows('lists','crfStatus',[crfStatus],cb,crfSetup.getContainer('data'));
+
+}
+
+//******************************************upload to database *********************
+
+crfVisit.onDatabaseUpload=
+function(){
+   let actionName='onDatabaseUpload';
+	let fName='['+actionName+']';
+	this.print(fName);
+   let pM=this.getIdManager();
+   let participantId=participantIdManager.getParticipantIdFromCrfEntry(pM);
+  
+   let that=this;
+   let crfRef=this.crfRef;
+   
+   //load lists and study data
+   //check what needs to be updated and upload
+   //a (reverse) sequence of functions
+   let completeUpload=function(){that.changeFormStatusAndNotify(actionName);};
+   let uploadData=function(){crfData.uploadData(participantId,crfRef,completeUpload);};
+   let loadStudy=function(){crfData.setData(crfRef,uploadData,'study');}
+   crfData.setData(crfRef,loadStudy,'lists');
+}
+
+
+//*************************update for further review *************************
+
+crfVisit.onUpdateForReview=
+function(){
+   let actionName='onUpdateForReview';
+   this.changeFormStatusAndNotify(actionName);
+}
+
+//************************************************ submit *******************************************
+
+crfVisit.onSubmit=
+function(){
+	//update list storage and change status
+
+	this.hideErr();
+	this.clearErr();
+	this.printErr("onSubmit");
+   let that=this;
+   let actionName='onSubmit';
+   let action=function(){that.verifyData(actionName);};
+	crfData.setData(this.crfRef,action);
+	
+
+
+}
+
+crfVisit.verifyData=
+function(actionName){
+	let fName='[verifyData/'+actionName+']';
+   let qList=crfData.getActiveQueries();
+   let that=this;
+   let doNothing=function(data){that.doNothing();};
+
+   let pM=this.getIdManager();
+   let fieldName=participantIdManager.getCrfEntryFieldName(pM);
+   let setId=crfData.getCrfEntry()[fieldName];
+
+   this.print(fName+' crfEntry ['+fieldName+'] '+crfData.getCrfEntry()[fieldName]);
+   
+   if (!setId){
+      this.printErr('Missing ID !');
+      return false;
+   }
+
+	for (let qId in qList){
+      let entry=qList[qId];
+      let q=entry['queryName'];
+		let qData=crfData.getQuerySnapshot(q);
+		if (q=="reviewComments") continue;
+		//copy snapshot to history
+      if (qData.rows.length==0){
+         this.print(fName+' no rows for '+q);
+      }
+      else
+		   runQuery.insertRows('lists',q+'History',qData.rows,doNothing,this.getContainer('data'));
+		//if it doesn't have additionalData, it is a sub query
+		if (!(q in crfSetup.getAdditionalDataObject())){
+			continue;
+		}
+		if (qData.rows.length<1){
+			this.printErr('Missing entry for query '+q);
+			return false;
+		}
+	}
+	//this is necessary only for Generated to Generation completed step
+	let actionSettings=crfSetup.getActionSettings(actionName);	
+	if (variableList.hasVariable(actionSettings,"updateRegistration")){
+      //if updateRegistrationFormId is set, only update when submit is used on that form
+      if (variableList.hasVariable(actionSettings,"updateRegistrationFormId")){
+         let formId=crfData.getCrfEntry()['Form'];
+         if (actionSettings['updateRegistrationFormId']==formId)
+            this.updateRegistration();
+      }
+      else
+		   this.updateRegistration();
+	}
+   this.changeFormStatusAndNotify(actionName);
+
+}
+
+crfVisit.getEmail=
+function(recipientCode){
+
+	this.print('getEmail w/'+recipientCode);
+	let recipients=new Array();
+	let typeTo=LABKEY.Message.recipientType.to;
+	let create=LABKEY.Message.createRecipient;
+   let userMap=crfSetup.getEntryMap('users');
+	let currentUser=userMap[LABKEY.Security.currentUser.id];
+	let currentSite=this.siteEntry;
+	let formCreator=userMap[crfData.getCrfEntry()['UserId']];
+
+
+	let userRows=crfSetup.getRows('users');
+	let parentUser=null;
+   if ("parentCrfData" in crfSetup){
+		let parentCrf=crfSetup.getRows('parentCrfData');
+		parentUser=userMap[parentCrf.rows[0].UserId];
+	}
+	
+
+	let recipientCategories=recipientCode.split(',');
+	for (let i=0;i<recipientCategories.length;i++){
+
+		let recipient=recipientCategories[i];
+		this.print('Checking '+recipient);
+		if (recipient=='crfEditor'){
+			this.print('Adding :'+formCreator.Email);
+			recipients.push(create(typeTo,formCreator.Email));
+			if (!parentUser) continue;
+			this.print('Adding :'+parentUser.Email);
+			recipients.push(create(typeTo,parentUser.Email));
+			continue;
+		}
+		//Monitor or Sponsor
+		let fList=recipient+'s';
+		let fRows=crfSetup.getRows(fList);
+		for (let i=0;i<fRows.length;i++){
+			this.print('Checking '+fRows[i].User+'/'+fRows[i].Site);
+			if (fRows[i].Site!=currentSite.siteNumber) continue;
+         let targetUser=userMap[fRows[i].User];
+			recipients.push(create(typeTo,targetUser.Email));
+		}
+	}
+
+	return recipients;
+}
+
+crfVisit.sendEmail=
+function(data,recipient='crfEditor',cb=null,subj='Form submitted'){
+
+	this.print('sendEmail; recipient: '+recipient);
+   let that=this;
+
+   if (!cb)
+      cb=function(){that.redirect();};
+
+   let cvar='sendEmail';
+	let cval=crfSetup.getSettings(cvar);
+	if (cval){
+
+		this.print(cvar+' set to '+cval);
+		if (cval=='FALSE'){
+			this.print('Skipping sending emails');
+			cb();
+			return;
+		}
+	}
+	if (recipient==null){
+      this.print('Skipping sending emails w/ no recipients');
+      cb();
+      return;
+   }
+
+
+	this.print('send email '+data.rows.length);
+	let crf=data.rows[0]['entryId'];
+	let formId=data.rows[0]['Form'];
+	let link=LABKEY.ActionURL.getBaseURL();
+	link+=LABKEY.ActionURL.getContainer();
+	link+='/crf_tecant-visit.view?';
+	link+='entryId='+crf;
+	link+='&formId='+formId;
+	link+='&role='+recipient;
+
+	//debug
+	let recipients=this.getEmail(recipient);
+	//from crfManagers list
+	
+	let typeHtml=LABKEY.Message.msgType.html;
+	let typePlain=LABKEY.Message.msgType.plain;
+	let msg1=LABKEY.Message.createMsgContent(typePlain,link);
+
+	//let cb=doNothing;
+	//let cb=redirect;
+	LABKEY.Message.sendMessage({
+		msgFrom:'labkey@fmf.uni-lj.si',
+		msgSubject:subj,
+		msgRecipients:recipients,
+		msgContent:[msg1],
+		success: cb
+	});
+
+}
+
+crfVisit.hideErr=
+function(){
+	let el=crfHTML.getElement("errorDiv");
+	el.style.display="none";
+}
+
+crfVisit.clearErr=
+function(){
+	let el=crfHTML.getElement("errorTxt");
+	el.value="";
+}
+
+crfVisit.showErr=
+function(){
+	let el=crfHTML.getElement("errorDiv");
+	el.style.display="block";
+}
+
+crfVisit.printErr=
+function(msg){
+	this.showErr();
+	el=crfHTML.getElement("errorTxt");
+	el.style.color="red";
+	el.value+="\n"+msg;
+}
+
+
+//**************************************************
+//
+crfVisit.onRemoveCRF=
+function(){
+   let fName='[onRemoveCRF]';
+   let crfRef=this.crfRef;
+   this.print(fName+' starting loop');
+   let actionName='onRemoveCRF';
+   let that=this;
+   let notify=function(){that.changeFormStatusAndNotify(actionName);};
+   //let removeCrfEntry=function(){crfData.removeCrfEntry(notify);}; 
+   let removeData=function(){crfData.removeData(notify);};
+   let setStudyData=function(){crfData.setData(crfRef,removeData,'study');};
+   let action=setStudyData;
+   let actionSettings=crfSetup.getActionSettings(actionName);
+   if (variableList.hasVariable(actionSettings,'removeWithParentCrf')){
+      //if some query needs to be deleted with parent crf, insert this as an intermediate action
+      let q=actionSettings['removeWithParentCrf'];
+      let parentCrf=crfData.getCrfEntry()['parentCrf'];
+      if (parentCrf){
+         let setParentStudyData=function(){crfData.setDataForQuery(q,parentCrf,removeData,'study');};
+         let setStudyData1=function(){crfData.setData(crfRef,setParentStudyData);}
+         action=function(){crfData.setDataForQuery(q,parentCrf,setStudyData1);};
+      }
+   }
+   crfData.setData(crfRef,action);
+}
+
+
+crfVisit.redirect=
+function(){
+	let debug=false;
+	let formUrl="begin";
+	let params=new Object();
+	params.name=formUrl;
+	params.pageId="CRF";
+
+	//points to crf container
+	let containerPath=crfSetup.getContainer('CRF');
+        
+	// This changes the page after building the URL. 
+	//Note that the wiki page destination name is set in params.
+        
+	var homeURL = LABKEY.ActionURL.buildURL(
+			"project", formUrl , containerPath, params);
+        this.print("Redirecting to "+homeURL);
+	if (debug) return;	 
+	window.location = homeURL;
+
+}
+
+//master section, entry point from html files
+crfVisit.generateMasterForm=
+function(){
+   let that=this;
+   let action=function(){that.setFormConfig();}
+   this.init(action);
+}
+
+
+//helper function to set basic parameters on web page 
+//(fields defined in html file)
+crfVisit.populateBasicData=
+function(){
+
+   let staticData=new Object();
+   let titles=new Object();
+   staticData['version']='0.16.3'	
+   titles['version']='Software version';
+   let varRows=crfSetup.getRows('crfStaticVariables');
+   for (let i=0;i<varRows.length;i++){
+      let vName=varRows[i].staticVariable;
+      let val=crfData.getCrfEntry()[vName];
+      if (val==undefined) continue;
+      staticData[vName]=val;
+      titles[vName]=varRows[i].Title;
+   }
+	staticData['investigatorName']=this.userEntry['DisplayName'];
+   titles['investigatorName']='Investigator';
+   staticData['email']=this.userEntry['Email'];
+   titles['email']='Email';
+   staticData['siteName']=this.siteEntry['siteName'];
+   titles['siteName']='Site';
+   staticData['sitePhone']=this.siteEntry['sitePhone'];
+   titles['sitePhone']='Telephone(site)';
+
+   for (f in staticData){
+      this.addStaticData(f,titles[f],staticData[f]);
+   }
+   let formId=crfData.getCrfEntry()['Form'];
+   let formEntry=crfSetup.getEntryMap('dataForms')[formId];
+   crfHTML.getElement('formTitle').innerText=formEntry['formName'];
+}
+
+crfVisit.addStaticData=
+function(f,title,value){
+   let el=crfHTML.getElement(f);
+
+   //populate only
+   if (el!=undefined){
+      el.innerText=value;
+      return;
+   }
+   
+   //add row to table if element cannot be found
+   let table=crfHTML.getElement('staticTable');
+   let row=table.insertRow();
+   let cell=row.insertCell();
+   cell.innerText=title;
+   let cell1=row.insertCell();
+   cell1.id=f;
+   cell1.style.fontWeight='bold';
+
+   //populate
+   cell1.innerText=value;
+}
+
+//come here after the layout is read from labkey page
+//
+crfVisit.generateErrorMsg=
+function(msg){
+	let txt=crfHTML.createParagraph(msg,this.masterForm);
+	this.generateButton("submitDiv",'Exit','Exit','redirect');
+}
+
+crfVisit.checkPermissions=
+function(){
+	//check if user has permission on the form
+   let userMap=crfSetup.getEntryMap('users');
+	let currentUser=userMap[LABKEY.Security.currentUser.id];
+	let currentSite=this.siteEntry;
+	let formCreator=userMap[crfData.getCrfEntry()['UserId']];
+	let formCreatorId=formCreator.UserId;
+
+
+
+	//let formSite=config.formConfig.crfEntry.Site;
+	let fList=this.role+'s';
+	let fRows=crfSetup.getRows(fList);
+	//let currentSiteId=-1;
+	
+	//depending on operator mode, we should decide what is right
+	let operator=this.role;
+	if (this.role=='crfEditor'){
+		//editor can only edit its own forms
+		if (currentUser.UserId!=formCreatorId){
+			let msg='User '+currentUser.DisplayName;
+			msg+=' has no permission on this form';
+			this.generateErrorMsg(msg);
+			return false;
+		}
+      return true;
+	}
+	if (operator=='crfMonitor' || operator=='crfSponsor'){
+		//monitor can look at forms based on his site
+		//find monitor line
+		let operatorSites=new Array();
+		for (let i=0;i<fRows.length;i++){
+			if (fRows[i].User!=currentUser.UserId) continue;
+			operatorSites.push(fRows[i].Site);
+		}
+		this.print('operator Site: '+operatorSites.length);
+		if (operatorSites.length==0){
+			let msg='User '+currentUser.DisplayName;
+			msg+=' is not a '+operator;
+			this.generateErrorMsg(msg);
+			return false;
+		}
+      //implementation of currentSite in operatorSites
+      if (!operatorSites.includes(currentSite.siteNumber)){
+
+			let msg='User '+currentUser.DisplayName;
+			msg+=' is not a '+operator+' for site ';
+			msg+=currentSite.siteName+'('+currentSite.siteNumber+')';
+			msg+='/'+operatorSites.join(',');
+			this.generateErrorMsg(msg);
+			return false;
+		}
+	}
+
+
+	this.print('User '+currentUser.DisplayName+'/'+
+		currentSite['siteName']+
+		' acting as '+this.role);	
+   return true;
+}
+
+crfVisit.afterConfig=
+function(){
+   let fName='[afterConfig]';
+	this.print(fName);	
+   
+	this.populateBasicData();
+
+   if (!this.checkPermissions()) return;
+   crfSetup.parseButtons();
+   
+   let formStatus=crfSetup.getRows('formStatus')[0]['formStatus'];
+
+	//let functionArray=new Array();
+
+	this.print("Generating buttons for formStatus \""+ formStatus+"\"");
+
+   let allButtonRows=crfSetup.getRows('crfButtons');
+	let buttonRows=new Array();
+   
+   //specifying role=X in actionSettings will limit button to that role
+   for (let i=0;i<allButtonRows.length;i++){
+      let action=allButtonRows[i]['action'];
+      //filter on actionSettings
+      let as=crfSetup.getActionSettings[action];
+      if (variableList.hasVariable(as,'role')){
+         this.print('Role['+this.role+'/'+as['role']+'] limited for action '+action);
+         //mismatch skips addition of button to buttonRows
+         if (this.role!=as['role']) continue;
+      }
+      buttonRows.push(allButtonRows[i]);
+   }
+
+
+
+	for (let i=0;i<buttonRows.length;i++){
+		let bt=buttonRows[i];
+		//if (typeof window[bt.action]==="function"){
+		this.generateButton("submitDiv",bt.caption,bt.label,bt.action,null);
+		//}
+		//else{
+		//	this.print('No match for function :'+bt.action+
+		//		' obj: '+window[bt.action]);
+		//}
+	}
+
+	this.print('Here');
+
+
+	//here we should get data. For now, just initialize objects that will hold data
+   let that=this;
+   let action=function(){that.afterDataLayout();};
+   let formId=crfData.getCrfEntry()['Form'];
+	crfData.setDataLayout(formId,this.role,action);//callback is afterDataLayout
+}
+
+crfVisit.afterDataLayout=
+function(){
+
+   let that=this;
+   let action=function(){that.afterData();};
+   //let action=function(){that.doNothing();};
+	crfData.setData(this.crfRef,action);//callback is afterData
+}
+
+crfVisit.updateRegistration=
+function(){
+	let fName="[updateRegistration]";
+	this.print(fName);
+   let pM=this.getIdManager();
+	let idFieldName=participantIdManager.getCrfEntryFieldName(pM,"STUDY");
+	//have to reload query data
+	let regQueryPars=variableList.parseVariables(crfSetup.getSettings('registrationQuery'));
+   let regQuery=regQueryPars['query'];
+	let fQuery=crfData.getQuerySnapshot(regQuery);
+
+	if (fQuery.rows.length==0) {
+		this.print(fName+" registration is empty");
+		return; //registration is empty
+	}
+	let regEntry=fQuery.rows[0];
+
+	for (x in regEntry){
+		this.print(fName+" ["+x+"] "+regEntry[x]);
+	}
+
+
+	let studyId=fQuery.rows[0][idFieldName];
+	if (!studyId) {
+		this.print(fName+" study id not set ("+idFieldName+'/'+studyId+")");
+		return; //study id not set
+	}
+	
+	//set 
+	participantIdManager.setParticipantIdToCrfEntry(pM,studyId,"STUDY");
+	//this will only update crfEntry in memory, but not on LabKey, 
+	//we are counting on updateFlag to follow updateRegistration
+
+	//update parentCRF as well, here we schedule update of data entry as well
+	if ("parentCrfData" in crfSetup){
+		let parentCrfEntry=crfSetup.getRows('parentCrfData')[0];
+		parentCrfEntry[idFieldName]=studyId;
+      let that=this;
+      let action={name:"updateRegistration",cb:function(){that.doNothing();}};
+		let cb=function(data){that.completeWithFlag(data,action);};
+      this.modifyRows('update','lists','crfEntry',[parentCrfEntry],cb,crfSetup.getContainer('CRF'));
+	}
+  
+
+}
+
+crfVisit.afterData=
+function(){
+	let fName='afterData';
+   this.configureIdManager();
+   this.generateSections();
+}
+
+
+crfVisit.configureIdManager=
+function(){
+   let idMode=this.formEntry['idMode'];
+   //set default value if no value is in the list (read value is null)
+   if (!idMode) idMode="STUDY:EDIT";
+
+   this.print(fName+': idMode '+idMode);
+   //add print to config so participantManager can use it
+   let pM=this.getIdManager();
+   //extend object
+   let that=this;
+   let action=new Object();
+   action.name='updateCrfEntry';
+   action.cb=function(){that.doNothing();};
+   let formStatus=crfData.getCrfEntry()['FormStatus'];
+   pM.updateCrfEntry=function(){that.updateFlag(formStatus,action);};   
+
+   let idModeArray=idMode.split(':');
+   pM.mode="STUDY";
+   if (idModeArray.includes("LOCAL")) {
+      pM.mode="LOCAL";
+      //OK, but check if CRF or registration indicate that study id is already set
+      participantIdManager.verifyCrfStudyId(pM);
+	  //study id should already be set by updateRegistration
+      //verifyRegistration(pM);
+   }
+   if (idModeArray.includes("READONLY")){
+      pM.readOnly="TRUE";
+   }
+   
+   let pId=participantIdManager.getParticipantIdFromCrfEntry(pM);
+   if (!pId){
+      participantIdManager.setEditMode(pM);
+   }
+   else{
+      let label=pId;
+      if (pM.mode=="STUDY"){
+         let loc=participantIdManager.getParticipantIdFromCrfEntry(pM,'LOCAL');
+         label=pId+':'+loc;
+         pM.readOnly="true";
+      }
+      participantIdManager.setLabelMode(pM,label);
+      //in STUDY mode also change LOCAL ID from crfEntry
+      
+   }
+}
+
+crfVisit.generateSections=
+function(){
+	let accessMode=this.role+'Mode';
+   let formId=crfData.getCrfEntry()['Form'];
+   let rowsSetup=crfSetup.selectFormSetupRows(formId);
+	for (let i=0;i<rowsSetup.length;i++){
+		let entry=rowsSetup[i];
+
+      //debug
+		let queryName=crfSetup.getMap('inputLists')[entry['queryName']];
+		this.print(fName+" ["+queryName+"]: showFlag: "+entry["showFlag"]);
+		this.print(fName+" ["+queryName+"]: accessMode: "+entry[accessMode]);
+		const nData=crfData.getQuerySnapshot(queryName).rows.length;
+		this.print(fName+" ["+queryName+"]: nData: "+nData);
+
+		//skip sections
+		//also from fields
+		if (entry[accessMode]=="NONE") continue;
+			
+		//section fits one dataset/list
+		this.generateSection(entry);
+	}
+
+}
+
+crfVisit.populateSection=
+function(sectionId){
+	let fName='[populateSection/'+sectionId+']';
+	this.print(fName);
+
+   //old setting
+	let entry=crfSetup.findSetupRow(sectionId);
+
+	//ignore names without associated entry in formSetup
+	if (!entry){
+		this.print(fName+': no matching FormSetup entry found');
+		return;
+	}
+	//populate comes after generate, we should be pretty safe in taking
+	//already generated additionalData
+	let queryName=crfSetup.getMap('inputLists')[entry['queryName']];
+
+	if (!(queryName in crfSetup.getAdditionalDataObject())){
+		this.print(fName+': no additionalData generated for '+queryName);
+		return;
+	}
+	
+	let additionalData=crfSetup.getAdditionalData(queryName);
+	this.print(fName+': using additionalData '+additionalData);
+	if ("isReview" in additionalData){
+      let action=function(){crfReviewSection.CB();};
+		crfReviewSection.generateSection(queryName,queryName,action);
+		return;	
+	}
+
+	let accessMode=this.role+'Mode';
+	let aM=entry[accessMode];
+	this.print(fName+': accessMode '+aM);
+
+	if (aM!='GENERATE'){
+		let writeMode=entry[accessMode]=='EDIT';
+		this.print(fName+': mode='+writeMode);
+      let setup=this.getStoredSetup(sectionId);
+	   this.populateTable(queryName,writeMode,setup);
+		return;
+	}
+
+	//deal with generate
+	//
+	//already available -> shift to READ mode
+	let divTable=queryName+'Table';
+	let divObj=crfHTML.getElement(divTable);
+	let divRev=crfHTML.getElement(queryName+'Review');
+	let divRLi=crfHTML.getElement(queryName+'ReviewList');
+	let divGBu=crfHTML.getElement(queryName+'GenerateButton');
+
+	this.print('div GBU: '+divGBu);
+	divObj.style.display="block";
+	divRev.style.display="block";
+	divRLi.style.display="block";
+	if (divGBu!=undefined) divGBu.style.display="none";
+
+	let nData=crfData.getQuerySnapshot(queryName).rows.length;
+   let setup=this.getSetup(sectionId,queryName,0);
+	this.print('['+queryName+']: nrows '+nData);
+	if (nData>0){
+		this.populateTable(queryName,0,setup);
+		return;
+	}
+	//hide table
+	divObj.style.display="none";
+	divRev.style.display="none";
+	divRLi.style.display="none";
+	if (divGBu!=undefined) divGBu.style.display="block";
+	//add buttons?
+	//is button already generated?
+	
+	//populateTable(entry);
+	
+}		
+
+//*******    generateQuery infrastructure *********************
+
+crfVisit.onGenerateQuery=
+function(queryName){
+
+   let fName='[onGenerateQuery]';
+	this.print(fName+' '+queryName);
+//
+	let cfgRows=crfSetup.getRows('generateConfigData');
+//	//queryName to queryId?
+   let qMapInverse=crfSetup.invertMap(crfSetup.getMap('inputLists'));
+	let queryId=qInverseMap[queryName];
+
+
+	let cfgRow=crfSetup.getEntryMap('generateConfigData')[queryId];
+	
+   if (!cfgRow){
+		this.print('generateConfig for queryName['+queryId+']='+queryName+' not found');
+		return;
+	}
+
+	//let formRows=crfSetup.selectFormSetupRows(cfgRow.formId);
+//
+//	//check if all required datasets were at least saved
+	this.checkGenerationFields(cfgRow);
+}
+
+crfVisit.checkGenerationFields=
+function(entry){
+   //entry is generateConfig row
+   let fName='[checkGenerationFields]';
+	let mailRecipient=crfRow.emailRecipient;
+	let qMap=crfSetup.getMap('inputLists');
+   let qName=qMap[entry['queryId']];
+	//list of queries that are part of Registration form
+	this.print(fName);	
+	this.print(fName+' setRecipient: '+mailRecipient);
+	let formId=entry['formId'];
+	this.print(fName+" Checking form w/id "+formId);
+	let formRows=this.selectFormSetupRows(formId);
+	//registration rows
+	for (let i=0;i<formRows.length;i++){
+		let row=formRows[i];
+		let queryId=row.queryName;
+		if (queryId==entry.queryId) continue;
+		let fQuery=crfData.getQuerySnapshot(qMap[queryId]);
+		this.print('Checking '+qMap[queryId]+' nrows: '+fQuery.rows.length);
+		if (fQuery.rows.length==0){ 
+			this.generateError(qName,qMap[queryId]);
+			return;
+		}
+	}
+	this.generateMessage(qName,'Vailidation OK');
+	this.print('callback: set recipient: '+mailRecipient);
+   let that=this;
+	let cb=function(){that.prepareForm(entry,mailRecipient);};
+	this.generateListEntry(entry.formId,qName,cb);
+}
+
+
+crfVisit.prepareForm=
+function(entry,mailRecipient){
+   //entry is generateConfig row
+   let fName="[prepareForm]";
+   let formId=entry['formId'];
+
+	this.print(fName+' recipient '+mailRecipient);
+
+	//look for existing registration entry
+   let that=this;
+	let action=function(data){that.generateForm(data,entry,mailRecipient);};
+	let formFilter=LABKEY.Filter.create('Form',formId);
+	let parentCrfFilter=LABKEY.Filter.create('parentCrf',this.crfRef);
+   let filters=[formFilter,parentCrfFilter];
+   this.selectRows('lists','crfEntry',filters,action,crfSetup.getContainer('data'));
+
+}
+
+crfVisit.generateError=
+function(queryName,fQueryName){
+	let elName=queryName+'GenerateButton'+'_reportField';
+	let el=crfHTML.getElement(elName);
+	el.innerText='Error: '+fQueryName+' was not set';
+	el.style.color='red';
+}
+
+crfVisit.generateMessage=
+function(queryName,msg){
+	let elName=queryName+'GenerateButton'+'_reportField';
+	let el=crfHTML.getElement(elName);
+	el.innerText=msg;
+	el.style.color='green';
+}
+
+crfVisit.generateForm=
+function(data,entry,mailRecipient){
+   //entry is generateConfig entry
+
+   let fName='[generateForm]';
+
+	this.print(fName+' recipient: '+mailRecipient);
+//	
+	const nData=data.rows.length;
+	this.print(fName+' Registration: '+nData+' rows');
+
+	//we have to generate masterQuery with parentCrf and crfRef 
+	//and crfEntry with new entryId and parentCrf equal to crfRef
+   let queryName=crfSetup.getMap('inputLists')[entry['queryId']];
+	if (nData>0) {
+		this.generateMessage(queryName,'Registration already generated.');
+		return;
+	}
+	let formId=entry['formId'];
+   let formEntry=crfSetup.getMap('dataForms')[formId];
+	let formName=formEntry.formName;
+	let crfBase=crfData.getCrfEntry();
+	let crfEntry=new Object();
+	//add new reference
+	crfEntry.entryId=Date.now();
+	crfEntry.parentCrf=this.crfRef;
+	crfEntry["Date"]=new Date();
+	crfEntry["View"]="[VIEW]";
+
+	crfEntry.formStatus=1;//In progress
+   //checks for both field presence (if not in query, undefined) and field value (if not set, null)
+   this.print(fName+' setup status: '+entry.formStatus);
+   if (entry.formStatus){
+      crfEntry.formStatus=entry.formStatus;
+   }
+
+   //get local Id
+   let pM=this.getIdManager();
+   
+   crfEntry[participantIdManager.getCrfEntryFieldName(pM)]=participantIdManager.getParticipantIdFromCrfEntry(pM);
+//	//set other variables
+	//requires studyData as part of formConfig
+//	let studyData=config.formConfig.studyData;
+	this.print('Adding study: '+crfBase.EudraCTNumber);
+	crfEntry.EudraCTNumber=crfBase.EudraCTNumber;
+	crfEntry.StudyCoordinator=crfBase.StudyCoordinator;
+	crfEntry.StudySponsor=crfBase.StudySponsor;
+	crfEntry.RegulatoryNumber=crfBase.RegulatoryNumber;
+//
+//	//find sponsor for site
+	let site=crfBase.Site;
+	let crfSponsors=crfSetup.getRows('crfSponsors');
+	let userMap=crfSetup.getEntryMap('users');
+   let sponsorId=null;
+	for (let i=0;i<crfSponsors.length;i++){
+		//take first matching sponsor
+		if (crfSponsors[i].Site!=site) contnue;
+		sponsorId=crfSponsors[i].User;
+		//finds first
+		break;
+	}
+   let sponsor=userMap[sponsorId];
+	this.print('Selecting '+sponsor.DisplayName+' as sponsor');
+	//different user than the original form...
+	//should be set to the study sponsor
+	crfEntry.UserId=sponsor.UserId;
+	crfEntry.Site=site;
+//	//set formId to one found through registration search
+	crfEntry.Form=formId;
+////
+
+   let crfStatus=crfData.createCrfStatus(crfEntry);
+   crfStatus.operator=this.role;
+   crfStatus.action='generateForm';
+
+   let that=this;
+   let action=function(){that.doNothing();};
+	let cb=function(data){that.sendEmail(data,mailRecipient,action,formName+' generated');}
+   let containerPath=crfSetup.getContainer('data');
+   let pass=function(data){runQuery.insertRows('lists','crfStatus',[crfStatus],cb,containerPath);};
+   runQuery.insertRows('lists','crfEntry',[crfEntry],pass,crfSetup.getContainer('data'));
+
+}
+
+crfVisit.generateListEntry=
+function(formId,queryName,cb){
+
+	//check if registration was already generated
+
+	let formRows=crfSetup.selectFormSetupRows(formId);
+
+	let nData=crfData.getQuerySnapshot(queryName).rows.length;
+
+   if (nData>0) return;
+
+
+   //create new list entry
+   let pM=this.getIdManager();
+   
+
+	let e2=new Object();
+	e2.crfRef=this.getCrfRef();
+	e2.registrationStatus=0;
+	e2.submissionDate=new Date();
+   e2[participantIdManager.getCrfEntryFieldName(pM)]=participantIdManager.getParticipantIdFromCrfEntry(pM);
+	this.print('set values');
+
+   runQuery.insertRows('lists',queryName,[e2],cb,crfSetup.getContainer('data'));
+
+}
+		
+// ******************** end form generator (Registration) ********************
+
+//jump to populate table/generate review, etc defined at the begining of the file
+
+//entry point from generateMasterForm
+crfVisit.setFormConfig=
+function(){
+   let fName="[setFormConfig]";
+   let crfRef=this.crfRef;
+   let that=this;
+   let afterCrfEntry=function(){that.afterCrfEntry();};
+   let action=function(){crfData.setCrfEntry(crfRef,afterCrfEntry);};//afterCrfEntry
+   crfSetup.setContainers(action);
+}
+
+crfVisit.afterCrfEntry=
+function(){
+   let fName='[afterCRFEntry]';
+	this.print("Setting crfEntry (x) to "+crfData.getCrfEntry()["entryId"]);
+	//for empty records or those with parentCrf not set, parentCrf comes up as null
+	//nevertheless, with two equal signs, check against undefined also works
+   crfSetup.formStatus=crfData.getCrfEntry()['FormStatus'];
+   let parentCrf=crfData.getCrfEntry()['parentCrf'];
+	this.print('parentCrf set to '+parentCrf);
+   if (parentCrf) crfSetup.parentCrf=parentCrf;
+   let that=this;
+   let action=function(){that.parseSetup();};
+   crfSetup.parseSetup(action);
+}
+
+crfVisit.parseSetup=
+function(){
+
+   //debug
+   let fName='[fcontinue]';
+   let varRows=crfSetup.getRows('crfStaticVariables');
+   let studyVars=crfSetup.getRows('studyData')[0];
+   for (let i=0;i<varRows.length;i++){
+      let vName=varRows[i].staticVariable;
+      this.print(fName+' '+vName+': '+studyVars[vName]);
+   }
+
+	//parse site
+	this.siteEntry=crfSetup.getEntryMap('siteData')[crfData.getCrfEntry()['Site']];
+	this.print("Setting site name to "+this.siteEntry['siteName']);
+	//study
+	this.print("XSetting participantField to "+studyVars["SubjectColumnName"]);
+	
+   //parse user
+   this.userEntry=crfSetup.getEntryMap('users')[crfData.getCrfEntry()['UserId']];
+	this.print("Setting user to "+this.userEntry["DisplayName"]);
+
+	this.print('Setting operator to: '+this.role);
+	
+   //point formId to point to form set in crfEntry
+   let formId=crfData.getCrfEntry()['Form'];
+	this.formEntry=crfSetup.getEntryMap('dataForms')[formId];
+
+   crfSetup.setAdditionalData(this.crfRef,formId);
+	
+	this.afterConfig();
+
+}
+
+crfVisit.uploadFile=
+function(inputElement,context){
+	//context should have ID and dirName attributes; 
+	//path will be dirName/ID/fieldName_ID.suf
+	//where suf is identical to localPath content picked from
+	//inputElement
+	this.print('uploadFile: '+inputElement.value+'/');
+	if (inputElement.type=="text") return;
+	this.print('uploadFile: '+inputElement.files+'/');
+	this.print('uploadFile: '+inputElement.files.length+'/');
+	if (inputElement.files.length>0){
+		let file=inputElement.files[0];
+		this.print('uploadFile: '+inputElement.value+'/'+file.size);
+      webdav.uploadFile(file,context);
+   }
+}
+
+crfVisit.printForm=
+function(){
+   crfPrint.printForm();
+}

+ 27 - 0
web/crf/fileManager.js

@@ -0,0 +1,27 @@
+function parseResponseXML(){
+	//print(config.config,'Status:' +this.status);
+	print('Status:'+this.status);
+	if (this.status!=200) return;
+	config.loadFileConfig.json=JSON.parse(this.responseText);
+	config.loadFileConfig.cb();
+}
+
+function loadFile(){
+	print('YY: '+config.loadFileConfig.url);
+
+	let connRequest=new XMLHttpRequest();
+	connRequest.addEventListener("loadend",parseResponseXML);
+		//function(e){parseResponseXML(e,config);});
+	connRequest.open("GET", config.loadFileConfig.url);
+	connRequest.send();
+}
+
+
+function getBasePath(){
+	let server=LABKEY.ActionURL.getBaseURL();
+	let basePath=server+"_webdav";
+	basePath+=LABKEY.ActionURL.getContainer();
+	return basePath;
+}
+
+

+ 299 - 0
web/crf/formGenerator.js

@@ -0,0 +1,299 @@
+//namespace
+var formGenerator={};
+
+formGenerator.set=
+function(parentClass){
+   this.parent=parentClass;
+}
+
+formGenerator.addFormGenerator=
+function(){
+   //parentClass should provide config and print and getContainer
+   let config=this.parent.config;
+      
+   let fName='[addFormGenerator]';
+   this.parent.print(fName);
+   //layout
+   let table=config.document.createElement("table");
+   table.className="t2";
+   config.document.getElementById(config.div).appendChild(table);
+   //this is a form manipulator
+   let fgForm=new Object();
+
+   fgForm.formSelect=this.addInputRow(table,'Select form',"select");
+   fgForm.crfSelect=this.addInputRow(table,'Select CRF',"select");
+   fgForm.comment=this.addInputRow(table,'Enter comment','text');
+   fgForm.details=this.addInputRow(table,'Details','label');
+   fgForm.warnings=this.addInputRow(table,'Warnings','label');
+   fgForm.warnings.innerHTML='formGenerator version 2.1.0';
+   this.addOption(fgForm.formSelect,'<Select>',-1);
+   let formRows=config.formConfig.generateConfigData.rows;
+   for (let i=0;i<formRows.length;i++){
+      let formId=formRows[i]["formId"];
+      let formName=this.getFormName(formId);
+      this.parent.print(fName+' '+formRows[i]["formId"]+'/'+formName);
+      this.addOption(fgForm.formSelect,formName,formId);
+   }
+   //callbacks should be called on copy of this
+   let that=this;
+   fgForm.formSelect.onchange=function(){that.updateIdList(fgForm);};
+   fgForm.crfSelect.onchange=function(){that.updateLabel(fgForm);};
+   fgForm.generateButton=this.addInputRow(table,'Generate Form','button');
+   fgForm.generateButton.value="Generate Form";
+   fgForm.generateButton.onclick=function(){that.createFormWithId(fgForm);};
+      
+}
+
+formGenerator.insertRow=
+function(schemaName,queryName,row,cb=null,containerPath=null){
+   let fName='[fgInsertRow]';
+   this.parent.print(fName);
+   //cb=function(data){....}
+	let qconfig=new Object();
+	if (containerPath)
+      qconfig.containerPath=containerPath;
+	qconfig.schemaName=schemaName;
+	qconfig.queryName=queryName;
+	qconfig.success=function(data){;};
+   if (cb) qconfig.success=cb;
+   qconfig.rows=[row];
+   this.parent.print(fName+' qconfig '+qconfig);
+	LABKEY.Query.insertRows(qconfig);
+}
+
+formGenerator.addInputRow=
+function(table,header,type){
+   let config=this.parent.config;
+   let fName='[addInputRow]';
+   this.parent.print(fName);
+   let row=table.insertRow();
+   let cell=config.document.createElement('th');
+   let text=config.document.createTextNode(header);
+   cell.appendChild(text);
+   row.appendChild(cell);
+   let input=null;
+      
+   if (type=="select")
+      input=config.document.createElement(type);
+
+   if (type=="button"){
+      input=config.document.createElement("input");
+      input.type="button";
+   }
+   if (type=="text"){
+      input=config.document.createElement('textarea');
+      input.cols="65";
+      input.rows="5";
+   }
+   if (type=="label")
+      input=config.document.createElement(type);
+
+   let cell1=row.insertCell();
+   cell1.appendChild(input);
+   return input;
+}
+
+formGenerator.getFormName=
+function(formId){
+   let config=this.parent.config;
+   let rows=config.formConfig.dataForms.rows;
+   for (let i=0;i<rows.length;i++){
+      if (rows[i]['Key']==formId){
+         return rows[i]['formName'];
+      }
+   }
+   return "NONE";
+}
+
+formGenerator.getQueryName=
+function(queryId){
+   let config=this.parent.config;
+   let rows=config.formConfig.inputLists.rows;
+   for (let i=0;i<rows.length;i++){
+      if (rows[i]['Key']==queryId){
+         return rows[i]['queryName'];
+      }
+   }
+   return "NONE";
+}
+
+formGenerator.getGCRow=
+function(formId){
+   let config=this.parent.config;
+   let formRows=config.formConfig.generateConfigData.rows;
+   for (let i=0;i<formRows.length;i++){
+      if (formRows[i]['formId']==formId){
+         return formRows[i];
+      }
+   }
+   return Object();
+}
+
+formGenerator.getCrfSelectRow=
+function(crfRef){
+   let config=this.parent.config;
+   let rows=config.formConfig.crfSelectRows;
+   for (let i=0;i<rows.length;i++){
+      if (rows[i]['crfRef']==crfRef)
+         return rows[i];
+
+   }
+   return Object();
+}
+
+
+formGenerator.addOption=
+function(input,name,value){
+   let config=this.parent.config;
+   let opt=config.document.createElement("option");
+   opt.text=name;
+   opt.value=value;
+   input.options[input.options.length]=opt;
+}
+
+formGenerator.clearOptions=
+function(input){
+   for(let i = input.options.length; i >= 0; i--) {
+		input.remove(i);
+   }
+}
+
+formGenerator.createFormWithId=
+function(fgForm){
+   //get form id and entry id from select and create form as above
+   let fName='[createFormWithId]';
+
+   this.parent.print(fName);
+   let config=this.parent.config;
+   let formId=fgForm.formSelect.options[fgForm.formSelect.selectedIndex].value;
+   let crfRef=fgForm.crfSelect.options[fgForm.crfSelect.selectedIndex].text;
+   let configRow=this.getGCRow(formId);
+   let crfSelectRow=this.getCrfSelectRow(crfRef);
+	let formConfig=config.formConfig;
+
+	this.parent.print("Create form w/id "+formId);
+	
+	let crfEntry=new Object();
+	crfEntry.entryId=Date.now();
+	crfEntry["Date"]=new Date();
+	crfEntry["View"]="[VIEW]";
+   crfEntry['participantStudyId']=crfSelectRow['participantStudyId'];
+   crfEntry['participantLocalId']=crfSelectRow['participantLocalId'];
+
+
+	crfEntry.formStatus=configRow['formStatus'];//In progress
+	//set other variables
+	//requires studyData as part of formConfig
+	let studyData=formConfig.studyData.rows[0];
+   let varRows=formConfig['crfStaticVariables'].rows;
+   for (let i=0;i<varRows.length;i++){
+      let varName=varRows[i].staticVariable;
+	   crfEntry[varName]=studyData[varName];
+   }
+	crfEntry.UserId=LABKEY.Security.currentUser.id;
+	crfEntry.Site=config.formConfig.currentSites[0].siteNumber;
+	this.parent.print("Setting site to id="+crfEntry.Site);
+	//from argument list
+	crfEntry.Form=formId;
+   crfEntry.parentCrf=crfRef;
+  
+   //
+   //compose a reviewComments entry
+   let reviewComment=new Object();
+   reviewComment['submissionDate']=crfEntry['Date'];
+   reviewComment['crfRef']=crfRef;
+   //comment length
+   let x=fgForm.comment.value;
+   this.parent.print(fName+' comment length '+x.length);
+   if (x.length==0){
+      fgForm.warnings.innerHTML='Supply a comment';
+      return;
+   }
+   reviewComment['reviewComment']=fgForm.comment.value;
+   reviewComment['queryName']=configRow['queryId'];
+
+   let crfStatus=new Object();
+   crfStatus.entryId=crfEntry.entryId;
+   crfStatus.submissionDate=new Date();
+   crfStatus.FormStatus=crfEntry.formStatus;
+   crfStatus.User=crfEntry.UserId;
+   crfStatus.Form=crfEntry.Form;
+   crfStatus.operator=config.role;
+   crfStatus.action='createFormWithId';
+
+   let that=this;
+   let containerPath=this.parent.getContainer('data');
+   let rd=function(data){that.redirect();};
+   let pass1=function(data){that.insertRow('lists','crfStatus',crfStatus,rd,containerPath);};
+   let pass=function(data){that.insertRow('lists','reviewComments',reviewComment,pass1,containerPath);};
+   this.insertRow('lists','crfEntry',crfEntry,pass,this.parent.getContainer('data'));
+}
+
+
+formGenerator.updateIdList=
+function(fgForm){
+   let fName='[updateIdList]';
+   let formId=fgForm.formSelect.options[fgForm.formSelect.selectedIndex].value;
+   this.parent.print(fName+' id '+formId);
+   //remove old options
+   this.clearOptions(fgForm.crfSelect);
+   this.parent.print(fName+' options cleared');
+   //get query associated with form
+   let configRow=this.getGCRow(formId);
+   let queryId=configRow['queryId'];
+   this.parent.print(fName+' queryId '+queryId);
+   if (!queryId || queryId<0)
+      return;
+
+   let qselect=new Object();
+   qselect.containerPath=this.parent.getContainer('data');
+   qselect.schemaName='lists';
+   qselect.queryName=this.getQueryName(queryId);
+   let that=this;
+   qselect.success=function(data){that.updateIdListWithData(fgForm,data);};
+   LABKEY.Query.selectRows(qselect);
+}
+
+formGenerator.updateIdListWithData=
+function(fgForm,data){
+   let config=this.parent.config;
+   let rows=data.rows;
+   config.formConfig.crfSelectRows=data.rows;
+   for (let i=0;i<rows.length;i++){
+      this.addOption(fgForm.crfSelect,rows[i]['crfRef'],i);
+   }
+   let event=new Event('change');
+   fgForm.crfSelect.dispatchEvent(event);
+}
+
+formGenerator.updateLabel=
+function(fgForm){
+   let crfRef=fgForm.crfSelect.options[fgForm.crfSelect.selectedIndex].text;
+   let crfSelectRow=this.getCrfSelectRow(crfRef);
+   fgForm.details.innerHTML='Generating for Study:'+crfSelectRow['participantStudyId']+' / Local:'+crfSelectRow['participantLocalId'];
+}
+
+formGenerator.redirect=
+function(){
+
+	let debug=false;
+	let formUrl="begin";
+	let params=new Object();
+	params.name=formUrl;
+	params.pageId="CRF";
+
+	//points to crf container
+	let containerPath=this.parent.getContainer('data');
+        
+	// This changes the page after building the URL. 
+	//Note that the wiki page destination name is set in params.
+        
+	var homeURL = LABKEY.ActionURL.buildURL(
+			"project", formUrl , containerPath, params);
+   this.parent.print("Redirecting to "+homeURL);
+	if (debug) return;	 
+	window.location = homeURL;
+
+	
+
+}

+ 166 - 0
web/crf/generateRegistration.js

@@ -0,0 +1,166 @@
+let generateRegistration={};
+
+generateRegistration.fName="[generateRegistration]";
+
+generateRegistration.set=
+function(){
+   ;
+}
+
+generateRegistration.init=
+function(cb=null){
+   let that=this;
+   let action=function(){that.afterScripts(cb);}
+   LABKEY.requiresScript(["crf/crfHTML.js"],action);
+}
+
+generateRegistration.afterScripts=
+function(cb=null){
+   if (cb) cb();
+}
+
+generateRegistration.print=
+function(msg){
+   console.log(msg);
+}
+
+generateRegistration.selectRows=
+function(gObj,cb){
+   this.print(this.fName+": selectRows");
+   let xRows=new Object();
+   let that=this;
+   xRows.schemaName=gObj.schemaName;
+   xRows.queryName=gObj.queryName;
+   xRows.success=cb;
+   xRows.failure=function(errorInfo){that.fail(errorInfo);};
+   LABKEY.Query.selectRows(xRows);
+   this.print(this.fName+": selectRows completed");
+}
+
+generateRegistration.insertRows=
+function(gObj,rows){
+   this.print(this.fName+": insertRows");
+   let iRows=new Object();
+   iRows.schemaName=gObj.schemaName;
+   iRows.queryName=gObj.queryName;
+   iRows.rows=rows;
+   iRows.success=function(data){gObj.callback(data);};
+   LABKEY.Query.insertRows(iRows);
+}
+ 
+generateRegistration.zeroPad=
+function(val,strLength=3){
+   let strK=val.toString();
+   return strK.padStart(strLength,'0');
+}
+
+generateRegistration.findFirstAvailableKey=
+function(rows){
+   let k=-1;
+   for (let i=0;i<rows.length;i++){
+      if (rows[i]['Key']>k){
+         k=rows[i]['Key'];
+      }
+   }
+   this.print(this.fName+': Key candidate: '+(k+1));
+   return k+1;
+}
+
+generateRegistration.generateObjectAtKey=
+function(gObj,k){
+   let regCode=gObj.codeBase+this.zeroPad(k);
+   this.print(this.fName+": regCode "+regCode);
+   let row=new Object();
+   row['Key']=k;
+   row[gObj.codeField]=regCode;
+   if ("addData" in gObj){
+      for (let q in gObj.addData){
+         row[q]=gObj.addData[q];
+      }
+   }
+   return row;
+}
+
+generateRegistration.getCode=
+function(gObj,row){
+   return row[gObj.codeField];
+}
+
+generateRegistration.updateField=
+function(gObj,text){
+   let el=crfHTML.getElement(gObj.elementId);
+   this.print(this.fName+": updateField "+gObj.elementId+'/'+el);
+   el.value=text;
+   if ('updateField' in gObj.qPar){
+      let id=gObj.setup.getInputId(gObj.qPar['updateField']);
+      crfHTML.getElement(id).value=gObj.qPar['updateValue'];
+   }
+
+}
+
+generateRegistration.generateId=
+function(gObj,data){
+   this.print(this.fName+": generateId "+data.rows.length);
+   let k=this.findFirstAvailableKey(data.rows);
+   let row=this.generateObjectAtKey(gObj,k);
+   this.updateField(gObj,this.getCode(gObj,row));
+   let rows=new Array();
+   rows.push(row);
+   this.insertRows(gObj,rows);
+}
+
+generateRegistration.doNothing=
+function(data){
+   this.print(this.fName+": doNothing() called");
+}
+
+generateRegistration.fail=
+function(errorInfo){
+   this.print(this.fName+": error "+errorInfo.exception);
+}
+
+generateRegistration.execute=
+function(gObj){
+   let that=this;
+   //this.print(this.fName+": execute "+gObj.elementId);
+   this.inspect(gObj);
+   this.selectRows(gObj,function(data){that.generateId(gObj,data);});
+}
+
+generateRegistration.inspect=
+function(gObj){
+   this.print(this.fName);
+   this.print("query: "+gObj.schemaName+'/'+gObj.queryName);
+   this.print("codeBase "+gObj.codeBase+" codeField "+gObj.codeField);
+   this.print("elementId "+gObj.elementId);
+   this.print("callback "+gObj.callback);
+   this.print("version 1.01");
+}
+
+//generic function for all functors
+//config is there by default
+//
+//pars is semicolon delimeted list of parName=value pairs;
+//required:
+//codeBase - prepend ids with this set of letters
+//schemaName - schema of queryName
+//queryName - query that keeps assigned ids
+//codeField - id field in queryName
+//
+//outputId is the field that gets updated with the button result
+//
+//object is initialized from a list in LabKey
+//
+generateRegistration.getObject=
+function(qPar,outputId){
+   let gObj=new Object();
+   gObj.codeBase=qPar["codeBase"];
+   gObj.schemaName=qPar["schemaName"];
+   gObj.queryName=qPar["queryName"];
+   gObj.codeField=qPar["codeField"];
+   gObj.setup=qPar['setup'];
+   gObj.qPar=qPar;
+   gObj.elementId=outputId;
+   //should set codeBase and elementId after initialization
+   return gObj;
+}

+ 404 - 0
web/crf/participantIdManager.js

@@ -0,0 +1,404 @@
+//all functions are based off of participantManager (print, config, etc.)
+var participantIdManager={};
+
+participantIdManager.print=
+function(msg){
+   console.log(msg);
+}
+
+participantIdManager.init=
+function(cb=null){
+   let that=this;
+   let action=function(){that.afterScripts(cb);};
+   LABKEY.requiresScript(['crf/crfHTML.js'],action);
+}
+
+participantIdManager.afterScripts=
+function(cb=null){
+   if (cb) cb();
+}
+
+participantIdManager.set=
+function(setup,data){
+   this.setup=setup;
+   this.data=data;
+}
+
+participantIdManager.addSelectOptions=
+function(pM){
+   if (pM.mode=="LOCAL") return;
+   
+   let input=this.getInputElement(pM);
+
+   let fName='addParticipantSelectOptions';
+   this.print(fName+' input '+input);
+
+
+   //here the lookup is being populated (registrationData)
+   let demoRows=this.setup.getRows('registrationData');
+   this.print(fName+" demoRows: "+demoRows.length);
+   let opts=new Object();
+   for (let i=0;i<demoRows.length;i++){
+      let id=demoRows[i][this.getCrfEntryFieldName(pM)];
+      let loc=demoRows[i][this.getCrfEntryFieldName(pM,'LOCAL')];
+      opt2[i+1]=id+' (Local: '+loc+')';
+   }
+   crfHTML.addSelectOptions(input,opts);
+}
+
+participantIdManager.updateElements=
+function(pM){
+   let fName='[updateElements]';
+   //reset all values (some might be different depending on the call timing)
+
+   //selector is with study
+   pM.cellSelector=crfHTML.getElement(pM.cellSelectorId);
+   pM.inputSelector=crfHTML.getElement(pM.inputSelectorId);
+   pM.textStudy=crfHTML.getElement(pM.textStudyId);
+  
+   this.print(fName+' selector '+pM.inputSelector+' id '+pM.inputSelectorId);
+   //value is with local
+   pM.cellValue=crfHTML.getElement(pM.cellValueId);
+   pM.inputValue=crfHTML.getElement(pM.inputValueId);
+   pM.textLocal=crfHTML.getElement(pM.textLocalId);
+   
+   //pM.inputManageLocal=this.parent.getElement(pM.inputManageLocalId);
+   //pM.inputManageStudy=this.parent.getElement(pM.inputManageStudyId);
+}
+
+
+participantIdManager.generateEntryField=
+function(pM){
+   let fName='[generateParticipantEntryField]:';
+   this.print(fName);
+
+   //this is HTML designator of area on page
+	let formName=this.masterForm;
+   this.print(fName+' master '+formName);
+
+    
+   pM.tb=crfHTML.createTable(formName);
+   let tb=pM.tb;
+	tb.className='t2';
+	let row=tb.insertRow();
+	
+   //label for local ID
+   let cell=crfHTML.createTblHeader(null,row);
+	cell.setAttribute("colspan","1");
+	cell.style.fontSize="20px";
+	cell.style.textAlign="left";
+   //Use study coding for participant field
+   cell.innerText='Local ID';
+	//cell.innerText=pM.participantField;
+
+
+   //value
+   let cellValue=row.insertCell();
+   cellValue.id=pM.cellValueId;
+
+   pM.cellManageLocal=row.insertCell();
+   
+   //second row for study id
+   let rowStudy=tb.insertRow();
+
+   //label for study ID
+   let cellStudy=crfHTML.createTblHeader(null,rowStudy);
+	cellStudy.setAttribute("colspan","1");
+	cellStudy.style.fontSize="20px";
+	cellStudy.style.textAlign="left";
+   //Use study coding for participant field
+   cellStudy.innerText='Study ID';
+
+	//selector for study id
+   let cellSelector=rowStudy.insertCell();
+   cellSelector.id=pM.cellSelectorId;
+
+   //manage
+   pM.cellManageStudy=rowStudy.insertCell();
+   this.print(fName+' done');
+
+}
+
+
+participantIdManager.getMode=
+function(pM,mode="NONE"){
+   if (mode=="NONE") return pM.mode;
+   return mode;
+}
+
+//reslovers which operate depending on mode
+participantIdManager.getInputId=
+function(pM,mode="NONE"){
+   let fName='[getInputId]';
+   this.print(fName);
+   if (this.getMode(pM,mode)=="LOCAL") return pM.inputValueId;
+   return pM.inputSelectorId;
+}
+
+
+participantIdManager.getInputCell=
+function(pM,mode="NONE"){
+   let fName='[getInputCell]';
+   this.print(fName+' mode '+mode+' getMode '+this.getMode(pM,mode));
+   if (this.getMode(pM,mode)=="LOCAL") return pM.cellValue;
+   return pM.cellSelector;
+}
+
+participantIdManager.getInputElement=
+function(pM,mode="NONE"){
+   let fName='[getInputElement]';
+   this.print(fName);
+   let elementType=this.getInputElementType(pM,mode);
+   let id=this.getInputId(pM,mode);
+   let cell=this.getInputCell(pM,mode);
+   let el=crfHTML.getElement(id);
+   this.print(fName+' mode '+this.getMode(pM,mode)+' type '+elementType+' id '+id+' cell '+cell+' el '+el);
+   if (el) return el;
+   
+   if (elementType=="input") el=crfHTML.createTextInput();
+   if (elementType=="select") el=crfHTML.createSelect(new Object());
+   this.print(fName+' input '+el);
+   el.id=id;
+
+   cell.replaceChildren(el);
+   this.addSelectOptions(pM);
+ 
+   return el;
+}
+
+participantIdManager.getInputElementType=
+function(pM,mode="NONE"){
+   let fName='[getInputElementType]';
+   this.print(fName);
+   if (this.getMode(pM,mode)=="LOCAL") return "input";
+   return "select";
+}
+
+
+participantIdManager.getTextFieldId=
+function(pM,mode="NONE"){
+   let fName='[getTextFieldId]';
+   this.print(fName);
+   if (this.getMode(pM,mode)=="LOCAL") return pM.textLocalId;
+   return pM.textStudyId;
+}
+
+  
+participantIdManager.getTextElement=
+function(pM,mode="NONE"){
+   let fName='[getTextElement]';
+   this.print(fName+' mode '+mode);
+   let id=this.getTextFieldId(pM,mode);
+   this.print(fName+' id '+id);
+   let el=crfHTML.getElement(id);
+   this.print(fName+' el '+el);
+   if (el) return el;
+   el=crfHTML.createParagraph('');
+   el.id=id;
+   let cell=this.getInputCell(pM,mode);
+   //let oldEl=pM.getInputElement(mode);
+   cell.replaceChildren(el);
+   return el;
+}
+
+
+//get the button, create if not there yet
+participantIdManager.getInputManage=
+function(pM,mode="NONE"){
+   let fName='[getInputManage]';
+   //this.print(fName);
+   //this prevents from having two inputs; it is either local or global from the outset
+   if ("inputManage" in pM) return pM.inputManage;
+
+   let cell=pM.cellManageStudy;
+   if (this.getMode(pM,mode)=="LOCAL") cell=pM.cellManageLocal;
+   pM.inputManage=crfHTML.createButton(null,cell);
+   let that=this;
+   pM.inputManage.onclick=function(){that.manageId(pM);};
+   //inputManageLocal.id=pM.inputManageLocalId;
+   //this.print(fName+' inputManage '+pM.inputManage+' cell '+cell+' mode '+pM.mode);
+   return pM.inputManage;
+}
+
+//callback that splits to edit or set/label mode
+
+participantIdManager.manageId=
+function(pM){
+   let fName='[manageId]';
+   this.print(fName);
+   //this can happen after object was created, so make sure current
+   //elements are used
+   this.updateElements(pM);
+   let x=this.getInputManage(pM);
+
+   if (x.value=="Set"){
+      this.setId(pM);
+      return;
+   }
+   if (x.value=="Edit"){
+      this.editId(pM);
+      return;
+   }
+}
+
+//set mode
+participantIdManager.setId=
+function(pM){
+   let fName='[setId]';
+   this.print(fName);
+   let el=this.getInputElement(pM);
+
+   this.print(fName+" value: "+el.value);
+   let pId=el.value;
+   let label=pId;
+   if (pM.mode!="LOCAL"){
+      if (el.value<0) return;
+      let opt=el.options[el.selectedIndex];
+      label=opt.text;
+      pId=label.replace(/\(.*\)/,'');
+      label=label.replace(/ \(Local: /,':');
+      label=label.replace(/\)/,'');
+   }
+   this.setParticipantIdToCrfEntry(pM,pId);//no argument (should come from mode)
+   this.print(fName+" new value "+pId);
+   this.setLabelMode(pM,label);
+   pM.updateCrfEntry();
+}
+
+participantIdManager.setLabelMode=
+function(pM,pId){
+   let fName='[setLabelMode1]';
+
+   this.print(fName+' id '+pId);
+   ids=pId.split(':');
+
+   let textValue=this.getTextElement(pM);
+   this.print(fName+' textElement '+textValue);
+   textValue.innerText=ids[0];
+
+   if (pM.mode=="STUDY"){
+      let loc=ids[1];
+      //pM.getParticipantIdFromCrfEntry('LOCAL');
+      this.print(fName+' setting local id '+loc);
+      let tValLocal=this.getTextElement(pM,'LOCAL');
+      tValLocal.innerText=loc;
+      this.setParticipantIdToCrfEntry(pM,loc,'LOCAL');
+   }
+
+   let x=this.getInputManage(pM);//getInputManage
+   if ("readOnly" in pM){
+      x.style.display="none";
+   }
+   x.value="Edit";
+
+   
+}
+
+//edit mode
+participantIdManager.editId=
+function(pM){
+   this.setEditMode(pM);
+}
+
+
+participantIdManager.setEditMode=
+function(pM){
+
+   let fName='[setEditMode1]';
+   this.print(fName+' pM '+pM+' mode '+pM.mode);
+   //input
+   let el=this.getInputElement(pM);
+
+   let x=this.getInputManage(pM);
+   x.value="Set";
+
+}
+
+//manage interaction to storage/CRF and study/LabKey
+participantIdManager.getParticipantField=
+function(){
+   return this.setup.getRows('studyData')[0]['SubjectColumnName'];
+}
+
+participantIdManager.getCrfEntryFieldName=
+function(pM,mode="NONE"){
+   let variable="Study";
+   if (mode=="NONE") mode=pM.mode;
+   if (mode=="LOCAL") variable="Local";
+   return 'participant'+variable+'Id';
+}
+
+participantIdManager.setParticipantIdToCrfEntry=
+function(pM,pId,mode="NONE"){
+   this.data.getCrfEntry()[this.getCrfEntryFieldName(pM,mode)]=pId;
+}
+
+participantIdManager.getParticipantIdFromCrfEntry=
+function(pM,mode="NONE"){
+   return this.data.getCrfEntry()[this.getCrfEntryFieldName(pM,mode)];
+}
+
+participantIdManager.verifyCrfStudyId=
+function(pM){
+
+   //is studyId already set for the crf
+   let studyId=this.getParticipantIdFromCrfEntry(pM,'STUDY');
+   if (!studyId) return;
+   pM.mode="STUDY";
+   pM.readOnly="TRUE";
+}
+
+participantIdManager.verifyRegistration=
+function(pM, formConfig){
+   //if registration is in, 
+   //then local id should not be changed any longer
+   let idFieldName=this.getCrfEntryFieldName(pM,"STUDY");
+   //let fQuery=config.formConfig.registrationData;
+   let fQuery=formConfig.registrationData;
+   if (fQuery.rows.length==0) return; //registration is empty
+
+   let studyId=fQuery.rows[0][idFieldName];
+   if (!studyId) return; //study id not set
+   //set 
+   pM.mode="STUDY";
+   pM.readOnly="TRUE";
+   //set crf (this happens later, but probably before the form will be corrected)
+   this.setParticipantIdToCrfEntry(pM,studyId,"STUDY");
+   pM.updateCrfEntry();
+}
+
+
+//main interface. Use this to generate object and to refer to it later on
+participantIdManager.getObject=
+function(){
+
+   let fName='[getParticipantManagerObject]';
+   this.print(fName);
+
+   let pM=new Object();
+
+   //this never change
+   pM.participantField=this.getParticipantField();
+
+
+   pM.cellSelectorId=pM.participantField+"_cellSelect";
+   pM.inputSelectorId=pM.participantField+"_Select";
+   pM.textStudyId=pM.participantField+"_textStudy";
+
+   pM.cellValueId=pM.participantField+"_cellValue";
+   pM.inputValueId=pM.participantField+"_Value";
+   pM.textLocalId=pM.participantField+"_textLocal";
+
+   pM.inputManageLocalId=pM.participantField+"_ManageLocal";
+   pM.inputManageStudyId=pM.participantField+"_ManageStudy";
+
+   pM.mode="LOCAL";//or "STUDY"
+   
+   //dummy function to be overloaded by calling class
+   pM.updateCrfEntry=function(){;}
+
+   //init
+   this.generateEntryField(pM);
+   this.updateElements(pM);
+   return pM;
+}

+ 197 - 0
web/crf/participantPortal.js

@@ -0,0 +1,197 @@
+var participantPortal={};
+
+participantPortal.print=function(msg){
+   console.log(msg);
+}
+
+participantPortal.idField='participantStudyId';
+
+participantPortal.init=
+function(cb=null){
+   let that=this;
+   let action=function(){that.scriptsLoaded(cb);};
+   LABKEY.Utils.requiresScript(["crf/crfSetup.js","crf/crfData.js","crf/crfHTML.js"],action);
+}
+
+participantPortal.scriptsLoaded=
+function(cb=null){
+   //if other script need init, just stack the init scripts
+   //let action=function(){runQuery.init(cb);}
+   crfData.setSetup(crfSetup);
+   crfHTML.init();
+   let action=function(){crfData.init(cb);};
+   crfSetup.init(action);
+}
+
+participantPortal.getParticipantMap=
+function(){
+   if (!("participantMap" in this)){
+      this.participantMap=new Object();
+      this.sortByParticipantId();
+   }
+   return this.participantMap;
+}
+   
+
+participantPortal.getParticipantArray=
+function(id,formId){
+   let fName='[getParticipantArray/'+id+','+formId+']';
+   //this.print(fName);
+   let pMap=this.getParticipantMap();
+   if (!(id in pMap))
+      pMap[id]=new Object();
+   if (!(formId in pMap[id]))
+      pMap[id][formId]=new Array();
+   return pMap[id][formId];
+}
+
+participantPortal.getParticipantLabel=
+function(entry){
+   let pid=entry['participantStudyId'];
+   let loc=entry['participantLocalId'];
+   let label='';
+   if (pid) label+=pid+' ';
+   if (loc) label+='(Local: '+loc+')';
+   if (label.length==0) label="NONE";
+   return label;
+ 
+}
+
+
+participantPortal.generateFormArray=
+function(){
+   let fName='[generateFormArray]';
+   this.print(fName);
+   //gang callbacks (last to first)
+   let that=this;
+   let makePortal=function(){that.makePortal();};
+   let setRegistration=function(){crfData.setRegistration(makePortal);};
+   let action=function(){crfSetup.parseSetup(setRegistration);}
+   crfSetup.setContainers(action);
+}
+
+participantPortal.sortByParticipantId=
+function(){
+   let fName='[sortByParticipantId]';
+   //this.print(fName);
+   //let pMap=this.getParticipantMap();
+   let rows=crfSetup.getRows('crfEntries');
+   for (let i=0;i<rows.length;i++){
+      let entry=rows[i];
+      let id=entry[this.idField];
+      if (!id) id="NONE";
+      let formId=entry['Form'];
+      let pArray=this.getParticipantArray(id,formId);
+      pArray.push(entry);
+      this.print(fName+' pushing '+id+','+formId);
+   }
+}
+
+participantPortal.printParticipantArray=
+function(){
+   let fName='[printParticipantMap]';
+   this.print(fName);
+   let pMap=this.getParticipantMap();
+   for (let q in pMap){
+      for (let x in pMap[q])
+         this.print(fName+' ['+q+','+x+'] '+pMap[q][x].length);
+   }
+}
+
+
+participantPortal.makePortal=
+function(){
+   let idMap=crfData.getRegistrationMap(this.idField);
+   let updatedMap=new Object();
+   for (q in idMap){
+      if (!idMap[q]) continue;
+      updatedMap[q]=idMap[q];
+   }
+   updatedMap[1000]='NONE';
+   this.participantSelect=crfHTML.makeSelect(updatedMap,'formDiv');
+   this.displayTable=crfHTML.createTable('formDiv');
+   let that=this;
+   this.participantSelect.onchange=function(){that.displayEntries();}
+}
+
+participantPortal.displayEntries=
+function(){
+   let fName='[displayEntries]';
+   this.print(fName);
+   let formRows=crfSetup.getRows('dataForms');
+   
+
+   //let idRows=crfData.getRegistration();
+   let selectId=this.participantSelect.options[this.participantSelect.selectedIndex].text;
+   //let formId=formIds[Object.keys(formIds)[0]];
+   //this.printParticipantArray();
+   this.print(fName+' rows '+this.displayTable.rows.length);
+
+   for (let i=0;i<formRows.length;i++){
+      let formId=formRows[i]['Key'];
+      let row=this.displayTable.rows[i];
+      if (!row) row=this.displayTable.insertRow(i);
+      let labelCell=row.cells[0];
+      let formName=crfSetup.getMap('dataForms')[formId];
+      if (!labelCell){
+         labelCell=row.insertCell();
+         labelCell.innerText=formName;
+         crfHTML.addStyle(labelCell,'medium');
+         crfHTML.addStyle(labelCell,'center');
+
+         //crfHTML.createParagraph(formName,null,labelCell);
+      }
+      let cell=row.cells[1];
+      if (!cell) {
+         cell=row.insertCell();
+         crfHTML.addStyle(cell,'stretch');
+      }
+      crfHTML.clear(cell);
+      let forms=this.getParticipantArray(selectId,formId);
+      this.displayForms(cell,forms);
+      this.print(fName+' ['+selectId+'/'+formId+'] forms '+forms.length);
+   }
+}
+
+participantPortal.displayForms=
+function(el,formList){
+   //formList is a list of crfEntry entries
+   
+   let table=crfHTML.createTable(null,el);
+   let row=table.insertRow();
+   let n=formList.length;
+
+   let fn=1;
+   if (n>fn) fn=n;
+
+   for (let i=0;i<fn;i++){
+      let entry=formList[i];
+      let cell=row.insertCell(i);
+      crfHTML.addStyle(cell,'stretch');
+
+      let fbox=crfHTML.createBox(null,cell);
+      if (n==0){
+         crfHTML.addStyle(fbox,'empty');
+         break;
+      }
+      //colormap of formStatus to colors
+      let stat=entry['FormStatus'];
+      let style=crfSetup.getEntryMap('formStatus')[stat]['color'];
+      if (!style) style='gold';
+      crfHTML.addStyle(fbox,style);
+      let user=crfSetup.getMap('users')[entry['UserId']];
+      let idLabel=this.getParticipantLabel(entry);
+      let formStatus=crfSetup.getMap('formStatus')[stat];
+      let text=[entry['entryId'],user,idLabel,formStatus];
+
+      for (let j=0;j<text.length;j++){
+         crfHTML.createParagraph(text[j],null,fbox);
+      }
+   }
+   return table;
+   
+}
+
+
+
+

+ 206 - 0
web/crf/runQuery.js

@@ -0,0 +1,206 @@
+var runQuery={};
+
+runQuery.print=
+function(msg){
+   console.log(msg);
+}
+
+runQuery.insertRows=
+function(schema,query,rows,action=null,container=null,failure=null){
+   this.modifyRows('insert',schema,query,rows,action,container);
+}
+
+runQuery.deleteRows=
+function(schema,query,rows,action=null,container=null,failure=null){
+   this.modifyRows('delete',schema,query,rows,action,container);
+}
+
+runQuery.modifyRows=
+function(mode,schema,query,rows,action=null,container=null,failure=null){
+	//insert rows to container/schema/query and return with action
+	let fName="[cvModifyRows/"+mode+"]";
+	this.print(fName+' '+schema+'/'+query);
+	let qconfig=new Object();
+	qconfig.schemaName=schema;
+	qconfig.queryName=query;
+   if (container) qconfig.containerPath=container;
+   if (!rows) {
+      this.print(fName+' rows '+rows);
+      return;
+   }
+	qconfig.rows=rows;
+   qconfig.success=function(data){;};
+	if (action) qconfig.success=action;
+   if (mode=='insert') LABKEY.Query.insertRows(qconfig);
+   if (mode=='update') LABKEY.Query.updateRows(qconfig);
+   if (mode=='delete') LABKEY.Query.deleteRows(qconfig);
+	this.print(fName+" done");
+}
+
+runQuery.selectRows=
+function(schema,query,filters=[],action=null, container=null, failure=null, columns=null){
+	let fName="[cvSelectRows]";
+	this.print(fName+' '+schema+' '+query+' '+container);
+	let qconfig=new Object();
+	qconfig.schemaName=schema;
+	qconfig.queryName=query;
+   if (container) qconfig.containerPath=container;
+   qconfig.filterArray=filters;
+   qconfig.success=function(data){;};
+	if (action) qconfig.success=action;
+   if (failure) qconfig.failure=failure;
+   if (columns) qconfig.columns=columns;
+	LABKEY.Query.selectRows(qconfig);
+	this.print(fName+" done");
+
+}
+
+
+runQuery.makeQuery=
+function(targetObject,containerName,queryName,fieldName=null,filterArray=null,schemaName=null){
+   //call with makeQuery(config.formConfig,getContainer(name),...
+	let e=new Object();
+	e.containerName=containerName;
+	e.queryName=queryName;
+	e.fieldName=queryName;
+   if (fieldName) e.fieldName=fieldName;
+	e.filterArray=[];
+   if (filterArray) e.filterArray=filterArray;
+   e.targetObject=targetObject;
+   e.schemaName='lists';
+   if (schemaName) e.schemaName=schemaName;
+	return e;
+}
+
+runQuery.makeModification=
+function(mode,containerName,schemaName,queryName,rows){
+	let e=new Object();
+   e.mode=mode;
+	e.containerName=containerName;
+   e.schemaName=schemaName;
+	e.queryName=queryName;
+	e.rows=rows;
+   return e;
+}
+
+
+runQuery.getDataFromQueries=
+function(parentClass,queryArray,cb){
+	//queryArray should contain elements created with make query
+	//- fieldName to set the data variable
+	//- containerName to select container (data,config,CRF)
+	//- queryName to select query
+	//- filterArray to perform filtering, empty array works
+	//- callback cb to be called with no arguments
+	//
+	this.afterQuery(new Object(),-1,parentClass,queryArray,cb);
+}
+
+runQuery.modifyDataFromQueries=
+function(parentClass,queryArray,cb){
+	//queryArray should contain elements created with make query
+	//- fieldName to set the data variable
+	//- containerName to select container (data,config,CRF)
+	//- queryName to select query
+	//- filterArray to perform filtering, empty array works
+	//- callback cb to be called with no arguments
+	//
+	this.afterQueryUpload(new Object(),-1,parentClass,queryArray,cb);
+}
+
+
+
+runQuery.afterQuery=
+function(data,id,parentClass,queryArray,cb){
+   let fName='[afterQuery]';
+
+	if (id>-1){
+	   let e1=queryArray[id];
+		let fieldName=e1.fieldName;
+		parentClass.print(fName+' ['+fieldName+']: '+data.rows.length);
+		e1.targetObject[fieldName]=data;
+	}
+	id+=1;
+	if (id==queryArray.length) {
+		if (cb) cb();
+		return;
+	}
+   
+
+	let e=queryArray[id];
+   for (v in e){
+      parentClass.print(fName+' value ['+v+'] '+e[v]);
+   }
+
+	let containerPath=parentClass.getContainer(e.containerName);
+   if ("containerPath" in e){
+      parentClass.print(fName+' containerPath '+e.containerPath);
+      containerPath=e.containerPath;
+   }
+
+	let schemaName="lists";
+	if ("schemaName" in e){
+		parentClass.print(fName+' schemaName='+e.schemaName);
+		schemaName=e.schemaName;
+	}
+   let columns=null;
+	if ("columns" in e){
+		parentClass.print(fName+' columns='+e.columns);
+		columns=e.columns;
+	}
+	//this should point to configuration container
+	//don't filter -> so we can pick up other forms (say registration) later on
+	//qconfig.filterArray=[LABKEY.Filter.create('Key',config.formId)];
+   let filterArray=[];
+	if ("filterArray" in e)
+		filterArray=e.filterArray;
+	
+	//qconfig.filterArray=[LABKEY.Filter.create('formStatus',1)]
+   let that=this;
+	let action=function(data){that.afterQuery(data,id,parentClass,queryArray,cb);};
+	let failure=function(errorInfo,responseObj){that.onTAFailure(parentClass,errorInfo,responseObj);};
+   this.selectRows(schemaName,e.queryName,filterArray,action, containerPath, failure, columns);
+
+}
+
+runQuery.afterQueryUpload=
+function(data,id,parentClass,queryArray,cb){
+   let fName='[afterQueryUpload]';
+
+	if (id>-1){
+	   let x=queryArray[id];
+		let q=x.queryName
+		parentClass.print(fName+' ['+q+']: '+data.rows.length);
+	}
+	id+=1;
+	if (id==queryArray.length) {
+		if (cb) cb();
+		return;
+	}
+   
+
+	let e=queryArray[id];
+   for (v in e){
+      parentClass.print(fName+' value ['+v+'] '+e[v]);
+   }
+	let containerPath=parentClass.getContainer(e.containerName);
+   if ("containerPath" in e){
+      parentClass.print(fName+' containerPath '+e.containerPath);
+      containerPath=e.containerPath;
+   }
+   let that=this;
+	let action=function(data){that.afterQueryUpload(data,id,parentClass,queryArray,cb);};
+	let failure=function(errorInfo,responseObj){that.onTAFailure(parentClass,errorInfo,responseObj);};
+   this.modifyRows(e.mode,e.schemaName,e.queryName,e.rows,action,containerPath,failure);
+	
+
+}
+
+
+runQuery.onTAFailure=
+function(parentClass, errorInfo, responseObj){
+   //don't have configObject to rely to
+   parentClass.print('[afterQuery]: Failure: '+errorInfo.exception);
+
+}
+

+ 59 - 0
web/crf/variableList.js

@@ -0,0 +1,59 @@
+var variableList={};
+
+variableList.parseVariables=
+function(pars){
+   if (!pars) return new Object();
+   let pA=pars.split(";");
+   let q=new Object();
+   for (let i=0;i<pA.length;i++){
+      let vA=pA[i].split('=');
+      q[vA[0]]=vA[vA.length-1];
+   }
+   return q;
+}
+
+variableList.printVariables=
+function(parentClass,q){
+	let fName="[printVariables]";
+	for (let x in q){
+		parentClass.print(fName+" ["+x+"] "+q[x]);
+	}
+}
+
+variableList.hasVariable=
+function(q,varName){
+   if (q && varName in q)
+      return true;
+
+   return false;
+}
+
+variableList.isFilterList=
+function(v){
+   if (typeof(v)!='string') return false;
+   if (v.search(';')==-1) return false;
+   return true;
+}
+
+variableList.convertToDictionary=
+function(rows){
+   let x=new Array();
+	for (let i=0;i<rows.length;i++){
+		let n=rows[i]['name'];
+		let v=rows[i]['value'];
+		x[n]=v;
+	}
+   return x;
+}
+ 
+variableList.convertToAssociatedArray=
+function(rows,fieldName="name"){
+   let x=new Object();
+	for (let i=0;i<rows.length;i++){
+		let n=rows[i][fieldName];
+		x[n]=rows[i];
+	}
+   return x;
+}
+
+

+ 98 - 0
web/crf/webdav.js

@@ -0,0 +1,98 @@
+var webdav={};
+
+webdav.set=
+function(parentClass){
+   this.parent=parentClass;
+}
+   
+webdav.uploadFile=
+function(file,context){
+
+	let url=LABKEY.ActionURL.getBaseURL();
+	url+='_webdav';
+	url+=LABKEY.ActionURL.getContainer();
+	url+='/@files';
+   url+='/'+context['dirName'];
+
+	this.parent.print('uploadFile url: '+url);
+	let uploadConfig=new Object();
+	uploadConfig.file=file;
+	uploadConfig.context=context;
+	uploadConfig.url=url;
+   let that=this;
+	uploadConfig.success=function(cfg){that.afterBaseDir(cfg);};
+	uploadConfig.failure=function(cfg){that.tryMakeDir(cfg);};
+	this.webdavCheck(uploadConfig);
+}
+
+webdav.afterBaseDir=
+function(cfg){
+	this.parent.print('afterBaseDir');
+	cfg.url+='/'+cfg.context['ID'];
+   let that=this;
+	cfg.success=function(x){that.afterIDDir(x);};
+	cfg.failure=function(x){that.tryMakeDir(x);};
+	this.webdavCheck(cfg);
+}
+
+webdav.afterIDDir=
+function(cfg){
+	this.parent.print('afterIDDir');
+	this.parent.print('Uploading '+cfg.file.name);
+	let suf=cfg.file.name.split('.').pop();
+	cfg.url+='/'+cfg.context['ID']+'.'+suf;
+   cfg.data=cfg.file;
+   let that=this;
+	cfg.success=function(x){that.afterUpload(x);};
+	cfg.failure=function(x){that.onFailure(x);};
+	this.webdavPut(cfg);
+}
+
+webdav.afterUpload=
+function(cfg){
+	this.parent.print('afterUpload');
+}
+
+webdav.tryMakeDir=
+function(cfg){
+	this.parent.print('tryMakeDir '+cfg.url);
+   let that=this;
+	cfg.failure=function(x){that.onFailure(x);};
+	this.webdavMakeDir(cfg);
+}
+
+
+webdav.request=
+function (cfg,verb,data){
+	this.parent.print('request['+verb+'] '+cfg.url);
+	let connRequest=new XMLHttpRequest();
+   let that=this;
+   let action=function(connRequest,cfg){that.checkResponse(connRequest,cfg);};
+	connRequest.addEventListener("loadend",action);
+	connRequest.open(verb, cfg.url);
+	connRequest.send(data);
+	//this.print('request['+verb+'] sent');
+}
+
+webdav.checkResponse=
+function(xrq,cfg){
+	//this.print('checkResponse: readyState '+xrq.readyState);
+	//this.print('checkResponse: status '+xrq.status);
+	if (xrq.status<400) {
+		//client errors 400-499
+		//server errors 500-599
+		cfg.success(cfg);
+		return;
+	}
+	cfg.status=xrq.status;
+	cfg.failure(cfg);
+}
+
+webdav.webdavMakeDir=function(cfg){ this.request(cfg,'MKCOL',null);}
+webdav.webdavCheck=function(cfg) { this.request(cfg,'GET',null);}
+webdav.webdavPut=function(cfg) { this.request(cfg,'PUT',cfg.data);}
+
+
+webdav.onFailuer=function(cfg){
+	this.parent.print('request failed with status='+cfg.status);
+}

+ 3230 - 0
web/crfTecant/crfVisit.js

@@ -0,0 +1,3230 @@
+var crfVisit={};
+
+crfVisit.config=new Object();
+
+crfVisit.setDebug=
+function(debug=null){
+   if (debug){
+      this.print=function(msg){debug.this.print(msg);};
+      this.clear=function(){debug.clear();}
+      return;
+   }
+   //provide default functions if not debug object is available
+   this.print=function(msg){console.log(msg);}
+   this.clear=function(){;}
+}
+
+crfVisit.setDebug();
+
+//harmonize signature
+//(schema,query,row,action=cvDoNothing,container=null
+
+crfVisit.insertRows=
+function(schema,query,rows,action=null,container=null,failure=null){
+   this.modifyRows('insert',schema,query,rows,action,container);
+}
+
+crfVisit.deleteRows=
+function(schema,query,rows,action=null,container=null,failure=null){
+   this.modifyRows('delete',schema,query,rows,action,container);
+}
+
+crfVisit.modifyRows=
+function(mode,schema,query,rows,action=null,container=null,failure=null){
+	//insert rows to container/schema/query and return with action
+	let fName="[cvModifyRows/"+mode+"]";
+	this.print(fName+' '+schema+'/'+query);
+	let qconfig=new Object();
+	qconfig.schemaName=schema;
+	qconfig.queryName=query;
+   if (container) qconfig.containerPath=container;
+   if (!rows) {
+      this.print(fName+' rows '+rows);
+      return;
+   }
+	qconfig.rows=rows;
+   qconfig.success=function(data){;};
+	if (action) qconfig.success=action;
+   if (mode=='insert') LABKEY.Query.insertRows(qconfig);
+   if (mode=='update') LABKEY.Query.updateRows(qconfig);
+   if (mode=='delete') LABKEY.Query.deleteRows(qconfig);
+	this.print(fName+" done");
+}
+
+crfVisit.selectRows=
+function(schema,query,filters=[],action=null, container=null, failure=null, columns=null){
+	let fName="[cvSelectRows]";
+	this.print(fName+' '+schema+' '+query+' '+container);
+	let qconfig=new Object();
+	qconfig.schemaName=schema;
+	qconfig.queryName=query;
+   if (container) qconfig.containerPath=container;
+   qconfig.filterArray=filters;
+   qconfig.success=function(data){;};
+	if (action) qconfig.success=action;
+   if (failure) qconfig.failure=failure;
+   if (columns) qconfig.columns=columns;
+	LABKEY.Query.selectRows(qconfig);
+	this.print(fName+" done");
+
+}
+
+crfVisit.createCrfStatus=
+function(crfEntry){
+   let crfStatus=new Object();
+   crfStatus.entryId=crfEntry.entryId;
+   crfStatus.submissionDate=new Date();
+   crfStatus.FormStatus=crfEntry.FormStatus;
+   crfStatus.User=crfEntry.UserId;
+   crfStatus.Form=crfEntry.Form;
+   return crfStatus;
+}
+
+crfVisit.init=
+function(cb=null){
+   let that=this;
+   let action=function(){that.scriptsLoaded(cb);};
+   LABKEY.Utils.requiresScript(["crfTecant/runQuery.js","crfTecant/crfReviewSection.js","crfTecant/participantIdManager.js","crfTecant/variableList.js","crfTecant/webdav.js","crfTecant/crfPrint.js"],action);
+}
+
+crfVisit.scriptsLoaded=
+function(cb=null){
+   participantIdManager.set(this);
+   webdav.set(this);
+   crfReviewSection.set(this);
+   crfPrint.set(this);
+   if (cb) cb();
+}
+
+
+crfVisit.getElement=
+function(id){
+   return this.config.document.getElementById(id);
+}
+
+crfVisit.setContainer=
+function(label,container){
+   let config=this.config;
+	if (!(config.formConfig.hasOwnProperty('container'))){
+		config.formConfig.container=new Array();
+	}
+	config.formConfig.container[label]=container;
+}
+
+crfVisit.getContainer=
+function(label){
+	return this.config.formConfig.container[label];
+}
+
+
+crfVisit.getCRFrefFirst=
+function(){
+	//crfRef is part of html call and gets stored in the page
+	return this.getElement(this.config.crfRefId).innerHTML;
+}
+
+crfVisit.getCRFref=
+function (){
+	//'crfRefId'
+	return this.config.formConfig.crfEntry['entryId'];
+}
+
+crfVisit.getCRFrefData=
+function(){
+	let parentCrf=this.config.formConfig.crfEntry['parentCrf'];
+	if (parentCrf!=undefined) return parentCrf;
+	return this.getCRFref();
+}
+
+crfVisit.onFailure=
+function(errorInfo, options, responseObj){
+	
+	if (errorInfo && errorInfo.exception)
+		alert("Failure: " + errorInfo.exception);
+	else
+		alert("Failure: " + responseObj.statusText);
+}
+
+crfVisit.doNothing=
+function (){
+	this.print('doNothing called');
+}
+
+crfVisit.getSnapshotObject=
+function(){
+   if (!("dataQueriesSnapshot" in this.config.formConfig))
+      this.config.formConfig.dataQueriesSnapshot=new Object();
+   return this.config.formConfig.dataQueriesSnapshot;
+}
+
+crfVisit.getQuerySnapshot=
+function(queryName){
+   //check whether queryName is in snapshotObject?
+   return this.getSnapshotObject()[queryName];
+}
+
+crfVisit.getLayoutObject=
+function(){
+   if (!("dataQueriesLayout" in this.config.formConfig))
+      this.config.formConfig.dataQueriesLayout=new Object();
+   return this.config.formConfig.dataQueriesLayout;
+}
+
+crfVisit.getQueryLayout=
+function(queryName){
+   //check whether queryName is in snapshotObject?
+   return this.getLayoutObject()[queryName];
+}
+
+crfVisit.getLookupObject=
+function(){
+   if (!("lookup" in this.config.formConfig))
+      this.config.formConfig.lookup=new Object();
+   return this.config.formConfig.lookup;
+}
+
+crfVisit.getLookup=
+function(queryName){
+   let x=this.getLookupObject();
+   if (queryName in x) return x[queryName];
+   return null;
+}
+
+crfVisit.getQueryList=
+function(){
+   if (!("queryList" in this.config.formConfig))
+      this.config.formConfig.queryList=new Object();
+   return this.config.formConfig.queryList;
+}
+
+crfVisit.getIdManager=
+function(){
+   if (!("idManager" in this.config.formConfig))
+      this.config.formConfig.idManager=participantIdManager.getObject();
+   return this.config.formConfig.idManager;
+}
+
+crfVisit.getAdditionalData=
+function(formSetupEntry){
+   //return information on additional data associated with the form
+   //additionalData is a sub-list with multiple entries per patient/visit
+   
+   let config=this.config;
+   //argument is the row of the formSetup setup list
+	let queryName=config.formConfig.queryMap[formSetupEntry['queryName']];
+	let fName='[getAdditionalData/'+queryName+']';
+	this.print(fName);
+
+   //additionalData holds a reference to all queries already parsed
+   //this helps in reducing number of calls to the database (I assume)
+	if (queryName in config.formConfig.additionalData){
+		this.print(fName+': Returning preset value');
+		return config.formConfig.additionalData[queryName];
+	}
+
+   //first time we see this query, so we have to do the setup
+	this.print(fName+': generating');
+	config.formConfig.additionalData[queryName]=new Object();
+	
+   //takes address, so further changes will be to the newly created object
+   //in fact, ad is just a short alias of the long variable name on the right
+	let ad=config.formConfig.additionalData[queryName];
+
+   //no additional data
+	if (formSetupEntry["showFlag"]==="NONE") {
+		this.print(fName+": empty");
+		return ad;
+	}
+
+   //use showFlag to setup report section of the CRF list
+	if (formSetupEntry["showFlag"]==="REVIEW") {
+		//abuse additionalData to signal different segment
+		this.print(fName+": generateReport");
+		ad.isReview=true;
+		return ad;
+	}
+
+   //setup the additionalData memory object
+	this.print(fName+': setting values');
+	ad.showFlag=formSetupEntry["showFlag"];
+	ad.showFlagValue=formSetupEntry["showFlagValue"];
+	ad.queryName=formSetupEntry["showQuery"];
+
+   //for data queries, limit to present CRF only
+	ad.filters=new Object();
+	ad.filters['crfRef']=this.getCRFref();
+
+   //compose a long debug message
+	let msg=fName+": flag "+ad.showFlag;
+	msg+=" value "+ad.showFlagValue;
+	msg+=" query "+ad.queryName;
+	this.print(msg);
+
+	return ad;	
+}
+
+crfVisit.selectFormSetupRows=
+function(formId){
+	let formSetupRows=new Array();
+   let config=this.config;
+	let allRows=config.formConfig.formSetup.rows;
+	for (let i=0;i<allRows.length;i++){
+		let formEntry=allRows[i];
+		if (formEntry.formName==formId)
+			formSetupRows.push(formEntry);
+	}
+	return formSetupRows;
+}
+
+crfVisit.findTitle=
+function(queryName){
+//find by name from formDatasets 
+//and set associated title as title
+   let config=this.config;
+	let rows=config.formConfig.formDatasets.rows;
+	for (let i=0;i<rows.length;i++){
+		let entry=rows[i];
+		if (entry['queryName']!=queryName) continue;
+		return entry['title'];
+	}
+	return "NONE";
+}
+
+
+crfVisit.makeSetup=
+function(listName){
+	//generate setup object whcih should contain fields:
+	//readonlyFlag - whether the dataset is writeable
+	//filters - selection fields that allow creation of LABKEY.Filter.create()
+	//getInputId - formating of unique ids for html elements
+   let fName='[Setup]';
+	this.print(fName+' '+listName);
+
+	let setup=new Object();
+	setup.queryName=listName;
+	setup.readonlyFlag=function(vName){return false};
+	setup.filters=new Object();
+	setup.filters['crfRef']=this.getCRFref();
+	setup.getInputId=function(vName){return listName+"_"+vName;}
+	setup.isReview=false;
+   return setup;
+	
+}
+
+crfVisit.getFullAccessSetup=
+function(listName){
+	//addApply - whether a submit/Save button is generated
+	let setup=this.makeSetup(listName);
+	setup.addApply="Save";
+	return setup;
+
+}
+
+crfVisit.getReadonlySetup=
+function(listName){
+   let setup=this.makeSetup(listName);
+	//see definition of setup object above, change readonly flag
+	setup.readonlyFlag=function(vName){return true};
+	return setup;
+}
+
+crfVisit.getSetup=
+function(listName,writeAccess=true){
+	//change to section granulated permission of type EDIT, COMMENT, READ
+	//let formStatus=config.formConfig.formStatus;
+	//equivalent to READ
+
+	if (!writeAccess)
+	//if (formStatus=="Submitted")
+		return this.getReadonlySetup(listName);
+	//if (formStatus=="Approved")
+	//	return readonlySetup(listName);
+	return this.getFullAccessSetup(listName);
+}
+
+crfVisit.generateSection=
+function(formSetupEntry){
+   let config=this.config;
+   let that=this;
+	let listName=config.formConfig.queryMap[formSetupEntry['queryName']];
+   //if (!listName) is for debugSection
+   if (!listName){
+      listName="debugSection";
+   }
+	let fName='[generateSection/'+listName+']';
+	let sectionTitle=formSetupEntry['title'];	
+	let accessModeColumn=config.formConfig.operator+'Mode';
+	let accessMode=formSetupEntry[accessModeColumn];
+	//this will fix it for later use as well
+	let additionalData=this.getAdditionalData(formSetupEntry);
+	this.print(fName);
+
+	let formName=config.masterForm;//this is HTML designator of area on page
+	let debug=true;
+	let tb=config.document.createElement('table');
+	tb.className='t2';
+	let row=tb.insertRow();
+	let cell=config.document.createElement('th');
+	row.appendChild(cell);	
+	cell.setAttribute("colspan","4");
+	cell.style.fontSize="20px";
+	cell.style.textAlign="center";
+	let cellData=config.document.createTextNode(sectionTitle);
+	cell.appendChild(cellData);
+	cell=row.insertCell();
+	let input=config.document.createElement("input");	
+	input.type="button";
+	input.value="Show";
+	input.id="toggle"+listName+"VisbilityButton";
+	input.onclick=function(){that.toggleVisibility(listName,input.id)};
+	cell.appendChild(input);
+	this.getElement(formName).appendChild(tb);
+
+	let div=config.document.createElement('div');
+	div.id=listName;
+	div.style.display="none";
+	this.getElement(formName).appendChild(div);
+
+   //here divert for debugArea
+   if (listName=="debugSection"){
+      let debugArea=config.document.createElement('textarea');
+      debugArea.rows=10;
+      debugArea.cols=95;
+      debugArea.id=config.debugId;
+      div.appendChild(debugArea);
+      return;
+   }
+
+
+
+	let divTable=config.document.createElement('div');
+	divTable.id=listName+"Table";
+	div.appendChild(divTable);
+	
+	if ("showFlag" in additionalData) {
+		additionalData.divName=listName+"SubDiv";
+		additionalData.divQueryName=listName+"SubDivList";
+
+		let div1=config.document.createElement('div');
+		div1.id=additionalData.divName;
+		div1.style.display="none";
+		div.appendChild(div1);
+	
+		let div2=config.document.createElement('div');
+		div2.id=additionalData.divQueryName;
+		div1.appendChild(div2);
+
+	}
+	this.print(fName+" generate master table");
+
+	let writeMode=accessMode=="EDIT";	
+	let setup=this.getSetup(listName,writeMode);
+	
+
+   if	("isReview" in additionalData){
+      crfReviewSection.set(this);
+      let action=function(){crfReviewSection.CB();};
+		crfReviewSection.generateSection(listName,div.id,action);
+		return;	
+	}
+	//master table is unique per visit
+
+	
+	setup.unique=true;
+	this.generateTable(listName,divTable.id,additionalData,setup);
+	
+	if (debug) this.print("generate master table: done");
+
+	let generateSubTable=true;
+	//generateSubTable equivalent to read/write access to section
+	if (accessMode != "EDIT")
+		generateSubTable=false;
+	
+	if (! ("showFlag" in additionalData) ) generateSubTable=false;
+	
+	if (generateSubTable){
+		let qName=additionalData.queryName;
+		let dName=additionalData.divName;
+		
+
+		let xsetup=this.getFullAccessSetup(qName);
+		//only set master query for additionalData
+		xsetup.masterQuery=listName;
+		//if (readonly) setup=readonlySetup(config);
+      xsetup.subTable=true;
+		this.generateTable(qName,dName,additionalData,xsetup);
+		//generateTable(formSetupEntry,qName,dName,additionalData,setup);
+	}
+
+	this.print("generate review");
+
+	let divReviewList=config.document.createElement('div');
+	divReviewList.id=listName+"ReviewList";
+	div.appendChild(divReviewList);
+	
+	let divReview=config.document.createElement('div');
+	divReview.id=listName+"Review";
+	div.appendChild(divReview);
+
+
+	//assume we already have listId (content of config.setupQueryName is listId)
+	//we need listName also
+	//qconfig.queryName=config.setupQueryName;
+	this.generateReview(divReview.id,divReviewList.id,listName,accessMode);
+
+	if (accessMode!='GENERATE') return;
+	this.print('Adding generate button');	
+	//add generateButton
+	let divGenerateButton=config.document.createElement('div');
+	divGenerateButton.id=listName+'GenerateButton';
+	div.appendChild(divGenerateButton);
+	this.print('Adding generate button completed to here');	
+   let cb=function(){that.onGenerateQuery(listName);};
+	this.generateButton(divGenerateButton.id,'Generate','Generate '+listName,'onGenerateQuery',cb);
+	this.print('Adding generate button completed');	
+}
+
+crfVisit.generateReview=
+function(divReviewId,divReviewListId, listName, accessMode){
+   let config=this.config;
+	let listId=config.formConfig.fields[listName].queryId;
+
+	//listId is a number->should it be queryName?
+	
+   let fName='[generateReview]';
+   this.print(fName+" list "+listId+'/'+listName);
+	
+	let reviewSetup=new Object();
+	reviewSetup.readonlyFlag=function(vName){
+		if (vName=="queryName") return true; 
+		if (vName=="queryname") return true; 
+		if (vName=="ModifiedBy") return true;
+		return false;};
+	reviewSetup.addApply="Add Review";
+   reviewSetup.reviewTable=true;
+
+	let generateTableFlag=true;
+	let formStatus=config.formConfig.formStatus;
+	//COMMENTS allowed or not
+	//three levels of access: EDIT, COMMENT, READ
+	if (accessMode == "READ"){
+	//if (formStatus == "Approved" ){
+		delete reviewSetup.addApply;
+		reviewSetup.readonlyFlag=function(vName){return false;}
+		generateTableFlag=false;
+	}
+	
+	reviewSetup.filters=new Object();
+	reviewSetup.filters["crfRef"]=this.getCRFref();
+   if (config.formConfig.crfEntry.parentCrf){
+      reviewSetup.filters["crfRef"]=this.getCRFref()+";"+config.formConfig.crfEntry.parentCrf;
+   }
+ 	reviewSetup.filters["queryName"]=listId;//entry in reviewComments list is queryname, all in small caps
+	//needs listName, in argument
+	
+	reviewSetup.getInputId=function(vName){return listName+"_add"+vName};
+	reviewSetup.divReviewListId=divReviewListId;
+	reviewSetup.isReview=true;	
+
+   let msg="Review: divId: "+divReviewId;
+   msg+=" inputId: "+reviewSetup.getInputId;
+   this.print(msg);
+	
+	this.updateListDisplay(divReviewListId,"reviewComments",reviewSetup.filters,true);
+
+	if (! generateTableFlag) return;
+
+	
+   this.generateTable("reviewComments",divReviewId,new Object(),reviewSetup);
+}	
+
+//>>>>>>>>>>trigger visibility of additional lists
+
+crfVisit.setListVisibility=
+function(input,setup,readonlyFlag){
+   let config=this.config;
+	let fName="[setListVisibility/"+setup.queryName+"]";
+	this.print(fName);
+	let additionalData=config.formConfig.additionalData[setup.queryName];
+	
+	let x = this.getElement(additionalData.divName);
+	this.print(fName+": Div: "+x);
+	x.style.display="none";
+
+	let sText;
+	if (readonlyFlag) sText=input.innerText;
+	else sText=input.options[input.selectedIndex].text;
+			
+	this.print(fName+": Selected option text: "+sText);
+
+	if (sText == additionalData.showFlagValue){
+		let filters=new Object();
+		if ("filters" in additionalData) filters=additionalData.filters;
+		x.style.display = "block";
+		this.updateListDisplay(additionalData.divQueryName,
+			additionalData.queryName,filters,readonlyFlag);
+	}
+}
+
+//>>have list refresh when data is added (not optimal yet)
+//
+
+crfVisit.updateListDisplay=
+function(divName,queryName,filters,readonlyFlag){
+	//use Labkey.QueryWebPart to show list
+
+	let fName="[updateListDisplay]";
+
+   this.print(fName+": UpdateListDisplay: Query - "+queryName
+      +" div - "+divName);
+
+	if (divName=="NONE") return;
+
+	let crfRef=this.getCRFref();
+	let div=this.getElement(divName);
+
+   this.print(fName+": generating WebPart: "+queryName);
+	
+	var qconfig=new Object();
+	qconfig.renderTo=divName;
+	//point to data container
+	qconfig.containerPath=this.getContainer('data');
+	qconfig.schemaName='lists'; 
+	qconfig.queryName=queryName;
+	qconfig.buttonBarPosition='top';
+	qconfig.filters=[];
+	for (f in filters){
+      let fType=LABKEY.Filter.Types.EQUAL;
+      this.print(fName+' filter ['+f+'] '+filters[f]+'/'+typeof(filters[f])+' ['+fType+']');
+     
+      if (variableList.isFilterList(filters[f])){
+         fType=LABKEY.Filter.Types.IN;
+      }
+		qconfig.filters.push(LABKEY.Filter.create(f, filters[f],fType));
+	}
+   let that=this;
+	qconfig.success=function(data){that.updateSuccess(data);};
+	qconfig.failure=function(errorInfo,options,responseObj){that.onFailure(errorInfo,options,responseObj);};
+	//show only print button
+	if (readonlyFlag){
+		qconfig.buttonBar=new Object();
+		qconfig.buttonBar.items=["print"];
+	}
+
+	LABKEY.QueryWebPart(qconfig);
+	
+}
+
+crfVisit.updateSuccess=
+function(data){
+	this.print("Update success");
+}
+
+//TODO: this should trigger a data refresh on section, ie populateData(field)
+crfVisit.toggleVisibility=
+function(divName,buttonName){
+	let fName='[toggleVisibility/'+divName+']';
+	this.print(fName);
+   let config=this.config;
+	let x = this.getElement(divName);
+	if (x.style.display === "none") {
+		//exclude non data sections (like debug)...
+		this.print(fName+': issuing setData(populateSection)');
+    		x.style.display = "block";
+		this.getElement(buttonName).value="Hide";
+      let that=this;
+		let cb=function(){that.populateSection(divName);};
+		this.setData(cb);
+
+  	} else {
+    		x.style.display = "none";
+		this.getElement(buttonName).value="Show";
+
+  	}
+}
+
+crfVisit.generateButton=
+function(divName,caption,label,callbackLabel,callback=null){
+	this.print("generateButtonX");
+   let config=this.config;
+	
+	let tb=config.document.createElement('table');
+	tb.className="t2";
+	
+	let r1=tb.insertRow();
+	th=config.document.createElement('th');
+	r1.appendChild(th);
+	th.innerHTML=caption;
+	//*!*
+	let c2=r1.insertCell();
+	let i1=config.document.createElement("input");	
+	i1.type="button";
+	i1.value=label;
+	i1.style.fontSize="20px";
+   let that=this;
+   if (callback)
+      i1.onclick=callback;
+   else
+	   i1.onclick=function(){that[callbackLabel]();};
+	i1.id='button_'+callbackLabel;
+	c2.appendChild(i1);	
+
+	let c1=r1.insertCell();
+	c1.setAttribute("colspan","1");
+	//this is only for saveReview?
+	c1.id=divName+'_reportField';
+	//c1.id=config.submitReportId;
+
+	let el=this.getElement(divName);
+	this.print("generateButton: element["+divName+"]: "+el);
+	
+	
+	el.appendChild(tb);
+	
+	
+}
+crfVisit.generateSubQuery=
+function(input, setup, readonlyFlag){
+	let fName="[generateSubQuery]";
+   let config=this.config;
+	if (setup.isReview) return;
+
+	if (!(setup.queryName in config.formConfig.additionalData)){
+		this.print(fName+': no additionalData entry (probably a subquery)');
+		return;
+	}
+
+	let additionalData=config.formConfig.additionalData[setup.queryName];
+	if (!("showFlag" in additionalData))
+		return;
+
+	this.print(fName);
+		
+	let expId=setup.getInputId(additionalData.showFlag);
+	if (expId!=input.id) {
+		this.print(fName+": ignoring field "+input.id+"/"+expId);
+		return;
+	}
+
+	this.print(fName+": Setting onChange to "+input.id);
+	if (readonlyFlag)
+      return;
+
+   let that=this;
+	input.onchange=function(){that.setListVisibility(input,setup,readonlyFlag)};
+}
+
+
+//>>populate fields
+//
+//
+//split to field generation and field population
+//
+crfVisit.addFieldRow=
+function(tb,field,setup,additionalData){
+
+	let fName="[addFieldRow/"+setup.queryName+':'+field.name+']';
+   let config=this.config;
+
+	let vName=field.name;
+	let vType=field.type;
+	let isLookup=("lookup" in field);
+	this.print(fName+": ["+vName+"/"+vType+'/'+isLookup+"]");
+
+	let row=tb.insertRow();
+	let cell=config.document.createElement('th');
+   cell.style.width='300px';
+	row.appendChild(cell);
+	
+	let text = config.document.createTextNode(field.shortCaption);
+	cell.appendChild(text);
+		
+	
+	let input=null;
+   let colSpan="3";
+	let cell1=row.insertCell();
+	cell1.colSpan=colSpan;
+	let readonlyFlag=setup.readonlyFlag(vName);
+
+
+	//set the html input object
+	while (1){
+
+		if (readonlyFlag){
+			input=config.document.createElement('label');
+			input.innerText='Loading';
+			break;
+		}
+	
+
+		//lookup
+		if (isLookup){
+			input = config.document.createElement("select");
+			break;
+		}
+
+		//date
+		if (vType=="date"){ 
+			input = config.document.createElement("input");
+			input.type="date";
+			break;
+		}
+
+		//string
+		if (vType=="string"){
+			//we have to make sure UNDEF is carried to below
+			//since we are adapting file to either show
+			//current file or allow user to select a file
+			//
+			//TODO change this so one can always select file
+			//but also show the selected file
+
+			if(vName.search("reviewComment")>-1){
+				input = config.document.createElement("textarea");
+				input.cols="65";
+				input.rows="5";
+				break;
+			}
+
+			input=config.document.createElement('input');
+			input.type="text";
+			
+			if (vName.search('_file_')<0) break;
+			cell1.setAttribute('colspan',"1");
+			let cell2=row.insertCell();
+			cell2.setAttribute('colspan',"2");
+			let input1=config.document.createElement('input');
+			input1.type="file";
+			input1.id=setup.getInputId(vName)+'_file_';
+			cell2.appendChild(input1);
+			break;
+				
+		}
+
+
+		if (vType=="float"){
+			input = config.document.createElement("input");
+			input.type="text";
+			break;
+		}	
+		
+		
+		if (vType=="boolean"){
+			input = config.document.createElement("input");
+			input.type="checkbox";
+			this.print("Creating checkbox");
+			break;
+		}
+		break;
+	}
+	
+	input.id=setup.getInputId(vName);
+	cell1.appendChild(input);
+	this.print(fName+': adding element '+input.id);
+	this.print(fName+': listing element '+this.getElement(input.id));
+	
+
+	//connect associated list
+	this.generateSubQuery(input,setup,readonlyFlag);	
+
+	if (readonlyFlag) {
+		this.print(fName+': exiting(readonlyFlag)');
+		return;
+	}
+	
+	if (!isLookup) 	{
+		this.print(fName+': exiting (not lookup)');
+		return;
+	}
+
+	let lookup=field["lookup"];
+
+	//get all values from config.formConfig.lookup[X]
+	let lObject=config.formConfig.lookup[lookup.queryName];
+	
+	//debug
+	this.print(fName+": query: "+lookup.queryName);
+	this.print(fName+": ElementId: "+input.id);
+	this.print(fName+": No of options: " +lObject.LUT.length);
+	this.print(fName+": Element: "+input);
+
+	//set the lut value (input is text label) for readonly
+
+   	//clear existing fields from input	
+	for(let i = input.options.length; i >= 0; i--) {
+		input.remove(i);
+   	}
+	
+	//create option -1
+	let opt = config.document.createElement("option");
+	opt.text = "<Select>";
+	opt.value = -1;
+	input.options[0] = opt;
+	this.print(fName+": Adding <Select>");
+	
+
+	//add other, label them with LUT
+	for (let v in lObject.LUT) {
+		this.print(fName+': populating '+v+': '+lObject.LUT[v]);
+
+		let opt = config.document.createElement("option");
+		opt.text = lObject.LUT[v];
+		opt.value = v;
+		input.options[input.options.length] = opt;
+		
+	}
+	input.selectedIndex=0;	
+
+}
+
+crfVisit.addSpecialFieldRows=
+function(tb,specFieldSetup,setup){
+   //tb is the table, specFieldSetup is a row from the table where special fields are being setup
+   //the first column is fieldUID, which is a colon joined amalgation of queryName:fieldName
+   let fieldUID=specFieldSetup["fieldUID"];
+   let x=fieldUID.split(':');
+   let fieldName=x[1];
+   let fName="[addSpecialFieldRow/"+fieldUID+"]";
+   let q=variableList.parseVariables(specFieldSetup['actionParameters']);
+   let config=this.config;//for add data
+   this.print(fName);
+   if (specFieldSetup['actionType']=='textArea'){
+      let row=tb.insertRow();
+      let cell1=row.insertCell();
+      cell1.colSpan="4";
+      cell1.style.textAlign="justify";
+      cell1.style.padding="10px";
+      cell1.style.backgroundColor="#e0e0e0";
+      cell1.innerText=q['description'];
+      return;
+   }
+   if (specFieldSetup['actionType']=='generationObject'){
+      //only in EDIT mode!!
+      let ro=setup.readonlyFlag(fieldName);
+      if (ro) return;
+      generateRegistration.set(this);
+      let gc=generateRegistration.getObject(q,setup.getInputId(fieldName));
+      let that=this;
+      let action=function(){that.doNothing();};
+      if ('mailRecipient' in q){
+         gc.callback=function(data){that.sendEmail(data,q['mailRecipient'],action,q['subject']);};
+      }
+      else 
+         gc.callback=function(data){that.doNothing();};
+      if ("addData" in q){
+         vars=q["addData"].split(',');
+         gc.addData=new Array();
+         for (let v in vars){
+            let s=vars[v]
+            //variable name can be written as A/B where A is the name in addData and B is the variable name in crfEntry
+            //useful for mocking up crfId from daughter crf-s such as registration
+            let sArray=s.split('/');
+            let sTarget=sArray[0];
+            let sSource=sArray[sArray.length-1];
+            gc.addData[sTarget]=config.formConfig.crfEntry[sSource];
+            this.print(fName+" addData ["+sTarget+"]: "+gc.addData[sTarget]);
+         }
+      }
+      let row=tb.insertRow();
+      let cell=config.document.createElement('th');
+      row.appendChild(cell);
+      let text = config.document.createTextNode("Automatic ID generator");
+      cell.appendChild(text);
+      let cell1=row.insertCell();
+      cell1.colSpan="3";
+      let b=config.document.createElement("input");
+      b.type="button";
+      b.id="generateIdButton";
+      b.onclick=function(){generateRegistration.execute(gc);};
+      b.value="Generate ID";
+      cell1.appendChild(b);
+
+   }
+}
+
+crfVisit.populateFieldRow=		
+function(entry,field,setup){
+	this.populateField(entry,field,setup);
+	this.populateSubQuery(entry,field,setup);
+}
+
+crfVisit.populateSubQuery=
+function(entry,field,setup){
+	let fName='[populateSubQuery/'+setup.queryName+':'+field.name+']';
+   let config=this.config;
+	if (setup.isReview) return;
+	
+	if (!(setup.queryName in config.formConfig.additionalData)){
+		let msg=fName+': no additionalData entry for '+setup.queryName;
+		msg+=' (probably a subquery)';
+		this.print(msg);
+		return;
+	}
+	//find if field is connected to a sub array
+	//find queryName
+	//
+	let additionalData=config.formConfig.additionalData[setup.queryName];	
+	this.print(fName);
+	//let flag=additionalData.showFlag;
+	
+	if (!("showFlag" in additionalData)) return;
+	let eId=setup.getInputId(additionalData.showFlag);
+	let id=setup.getInputId(field.name);
+	
+	if (eId!=id) {
+		this.print(fName+": ignoring field "+id+"/"+eId);
+		return;
+	}
+	
+	this.print(fName+': id '+id);
+	//hard to estimate readonlyFlag
+	//
+	let input=this.getElement(id);
+	let eType=input.nodeName.toLowerCase();
+	let readonlyFlag=eType!="select";
+	this.setListVisibility(input,setup,readonlyFlag);
+
+}
+
+crfVisit.clearField=
+function(field,setup){
+   let foo=new Object();
+   this.populateField(foo,field,setup);
+}
+
+crfVisit.populateField=
+function(entry,field,setup){
+
+	let vName=field.name;
+	let fName='[populateFieldName/'+vName+']';
+   let config=this.config;
+
+	let varValue="UNDEF";
+
+	//if (vName in setup.filters) varValue=setup.filters[vName];
+	if (vName in entry) varValue=entry[vName];
+	//if part of the filter, set it to value
+	if (vName in setup.filters) varValue=setup.filters[vName];
+	
+	let isLookup=("lookup" in field);
+	
+	this.print(fName+' v='+varValue+'/'+isLookup+' ['+
+		setup.getInputId(field.name)+']');
+	
+	let vType=field.type;
+	let id=setup.getInputId(vName);
+	let input=this.getElement(id);
+
+		
+	//date
+	if (vType=="date"){
+		if (varValue=="UNDEF") varValue=new Date();
+		else varValue=new Date(varValue);
+	}
+	
+	//lookup for readonly
+	if (isLookup && varValue!="UNDEF"){
+		let lookup=field["lookup"];
+		//get all values from config.formConfig.lookup[X]
+		let lObject=config.formConfig.lookup[lookup.queryName];
+		varValue=lObject.LUT[varValue];
+	}
+
+	this.print('Element: '+input);
+	//figure out the element type
+	let eType=input.nodeName.toLowerCase();
+	this.print('Element type: '+eType);
+
+	//change varValue for printing
+	if (varValue=="UNDEF") varValue="";
+	//HTMLTextArea, createElement(textArea)
+	if (eType==="textarea"){
+		input.value=varValue;
+		return;
+	}
+	//Text, createTextNode
+	if (eType==="#text"){
+		input.nodeValue=varValue;
+		return;
+	}
+	//HTMLLabelElement, createElement('label')
+	if (eType==="label"){
+		input.innerText=varValue;
+		return;
+	}
+
+	//HTMLSelectElement, createElement('select')
+	if (eType==="select"){
+		input.selectedIndex=0;
+		for (let i=0;i<input.options.length;i++){
+			let v=input.options[i].text;
+			if (v!=varValue) continue;
+			input.selectedIndex=i;
+			break;
+		}
+		return;
+	}
+
+	if (eType!="input"){
+		this.print('Unknown type: '+eType+' encountered, igonring');
+		return;
+	}
+	
+	//HTMLInputElement
+	let type=input.type;
+
+	if (type=="date"){
+		input.valueAsDate=varValue;
+		return;
+	}
+	//string,float
+	if (type=="text"){
+		input.value=varValue;
+		return;
+	}
+	//boolean
+	if (type=="checkbox"){
+		input.checked=varValue;
+		return;
+	}
+	this.print('Unknown input type: '+type+'. Ignoring.');
+}
+
+crfVisit.populateTable=
+function(listName,writeMode){
+//function populateTable(formSetupEntry){
+	//let listName=config.formConfig.queryMap[formSetupEntry['queryName']];
+	//let accessMode=config.formConfig.operator+'Mode';
+	//let writeMode=formSetupEntry[accessMode]=='EDIT';
+
+	let fName='[populateTable/'+listName+']';
+   
+
+	let setup=this.getSetup(listName,writeMode);
+	let entry=new Object();
+	
+	//data snapshot
+	let fQuery=this.getQuerySnapshot(listName);
+   let queryLayout=this.getQueryLayout(listName);
+
+	//here I assume that listName was parsed during setDataLayout and setData 
+	//so that rows was set (even if they are empty)
+	this.print(fName+"]: nrows "+fQuery.rows.length);
+	
+	if (fQuery.rows.length>0)
+		entry=fQuery.rows[0];
+	
+	let fields=queryLayout.fields;
+		
+	for (f in fields){	
+		let field=fields[f];
+		//each field is a new row
+		this.print(fName+": Adding field: "+f+'/'+field.name+' hidden: '+field.hidden+' type:'+field.type);
+		if (field.hidden) continue;
+		if (field.name=="crfRef") continue;
+		this.populateFieldRow(entry,field,setup);
+		
+	}
+
+}
+
+crfVisit.generateTable=
+function(listName,divName,additionalData,setup){
+	let fName="[generateTable/"+listName+"]";	
+   let config=this.config;
+	this.print(fName);
+
+	//is listName and setup.queryName a duplicate of the same value
+	this.print(fName+': setup.queryName '+setup.queryName);	
+	//assume data is set in config.formConfig.dataQueries[data.queryName].rows;
+   let populateData=true;
+   if ("subTable" in setup){
+      this.print(fName+" is subTable");
+      populateData=false;
+   }
+
+	let entry=new Object();
+
+
+
+	//data snapshot
+	let fQuerySnapshot=this.getQuerySnapshot(listName);
+   let queryLayout=this.getQueryLayout(listName);
+	//here I assume that listName was parsed during setDataLayout and setData 
+	//so that rows was set (even if they are empty)
+	this.print(fName+": Nrows "+fQuerySnapshot.rows.length);
+	
+	if (fQuerySnapshot.rows.length>0)
+		entry=fQuerySnapshot.rows[0];
+
+   
+   if ("reviewTable" in setup){
+      entry['reviewComment']='';
+      delete entry["ModifiedBy"];
+   }
+	
+	let tb=config.document.createElement('table');
+	tb.className="t2";
+	this.getElement(divName).appendChild(tb);
+
+	//this are the fields (probably constant)
+	let fields=queryLayout.fields;
+		
+	for (f in fields){
+		let field=fields[f];
+      let fieldUID=listName+":"+field.name;
+		//each field is a new row
+		this.print(fName+": Adding field: "+f+'/'+field.name+' ('+fieldUID+').');
+      //unique name
+		if (field.hidden) continue;
+		if (field.name=="crfRef") continue;
+		this.addFieldRow(tb,field,setup,additionalData);
+		if (populateData) this.populateFieldRow(entry,field,setup);
+      if (fieldUID in config.formConfig["specialFields"]){
+         let specFieldSetup=config.formConfig["specialFields"][fieldUID];
+         this.addSpecialFieldRows(tb,specFieldSetup,setup);
+      }
+
+		
+	}
+   //finish of if apply button is not required
+	if (!("addApply" in setup)) {
+		this.print(fName+"populateTable: done");
+		return;
+	}
+	
+	let row=tb.insertRow();
+
+	let th=config.document.createElement('th');
+	row.appendChild(th);
+	th.innerHTML=setup.addApply; 
+	let cell=row.insertCell();
+	//cell.setAttribute("colspan","2");
+	let input=config.document.createElement("input");	
+	input.type="button";
+	input.value=setup.addApply;
+	cell.appendChild(input);	
+	let cell1=row.insertCell();
+	cell1.setAttribute("colspan","2");
+	cell1.id=setup.getInputId("rerviewLastSave");
+	cell1.innerHTML="No recent update";
+	//saveReview is a generic name for saving content of the html page to a list entry
+   let that=this;
+	input.onclick=function(){that.saveReview(listName,cell1.id,setup)};
+}	
+
+crfVisit.setEntryFromElement=
+function(entry,elementId, field){
+   //set value to entry from element using representation (field) from labkey
+   //
+   //
+   
+   let fName='setEntryFromElement';
+   let config=this.config;
+
+   let el=this.getElement(elementId);
+				
+   if (!el) {
+      this.print(fName+" element: "+elementId+" not found");
+      return;
+   }
+   this.print(fName+" element: "+elementId);
+      
+
+   let vName=field.name;
+   let vType=field.type;
+
+   let eType=el.nodeName.toLowerCase();
+
+   if (eType==="select"){
+      entry[vName]=el.options[el.selectedIndex].value;
+      return;
+   }
+
+   if (eType==="td"){
+      entry[vName]=el.innerText;
+      return;
+   }
+   
+   if (vType=="date"){
+      let date=el.valueAsDate;
+      if (!date) return;
+
+      date.setUTCHours(12);
+      entry[vName]=date.toString();
+      this.print(fName+" setting date to "+entry[vName]);
+      return;
+   }
+
+   if (vType=="string"){
+      entry[vName]=el.value;
+      
+      if (vName.search('_file_')<0) 
+         return;
+      
+      //upload file
+      let id1=elementId+'_file_';
+      let input1=this.getElement(id1);
+      this.print(fName+' attachment field: '+input1.value);
+      //entry[vName]=el.files[0].stream();
+      let ctx=new Object();
+      ctx['dirName']='consent';
+      ctx['ID']=entry['crfRef'];
+      //should point to data container
+      ctx['project']=getContainer('data');
+      //need ID->crf!
+      //assume crfRef will get set before this
+      //element is encountered
+      this.uploadFile(input1,ctx);
+      let fv=el.value;
+      let suf=fv.split('.').pop();
+      entry[vName]=entry['crfRef']+'.'+suf;
+      return;
+      
+   }	
+   if (vType=="float" || vType=="int"){
+      entry[vName]=el.value;
+      
+      if (vName=="queryName") {
+         this.print(fName+' parsing queryName: '+el.innerText);
+         entry[vName]=config.formConfig.fields[el.innerText].queryId;
+         //use queryMap lookup
+      }
+      return;
+   }	
+   if (vType=="boolean"){
+      entry[vName]=el.checked;
+      return;
+	}
+   return;
+}
+
+crfVisit.saveReview=
+function(queryName,elementId,setup){
+	//loads any queryName
+
+	let debug=true;
+   let fName='[saveReview/'+queryName+']';
+	this.print(fName+" elementId "+elementId);
+
+
+   let unique=("unique" in setup);
+	
+   //data snapshot
+	let fQuerySnapshot=this.getQuerySnapshot(queryName);
+   let nRows=fQuerySnapshot.rows.length;
+   
+   let mode='insert';
+   
+   //data layout 
+	let queryLayout=this.getQueryLayout(queryName);
+
+
+	let entry=new Object();
+	
+   //determine mode based on entry uniqueness and presence of data
+   if (unique && nRows>0){
+		entry=fQuerySnapshot.rows[0];
+      mode='update';
+ 	}
+
+   this.print(fName+' unique '+unique+' mode '+mode+' nRows '+nRows);
+
+	entry.crfRef=this.getCRFrefData();
+
+	this.print(fName+" set crfRef="+entry.crfRef);
+
+
+	let fields=queryLayout.fields;
+	for (f in fields){
+
+		let field=fields[f];
+		this.print(fName+" saveReview field: "+field.name);
+		if (field.hidden) continue;
+		
+		let vName=field.name;
+		let vType=field.type;
+
+		this.print(fName+" vType: "+vType);
+		
+		if (vName=="crfRef") continue;
+		//need to save queryName for reviewComments
+		
+		let eId=setup.getInputId(vName);
+      //copy values from form to entry
+      this.setEntryFromElement(entry,eId,field);
+
+      //clear field value
+      if (!unique) this.clearField(field,setup);
+
+	}
+   let that=this;
+	let action=function(data){that.updateLastSavedFlag(data,setup,elementId)};
+
+   this.modifyRows(mode,'lists',queryName,[entry],action,this.getContainer('data'));
+
+}
+
+crfVisit.updateLastSavedFlag=
+function(data,setup,elementId){
+   let fName='[updateLastSavedFlag]';
+	let config=this.config;
+   this.print(fName+" update last saved flag to "+elementId);
+	let el=this.getElement(elementId);
+	let dt=new Date();
+	el.innerHTML="Last saved "+dt.toString();
+	if (data.queryName=="reviewComments"){
+		this.updateListDisplay(setup.divReviewListId,"reviewComments",setup.filters,true);
+	}	
+	//refresh stored data!
+	let writeMode=!setup.readonlyFlag();
+   let that=this;
+	if ("unique" in setup)
+		this.setData(function (){that.populateTable(data.queryName,writeMode);});
+	if ("masterQuery" in setup){
+		let ad=config.formConfig.additionalData[setup.masterQuery];
+		this.print('Updating list display: '+setup.queryName+'/'+ad.queryName);
+		this.updateListDisplay(ad.divQueryName,ad.queryName,ad.filters,false);
+	}
+}
+
+//******************************************upload to database *********************
+
+crfVisit.onDatabaseUpload=
+function(){
+	let fName='[onDatabaseUpload]';
+	this.print(fName);
+   let config=this.config;
+	config.upload=new Object();
+   let fc=new Object();
+   let pM=this.getIdManager();
+   fc.participantId=participantIdManager.getParticipantIdFromCrfEntry(pM);
+	this.print(fName+' id '+fc.participantId);
+   this.afterParticipantId(fc);
+}
+
+crfVisit.afterParticipantId=
+function(fc){
+	this.print("Setting participantId to "+fc.participantId);
+   let config=this.config;
+	config.upload.participantId=fc.participantId;
+	//another select rows to update all queries from setup
+	//just use registration for test
+	let formSetupRows=config.formConfig.formSetupRows;
+	config.upload.queries=new Array();
+	this.print("Form rows: "+formSetupRows.length);
+	for (let i=0;i<formSetupRows.length;i++){
+		let entry=formSetupRows[i];
+		//skip reviews
+		if (entry.showFlag=="REVIEW") continue;
+		//use lookup table to convert from id to name
+		let queryName=config.formConfig.queryMap[entry.queryName];
+		config.upload.queries.push({queryName:queryName,queryStatus:"QUEUED"});
+		this.print('form ['+i+']='+queryName+' '+entry.showFlag+'/'+entry.showQuery);
+		if (entry.showQuery=="NONE")
+			continue;
+		config.upload.queries.push({queryName:entry.showQuery,queryStatus:"QUEUED"});
+	}
+	//add reviews
+	config.upload.queries.push({queryName:"reviewComments",queryStatus:"QUEUED"});
+	config.upload.queryId=0;
+	this.copyToDataset();
+
+}
+
+
+crfVisit.copyToDataset=
+function(){
+	let fName='[copyToDataset]: ';
+   let config=this.config;
+	this.print(fName+'['+config.upload.queryId+'/'+config.upload.queries.length+']');
+	//watch dog + scheduler
+	//
+   
+   let that=this;
+
+	//watchdog part
+	if (config.upload.queryId==config.upload.queries.length) {
+		this.print(fName+'completing');
+		let targetStatus=config.formConfig.targetStatus['onDatabaseUpload'];
+		let targetRecipient=config.formConfig.targetRecipient['onDatabaseUpload'];
+		let action=new Object();
+		action.name='onDatabaseUpload';
+      let redirect=function(){that.redirect();};
+		action.cb=function(data){that.sendEmail(data,targetRecipient,redirect,'Form uploaded');}
+		this.updateFlag(targetStatus,action);//Approved
+		return;
+	}
+
+	//scheduler
+	let queryName=config.upload.queries[config.upload.queryId].queryName;
+	this.print("copyToDataset["+config.upload.queryId+"/"+
+			config.upload.queries.length+"]: "+queryName);
+
+	let filters=[LABKEY.Filter.create('crfRef',this.getCRFref())];
+   let action=function(data){that.afterListData(data);};
+   this.selectRows('lists',queryName,filters,action,this.getContainer('data'));
+}
+
+crfVisit.afterListData=
+function(data){
+	let fName='[afterListData]: ';
+   let config=this.config;
+
+	let queryName=config.upload.queries[config.upload.queryId].queryName;
+	this.print(fName+" ["+queryName+"/list]: "+data.rows.length+" entries");
+	config.upload.queries[config.upload.queryId].listData=data;
+	let id=config.upload.participantId;
+
+	let filters=[LABKEY.Filter.create('crfRef',this.getCRFref()),LABKEY.Filter.create('ParticipantId',id)];
+   let that=this;
+   let action=function(data){that.afterStudyData(data);};
+   this.selectRows('study',queryName,filters,action,this.getContainer('data'));
+}
+
+crfVisit.afterStudyData=
+function(data){
+
+	let fName='[afterStudyData]: ';
+   let config=this.config;
+	let queryObj=config.upload.queries[config.upload.queryId];
+	queryObj.studyData=data;
+
+	let msg=fName+"["+queryObj.queryName+"/study]: "+data.rows.length+" entries";
+	this.print(msg);
+	
+
+	let listRows=queryObj.listData.rows;
+	//skip uploading an empty set
+	if (listRows.length==0){
+		this.printErr("List "+queryObj.queryName+" empty.");
+		queryObj.queryStatus="DONE";
+		config.upload.queryId+=1;
+		//back to watchdog
+		this.copyToDataset();
+		return;
+	}
+	
+
+	let studyRows=queryObj.studyData.rows;
+
+	for (let i=0;i<studyRows.length;i++){
+		let entry=studyRows[i];
+		//
+		if (! (i<listRows.length) ) continue;
+		let entryList=listRows[i];
+		//keeps study only variables (ParticipantId, SequenceNum)
+		for (let f in entryList) {
+			entry[f]=entryList[f];
+			this.print(fName+"Copying ["+f+"]: "+entry[f]+"/"+entryList[f]);
+		}
+	}
+	this.print(fName+' copying completed');
+
+	if (studyRows.length>0) {
+      let that=this;
+      let action=function(data){that.afterStudyUpload(data);};
+      this.modifyRows('update','study',queryObj.queryName,studyRows,action,this.getContainer('data'));
+		this.print(fName+'updateRows sent');
+
+	}
+	else{
+		let data=new Object();
+		data.rows=new Array();
+		this.afterStudyUpload(data);
+	}
+}
+
+crfVisit.afterStudyUpload=
+function(data){
+	let fName='[afterStudyUpload] ';
+   let config=this.config;
+   let that=this;
+	this.print(fName);
+	//let participantField=config.participantField;
+	let participantField=config.formConfig.studyData["SubjectColumnName"];
+	this.print(fName+' participantField: '+participantField);
+
+	let queryObj=config.upload.queries[config.upload.queryId];
+	let queryName=queryObj.queryName;
+	this.printErr("Updated "+data.rows.length+" rows to "+queryName);
+	
+	let studyRows=queryObj.studyData.rows;
+	let listRows=queryObj.listData.rows;
+	
+	let rows=new Array();
+	//also updating existing rows, if they exist
+	for (let i=studyRows.length;i<listRows.length;i++){
+		let entry=listRows[i];
+		//make sure you have the participantField right
+		//
+		entry[participantField]=config.upload.participantId;
+		entry.crfRef=this.getCRFref();
+		entry.SequenceNum=this.getCRFref();
+		entry.SequenceNum=entry.SequenceNum % 1000000000;
+		
+		if (listRows.length>1){
+			entry.SequenceNum+=i/100;
+		}
+		this.print( "Adding sequence number "+entry.SequenceNum);
+		rows.push(entry);
+	}
+	if (rows.length>0){
+      let action=function(data){that.afterListUpload(data);};
+      this.insertRows('study',queryName,rows,action,this.getContainer('data'));
+	}
+	else{
+		let data=new Object();
+		data.rows=rows;
+		this.afterListUpload(data);
+	}
+			
+}
+
+crfVisit.afterListUpload=
+function(data){
+	let config=this.config;
+	let queryObj=config.upload.queries[config.upload.queryId];
+	let queryName=queryObj.queryName;
+	this.printErr("Inserted "+data.rows.length+" rows to "+queryName);
+	queryObj.queryStatus="DONE";
+	config.upload.queryId+=1;
+	this.copyToDataset();
+
+}
+
+
+
+
+//*************************update for further review *************************
+crfVisit.onUpdateForReview=
+function(){
+   let config=this.config;
+	let targetStatus=config.formConfig.targetStatus['onUpdateForReview'];
+	let targetRecipient=config.formConfig.targetRecipient['onUpdateForReview'];
+	let action=new Object();
+	action.name='onUpdateForReview';
+   let that=this;
+   let redirect=function(){that.redirect();};
+	action.cb=function(data){that.sendEmail(data,targetRecipient,redirect,'Form updated for review');};
+
+	this.updateFlag(targetStatus,action);
+}
+
+crfVisit.updateFlag=
+function(flag,action){
+	let fName='[updateFlag 1]';
+   let config=this.config;
+
+	let entry=config.formConfig.crfEntry;
+	entry.FormStatus=flag;
+	let uId=config.formConfig.currentUser.UserId;
+	entry[config.formConfig.operator]=uId;
+
+	this.print(fName+': Form: '+entry.Form);
+	this.print(fName+": set form status to "+entry.FormStatus);
+   let that=this;	
+	let cb=function(data){that.completeWithFlag(data,action);};
+   this.modifyRows('update','lists','crfEntry',[entry],cb,this.getContainer('data'));
+		
+}
+
+
+crfVisit.completeWithFlag=
+function(data,action){
+	let fName='[completeWithFlag]';
+	this.print(fName+': nrows '+data.rows.length);
+
+	let fentry=data.rows[0];
+	this.print(fName+': form status '+fentry.FormStatus);
+	this.print(fName+': form '+fentry.Form);
+
+	let crfStatus=this.createCrfStatus(fentry);
+   let config=this.config;
+	crfStatus.operator=config.formConfig.operator;
+	crfStatus.action=action.name;
+   let that=this;
+   let cb=function(){that.doNothing();};
+   if (action.cb) cb=action.cb;
+
+   this.insertRows('lists','crfStatus',[crfStatus],cb,this.getContainer('data'));
+
+}
+
+//************************************************ submit *******************************************
+
+crfVisit.onSubmit=
+function(){
+	//update list storage and change status
+
+	this.hideErr();
+	this.clearErr();
+	this.printErr("onSubmit");
+   let that=this;
+   let action=function(){that.verifyData();};
+	this.setData(action);
+	
+
+
+}
+
+crfVisit.verifyData=
+function(){
+	let fName='[verifyData]';
+   let config=this.config;
+   let qList=this.getQueryList();
+   let that=this;
+   let doNothing=function(data){that.doNothing();};
+	for (q in qList){
+		let qData=this.getQuerySnapshot(q);
+		if (q=="reviewComments") continue;
+		//copy snapshot to history
+      if (qData.rows.length==0){
+         this.print(fName+' no rows for '+q);
+      }
+      else
+		   this.insertRows('lists',q+'History',qData.rows,doNothing,this.getContainer('data'));
+		//if it doesn't have additionalData, it is a sub query
+		if (!(q in config.formConfig.additionalData)){
+			continue;
+		}
+		if (qData.rows.length<1){
+			this.printErr('Missing entry for query '+q);
+			return false;
+		}
+	}
+	//this is necessary only for Generated to Generation completed step
+	let actionSettings=config.formConfig.actionSettings['onSubmit'];	
+	if (variableList.hasVariable(actionSettings,"updateRegistration")){
+		this.updateRegistration();
+	}
+	let targetStatus=config.formConfig.targetStatus['onSubmit'];	
+	let targetRecipient=config.formConfig.targetRecipient['onSubmit'];
+	this.print(fName+' targetStatus: '+targetStatus);
+	
+	let finalStep=function(){that.redirect();};
+	if (variableList.hasVariable(actionSettings,"finalStep")){
+		//set to doNothing to remain on submit window
+		if (actionSettings.finalStep=="doNothing"){
+			finalStep=doNothing;
+		}
+	}
+	
+	let action=new Object();
+	action.name='onSubmit';
+	action.cb=function(data){that.sendEmail(data,targetRecipient,finalStep,'Form sumbitted');};
+	this.updateFlag(targetStatus,action);
+}
+
+crfVisit.getEmail=
+function(recipientCode){
+
+	this.print('getEmail w/'+recipientCode);
+   let config=this.config;
+	let recipients=new Array();
+	let typeTo=LABKEY.Message.recipientType.to;
+	let create=LABKEY.Message.createRecipient;
+	let currentUser=config.formConfig.currentUser;
+	let formCreator=config.formConfig.formCreator;
+	let currentSite=config.formConfig.currentSite;
+	let userRows=config.formConfig.userRows;
+	let parentUser=undefined;
+	if ("parentCrfData" in config.formConfig){
+		let parentCrf=config.formConfig.parentCrfData;
+		parentUser=this.getUser(parentCrf.rows[0].UserId,'parentUser');
+	}
+	
+
+	let recipientCategories=recipientCode.split(',');
+	for (let i=0;i<recipientCategories.length;i++){
+
+		let recipient=recipientCategories[i];
+		this.print('Checking '+recipient);
+		if (recipient=='crfEditor'){
+			this.print('Adding :'+formCreator.Email);
+			recipients.push(create(typeTo,formCreator.Email));
+			if (parentUser==undefined) continue;
+			this.print('Adding :'+parentUser.Email);
+			recipients.push(create(typeTo,parentUser.Email));
+			continue;
+		}
+		//Monitor or Sponsor
+		let fList=recipient+'s';
+		let fRows=config.formConfig[fList];
+		for (let i=0;i<fRows.length;i++){
+			this.print('Checking '+fRows[i].User+'/'+fRows[i].Site);
+			if (fRows[i].Site!=currentSite.siteNumber) continue;
+			for (let j=0;j<userRows.length;j++){
+				if (userRows[j].UserId!=fRows[i].User) continue;
+				this.print('Adding :'+userRows[j].Email);
+				recipients.push(create(typeTo,userRows[j].Email));
+				break;
+			}
+		}
+	}
+
+	return recipients;
+}
+
+crfVisit.sendEmail=
+function(data,recipient='crfEditor',cb=null,subj='Form submitted'){
+
+	this.print('sendEmail; recipient: '+recipient);
+   let config=this.config;
+   let that=this;
+
+   if (!cb)
+      cb=function(){that.redirect();};
+
+	let st=config.formConfig.settings;
+	let cvar='sendEmail';
+	if (cvar in st){
+		this.print(cvar+' set to '+st[cvar]);
+		if (st[cvar]=='FALSE'){
+			this.print('Skipping sending emails');
+			cb();
+			return;
+		}
+	}
+	if (recipient==null){
+      this.print('Skipping sending emails w/ no recipients');
+      cb();
+      return;
+   }
+
+
+	this.print('send email '+data.rows.length);
+	let crf=data.rows[0]['entryId'];
+	let formId=data.rows[0]['Form'];
+	let link=LABKEY.ActionURL.getBaseURL();
+	link+=LABKEY.ActionURL.getContainer();
+	link+='/crf_tecant-visit.view?';
+	link+='entryId='+crf;
+	link+='&formId='+formId;
+	link+='&role='+recipient;
+
+	//debug
+	let recipients=this.getEmail(recipient);
+	//from crfManagers list
+	
+	let typeHtml=LABKEY.Message.msgType.html;
+	let typePlain=LABKEY.Message.msgType.plain;
+	let msg1=LABKEY.Message.createMsgContent(typePlain,link);
+
+	//let cb=doNothing;
+	//let cb=redirect;
+	LABKEY.Message.sendMessage({
+		msgFrom:'labkey@fmf.uni-lj.si',
+		msgSubject:subj,
+		msgRecipients:recipients,
+		msgContent:[msg1],
+		success: cb
+	});
+
+}
+
+crfVisit.hideErr=
+function(){
+	let el=this.getElement("errorDiv");
+	el.style.display="none";
+}
+
+crfVisit.clearErr=
+function(){
+	let el=this.getElement("errorTxt");
+	el.value="";
+}
+
+crfVisit.showErr=
+function(){
+	let el=this.getElement("errorDiv");
+	el.style.display="block";
+}
+
+crfVisit.printErr=
+function(msg){
+	this.showErr();
+	el=this.getElement("errorTxt");
+	el.style.color="red";
+	el.value+="\n"+msg;
+}
+
+
+//**************************************************
+//
+crfVisit.onRemoveCRF=
+function(){
+   let fName='[onRemoveCRF]';
+   let config=this.config;
+	config.inputListsIterator=0;
+   this.print(fName+' starting loop');
+
+   //let rd=function(data){redirect();};
+   //let cb=function(){cvInsertRows('lists','crfStatus',[crfStatus],rd,getContainer('data'));};
+   let that=this;
+   let action=function(){that.redirect();}; 
+   let cb=function(){that.removeCrfEntries(action);};
+	this.removeCRFLoop(cb);
+}
+
+crfVisit.removeCRFLoop=
+function(cb){
+   let fName='[removeCRFLoop()]';
+   let config=this.config;
+   let that=this;
+
+	let i=config.inputListsIterator;
+	let iMax=config.formConfig.inputLists.rows.length;
+	//in fact, we are adding two additional passages of the loop, one for 
+	//crfEntry, the second for the same query, but using parentCrf as the 
+	//selection variable
+	//let iTotal=iMax+1;
+	//let iTotal=iMax+1;
+   //
+   let actionSettings=config.formConfig.actionSettings['onRemoveCRF'];
+   let queryNameDeleteWithParentCrf='NONE';
+   if (variableList.hasVariable(actionSettings,'removeWithParentCrf')){
+      queryNameDeleteWithParentCrf=actionSettings['removeWithParentCrf'];
+   }
+	
+	this.print(fName+" ["+i+"/"+iMax+"]");
+
+	if (!(i<iMax)){
+      cb();
+      return;
+	}
+	//in all but crfEntry, variable is called crfRef
+	let queryName=config.formConfig.inputLists.rows[i].queryName;
+   let idVar="crfRef";
+   let idValue=config.formConfig.crfEntry['entryId'];
+
+	//delete also crfEntries where parentCrf is set to crf that we are deleting
+
+   if (queryNameDeleteWithParentCrf==queryName){
+      idValue=config.formConfig.crfEntry['parentCrf'];
+   }
+	
+	this.print(fName+" ["+i+"/"+iMax+"] "+queryName+":"+idVar+'/'+idValue);
+
+   let filters=[LABKEY.Filter.create(idVar,idValue)];
+   let action=function(data){that.removeListCRF(data,cb);};
+   let failure=function(errorInfo){that.skipListCRF(errorInfo,cb);};
+   this.selectRows('lists',queryName,filters,action,this.getContainer('data'),failure);
+	//selectRows.failure=skipListCRF;
+}
+
+crfVisit.removeListCRF=
+function(data,cb){
+   let fName="[removeListCRF]";
+   let config=this.config;
+   let that=this;
+	this.print(fName+" "+data.queryName+": "+data.rows.length);
+
+	config.inputListsIterator+=1;
+	
+	if (data.rows.length==0){
+		this.removeCRFLoop(cb);
+		return;
+	}
+
+   let action=function(data){that.removeCRFLoop(cb)};
+   this.deleteRows(data.schemaName,data.queryName,data.rows,action,this.getContainer('data'));
+	
+}
+
+crfVisit.skipListCRF=
+function(errorInfo,cb){
+   let fName='[skipListCRF]';
+	this.print(fName+" error in removeCRF: "+errorInfo.exception);
+   let config=this.config;
+
+	config.inputListsIterator+=1;
+	
+	this.removeCRFLoop(cb);
+	
+}
+
+crfVisit.removeCrfEntries=
+function(cb){
+	let queryName="crfEntry";
+	let idVar="entryId";
+	let crfRef=this.getCRFref();
+   let that=this;
+
+   let filters=[LABKEY.Filter.create('entryId',crfRef)];
+   let action=function(data){that.deleteAndUpdateCrfStatus(data,null);}
+   this.selectRows('lists',queryName,filters,action,this.getContainer('CRF'));
+   let action1=function(data){that.deleteAndUpdateCrfStatus(data,cb);}
+   let filters1=[LABKEY.Filter.create('parentCrf',crfRef)];
+   this.selectRows('lists',queryName,filters1,action1,this.getContainer('CRF'));
+}
+
+crfVisit.deleteAndUpdateCrfStatus=
+function(data,cb){
+   let fName='[deleteAndUpdateCrfStatus]';
+   let config=this.config;
+   let that=this;
+   let rows=data.rows;
+   let stack=new Array();
+   stack.push(cb);
+   for (let i=0;i<rows.length;i++){
+      //generate crfStatus entry out of crfEntry
+      let crfStatus=this.createCrfStatus(rows[i]);
+      crfStatus.action='onRemoveCRF';
+      crfStatus.FormStatus=config.formConfig.targetStatus[crfStatus.action];
+      this.print(fName+' status '+crfStatus.FormStatus);
+      crfStatus.operator=config.formConfig.operator;
+      let k=stack.length-1;
+      let containerPath=this.getContainer('CRF');
+      stack.push(function(){that.insertRows('lists','crfStatus',[crfStatus],stack[k],containerPath);});
+      let k1=k+1;
+      stack.push(function(){that.deleteRows('lists','crfEntry',[rows[i]],stack[k1],containerPath);});
+   }
+   //execute the whole stack
+   let m=stack.length-1;
+   stack[m]();
+}
+
+crfVisit.redirect=
+function(){
+	let debug=false;
+	let formUrl="begin";
+	let params=new Object();
+	params.name=formUrl;
+	params.pageId="CRF";
+
+	//points to crf container
+	let containerPath=this.getContainer('CRF');
+        
+	// This changes the page after building the URL. 
+	//Note that the wiki page destination name is set in params.
+        
+	var homeURL = LABKEY.ActionURL.buildURL(
+			"project", formUrl , containerPath, params);
+        this.print("Redirecting to "+homeURL);
+	if (debug) return;	 
+	window.location = homeURL;
+
+	
+
+}
+
+//master section, entry point from html files
+crfVisit.generateMasterForm=
+function(){
+   let that=this;
+   let action=function(){that.setFormConfig();}
+   this.init(action);
+}
+
+
+//helper function to set basic parameters on web page 
+//(fields defined in html file)
+crfVisit.populateBasicData=
+function(){
+
+   let staticData=new Object();
+   let titles=new Object();
+   let config=this.config;
+   staticData['version']=config.formConfig.softwareVersion;	
+   titles['version']='Software version';
+   let varRows=config.formConfig['crfStaticVariables'].rows;
+   for (let i=0;i<varRows.length;i++){
+      let vName=varRows[i].staticVariable;
+      let val=config.formConfig.crfEntry[vName];
+      if (val==undefined) continue;
+      staticData[vName]=val;
+      titles[vName]=varRows[i].Title;
+   }
+	staticData['investigatorName']=config.formConfig.user['DisplayName'];
+   titles['investigatorName']='Investigator';
+   staticData['email']=config.formConfig.user['Email'];
+   titles['email']='Email';
+   staticData['siteName']=config.formConfig.currentSite['siteName'];
+   titles['siteName']='Site';
+   staticData['sitePhone']=config.formConfig.currentSite['sitePhone'];
+   titles['sitePhone']='Telephone(site)';
+
+   for (f in staticData){
+      this.addStaticData(f,titles[f],staticData[f]);
+   }
+}
+
+crfVisit.addStaticData=
+function(f,title,value){
+   let el=this.getElement(f);
+
+   //populate only
+   if (el!=undefined){
+      el.innerText=value;
+      return;
+   }
+   
+   //add row to table if element cannot be found
+   let table=this.getElement('staticTable');
+   let row=table.insertRow();
+   let cell=row.insertCell();
+   cell.innerText=title;
+   let cell1=row.insertCell();
+   cell1.id=f;
+   cell1.style.fontWeight='bold';
+
+   //populate
+   cell1.innerText=value;
+}
+
+//come here after the layout is read from labkey page
+//
+crfVisit.generateErrorMsg=
+function(msg){
+   let config=this.config;
+	let txt=config.document.createElement('p');
+	txt.innerText=msg;
+	this.getElement(config.masterForm).appendChild(txt);
+	this.generateButton("submitDiv",'Exit','Exit','redirect');
+}
+
+crfVisit.getUser=
+function(id,field){
+   let config=this.config;
+	if (field in config.formConfig) return config.formConfig[field];
+	let uRows=config.formConfig.userRows;
+	for (let i=0;i<uRows.length;i++){
+		let userId=uRows[i].UserId;
+		if (userId!=id) continue;
+		config.formConfig[field]=uRows[i];
+		return config.formConfig[field];
+	}
+	return null;
+}	
+
+crfVisit.afterConfig=
+function(){
+   let fName='[afterConfig]';
+   let config=this.config;
+	this.print(fName);	
+   
+	this.populateBasicData();
+	
+	//check if user has permission on the form
+	let currentUser=this.getUser(LABKEY.Security.currentUser.id,'currentUser');
+	let currentSite=config.formConfig.currentSite;
+	let formCreator=this.getUser(config.formConfig.crfEntry.UserId,'formCreator');
+	let formCreatorId=formCreator.UserId;
+
+
+
+	//let formSite=config.formConfig.crfEntry.Site;
+	let fList=config.formConfig.operator+'s';
+	let fRows=config.formConfig[fList];
+	//let currentSiteId=-1;
+	
+	//depending on operator mode, we should decide what is right
+	let operator=config.formConfig.operator;
+	if (operator=='crfEditor'){
+		//editor can only edit its own forms
+		if (currentUser.UserId!=formCreatorId){
+			let msg='User '+currentUser.DisplayName;
+			msg+=' has no permission on this form';
+			this.generateErrorMsg(msg);
+			return;
+		}
+	}
+	if (operator=='crfMonitor' || operator=='crfSponsor'){
+		//monitor can look at forms based on his site
+		//find monitor line
+		let operatorSites=new Array();
+		for (let i=0;i<fRows.length;i++){
+			if (fRows[i].User!=currentUser.UserId) continue;
+			operatorSites.push(fRows[i].Site);
+		}
+		this.print('operator Site: '+operatorSites.length);
+		if (operatorSites.length==0){
+			let msg='User '+currentUser.DisplayName;
+			msg+=' is not a '+operator;
+			this.generateErrorMsg(msg);
+			return;
+		}
+
+		let selectedSite=-1;
+		let siteCandidates="[";
+		for (let i=0;i<operatorSites.length;i++){
+			if (i>0) siteCandidates+=", ";
+			siteCandidates+=operatorSites[i];
+			if (operatorSites[i]!=currentSite.siteNumber) continue;
+			selectedSite=currentSite.siteNumber;
+			break;
+		}
+		siteCandidates+="]";
+		if (selectedSite==-1){
+			let msg='User '+currentUser.DisplayName;
+			msg+=' is not a '+operator+' for site ';
+			msg+=currentSite.siteName+'('+currentSite.siteNumber+')';
+			msg+='/'+siteCandidates;
+			this.generateErrorMsg(msg);
+			return;
+		}
+	}
+
+
+	this.print('User '+currentUser.DisplayName+'/'+
+		config.formConfig.currentSite['siteName']+
+		' acting as '+config.formConfig.operator);	
+
+
+	let rows=config.formConfig.crfButtons.rows;
+	config.formConfig.targetStatus=new Array();
+	config.formConfig.targetRecipient=new Array();
+	config.formConfig.actionSettings=new Array();
+
+	for (let i=0; i<rows.length; i++){
+		let action=rows[i].action;//String
+		let tstatus=rows[i].targetFormStatus;
+		let trecip=rows[i].targetRecipient;
+		config.formConfig.targetStatus[action]=tstatus;
+		config.formConfig.targetRecipient[action]=trecip;
+		//allow for settings to be promoted with each action (and potentially parsed and acted upon)
+		config.formConfig.actionSettings[action]=undefined;
+		let aSet=rows[i].actionSettings;
+		if (aSet){
+			config.formConfig.actionSettings[action]=variableList.parseVariables(aSet);
+			variableList.printVariables(this,config.formConfig.actionSettings[action]);
+		}
+
+	}
+	let formStatus=config.formConfig.formStatus;
+
+	//let functionArray=new Array();
+
+	this.print("Generating buttons for formStatus \""+ formStatus+"\"");
+
+   let allButtonRows=config.formConfig.crfButtons.rows;
+	let buttonRows=new Array();
+   
+   //specifying role=X in actionSettings will limit button to that role
+   for (let i=0;i<allButtonRows.length;i++){
+      let action=allButtonRows[i]['action'];
+      //filter on actionSettings
+      let as=config.formConfig.actionSettings[action];
+      if (variableList.hasVariable(as,'role')){
+         this.print('Role['+config.formConfig.operator+'/'+as['role']+'] limited for action '+action);
+         //mismatch skips addition of button to buttonRows
+         if (config.formConfig.operator!=as['role']) continue;
+      }
+      buttonRows.push(allButtonRows[i]);
+   }
+
+
+
+	for (let i=0;i<buttonRows.length;i++){
+		let bt=buttonRows[i];
+		//if (typeof window[bt.action]==="function"){
+		this.generateButton("submitDiv",bt.caption,bt.label,bt.action,null);
+		//}
+		//else{
+		//	this.print('No match for function :'+bt.action+
+		//		' obj: '+window[bt.action]);
+		//}
+	}
+
+	this.print('Here');
+
+
+	//here we should get data. For now, just initialize objects that will hold data
+   let that=this;
+   let action=function(){that.afterDataLayout();};
+	this.setDataLayout(action);//callback is afterDataLayout
+}
+
+crfVisit.afterDataLayout=
+function(){
+
+   let that=this;
+   let action=function(){that.afterData();};
+   //let action=function(){that.doNothing();};
+	this.setData(action);//callback is afterData
+}
+
+crfVisit.updateRegistration=
+function(){
+	let fName="[updateRegistration]";
+   let config=this.config;
+	this.print(fName);
+   let pM=this.getIdManager();
+	let idFieldName=participantIdManager.getCrfEntryFieldName(pM,"STUDY");
+	//have to reload query data
+	let regQueryPars=variableList.parseVariables(config.formConfig.settings['registrationQuery']);
+   let regQuery=regQueryPars['query'];
+	let fQuery=this.getQuerySnapshot(regQuery);
+
+	if (fQuery.rows.length==0) {
+		this.print(fName+" registration is empty");
+		return; //registration is empty
+	}
+	let regEntry=fQuery.rows[0];
+
+	for (x in regEntry){
+		this.print(fName+" ["+x+"] "+regEntry[x]);
+	}
+
+
+	let studyId=fQuery.rows[0][idFieldName];
+	if (!studyId) {
+		this.print(fName+" study id not set ("+idFieldName+'/'+studyId+")");
+		return; //study id not set
+	}
+	
+	//set 
+	participantIdManager.setParticipantIdToCrfEntry(pM,studyId,"STUDY");
+	//this will only update crfEntry in memory, but not on LabKey, 
+	//we are counting on updateFlag to follow updateRegistration
+
+	//update parentCRF as well, here we schedule update of data entry as well
+	if ("parentCrfData" in config.formConfig){
+		let parentCrfEntry=config.formConfig.parentCrfData.rows[0];
+		parentCrfEntry[idFieldName]=studyId;
+      let that=this;
+      let action={name:"updateRegistration",cb:function(){that.doNothing();}};
+		let cb=function(data){that.completeWithFlag(data,action);};
+      this.modifyRows('update','lists','crfEntry',[parentCrfEntry],cb,this.getContainer('CRF'));
+	}
+}
+
+crfVisit.afterData=
+function(){
+	let fName='afterData';
+   let config=this.config;
+	//operatorBasedAccessMode	
+	let accessMode=config.formConfig.operator+'Mode';
+	let rowsSetup=config.formConfig.formSetupRows;
+
+   let idMode=config.formConfig.form['idMode'];
+   //set default value if no value is in the list (read value is null)
+   if (!idMode) idMode="STUDY:EDIT";
+
+   this.print(fName+': idMode '+idMode);
+   //add print to config so participantManager can use it
+   let pM=this.getIdManager();
+   //extend object
+   let that=this;
+   let action=new Object();
+   action.name='updateCrfEntry';
+   action.cb=function(){that.doNothing();};
+   pM.updateCrfEntry=function(){that.updateFlag(config.formConfig.crfEntry['FormStatus'],action);};   
+
+   let idModeArray=idMode.split(':');
+   pM.mode="STUDY";
+   if (idModeArray.includes("LOCAL")) {
+      pM.mode="LOCAL";
+      //OK, but check if CRF or registration indicate that study id is already set
+      participantIdManager.verifyCrfStudyId(pM);
+	  //study id should already be set by updateRegistration
+      //verifyRegistration(pM);
+   }
+   if (idModeArray.includes("READONLY")){
+      pM.readOnly="TRUE";
+   }
+   
+   let pId=participantIdManager.getParticipantIdFromCrfEntry(pM);
+   if (!pId){
+      participantIdManager.setEditMode(pM);
+   }
+   else{
+      let label=pId;
+      if (pM.mode=="STUDY"){
+         let loc=participantIdManager.getParticipantIdFromCrfEntry(pM,'LOCAL');
+         label=pId+':'+loc;
+         pM.readOnly="true";
+      }
+      participantIdManager.setLabelMode(pM,label);
+      //in STUDY mode also change LOCAL ID from crfEntry
+      
+   }
+
+	for (let i=0;i<rowsSetup.length;i++){
+		let entry=rowsSetup[i];
+		let queryName=config.formConfig.queryMap[entry['queryName']];
+		
+		this.print(fName+" ["+queryName+"]: showFlag: "+entry["showFlag"]);
+		this.print(fName+" ["+queryName+"]: accessMode: "+entry[accessMode]);
+
+		const nData=this.getQuerySnapshot(queryName).rows.length;
+		
+		this.print(fName+" ["+queryName+"]: nData: "+nData);
+
+
+		//skip sections
+		//also from fields
+		if (entry[accessMode]=="NONE") continue;
+		//skip readonly empty records
+		//if (entry[accessMode]=="READ" && nData==0) continue;
+			
+		//let additionalData=new Object();
+		//setAdditionalData(additionalData,entry);
+		//section fits one dataset/list
+      
+		this.generateSection(entry);
+		//generateSection(queryName,entry["title"],entry[accessMode], 
+		//		additionalData);
+	}
+
+}
+
+crfVisit.findSetupRow=
+function(queryName,formId){
+   let config=this.config;
+	let rowsSetup=config.formConfig.formSetupRows;
+	for (let i=0;i<rowsSetup.length;i++){
+		let e=rowsSetup[i];
+		let queryName1=config.formConfig.queryMap[e['queryName']];
+		if (e.formName!=formId) continue;
+		if (queryName1!=queryName) continue;
+		return e;
+	}
+	return null;
+}
+
+crfVisit.populateSection=
+function(queryName){
+	let fName='[populateSection/'+queryName+']';
+   let config=this.config;
+	this.print(fName);
+
+   //old setting
+   let formId=config.formId;
+   //new setting
+   formId=config.formConfig.formId;
+
+	let entry=this.findSetupRow(queryName,formId);
+	//ignore names without associated entry in formSetup
+	if (entry==undefined){
+		this.print(fName+': no matching FormSetup entry found');
+		return;
+	}
+	//populate comes after generate, we should be pretty safe in taking
+	//already generated additionalData
+	
+	if (!(queryName in config.formConfig.additionalData)){
+		this.print(fName+': no additionalData generated for '+queryName);
+		return;
+	}
+	
+	let additionalData=config.formConfig.additionalData[queryName];
+	this.print(fName+': using additionalData '+additionalData);
+	if ("isReview" in additionalData){
+      let action=function(){crfReviewSection.CB();};
+		crfReviewSection.generateSection(queryName,queryName,action);
+		return;	
+	}
+
+	let accessMode=config.formConfig.operator+'Mode';
+	let aM=entry[accessMode];
+	this.print(fName+': accessMode '+aM);
+
+	if (aM!='GENERATE'){
+		let writeMode=entry[accessMode]=='EDIT';
+		this.print(fName+': mode='+writeMode);
+	   this.populateTable(queryName,writeMode);
+		return;
+	}
+
+	//deal with generate
+	//
+	//already available -> shift to READ mode
+	let divTable=queryName+'Table';
+	let divObj=this.getElement(divTable);
+	let divRev=this.getElement(queryName+'Review');
+	let divRLi=this.getElement(queryName+'ReviewList');
+	let divGBu=this.getElement(queryName+'GenerateButton');
+
+	this.print('div GBU: '+divGBu);
+	divObj.style.display="block";
+	divRev.style.display="block";
+	divRLi.style.display="block";
+	if (divGBu!=undefined) divGBu.style.display="none";
+
+	let nData=this.getQuerySnapshot(queryName).rows.length;
+	this.print('['+queryName+']: nrows '+nData);
+	if (nData>0){
+		this.populateTable(queryName,0);
+		return;
+	}
+	//hide table
+	divObj.style.display="none";
+	divRev.style.display="none";
+	divRLi.style.display="none";
+	if (divGBu!=undefined) divGBu.style.display="block";
+	//add buttons?
+	//is button already generated?
+	
+	//populateTable(entry);
+	
+}		
+
+//*******    generateQuery infrastructure *********************
+
+crfVisit.onGenerateQuery=
+function(queryName){
+
+   let fName='[onGenerateQuery]';
+	this.print(fName+' '+queryName);
+//
+   let config=this.config;
+	let cfgRows=config.formConfig.generateConfigData.rows;
+//	//queryName to queryId?
+	let queryId=config.formConfig.fields[queryName].queryId;
+	let cfgRow=undefined;
+
+	for (let i=0;i<cfgRows.length;i++){
+		if (cfgRows[i].queryId!=queryId) continue;
+		cfgRow=cfgRows[i];
+		break;
+	}
+	if (cfgRow==undefined){
+		this.print('generateConfig for queryName['+queryId+']='+queryName+' not found');
+		return;
+	}
+	//add config to the list
+	if (!("generateConfig" in config.formConfig)){
+		config.formConfig.generateConfig=new Object();
+	}
+	config.formConfig.generateConfig[queryName]=cfgRow;
+
+	if (!("generateForm" in config.formConfig)){
+		config.formConfig.generateForm=new Object();
+	}
+	
+//
+	let formRows=config.formConfig.formRows;	
+	let formId=cfgRow.formId;
+	for (let i=0;i<formRows.length;i++){
+		if (formRows[i].Key==formId) {
+			config.formConfig.generateForm[queryName]=formRows[i];
+			break;
+		}
+	}
+	//this.print('XcfgRow '+config.formConfig.generateForm[queryName]);
+//
+//	//check if all required datasets were at least saved
+	this.checkGenerationFields(queryName);
+}
+
+crfVisit.checkGenerationFields=
+function(queryName){
+   let fName='[checkGenerationFields]';
+   let config=this.config;
+	let genForm=config.formConfig.generateForm[queryName];
+	let genCfg=config.formConfig.generateConfig[queryName];
+	let mailRecipient=genCfg.emailRecipient;
+	
+	//list of queries that are part of Registration form
+	this.print(fName);	
+	this.print(fName+' setRecipient: '+mailRecipient);
+	let formId=genForm.Key;
+	this.print(fName+" Checking form w/id "+formId);
+	let selectGenerationRows=this.selectFormSetupRows(formId);
+	//registration rows
+	for (let i=0;i<selectGenerationRows.length;i++){
+		let row=selectGenerationRows[i];
+		let queryId=row.queryName;
+		let fQueryName=config.formConfig.queryMap[queryId];
+		if (fQueryName==queryName) continue;
+		let fQuery=this.getQuerySnapshot(fQueryName);
+		this.print('Checking '+fQueryName+' nrows: '+fQuery.rows.length);
+		if (fQuery.rows.length==0){ 
+			this.generateError(queryName,fQueryName);
+			return;
+		}
+	}
+	this.generateMessage(queryName,'Vailidation OK');
+	this.print('callback: set recipient: '+mailRecipient);
+   let that=this;
+	let cb=function(){that.prepareForm(queryName,formId,mailRecipient);};
+	this.generateListEntry(formId,queryName,cb);
+}
+
+
+crfVisit.prepareForm=
+function(queryName,formId,mailRecipient){
+   let fName="[prepareForm]";
+
+	this.print(fName+' recipient '+mailRecipient);
+
+	//look for existing registration entry
+   let that=this;
+	let action=function(data){that.generateForm(data,queryName,mailRecipient);};
+	let formFilter=LABKEY.Filter.create('Form',formId);
+	let parentCrfFilter=LABKEY.Filter.create('parentCrf',this.getCRFref());
+   let filters=[formFilter,parentCrfFilter];
+   this.selectRows('lists','crfEntry',filters,action,this.getContainer('data'));
+
+}
+
+crfVisit.generateError=
+function(queryName,fQueryName){
+	let elName=queryName+'GenerateButton'+'_reportField';
+	let el=this.getElement(elName);
+	el.innerText='Error: '+fQueryName+' was not set';
+	el.style.color='red';
+}
+
+crfVisit.generateMessage=
+function(queryName,msg){
+	let elName=queryName+'GenerateButton'+'_reportField';
+	let el=this.getElement(elName);
+	el.innerText=msg;
+	el.style.color='green';
+}
+
+crfVisit.generateForm=
+function(data,queryName,mailRecipient){
+
+   let fName='[generateForm]';
+
+	this.print(fName+' recipient: '+mailRecipient);
+//	
+	const nData=data.rows.length;
+	this.print(fName+' Registration: '+nData+' rows');
+
+   let config=this.config;
+	let formRow=config.formConfig.generateForm[queryName];
+	let formCfg=config.formConfig.generateConfig[queryName];
+
+	//we have to generate masterQuery with parentCrf and crfRef 
+	//and crfEntry with new entryId and parentCrf equal to crfRef
+	if (nData>0) {
+		this.generateMessage(queryName,'Registration already generated.');
+		return;
+	}
+	let formId=formRow.Key;
+	let formName=formRow.formName;
+	let crfBase=config.formConfig.crfEntry;
+	let crfEntry=new Object();
+	//add new reference
+	crfEntry.entryId=Date.now();
+	crfEntry.parentCrf=this.getCRFref();
+	crfEntry["Date"]=new Date();
+	crfEntry["View"]="[VIEW]";
+
+	crfEntry.formStatus=1;//In progress
+   //checks for both field presence (if not in query, undefined) and field value (if not set, null)
+   this.print(fName+' setup status: '+formCfg.formStatus);
+   if (formCfg.formStatus){
+      crfEntry.formStatus=formCfg.formStatus;
+   }
+
+   //get local Id
+   let pM=this.getIdManager();
+   
+   crfEntry[participantIdManager.getCrfEntryFieldName(pM)]=participantIdManager.getParticipantIdFromCrfEntry(pM);
+//	//set other variables
+	//requires studyData as part of formConfig
+//	let studyData=config.formConfig.studyData;
+	this.print('Adding study: '+crfBase.EudraCTNumber);
+	crfEntry.EudraCTNumber=crfBase.EudraCTNumber;
+	crfEntry.StudyCoordinator=crfBase.StudyCoordinator;
+	crfEntry.StudySponsor=crfBase.StudySponsor;
+	crfEntry.RegulatoryNumber=crfBase.RegulatoryNumber;
+//
+//	//find sponsor for site
+	let site=crfBase.Site;
+	let crfSponsors=config.formConfig.crfSponsors;
+	let users=config.formConfig.userRows;
+	for (let i=0;i<crfSponsors.length;i++){
+		//this.print('Checking for site '+crfSponsors[i].Site);
+		if (crfSponsors[i].Site!=site) continue;
+		config.formConfig.sponsorId=crfSponsors[i].User;
+		//this.print('Setting id '+config.formConfig.sponsorId);
+		//finds first
+		break;
+	}
+	for (let j=0;j<users.length;j++){
+		if (config.formConfig.sponsorId!=users[j].UserId) continue;
+		config.formConfig.sponsor=users[j];
+		//finds first (should be unique)
+		break;
+	}
+	this.print('Selecting '+config.formConfig.sponsor.DisplayName+' as sponsor');
+	//different user than the original form...
+	//should be set to the study sponsor
+	crfEntry.UserId=config.formConfig.sponsor.UserId;
+	crfEntry.Site=site;
+//	//set formId to one found through registration search
+	crfEntry.Form=formId;
+////
+
+   let crfStatus=this.createCrfStatus(crfEntry);
+   crfStatus.operator=config.formConfig.operator;
+   crfStatus.action='generateForm';
+
+   let that=this;
+   let action=function(){that.doNothing();};
+	let cb=function(data){that.sendEmail(data,mailRecipient,action,formName+' generated');}
+   let containerPath=this.getContainer('data');
+   let pass=function(data){that.insertRows('lists','crfStatus',[crfStatus],cb,containerPath);};
+   this.insertRows('lists','crfEntry',[crfEntry],pass,this.getContainer('data'));
+
+}
+
+crfVisit.generateListEntry=
+function(formId,queryName,cb){
+
+	//check if registration was already generated
+   let config=this.config;
+
+	let formRows=config.formConfig.formRows;
+	let qForm=undefined;
+	for (let i=0;i<formRows.length;i++){
+		if (formRows[i].Key!=formId) continue;
+		qForm=formRows[i];
+	}
+	let nData=this.getQuerySnapshot(queryName).rows.length;
+
+   if (nData>0) return;
+
+
+   //create new list entry
+   let pM=this.getIdManager();
+   
+
+	let e2=new Object();
+	e2.crfRef=this.getCRFref();
+	e2.registrationStatus=0;
+	e2.submissionDate=new Date();
+   e2[participantIdManager.getCrfEntryFieldName(pM)]=participantIdManager.getParticipantIdFromCrfEntry(pM);
+	this.print('set values');
+
+   this.insertRows('lists',queryName,[e2],cb,this.getContainer('data'));
+
+}
+		
+// ******************** end form generator (Registration) ********************
+
+//jump to populate table/generate review, etc defined at the begining of the file
+
+//entry point from generateMasterForm
+crfVisit.setFormConfig=
+function(){
+   let fName="[setFormConfig]";
+   let config=this.config;
+
+	//add object to store form related data
+	config.formConfig=new Object();
+
+	config.formConfig.softwareVersion='T.15.68';
+
+	this.print(fName+" generateMasterForm");	
+	
+	//set containers for data and configuration
+
+	//TODO: set this from a query
+	//
+	
+	this.setContainer('data',LABKEY.ActionURL.getContainer());
+	this.setContainer('config',LABKEY.ActionURL.getContainer());
+	this.setContainer('CRF',LABKEY.ActionURL.getContainer());
+
+	//this is local data
+   let that=this;
+   let action=function(data){that.afterSettings(data);};
+   this.selectRows('lists','crfSettings',[],action,this.getContainer('CRF'));
+	//store form related data to this object
+
+}
+
+
+crfVisit.afterSettings=
+function(data){
+   let fName='[afterSettings]';
+   let config=this.config;
+   
+	config.formConfig.settings=variableList.convertToDictionary(data.rows);
+
+	let st=config.formConfig.settings;
+	this.print('afterSettings');
+	for (let k in st){
+		this.print(fName+'\t'+k+'='+st[k]);
+	}
+
+	//if ('dataContainer' in st){
+	//	setContainer('data',st['dataContainer']);
+	//}
+	let vname='configContainer';
+	if (vname in st){
+		this.setContainer('config',st[vname]);
+	}
+	this.print('Config: '+this.getContainer('config'));
+	this.print('Data: '+this.getContainer('data'));
+
+	//use first-> we must first establish link to the rigth crf entry
+	let filters=[LABKEY.Filter.create('entryId',this.getCRFrefFirst())];
+   let that=this;
+   let action=function(data){that.afterCRFEntry(data);};
+   this.selectRows('lists','crfEntry',filters,action,this.getContainer('data'));
+}
+
+crfVisit.afterCRFEntry=
+function(data){
+   let config=this.config;
+   let fName='[afterCRFEntry]';
+	config.formConfig.crfEntry=data.rows[0];
+	this.print("Setting crfEntry (x) to "+config.formConfig.crfEntry["entryId"]);
+	//for empty records or those with parentCrf not set, parentCrf comes up as null
+	//nevertheless, with two equal signs, check against undefined also works
+	this.print('parentCrf set to '+config.formConfig.crfEntry.parentCrf);
+	
+	this.collectData();
+}
+
+crfVisit.collectData=
+function(){
+   let config=this.config;	
+   let targetObject=config.formConfig;
+	let queryArray=new Array();
+   //k
+	//site
+	queryArray.push(runQuery.makeQuery(targetObject,'config','site','siteData',[]));
+	//users
+	queryArray.push(runQuery.makeQuery(targetObject,'CRF','users','userData',[]));
+	queryArray[queryArray.length-1].schemaName='core';
+	//crfEditors
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfEditors','crfEditorsData',[]));
+	//crfMonitors
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfMonitors','crfMonitorsData',[]));
+	//crfSponsors
+	queryArray.push(runQuery.makeQuery(targetObject,'config','crfSponsors','crfSponsorsData',[]));
+
+   //study static data
+   queryArray.push(
+      runQuery.makeQuery(targetObject,'data','crfStaticVariables','crfStaticVariables',[]));
+
+   queryArray.push(runQuery.makeQuery(targetObject,'data','specialFields','specialFieldsQuery',[]));
+	//study
+	queryArray.push(runQuery.makeQuery(targetObject,'data','Study','studyDataAll1',[]));
+	let e=queryArray[queryArray.length-1];
+	//overload schema name
+	e.schemaName='study';
+	//make sure variables not part of default view are loaded
+	//here we should already have read crfStaticVariables table
+	e.columns="SubjectColumnName,EudraCTNumber,StudySponsor";
+	e.columns+=",StudyCoordinator,RegulatoryNumber";
+	
+	//formStatus
+	let varLabel='sourceFormStatus';
+	let formStatus=config.formConfig.crfEntry['FormStatus'];
+	let formFilter=LABKEY.Filter.create('Key',formStatus);
+	queryArray.push(
+		runQuery.makeQuery(targetObject,'config','FormStatus','formStatusData',[formFilter]));
+	//crfButtons
+	let statusFilter=LABKEY.Filter.create(varLabel,formStatus);
+	queryArray.push(
+		runQuery.makeQuery(targetObject,'config','crfButtons','crfButtons',[statusFilter]));
+	//Forms	
+	queryArray.push(runQuery.makeQuery(targetObject,'config','Forms','formData',[]));
+	//FormSetup	
+	queryArray.push(runQuery.makeQuery(targetObject,'config','FormSetup','formSetup',[]));
+	//generateConfig
+	queryArray.push(
+		runQuery.makeQuery(targetObject,'config','generateConfig','generateConfigData',[]));	
+	
+   //inputLists 
+	queryArray.push(
+		runQuery.makeQuery(targetObject,'config','inputLists','inputLists',[]));	
+   //parentCrf
+	let parentCrf=config.formConfig.crfEntry['parentCrf'];
+	if (parentCrf!=undefined){
+		let crfFilter=LABKEY.Filter.create('entryId',parentCrf);
+		queryArray.push(runQuery.makeQuery(targetObject,'data','crfEntry','parentCrfData',[crfFilter]));	
+	}	
+
+	this.print('running getDataFromQueries');
+   let that=this;
+   //let action=function(data){that.doNothing();};
+   let action=function(){that.addStudyData();};
+	runQuery.getDataFromQueries(this,queryArray,action);
+}
+
+crfVisit.addStudyData=
+function(){
+   let fName='addStudyData';
+   let config=this.config;
+
+   //convert specialFields to array
+   let q=config.formConfig["specialFieldsQuery"].rows;
+   config.formConfig.specialFields=variableList.convertToAssociatedArray(q,"fieldUID");
+   this.print(fName);
+	let queryArray=new Array();
+	
+   let targetObject=config.formConfig;
+	//study
+	queryArray.push(runQuery.makeQuery(targetObject,'data','Study','studyDataAll',[]));
+	//queryArray.push(runQuery.makeQuery('data','Study','studyDataAll',[]));
+	let e=queryArray[queryArray.length-1];
+	//overload schema name
+	e.schemaName='study';
+	//make sure variables not part of default view are loaded
+	//here we should already have read crfStaticVariables table
+   let staticVarRows=config.formConfig['crfStaticVariables'].rows;
+   let columnModel=""
+   for (let i=0;i<staticVarRows.length;i++){
+      if (i>0) columnModel+=',';
+      columnModel+=staticVarRows[i]['staticVariable'];
+   }
+	e.columns=columnModel;
+
+   //also collect ids already in study
+   //registrationQuery should be a dataset
+   //since monitors can review late, it might be profitable to use lists
+   //rather than study
+   let regQueryPars=variableList.parseVariables(config.formConfig.settings['registrationQuery']);
+   let regQuery=regQueryPars['query'];
+   let regSchema='study';
+   if ('schema' in regQueryPars){
+      regSchema=regQueryPars['schema'];
+   }
+   queryArray.push(runQuery.makeQuery(targetObject,'data',regQuery,'registrationData',[]));
+   queryArray[queryArray.length-1].schemaName=regSchema;
+      
+	let that=this;
+   let action=function(){that.fcontinue();};
+	runQuery.getDataFromQueries(this,queryArray,action);
+
+}
+
+crfVisit.fcontinue=
+function(){
+
+   //debug
+   let fName='[fcontinue]';
+   let config=this.config;
+   let varRows=config.formConfig['crfStaticVariables'].rows;
+   let studyVars=config.formConfig['studyDataAll'].rows[0];
+   for (let i=0;i<varRows.length;i++){
+      let vName=varRows[i].staticVariable;
+      this.print(fName+' '+vName+': '+studyVars[vName]);
+   }
+
+	//parse site
+	config.formConfig.siteRows=config.formConfig.siteData.rows;
+	let sRows=config.formConfig.siteRows;
+	for (let i=0;i<sRows.length;i++){
+		let siteId=sRows[i].siteNumber;
+		this.print('site '+siteId);
+		if (siteId==config.formConfig.crfEntry.Site){
+			config.formConfig.currentSite=sRows[i];
+			break;
+		}
+	}
+	//config.formConfig.site=data.rows[0];
+	this.print("Setting site name to "+config.formConfig.currentSite.siteName);
+	//study
+	config.formConfig.studyData=config.formConfig.studyDataAll.rows[0];
+	this.print("XSetting participantField to "+
+			config.formConfig.studyData["SubjectColumnName"]);
+	
+	config.formConfig.crfEditors=config.formConfig.crfEditorsData.rows;
+	config.formConfig.crfMonitors=config.formConfig.crfMonitorsData.rows;
+	config.formConfig.crfSponsors=config.formConfig.crfSponsorsData.rows;
+
+	config.formConfig.userRows=config.formConfig.userData.rows;
+	let uRows=config.formConfig.userRows;
+	for (let i=0;i<uRows.length;i++){
+		let userId=uRows[i].UserId;
+		if (userId==config.formConfig.crfEntry.UserId){
+			config.formConfig.user=uRows[i];
+			break;
+		}
+	}
+	//config.formConfig.user=data.rows[0];
+	this.print("Setting user to "+config.formConfig.user["DisplayName"]);
+
+
+
+	let fsRows=config.formConfig.formStatusData.rows;
+	config.formConfig.formStatus=fsRows[0].formStatus;
+	config.formConfig.operator=config.role;
+	//config.formConfig.operator=fsRows[0].operator;
+
+	this.print('Setting operator to: '+config.formConfig.operator);
+	
+	config.formConfig.formRows=config.formConfig.formData.rows;
+
+   //point formId to point to form set in crfEntry
+   config.formConfig.formId=config.formConfig.crfEntry['Form'];
+
+   //old setting, set from URL in visit.html
+   let formId=config.formId;
+   //new setting, set from crfEntry
+   formId=config.formConfig.formId;
+
+	let formRows=config.formConfig.formRows;
+	//filter out the current form
+	for (let i=0;i<formRows.length;i++){
+		if (formRows[i].Key==formId){
+			config.formConfig.form=formRows[i];
+			break;
+		}
+	}
+	
+	config.formConfig.formSetupRows=this.selectFormSetupRows(formId);
+
+	this.print("Number of datasets for form ["+formId+"]: "+
+			config.formConfig.formSetupRows.length);
+
+
+
+	let fields=config.formConfig.formSetup.metaData.fields;
+
+	//get the lookup for queryName column
+	let formQueryName='queryName';
+	let field="NONE";
+	for (f in fields){
+		if (fields[f]['name']!=formQueryName) continue;
+		field=fields[f];
+		break;
+	}
+	let lookup=field.lookup;
+
+	this.print("Getting dataset names from "+lookup.queryName);
+
+	//inputLists should be in configuration container
+   let that=this;
+   let action=function(data){that.afterFormDatasets(data);};
+   //let action=function(data){that.doNothing();};
+
+   this.selectRows(lookup.schemaName,lookup.queryName,[],action,this.getContainer('config'));
+
+}
+
+crfVisit.afterFormDatasets=
+function(data){
+   let fName='[afterFormDatasets]';
+	this.print(fName+' nrows '+data.rows.length);
+   let config=this.config;
+	config.formConfig.formDatasets=data;//inputLists
+	config.formConfig.fields=new Object();
+	config.formConfig.queryMap=new Object();
+	config.formConfig.additionalData=new Object();
+
+	let rows=config.formConfig.formSetupRows;
+
+	//should skip report only rows
+	for (let i=0;i<rows.length;i++){
+		let entry=rows[i];
+		let reviewField=(entry['showFlag']=='REVIEW');
+		//is the operator set yet?
+		let accessMode=config.formConfig.operator+'Mode';
+		let skipField=(entry[accessMode]=="NONE");
+		let queryId=entry['queryName'];
+		let lookupRows=config.formConfig.formDatasets.rows;
+		this.print('QueryID['+i+']='+queryId);
+		let dentry;
+
+		for (let j=0;j<lookupRows.length;j++){
+	
+			if (queryId!=lookupRows[j]['Key']) continue;
+			dentry=lookupRows[j];
+			break;
+		}
+		let qName=dentry['queryName'];
+
+		//update list of dataset formConfig is observing (fields/queryMap)
+		while (1){
+			//review contains no data
+			if (reviewField) break;
+			if (skipField) break;
+			//already in fields
+			if (qName in config.formConfig.fields) break;
+			config.formConfig.fields[qName]=new Object();
+			break;
+		}
+
+		while(1){
+			//already done
+			if (queryId in config.formConfig.queryMap) break;
+			config.formConfig.queryMap[queryId]=qName;
+			break;
+		}
+		if (reviewField) continue;
+		if (skipField) continue;
+		//only do this for real lists
+		let field=config.formConfig.fields[qName];
+		field.title=entry['title'];
+		field.queryId=queryId;
+
+	}
+	this.print("List of datasets in form : ");
+	for (f in config.formConfig.fields){
+		let field=config.formConfig.fields[f];
+		this.print("\t"+f+" ID: "+field.queryId+' title '+field.title);
+	}
+	this.afterConfig();
+
+}
+
+//>>>>>>>>>>>>>>>>>new>>>>>>>>>>>>
+crfVisit.setDataLayout=
+function(cb){
+   let fName='[setDataLayout]';
+   let config=this.config;
+   this.print(fName);
+	let rowsSetup=config.formConfig.formSetupRows;
+   let queryArray=new Array();
+	let dS=this.getLayoutObject();//reference only
+   let qList=this.getQueryList();
+	let qMap=config.formConfig.queryMap;
+	//config.formConfig.lookup=new Object();
+	for (let i=0;i<rowsSetup.length;i++){
+		let entry=rowsSetup[i];
+		//skip review rows
+		if (entry['showFlag']=='REVIEW')
+			continue;
+		let queryId=entry['queryName'];
+		let q=qMap[queryId];
+		queryArray.push(runQuery.makeQuery(dS,'data',q,q,[]));
+      qList[q]=0;
+      this.print(fName+' adding '+q);
+		if (entry['showQuery']!="NONE"){
+			let sq=entry['showQuery'];
+		   queryArray.push(runQuery.makeQuery(dS,'data',sq,sq,[]));
+         qList[sq]=0;
+         this.print(fName+' adding '+sq);
+			
+		}
+	}
+
+	//always add reviews
+   let q='reviewComments';
+   queryArray.push(runQuery.makeQuery(dS,'data',q,q,[]));
+   qList[q]=0;
+   let that=this;
+   let action=function(){that.processLayout(cb);};
+   runQuery.getDataFromQueries(this,queryArray,action);
+}
+
+//this happens after the for loop, so all dataQueries objects are set
+crfVisit.processLayout=
+function(cb){
+   let fName='[processLayout]';
+   let qList=this.getQueryList();
+   //for layouts
+   let queryArray=new Array();
+   let targetObject=this.getLookupObject();
+   let lookupSet=new Object();
+   for (let q in qList){
+      let qobject=this.getQueryLayout(q);
+	   this.print(fName+" inspecting layout for "+q+" "+qobject);
+	   qobject.fields=qobject.metaData.fields;
+	   qobject.title=this.findTitle(q);
+
+      //check for lookups
+	   for (let f in qobject.fields){
+		   //anything else is simple but lookup
+		   let field=qobject.fields[f];
+		   if (!("lookup" in field)) continue;
+         let lookup=field.lookup;
+         let qObject=this.getLookup(lookup.queryName);
+         if (qObject) continue;
+         //add to list
+         let qName=lookup.queryName;
+         let qCode=qName+':'+lookup.keyColumn+':'+lookup.displayColumn;
+         let e=runQuery.makeQuery(targetObject,'data',qName,qCode,[]);
+         //adjust minor settings
+         if (lookup.containerPath) e.containerPath=lookup.containerPath;
+         e.schemaName=lookup.schemaName;
+         e.columns=lookup.keyColumn+','+lookup.displayColumn;
+         lookupSet[qCode]=e;
+         this.print(fName+' inserting '+qCode);
+      }
+   }
+   for (let x in lookupSet){
+      queryArray.push(lookupSet[x]);
+      this.print(fName+' adding '+x);
+      for (let v in lookupSet[x]){
+         this.print(fName+' value ['+v+'] '+lookupSet[x][v]);
+      }
+   }
+   //this.print(fName+' print '+targetObject.print);
+   let that=this;
+   let action=function(){that.processLookup(cb);};
+   this.print(fName+' getDataFromQueries');
+   runQuery.getDataFromQueries(this,queryArray,action);
+   this.print(fName+' getDataFromQueries done');
+}
+
+crfVisit.processLookup=
+function(cb){
+   let fName="[processLookup]";
+
+   let obj=this.getLookupObject();
+   for (let q in obj){
+	   this.print(fName+" "+q);
+      let a=q.split(':');
+      if (a.length<3) continue;
+      let lookupName=a[0];
+      let key=a[1];
+      let val=a[2];
+      obj[lookupName]=new Object();
+      this.print(fName+' adding ['+lookupName+'] '+key+'/'+val);
+      let lObject=obj[lookupName];
+
+	   lObject.LUT=new Array();//key to value
+	   lObject.ValToKey=new Array();//value to key
+	   lObject.keyColumn=key
+	   lObject.displayColumn=val;
+      
+      let qRows=obj[q].rows;
+	   for (let i=0;i<qRows.length;i++){
+         let r=qRows[i];
+         this.print(fName+' LUT ['+r[key]+'] '+r[val]);
+		   lObject.LUT[r[key]]=r[val];
+		   lObject.ValToKey[r[val]]=r[key];
+	   }
+   }
+	cb();
+}
+
+crfVisit.setData=
+function(cb){
+	fName='[setData]';
+	let crfMatch=this.getCRFref();
+   let config=this.config;
+	let parentCrf=config.formConfig.crfEntry['parentCrf'];
+	if (parentCrf!=undefined) crfMatch=parentCrf;
+
+	this.print(fName+' form crf ['+this.getCRFref()+'] matching for crfRef='+crfMatch);
+
+   let queryArray=new Array();
+   let targetObject=this.getSnapshotObject();
+	//collect data and execute callback cb for queries in cb.queryList
+   let qList=this.getQueryList();
+	for (q in qList){
+
+		let filters=[LABKEY.Filter.create("crfRef",crfMatch)];
+      queryArray.push(runQuery.makeQuery(targetObject,'data',q,q,filters));
+
+	}
+   runQuery.getDataFromQueries(this,queryArray,cb);
+}
+
+crfVisit.uploadFile=
+function(inputElement,context){
+	//context should have ID and dirName attributes; 
+	//path will be dirName/ID/fieldName_ID.suf
+	//where suf is identical to localPath content picked from
+	//inputElement
+	this.print('uploadFile: '+inputElement.value+'/');
+	if (inputElement.type=="text") return;
+	this.print('uploadFile: '+inputElement.files+'/');
+	this.print('uploadFile: '+inputElement.files.length+'/');
+	if (inputElement.files.length>0){
+		let file=inputElement.files[0];
+		this.print('uploadFile: '+inputElement.value+'/'+file.size);
+      webdav.uploadFile(file,context);
+   }
+}
+
+crfVisit.printForm=
+function(){
+   crfPrint.printForm();
+}