Index: webform.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/webform/webform.module,v
retrieving revision 1.162
diff -u -r1.162 webform.module
--- webform.module	23 Jan 2010 22:56:02 -0000	1.162
+++ webform.module	25 Jan 2010 04:28:44 -0000
@@ -204,6 +204,16 @@
     'weight' => 5,
     'type' => MENU_LOCAL_TASK,
   );
+  $items['node/%webform_menu/webform-results/analysis/%webform_menu_component'] = array(
+    'title' => 'Analysis',
+    'load arguments' => array(1, 4),
+    'page callback' => 'webform_results_analysis',
+    'page arguments' => array(1, array(), 4),
+    'access callback' => 'webform_results_access',
+    'access arguments' => array(1),
+    'file' => 'includes/webform.report.inc',
+    'type' => MENU_CALLBACK,
+  );
   $items['node/%webform_menu/webform-results/table'] = array(
     'title' => 'Table',
     'page callback' => 'webform_results_table',
@@ -546,6 +556,10 @@
       'arguments' => array('element' => NULL),
       'file' => 'includes/webform.report.inc',
     ),
+    'webform_results_analysis' => array(
+      'arguments' => array('node' => NULL, 'data' => NULL, 'sids' => array(), 'component' => NULL),
+      'file' => 'includes/webform.report.inc',
+    ),
     // webform.submissions.inc
     'webform_submission_page' => array(
       'arguments' => array('submission' => NULL, 'submission_navigation' => NULL, 'submission_information' => NULL),
@@ -1402,22 +1416,28 @@
   }
 }
 
-function webform_client_form_validate($form, $form_state) {
+function webform_client_form_validate($form, &$form_state) {
   $node = node_load($form_state['values']['details']['nid']);
 
   // Run all #element_validate and #required checks. These are skipped initially
   // by setting #validated = TRUE on all components when they are added.
   _webform_client_form_validate($form, $form_state);
 
-  // Flatten trees within the submission.
-  $form_state['values']['submitted_tree'] = $form_state['values']['submitted'];
-  $form_state['values']['submitted'] = _webform_client_form_submit_flatten($node, $form_state['values']['submitted']);
-
+  // TODO: Remove additional validation (and submission) handling entirely? This
+  // is a terrible hack where we need to flatten the submission for validation
+  // but then restore the tree version so that the submit handling functions.
   if (trim($node->webform['additional_validate'])) {
+    // Flatten trees within the submission.
+    $form_state['values']['submitted_tree'] = $form_state['values']['submitted'];
+    $form_state['values']['submitted'] = _webform_client_form_submit_flatten($node, $form_state['values']['submitted']);
+
     // Support for Drupal 5 validation code.
     $form_values =& $form_state['values'];
     // We use eval here (rather than drupal_eval) because the user needs access to local variables.
     eval('?>'. $node->webform['additional_validate']);
+
+    // Restore the original form state so that the submit handler can reflatten.
+    $form_state['values']['submitted'] = $form_state['values']['submitted_tree'];
   }
 }
 
@@ -1783,8 +1803,11 @@
       if (isset($node->webform['components'][$cid])) {
         // Call the component process submission function.
         $component = $node->webform['components'][$cid];
-        if ((!isset($types) || in_array($component['type'], $types)) && ($new_value = webform_component_invoke($component['type'], 'submit', $component, $form_values[$component['form_key']]))) {
-          $form_values[$component['form_key']] = $new_value;
+        if ((!isset($types) || in_array($component['type'], $types))) {
+          $new_value = webform_component_invoke($component['type'], 'submit', $component, $form_values[$component['form_key']]);
+          if ($new_value !== NULL) {
+            $form_values[$component['form_key']] = $new_value;
+          }
         }
       }
     }
Index: components/select.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/webform/components/select.inc,v
retrieving revision 1.32
diff -u -r1.32 select.inc
--- components/select.inc	23 Jan 2010 22:56:02 -0000	1.32
+++ components/select.inc	25 Jan 2010 04:28:44 -0000
@@ -22,6 +22,8 @@
       'items' => '',
       'multiple' => NULL,
       'aslist' => NULL,
+      'other_option' => NULL,
+      'other_text' => t('Other...'),
       'description' => '',
     ),
   );
