FarCry – grouping formtool category items

A new project we’re working has the need to categorise each new content object, for this we use the built in FarCry category/keyword formtool.

By default the render type is a multi-select tree, I wanted a single select dropdown. Luckily formtools makes this oh so simple.

<cfproperty name="catHTML" type="nstring" required="true" default="" ftSeq="2" ftwizardStep="Content"  ftValidation="required" ftFieldset="General Details" ftLabel="Categories" ftType="Category" ftAlias="sections" ftRenderType="dropdown" ftSelectSize="1" ftSelectMultiple="false" ftAliasAsOptGroup="true">

Note that I’ve set the dropdown size to 1 and multiple select to false. Basically I have a group of “sections” that each content object needs to belong to, section 1 through 11. I’ve setup the categories already under a parent category called “sections” (note the ftAlias in the formtool definition). Unfortunately the default rendering was showing “sections” as a selectable category, however I didn’t want this. I only wanted the users to be able to choose the children of this category, not the category itself.

Luckily FarCry makes this kind of thing simple to override. I added my own category.cfc inside my projects packages/formtools directory and made sure to extend category.cfc from farcry.core.packages.formtools.category. In this I only used the edit() function to render an optgroup from a new custom property called ftAliasAsOptGroup (see above snippet). This gave me the following:

FarCry optgroup

The full category.cfc is below if you’re interested.

