Stage

For a long time at my day job, one of our big web site issues has been the staging of database-driven content. Particularly if you’re editing Drupal pages that have a lot of markup in them, publishing a node can be sort of scary, as it goes live instantly with any bugs you’ve introduced. In theory, Drupal’s preview feature can be used to view your changes before you commit to them, but this too is scary, as the content isn’t rendered exactly as it will be once published. Further, using vanilla Drupal with its preview function to stage content requires that you roll out changes one by one. If you want to group changes for a mass rollout, the best you can do is wrap your changes in html comments and uncomment them one by one during deployment, hoping you don’t fat-finger anything in the process. I’ve always thought this would be a pretty difficult problem to solve, but yesterday, I came up with what feels like a satisfactory method for staging content.

The new stage module addresses both safety-netted staging of individual content and management of change sets.

It works by tapping into Drupal’s revision system, which already allows you to track changes to content over time and to revert to older content. For specified types of content, any additions or edits are published using the normal Drupal workflow, but on publish, the revision number is pinned at its last blessed point. You can edit or add any number of documents, and they all remain pinned at their pre-edit revision until you roll the whole batch of changes forward. When you roll a batch forward, all the revision numbers are brought to their most recent and pinned there until the next deployment. In the administration section, you identify staging and production servers. If you view an affected node from one of the specified staging hosts, you see the latest copy; if you view it from a production host, you see the pinned version.

This workflow is ideal for environments in which fairly frequent milestones are deployed. Because of Drupal’s handy dandy revision system, you can compare versions of the content across pushes to see what’s changed.

The module is hot off the presses this morning and so is probably still buggy and feature-poor, but it’s a start.

Flexible Drupal surveys

A few weeks ago, my company needed to publish a survey with a pretty flexible layout. Had we been constrained to the one-field-per-row constraint that Drupal’s survey module allows for, we would have had a very long an ugly form when in fact what we wanted was a nice tidy grid of small form controls that was much less imposing for users to consider filling out. So I hacked our old version of the survey module (for Drupal 4.6) to add a “layout” field to the fields tab for a survey. In the layout field, those who can administer surveys can specify markup and drop form fields into the markup using numeric placeholders wrapped in curly braces. Surveys for which no layout is defined use the default layout with numeric field weighting. I’ve submitted a patch to incorporate this functionality, and you can find the bug report with attached patch here if you’re in need of such functionality before the patch gets review or if the survey module developer declines to integrate it. The patch is for Drupal 5.x.

On a related note, I created a patch for the forms module that lets you add HTML between form elements. This hack/patch arose out of a need to stick a quick text snippet between two fields, where the snippet couldn’t be contained gracefully in the “description” line of the topmost of the fields. I add a form field type “html” that spits out whatever HTML you specify. I wrote this code (or the old version of it; this patch too is for Drupal 5.x) when my company needed explanatory HTML between two fields but the need to manipulate the form fields themselves hadn’t arisen yet. This functionality is good for insertion of quick snippets of HTML, where the first patch I mention above is best for layout overhauls.

Both patches probably represent security risks on sites that allow non-administrators to create forms or surveys, as I’m not (as yet) filtering content, so anybody who can create a survey can add arbitrary HTML. So apply the patch with that in mind.

Styling Drupal 5.x search forms

I’m working on a project that requires me to apply a fancy pants style to a Drupal search form. I thought this would be simple enough, as it’s pretty easy to override default themes for pretty much everything else in Drupal, but it turns out either that I’m a dolt or that there’s not much clarity out there on this topic. After screwing around with a lot of things (e.g. poring over debug_backtrace() output, writing die() statements all over the place, temporarily hacking the search and node modules, etc.), I searched Drupal’s site and found a promising link that turned out not to be the solution I needed (it simply didn’t work).

At long last, I tried creating a theme function named mytheme_search_form(). The search module has a function named search_form(), but in all my hair-pulling, I never saw anything that indicated that you could override this function by prepending your theme name to it (I would have expected to find calls to “theme(‘search_form’, $args)” somewhere). At any rate, I ultimately created the above-named function and gave it the following definition:

function mytheme_search_form($form){
        return _phptemplate_callback('search_theme_form', array('form' => $form));
}

Then I created a file in my template directory named “search_theme_form.tpl.php” and built a custom form.

Next up was adding additional search fields to the form. To do this, I looked at node.module, where the function node_form_alter() adds fields to the default form. It didn’t seem necessary to jump through that hoop since my form was mostly hard-coded anyway, so I just added some radio buttons with the name “category” so that I could filter search results by taxonomy. Simple enough. But the search never actually filtered on my results. Here I did more hair-pulling and weird debugging. Finally, I went back to the hook_form_alter() functionality, having noticed a “processed_keys” key in the $form array. So to make my form honor my category search, I added the following things to my template.php file. It’s not clear to me how much of this is necessary, and I rather suspect I’m doing something stupid here, but it seems to work and I’m on deadline, so I’m rolling with it.

function mytheme_search_keys($type = null){
        $keys = search_get_keys();
        if($type){
                $keys = search_query_insert($keys, 'type', $type);
        }
        if($_POST['category']){
                $keys = search_query_insert($keys, 'category', $_POST['category']);
        }
        return $keys;
}

/**
 * Taking over this function so that I can call mytheme_search_validate to do the advanced search.
 */
function blog_form_alter($form_id, &$form){
        if($form_id == 'search_form'){
                $form['#validate']['mytheme_search_validate'] = array();
        }
}

/**
 * Need to call this to add category to the processed_keys array item so that
 * the category actually gets searched in the mini-advanced form we generate
 * in mytheme_search_form().
 */
function mytheme_search_validate($form_id, $form_values, &$form){
        $keys = $form_values['processed_keys'];
        $keys = mytheme_search_keys($form_values['module']);
        form_set_value($form['basic']['inline']['processed_keys'], $keys);
}

function faq_search($op = 'search', $keys){
        switch($op){
                case 'name':
                        return t('content');
                default:
                        $keys = mytheme_search_keys('faq');
                        return node_search('search', $keys);
        }
}

function forum_search($op = 'search', $keys){
        switch($op){
                case 'name':
                        return t('content');
                default:
                        $keys = mytheme_search_keys('forum');
                        return node_search('search', $keys);
        }
}

function blog_search($op = 'search', $keys){
        switch($op){
                case 'name':
                        return t('content');
                default:
                        $keys = mytheme_search_keys('blog');
                        return node_search('search', $keys);
        }
}

Now for an explanation. The “mytheme_search_keys()” function is a helper that lets me make sure I’m limiting the search to a given node type. It’s not clear to me that this is absolutely necessary, but things seem not to work if I don’t add the “type:” string to the search, so I’m leaving it in. Note that this function also looks for $_POST['category'] and adds it to the keys. If I wanted to add other search fields, I’d add them here as well. I suppose that since mytheme_search_validate() calls this function to set keys, I could eliminate the extra function and just do the same work in mytheme_search_validate().

Next up, blog_form_alter(). The hook_form_alter() functions are associated with modules, so I chose one I knew my site would be using that didn’t have a form_alter hook defined already. It feels kind of hacky, but it seems to work. The idea here is that we need to make the form run a validation function in order to add the keys we’re pulling in from mytheme_search_keys(). It was when I added this code that the category filter actually started working, so it seems to be a crucial bit. The key seems to be adding the keys to the $form['basic']['inline']['processed_keys'] array item, which seems to handle adding the search criteria to the URL and to the search itself.

Finally, I added the three _search() functions, which again feel a little extraneous, but the thing doesn’t work unless I add them, so they’re staying put for now. All we’re doing in these functions is adding the node type to the keys being searched (the search code extracts things like “type:blog” and “category:3″ from the query to do advanced searches) and then executing the node_search() function with the revised $keys value.

So, there you have it, the long way around to having a custom-themed Drupal search form with additional filters based on node type and category. Hope this saves somebody a few of the 10 or so hours I spent staring incredulously at my screen as solution after solution failed to work.