Abstract: This case study describes two ways to push data collected within a Flash activity into the Drupal database. The two solutions vary in some important ways: the ability to enable AHAH form submission to avoid page refreshes, the ability to access collected data from Views, the ability to export collected data to a spreadsheet, and of course, ease of implementation. One caveat for both solutions is that they do rely on javascript. The only non-javascript solution I am aware of involves the use of AMFPHP and the Services module. Implementation of these is easy if you are working in AS3. Not so much for AS2.
Flash has always been one of the most powerful ways to create complex educational activities for the web. Many of us in educational technology have built up libraries of code, components, and templates to speed the development of Flash content. Connecting Flash with a database has traditionally involved the use of a so-called middleware script. I have personally always written my middleware in php, so at first glance, integrating my existing flash-based materials into Flash would seem an easy and natural thing to do. Not so.
The basic steps for connecting a non-drupalized Flash activity to a database are as follows:
1) Create a database table with the desired fields
2) Write a php script that catches Flash data and sends it to the database
3) Use a flash LoadVars object to POST data to the script.
The drupal equivalent for each of these steps was surprisingly difficult to discover, at least, I found it so. Now that I have a solution for each step, I thought I would document it for others struggling with the same issue, and hopefully so that others can suggest better ones.
Step 1: Create a database table with the desired fields
There are two approaches to this problem. Well, two that I tried. The first is to take advantage of existing tables. The second is to create a custom table within the Drupal database. I tried both approaches, and found, not surprisingly, that there are pros and cons to each.
In my search for existing tables, the obvious candidates were the ones CCK generates when you add fields to a content type, and the tables generated by modules like Gradebook, Quiz, Webform, or Userpoints. Of these, the easiest to use are Quiz and Webform; the two that place form inputs on the same screen with the activity. A simple jquery command sent to the page from Flash can both change the value of these fields, and submit the form. The downside of this approach is that when you submit the form, the entire page refreshes. This causes the Flash activity to re-start from the beginning. Sometimes this simply is not acceptable. Other times, it is just fine. In those cases, I highly recommend the Webforms approach, unless you really,really need Views integration.
Code for placing a webform form into a new page.
One reason to do this would be to hide the form from view:
<div style="display:none;">
<?php
$node = node_load(349);
$submission = array();
$enabled = TRUE;
$preview = FALSE;
print drupal_get_form('webform_client_form_349', $node, $submission, $enabled, $preview);
?>
</div>To create a custom table within the Drupal database, one coulde go directly into the database and add the table. However, I wanted to add and access the table properly. This involves writing a module. This might seem like a lot of extra trouble, but the payoffs are good: AHAH and Views integration are both possible, and you don't have to write a conventional php script that probably bypasses drupal's security.
Code for table creation
In your .module file
function e_page_grade_form(){
//this first part is unnecessary if you always want to start with a blank form
$form = array();
global $user;
$node = node_load(arg(1));
$nid = $node->nid;
$grade = db_fetch_object(db_query('SELECT * FROM {e_page_grade} WHERE nid=%d AND uid=%d', $nid, $user->uid));
// If a noteid exists, then get the existing values.
if ($grade) {
$earned = $grade->earned;
$time = $grade -> timestamp ;
}
else {
$time = time();
}
// build the table here
$cols = 8;
$form['earned'] = array(
'#type' => 'textfield',
'#title' => 'My Grade',
'#required' => true,
'#default_value' => $earned,
'#weight' => 0,
'#cols' => $cols,
'#rows' => 1,
'#description' => 'Enter a new grade here.',
);
$form['time'] = array(
'#type' => 'hidden',
'#value' => $time,
);
$form['nid'] = array(
'#type' => 'textfield',
'#default_value' => $nid,
);
$form['uid'] = array(
'#type' => 'textfield',
'#default_value' => $user -> uid,
);
// Here is the stuff you need for AHAH implementation
$form['send'] = array(
'#type' => 'submit',
'#ahah' => array(
'path' => 'e_page/js',
'method' => 'replace',
'wrapper' => 'hidden-div'
),
'#value' => 'Submit Score',
'#weight' => 3,
);
return $form;
} Step 2: Write a php script that catches Flash data and sends it to the database
If you use an existing content type such as Webform or Quiz, this step is unnecessary. Simply place your Flash content in the Webform description or Quiz question field. The short-answer question type is the natural choice for this, since it provides a text input which is evaluated by a human-being, however, with clever work one could highjack other question types as well. It sort of depends on what type of data you are trying to capture. The webform is the most flexible in this regard, but since webform is not integrated with Views, and since quiz questions can be bundled together into an overall score, there could easily be situations in which you would prefer to live within the limitations of the available quiz forms, or even spin out a custom quiz question type.
If you write custom code, the equivalent for writing your middleware script is to drop a function into your module, and then add a menu item (via hook_menu) that points to the function. This to me was the most counter-intuitive step in the entire process. I understood that my module could contain a function that would process the form submission, what I didn't understand was that I could call the function by writing a menu item! Luckily, the following AHAH documentation cleared the fog away.
Code for processing form submissions from your own module.
In your .module file
//this first part is used to route form postings to the right php function, in this case, e_page_js
function e_page_menu() {
$items['e_page/js'] = array(
'page callback' => 'e_page_js',
'access arguments' => array('access e_page js'),
'type' => MENU_CALLBACK,
);
return $items;
}
// and now we write the function
function e_page_js() {
$nid = $_POST['nid'];
$earned = $_POST['earned'];
$uid = $_POST['uid'];
$timestamp = time();
db_query(
'INSERT INTO {e_page_grade} (uid, nid, earned, timestamp)'
. " VALUES (%d, %d, '%d', '%d')",
$uid,
$nid,
$earned,
$timestamp
);
return drupal_json(array('status' => TRUE, 'data' => $nid));;
}Step 3: POST data to Flash
Since we are using Drupal forms to submit the data to the database, all you need to do is write the appropriate javascript calls into Flash.
actionscript to send an input value from Flash to the form
getURL("javascript:$(#form-input-id.val('" + varName + "');");
actionscript to submit via AHAH
getURL("javascript:$(#your-edit-submit-id.onmousedown();");
actionscript to submit without AHAH
getURL("javascript:$(#your-form-id.submit();");
To perform more than one form action with a single javascript call, simply string lines of javascript together, separated by semi-colons:
getURL("javascript:$(#your-form-input-id.val('" + varName + "');$(#your-edit-submit-id.onmousedown();");
You can also just put consecutive getURL calls into the same AS2 function. As far as I can tell, there is no need to wait between getURL calls, which is only to say, it hasn't caused any problems in my testing. You could, of course, write javascript functions in your node and call them, either with an old-fashioned get URL or the more recent ExternalInterface.call(), but that introduces extra steps, and I don't see that it adds any value. Maybe someone else can point out why that would be superior.
Note that contrary to the documentation cited earlier, I have learned that calling a simple form.submit(), or calling a form's submit button's "click" fails to invoke AHAH. Instead, call the form's submit button's onmousedown(). Had to snoop drupal's core to find that one. Perhaps this is only because I failed to populate my AHAH's event field, but the default was not 'click'. Assuming that this is for a good reason, I'm sticking to onmousedown. Note also that the portions of the script starting with #your and ending in -id all need to be replaced with the appropriate form id's. The easiest way to find these is to throw the form into the page, and then view the page source.
That's it for now. I will be returning with actual code snippets a bit later today.
Comments
alternate approach
Hi Becky,
Thanks for posting this article (way back when). It was very helpful and ultimately steered me to an even simpler approach (at least for my needs).
My challenge was the same: embed a Flash object as a quiz question and save the resulting answer data. However instead of using AHHA, I found it easier to simply populate the answer field with the Flash data via JavaScript. In a nutshell, here's the steps:
1) Install/enable SWFTools suite.
2) Enable the Short Answer module,
3) Optional: relabel the content type to "Flash answer",
4) Create Short Answer CCK upload field (exp. field_flash_file) for the Flash file.
5) Create a custom module and use hook_form_alter to achieve the following:
/**
* Implementation of hook_form_alter().
*/
function custom_form_alter(&$form, &$form_state, $form_id) {
switch($form_id) {
case 'short_answer_node_form':
$form['body_field']['body']['#default_value'] = '[Placeholder text; this will be replaced at test time with uploaded flash object.]';
$form['body_field']['body']['#disabled'] = TRUE;
break;
case 'quiz_question_answering_form':
$node = node_load($form['question_nid']['#value']);
if ($node->type == 'short_answer') {
$form['tries']['#type']['wrapper'] = 'hidden-div';
$form['question']['#value'] = swf($node->field_flash_file[0]['filepath'], array('height'=>840, 'width'=>625));
drupal_add_js('
$.answerData = function(data) {
$("#edit-tries").val(data);
return false;
};
', 'inline');
}
break;
}
}
6) Enable the newly created custom module.
7) Create a Flash answer question and add it to a quiz.
8) From the Flash movie, call the following JavaScript (jQuery) function to write your data:
$.answerData(""); // put your data between the quotesTo test, you can temporarily disable the hook_form_alter line that adds the wrapper attribute to hide the answer field.
Now, when taking a quiz, the Flash question will display. Note the hidden answer field must contain data when clicking "Next" otherwise a validation error will occur. This would obviously be confusing for the user. To overcome this a default value could be added (either by Flash or hook_form_alter), which would be overwritten when the Flash object write legitimate data.
Note the following code example was written for Drupal 6, but it would take little effort to convert to Drupal 7.
Hopefully this alternate solution might be useful for those not requiring the power and sophistication of AHHA.
Cheers,
John
Services Module
Hi,
Out of curiosity, why not just use the services module with AMFPHP? I did that on a site I did where the Flash developer still used AS2. I had to find and drop in the remoting classes manually, but I think I can track those down and post them here if that'd help.
James
--James
Services & AMFPHP
I agree, a web service is much more elegant and efficient than either of the above solutions.
I actually had the Services module with AMFPHP up and running, but the Flash developer said she didn't know how (via ActionScript) to PUT the node data using AS2 (I'm not a Flash developer, but apparently it's easier with AS3).
In my case we're probably going to stick with the above solution (for the sake of the production schedule and budget), but it would be fantastic if you could post the sample code along with brief instructions on how to implement it.
John
SCORM?
This sounds a bit like SCORM.
http://en.wikipedia.org/wiki/Sharable_Content_Object_Reference_Model