<cfcomponent extends="farcry.core.packages.formtools.category">

	<cfproperty name="ftAliasAsOptGroup" default="" hint="If true, renders ftAlias as an optgroup">

	<cffunction name="edit" access="public" output="false" returntype="string" hint="his will return a string of formatted HTML text to enable the user to edit the data">
		<cfargument name="typename" required="true" type="string" hint="The name of the type that this field is part of.">
		<cfargument name="stObject" required="true" type="struct" hint="The object of the record that this field is part of.">
		<cfargument name="stMetadata" required="true" type="struct" hint="This is the metadata that is either setup as part of the type.cfc or overridden when calling ft:object by using the stMetadata argument.">
		<cfargument name="fieldname" required="true" type="string" hint="This is the name that will be used for the form field. It includes the prefix that will be used by ft:processform.">

		<cfset var html = "" />
		<cfset var navid = "" />
		<cfset var oCategory = createObject("component",'farcry.core.packages.farcry.category')>
		<cfset var lSelectedCategoryID = "" >
		<cfset var lCategoryBranch = "" />
		<cfset var CategoryName = "" />
		<cfset var i = "" />
		<cfset var rootNodeText = "" />
		<cfset var rootID = "" />

		<cfif structKeyExists(application.catid, arguments.stMetadata.ftAlias)>
			<cfset rootID = application.catid[arguments.stMetadata.ftAlias] >
		<cfelse>
			<cfset rootID = application.catid['root'] >
		</cfif>

		<cfset lSelectedCategoryID = oCategory.getCategories(objectid=arguments.stObject.ObjectID,bReturnCategoryIDs=true,alias=arguments.stMetadata.ftAlias) />

		<cfset rootNodeText = oCategory.getCategoryNamebyID(categoryid=rootID) />

		<cfswitch expression="#arguments.stMetadata.ftRenderType#">

			<cfcase value="dropdown">
				<cfset lCategoryBranch = oCategory.getCategoryBranchAsList(lCategoryIDs=rootID) />

				<cfsavecontent variable="html">
					<cfoutput><select id="#arguments.fieldname#" name="#arguments.fieldname#"  <cfif arguments.stMetadata.ftSelectMultiple>size="#arguments.stMetadata.ftSelectSize#" multiple="true"</cfif> class="selectInput #arguments.stMetadata.ftSelectSize# #arguments.stMetadata.ftClass#"></cfoutput>
					<cfloop list="#lCategoryBranch#" index="i">
						<!--- If the item is the actual alias requested then it is not selectable. --->
						<cfif i EQ rootID>
							<cfif len(arguments.stMetadata.ftDropdownFirstItem)>
								<cfoutput><option value="">#arguments.stMetadata.ftDropdownFirstItem#</option></cfoutput>
							<cfelse>
								<cfset CategoryName = oCategory.getCategoryNamebyID(categoryid=i,typename='dmCategory') />
								<cfif arguments.stMetadata.ftAliasAsOptGroup EQ true>
									<cfoutput><optgroup label="#CategoryName#"></cfoutput>
								<cfelse>
									<cfoutput><option value="">#CategoryName#</option></cfoutput>
								</cfif>
							</cfif>

						<cfelse>
							<cfset CategoryName = oCategory.getCategoryNamebyID(categoryid=i,typename='dmCategory') />
							<cfoutput><option value="#i#"<cfif listContainsNoCase(lSelectedCategoryID, i)> selected="selected"</cfif>>#CategoryName#</option></cfoutput>
						</cfif>

					</cfloop>
					<cfif arguments.stMetadata.ftAliasAsOptGroup EQ true>
						<cfoutput></optgroup></cfoutput>
					</cfif>
					<cfoutput></select></cfoutput>
				</cfsavecontent>
			</cfcase>

			<cfcase value="prototype">
				<cfsavecontent variable="html">

					<cfoutput><fieldset style="width: 300px;">
						<cfif len(arguments.stMetadata.ftLegend)><legend>#arguments.stMetadata.ftLegend#</legend></cfif>

						<div class="fieldsection optional full">

							<div class="fieldwrap">
							</cfoutput>

								<ft:NTMPrototypeTree id="#arguments.fieldname#" navid="#rootID#" depth="99" bIncludeHome=1 lSelectedItems="#lSelectedCategoryID#" bSelectMultiple="#arguments.stMetadata.ftSelectMultiple#">

							<cfoutput>
							</div>

							<br class="fieldsectionbreak" />
						</div>
						<input type="hidden" id="#arguments.fieldname#" name="#arguments.fieldname#" value="" />
					</fieldset></cfoutput>

				</cfsavecontent>
			</cfcase>
			<cfcase value="extjs">
				<!--- <skin:htmlHead library="extjs" />
				<skin:htmlHead library="farcryForm" /> --->

				<cfsavecontent variable="html">

					<cfoutput><fieldset style="width: 300px;">
						<cfif len(arguments.stMetadata.ftLegend)><legend>#arguments.stMetadata.ftLegend#</legend></cfif>

						<!--- <div id="tree-div" style="border:1px solid #c3daf9;"></div> --->
						<div class="fieldsection optional full">

							<div class="fieldwrap">

								<div id="#arguments.fieldname#-tree-div"></div>	

							</div>

							<br class="fieldsectionbreak" />
						</div>
						<input type="hidden" id="#arguments.fieldname#" name="#arguments.fieldname#" value="#lSelectedCategoryID#" />
						<input type="hidden" name="#arguments.fieldname#" value="" />
					</fieldset>
					</cfoutput>

				</cfsavecontent>

				<skin:onReady>
				<cfoutput>
				    createFormtoolTree('#arguments.fieldname#','#rootID#', '#application.url.webtop#/facade/getCategoryNodes.cfm', '#rootNodeText#','#lSelectedCategoryID#', 'categoryIconCls');
				</cfoutput>
				</skin:onReady>
			</cfcase>
			<cfcase value="jquery">

				<skin:onReady>
				<cfoutput>
					$j("###arguments.fieldname#_list").treeview({
						url: "#application.url.webtop#/facade/getCategoryNodes.cfm?node=#rootID#&fieldname=#arguments.fieldname#&multiple=#arguments.stMetadata.ftSelectMultiple#&lSelectedItems=#lSelectedCategoryID#"
					})
				</cfoutput>
				</skin:onReady>

				<cfsavecontent variable="html">

					<cfoutput>
					<div class="multiField">
						<ul id="#arguments.fieldname#_list" class="treeview"></ul>
					</div>
					<input type="hidden" name="#arguments.fieldname#" value="" />
					</cfoutput>

				</cfsavecontent>
			</cfcase>

			<cfdefaultcase>

				<skin:loadJS id="jquery" />
				<skin:loadJS id="jquery-checkboxtree" basehref="#application.url.webtop#/thirdparty/checkboxtree/js" lFiles="jquery.checkboxtree.js" />
				<skin:loadCSS id="jquery-checkboxtree" basehref="#application.url.webtop#/thirdparty/checkboxtree/css" lFiles="checkboxtree.css" />

				<skin:onReady>
				<cfoutput>
					$j.ajax({
					   type: "POST",
					   url: '#application.fapi.getLink(type="dmCategory", objectid="#rootID#", view="displayCheckboxTree", urlParameters="ajaxmode=1")#',
					   data: {
					   	fieldname: '#arguments.fieldname#',
					   	rootNodeID:'#rootID#',
					   	selectedObjectIDs: '#lSelectedCategoryID#'
						},
					   cache: false,
					   success: function(msg){
					   		$j("###arguments.fieldname#-checkboxDiv").html(msg);
							$j("###arguments.fieldname#-checkboxTree").checkboxTree({
									collapsedarrow: "#application.url.webtop#/thirdparty/checkboxtree/images/checkboxtree/img-arrow-collapsed.gif",
									expandedarrow: "#application.url.webtop#/thirdparty/checkboxtree/images/checkboxtree/img-arrow-expanded.gif",
									blankarrow: "#application.url.webtop#/thirdparty/checkboxtree/images/checkboxtree/img-arrow-blank.gif",
									checkchildren: false,
									checkparents: false
							});
							$j("###arguments.fieldname#-checkboxDiv input:checked").addClass('mjb');
							$j("###arguments.fieldname#-checkboxDiv input:checked").parent().addClass('mjb');
					   }
					 });

				</cfoutput>
				</skin:onReady>

				<cfsavecontent variable="html">

					<cfoutput>
					<div class="multiField">
						<div id="#arguments.fieldname#-checkboxDiv">loading...</div>
						<input type="hidden" name="#arguments.fieldname#" value="" />
					</div>
					</cfoutput>

				</cfsavecontent>
			</cfdefaultcase>

		</cfswitch>

		<cfreturn html>
	</cffunction>

