Thursday, August 5, 2010

ColdMVC: Deploying my first app.

I just wanted to share some things I struggled with when deploying my first ColdMVC app.

1. Deploying the code to the server.
My web root on the hosted server looks like this:

->myApp
->coldMVC
->hyrule

2. Make sure the config.ini is setup correctly.
config.ini file
[default]
controller=route
action=render

[development]
development=true

[production]
datasource=myDataSourceName
development=false
sesURLs=true
urlPath=
assetPath=http://www.myDomain.com/myApp/public/
tagPrefix=c

In the block [production] you will see that sesURLs=true. This will get rid of /index.cfm on the tail of url. Example www.myDomain.com/public/

Next, I changed my urlPath to nothing because I want urls to not have the public in front of them. Example www.myDomain.com/

Finally, Since my urlPath doesn't point to the public folder any more all my assets ( css, js..etc) will be broken. So I point my assets back to the public folder. Examples http://www.myDomain.com/myApp/public/

Note: If you haven't already, make sure you create a default controller and action to hit. This will be excuted if somebody hits your base url. Example www.myDomain.com

3. Use apache or isapi rewrite rules to make the url prettier.
With out the /public/index.cfm at the end of url the app can't do routes. In order to solve this I had to make isapi rewrite rules to point anything after www.myDomain.com to the http://www.myDomain.com/myApp/public/index.cfm files so routes would work again.
.htaccess file
RewriteEngine on

#---redirect actions for www.myDomain.com
RewriteCond %{HTTP_HOST} ^www.myDomain.com [QSA]
RewriteCond %{SCRIPT_NAME} ^/index.cfm$
RewriteRule ^(.*)$ http://www.myDomain.com/myApp/public/index.cfm/%{REQUEST_URI} [QSA]

#---redirect assets for www.myDomain.com
RewriteCond %{HTTP_HOST} ^www.myDomain.com [QSA]
RewriteCond %{SCRIPT_NAME} !^/index.cfm$
RewriteCond %{SCRIPT_NAME} !(css|js|images)
RewriteCond %{SCRIPT_NAME} !-f
RewriteCond %{SCRIPT_NAME} !-d
RewriteRule ^(.*)$ http://www.myDomain.com/myApp/public/index.cfm/%{REQUEST_URI} [QSA,L]

4. Make sure the production environment.txt has the production text in it.
Since my app's config.ini file has block called [production] in it the environment.txt on the hosted server needs to have the text "production" in it.

5. Remember to create a datasource.
If the datasource name is not the same as your app folder name you need to add the datasource name in config.ini
[production]
datasource=myDataSourceName

Other than the isapi rewrite rules, it was my first time writing them, deploying my first ColdMVC app went well.

Wednesday, August 4, 2010

Select Top N Rows Per Group

I had an interesting sql task today. Someone asked me to get a max of three time-sheet entries per project. When an employee fills out a time-sheet they select a project for which the time is applied to. I was asked to grab a max of three of these entries and dump them in a csv for the accountant to audit. I was troubled on where to start this task. I am not an sql guy, so this was a challenge for me. I talked to the sql guys, but they were pretty busy today. One helped me but are minds together struggled to find the answer. After the googling, I tried a search result called "Cosmo Central" and found the anwser. Below is the piece that helped me so much...


with data as(
select row_number() over(partition by projects.name order by timesheet.id) as 'RowNumber’,
timesheet.id, projects.name
from timesheet
inner join projects on projects.id = timesheet.project_id
)

select id, name
from data
where RowNumber <= 3


Resource Used:
http://www.cosmocentral.com/2010/04/select-top-n-rows-per-group-ms-sql/

Yes. It looks like I copied the post and re-posted, but I am more so happy that I found the answer that I wanted to share the solution again. The credit still goes to Cosmo Central. Thanks a bundle.

ColdMVC: Basic CMS app request handling

I wanted to try make a cms app with ColdMVC so I took a stab at it. This is my first draft at a cms app. Below is a light weight request handler for cms views. We will break it down in a sec.


