Showing posts with label How To's. Show all posts
Showing posts with label How To's. Show all posts

Thursday, December 22, 2011

Full Calender JS - unique event id's

I was working with full calendar js dragging and dropping my tasks on to the calendar as events for a timesheet app and ran into a wierd issue. When I loaded up the available tasks to be dragged I set the event object's id key equal to the task's id as shown below.


//---load in all of task to be dragged onto the calendar
var loadTasks = function(){

$('##external-events div.external-event').each(function() {

var self = $(this);

var eventObject = {
title: $.trim(self.text())
,id: $.trim(self.data("taskid")) //---RIGHT HERE
,taskID:$.trim(self.data("taskid"))
};

self.data('eventObject', eventObject);

self.draggable({
zIndex: 999,
revert: true,
revertDuration: 0
});
});

};


This will work if you don't drag the same task on the calendar twice. If you worked on a task from 8am to 9am and 1pm to 2pm if you grabbed the 8am to 9am event and attempting to resize it to 8am to 8.30am, full calendar js would move it and then it would also move the 1pm to 2pm event as well to 1pm to 1.30pm.

Every event object you put on the calendar should have a unique id.

If found this little snippet on the web that helped me make unique id's for event objects.

http://snipplr.com/view/2574/

var uid = (
function(){
var id=0;
return function(){
return id++ ;
};
}
)();


And on the "drop" variable of the full calendar js invoking I reset the event's id and rerendered the event.

, drop: function(date, allDay, jsEvent, ui) {

var originalEventObject = $(this).data('eventObject');
var copiedEventObject = $.extend({}, originalEventObject);
copiedEventObject.id = uid(); //---RIGHT HERE
copiedEventObject.start = date;
copiedEventObject.end = new Date(date).hours().add(-1);
copiedEventObject._end = new Date(date).hours().add(-1);
copiedEventObject.allDay = allDay;

$('##calendar').fullCalendar('renderEvent', copiedEventObject, true);

saveEvent($(this).data("taskid"),'','',copiedEventObject.start,copiedEventObject.end,'');

}

This fixed the issue.

Full Calendar JS

Using full calendar js I was able to make a timesheet app. The app involved dragging tasks from a left on to a day view of calendar. Simliar to this drag and drop events demo. I added trash can on the top of calendar to drag and drop events to be removed. Here is the js I used to make it happen. All of which is in document.ready. We will break down after this code snippet.


var loadTasks = function(){

$('##external-events div.external-event').each(function() {

var self = $(this);

var eventObject = {
title: $.trim(self.text())
,id: $.trim(self.data("taskid"))
,taskID:$.trim(self.data("taskid"))
};

self.data('eventObject', eventObject);

self.draggable({
zIndex: 999,
revert: true,
revertDuration: 0
});
});

};

var saveEvent = function(taskID,orgStartObj,orgEndObj,newStartObj,newEndObj){

var format = "yyyy-MM-dd HH:mm:ss";

$.ajax({
url:'/timesheet/saveEvent'
,type:'POST'
,data:{
orginalStartTime:orgStartObj.toString(format)
,orginalEndTime:orgEndObj.toString(format)
,newStartTime:newStartObj.toString(format)
,newEndTime:newEndObj.toString(format)
,taskID:taskID
}
,dataType:'json'
});

};

//---Check if inside the trashcan div
var draggedOverTrashCan = function(draggedItem, dropArea) {
var itemOffset = draggedItem.offset;
var trashCanOffset = $(dropArea).offset();

itemOffset.right = $(draggedItem.helper).outerWidth() + itemOffset.left;
itemOffset.bottom = $(draggedItem.helper).outerHeight() + itemOffset.top;

trashCanOffset.right = $(dropArea).outerWidth() + trashCanOffset.left;
trashCanOffset.bottom = $(dropArea).outerHeight() + trashCanOffset.top;

// Compare
if (itemOffset.right >= trashCanOffset.left
&& itemOffset.bottom >= trashCanOffset.top
&& itemOffset.top <= trashCanOffset.bottom
&& itemOffset.left <= trashCanOffset.right
){
return true;
}else{
return false;
}
};