</cfcomponent>
No Comments

FarCry Tip – remove install directory

After you install FarCry you must make sure to delete the “install” folder(s) that FarCry leaves behind. Otherwise it’s possible for anyone to access the installer wizard again from your production website, and as it’s the installer they don’t even need to be logged in to cause real havoc. Not good.

By default, as of FarCry 6.1.x, there are 2 install folders to delete:

  • farcry.core.webtop.install
  • farcry.plugins.farcrycms.install
No Comments

Mura – overriding dspCrumblistLinks

Like most CMS solutions out there, Mura CMS gives you some cool hooks to override core functionality. See the developer guides for more information.

I’ve created a repository on github to store any customisations or extensions that could be cherry picked for use in any Mura app.

First up was a super simple one for breadcrumbs. By default, Mura will put an “a href” around the last item in the crumb list. The site I was working on specified that the last item should be plain text (no hyperlink).

Mura makes this nice and easy by extending the contentRenderer.cfc inside your /[siteid]/includes folder. Here is the function which includes my custom argument linkLastItem [bool] to determine whether or not to link the last item:

<cffunction name="dspCrumblistLinks"  output="false" returntype="string">
	<cfargument name="id" type="string" default="crumblist">
	<cfargument name="separator" type="string" default="">
	<cfargument name="linkLastItem" type="boolean" required="false" default="true" hint="Whether to generate an 'a href' for the last crumb item">

	<cfset var thenav="" />
	<cfset var theOffset=arrayLen(this.crumbdata)- this.navOffSet />
	<cfset var I = 0 />

	<cfif arrayLen(this.crumbdata) gt (1 + this.navOffSet)>
		<cfsavecontent variable="theNav">
			<cfoutput><ul id="#arguments.id#">
				<cfloop from="#theOffset#" to="1" index="I" step="-1">
					<cfif I neq 1>
						<li class="#iif(I eq theOffset,de('first'),de(''))#">
						<cfif i neq theOffset>#arguments.separator#</cfif>
						#addlink(this.crumbdata[I].type, this.crumbdata[I].filename, this.crumbdata[I].menutitle, '_self', '', this.crumbdata[I].contentid, this.crumbdata[I].siteid, '', application.configBean.getContext(), application.configBean.getStub(), application.configBean.getIndexFile(), event.getValue('showMeta'),0)#</li>
					<cfelse>
						<li class="#iif(arraylen(this.crumbdata),de('last'),de('first'))#">
							<cfif arguments.linkLastItem>
								#arguments.separator##addlink(this.crumbdata[1].type, this.crumbdata[1].filename, this.crumbdata[1].menutitle, '_self', '', this.crumbdata[1].contentid, this.crumbdata[1].siteid, '', application.configBean.getContext(), application.configBean.getStub(), application.configBean.getIndexFile(),event.getValue('showMeta'),0)#
							<cfelse>
								#arguments.separator##this.crumbdata[1].menutitle#
							</cfif>
						</li>
					</cfif>
				</cfloop>
			</ul></cfoutput>
		</cfsavecontent>
	</cfif>

	<cfreturn trim(theNav)>
