Things learned from creating the RoomBooking System (part 2)

The Room Booking system is now available as v 1.01; the main addition being permissions, roles and authentication. As before, I thought it might be useful to highlight a few of the thought processes behind this update, and look at some of the problems come across when beginning to think about an application which is due for distribution in all sorts of environments.

Authentication varies massively depending on the setup – here are a few common scenarios:

Using authentication external to the application, i.e at a IIS/Apache level:

  1. Block the whole website: e.g, have everything behind apache’s htpasswd authentication, or restrict to a specific IP Range, or VPN connection etc
  2. Block only “admin” level functions: e.g specific authentication triggered for all /admin/ requests: again, trigger via .htpasswd or somesuch.
  3. Using Active Directory/LDAP to authenticate a user, and use groups specified outside the main application to load a user into a specific role
  4. Other SSO systems (shibboleth etc) integrated into IIS/Apache

Authentication within the application:

  1. Using traditional cflogin, and built in CF login functions
  2. Testing for session based variables specifically, i.e session.user.loggedinAt

Obviously there are a million other variations on this, but it does highlight that whatever your doing should at least consider these other methods.

What are you trying to protect?

Obviously, this is the first question you have to ask yourself. What is it that requires some level of authentication? Nine times out of ten, it’s the privilege of changing or viewing data, that much is pretty self explanatory. But it’s worth considering all data you’re storing, and how that might be used. For instance, blocking the web interface of an application is one thing, but what about potential API usage? RSS Feeds? Other connection types which don’t require a browser? For the Room Booking system (RBS after this point, I can’t be bothered with the typing), it’s relatively straightforward, at least at the moment. But in the future I’m planning to add RSS/iCS/JSON/XML feeds – perhaps only read only, but whatever authentication system, I’ve got to consider future usage.

What I ended up with:

In order for the RBS to be as flexible as possible to the most number of users, I ended up decided on (application specific) Role based permissions. In short, if person ‘bob’ is in role ‘admin’, and tries to do ‘x’ there needs to be some sort of permissions matrix indicating the admin role’s permission to do ‘x’. The important part to note here is that I’m not ever directly testing against a role. There should be  no conditional statements like if(isAdmin(user)) then do ‘x’; this leads to a nightmare later on when you suddenly decide you want to add a role, as for each ‘isAdmin()’ check, you then have to start adding ‘isEditor()’ or somesuch, and probably all over your application.

It’s better to build a privilege called ‘canDoX’ and test against that, as roles can more easily be added in the future.

Wheels lets us handle this quite neatly with two models, users & permissions. A user has a role, such as ‘admin’ or ‘editor’ etc; Then in the permissions table, you have a permission key, which is the name of your permission, i.e canDoX, and a column for each role, with a tinyint of 1 or 0 depending. So it’s a simple matrix of yes/no type data.

Then, onApplicationStart, we can check for an load in the matrix of permissions into the application scope for referencing later:

<cfset application.rbs.permissionsQuery=model("permission").findAll()>
 <cfloop query="application.rbs.permissionsQuery">
   <cfscript>
      application.rbs.permission["#id#"]={};
      application.rbs.permission["#id#"]["admin"]=application.rbs.permissionsQuery["admin"];
      application.rbs.permission["#id#"]["editor"]=application.rbs.permissionsQuery["editor"];
      application.rbs.permission["#id#"]["user"]=application.rbs.permissionsQuery["user"];
      application.rbs.permission["#id#"]["guest"]=application.rbs.permissionsQuery["guest"];
   </cfscript>
 </cfloop>

Once we’ve got these in the application scope, all we need are four functions to handle most of the permission based work:

<cffunction name="checkPermission" hint="Checks a permission against permissions loaded into application scope for the user" returntype="boolean">
	<cfargument name="permission" required="true" hint="The permission name to check against">
	<cfscript>
		if(_permissionsSetup() AND structKeyExists(application.rbs.permission, arguments.permission)){
			return application.rbs.permission[arguments.permission][_returnUserRole()];
		}
	</cfscript>
</cffunction>

<cffunction name="checkPermissionAndRedirect" hint="Checks a permission and redirects away to access denied, useful for use in filters etc">
	<cfargument name="permission" required="true" hint="The permission name to check against">
	<cfscript>
		if(!checkPermission(arguments.permission)){
			redirectTo(route="denied", error="Sorry, you have insufficient permission to access this. If you believe this to be an error, please contact an administrator.");
		}
	</cfscript>
</cffunction>