$('##calendar').fullCalendar({
header: {left: 'prev,next today',center: 'title',right: 'month,agendaWeek,agendaDay'}
, editable: true
, firstHour: 6
, slotMinutes: 15
, defaultView: "agendaDay"
, aspectRatio: "1.60"
, year: #datePart("yyyy", now())#
, month: #datePart("m", now())-1#
, date: #datePart("d", now())#
, events: #serializeJSON(viewBag.events)#
, droppable: true
, drop: function(date, allDay, jsEvent, ui) {

var originalEventObject = $(this).data('eventObject');
var copiedEventObject = $.extend({}, originalEventObject);
copiedEventObject.id = uid();
copiedEventObject.start = date;
copiedEventObject.end = new Date(date).hours().add(-1);
copiedEventObject._end = new Date(date).hours().add(-1);
copiedEventObject.allDay = allDay;

$('##calendar').fullCalendar('renderEvent', copiedEventObject, true);

saveEvent($(this).data("taskid"),'','',copiedEventObject.start,copiedEventObject.end,'');


}
, eventResize: function(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view){

var newStart = event.start;
var newEnd = event.end;
var orgStart = new Date(newStart);
var orgEnd = new Date(newEnd).addMinutes(minuteDelta * -1);

saveEvent(event.taskID,orgStart,orgEnd,newStart,newEnd);

}
, eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view ){

var newStart = event.start;
var newEnd = event.end;
var orgStart = new Date(newStart).addDays(dayDelta * -1);
orgStart.addMinutes(minuteDelta * -1);
var orgEnd = new Date(newEnd).addDays(dayDelta * -1);
orgEnd.addMinutes(minuteDelta * -1);

saveEvent(event.taskID,orgStart,orgEnd,newStart,newEnd);

}
,eventDragStop:function(event, jsEvent, ui, view){


if (draggedOverTrashCan(ui, $('div##trash-can'))) {

var format = "yyyy-MM-dd HH:mm:ss";

$.ajax({
url:'/timesheet/deleteEvent'
,type:'POST'
,data:{
startTime:event.start.toString(format)
,endTime:event.end.toString(format)
,taskID:event.taskID
,userID:'#viewBag.userID#'
}
,dataType:'json'
,success:function(){
$("##calendar").fullCalendar('removeEvents', event._id);
}
});


}
}
});

//--- add a trash can div to the top of the calendar
$('##calendar').children('.fc-content').prepend('<div id="trash-can" style="border: 2px solid ##C1454B;padding:15px 100px 15px;border-radius:5px;background:##DD9094;text-align:center;">Drag Events Here To Remove</div>');

//---make the tasks available.
loadTasks();


loadTasks() - is used to make the tasks in the left nav drag and droppable for the calendar. The task are created in ColdFusion like this.

<div id="external-events">
<ul>
<cfloop query="viewBag.tasks">
<li>
<div class="external-event ui-draggable" data-taskID="#viewBag.tasks.taskID#">
<div><strong>###viewBag.tasks.taskID# #viewBag.tasks.name#</strong></div>
<div><strong>Status: </strong>#viewBag.tasks.taskStatusName#</div>
</div>
</li>
</cfloop>
</ul>
</div>


Then document.ready I do this:

var loadTasks = function(){

//--- loop through each draggable task
$('##external-events div.external-event').each(function() {

var self = $(this);

//---set the event Object's title which will be seen on the calendar, set the id so that each event on the calendar is unique, and set any other values you want to be carried along with that event. in the case below taskID is an extra value I want. It will be used later to saveACalendarEvent.
var eventObject = {
title: $.trim(self.text())
,id: $.trim(self.data("taskid"))
,taskID:$.trim(self.data("taskid"))
};

self.data('eventObject', eventObject);

//---make the task draggable
self.draggable({
zIndex: 999,
revert: true,
revertDuration: 0
});
});

};

saveEvent() - is used to make an ajax call with the taskID, orginal start time, orginal end time, new start time, and new end time. I send the orginal and new dates so if I am resizing a task or moving the task on calendar I want to make sure I delete the old record.


