Blog

Email confirmation of anonymous content through Rules and Flag

We regularly need to create workflows that enable anonymous users to add content to one of our Drupal sites. Often it is desirable to make anonymous users confirm the nodes they post by following a generated 'secret' link they have received by email.

Thanks to the indispensable Rules and Flag modules this can be accomplished without ever leaving the administration area of a Drupal site. Now don't misunderstand me. I love to code. But the Rules module often offers a faster and more easily maintainable way to create complex processes. It also keeps Drupal installs relatively clean, since Rules is able to sit in for a whole bunch of contrib modules.

But let's get back to work on the anonymous email confirmation process! I do assume some basic experience with Rules. For a first introduction I can do no better then refer you to the great series of Rules tutorials by Johan Falk at Node One.

My screencasts below are of a Drupal 6 install. I had to recreate the rules for D7 last week, follows the same logic as the D6 version *. An export of both D6 and D7 rules are attached. Both have been exported using a content type called "Complaint" (machine name "complaint") - different from the content type name seen in the screencasts.

A. Prerequisites

1. Download and enable the necessary modules

Download and enable the following modules:

- cck
- email (for the email field)
- flag
- token
- rules
- rules_forms (only necessary for D7)
- pathrules (only necessary for D6)

2. Create the Content Type you want anonymous users to submit

Create the relevant Content Type.

Add a regular text field to the Content Type named "random" (do not hide this field under the "Manage Display" tab, this would mess with the first rule we will create - I will hide the Random form field later on using a form rule).

Add an e-mail field called "email".

Set the permissions for the content type, anonymous users should be able to create this particular content type, the "random" field should be editable and viewable by anonymous users.

3. Add a new node Flag

Add a new Flag at the flag administration page. Make it of type "nodes", set it to "global" and assign it to the Content Type created in step 2.

B. Rules

It's as easy as one, two, three!

5. First rule: Send e-mail on submit IF anonymous user

The first rule checks if someone who tries to submit a the relevant content type is an anonymous visitor of the site. If that is the case, it will generate a random string (borrowing the Drupal function user_password()), add it to the "random" field, save the content type and mail the user (who entered an e-mail address in the form) a link that is a combination of the content type's URL and the generated string.

Relevant code :
[node:field_email-raw]
[node:node-url]/[node:field_random-formatted]/pblsh
return array(0 => array('value' =>  user_password()));

6. Second rule: Flag content as visible when correct URL from mail

The second rule checks if the value contained in the link received by the anonymous user conforms to the hidden saved value of the random field.

Relevant code:
[node:field_random-formatted]/pblsh

7. Third rule: If content NOT flagged hide for anonymous user

The third rule is simple: if an anonymous user tries to view content that is not flagged visible, redirect the user to the front page.

9. The proof of the pudding

If everything went well, an anonymous user should receive the following mail on submitting our specially prepared content type:

Click the link - and our previously hidden content is visible! It worked!

The fourth rule makes sure that content added by logged in users is automatically be made visible. The fifth hides the random entry field on the submit page of our content type. These last two rules are complementary, so I did not include screencasts for them. All rules for both D6 and D7 versions of Rules are attached below (remember: content type "Complaint", machine name "complaint").

* Though I had to remember to set the correct user for my flag rule to make it fire for anon users - the user "on whose behalf to flag" does not need to be the currently logged in or acting user in D7 Rules. You can specify any existing user that has sufficient rights to flag. Just switch to "direct data selection" and enter a uid of a user with sufficient rights to set a flag.

PreviewAttachmentSize
complaint_rules_export_d7.zip2.38 KB
complaint_rules_export_d6.zip2.25 KB

Easy embed

Although the following code (a video embed code to be used by visitors of a video site) is discussed in the light of a MediaMosa related site, the implementation can easily be generalized to other video solutions.
It has been a pleasure to work with the MediaMosa framework, a tried and tested Dutch open source software solution enabling you to easily build a full featured, webservice oriented media management and distribution platform. Sort of Drupal based YouTube in a box, including both client and server solutions.

The client site we developed is based on the MediaMosa CK module, which adds support for MediaMosa videos to the Embedded Media Field module.

Almost everything we needed worked out of the box. There was just one major video related feature in our spec that we had to cater for ourselves: the embedding of our video's in other sites. Because the MediaMosa server provides temporary tickets for the within-site embed code, any embed solution has to retrieve a new ticket (URI) from the remote site.

Taking a cue from YouTube we decided the most efficient way to do this might be to simply provide the video in an iFrame.

To do this, we needed to implement two features:

- a special URL, showing only the video
- the iFrame link, easily to be copied and pasted by a user