/**
* @accessors true
* @extends coldmvc.Controller
* @controller cmsview
* @layout cms_public
*/
component {

property _CMSPage;

/**
* @events requestStart
*/
function requestStart(){

var page = _CMSPage.findByAddress(getPath());

/*---check if the path is a cms page---*/
if(len(page.id()) gt 0){
$.event.action("render");
$.event.controller("cmsView");
$.event.view("cms/view/index.cfm");
}

}

function render(){

var path = getPath();
var actual_path = expandPath("/app/views/#path#");

/*---handles pages that end in a slash. Ex: www.mydomain.com/public/index.cfm/products/---*/
if(right(path,1) eq "/"){
actual_path = actual_path & "index.cfm";
path = path & "index.cfm";
}

/*---if the file exists render it, else run the page's html through the cms_public layout.---*/
if(fileExists(actual_path)){

$.event.view(path);

}else{

params.page = _CMSPage.findByAddress(getPath());

if(len(params.page.id()) eq 0){

render404();

}

}

}

/**
* @events invalidController, invalidAction
*/
function render404(){

params.page = _CMSPage.findByAddress("404");

if(len(params.page.id()) eq 0){
params.page._set("html","Sorry. We couldn't find the page you were looking for.");
}

$.event.controller("cmsView");
$.event.action("render404");
$.event.view("cms/view/index.cfm");

}

function getPath(){

var path = $.event.path();

if(left(path,1) eq "/"){
path = replace(path,"/","");
}

return path;

}

}


When a request begins I use the event "requestStart" to check if it's a cms page. This happens here...


/**
* @events requestStart
*/
function requestStart(){

var page = _CMSPage.findByAddress(getPath());

/*---check if the path is a cms page---*/
if(len(page.id()) gt 0){
$.event.action("render");
$.event.controller("cmsView");
$.event.view("cms/view/index.cfm");
}

}


As you can see above we are checking if getPath() is a cms page. getPath() gets the tail end of the url. Example: If the url read "www.mydomain.com/public/index.cfm/contact_us/", getPath() would return "contact_us/". This happens here...

function getPath(){

var path = $.event.path();

if(left(path,1) eq "/"){
path = replace(path,"/","");
}

return path;

}


If a cms page isn't found the request will run as usual. If a cms page is found it will go to render(). render() does a lot of checks.

First, we check if there is no file extension, example ".cfm", on path. If there is none I put in an "index.cfm" at the end. I do this for the next check which checks to see if the file exists.

Second, next we check if the file path actually exists in the app/views/ directory. If it does I render it. I do this because some pages might need to be coded where as other pages will be setup with a "web page generater" tool.

Third, if the file doesn't exists I can assume it's a page that was created by web page generator tool.

Lastly, I check if the page doesn't exists. I do this because my config.ini uses render() as defaults.
[default]
controller=cmsView
action=render

If someone requests "www.mydomain.com/public/index.cfm" the path won't exist. Therefore we excute the 404 handler, which is called render404().

This happens here...

function render(){

var path = getPath();
var actual_path = expandPath("/app/views/#path#");

/*---handles pages that end in a slash. Ex: www.mydomain.com/public/index.cfm/products/---*/
if(right(path,1) eq "/"){
actual_path = actual_path & "index.cfm";
path = path & "index.cfm";
}

/*---if the file exists render it, else run the page's html through the cms_public layout.---*/
if(fileExists(actual_path)){

$.event.view(path);

}else{

params.page = _CMSPage.findByAddress(getPath());

if(len(params.page.id()) eq 0){

render404();

}

}

}

If a page doesn't exists in the cms pages, an invalid controller or action is found I render404() is executed. I first check to see if a 404 page was created in the cms pages else I render my own html message.

This happens here...


/**
* @events invalidController, invalidAction
*/
function render404(){

params.page = _CMSPage.findByAddress("404");

if(len(params.page.id()) eq 0){
params.page._set("html","Sorry. We couldn't find the page you were looking for.");
}

$.event.controller("cmsView");
$.event.action("render404");
$.event.view("cms/view/index.cfm");

}