var saveEvent = function(taskID,orgStartObj,orgEndObj,newStartObj,newEndObj){

var format = "yyyy-MM-dd HH:mm:ss";

$.ajax({
url:'/timesheet/saveEvent'
,type:'POST'
,data:{
orginalStartTime:orgStartObj.toString(format)
,orginalEndTime:orgEndObj.toString(format)
,newStartTime:newStartObj.toString(format)
,newEndTime:newEndObj.toString(format)
,taskID:taskID
}
,dataType:'json'
});

};

draggedOverTrashCan() - checks to see if the user drags a calendar event over the trash can. The trash can code gets prepended later in code.


var draggedOverTrashCan = function(draggedItem, dropArea) {
var itemOffset = draggedItem.offset;
var trashCanOffset = $(dropArea).offset();

itemOffset.right = $(draggedItem.helper).outerWidth() + itemOffset.left;
itemOffset.bottom = $(draggedItem.helper).outerHeight() + itemOffset.top;

trashCanOffset.right = $(dropArea).outerWidth() + trashCanOffset.left;
trashCanOffset.bottom = $(dropArea).outerHeight() + trashCanOffset.top;

// Compare
if (itemOffset.right >= trashCanOffset.left
&& itemOffset.bottom >= trashCanOffset.top
&& itemOffset.top <= trashCanOffset.bottom
&& itemOffset.left <= trashCanOffset.right
){
return true;
}else{
return false;
}
};

invoking the full calendar js
drop() - handles dragging new events from off the calendar onto the calendar.

eventResize(),eventDrop() - handles just that. Full calendar give you back the deltas from where the event used to be on the calendar. With those we can create the orginal date.

eventDragStop() - used when event has stopped being dragged. For my use case, I want to know if they drug the task over the tash can so I can do an ajax call and delete the task.

$('##calendar').fullCalendar({
header: {left: 'prev,next today',center: 'title',right: 'month,agendaWeek,agendaDay'}
, editable: true
, firstHour: 6
, slotMinutes: 15
, defaultView: "agendaDay"
, aspectRatio: "1.60"
, year: #datePart("yyyy", now())#
, month: #datePart("m", now())-1#
, date: #datePart("d", now())#
, events: #serializeJSON(viewBag.events)#
, droppable: true
, drop: function(date, allDay, jsEvent, ui) {

var originalEventObject = $(this).data('eventObject');
var copiedEventObject = $.extend({}, originalEventObject);
copiedEventObject.start = date;
copiedEventObject.end = new Date(date).hours().add(-1);
copiedEventObject._end = new Date(date).hours().add(-1);
copiedEventObject.allDay = allDay;

$('##calendar').fullCalendar('renderEvent', copiedEventObject, true);

saveEvent($(this).data("taskid"),'','',copiedEventObject.start,copiedEventObject.end,'');


}
, eventResize: function(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view){

var newStart = event.start;
var newEnd = event.end;
var orgStart = new Date(newStart);
var orgEnd = new Date(newEnd).addMinutes(minuteDelta * -1);

saveEvent(event.taskID,orgStart,orgEnd,newStart,newEnd);

}
, eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view ){

var newStart = event.start;
var newEnd = event.end;
var orgStart = new Date(newStart).addDays(dayDelta * -1);
orgStart.addMinutes(minuteDelta * -1);
var orgEnd = new Date(newEnd).addDays(dayDelta * -1);
orgEnd.addMinutes(minuteDelta * -1);

saveEvent(event.taskID,orgStart,orgEnd,newStart,newEnd);

}
,eventDragStop:function(event, jsEvent, ui, view){

if (draggedOverTrashCan(ui, $('div##trash-can'))) {

var format = "yyyy-MM-dd HH:mm:ss";

$.ajax({
url:'/timesheet/deleteEvent'
,type:'POST'
,data:{
startTime:event.start.toString(format)
,endTime:event.end.toString(format)
,taskID:event.taskID
,userID:'#viewBag.userID#'
}
,dataType:'json'
,success:function(){
$("##calendar").fullCalendar('removeEvents', event._id);
}
});


}
}
});

