Browse Source

Adding pdf generation support via pdfkit and blob-stream, remodelling data collection in crfVisit.js and adding email support on Submit

Andrej Studen 3 years ago
parent
commit
c20e2e09df

+ 6 - 2
views/visit.view.xml

@@ -2,8 +2,12 @@
 	<dependencies>
 		<dependency path="crf/crfVisit.js"/>
 		<dependency path="crf/crfReview.js"/>
-		<!--<dependency path="https://github.com/devongovett/pdfkit/releases/download/v0.10.0/pdfkit.standalone.js" />-->
-		<!--<dependency path="https://github.com/devongovett/blob-stream/releases/download/v0.1.3/blob-stream.js"/>-->
+		<!--local copy of pdfkit, version 0.10.0-->
+		<!--https://github.com/devongovett/pdfkit/releases/download/v0.10.0/pdfkit.standalone.js-->
+		<dependency path="crf/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-->
+		<dependency path="crf/blob-stream.js"/>
 
  </dependencies>
 </view>

File diff suppressed because it is too large
+ 0 - 0
web/crf/blob-stream.js


+ 8 - 0
web/crf/blob-stream.js.license

@@ -0,0 +1,8 @@
+MIT LICENSE
+Copyright (c) 2014 Devon Govett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 329 - 125
web/crf/crfVisit.js