As I have been going through these functions I didn't explain where the pages are getting renderer. If you noticed at the top of first block of code there was an annotation @layout cms_public. All cms pages, whether they are coded up or database driven, run through the layout cms_public.cfm. Here's what it looks like...

<cfoutput>
< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en-us" xmlns="http://www.w3.org/1999/xhtml">

<cfif structKeyExists(params,"page")>
#page.html()#
<cfelse>
#render()#
</cfif>

</html>
</cfoutput>


All database driven pages run through a view in "views/cms/view/index.cfm" which looks like this...

<cfoutput>#page.html()#</cfoutput>


Thoughts on what I have so far?

Sunday, July 25, 2010

Declaring a variable with same name as a column in cfloop

I have a query with a column name json in it, which may or maynot contain a json string. If it does contain json I want to deserialize it, put it into an array loop, and output the results. I want to use the same in a variable as the column coming from the query because it reads good. When I did this I noticed that you can't declare a variable with the same name...even if you prefix the query column.

Here's is my test case. We will break it down in a sec.

<cfset query = queryNew("id,json")/>

<cfset queryAddRow(query)/>
<cfset querySetCell(query,"id","1")/>
<cfset querySetCell(query,"json","")/>

<cfset array = [{id=1,name="Joe"},{id=2,name="Tony"},{id=3,name="Ryan"}]/>

<cfset queryAddRow(query)/>
<cfset querySetCell(query,"id","2")/>
<cfset querySetCell(query,"json",serializeJSON(array))/>

<cfloop query="query">

<cfset json = []/>

<cfif len(query.json) gt 0>
<cfset json = deserializeJSON(query.json)/>
</cfif>

<cfloop from="1" to="#arrayLen(json)#" index="i">
#i.name#
</cfloop>

</cfloop>


The above test case will error out. Because you can't loop the following because ColdFusion thinks that variable "json" is a string, even though I declared it as an array.

<cfloop from="1" to="#arrayLen(json)#" index="i">


First we make a query with a json string in it

<cfset query = queryNew("id,json")/>

<cfset queryAddRow(query)/>
<cfset querySetCell(query,"id","1")/>
<cfset querySetCell(query,"json","")/>

<cfset array = [{id=1,name="Joe"},{id=2,name="Tony"},{id=3,name="Ryan"}]/>

<cfset queryAddRow(query)/>
<cfset querySetCell(query,"id","2")/>
<cfset querySetCell(query,"json",serializeJSON(array))/>


Next we loop the query and check if there is a json string in the query column "JSON". I started an array with the same name as query column json. I did this so if there isn't any json after we deserialize the json it won't error when we try to loop the array.

<cfset json = []/>

<cfif len(json) gt 0>
<cfset json = deserializeJSON(query.json)/>
</cfif>


Lastly I try to loop the array "json" and output the data, but it errors out because it still is looking at the loop variable "json" not the variable I declared "json". Yes, I can solve this problem easily but changing my declared variable from "json" to "array" or something like that, but I wanted to be clear what I was loop...and I just wanted to use the variable name "json".

Saturday, July 24, 2010

ColdMVC: Flatten an array of objects with children.

In record based systems "non object based" if you wanted to store a parent/child relationship you usually stored a parent_id on the same table.

Example:

CMS example showing a page table where a page can have many pages underneath it.

Page table.
ID,Name,Address,Parent_ID
1,Products,products/,null
2,Tiles,products/tiles/,1

Then in order to simulate an object based system, you usually loop the query and put the records in structs of structs.

Example.

result = {
id="1",
name="Products",
address="products/",
children=[
{
id="1",
name="Products",
address="products/",
children=[]
}
]
}

Notice there is children an array.

Using ColdMVC we actually start with "struct of structs" or object based, instead of starting with a query. So we need to take the objects array, we are using array of objects because that is what is return from an hql query, and pull out the children and put them in the array, thus flattening the tree.