This piece prepends a div to the top of the calendar to be used as a trash can.

$('##calendar').children('.fc-content').prepend('<div id="trash-can" style="border: 2px solid ##C1454B;padding:15px 100px 15px;border-radius:5px;background:##DD9094;text-align:center;">Drag Events Here To Remove</div>');

Wednesday, June 15, 2011

Writing text over an image

I know alot of people have already done this, but I thought I would share writing text over image as well. Only because the first time you do it, it's really cool.

Read in the image

<cfimage name = "local.image" action="read" source="test.jpg"/>


Turn on anti-aliasing ("softens jagged edges")

<cfset ImageSetAntiAliasing(local.image)/>


Set the text color

<cfset ImageSetDrawingColor(local.image, "000000")/>


Set extra attributes

<cfset local.attrs = {
Font = 'Arial',
Size = 36,
Style = 'bold'
}/>


Draw over the image (image,text,x-position,y-position,attributes)

<cfset ImageDrawText(local.image, "Hello World!", 10, 10, local.attrs)/>

SQL replace() blank stuff

Will never return 0

select replace('','',0)


Will return zero if the value is ''

select case when '' = '' then 0 else 1 end

SQL Selects ='s

Instead of writing a select in a query like this


select
[user].id,
[user].name,
snowboard.name as snowboard
from [user]
inner join snowboard on snowboard.id = [user].snowboard_id


try this


select
[user].id,
[user].name,
snowboard = snowboard.name
from [user]
inner join snowboard on snowboard.id = [user].snowboard_id


It helps all the columns line up and read nicer.

Monday, March 28, 2011

ColdMVC: get an object's parents

Awhile back I posted on flattening an array of objects with Parent-Child relationship (link) and have been using it alot. But one issue I had with it was when I was looping through the array of objects I didn't know who my parents were for the current object I was working with. I already knew how deep thanks to the treeDepth property. I made this function to recursively go up the tree of objects and bring back an array of parent objects for an object.


<cffunction name="getObjectsParents" access="public" output="false" returntype="array">
<cfargument name="object" required="true"/>
<cfargument name="result" required="false" default="#[]#"/>

<cfif isObject(arguments.object.parent())>
<cfset arrayPrepend(arguments.result,arguments.object.parent())/>
<cfreturn getObjectsParents(arguments.object.parent(),arguments.result)/>
<cfelse>
<cfreturn arguments.result/>
</cfif>

</cffunction>

Tuesday, January 4, 2011

CF9 computed properties

I wanted to make a computed property in CF9 just like you can on a sql table (computed column). Dan Vega had a nice how-to awhile back but it didn't show a complex example.

In my project I had a "project" class that had many ratings on it (Example: rating of 1-5, 1 being bad and 5 being great) and I wanted to have a computed property that gave me the average rating of a project. Here is what my hib file looks like for the "project" class.


<class entity-name="Project" lazy="true" name="cfc:myproject.app.model.Project" table="`Project`">
<id name="id" type="int">
<column length="10" name="ID" />
<generator class="identity" />
</id>
<property name="name" type="string">
<column name="Name" sql-type="varchar(max)" />
</property>
<property name="description" type="string">
<column name="Description" sql-type="varchar(max)" />
</property>
<property type="float"
formula="(select (sum(r.rank)*100.0) / (count(pr.id) *100.0) from project as p inner join project_rating as pr on pr.project_id = p.id inner join rating as r on r.id = pr.rating_id where pr.project_id = id)"
name="rating"/>
</class>

Some key things to point out:
1. The formula is a sql query not a hql query.
2. I wrapped my entire statement in (), don't know why but it worked.
3. You always start the sql statement with the table of the class you are working on. Reason being is when you get to the "where" clause notice this piece "pr.project_id = id". "id" is not aliased because it grabs the current id of the object that is running the formula against. This is how the query will only run for single object and not all of the other db records in the project table.
4. Alias every thing except the "where id" part.

