Tuesday, August 10, 2010

ColdMVC: Plugging in fckeditor

I am working on a CMS app and I wanted the fckeditor in my app, while using ColdMVC. Here's what I did to get it in my app.

I downloaded the fckeditor and put it in the public folder, like below...

app
>public
>>plugins
>>>fckeditor

Next, on a custom helper called "util.cfc", I added a function called editor().
1. Create a fckeditor bean.
2. Set the properties of the fckeditor. Specifically the basepath is really important. I used $.config.get('assetPath') to get to the directory where the fckeditor is located.
3. Lastly, I wrap the fckeditor in ColdMVC's field() so it looks like the other fields.

<cffunction name="editor" access="public" output="false" returntype="string">
<cfargument name="name" required="true"/>
<cfargument name="value" required="false" default=""/>
<cfargument name="width" required="false" default="100%"/>
<cfargument name="height" required="false" default="300px"/>

<cfset local.bean = application.coldmvc.beanFactory.getBean("fckeditor")/>

<cfset local.bean.basePath = "#$.config.get('assetPath')#plugins/fckeditor/"/>
<cfset local.bean.instanceName = arguments.name/>
<cfset local.bean.value = arguments.value/>
<cfset local.bean.width = arguments.width/>
<cfset local.bean.height = arguments.height/>

<cfif not StructKeyExists(arguments,"label")>
<cfset arguments.label = $.string.humanize(arguments.name)/>
</cfif>

<cfoutput>
<cfsavecontent variable="local.field">
#local.bean.create()#
</cfsavecontent>
</cfoutput>

<cfreturn $.form.field(label=arguments.label,field=trim(local.field))/>

</cffunction>


Finally on my view, I call the helper function editor() and pass in my value. The use case below is for the editing a layout record for a cms app.

<cfoutput>
<c:form action="save" bind="layout">
<c:hidden name="id" value="#layout.id()#" />
<c:input name="name" value="#layout.name()#" />

#$.util.editor(label="Layout",name="layout.layout",value=layout.layout())#

<c:submit name="save" />
</c:form>
</cfoutput>


One thing to note is that if you want to bind the fckeditor textarea to an object, so it can be used in ColdMVC's populate() for a save, you will need to prefix your instanceName in the fckeditor. I do this by wrapping the fckeditor in a helper function and use the "name" argument like this...


<--- on my view I put in "layout.layout" as the name--->
#$.util.editor(label="Layout",name="layout.layout",value=layout.layout())#

<--- inside the editor()--->
<cfset local.bean.instanceName = arguments.name/>

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?