To do this I run it through a helper function I made below.

<cffunction name="flattenArrayTree" access="public" output="false" returntype="array">
<cfargument name="array" required="true"/>
<cfargument name="result" required="false" default="#[]#"/>
<cfargument name="treeDepth" required="false" default="0"/>
<cfargument name="childrenPropertyName" required="false" default="children" hint="A property with an array of child objects."/>

<cfset var local = {}/>
<cfset var i = ""/>

<cfloop from="1" to="#arrayLen(arguments.array)#" index="i">

<cfset local.object = arguments.array[i]/>

<cfset local.object.setTreeDepth(arguments.treeDepth)/>

<cfset arrayAppend(arguments.result,local.object)>

< !---make sure the "children" property exists is in the object--->
<cfif not structKeyExists(local.object,"set"&arguments.childrenPropertyName)>

<cfthrow detail="The argument childrenPropertyName which is currently #arguments.childrenPropertyName# does not exist as a property in the object"/>

< /cfif>

<cfif arrayLen(local.object._get(arguments.childrenPropertyName)) gt 0>

<cfset arguments.treeDepth++/>

<cfset arguments.result = flattenArrayTree(local.object._get(arguments.childrenPropertyName),arguments.result,arguments.treeDepth)/>

<cfset arguments.treeDepth--/>

< /cfif>

< /cfloop>

<cfreturn arguments.result/>

< /cffunction>

You will notice I add a property called "treeDepth". In order to use the function you need to add a property to the model cfc called "treeDepth". Don't worry about it being added to db, if it's not mapped in hibernate file it won't be added to the db. I use the treeDepth property to know how far in a child object is. Technically...you don't need this, but I find a very handy.

Monday, July 12, 2010

CF9 ormsetting dialect

I was just wondering why you need to specify a dialect in ORM, if you define this in your datasource?

Example:
Application.cfc
this.ormSettings = { datasource="jbankenBlogspot",dialect="MySQLwithInnoDB" };

I shouldn't have to specify dialect="MySQLwithInnoDB" if I did it already in my datasource. Maybe I am missing something here.

The real hard part is that hibernate was throwing an error that was the same as if you had a bad path to your entity cfc.

Wednesday, July 7, 2010

ColdMVC: Parse checkboxes or radios generically

I ran into an interesting issue awhile, back. I wanted to edit a product and click on checkboxes for one to many relationships to colors, categories, and sizes. When I post the form to the ProductController save() I wanted a generic way to convert checkbox values (which are ids) to actually objects. Below are the steps I took followed by the code.

First, I call private functions to parse the specific checkbox ids ( Ex. parseCategories()), but they all just call parseResource().

Next, while in parseResource() I look into the variables scope for the model (Ex. _Size) to dynamically get the object by using findByID().

Lastly, I append the object to an array and populate the Product object and save it.

ProductController.cfc

/**
* @accessors true
* @action list
* @extends coldmvc.Controller
*/
component {
property _Size;
property _Color;
property _Category;

function save() {

var product = _Product.new();

params.product.categories = parseCategories(params.product.categories);
params.product.sizes = parseSizes(params.product.sizes);
params.product.colors = parseColors(params.product.colors);

product.populate(params.product);

product.save();

redirect({controller="product",action="setup"},"productID=#product.id()#");
}

private array function parseCategories(string categoryIDs) {

return parseResource("Category",arguments.categoryIDs);

}

private array function parseSizes(string sizeIDs) {

return parseResource("Size",arguments.sizeIDs);

}

private array function parseColors(string colorIDs) {

return parseResource("Color",arguments.colorIDs);

}

private array function parseResource(string resource, string resourceIDs){

var resources = $.string.toArray(arguments.resourceIDs);

var result = [];
var i = "";

for (i=1; i <= arrayLen(resources); i++) {

arrayAppend(result, variables["_#arguments.resource#"].findByID(resources[i]));

}

return result;

}
}


I wanted to share this just in case someone else is running into the issue.