I struggled through these above. Hope this helps someone.

Thursday, December 30, 2010

DSL's or Domain Specific Languages

I saw this at Adobe MAX last year and I finally got a chance to try it out. DSL or Domain Specifically Languages are languages that only reside in a domain, in our case the web app we are developing. A good example of where DSLs are used is in gMail in the search box. If you haven't noticed it yet they are really handy, but they only work in gMail. Another good one is MS Outlook, they support crazy operators. Here is what one would look like:

dsl

I grabbed this one from gMail. The convention seems to use a keyword followed by a ":". In the case above it's "from:". I have only found DSLs helpfully for search boxes. If you are going to use DSLs you need to have legend so your users know what keys are available. You need to support it everywhere or tell the user where it is supported. If you are going to use them you should do whatever it takes in the keys and language to make it easier for the user. DSLs should be quick to use. Some advanced DSLs might look like this "from:(name:*joe)" or something like that or if operators are involved.

Once the user enters the DSLs you need to parse the string and do something with it. I create a query criteria snippet for sql or orm queries to allow the user to search across other things in the search box.

DSLs aren't really user friendly that's why I would recommend "search areas". Search areas are much easier to read, but less flexible. I posted on it early tonight here.

Getting all the URL params in Javascript

I've been updating a lot of href's lately and found these functions to be very handy. They basically take all the pars in a string and put them in a JS object. There is other functions out there that get you 1 url param, but I wanted them all. Reason being, is I need to see if that param exists and do some more checks on it for business logic, then take all the pars and turn then into a querystring and add them back to href again.


getURLParameters = function(url){

var results = {};

if(url == ""){
var url = window.document.URL.toString();
}
if (url.indexOf("?") > 0){
var splitURL = url.split("?");
params = splitURL[1].split("&");

for (var i=0;i var param = params[i].split("=");
if (param[1] != "")
results[param[0]] = unescape(param[1]);
else
results[param[0]] = "";
}

}

return results;
};
getURL = function(url){

if(url == ""){
var url = window.document.URL.toString();
}

if(url.indexOf("?") > 0){
var splitURL = url.split("?");
url = splitURL[0];
}

return url;
};


getURL() actually serves two purposes. If a url isn't passed in get the current url. And it also cleans off all of params so all you get the base url.

Search Areas

Had a task where a user didn't want to use the primary "search by" of searching in a list which is usually to search by "name" or "number". I did some research and found a way to keep the majority users satisfied, yet still giving the ability to the advanced user. Check it out...

Regular search box:
plain

Search box with search areas:
search_areas

It's actually not to tricky to do. It's just a table with two cells. The left cell is the input box with a style of no boarders and right cell is a div with a pic in it. When you click the div with the pic in it another div below is shown with absolute positioning. I will let you figure out the javascript and css. It's not too hard.

iStockPhotography currently has something similar.

ColdMVC: AJAX call

Trying to make ajax calls with ColdMVC was a little difficult for me because I struggle with routes, but I found a way. Here is what I did to make an AJAX call with ColdMVC.

First I created a function on a Controller so I can return "hi" back from the AJAX call. Notice I have @view json.cfm above the function. Since ColdMVC uses routes I need to send the data to a view and cfoutput it on the page.

TestController

/**
* @accessors true
* @extends coldmvc.controller
*/
component {

/**
@view json.cfm
*/
function testMethod(){
params.json = "hi";
}

}


Here is json.cfm. All that I do with it is output the variable "json".

json.cfm

<cfoutput>#params.json#</cfoutput>


And lastly I preform my AJAX call using jQuery. In the url setting of jQuery.ajax() I just link to the controller. In my case, my controller's name is TestController, so my route with just be "test" and then I can change out the method. In my case I am calling the "testMethod" on the controller.

ajaxCall = function(){

jQuery.ajax({
url:"#coldmvc.link.to('/test')#/test?returnformat=json",
type:"GET",
success:function(data){
jQuery("body").html(data);
}
});
};