@@ -77,6 +79,21 @@
     '#default_value' => $component['extra']['aslist'],
     '#description' => t('Check this option if you want the select component to be of listbox type instead of radiobuttons or checkboxes.'),
   );
+  $form['extra']['other_option'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Allow "Other..." option'),
+    '#return_value' => 'Y',
+    '#default_value' => $component['extra']['other_option'],
+    '#description' => t('Check this option if you want to allow users to enter an option not on the list.'),
+    '#access' => module_exists('select_or_other'),
+  );
+  $form['extra']['other_text'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Text for "Other..." option'),
+    '#default_value' => $component['extra']['other_text'],
+    '#description' => t('If allowing other options, enter text to be used for other-enabling option.'),
+    '#access' => module_exists('select_or_other'),
+  );
 
   return $form;
 }
@@ -179,8 +196,29 @@
       $element['#default_value'] = $default_value;
     }
   }
+  elseif ($component['extra']['multiple'] === 'Y') {
+    $element['#default_value'] = array();
+  }
 
-  if ($component['extra']['aslist'] === 'Y') {
+  if ($component['extra']['other_option'] === 'Y' && module_exists('select_or_other')) {
+    // Set display as a select list:
+    $element['#type'] = 'select_or_other';
+    $element['#other'] = empty($component['extra']['other_text']) ? $component['extra']['other_text'] : t('Other...');
+    $element['#other_unknown_defaults'] = 'other';
+    $element['#other_delimiter'] = ', ';
+    if ($component['extra']['multiple'] === 'Y') {
+      $element['#multiple'] = TRUE;
+      $element['#select_type'] = 'checkboxes';
+    }
+    else {
+      $element['#multiple'] = FALSE;
+      $element['#select_type'] = 'radios';
+    }
+    if ($component['extra']['aslist'] === 'Y') {
+      $element['#select_type'] = 'select';
+    }
+  }
+  elseif ($component['extra']['aslist'] === 'Y') {
     // Set display as a select list:
     $element['#type'] = 'select';
     if ($component['extra']['multiple'] === 'Y') {
@@ -228,6 +266,7 @@
  * Implementation of _webform_display_component().
  */
 function _webform_display_select($component, $value, $format = 'html') {
+  ksort($value);
   return array(
     '#title' => $component['name'],
     '#weight' => $component['weight'],
@@ -250,17 +289,31 @@
 
   if (is_array($value)) {
     foreach ($value as $key => $option_value) {
-      if ($option_value != '') {
-        $value[$key] = $options[$key];
+      // Handle options that are specified options.
+      if ($option_value !== '' && isset($options[$key])) {
+        // Checkboxes submit an *integer* value of 0 when not checked.
+        if ($option_value === 0 && $options[$key] != '0' && $component['extra']['aslist'] !== 'Y' && $component['extra']['multiple'] === 'Y') {
+          unset($value[$key]);
+        }
+        else {
+          $value[$key] = $options[$key];
+        }
       }
-      // Checkboxes submit a value of 0 when not checked.
-      elseif ($option_value == 0 && $component['extra']['aslist'] !== 'Y' && $component['extra']['multiple'] === 'Y') {
-        unset($value[$key]);
+      // Handle options that are added through the "other" field.
+      elseif ($component['extra']['other_option'] === 'Y' && module_exists('select_or_other')) {
+        $value[$key] = $option_value;
       }
       else {
         unset($value[$key]);
       }
     }
+
+    // Always save at least something, even if it's an empty single value.
+    if (empty($value)) {
+      $value[0] = '';
+    }
+
+    $value = array_values($value);
   }
 
   return $value;
@@ -378,15 +431,27 @@
   if ($component['extra']['multiple']) {
     foreach ((array) $element['#value'] as $option_value) {
       if ($option_value !== '') {
+        // Administer provided values.
         if (isset($options[$option_value])) {
           $items[] = $options[$option_value];
         }
+        // User-specified in the "other" field.
+        else {
+          $items[] = $option_value;
+        }
       }
     }
   }
   else {
-    if (isset($element['#value'][0]) && $element['#value'][0] !== '' && $options[$element['#value'][0]]) {
-      $items[] = $options[$element['#value'][0]];
+    if (isset($element['#value'][0]) && $element['#value'][0] !== '') {
+      // Administer provided values.
+      if (isset($options[$element['#value'][0]])) {
+        $items[] = $options[$element['#value'][0]];
+      }
+      // User-specified in the "other" field.
+      else {
+        $items[] = $element['#value'][0];
+      }
     }
   }
 
@@ -412,28 +477,57 @@
 /**
  * Implementation of _webform_analysis_component().
  */
-function _webform_analysis_select($component, $sids = array()) {
+function _webform_analysis_select($component, $sids = array(), $single = FALSE) {
   $options = _webform_select_options($component['extra']['items'], TRUE);
-  $placeholders = count($sids) ? array_fill(0, count($sids), "'%s'") : array();
-  $sidfilter = count($sids) ? " AND sid in (".implode(",", $placeholders).")" : "";
+  $show_other_results = $single;
 
-  $query = 'SELECT data, count(data) as datacount '.
-    ' FROM {webform_submitted_data} '.
-    ' WHERE nid = %d '.
-    ' AND cid = %d '.
-    " AND data != '0' AND data != '' $sidfilter ".
+  $sid_placeholders = count($sids) ? array_fill(0, count($sids), "'%s'") : array();
+  $sid_filter = count($sids) ? " AND sid IN (".implode(",", $sid_placeholders).")" : "";
+
+  $not = $show_other_results ? 'NOT ' : '';
+  $placeholders = count($options) ? array_fill(0, count($options), "'%s'") : array();
+  $query = 'SELECT data, count(data) as datacount ' .
+    ' FROM {webform_submitted_data} ' .
+    ' WHERE nid = %d ' .
+    ' AND cid = %d ' .
+    " AND data != ''" . $sid_filter .
+    ($placeholders ? ' AND data ' . $not . 'IN (' . implode(',', $placeholders) . ')' : '') .
     ' GROUP BY data ';
-  $result = db_query($query, array_merge(array($component['nid'], $component['cid']), $sids));
+
+  $count_query = 'SELECT count(*) as datacount ' .
+    ' FROM {webform_submitted_data} ' .
+    ' WHERE nid = %d ' .
+    ' AND cid = %d ' .
+    " AND data != ''" . $sid_filter;
+
+  $result = db_query($query, array_merge(array($component['nid'], $component['cid']), $sids, array_keys($options)));
   $rows = array();
+  $normal_count = 0;
   while ($data = db_fetch_array($result)) {
-    if (isset($options[$data['data']])) {
-      $display_option = $options[$data['data']];
-    }
-    else {
-      $display_option = $data['data'];
+    $display_option = $single ? $data['data'] : $options[$data['data']];
+    $rows[$data['data']] = array(check_plain($display_option), $data['datacount']);
+    $normal_count += $data['datacount'];
+  }
+
+  if (!$show_other_results) {
+    // Order the results according to the normal options array.
+    $ordered_rows = array();
+    foreach (array_intersect_key($options, $rows) as $key => $label) {
+      $ordered_rows[] = $rows[$key];
+    }
+  
+    // Add a row for any unknown or user-entered values.
+    if ($component['extra']['other_option'] === 'Y') {
+      $full_count = db_result(db_query($count_query, array_merge(array($component['nid'], $component['cid']), $sids)));
+      $other_count = $full_count - $normal_count;
+      $display_option = empty($component['extra']['other_text']) ? $component['extra']['other_text'] : t('Other...');
+      $other_text = $other_count ? $other_count . ' (' . l(t('view'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']) . ')' : $other_count;
+      $ordered_rows[] = array(check_plain($display_option), $other_text);
     }
-    $rows[] = array($display_option, $data['datacount']);
+
+    $rows = $ordered_rows;
   }
+
   return $rows;
 }
 
@@ -445,7 +539,7 @@
   // Set the value as a single string.
   if (is_array($value)) {
     foreach ($value as $option_value) {
-      if ($option_value !== '0') {
+      if ($option_value !== '') {
         $output .= check_plain($option_value) .'<br />';
       }
     }
@@ -470,6 +564,10 @@
     $headers[0][] = '';
     $headers[1][] = $component['name'];
     $items = _webform_select_options($component['extra']['items'], TRUE);
+    if ($component['extra']['other_option'] === 'Y') {
+      $other_label = !empty($component['extra']['other_text']) ? $component['extra']['other_text'] : t('Other...');
+      $items[$other_label] = $other_label;
+    }
     $count = 0;
     foreach ($items as $key => $item) {
       // Empty column per sub-field in main header.
@@ -498,13 +596,20 @@
 
   if ($component['extra']['multiple']) {
     foreach ($options as $key => $item) {
-      if (in_array($key, (array) $value) === TRUE) {
+      $index = array_search($key, (array) $value);
+      if ($index !== FALSE) {
         $return[] = ($export_options['select_format'] == 'separate') ? 'X' : $key;
+        unset($value[$index]);
       }
       elseif ($export_options['select_format'] == 'separate') {
         $return[] = '';
       }
     }
+
+    // Any remaining items in the $value array will be user-added options.
+    if ($component['extra']['other_option'] === 'Y') {
+      $return[] = count($value) ? implode(',', $value) : '';
+    }
   }
   else {
     $return = $value[0];
Index: includes/webform.report.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/webform/includes/webform.report.inc,v
retrieving revision 1.14
diff -u -r1.14 webform.report.inc
--- includes/webform.report.inc	24 Jan 2010 00:07:17 -0000	1.14
+++ includes/webform.report.inc	25 Jan 2010 04:28:44 -0000
@@ -614,26 +614,66 @@
 
 /**
  * Provides a simple analysis of all submissions to a webform.
+ *
+ * @param $node
+ *   The webform node on which to generate the analysis.
+ * @param $sids
+ *   An array of submission IDs to which this analysis may be filtered. May be
+ *   used to generate results that are per-user or other groups of submissions.
+ * @param $component
+ *   A webform component. If passed in, additional information may be returned
+ *   relating specifically to that component's analysis, such as a list of
+ *   "Other" values within a select list.
  */
-function webform_results_analysis($node, $sids = array()) {
+function webform_results_analysis($node, $sids = array(), $single_component = NULL) {
+  if (!is_array($sids)) {
+    $sids = array();
+  }
+
+  // If showing a component's details, we don't want to loose the menu tabs.
+  if ($single_component) {
+    $item = menu_get_item('node/' . $node->nid . '/webform-results/analysis');
+    menu_set_item(NULL, $item);
+  }
+
+  $components = isset($single_component) ? array($single_component['cid'] => $single_component) : $node->webform['components'];
+  $data = array();
+  foreach ($components as $cid => $component) {
+    // Do component specific call.
+    if ($row_data = webform_component_invoke($component['type'], 'analysis', $component, $sids, isset($single_component))) {
+      $data[$cid] = $row_data;
+    }
+  }
+
+  return theme('webform_results_analysis', $node, $data, $sids, $single_component);
+}
+
+/**
+ * Output the content of the Analysis page.
+ *
+ * @see webform_results_analysis()
+ */
+function theme_webform_results_analysis($node, $data, $sids = array(), $component = NULL) {
+
   $rows = array();
   $question_number = 0;
+  $single = isset($component);
 
   $headers = array(
-    t('Q'),
-    array('data' => t('responses'), 'colspan' => '10')
+    $single ? $component['name'] : t('Q'),
+    array('data' => $single ? '&nbsp' : t('responses'), 'colspan' => '10')
   );
 
-  foreach ($node->webform['components'] as $component) {
+  foreach ($data as $cid => $row_data) {
     $question_number++;
 
-    // Do component specific call.
-    if ($crows = webform_component_invoke($component['type'], 'analysis', $component, $sids)) {
-      if (is_array($crows)) {
-        $row[0] = array('data' => '<strong>'. $question_number .'</strong>', 'rowspan' => count($crows) + 1, 'valign' => 'top');
-        $row[1] = array('data' => '<strong>'. $component['name'] .'</strong>', 'colspan' => '10');
-        $rows = array_merge($rows, array_merge(array($row), $crows));
+    if (is_array($row_data)) {
+      $row = array();
+      if (!$single) {
+        $row[] = array('data' => '<strong>'. $question_number .'</strong>', 'rowspan' => count($row_data) + 1, 'valign' => 'top');
+        $row[] = array('data' => '<strong>'. check_plain($node->webform['components'][$cid]['name']) .'</strong>', 'colspan' => '10');
       }
+      $rows = array_merge($rows, array_merge(array($row), $row_data));
     }
   }
 