@@ -828,23 +828,24 @@ function generateTable(listName,divName,unique,additionalData,setup){
 	//add temp variables
 	//this will capture the state of the data prior to updating
 	let qconfig=new Object();
-	qconfig.containerPath=config.containerPath;
+	//qconfig.containerPath=config.containerPath;
 	qconfig.schemaName='lists';
 	qconfig.queryName=listName;
 	//apply filters on data to get right entries
 	qconfig.filterArray=[];
-	for (f in setup.filters){
+	for (let f in setup.filters){
+		print('Filtering on '+f+' val '+setup.filters[f]);
 		qconfig.filterArray.push(LABKEY.Filter.create(f,setup.filters[f]));
 	}
-	qconfig.success=function(data){populateTable(data,divName,unique,additionalData,setup)};
+	qconfig.success=function(data){populateTable(data,divName,unique,additionalData,setup);};
 	LABKEY.Query.selectRows(qconfig);
 }
 
 function printTableSetup(data){
 	let obj=data.metaData.fields;
-	for (f in obj){	
+	for (let f in obj){	
 		print("Data["+f+"]: "+obj[f]);
-		for (g in obj[f]){
+		for (let g in obj[f]){
 			print("Data.metaData.fields["+f+"]["+g+"]: "+obj[f][g]);
 		}
 		if ("lookup" in obj[f]){
@@ -859,6 +860,8 @@ function printTableSetup(data){
 function populateTable(data,divName,unique,additionalData,setup){
 	//generate and populate table with the first suitable entry
 	let debug=true;
+	
+	print('populateTable: '+config.formConfig.dataFields);
 	//avoid resetting of values
 	if (!(data.queryName in config.formConfig.dataFields))
 		config.formConfig.dataFields[data.queryName]=data.metaData.fields;
@@ -934,6 +937,13 @@ function populateTable(data,divName,unique,additionalData,setup){
 				input = config.document.createElement("input");
 				input.type="date";
 			}
+			if (vType=="string"){
+				if (varValue==null) varValue="UNDEF";
+				else{
+					if (varValue.length==0)
+						varValue="UNDEF";
+				}
+			}
 			if (vType=="string" && !("lookup" in field)){
 				if(vName.search("reviewComment")>-1){
 					input = config.document.createElement("textarea");
@@ -941,9 +951,18 @@ function populateTable(data,divName,unique,additionalData,setup){
 					input.rows="5";
 				}
 				else{
-					input = config.document.createElement("input");
-					input.type="text";
-				}
+					input=config.document.createElement('input');
+					if (vName.search('_file_')>-1){ 
+						print('varValue: '+varValue);
+						if (varValue=="UNDEF")
+							input.type="file";
+						else
+							input.type="text";
+					}
+					else{
+						input.type="text";
+					}
+				}	
 			}
 			if (vType=="float"){
 				input = config.document.createElement("input");
@@ -957,7 +976,8 @@ function populateTable(data,divName,unique,additionalData,setup){
 
 			input.id=setup.getInputId(vName);
 			if (varValue != "UNDEF"){
-				if (vType=="string") input.value=varValue;
+				if (vType=="string") 
+					input.value=varValue;
 				if (vType=="float")  input.value=varValue;
 				if (vType=="date") input.valueAsDate=varValue;
 				if (vType=="boolean") input.checked=varValue;
@@ -1199,6 +1219,22 @@ function saveReviewToList(data,elementId,setup){
 		}	
 		if (vType=="string"){
 			entry[vName]=el.value;
+			let idx=field.name.search('_file_');
+			let lg=el.value.length;
+			if (idx>-1 && lg>0){
+				print('Attachment field: '+el.value);
+				//entry[vName]=el.files[0].stream();
+				let ctx=new Object();
+				ctx['dirName']='consent';
+				ctx['ID']=entry['crfRef'];
+				ctx['project']=config.containerPath;
+				//need ID->crf!
+				//assume crfRef will get set before this
+				//element is encountered
+				uploadFile(el,ctx);
+				let suf=entry[vName].split('.').pop();
+				entry[vName]=entry['crfRef']+'.'+suf;
+			}
 		}	
 		if (vType=="float"){
 			entry[vName]=el.value;
@@ -1634,10 +1670,42 @@ function changeCRFStatus(data){
 	c1.containerPath=config.containerPath;
 	c1.rows=[entry];
 	//close window upon success
-	c1.success=function(data){redirect()};
+	c1.success=sendEmail;
 	LABKEY.Query.updateRows(c1);
 }
 
+function sendEmail(data){
+
+	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-visit.view?';
+	link+='entryId='+crf;
+	link+='&formId='+formId;
+
+	//debug
+	let recipients=new Array();
+	let typeTo=LABKEY.Message.recipientType.to;
+	//from crfManagers list
+	let as=LABKEY.Message.createRecipient(typeTo,'andrej.studen@ijs.si');
+	recipients.push(as);
+	let typeHtml=LABKEY.Message.msgType.html;
+	let typePlain=LABKEY.Message.msgType.plain;
+	let msg=LABKEY.Message.createMsgContent(typeHtml,'<h2>Test</h2>');
+	let msg1=LABKEY.Message.createMsgContent(typePlain,link);
+
+	LABKEY.Message.sendMessage({
+		msgFrom:'labkey@fmf.uni-lj.si',
+		msgSubject:'Form submitted',
+		msgRecipients:recipients,
+		msgContent:[msg1],
+		success: redirect
+	});
+
+}
+
 function hideErr(){
 	let el=config.document.getElementById("errorDiv");
 	el.style.display="none";
@@ -1663,7 +1731,7 @@ function printErr(msg){
 
 //validation worker
 function checkForm(){
-	let debug=false;
+	let debug=true;
 	let crfRef=getCRFref()
 
 
@@ -1671,8 +1739,8 @@ function checkForm(){
 	//fields are queries that are part of the form
 	//this allows validation to be performed on all
 	//queries that are part of the form
-	for (f in config.fields){
-		let field=config.fields[f];
+	for (f in config.formConfig.fields){
+		let field=config.formConfig.fields[f];
 		field.status="UNKNOWN";
 
 		if (debug) 
@@ -1730,11 +1798,12 @@ function waitForCheckForm(cb){
 
 function checkData(data){
 	//insulated worker. Data corresponds to entry of a particular query
-	let debug=false;
+	let debug=true;
 	if (debug) print("checkData ");
-	let field=config.fields[data.queryName];
+	let field=config.formConfig.fields[data.queryName];
 	field.status="NONE";
-	if (debug) print("Setting status for "+data.queryName+" to "+field.status);
+	if (debug) print("Setting status for "+data.queryName+" to "+
+			field.status);
 
 	//this is the only test implemented - we should have at least one row in each of the queries
 	//we could also have a more targeted functions, but their form escapes me at the moment
@@ -1880,8 +1949,6 @@ function redirect(){
 
 }
 
-config.blob;
-config.blobInterval;
 
 function checkBlob(){
 	print("checkBlob: "+config.blob);
@@ -1903,11 +1970,8 @@ function checkBlob(){
 
 function printForm(){
 
-	let doPrint=false;
-	
 	config.doc=new PDFDocument();
-	config.doc.fontSize(25).text("Some text with standard font Andrej!", 100, 100);
-	//doc.end();
+	//config.doc.end();
 	let stream = config.doc.pipe(blobStream()).on("finish",function(){
 			config.blob=stream.toBlob("application/pdf");});
 	
@@ -1922,63 +1986,121 @@ function printForm(){
 
 	//pick data from crfForm list
         print("Printing form");
-
-	for (let i=0;i<config.formConfig.formSetup.rows.length;i++){
-		let entry=config.formConfig.formSetup.rows[i];
-		let qName=config.formConfig.queryMap[entry['queryName']];
-		let selectRows=new Object();
-		print('Adding data for '+qName);
-		selectRows.containerPath=config.containerPath;
-		selectRows.schemaName='lists';
-		selectRows.queryName=qName;
-		selectRows.filterArray=[LABKEY.Filter.create('crfRef',config.formConfig.crfEntry['entryId'])];
-		selectRows.success=collectFormData;
-		//skip for now
-		LABKEY.Query.selectRows(selectRows);
-		//deal with additional data
-	}
-	
-}
-
-function collectFormData(data){
-	print(config,"Adding "+data.queryName);
-	config.doc.fontSize(25).text("Adding "+data.queryName, 100, 100);
-	config.formConfig.fields[data.queryName].data=data;
-	for (f in config.formConfig.fields){
-		let field=config.formConfig.fields[f];
-		//serve as monitor - if any of the fields has no data object, return in the loop.
-		//Only when all fields have a data entry one should exit
-		if ("data" in field) continue;
-		return;
+	printHeader();
+	setData(formatPrintData);
+}
+
+function printHeader(){
+	config.doc.fontSize(25).text(config.formConfig.form['formName']);
+	config.doc.moveDown();
+	let crfEntry=config.formConfig.crfEntry;
+	let site=config.formConfig.site;
+	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){
+		print('Printing for '+f);
+		let e=val[f];
+		let entry=new Object();
+		entry[f]=e.o[e.f];
+		printPDF(entry,
+			{name:f,caption:e.t,type:'string'},null);
+	}
+	config.doc.moveDown();
+}
+
+function formatPrintData(){
+	qS=config.formConfig.dataQueries;
+	for (let q in qS){
+		print('Setting up '+q);
+		let qData=qS[q];
+		print('Number of rows: '+qData.rows.length);
+		if (qData.rows.length>0){
+			config.doc.fontSize(20).text(qData.title);
+		}
+		for (let i=0;i<qData.rows.length;i++){
+			let entry=qData.rows[i];
+		       	for (let f in qData.fields){
+				let field=qData.fields[f];
+				let lookup=null;
+				if (field.lookup){
+					lookup=config.formConfig.lookup[field.lookup.queryName];
+				}
+				if (field.hidden) continue;
+				printPDF(entry,field,lookup);
+			}
+		}
+		config.doc.moveDown();
 	}
 	print("All done");
-	formatFormData();
 	config.doc.end();
 }
 
-function formatFormData(){
-	for (queryName in config.formConfig.fields){
-		formatDataset(queryName);
-	}
-}
+function printPDF(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;	
+	
+	print('printPDF: entry['+field.name+']='+entry[field.name]);
+	let v=entry[field.name];
+	if (lookup!=null){
+		v=lookup.LUT[v];
+	}
+	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=' / ';
 
-function formatDataset(queryName){
-	let data=config.formConfig.fields[queryName].data;
-	let fields=data.metaData.fields;
-	for (let i=0;i<data.rows.length;i++){
-		for (f in fields){
-			let field=fields[f];
-			if (field.hidden) continue;
-			let vName=field.name;
-			let vType=field.type;
-			print('['+vName+'/'+vType+']: '+data.rows[i][vName]);
-		}
-	}
+	//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;
+	
+	config.doc.font('Courier-Bold').text(v,opt);
 
+	//restore x value
+	config.doc.x=tx;
+	
+}
 
 function generateMasterForm(){
 
@@ -2057,7 +2179,7 @@ function afterConfig(){
 
 
 	//here we should get data. For now, just initialize objects that will hold data
-	config.formConfig.dataFileds=new Object();
+	config.formConfig.dataFields=new Object();
 	config.formConfig.entries=new Object();
 	setDataLayout(afterDataLayout);//callback is afterDataLayout
 }
@@ -2172,6 +2294,22 @@ function afterFormStatus(data){
 	print("afterFormStatus: ");
 	config.formConfig.formStatusData=data;
 	
+	let qconfig=new Object();
+
+	qconfig.schemaName="lists";
+	qconfig.queryName="Forms";
+	qconfig.filterArray=[LABKEY.Filter.create('Key',config.formId)];
+	
+	//qconfig.filterArray=[LABKEY.Filter.create('formStatus',1)]
+	qconfig.success=afterForms1;
+	LABKEY.Query.selectRows(qconfig);
+
+}
+
+function afterForms1(data){	
+	print("afterForms1: ");
+	config.formConfig.form=data.rows[0];
+
 	let selectRows=new Object();
 	selectRows.containerPath=config.containerPath;
 	selectRows.schemaName='lists';
@@ -2212,7 +2350,7 @@ function afterFormSetup(data){
 
 function afterFormDatasets(data){
 	print('afterFormDatasets: '+data.rows.length);
-	config.formConfig.formDatasets=data;
+	config.formConfig.formDatasets=data;//inputLists
 	config.formConfig.fields=new Object();
 	config.formConfig.queryMap=new Object();
 
@@ -2271,13 +2409,20 @@ function afterFormDatasets(data){
 function setDataLayout(cb){
 	let rowsSetup=config.formConfig.formSetup.rows;
 	config.formConfig.dataQueries=new Object();
+	let dS=config.formConfig.dataQueries;//reference only
+	let qMap=config.formConfig.queryMap;
 	config.formConfig.lookup=new Object();
 	for (let i=0;i<rowsSetup.length;i++){
 		let entry=rowsSetup[i];
-		let queryName=config.formConfig.queryMap[entry['queryName']];
-		config.formConfig.dataQueries[queryName]=new Object();
+		let queryId=entry['queryName'];
+		let queryName=qMap[entry['queryName']];
+		dS[queryName]=new Object();
+		dS[queryName].title=entry['title'];
 		if (entry['showQuery']!="NONE"){
-			config.formConfig.dataQueries[entry['showQuery']]=new Object();
+			let sqName=entry['showQuery'];
+			dS[sqName]=new Object();
+			dS[sqName].title=findTitle(sqName);
+			
 		}
 	}
 	//always add reviews
@@ -2287,7 +2432,8 @@ function setDataLayout(cb){
 
 	//perhaps we will need queryId, but this is stuff for later
 	for (q in config.formConfig.dataQueries){
-		//callback will be a watchdog and will complete only when all data will be gathered
+		//callback will be a watchdog and will complete only 
+		//when all data will be gathered
 		let dq=config.formConfig.dataQueries[q];
 		dq.collectingLayout="INITIALIZED";
 
@@ -2300,6 +2446,17 @@ function setDataLayout(cb){
 	}
 }
 
+function findTitle(queryName){
+//find by name from formDatasets 
+//and set associated title as title
+	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";
+}
 
 //this happens after the for loop, so all dataQueries objects are set
 function afterDatasets(data,cb){
@@ -2413,57 +2570,104 @@ function assembleData(data,cb){
 	cb();
 }
 
-//function loadFile(config){
-//
-//	let file=config.fb.files[0];
-//	print(config.config,'Y: '+file.name);
-//
-	//will be part of config
-//	let server='https://merlin.fmf.uni-lj.si/labkey/_webdav';
-//	let project='Test';
-//        let path='@files';
-//        let url=server+'/'+project+'/'+path+'/'+file.name;
-//	let connConfig=new Object();
-//	connConfig.disableCaching=true;
-//	connConfig.method='PUT';
-//	connConfig.url=url;¸
-//        let conn = new Ext4.data.Connection(connConfig);
-//	print(config.config,'YY: '+url);
-
-//	let request=new Object();
-//	request.headers=new Object();
-//        headers['Content-Type']='application/octet-stream';
-//        headers['X-Requested-With']='XMLHttpRequest';
-	//add file as rawData element
-//	request.rawData=file;
-	//add success call-back
-//	request.success=function(){print(config.config,'YYY');}
-//	conn.request(request);
-//	print(config.config,'YYYY');
-//
-//}
 
-function addFileUpload(config, divName){
-	let el=config.document.getElementById(divName);
-	let tab=config.document.createElement('table');
-	let row=tab.insertRow(0);
-	let c1=row.insertCell(0);
-	let fb=config.document.createElement('input');
-	fb.setAttribute("type", "file");
-	fb.id=divName+'_fileSelect';
-	c1.appendChild(fb);
-	let c2=row.insertCell(1);
-	let sb=config.document.createElement('input');
-	sb.setAttribute("type", "button");
-	sb.id=divName+'_submitFiles';
-	sb.value='Submit';
-	el.appendChild(tab);
-
-	let loadFileConfig=new Object();
-	loadFileConfig.fb=fb;
-	loadFileConfig.config=config;
-//	sb.addEventListener('click', function(){loadFile(loadFileConfig);});
-	print('X: '+sb);
+function uploadFile(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
+	print('uploadFile: '+inputElement.value+'/');
+	if (inputElement.type=="text") return;
+	print('uploadFile: '+inputElement.files+'/');
+	print('uploadFile: '+inputElement.files.length+'/');
+	if (inputElement.files.length>0){
+		let file=inputElement.files[0];
+		print('uploadFile: '+inputElement.value+'/'+file.size);
+	}
+
+	let url=LABKEY.ActionURL.getBaseURL();
+	url+='_webdav';
+	url+=LABKEY.ActionURL.getContainer();
+	url+='/@files';
+	url+='/'+context['dirName'];
+
+	print('uploadFile url: '+url);
+	let uploadConfig=new Object();
+	uploadConfig.inputElement=inputElement;
+	uploadConfig.context=context;
+	uploadConfig.url=url;
+	uploadConfig.success=afterBaseDir;
+	uploadConfig.failure=tryMakeDir;
+	webdavCheck(uploadConfig);
+}
+
+function afterBaseDir(cfg){
+	print('afterBaseDir');
+	cfg.url+='/'+cfg.context['ID'];
+	cfg.success=afterIDDir;
+	cfg.failure=tryMakeDir;
+	webdavCheck(cfg);
+}
+
+function afterIDDir(cfg){
+	print('afterIDDir');
+	if (cfg.inputElement.files.length==0){
+	       	print('No files found');	
+		return;
+	}
+	let file=cfg.inputElement.files[0];
+	print('Uploading '+file.name);
+	let suf=file.name.split('.').pop();
+	cfg.url+='/'+cfg.context['ID']+'.'+suf;
+	cfg.success=afterUpload;
+	cfg.failure=onFailure;
+	cfg.data=file;
+	webdavPut(cfg);
+}
+
+function afterUpload(cfg){
+	print('afterUpload');
 }
 
+function tryMakeDir(cfg){
+	print('tryMakeDir '+cfg.url);
+	cfg.failure=onFailure;
+	webdavMakeDir(cfg);
+}
+
+
+
+
+
+function request(cfg,verb,data){
+	print('request['+verb+'] '+cfg.url);
+	let connRequest=new XMLHttpRequest();
+	connRequest.addEventListener("loadend",
+		function(){checkResponse(connRequest,cfg);});
+	connRequest.open(verb, cfg.url);
+	connRequest.send(data);
+	//print('request['+verb+'] sent');
+}
+
+function checkResponse(xrq,cfg){
+	//print('checkResponse: readyState '+xrq.readyState);
+	//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);
+}
+
+function webdavMakeDir(cfg){ request(cfg,'MKCOL',null);}
+function webdavCheck(cfg) { request(cfg,'GET',null);}
+function webdavPut(cfg) { request(cfg,'PUT',cfg.data);}
+
+
+function onFailure(cfg){
+	print('request failed with status='+cfg.status);
+}
 

File diff suppressed because it is too large
+ 0 - 0
web/crf/pdfkit.standalone.js


+ 8 - 0
web/crf/pdfkit.standalone.js.license

@@ -0,0 +1,8 @@
+MIT LICENSE
+Copyright (c) 2014 Devon Govett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Some files were not shown because too many files changed in this diff