If you want to return json back just go to json.cfm and wrap params.json in SerializeJSON(). Then in your ajax call, do jQuery.parseJSON(). This is the only way I can think of right now to make an ajax call with ColdMVC, if there are any other techniques I would be more then happy to listen.

Tuesday, October 19, 2010

CF9 hibernate file includes.

This was so cool I had to repost it. After you've defined a few hibernate.hbmxml files it gets annoying to have type the same properties on every class. My fingers were getting sore to complain a little. After a reference shown to me by Tony Nelson, Mark Mandal had a great post on "includes in the hibernate file". Check it out.

If you notice in the hibernate.hbmxml below there is a line that says "&common;".

hibernate.hbmxml

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"
[ <!ENTITY common SYSTEM "common.hbm.xml"> ]
>
<hibernate-mapping>

<class entity-name="User" lazy="true" name="cfc:beforeandafter.app.model.User" table="`User`">
&common;
<property name="firstName" type="string">
<column name="First_Name" length="200" />
</property>
<property name="lastName" type="string">
<column name="Last_Name" length="200" />
</property>
<property name="emailAddress" type="string">
<column name="Email_Address" length="200" />
</property>
<property name="password" type="string">
<column name="Password" length="200" />
</property>
<bag name="projects">
<key column="User_ID" />
<one-to-many class="cfc:beforeandafter.app.model.Project" />
</bag>
<bag name="projectRatings">
<key column="User_ID" />
<one-to-many class="cfc:beforeandafter.app.model.ProjectRating" />
</bag>
</class>
</hibernate-mapping>


&common; points to file called common.hbm.xml. Within common.hbm.xml file I defined all my similiar properties and just include it on all my classes.

common.hbm.xml

<id name="id" type="int">
<column length="10" name="ID" />
<generator class="identity" />
</id>
<property name="isDeleted" type="boolean">
<column name="isDeleted" />
</property>
<property name="createdBy" type="int">
<column length="10" name="Created_By" />
</property>
<property name="createdOn" type="timestamp">
<column name="Created_On" />
</property>
<property name="updatedBy" type="int">
<column length="10" name="Modified_By" />
</property>
<property name="updatedOn" type="timestamp">
<column name="Modified_On" />
</property>

Make sure your include file ends in .xml and not .hbmxml otherwise it won't work. Made my life a ton a easier. Thanks Mark and Tony for sharing this.

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?

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.

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.

Sunday, April 11, 2010

ColdMVC: Create your own Helpers.

I wanted to try extending ColdMVC's helpers and create my own helper that formatted stuff for me and it turned out to be quite simple.

First, I created my own directory in my project called "helpers".

Second, I added a cfc named "format".

Third, I extended the ViewHelper from ColdMVC. Not sure, If this is the correct Helper Util class I am suppose to be using...but it worked.


<cfcomponent extends="coldmvc.utils.ViewHelper">

<!------>

<cffunction name="money" access="public" output="false" returntype="any">
<cfargument name="amount" required="false" default="0"/>

<cfreturn dollarFormat(arguments.amount)/>


</cffunction>

<!------>

</cfcomponent>


Lastly, I used my new helper function:

#$.format.money(product.getPrice())#


If you noticed in money() I set a default value of zero. For some odd reason when I put "product.getPrice()" in for the amount argument it has a value of [empty string] and it throws an error. The error says "The AMOUNT parameter to the money function is required but was not passed in.". But clearly there is a value of [empty string]. If any one knows what up give me hollar.

Entity Name is same for two CFCs

As I have been trying out CF9 and the ColdMVC I occasionally get this error "Entity Name Category is same for two CFCs". Where "Category" is my model cfc. This is ok if I really had two CFC's with the same name. Here's the rest of the error:

Entity Name Category is same for two CFCs, superior.app.model.Category and superior.app.model.Category.

As you can see, the error looks like it's pointing at the same file. Searching up and down my Eclipse project explorer I find no two cfc with the same name. Guess what...do a refresh on your model directory or where ever you put your VO's. You probably have two hibernate files, both containing your VO's name...mine was "Category".

Get rid of one of them and it should work.