<cffunction name="_permissionsSetup" hint="Checks for the relevant permissions structs in application scope">
	<cfscript>
		if(structKeyExists(application, "rbs") AND structKeyExists(application.rbs, "permission")){
				return true;
		}
		else
		{
			return false;
		}
	</cfscript>
</cffunction>

<cffunction name="_returnUserRole" hint="Looks for user role in session, returns guest otherwise">
	<cfscript>
		if(_permissionsSetup() AND isLoggedIn() AND structKeyExists(session.currentuser, "role")){
			return session.currentuser.role;
		} else {
			return "guest";
		}
	</cfscript>
</cffunction>

So all you’d need to do using the above is:

<cfscript>
if(checkPermission("canDoX")){
// Do stuff
}
</cfscript>

The checkPermissionAndRedirect() function is very similar, and can be used on a filter level in your controller to just push away the user if they shouldn’t be accessing a view/function etc

i.e, on my Locations.cfc controller, where CRUD updating for the RBS board locations takes place:

filters(through="checkPermissionAndRedirect", permission="accessLocations");

The advantages of this approach are:

  • You’ve got an ‘out-the-box’ system which doesn’t require extensive server setup
  • You could extend this principle to allow for 3rd party authentication – i.e, if you’re going via LDAP, once a user is approved, a ‘local’ account could easily be created which duplicates core information such as email/name from the initial authentication (see an example I did a few years ago here)
  • Permissions are cached in the application scope, meaning you potentially avoid another database hit for every user login
  • In the future, user based permissions which override role based permissions could easily be added: our checkPermission(“foo”) function could easily look for the equivalent key in the user’s session scope
  • Adding new permissions to use is as simple as adding a line in the permissions table
  • Each installation can redefine the abilities of each role to suit their needs: so in the RBS example, you could make every role do everything, and then use some other authentication method, or lock down guest users etc.

All this code can be seen in context at the GitHub Repo

v1.01 RoomBooking system released

So I admit that’s a pretty quick release cycle, but hey, I was inspired (or something). There’s some massive changes, most noticeably the addition of user authentication, roles, permissions, logging & password resets. All documentation has been moved to https://github.com/neokoenig/RoomBooking/wiki, where you can find a breakdown of all the settings, permissions, roles and upgrading instructions.

Things learned from creating the RoomBooking System

Since launching my little Room Booking system as an open source project, I thought it might be useful to highlight a few techniques used which others might find useful. Obviously, this isn’t to say that this is the ‘only way to do x’, but merely to flag some ways to approach common problems. All the below can be seen in context at the gitHub repo

events/functions.cfm

One issue a had a while back was determining the IP address of a user. Now, historically, this wasn’t an issue, as cgi.remote_host worked fine. However, since Railo’s installer changed the way it forwards requests from Apache (mod_jk-> mod_proxy), it always saw the remote IP as 127.0.0.1. This function checks the incoming request header and tries to grab the IP there, rather than in the CGI scope.

<cffunction name="getIPAddress" hint="Gets IP address even from Railo" returntype="String">
<cfscript>
   var result="127.0.0.1";
   var myHeaders = GetHttpRequestData();
    if(structKeyExists(myHeaders, "headers") AND structKeyExists(myHeaders.headers, "x-forwarded-for")){
      result=myHeaders.headers["x-forwarded-for"];
    }
    return result;
</cfscript>
</cffunction>

events/onrequeststart.cfm

This simply checks every incoming request for the URL or FORM scope, loops over each key in the struct, and trims the whitespace.

<cfif StructCount(form)>
	<cfloop collection="#form#" item="key">
		<cfset form[key] = Trim(form[key])>
	</cfloop>
</cfif>
<cfif StructCount(url)>
	<cfloop collection="#url#" item="key">
		<cfset url[key] = Trim(url[key])>
	</cfloop>
</cfif>

Personally, I think cfWheels should do this by default, but then, it would probably break a load of people’s applications. Simply put, I can’t imagine a case where I’d want whitespace at the beginning or end of any key in my url or form scope.

view/helpers.cfm

This one was an interesting one – Javascript often passes it’s values in Epoch time, i.e number of seconds since 1970; I needed to convert this to localTime in a few places. Surprisingly, I’ve never had to do this before!

<cffunction name="eToLocal" hint="convert epoc to localtime">
	<cfargument name="e">
	<cfreturn DateAdd("s", arguments.e ,DateConvert("utc2Local", "January 1 1970 00:00"))>
</cffunction>

