Words about stuff, and things related to stuff

Remove empty paragraphs (and Mura editable regions) with CSS

When logged into a Mura site and looking at a front end page, the 'editable region' for the page body often contains an empty paragraph tag created by the wysiwyg, even if there isn't any content in the body, and can take on an unwanted space on the page. This is a common CMS/wysiwyg thing in general, and an annoying waste of time if you go chasing it down each time it happens.

The fix is simple - put this in your default stylesheet, and those empty

elements go away:

view plain print about
1p:empty {
2 display: none;
3}

Download CFQuery results as Excel with CFspreadsheet

Goal: convert a ColdFusion query object into a spreadsheet, save the spreadsheet to the server, and download it in the browser (without the annoying Excel warning about the wrong file extension):

Code:

view plain print about
1<cfparam name="session.searchQuery" default="#queryNew('')#">
2    <cfset fileDir = expandPath('.' & '/searchdata/')>
3    <cfif not directoryExists(fileDir)>
4        <cfdirectory action="create" directory="#fileDir#">
5    </cfif>
6
7 <cfset filename = dateFormat(now(),'yyyy-mm-dd') & '-' & timeFormat(now(),'hh-mm-ss') & '.xls'>
8
9    <cfset fileLocation = fileDir & filename>        
10    <cfspreadsheet action="write" filename="#fileLocation#" query="session.searchquery">
11
12    <cfheader name="Content-Disposition" value="attachment;filename=#filename#">
13    <cfcontent type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" file="#fileLocation#">

When I used content type of mxexcel, or x-msexcel (or a few others I tried), i would get a warning from excel that the format specified did not match the extension.

This method, cobbled together from a few different forum threads and blog posts, seems to get around those issues.

Add index.cfm as default doucment in .htaccess

Simple tip: Add this line to your .htaccess file

view plain print about
1DirectoryIndex index.cfm

This tells the server to default to /index.cfm inside of any directory.

Why I needed it: I just installed Mura CMS on a Railo/cPanel server where index.cfm is not set as a global default document, and the mura admin kept redirecting me back to the home page. I just added that line to the .htaccess in the web root, and we're good to go.

Bootstrap Screen Size indicator

Working with Bootstrap's default system of 4 screen size ranges, I wanted a way to know, quickly and visually, which size BS is reporting on each page view.

Here's a quick snippet which will create 4 divs, each visible only to the Bootstrap screen size currently in use.

view plain print about
1<div>CURRENT VIEW SIZE:
2        <cfloop list="xs,sm,md,lg" index="i">
3            <span class="visible-#i#">#i#</span>
4        </cfloop>
5    </div>

As you resize your browser window, the value changes. Simple.

Load Mura content based on Extended Attribute value in URL

I'm working on a site where i want to capture part of the URL and pull up a Mura content item (class listing) based on a course ID number, which is stored as an extended attribute.

The goal is to do this without having an actual page in place for each class. (For this site, we also want to do this without the need for a class ID passed in the querystring. Instead, we want to use the Mura URL path, e.g. mysite.com/class/123456 instead of mysite.com/class?classid=123456 )

The overall solution was multiple parts.

Replacing the Content Bean

