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) .'
'; } } @@ -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 ? ' ' : 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' => ''. $question_number .'', 'rowspan' => count($crows) + 1, 'valign' => 'top'); - $row[1] = array('data' => ''. $component['name'] .'', 'colspan' => '10'); - $rows = array_merge($rows, array_merge(array($row), $crows)); + if (is_array($row_data)) { + $row = array(); + if (!$single) { + $row[] = array('data' => ''. $question_number .'', 'rowspan' => count($row_data) + 1, 'valign' => 'top'); + $row[] = array('data' => ''. check_plain($node->webform['components'][$cid]['name']) .'', 'colspan' => '10'); } + $rows = array_merge($rows, array_merge(array($row), $row_data)); } }