So Bootstrap3 has panels. I love panels. But I’m lazy, I don’t want to type out the markup for a panel all the time. So these two helpers do it for me:

<cffunction name="panel" hint="Renders a bootstrap Panel">
    <cfargument name="title" required="true">
    <cfargument name="class" default="panel-default">
    <cfargument name="ignorebody" default="false">
    <cfset var r ="">
    <cfsavecontent variable="r"><Cfoutput>
    <!--- Start Panel --->
    <div class="panel #arguments.class#">
        <div class="panel-heading">
            <h3 class="panel-title">#arguments.title#</h3>
        </div>
        <cfif !arguments.ignorebody>
        <div class="panel-body">
        </cfif>
    </Cfoutput>
    </cfsavecontent>
    <cfreturn r />
</cffunction>

<cffunction name="panelend" hint="Close Panel">
    <cfargument name="ignorebody" default="false">
    <cfif !arguments.ignorebody>
        <cfreturn "</div></div>" />
    <cfelse>
        <cfreturn "</div>" />
    </cfif>
</cffunction>

Usage:

#panel(title="foo")#
.. things here
#panelEnd()#

Sometimes Bootstrap needs it’s content to be flush with the panel, so you occasionally need to ignore the panel-body – flush list-group-items – I’m looking at YOU.

#panel(title="foo", ignorebody=true)#
<ul class="list-group">
<li class="list-group-item">FOO</li>
</ul>
#panelEnd(ignorebody=true)#

Previously, I’d have done this using custom tags, i.e , but with the added import needed on every page, I find this a *lot* quicker/easier.

views/layout.cfm

One of things I like to do is have all the CSS in the header, and all the JS in the footer. So what happens when you want to do your JS stuff in your view folders? Here’s one solution..

In your view file, simply wrap your JS bits in cfsavecontent, i.e

<cfsavecontent variable="request.js.mystuff">
<script>// JS goes here</script>
</cfsavecontent>

Then in your layout file, after all your calls to jQuery etc:

<cfif structkeyexists(request, "js")>
<cfloop list="#structKeyList(request.js)#" index="key"><cfoutput>#request.js[key]#</cfoutput></cfloop>
</cfif>

The only thing this doesn’t really deal with is calling JS in a specific order, as structs are by their nature, unordered. I guess you could turn this principle into an array based system easily enough though: generally speaking, I haven’t found needing to write JS over many view files which needs to be executed in a specific order. YMMV.

bookings/index.cfm

This bit of JS is very useful:

 $('body').on('hidden.bs.modal', '.modal', function () {
        $(this).removeData('bs.modal');
});

Bootstrap caches modals which have had dynamic data inserted in, so initially, when I was building it, every time I clicked on an event, the modal would display the first event I’d click on’s data. This destroys the modal data after you close it. Handy.

And another thing…

The last thing to note is that this has an example of using Ajax in cfWheels, examples of which are fairly rare. If you’re interested in how that’s done, look at the javascript call in bookings/index.cfm under event sources: it’s just a jQuery ajax request. Now look at controllers/Eventdata.cfc = you’ll see the renderWith call, and if you look at the function ‘prepdata’, you’ll see me converting a query into a array of data (in the format I wanted it), which is then passed back by wheels using renderWith(events);

So there you go. If, of course, you guys have better ways of handling the above, I’d love to hear them.

New Open Source Project – Room Booking System

I’m pleased to announce a new open source project for a room booking system – it’s basically a backend to the excellent fullcalendar.js jQuery plugin, using cfWheels as the back end.

Features include:

  • All (well most) configuration done via database settings
  • Uses the DBMigrate plugin for cfWheels for easy future updates
  • Uses the FullCalendar.js month/day/week views
  • Allows for multiple locations, which can be filtered from the main view
  • Each location can be colour coded, and have room layout options
  • Allows you to bulk create events, so easy to add placeholders for repeating series
  • Completely configurable via web interface, all you need to do is setup a datasource called ‘roombooking’
  • Has a theme switcher so you can use/try any theme on http://bootswatch.com/
  • Can send confirmation emails on a new booking

You can download the code from gitHub – https://github.com/neokoenig/RoomBooking or try a demo at http://roombooking.oxalto.co.uk. Some screenshots below. Full documentation at http://roombooking.readme.io/

 

Feel free to contribute or log a bug at https://github.com/neokoenig/RoomBooking – hopefully it’ll be useful for someone!

Upgrading JRE for Railo on Centos