</cffunction>
No Comments

FarCry to Mura – migrating data from FarCry to Mura CMS

For years here at Learnosity we’ve been using FarCry for most of our CMS requirements (yes, I know that FarCry, or rather farcrycms, is a lot more than just a CMS).

For one reason or another we decided it was time to look at some of the other options for content management in the ColdFusion sphere, Mura CMS was a likely candidate for evalutation.

We had a small-ish site that was already running in FarCry 6+ which we wanted to port to Mura so we could really get a feel for how things worked in Mura-land. If things go well we may decide to move more sites across, so I wanted a way to migrate existing content from a FarCry database to a Mura one. A quick search for something to do this came up blank so I spent half a day coming up with a little tool to do this. Here it is in case anyone else might find it useful.

You can find the project on github (https://github.com/michaelsharman/farcrytomura), below is the “help” page I added:

Introduction

This script imports basic navigation and HTML data from an existing FarCry database into a Mura one.

Requirements

This was built on

  • Railo 3.2+
  • ColdFusion 7+
  • FarCry 6+
  • Mura 5.4+
  • MySQL 5+

Not sure about FarCry 5, you should be ok as I don’t think the v6 schema changes will effect this script

To make things easier, session management must enabled

Installation

Probably easiest to put this folder (farcrytomura) in the webroot of an existing project/vhost,
then call it from http://www.yourproject.com/farcrytomura/

If you want, you can setup a project/vhost specifically for this, but it’s not necessary.

This script has its own Application.cfc to avoid scope conflict

Caveat – the ColdFusion/Railo server (instance/context) that you run this from needs to have BOTH dsn’s (the FarCry and Mura ones) in the cf admin

FarCry content types

Currently we examine the FarCry navigation tree and bring across the entire structure underneath ‘Root’ (i.e. any nLevel 2+ nodes)

If people want it, we can extend the functionality to import from a specific node, e.g. from ‘Home’. This would be handy
if your FarCry site has secondary/utility navigation etc that you want imported separately.

Although all navigation nodes will be created in Mura, the only actual content that is imported is dmHTML, we ignore:

  • dmInclude
  • dmLink
  • Anything but the first content object under a navigation node
  • dmNews, dmEvent or any other “dynamic” content type
  • Custom tree content types

Basically we import all navigation nodes under the site tab, from “Root” down, including the HTML content.

FarCry homepage content

Currently we ignore the farcry homepage content because we assume there is at least a “home” page in Mura. This is a TODO

SES URLs

Mura ses url’s are created on import based off the title of the page from FarCry. This should be the default behaviour as
if you were created a page from within Mura admin.

Currently we’re NOT importing the FarCry friendly URL into Mura.

Rolling back

If you want to rollback the import, you’ll need to:

DELETE FROM tcontent WHERE siteID = '[yoursite]' AND lastUpdateBy = 'farcrytomura'

TODOs

  • Ability to select the page template from Mura
  • When previewing the Mura navigation tree, indent properly
  • Import the FarCry “home” page content
  • Handle farcry secondary/utility nav’s (anything else at the same level as “home”)
  • Fix sortorder on sub items, they work fine but not as neat as they could be






3 Comments

WordPress – detecting the categories of a post

I was creating a WordPress child theme (the best way to modify/override your themes) tonight and had the need to have different display options depending on which categories the blog article had. So for example if I had a post categorised with “portfolio”, I wanted to hide the comments form which usually sits at the bottom of a post.

The following snippet will loop over all post categories, if it finds that “portfolio” is one of the categories assigned to the current post then a local variable (boolean) is set which you can use to hide WordPress functions anywhere in the current template.

<?php
	$isPortfolio = false;

	$categories = get_the_category();

	foreach($categories as $cat)
	{
		if ($cat->cat_name == 'portfolio')
		{
			$isPortfolio = true;
			break;
		}
	}
?>

These changes were at the top of my child theme’s single.php template. Not displaying the comments form was as simple as:

<?php if (!$isPortfolio){comments_template('', true);} ?>

Update 12th May 2011
Thanks to Martin in the comments below for alerting me to the fact that WordPress already has something for this, has_category($category, $post);

<?php $isPortfolio = has_category('portfolio'); ?>

Note I didn’t need the 2nd argument as it defaults to the current post.

2 Comments