I have a query which I'd like to output in a spreadsheet in Excel. I'd like some of the cell columns to be formatted in a certain way, which is thousands grouping and in Number format so that sums, additions etc can be done on that row without any further alteration.
I have read through the documentation but it has left me a bit confused on how to output to Excel in the first place.
I started out with a comment but it will be easier to read as an answer.
What have you tried? Have you read through the CFSpreadsheet documentation? Should be pretty straight forward. One of the parameters to the CFSpreadsheet tag is 'query'. Why not start with that and see how it formats the columns for you by default to see what needs tweaking.
Here is an example taken directly from the referenced documentation page:
<cfquery name="courses" datasource="cfdocexamples">
SELECT CORNUMBER, DEPT_ID, COURSE_ID, CORNAME
FROM COURSELIST
</cfquery>
<cfscript>
//Use an absolute path for the files. --->
theDir=GetDirectoryFromPath(GetCurrentTemplatePath());
theFile=theDir & "courses.xls";
//Create an empty ColdFusion spreadsheet object. --->
theSheet = SpreadsheetNew("CourseData");
//Populate the object with a query. --->
SpreadsheetAddRows(theSheet,courses);
</cfscript>
<!--- Write the sheet to a file --->
<cfspreadsheet action="write" filename="#theFile#" name="theSheet" sheetname="courses" overwrite=true>
See the documentation for SpreadsheetFormatColumn, SpreadsheetFormatColumns, SpreadsheetFormatRow and SpreadsheetFormatRows to read about formatting particular cells.
You just need to use the cfspreadsheet tag to create the file, and you can format the cells with the spreadsheetFormat* functions. You can find an example of how to do this at Ray Camden's site.
Related
I have a report stored as an .cfm file. I have been able to retrieve it fine with a cffile read. Now I want the option of retrieving only part of the report, say the first 50 lines. I decided to try a fileReadLine():
<cfset repname = url['rep']>
<cfset type = url['type']>
<cfset dataFile = fileOpen("/var/www/reports/moxrep/#repname#.cfm", "read" ) >
<cfset i = 0>
<cfoutput>
<cfloop
condition = "NOT FileIsEOF(dataFile) AND i LT 100">
<cfset i = i + 1>
<cfset inf = fileReadLine( dataFile ) >
#inf#
</cfloop>
</cfoutput>
<cfset fileClose( dataFile ) >
It did not retrieve things at all correctly. The formatting was messed up. All the dynamic data in the report was missing. The CSS links did not operate. And there were many extra blank lines.
Am I doing something wrong? Or is fileReadLine just not meant for retrieving a formatted report? And if not, is there any way to retrieve just part of the report with cffile?
Use cfhttp to get the report, then take that result and strip it down to what you need.
I am not sure you realize that the FileOpen() function is actually reading the raw CFML template and not actually executing the queries that populate your report. Using the CFHTTP tag is definitely a better approach, but be careful because your rendered page is likely to contain all your CSS that would be necessary for the report to render properly so use View Source on your report to see if you want just 50 lines.
The question burning in my mind is "why" do you want just 50 lines? are you previewing the report? it is only 1 page long? Are you embedding it into a dashboard? You may want to consider modifying the .cfm "report" so that the area you want to display elsewhere is wrapped with a specific tag (such as a Span or even something custom) and then when you fetch the report using CFHTTP, then you can parse the results with XMLParse() function (assuming it is properly formatted) and render the section of the report you actually want.
I'm using <cfspreadsheet> to output a database query to an Excel spreadsheet. When these spreadsheets are created manually it is possible to highlight all the cells and 'Format as table'. This means when the table header is clicked, the user can sort the table ascending and descending.
Is it possible to specify this formatting in the ColdFusion code when generating the Excel file?
If you are on Coldfusion 9, you can use SpreadSheetNew, then SpreadSheetAddRow, SpreadSheetFormat functions to style an excel spreadsheet from a data set
<cfset sObj = spreadsheetNew("myreport","yes")>
<cfset SpreadsheetAddRow(sObj, "Column_1, ... , Column27")>
<cfset SpreadsheetFormatRow(sObj, {bold=TRUE, alignment="center"}, 1)>
<cfset spreadsheetAddRows(sObj, qMyQuery)>
<cfheader name="content-disposition" value="attachment; filename=report_#Dateformat(NOW(),"MMDDYYYY")#.xlsx">
BE WARNED however, this can be extremely taxing to the JVM, I had a query i was creating an xls with, applying only two styles (bold, text-center) to the header row, and any query over 700 rows would shut down the entire server via JVM memory loss...here is my SO question about it, with related code/answer SpreadsheetAddRows failing on moderate size query
It has been documented with adobe as being a bug
The code below will generate the file with two notable errors:
The SpreadSheetSetColumnWidth appears to stop working after 21 lines on the second tab
The image does not appear on the second tab
Hopefully this is just a rookie mistake and not something along the lines of the date formatting issue. Any help or constructive criticism to fix/improve would be greatly welcomed.
<!--- We cannot directly reference the image --->
<cfimage source="img/A2LA_Logo.jpg" name="A2LA_Logo">
<!--- We need an absolute path, so get the current directory path. --->
<cfset theFile=GetDirectoryFromPath(GetCurrentTemplatePath()) & "Final_Report.xlsx">
<cfscript>
/*********************************************************************** Page 1 ***********************************************************************/
//Create a new Excel spreadsheet object - SpreadsheetNew("Tab name", "yes=2007+ no=2003");
Tab1 = SpreadsheetNew("Final Report Page 1","yes");
SpreadSheetSetRowHeight(Tab1,1,45);
SpreadsheetSetCellValue(Tab1,"Final Test Report",1,1); SpreadsheetMergeCells(Tab1,1,1,1,10);
SpreadsheetAddImage(Tab1, A2LA_Logo,"jpg","1,12,2,20");
SpreadsheetSetCellValue(Tab1,"Confidential",1,21); SpreadsheetMergeCells(Tab1,1,1,21,30);
</cfscript>
<cfscript>
/*********************************************************************** Page 2 ***********************************************************************/
//Create a new Excel spreadsheet object - SpreadsheetNew("Tab name", "yes=2007+ no=2003");
Tab2 = SpreadsheetNew("Final Report Page 2","yes");
SpreadSheetSetRowHeight(Tab2,1,45);
SpreadsheetSetCellValue(Tab2,"Final Test Report",1,1); SpreadsheetMergeCells(Tab2,1,1,1,10);
SpreadsheetAddImage(Tab1, A2LA_Logo,"jpg","1,12,2,20");
SpreadsheetSetCellValue(Tab2,"Confidential",1,21); SpreadsheetMergeCells(Tab2,1,1,21,30);
</cfscript>
<!--- There must be a better way --->
<cfscript>
for(index=1; index LTE 30; index++) {SpreadSheetSetColumnWidth(Tab1,index,3);}
for(index=1; index LTE 30; index++) {SpreadSheetSetColumnWidth(Tab2,index,3);}
</cfscript>
<!--- Write the spreadsheet to a file, replacing any existing file, then append the other tabs. --->
<cfspreadsheet action="write" filename="#theFile#" name="Tab1" overwrite=true>
<cfspreadsheet action="update" filename="#theFile#" name="Tab2">
<!--- Needs to be at the bottom --->
<cfheader name="Content-disposition" value="attachment;filename=Final_Report.xlsx">
<cfcontent type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" file="#theFile#">
<!--- Clean up your mess after it is served to the browser --->
<cffile action="delete" file="#theFile#">
(From the comments)
The images show up when saved separately. So I am guessing it is an update bug. My usual recommendation is "do not use update if you can avoid it". Create a single workbook, add multiple sheets and save it with a single write action.
Update: As for why, in your original code, you are creating two separate workbook objects. Conceptually think of it like creating two separate Excel files, with one sheet each. Then trying to merge them into one file:
c:/path/to/MyWorkbookNamedTab1.xlsx
| Sheet1 /
c:/path/to/MyWorkbookNamedTab2.xlsx
| Sheet1 /
That is different than creating a single workbook, with multiple sheets (like in the link above):
c:/path/to/MySingleWorkbook.xlsx
| Sheet1 / Sheet2 /
There is no easy way to merge separate files (or workbooks). You basically have to copy everything from one workbook into another (which is a lot more involved than it sounds). That is what action=update does. Somewhere in the copying process it loses one of the images. I am not sure if it that is due to a CF bug one in POI.
In any case, the simplest solution is to avoid the need for copying altogether. If you stick with a single workbook (and add multiple sheets to it) then there is no need for copying or merging. Everything is already contained in one workbook. So all action=write has to do is save it to disk, exactly "as is".
I am receiving excel files from our client and I need to create a csv file from them. My issue is that the values in the excel files do not match our standards to create the csv file just by copying it. I need to get the values and put them in the correct order while I'm creating the file. I read through the livedocs for CF but I couldn't find anything the worked.
I was thinking to create a struct and pull the data out in it and then create the csv file based on my struct but I haven't done anything like this before and I am not sure if it is even possible. I also thought of using listGetAt instead of the struct but still haven't tried it.
Anyone did something like this before? I'm trying to hardcode as less values as possible because I see this becoming a future problem if we get a second client with the same issue.
Update (I changed my code around a lot the past few days so this is what I have at the moment)
<cfset DataDirectory = "E:\testfolder\test.xlsx">
<cfspreadsheet action="read" src="#DataDirectory#" excludeHeaderRow="true" headerrow="1" query="queryData">
<cfquery name="contact" datasource="#ds#">
select ClientBrandID
from ClientBrands
where surveyReferralID IN
(select surveyReferralID from clientAdmin where username = '#res#')
</cfquery>
<cfscript>
dataFile = structNew();
StructInsert(datafile,"ClientBrandID",contact.ClientBrandID);
StructInsert(datafile,"surveyType", queryData.surveyType);
</cfscript>
<cfscript>
///We need an absolute path, so get the current directory path.
theFile= "E:\testfolder\NewTest.csv";
//Create a new Excel spreadsheet object and add the query data.
theSheet = SpreadsheetNew("PatientData");
SpreadsheetAddRow(theSheet, "ClientBrandID,SurveyType,Location,ClientContactID,FirstName,LastName,HomePhone,WorkPhone,CellPhone,Email"); <!---This is the header of the new CSV --->
SpreadsheetAddRows(theSheet, "datafile.clientbrandid, datafile.surveytype"); <!--- This is my latest failed attempt. Tried to pass in data from my struct, probably the quotes are the issue but haven't tried it without them --->
</cfscript>
<!--- Write the spreadsheet to a file, replacing any existing file. --->
<cfspreadsheet action="write" filename="#theFile#" format="csv" name="theSheet" overwrite=true >
All of these operations will need to be in a cfloop in order to go through all of the files in my test folder. Right now I am hardcoding a single file to fix the issue at hand before I do anything else. Also, I am missing another loop in between that will need to go through all the values of the file. It should be something like <cfloop query='queryData'> which is the query that Im getting from cfspreadsheet.
You could make it accept any order but you'll still need to hard code the expected column headers into an array or something and considering it is just for your internal use I would just add the data back to the CSV in the order it's supposed to be in. Last time I made a spreadsheet from a query I did it like this. Perhaps it will help.
<cfscript>
sMySpreadSheet = spreadsheetNew();
column = 1;
<!--- header row --->
<!--- Remember what is expected from SpreadsheetSetCellValue --->
<!--- SpreadsheetSetCellValue(spreadsheetObj, value, row, column) --->
spreadsheetSetCellValue(sMySpreadSheet ,"First Name",1,column); column++;
spreadsheetSetCellValue(sMySpreadSheet ,"Last Name",1,column); column++;
spreadsheetSetCellValue(sMySpreadSheet ,"DOB",1,column); column++;
spreadsheetSetCellValue(sMySpreadSheet ,"Phone Number",1,column); column++;
spreadsheetSetCellValue(sMySpreadSheet ,"Number of Kids",1,column); column++;
<!--- data rows --->
<!--- if you're not using a header row with real titles you can just use the
default col_x
example: spreadsheetSetCellValue(sMySpreadSheet , queryData.col_1[q], q+1, column); column++; --->
for(q=1; q LTE queryData.recordCount; q++){
column = 1;
spreadsheetSetCellValue(sMySpreadSheet , queryData.first[q], q+1, column); column++;
spreadsheetSetCellValue(sMySpreadSheet , queryData.last[q], q+1, column); column++;
spreadsheetSetCellValue(sMySpreadSheet , queryData.dob[q], q+1, column); column++;
spreadsheetSetCellValue(sMySpreadSheet , queryData.phoneNumber[q], q+1, column); column++;
spreadsheetSetCellValue(sMySpreadSheet , queryData.kidCount[q], q+1, column);
}
<!--- make it purdy (optional) --->
spreadsheetFormatRow(queryData, {fgcolor="light_cornflower_blue"},1);
</cfscript>
If you want to simply add to a CSV you can do something like this (wrap your stuff in " if you have them qualified):
<cfsetting enableCFoutputOnly = "Yes">
<cfsaveContent variable = "myCSV">
<cfset newline = #chr(13)#&#chr(10)#>
<cfoutput>First Name,Last Name,DOB,Phone Number,Number of Kids#newline#</cfoutput>
<cfoutput query="queryData">#queryData.first#,#queryData.last#,#queryData.dob#,#queryData.phoneNumber#,#kqueryData.idCount##newline#/cfoutput>
</cfsaveContent>
</cfsetting>
<cffile action = "append" file = "[your file]" output = "#myCSV#">
If cfsavecontent causes white spaces issues and cfsetting doesn't help, here is another alternative.
<cfset myCSV = "First Name,Last Name,DOB,Phone Number,Number of Kids#newline#">
<cfloop query="queryData">
<cfset myCSV &= "#queryData.first#,#queryData.last#,#queryData.dob#,#queryData.phoneNumber#,#queryData.kidCount##newline#">
</cfloop>
Here are some suggestions based on the edited question. I'm making them even though the edited question does not make it clear what you are trying to achieve.
Regarding, "All of these operations will need to be in a cfloop in order to go through all of the files in my test folder", you can use cfdirectory on that folder. It will return a ColdFusion query through which you can loop.
The rest of your code looks more complicated than it needs to be. Reading the spreadsheet gives you a query object - so far so good.
Your database query, named contact looks supicious. You refer to a variable named res but it's not clear where that comes from. If it comes from the spreadsheet, then a value list might be more appropriate. By the way, use query parameters.
It seems you want your csv file to combine data from the spreadsheet and the database query. If that's the case, a query of queries might be useful.
If it were me, I'd use cffile action="write" to create my csv file. I would also surround each value with double quotes in case any of them contained commas.
I'm using a cfspreadsheet read to read a sheet into a query object.
<cfspreadsheet action="read" src="TestExcel.xls" sheet="1" query="spreadsheetData" headerrow="1" excludeHeaderRow="true">
The problem is, some of the headers contain more than one word. So I end up with a query a bit like this:
ID Name Start Date End Date
3 Test 1/1/2009 1/1/2013
17 Test 2 11/11/2010 11/11/2012
If I try to access one of the columns that have a space in the column name, I get an error.
<cfoutput query="spreadsheetData">
#start date#
</cfoutput>
I've tried #[start date]# as well, but that didn't work. I cannot control the format of the excel sheet that I receive. Is there any way to access the multiple-worded-header columns?
When using bracket notation the contents must end up as a string, so:
<cfoutput query="spreadsheetData">
#spreadsheetData['start date'][CurrentRow]#
</cfoutput>
If you don't use quotes, you are passing in a variable, which is done like so:
<cfset ColumnName = 'start date' />
<cfoutput query="spreadsheetData">
#spreadsheetData[ColumnName][CurrentRow]#
</cfoutput>
Note that you must use the query name before the brackets - if you simply write [ColumnName] then this is inline array creation notation, not accessing the variable.
Also, if using this outside of a query loop (i.e. not within cfoutput/cfloop with query attribute), you also need to scope the CurrentRow variable, i.e.
spreadsheetData[ColumnName][spreadsheetData.CurrentRow]
(or provide your own explicit number/variable).
As Leigh notes below, for cfspreadsheet-specific behaviour, you can also specify the columnnames attribute, to rename the column to something directly accessible, e.g.
<cfspreadsheet query=".." columnNames="Foo,Bar,StartDate,Etcetera" ..>