This is really a set of notes for me so I don’t forget all this in the future, but hopefully might help others.

If you look at the Railo wiki on github about this, it gives you great instructions about 6, but I found my experience slightly different for 7.

These are the steps to install JRE from Oracle on Centos 64bit; (for the purposes of this post, this is Version 7u25)

  1. Go to http://www.oracle.com/technetwork/java/javase/downloads/index.html
  2. Look for the JRE Download Link
  3. Save the jre-7u25-linux-x64.rpm to your local machine
  4. Upload it to your root or home folder on the server
  5. Assuming you’re logged in via SSH, run:
    rpm -i jre-7u25-linux-x64.rpm
  6. Shut down railo:
    /opt/railo/railo_ctl stop
  7. Backup / rename the old JRE:
    cp -R /opt/railo/jdk/ /opt/railo/jdk.bak 
    rm -f /opt/railo/jdk 
  8. Find the newly installed .rpm  – hint, probably:
    /usr/java/jre-7u25/
  9. Rename to:
    /usr/java/jdk
  10. Now, you can symlink this version to the Railo install:
    ln -s /usr/java /opt/railo/jdk
  11. Restart Railo:
    /opt/railo/railo_ctl start
  12. Login to Railo Server Admin and double check the JRE version running on the front page.
  13. Beer.

Thanks to  Scott at Viviotech for the hint about the .rpm and symlinking!

What a difference a new server can make

Every now and then, it’s good to have a clean out huh.. 2-3 seconds response time (!) down to 200ms. Not quite sure what happened with that old box, but hey. Fresh installs FTW.

VPS Hosting revisited

So two years ago I started up a linode – yep, I’m still with them for my LAMP stack stuff. I really have no complaints, whatsoever. Since being with them they’ve bumped up my storage to 48GB from 40GB (for free), I’ve had one network problem (which affected everyone in my datacentre) about 18 months ago, but apart from that – nada. zip. Not had to contact support, performance from the VPS itself has been excellent. Awesome.

So now it’s time for me to re-evaluate my other box which is running Railo (and everything else under the sun). This one, running Centos5 is feeling fairly long in the tooth. One of the reasons I’m looking round for a new VPS provider for Railo is that for some reason, Railo has decided to start being slow – like, instead of the usual 500ms response time average, that average has slipped to 2-3 seconds. However, as a) it’s an unmanaged VPS and b) even if I contacted my VPS support they wouldn’t really have a clue about JVM tuning, I’m a little out of luck, bar reinstalling Railo/Java and hoping for the best.

As hosting Java is a smidge more complex than your average PHP hosting, (well, different anyway) ‘Googling’ it doesn’t alway help much.

So, I’ve decided to try Viviotech. Several things have lead me here: 1) very, very good reports from the CF community generally 2) my pre-sales questions were answered so stupidly comprehensively by Jordan Michaels, including appropriate linux distros and future plans for load balancing 3) the price is about right – not worryingly cheap, but not stupidly expensive either and 4) Jordan actually wrote the Railo installer, plus he’s actually answered my questions in the past on the Railo Google Group, which gives me some faith in the support structure.

I ordered it at 9am UK time: within 10 minutes I got a call from Viviotech (from the US, to my UK mobile!) checking/confirming the order. It must have been 1am their time (!). Once all confirmed, off they went. It did take them a day to setup, but I wasn’t in much of a rush, so no problem there. Once all the details arrived, it was just a matter of logging in.

Some nice things they do out the box – move SSH to a non-standard port, add an anti-brute force hack script, setup backups, configure some basic firewall stuff and install vsftp (which personally I don’t use, as sftp should be a minimum). I  asked for Railo to be installed as I was feeling lazy: the only thing which caught me by surprise was Railo running as root – on querying this, they do it for usability, and pointed me to a simple script to change the user Railo runs as. All the Tomcat/Apache connectors were setup and working too.

So so far all very good – there’s something nice in not being *completely* on your own like with fully unmanaged solutions such as Linode. The only thing I miss is those lovely graphs you get in the Linode dashboard (oh and that iPhone app which is amazing) – still I guess there’s always Munin.

You can find out more about Viviotech here

A response to ‘What does your VLE say about your institution?’

In response to “What does your VLE say about your institution?” on the TALL blog at Oxford University – they pose the statement:

The evolution of an institution’s VLE is the narrative of that institution’s attitude towards, and relationship with, learning technologies.”

To me, VLE design usually falls into two categories: 1) an open source solution, such as Moodle/Sakai, and 2) bespoke applications, usually written in house.