First, I discovered you can override the entire content bean (the whole ball o' wax in terms of what mura shows on a page, the HTML title, meta info, the works), by setting the value of "request.contentbean" to a valid Mura content object.

So, where in a normal Mura template you can show the values of the current content item structure using this notation:

view plain print about
1$.content('title')
2$.content('filename')
3etc

You can actually override the entire $.content() object, by overriding request.contentbean, like this:

view plain print about
1<cfset request.customid = 'Mura-Content-ID-Here'>
2<cfset request.contentbean = $.getBean('content').loadBy(siteid=request.siteid,contentid=request.customid)>

There are several ways to load the content bean. Google it. But the idea is, once you have a content bean loaded, you can set 'request.contentbean' and override whatever content mura would otherwise show on that request. So, this is good. If I can figure out which content to show, and get its ID (or another value I can use inside of "loadBy()") , I can push my own content into the page request, regardless of the URL.

Parse the Path

So, how do I know which content to get? In this case I am using a url like this: sitename.com/class/123456 (worth noting, sometimes there's a trailing slash, e.g. sitename.com/class/123456/ )

My goal was to parse that string from the end of the URL, then get the Mura content that matches it.

view plain print about
1<!--- if /class/ is in the url --->
2<cfif request.path contains '/class/'>
3    <!--- if class id is provided --->
4    <cfif right(request.path,1) is '/'>
5        <cfset request.classpath = left(request.path,len(request.path)-1)>
6    <cfelse>
7        <cfset request.classpath = request.path>
8    </cfif>
9    <cfset request.classnumber = listLast(request.classpath,'/')>    
10</cfif>

So, now I have a class number, taken from the URL. Of course I'll want to use a cfparam if I'm going to depend on the value of request.classnumber, since it may not always exist... but that's the basic idea of how I'm getting it.

Load by Extended Attribute (via Feed)

Ok, so now I have a value. 123456. So what. How can I get a Mura content bean based on that value? If it were the contentID or even the remoteID, I could just use loadby (see first example above) and load it up. But this is an Extended Attribute. not part of the standard LoadBy() logic (as far as i know).

So... custom stuff. A while ago, I created a wrapper function for Mura feeds, to work with a custom search interface. It has been useful many times since then and this is one of them.

view plain print about
1<cfset contentQuery = GWcontent.GWcontentSearch(
2    siteid=request.siteid,
3    type="Page",
4    parentid="B98EA962-155D-8F07-1D7F47530491001B",
5    extsearch="ClassNumber=#request.classnumber#"
6    )
>
    
7<cfif contentQuery.recordCount>            
8 <cfset request.customid = contentQuery.contentid[1]>
9 <cfset request.contentbean = $.getBean('content').loadBy(siteid=request.siteid,contentid=request.customid)>
10</cfif>

What this does: load up a Mura feed with the given siteid(required), page type (optional), parent id (optional - used here since I know the ID of the parent for all of these "class" entries), and the kicker extsearch (a custom wrapper that adds feed params to the Mura lookup)

In my case, since the CourseCode is unique, it will either return one item , or none.

So then, I can treat the mura feed like any query object, and latch on to the value of the 'contentid', which is used to load my bean, which is then used to override the current request's $.content() object.

Custom Functions

Wrapping this all together, I put it inside a function called "onSiteRequestStart", which is inside of my theme's eventHandler.cfc (it should also work in the site eventhandler.cfc).

view plain print about
1<!--- CUSTOM SITE REQUEST START --->
2<cffunction name="onSiteRequestStart" output="false">
3    <cfargument name="$" hint="mura scope" />
4        
5    <!--- CUSTOM handle class URLs --->
6    <cfparam name="request.path" default="">
7    <cfparam name="request.classpath" default="">
8    <cfparam name="request.classnumber" default="">
9    
10    <!--- if /class/ is in the url --->
11    <cfif request.path contains '/class/'>
12        <!--- if class id is provided --->
13        <cfif right(request.path,1) is '/'>
14            <cfset request.classpath = left(request.path,len(request.path)-1)>
15        <cfelse>
16            <cfset request.classpath = request.path>
17        </cfif>
18        <cfset request.classnumber = listLast(request.classpath,'/')>    
19    </cfif>     
20    <!--- attempt to set content bean --->
21    <cfif len(trim(request.classnumber))>        
22
23        <!--- get content functions --->
24        <cfset GWcontent = createObject('component','interactive.GWcontent')>
25        <!--- load a feed of the current class --->    
26        <cfset contentQuery = GWcontent.GWcontentSearch(
27            siteid=request.siteid,
28            type="Page",
29            parentid="B98EA962-155D-8F07-1D7F47530491001B",
30            extsearch="classCode=#request.classnumber#"
31            )
>
        
32
33    <cfif contentQuery.recordCount>            
34        <cfset request.contentbean = $.getBean('content').loadBy(siteid=request.siteid,contentid=contentQuery.contentid[1])>
35    </cfif>
36
37    </cfif>
38    <!--- /end class URLs --->    
39
40</cffunction>

And here's the current version of my GWcontentSearch() method, which is currently in a separate CFC (see the 'interactive' directory referenced in the example above), but can also be added to the eventHandler.cfc in Mura in which case the createObject reference above can be remoeved.

Custom Mura Content Search Function:

view plain print about
1<!--- // ---------- // GWcontentSearch // ---------- // --->
2<cffunction name="GWcontentSearch"
3            access="public"
4            output="false"
5            returntype="query"
6            hint="returns feed based on custom content"
7            >

8
9    <cfargument name="siteid"
10            required="true"
11            type="string"
12            hint="site id">

13            
14    <cfargument name="type"
15            required="false"
16            default="page"
17            type="string"
18            hint="a Mura content type to limit content to (page|folder|portal|etc)">
        
19            
20    <cfargument name="subtype"
21            required="false"
22            default=""
23            type="string"
24            hint="a Mura subtype to limit content to">
        
25            
26    <cfargument name="parentid"
27            required="false"
28            default=""
29            type="string"
30            hint="id of a content node that contains the item">

31            
32    <cfargument name="extsearch"
33            required="false"
34            default=""
35            type="string"
36            hint="list of name/value pairs to search by
37                    use ^ delimiter
38                 can use = for equals, ~ for LIKE
39                    eg attr1=blue^attr2~xl
40                    "
>

41            
42    <cfargument name="maxrows"
43            required="false"
44            default="0"
45            type="numeric"
46            hint="max number of items to return">

47        
48    <cfargument name="sortby"
49            required="false"
50            default=""
51            type="string"
52            hint="column to sort feed by">

53
54    <cfargument name="sortdir"
55            required="false"
56            default="asc"
57            type="string"
58            hint="sort direction">

59
60    <cfset var feed = ''>
61    <cfset var feedContents = ''>
62    <cfset var afield = ''>
63    <cfset var acrit = ''>
64
65    <!--- load feed --->
66    <cfset feed=application.feedManager.read('')>
67    <!--- set site id --->
68    <cfset feed.setSiteID(arguments.siteid)>
69    <!--- type --->
70    <cfif len(trim(arguments.type))>
71        <cfset feed.addParam(relationship="AND", field="type", condition="=", criteria=arguments.type, dataType="varchar")>
72        <cfset feed.addParam(relationship="AND", field="type", condition="=", criteria=arguments.type, dataType="varchar")>
73    </cfif>
74    <!--- subtype --->
75    <cfif len(trim(arguments.subtype))>
76        <cfset feed.addParam(relationship="AND", field="subType", condition="=", criteria=arguments.subtype, dataType="varchar")>
77    </cfif>
78    <!--- parent --->
79    <cfif len(trim(arguments.parentid))>
80        <cfset feed.addParam(relationship="AND", field="parentid", condition="=", criteria=arguments.parentid, dataType="varchar")>
81    </cfif>
82    <!--- extended attributes --->
83    <cfif len(trim(arguments.extsearch))>    
84        
85        <cfloop list="#trim(arguments.extsearch)#" index="a">
86            <!--- equals (=) --->
87            <cfif a contains '='>
88                <cfset afield = listFirst(a,'=')>
89                <cfset acrit = listLast(a,'=')>
90                <cfset feed.addParam(relationship="AND", field=afield, condition="=", criteria=acrit, dataType="varchar")>
91            <!--- like (~) --->
92            <cfelseif a contains '~'>
93                <cfset afield = listFirst(a,'~')>
94                <cfset acrit = listLast(a,'~')>
95                <cfset feed.addParam(relationship="AND", field=afield, condition="LIKE", criteria='%' & acrit & '%', dataType="varchar")>
96            </cfif>
97        </cfloop>
98    </cfif>
99    <!--- sort --->
100    <cfif len(trim(arguments.sortby))>
101        <cfset feed.setSortBy(arguments.sortby)>
102        <cfset feed.setSortDirection(arguments.sortdir)>
103    </cfif>
104    <!--- max rows --->
105    <cfset feed.setMaxItems(val(arguments.maxrows))>
106    
107    <!--- get query of feedcontents --->
108    <cfset feedContents=application.feedManager.getFeed(feed) />
109
110    <cfreturn feedContents>
111
112</cffunction>

See the arguments and descriptions of same in the code. Like everything this is a work in progress, comments welcome.

That does it for how I am loading up custom mura page content based on an extended attribute in the url. If you need to get the extended values as part of your content, keep reading...

Getting Extended Attributes

Important: the mura feed does not return the Extended Attributes as query columns! For that I have another wrapper function, which takes either (a) all the same attributes as the above, and passes them through, or (b) a specific content ID. Then it returns a structure of all the content attributes as if you'd run .getAllValues(), but with a lot of options via the arguments, such as what you actually want returned.

It currently looks like this

view plain print about
1<!--- // ---------- // GWcontentAttr // ---------- // --->
2<cffunction name="GWcontentAttr"
3            access="public"
4            output="false"
5            returntype="struct"
6            hint="returns structure of content attributes"
7            >

8
9    <cfargument name="siteid"
10            required="true"
11            type="string"
12            hint="site id">

13            
14    <cfargument name="type"
15            required="false"
16            default="page"
17            type="string"
18            hint="a Mura content type to limit content to (page|folder|portal|etc)">
        
19            
20    <cfargument name="subtype"
21            required="false"
22            default=""
23            type="string"
24            hint="a Mura subtype to limit content to">
        
25            
26    <cfargument name="parent"
27            required="false"
28            default=""
29            type="string"
30            hint="name of a Mura content node that contains the item">
        
31            
32    <cfargument name="parentid"
33            required="false"
34            default=""
35            type="string"
36            hint="ID of a Mura content node that contains the item">
        
37
38    <cfargument name="extsearch"
39            required="false"
40            default=""
41            type="string"
42            hint="^ delimited list of name/value pairs to search by">

43    
44    <cfargument name="extendset"
45            required="false"
46            default=""
47            type="any"
48            hint="list of extended attribute sets to include">

49
50    <cfargument name="returnvals"
51            required="false"
52            default="all"
53            type="string"
54            hint="values to return, options are all|extended|'{list,of,specific,values (extended can be added here as well)}' ">

55
56    <cfargument name="contentid"
57            required="false"
58            default=""
59            type="string"
60            hint="if supplied, this specific item is looked up">

61    
62    <cfset var contentQuery = ''>    
63    <cfset var contentBean = ''>
64    <cfset var parid = ''>
65    <cfset var subt = ''>
66    <cfset var extAttrs = structNew()>
67    <cfset var extStruct = structNew()>
68    <cfset var allStruct = structNew()>
69    <cfset var returnStruct = structNew()>
70    
71    <!--- if content id is provided --->
72    <cfif len(trim(arguments.contentid))>
73        <cfset contentId = arguments.contentid>
74    <cfelse>
75        <!--- get content ID for search by parent --->
76        <cfif len(trim(arguments.parentid))>
77            <cfset parid = trim(arguments.parentid)>
78        <cfelseif len(trim(arguments.parent))>
79            <cfset parid = application.contentManager.getActiveContentByFilename(arguments.parent,arguments.siteid).getcontentId()>    
80        </cfif>
81            
82        <!--- get content ID based on search parameters --->
83        <cfset contentQuery = GWcontentSearch(    arguments.siteid,
84                                                arguments.type,
85                                                arguments.subtype,
86                                                parid,
87                                                arguments.extsearch,
88                                                1 )
>

89        <cfset contentId = contentQuery.contentId>
90    </cfif>
91    <!--- /end if content id provided --->
92
93    <!--- get content --->
94    <cfset contentBean = application.contentmanager.getBean('content').loadBy(siteid=arguments.siteid,contentId=contentId)>
95
96    <!--- set known values (used if bean returns no match / blank) --->
97    <cfif len(trim(arguments.type))>
98        <cfset contentBean.setType(arguments.type)>
99    </cfif>
100    <cfif len(trim(arguments.subtype))>
101        <cfset contentBean.setSubtype(arguments.subtype)>
102    </cfif>
103    <cfif len(trim(parid))>
104        <cfset contentBean.setValue('parentid',parid)>
105    </cfif>
106    
107    <!--- get all values --->
108    <cfset allStruct = contentBean.getAllValues()>
109
110        <!--- get extended attributes --->
111        <cfif len(trim(arguments.extendset))>
112            <cfset subt = application.classExtensionManager.getSubTypeByName(arguments.type,arguments.subtype,arguments.siteid)>
113            <cfset extendSet = subt.getExtendSetByName(arguments.extendset)>
114            
115            <cfset extAttrs = extendSet.getAttributes()>
116            <cfif arrayLen(extAttrs)>
117                <cfloop from="1" to="#arrayLen(extAttrs)#" index="i">
118                    <!--- add extended values to values struct --->
119                    <cfset extStruct[extAttrs[i].getName()] = contentBean.getValue(extAttrs[i].getName())>
120                </cfloop>
121            </cfif>
122        </cfif>
123
124            <!--- return values depending on content requested --->
125            <cfswitch expression="#arguments.returnvals#">
126            
127                <!--- extended values only --->
128                <cfcase value="extended">    
129                    <cfset returnStruct = extStruct>
130                </cfcase>
131            
132                <!--- all values --->
133                <cfcase value="all">
134                    <cfset returnStruct = allStruct>
135                </cfcase>    
136            
137                <!--- if a list is specified --->
138                <cfdefaultcase>
139                    <cfif len(arguments.returnvals)>
140                        <cfloop list="#arguments.returnvals#" index="v">
141                            <cfif structKeyExists(allStruct,v)>
142                                <cfset returnStruct[v] = allStruct[v]>
143                            </cfif>
144                        </cfloop>
145                    <cfif listFindNoCase(arguments.returnvals,'extended')>
146                        <cfset structAppend(returnStruct,extStruct)>
147                    </cfif>
148                    </cfif>
149                    
150                </cfdefaultcase>
151            
152            </cfswitch>
153            <!--- /end return values --->
154
155    <cfreturn returnStruct>
156
157</cffunction>

MySQL order by in() list

Here's a simple but powerful MySQL thing. Sometimes when selecting records using a list of values in an IN() statement, you also want to order by that list. (If you've ever tried this, you know that the matches found via IN() are not returned in the same order as the values from you IN() list).

There are a lot of complex ways you could do this, but here's the simplest way I know, ordering the query results by the same list.

view plain print about
1<cfquery datasource="#request.dsn#" name="getstuff">
2 SELECT m.id, m.title
3 FROM mytable m
4 WHERE m.id IN(#request.getids#)
5 GROUP BY m.id
6 ORDER BY m.id IN(#request.getids#)
7 </cfquery>

The trick is the ORDER BY, using the same IN() statement as the IN() from the where clause.

That should do it.

Adobe Edge Inspect mobile device preview

As part of a google groups thread about putting a Bootstrap skin on this blog, and a question about testing mobile devices, I found this useful little link on Charlie Arehart's list of Page Appearance Testing Tools:

 Adobe Edge Inspect, free, allows devs "to preview their content across multiple mobile devices. Wirelessly pair multiple iOS and Android devices to your computer, grab screenshots from any connected device, and see real-time results from changes to HTML, CSS, and JavaScript." 

Continued...

PayPal does not want you to change your encoding

Apparently there's a conspiracy among PayPal's website developers to keep the average human from changing character set encoding preferences. If you have product names with special characters, chances are you'll want to use UTF-8 encoding. And though you can set your page's charset, and pass a special setting into your transaction to specify encoding...

While the PayPal help files, numerous blog posts and other Google results show a series of selections something like this

Login to your PayPal account
Go to the "My Account Overview -> Profile -> More options" section.
In column "Selling Preferences" click the "Language Encoding" link.
Ensure that setting for "Your website's language" is correct.
Click the "More Options" button, select the "UTF-8" option for "Encoding" and leave the "Yes" option selected for "Do you want to use the same encoding for data sent from PayPal to you (e.g., IPN, downloadable logs, emails)?".
Click "Save" to save your changes.

but... no. The entire PayPal UI has changed, and the option you're trying to change is nowhere in the complex menu.

Instead, you'll want to go to Profile (that part is the same) and then go to the very bottom of the page. Forget all the options on the left, it would be far too easy to put a link there for Character Encoding. No... you have to find the link at the foot of the page that says "PayPal button language encoding". But, you don't have PayPal buttons... you are using IPN with your own eCommerce integration. Don't worry about it making sense, click the "PayPal button language encoding" link.

And there you are presented with a wealth of options.

Western European Languages (including English) Chinese (Traditional) Chinese (Simplified) Japanese Korean Russian

First of all, what does that have to do with ... nevermind. It isn't supposed to make sense. And then, those aren't web standard encoding options... nevermind. Just... nevermind. you're almost there.

Find the "more options" button (in between "save" and "cancel" as of this writing). And there... you will find the secret buried setting.

Use the following drop-down menu to select the encoding used on your website.
(here you can select encoding)
Do you want to use the same encoding for data sent from PayPal to you (e.g., IPN, downloadable logs, emails)?
(and here you can select different encoding if needed, for IPN)

By the time you read this they may bury it behind a few more levels of spaghetti-nav, but for today, that's where you can find the setting for IPN language encoding. If you set your page to UTF-8, and change this setting to UTF-8, you should be able to pass weird characters into your transactions.

php version of CFDUMP: dBug.php

Anybody who works in CFML knows about cfdump, an indispensable tool for debugging any otherwise hidden values in your ColdFusion logic.

In php, you can use print_r() to dump out an array, but the result is far from reader-friendly. Enter "dBug.php". Get it here: dBug.php project home (If you use it be sure to make at least a small donation to the developer)

Continued...

Quick jQuery email address obfuscator

Here's a quick little jQuery script I created just now. Don't give your email address to spam bots! Instead, try this.

Markup:

view plain print about
1<a href="/contact"><span class="no-script">Contact Us</span><span class="obfuscate">com/mydomain//me</span></a>
(Use your email address in the format above, where your original address is me@mydomain.com)

Continued...

File Locator Lite

As part of his presentation " CF911: Solving Frequent CF Server Problems in New/Better Ways ", Charlie Arehart showed a series of examples, searching CF log files with impressive speeds. He mentioned the tool that was being used, File Locator Lite and gave it a great review. Anything that is two thumbs up with @carehart is generally worth checking out.

Today I got tired of Notepad++ text search, which is great but seems to just quit working at random times, and just didn't feel like using Eclipse. So I downloaded and fired up File Locator and bam! it is just what I needed. The link above has examples and screenshots so I won't bother here, but all the options I could think of are visible at first glance, and it does what it says... locates files, fast and light.

Read More...

Recent Comments

Log in a Mura user programmatically with loginByUserID()
Michael Evangelista said: @Sebastiaan, assuming you have a user bean loaded at some point of your process, you can add them t... [More]

Log in a Mura user programmatically with loginByUserID()
Sebastiaan said: Hi Michael, Question: what if I have a site set behind a login and content is for member groups onl... [More]

Filezilla - open site manager or a specific site connection on startup
ggibby said: Took me some tinkering to discover that there should be NO quotes around "0/myftpsite" May... [More]

Feed Unavailable