As usual there are several ways to implement this kind of functionality in Drupal. We could, for example, have created a simple module. Yet in this case we decided to make use of modules already installed.

Basic Video URL

For the first step, the special URL, we used the ThemeKey module. ThemeKey allows you to define theme-switching rules allowing automatic selection of a theme depending on, among others, query parameters. That way we can conditionally select an empty, clean theme only showing the video just by adding a specific query-parameter to the default URL.

In order to get this to work, first download and enable the ThemeKey (themekey, themekey_ui) module, and on D6, ThemeKey Properties (already incorporated in ThemeKey for D7). Next, create a blank theme, containing almost no CSS or markup (like this one, for example) and enable this blank theme as well.

Now go to admin/settings/themekey and add the following:

Adding "?embedded" to any URL will from now on serve the page using our blank theme.

To really only show the video, we do still have to remove all the other fields that are part of the mediamosa_videocontent content type. To do this, we can add a custom node-mediamosa_videocontent.tpl.php (don't forget to copy a default node.tpl.php to the theme dir as well to enable theming content types this way) to our blank theme directory, which only shows the relevant video-field:

print $node->field_mediamosa_videofile[0]["view"];

Don't forget to clear the cache after making changes in your tpl files, go to a video page, add "?embedded" and there you are: a clean, embeddable video.

The Embed Code

Step two is even more easy than step one: all we have to do is make an iFrame available, containing the embeddable link to the video we just created. Let's test this by hand-typing an iFrame in my blog (different from the MediaMosa site) first:

<iframe width="425" height="375" src="http://site.com/content/video_page?embedded" frameborder="0" allowfullscreen></iframe>

If width and height are correct, the video should appear perfectly:

Now all there is left to do is to automate the generation of the embed links, making it available for the visitors of the site. Since this field is just a variation upon the URL, it can be easily "computed" from values already available within this node. Clearly something that can be solved using the Computed Field module.

As usual, first download and enable the computed_field module. Now add a Computed CCK field to the Video node, and use the following for the "Computed Code" section:

$node_field[0]['value'] = '<textarea class="share-embed-code"> <iframe width="425" height="375" src="' .url("node/".$node->nid, array('query' => 'embedded','absolute' => TRUE)) . '" frameborder="0" allowfullscreen></iframe></textarea>' ;

After saving a video-node containing this computed field, a textarea with the embed code will appear, enabling any visitor to embed the video on any other site:

PreviewAttachmentSize
blank.zip7.94 KB

Parsing a taxonomy tree in Flash Actionscript 3

I often need to integrate Flash AS3 elements into Drupal projects. Something made very easy by the great AS3 Drupal Proxy and Drupal AMFServer module from the guys at DPDK. Of the structures that are returned by the default Services module, the taxonomy tree needs a little more processing than most.

In order to parse the taxonomy tree in my AS3 frontend code I use the recursive function shown below (here on its own, stripped a bit - by default part of a DrupalUtils helper class & returning a as3ds tree).

As an aside: because Actionscript 3.0 primitive types like ints are immutable (in effect passing-by-value), I wrap the variable that keeps track of the callee level in a separate Reference class (enabling passing-by-reference) to forgo global variables, a no-no in recursive functions.

package org.drupal.amfserver
{
    ...

    public class DrupalNewsServer extends MovieClip
    {

        ...

        public function DrupalNewsServer()
        {
            ...
            proxy = new DrupalProxy(endpoint,DrupalProxy.VERSION_7);
            proxy.setHandler("taxonomy_vocabulary", "getTree", onTreeResult, onStatus);
            sequence.add(new CallBackTask(proxy.setRemoteCallId, "taxonomy_vocabulary.getTree(2)"));
            sequence.add(new DrupalInvokeTask(proxy, "taxonomy_vocabulary", "getTree", 2));

            sequence.execute();
        }       
       
        private function onTreeResult(data : DrupalData):void
        {
            parseDrupalTree(data.getData());
        }       

        /********************/
        /* recursive parser */
        /********************/

        public function parseDrupalTree( obj : *, level : int = 0, parentTid: int = 0,
                                         main_level:Reference=null):void
        {
            if (!main_level)
            {
                main_level = new Reference();
                main_level.value = 0;
            }
           
            // tabs for testing by trace
            var tabs:String = "";
            for (var i : int = 0; i < level; i++)
            {
                tabs += "\t\t";
            }

            var level_array = new Array();
            for (var prop:String in obj)
            {
                if (obj[prop].depth == level
                    && obj[prop].parents.indexOf(String(parentTid)) != -1
                    && level_array.indexOf(obj[prop].tid) == -1)
                {
                    if (level>main_level.value)
                    {
                        // do things on down tree
                        level_array.splice(0);
                    }
                    if (level<main_level.value)
                    {   
                        // do things on up tree
                        level_array.splice(0);
                    }
                    level_array.push(obj[prop].tid);
                    // trace
                    trace( tabs + "[" + obj[prop].tid + "] " + obj[prop].name + " level " + level);
                    main_level.value = level;
                    parseDrupalTree( obj, level + 1, obj[prop].tid, main_level);
                }
            }
        }
    }
}

class Reference {
    public var value:*;
}

The result is a nicely indented taxonomy tree:

[1] Milieu level 0
                [7] Subthema 3 level 1
                [8] Subthema 4 level 1
[3] Scholen level 0
                [7] Subthema 3 level 1
                [9] Subthema 5 level 1
[4] Educatie level 0
                [5] Subthema 1 level 1
                                [7] Subthema 3 level 2
                [6] Subthema 2 level 1
[2] Vrouwen level 0
                [10] Subthema 6 level 1

When used with as3ds tree structure this translates for example into the following (AS3 application screenshot):

Find and Replace Script

When moving a Drupal install from one server to the next you often need to replace paths within several database fields and tables. To make this chore a little easier I have started to use the following DB script (thanks, krazyworks).

#!/bin/bash
echo -n "Enter username: " ; read db_user
echo -n "Enter $db_user password: " ; stty -echo ; read db_passwd ; stty echo ; echo ""
echo -n "Enter database name: " ; read db_name
echo -n "Enter host name: " ; read db_host
echo -n "Enter search string: " ; read search_string
echo -n "Enter replacement string: " ; read replacement_string

MYSQL="/usr/bin/mysql --skip-column-names -h${db_host} -u${db_user} -p${db_passwd}"

echo "SHOW TABLES;" | $MYSQL $db_name | while read db_table
do
echo "SHOW COLUMNS FROM $db_table;" | $MYSQL $db_name| \
awk -F'\t' '{print $1}' | while read tbl_column
do
echo "update $db_table set ${tbl_column} = replace(${tbl_column}, '${search_string}', '${replacement_string}');" |\
$MYSQL $db_name
done
done

The DB info needed for the script can be found using Drush:

drush sql-connect

Finding the vocabulary id in Drupal 7 with Drush

In Drupal 6 as you could go into the taxonomy section of the admin area and look at the vocabulary edit URL to find the numerical vocabulary id. In Drupal 7 the URL is no longer as verbose as it now shows the machine name of the vocabulary, for example admin/structure/taxonomy/my_vocabulary/edit.

If you have access to Drush, there is another way to quickly find the VID though:

drush php-eval '$tax=taxonomy_vocabulary_machine_name_load("main_site_structure"); echo $tax->vid;'
 

Webform conditional answer options

For a Webform based survey site I needed to create questions offering a choice between some *conditionally shown* answers. Because I had very little time, I decided to make some quick & easy changes to the Webform template, instead of creating a custom module.

My solution was to check for previous entries within the webform-form.tpl.php file & remove unneeded answers using some jQuery magic. Since it is no problem if the user unexpectedly might still see the hidden answers (for example, when JS is turned off), this suffices for now.

The resulting quick-n-easy adapted webform-form.tpl.php is attached below.

<?php
/**
 * @file
 * Customize the display of a complete webform.
 *
 * This file may be renamed "webform-form-[nid].tpl.php" to target a specific
 * webform on your site. Or you can leave it "webform-form.tpl.php" to affect
 * all webforms on your site.
 *
 * Available variables:
 * - $form: The complete form array.
 * - $nid: The node ID of the Webform.
 *
 * The $form array contains two main pieces:
 * - $form['submitted']: The main content of the user-created form.
 * - $form['details']: Internal information stored by Webform.
 */

 // Retrieve total pages and current page.
 
$current_page = $form['details']['page_num']['#value'];
 
$total_pages = $form['details']['page_count']['#value'];
?>


<div class="webformed-<?php echo $current_page; ?>">

<?php
 
echo '<div id="page-count">' . $current_page . " / " . $total_pages . '</div>';

 
// If editing or viewing submissions, display the navigation at the top.
 
if (isset($form['submission_info']) || isset($form['navigation'])) {
    print
drupal_render($form['navigation']);
    print
drupal_render($form['submission_info']);
  }

 
// Print out the main part of the form.
  // Feel free to break this up and move the pieces within the array.
 
print drupal_render($form['submitted']);

 
// Always print out the entire $form. This renders the remaining pieces of the
  // form that haven't yet been rendered above.
 
print drupal_render($form);

 
// Print out the navigation again at the bottom.
 
if (isset($form['submission_info']) || isset($form['navigation'])) {
    unset(
$form['navigation']['#printed']);
    print
drupal_render($form['navigation']);
  }


?>

</div>

<script type='text/javascript'>
<?php

// Check if current page is relevant one.
if ($current_page==15) {

 
// Retrieve current submission id.
 
$sid = $form['#submission']->sid;
 
// Include webform.submissions.inc.
 
include_once(drupal_get_path('module', 'webform') .'/includes/webform.submissions.inc');
 
// Retrieve results
 
$subm = webform_get_submission($nid, $sid);

 
// Remove relevant answers using jQuery - no case statement, loop or function, just some quick if/thens 
 
if ($subm->data[22]['value'][0]!=4) echo "$('#edit-submitted-question77-1-wrapper').remove();"
  if (
$subm->data[33]['value'][0]!=7) echo "$('#edit-submitted-question77-2-wrapper').remove();";
  if (
$subm->data[64]['value'][0]!=3) echo "$('#edit-submitted-question77-3-wrapper').remove();"
  if (
$subm->data[15]['value'][0]==1) echo "$('#edit-submitted-question77-4-wrapper').remove();"
  if (
$subm->data[12]['value'][0]!=7) echo "$('#edit-submitted-question77-5-wrapper').remove();"
  if (
$subm->data[13]['value'][0]!=3) echo "$('#edit-submitted-question77-6-wrapper').remove();";

}
?>

</script>

Drupal Staging - Site Update

In researching more streamlined options for our Aegir based dev/live cycle, I (re)discovered several takes on the staging problem: everything in code methodology, the deploy module, the site_update module, migraine, the patterns module and of course features.

Each seems interesting in its own right, so I have decided to do some testing to see if one or more can help us better our current staging procedure. I will start with the "site update" module, since it seems to offer a light yet efficient take on sharing settings between sites. It might also be easily integrated in our current Aegir based workstream.

So I got to work. Following the spirit of the README.txt I created three new sites in Aegir, a DEV, LIVE and BASE install. Drush downloaded some essential modules (views, cck, rules), of course including site_update-6.x-1.x-dev. Added a test content type, a test view & changed some permissions on the base install. Then enabled the module on all three sites, necessitating the install of the "bad judgement" module(!).

Following the base site configuration I tried to run the database dump script from the sites/basesite/ directory. That didn't work out of the box on our Aegir server, since the database dump script retrieves its database settings from settings.php, which in an Aegir based site is to be found in drushrc.php.

Luckily, all that was needed for Aegir compatibility were some small changes to the site_update_dump script:


...

$parse_error = false;

// RvE 03-03-2011 added drushrc $options

if (isset ($db_url) && preg_match('/^mysqli?:\/\/(.*):(.*)@(.*)\/(.*)$/',$db_url,$matches)) {

  $db_conf->username = $matches[1];
  $db_conf->password = $matches[2];
  $db_conf->hostname = $matches[3];
  $db_conf->database = $matches[4];

} else if ( isset($options) && $options['db_type']='mysqli' ) {

  $db_conf->password = $options['db_passwd'];
  $db_conf->database = $options['db_name'];
  $db_conf->username = $options['db_user'];
  $db_conf->hostname = $options['db_host'];

} else {
  $parse_error = true;
}

if (!$parse_error) {

...

// RvE 03-03-2011 added -h option

function build_mysqldump_command($db_conf, $options, $ignore_tables, $include_tables) {
  $command = "mysqldump -u $db_conf->username -p$db_conf->password -h$db_conf->hostname  $options";

...

Now you are able to generate sites/basesite/database/site_update.sql by running site_update_dump from the base site root and copy the resulting site_update.sql to the previously created sites/devsite/database and sites/livesite/database directories.

Before running an update, there is one additional step specific to multi-site installs. Site_update by default looks for the SQL file in sites/all/database. In the case of a multi-site install you have to tell every site where its particular site_update.sql resides. Normally you set this path in settings.php. Within an Aegir controlled environment, you have to add the path to a local.settings.php file (since Aegir is allowed to override settings.php).

<?php
//put the following in the sites settings.php or local.settings.php
$conf['site_update_sql_file'] = 'sites/devsite/database/site_update.sql';

Run update.php and all base data is indeed nicely added to the dev and live sites. Still, I had hoped the module might offer me a little more by default. For example creation of content type tables when adding new CCK types. For now I'll keep this module in mind. Next module to be tested: deploy.

// added 06-03-2010:
made a patch available at http://drupal.org/node/1081230#comment-4175934