Out the box, OSS solutions often don’t fufil the required criteria from a VLE: they all require customisation, setting permissions/roles, branding etc. Even then, the chances of them doing what you want them to do without a fair bit of work is fairly slim.

It’s a classic – “here’s a potential solution, how can we manipulate it to our needs to solve our problem” – approach.

From the other end, bespoke solutions reverse this thinking  – “we have a problem, how do we solve it, let’s make a solution specific to our needs.”

The latter approach usually requires more money, but can ultimately save time and effort by not trying to manipulate an existing system – to me this shows a more mature approach to online learning: a badly configured OSS solution (such as found in the majority of Schools – less so in Higher Edu) just represent either underinvestment, or a lack of knowledge in what is required from a website, let alone a fully fledged VLE.

I do believe VLEs are pretty much representative of an institution’s relationship & attitude to online learning (and indeed the web as a whole), but a good VLE deployment requires more than just good intentions, and needs investment, and more importantly – continual development – which is where most of them fall short.

cfWheels Relationships (Part 5)

HasManycheckbox() is obviously only one way of updating these relationships. The other main way is via cunning uses of includePartial().

Let’s return to our contact’s email addresses – remember the contact ‘hasMany’ of those. We need a way to a) list their existing email addresses, b) add new ones (preferably within the same update form as the main contact details and c) edit or delete existing addresses.

When you need to display/update more information that what a checkbox could hold, includePartial() comes to the rescue. Whilst I’m demonstrating this with the email address field, which just has a single value, there’s nothing stopping you adding additional fields for this example.

In our Contact’s edit form, we can simply do this:

<fieldset>
<legend>Email</legend>
    #includePartial(contact.emailaddresses)#
    #includePartial("emailaddressNew")#
</fieldset>

And then create a partial called _emailaddress.cfm in the /views/contacts/ folder, containing:

<cfoutput>
#btextField(
    objectName="contact",
    association="emailaddresses",
    position=arguments.current,
    property="email",
    label="Email Address",
    class="span4"
)#
</cfoutput>

This should look pretty familiar – after all, it’s *almost* the same as a bog standard textField on a model, except we’re listing the association, and crucially, the postition.

So if we’ve got multiple email addresses, what does the generated HTML look like?

<label class="control-label" for="contact-emailaddresses-2-email">Email Address</label>
<input class="span4" id="contact-emailaddresses-2-email" maxlength="500" name="contact[emailaddresses][2][email]" value="joe3@bloggers.com" type="text">
<label class="control-label" for="contact-emailaddresses-3-email">Email Address</label>
<input class="span4" id="contact-emailaddresses-3-email" maxlength="500" name="contact[emailaddresses][3][email]" value="joe2@bloggers.com" type="text">

The [emailaddresses][3][email] part of the naming convention translates to “object[association][id][property]”.

And when submitted, with params.contact, we can see a nested struct:

If we’ve included the association when we do the contact updating, i.e (contact.update(params.contact);) these properties will automatically update, with no extra code.

That’s all good for existing entries, but what about adding new ones? Wouldn’t it be nice if we could just “reinclude” that partial to add a new form? If you try that, you’ll hit the ‘arguments.position’ error – as that partial is expected a numeric value for the position in the array that the property is in. Even if you try and set a tickCount as a unique identifier, you’ll hit snags too. This is probably the most annoying thing (currently, i.e wheels 1.1.8) – if someone’s got an elegant solution to this, please let me know 🙂 At the moment I fall back to either adding via a separate params struct, and testing for it’s existence, or doing it via Javascript.

Right, that’s all for now – thoughts on improving this greatly recieved.

cfWheels Relationships (part 4)

So far we’ve setup the database tables and the models, so now we need to start thinking about getting and updating this contacts data, including the associated nested properties.

Outputting a simple add/edit form should hopefully be familiar to you by now (if not I recommend checking out the cfwheels screencasts: http://cfwheels.org/screencasts ). When updating our nested properties, there are several methods, each with pros & cons.

Firstly, let’s have a look at HasManyCheckBox();

The hasManyCheckbox() helper is really, really useful. I’m going to use our technologies relationship, as this is just the sort of data which is designed to be displayed/updated in this fashion.

#hasManyCheckBox(
    objectName="Contact",
    association="contactTechnologies",
        keys="#contact.key()#,#id#",
        label=name)#

Now what does this actually do? The loop itself should be obvious – we’re looping over *all* the potential values for technologies – after all, we want to be able to list those entries which aren’t ticked too: but we need to be able to make sure that the entries which are present in the join table are properly ticked. ObjectName=”Contact” is our parent object – the one which holds the nested properties. Association is the name of the nested property itself. Next, you have the most important part, the composite key. Remember when I mentioned about the column key order in the database construction for our join table? *This* is why it’s important: the order of the keys here *MUST* reflect the order in the database schema. I’ve used contact.key() which allows me to do this on a new model (which won’t actually have a primary key per se – yet).

So what does this wonderful piece of code actually output?

<label class="checkbox" for="Contact-contactTechnologies-1-1-_delete">ColdFusion<input id="Contact-contactTechnologies-1-1-_delete" type="checkbox" name="Contact[contactTechnologies][1,1][_delete]" value="0" checked="checked" /><input id="Contact-contactTechnologies-1-1-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,1][_delete]($checkbox)" value="1" /></label>

<label class="checkbox" for="Contact-contactTechnologies-1-4-_delete">CSS<input id="Contact-contactTechnologies-1-4-_delete" type="checkbox" name="Contact[contactTechnologies][1,4][_delete]" value="0" /><input id="Contact-contactTechnologies-1-4-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,4][_delete]($checkbox)" value="1" /></label>

<label class="checkbox" for="Contact-contactTechnologies-1-5-_delete">HTML5<input id="Contact-contactTechnologies-1-5-_delete" type="checkbox" name="Contact[contactTechnologies][1,5][_delete]" value="0" /><input id="Contact-contactTechnologies-1-5-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,5][_delete]($checkbox)" value="1" /></label>

<label class="checkbox" for="Contact-contactTechnologies-1-6-_delete">jQuery<input id="Contact-contactTechnologies-1-6-_delete" type="checkbox" name="Contact[contactTechnologies][1,6][_delete]" value="0" /><input id="Contact-contactTechnologies-1-6-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,6][_delete]($checkbox)" value="1" /></label>

etc. As you should be able to see from the above, there’s actually some fairly complex stuff going on: imagine it as cfWheels by default adding all the associations & relationships, then deleting all the relationships, then by checking a checkbox, you save that association from being deleted. Looking at the dumped params struct makes this more obvious:

The ‘1,1’, ‘1,2’ struct keys are the composite keys: 1 representing the contactid, and the second representing the technology (in this case). If the checkbox is checked, then the _DELETE sub struct returns false.

What’s nice about this approach is that you don’t have to do anything extra; when submitting and creating the final contact object, you just have to make sure you include the associations.

‘Why that’s awesome!’ I hear you say. Yes, yes it is. BUT. It doesn’t help us in a few areas.

1) We can’t add new values to the list easily – i.e I can’t add a new technology
2) We can’t edit the associated properties easily – i.e I can’t edit an existing technology

Of those two points, I would argue editing existing properties from within the contact update/add form is perhaps not the place for it – these properties could apply to other contacts, so pushing them out into their own form is probably better from a user perspective – there’s then no chance of the user thinking ‘oh I’ll just change ColdFusion to Railo’ and then the horror when all other contacts then list Railo too.

Adding new values to the technology list is however something I see happening. To do this, I’ve found the easiest solution is to just create a new form field tag, and then test for the values when updating the contact object.

This means when we submit our update or create request, we need to test for the existence of the field, return the created key, and then add the association manually.

I’ve found the easiest way to do this (so far) is to add it to a new private function: So our update function calls our new checking function ‘checkForNewTechnologies()’:

contact.update(params.contact);
	if(contact.hasErrors()){
         renderPage(action="edit");
	}
        else {
	checkForNewTechnologies();
	 flashInsert(success="The contact was updated successfully.");
	 redirectTo(action="view", key=contact.id);
}

Which looks like:

<cffunction name="checkForNewTechnologies" access="private">
<cfscript>
  if(structkeyexists(params, "newtechnology") AND len(params.newtechnology) GT 2) {
		nTechnology=model("technology").create(
			name=params.newtechnology
		  );
		nTechnologyAssociation=model("contacttechnology").create(
			contactid=contact.key(), technologyid=nTechnology.key()
		 );
  }
</cfscript>
</cffunction>

Obviously, this is just one way of many to do it. You could add the new technology via Ajax, return the generated ID, then replicate the form HTML via JS to pass the new value in along with the other checkboxes: this is quite a cumbersome technique as you rely on recreating the wheels generated HTML manually, but might have it’s uses.