diff --git a/components/number.inc b/components/number.inc index e69de29..e88d055 100644 --- a/components/number.inc +++ b/components/number.inc @@ -0,0 +1,654 @@ + '', + 'form_key' => NULL, + 'pid' => 0, + 'weight' => 0, + 'value' => '', + 'mandatory' => 0, + 'extra' => array( + 'type' => 'textfield', + 'size' => '', + 'maxlength' => '', + 'field_prefix' => '', + 'field_suffix' => '', + 'unique' => 0, + 'title_display' => 0, + 'description' => '', + 'attributes' => array(), + 'private' => FALSE, + 'minimum' => '', + 'maximum' => '', + 'step' => '', + 'decimals' => '0', + 'point' => '.', + 'separator' => ',', + 'integer' => 0, + 'excludezero' => 0, + ), + ); +} + +/** + * Implements _webform_theme_component(). + */ +function _webform_theme_number() { + return array( + 'webform_display_number' => array( + 'render element' => 'element', + 'file' => 'components/number.inc', + ), + ); +} + +/** + * Implements _webform_edit_component(). + */ +function _webform_edit_number($component) { + $form = array(); + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Default value'), + '#default_value' => $component['value'], + '#description' => t('The default value of the field.') . theme('webform_token_help'), + '#size' => 60, + '#maxlength' => 1024, + '#weight' => 0, + ); + $form['display']['type'] = array( + '#type' => 'radios', + '#title' => t('Element type'), + '#options' => array( + 'textfield' => t('Text field'), + 'select' => t('Select list'), + ), + '#default_value' => $component['extra']['type'], + '#description' => t('A minimum, maximum, and step value are all required if displaying as a select.'), + '#weight' => -1, + '#parents' => array('extra', 'type'), + ); + $form['display']['size'] = array( + '#type' => 'textfield', + '#title' => t('Width'), + '#default_value' => $component['extra']['size'], + '#description' => t('Width of the textfield.') . ' ' . t('Leaving blank will use the default size.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 0, + '#parents' => array('extra', 'size'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['display']['decimals'] = array( + '#type' => 'textfield', + '#title' => t('Decimals'), + '#default_value' => $component['extra']['decimals'], + '#description' => t('Number of decimal points to display.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 1, + '#parents' => array('extra', 'decimals'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['display']['field_prefix'] = array( + '#type' => 'textfield', + '#title' => t('Label placed to the left of the textfield'), + '#default_value' => $component['extra']['field_prefix'], + '#description' => t('Examples: $, #, -.'), + '#size' => 20, + '#maxlength' => 127, + '#weight' => 1.1, + '#parents' => array('extra', 'field_prefix'), + ); + $form['display']['field_suffix'] = array( + '#type' => 'textfield', + '#title' => t('Label placed to the right of the textfield'), + '#default_value' => $component['extra']['field_suffix'], + '#description' => t('Examples: lb, kg, %.'), + '#size' => 20, + '#maxlength' => 127, + '#weight' => 1.2, + '#parents' => array('extra', 'field_suffix'), + ); + $form['display']['separator'] = array( + '#type' => 'select', + '#title' => t('Thousands separator'), + '#options' => array( + ',' => t('Comma (,)'), + '.' => t('Period (.)'), + ' ' => t('Space ( )'), + '' => t('None'), + ), + '#default_value' => $component['extra']['separator'], + '#weight' => 3, + '#parents' => array('extra', 'separator'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['display']['point'] = array( + '#type' => 'select', + '#title' => t('Decimal point'), + '#options' => array( + ',' => t('Comma (,)'), + '.' => t('Period (.)'), + ), + '#default_value' => $component['extra']['point'], + '#weight' => 4, + '#parents' => array('extra', 'point'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['validation']['unique'] = array( + '#type' => 'checkbox', + '#title' => t('Unique'), + '#return_value' => 1, + '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'), + '#weight' => 1, + '#default_value' => $component['extra']['unique'], + '#parents' => array('extra', 'unique'), + ); + $form['validation']['integer'] = array( + '#type' => 'checkbox', + '#title' => t('Integer'), + '#return_value' => 1, + '#description' => t('Permit only integer values as input. e.g. 12.34 would be invalid.'), + '#weight' => 1.5, + '#default_value' => $component['extra']['integer'], + '#parents' => array('extra', 'integer'), + ); + $form['validation']['minimum'] = array( + '#type' => 'textfield', + '#title' => t('Minimum'), + '#default_value' => $component['extra']['minimum'], + '#description' => t('Minimum numeric value for range checking.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 2, + '#parents' => array('extra', 'minimum'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['validation']['maximum'] = array( + '#type' => 'textfield', + '#title' => t('Maximum'), + '#default_value' => $component['extra']['maximum'], + '#description' => t('Maximum numeric value for range checking.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 2, + '#parents' => array('extra', 'maximum'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['validation']['step'] = array( + '#type' => 'textfield', + '#title' => t('Step'), + '#default_value' => $component['extra']['step'], + '#description' => t('Limit options to a specific increment. e.g. a step of "5" would allow values 5, 10, 15, etc.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 3, + '#parents' => array('extra', 'step'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + // Analysis settings. + $form['analysis'] = array( + '#type' => 'fieldset', + '#title' => t('Analysis'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#weight' => 10, + ); + $form['analysis']['excludezero'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude zero'), + '#return_value' => 1, + '#description' => t('Exclude entries of zero (or blank) when counting submissions to calculate average and standard deviation.'), + '#weight' => 1.5, + '#default_value' => $component['extra']['excludezero'], + '#parents' => array('extra', 'excludezero'), + ); + return $form; +} + +/** + * Implements _webform_render_component(). + */ +function _webform_render_number($component, $value = NULL, $filter = TRUE) { + if ($component['extra']['type'] == 'textfield') { + // Render as textfield. + $element = array( + '#type' => 'textfield', + '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', + '#default_value' => $filter ? _webform_filter_values($component['value'], NULL, NULL, NULL, FALSE) : $component['value'], + '#required' => $component['mandatory'], + '#weight' => $component['weight'], + '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? _webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']), + '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? _webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']), + '#description' => $filter ? _webform_filter_descriptions($component['extra']['description']) : $component['extra']['description'], + '#attributes' => $component['extra']['attributes'], + '#theme_wrappers' => array('webform_element'), + '#webform_component' => $component, + ); + + if ($component['extra']['size'] > 0) { + $element['#size'] = $component['extra']['size']; + } + if ($component['extra']['maxlength'] > 0) { + $element['#maxlength'] = $component['extra']['maxlength']; + } + + if (isset($value)) { + if ($component['extra']['decimals'] != '') { + $value[0] = _webform_number_format($component, $value); + } + $element['#default_value'] = $value[0]; + } + } + else { + // Render as select. + $element = array( + '#title' => $filter ? _webform_filter_xss($component['name']) : $component['name'], + '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before', + '#required' => $component['mandatory'], + '#weight' => $component['weight'], + '#description' => $filter ? _webform_filter_descriptions($component['extra']['description']) : $component['extra']['description'], + '#theme_wrappers' => array('webform_element'), + '#webform_component' => $component, + '#type' => $component['extra']['type'], + ); + + // Create user-specified options list as an array. + $default_value = $filter ? _webform_filter_values($component['value'], NULL, NULL, NULL, FALSE) : $component['value']; + $options = _webform_number_select_options($component); + + // Add default options if using a select list with no default. This trigger's + // Drupal 7's adding of the option for us. See @form_process_select(). + if ($component['extra']['type'] == 'select' && $default_value === '') { + $element['#empty_value'] = ''; + } + $element['#default_value'] = $default_value; + + // Set the component options. + $element['#options'] = $options; + } + + // Component validation. + $element['#element_validate'][] = '_webform_validate_number'; + + // Enforce uniqueness. + if ($component['extra']['unique']) { + $element['#element_validate'][] = 'webform_validate_unique'; + } + + return $element; +} + +/** + * Implements _webform_display_component(). + */ +function _webform_display_number($component, $value, $format = 'html') { + if (isset($value[0]) && $component['extra']['decimals'] != '') { + $value[0] = _webform_number_format($component, $value[0]); + } + return array( + '#title' => $component['name'], + '#weight' => $component['weight'], + '#theme' => 'webform_display_textfield', + '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'), + '#field_prefix' => $component['extra']['field_prefix'], + '#field_suffix' => $component['extra']['field_suffix'], + '#format' => $format, + '#value' => isset($value[0]) ? $value[0] : '', + '#webform_component' => $component, + ); +} + +/** + * Format the output of data for this component. + */ +function theme_webform_display_number($variables) { + $element = $variables['element']; + $prefix = $element['#format'] == 'html' ? '' : $element['#field_prefix']; + $suffix = $element['#format'] == 'html' ? '' : $element['#field_suffix']; + if ($element['#webform_component']['extra']['decimals'] != '') { + $element['#value'] = _webform_number_format($element['#webform_component'], $element['#value']); + } + $value = $element['#format'] == 'html' ? check_plain($element['#value']) : $element['#value']; + return $value !== '' ? ($prefix . $value . $suffix) : ' '; +} + +/** + * Implements _webform_analysis_component(). + */ +function _webform_analysis_number($component, $sids = array(), $single = FALSE) { + $advanced_stats = $single; + + $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC)) + ->fields('wsd', array('data')) + ->condition('nid', $component['nid']) + ->condition('cid', $component['cid']); + + if (count($sids)) { + $query->condition('sid', $sids, 'IN'); + } + + $population = array(); + $submissions = 0; + $nonzero = 0; + $sum = 0; + + $result = $query->execute(); + foreach ($result as $data) { + $value = trim($data['data']); + if ($value == '') { + $number = 0.0; + } + else { + $number = $value * 1.0; + } + + if ($number > 0) { + $nonzero++; + $sum += $number; + } + $population[] = $number; + $submissions++; + } + sort($population, SORT_NUMERIC); + + // Average and population count (if we have values). + if ($sum) { + if ($component['extra']['excludezero']) { + $average = $sum / $nonzero; + $average_title = t('Average !mu excluding zeros/blanks', array('!mu' => $advanced_stats ? '(μ)' : '')); + // Sample (sub-set of total population). + $population_count = $nonzero - 1; + $sigma = 'sd'; + $description = t('sample'); + } + else { + $average = $sum / $submissions; + $average_title = t('Average !mu including zeros/blanks', array('!mu' => $advanced_stats ? '(μ)' : '')); + // Population. + $population_count = $submissions; + $sigma = 'σ'; + $description = t('population'); + } + } + + // Formatting. + if ($component['extra']['decimals'] != '') { + $average = _webform_number_format($component, $average); + $sum = _webform_number_format($component, $sum); + } + + $rows[0] = array(t('Zero/blank'), ($submissions - $nonzero)); + $rows[1] = array(t('User entered value'), $submissions); + $rows[2] = array(t('Sum') . ($advanced_stats ? ' (Σ)' : ''), $sum); + $rows[3] = array($average_title, $average); + $rows[4] = array('', l(t('More stats ยป'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid'])); + + // Normal distribution information. + if ($advanced_stats && $population_count) { + // Standard deviation. + $stddev = 0; + foreach($population as $value) { + // Obtain the total of squared variances. + $stddev += pow(($value - $average), 2); + } + if ($population_count > 0) { + $stddev = sqrt($stddev / $population_count); + } + else { + $stddev = sqrt($stddev); + } + + // Build distribution table. + $count = array(); + $percent = array(); + $limit = array(); + + $count[] = t('Count'); + $percent[] = t('% of !description', array('!description' => $description)); + $limit[] = ''; + + $index = 1; + $count[$index] = 0; + $limit[] = $average - ($stddev * 4); + foreach($population as $value) { + while ($value >= $limit[$index]) { + $percent[] = number_format($count[$index] / $population_count * 100, 2, '.', ''); + $limit[] = $limit[$index] + $stddev; + $index += 1; + if ($limit[$index] == $average) { + $limit[$index] = $limit[$index] + $stddev; + } + $count[$index] = 0; + } + $count[$index] += 1; + } + $percent[] = number_format($count[$index] / $population_count * 100, 2, '.', ''); + + $header = array( + t('Normal Distribution'), + '-4' . $sigma, + '-3' . $sigma, + '-2' . $sigma, + '-1' . $sigma, + '+1' . $sigma, + '+2' . $sigma, + '+3' . $sigma, + '+4' . $sigma, + ); + + // Output distribution table. + if ($component['extra']['decimals'] != '') { + $stddev = _webform_number_format($component, $stddev); + $low = _webform_number_format($component, $population[0]); + $high = _webform_number_format($component, end($population)); + foreach($limit as $key => $value) { + $limit[$key] = _webform_number_format($component, $value); + } + } + else { + foreach($limit as $key => $value) { + $limit[$key] = number_format($value, 2, '.', ''); + } + } + $limit[0] = t('Boundary'); + + $distrows = array(); + $distrows[] = $limit; + $distrows[] = $count; + $distrows[] = $percent; + + $output = theme('table', array('header' => $header, 'rows' => array($limit, $count, $percent))); + + $rows[4] = array(t('Range'), t('!low to !high', array('!low' => $low, '!high' => $high))); + $rows[5] = array(t('Standard deviation (!sigma)', array('!sigma' => $sigma)), $stddev); + $rows[6] = array(array('data' => $output, 'colspan' => 2)); + } + + return $rows; +} + +/** + * Implements _webform_table_component(). + */ +function _webform_table_number($component, $value) { + if ($component['extra']['decimals'] != '' && isset($value[0])) { + $value[0] = _webform_number_format($component, $value[0]); + } + return check_plain(empty($value[0]) ? '' : $value[0]); +} + +/** + * Implements _webform_csv_headers_component(). + */ +function _webform_csv_headers_number($component, $export_options) { + $header = array(); + $header[0] = ''; + $header[1] = ''; + $header[2] = $component['name']; + return $header; +} + +/** + * Implements _webform_csv_data_component(). + */ +function _webform_csv_data_number($component, $export_options, $value) { + if ($component['extra']['decimals'] != '' && isset($value[0])) { + $value[0] = number_format($value[0], $component['extra']['decimals'], '.', ''); + } + return !isset($value[0]) ? '' : $value[0]; +} + +/** + * A Drupal Form API Validation function. Validates the entered values from + * number components on the client-side form. + * + * @param $form_element + * The number form element. + * @param $form_state + * The full form state for the webform. + * @return + * None. Calls a form_set_error if the number is not valid. + */ +function _webform_validate_number($form_element, &$form_state) { + $component = $form_element['#webform_component']; + $value = trim($form_element['#value']); + + if ($value != '') { + // Numeric test. + if (is_numeric($value)) { + // Range test. + if ($component['extra']['maximum'] != '') { + if ($component['extra']['maximum'] > $component['extra']['minimum']) { + $min = $component['extra']['minimum']; + $max = $component['extra']['maximum']; + } + else { + $min = $component['extra']['maximum']; + $max = $component['extra']['minimum']; + } + if ($value > $max || $value < $min) { + form_error($form_element, t('%name field value of %value should be in the range %min to %max.', array('%name' => $component['name'], '%value' => $value, '%min' => $min, '%max' => $max))); + } + } + // Integer test. + if ($component['extra']['integer']) { + if (!is_int($value * 1)) { + form_error($form_element, t('%name field value of %value should be an integer.', array('%name' => $component['name'], '%value' => $value))); + } + } + } + else { + form_error($form_element, t('%name field value of %value should be numeric.', array('%name' => $component['name'], '%value' => $value))); + } + } + +} + +/** + * Validation of number edit form items. + */ +function _webform_edit_number_validate($element, &$form_state) { + // Shorten field names. + $values = $form_state['values']['extra']; + + switch ($element['#name']) { + case 'extra[minimum]': + if ($values['minimum'] == '') { + if ($values['type'] == 'select') { + form_error($element, t('Minimum is required when using a select list element.')); + } + } + else { + if (!is_numeric($values['minimum'])) { + form_error($element, t('Minimum must be numeric.')); + } + if ($values['integer'] && !is_int($values['minimum'] * 1)) { + form_error($element, t('Minimum must have an integer value.')); + } + } + break; + case 'extra[maximum]': + if ($values['maximum'] == '') { + if ($values['type'] == 'select') { + form_error($element, t('Maximum is required when using a select list element.')); + } + } + else { + if (!is_numeric($values['maximum'])) { + form_error($element, t('Maximum must be numeric.')); + } + if ($values['integer'] && !is_int($values['maximum'] * 1)) { + form_error($element, t('Maximum must have an integer value.')); + } + } + break; + case 'extra[step]': + if ($values['step'] != '') { + if (!is_numeric($values['step'])) { + form_error($element, t('Step must be numeric.')); + } + else { + if ($values['integer'] && !is_int($values['step'] * 1)) { + form_error($element, t('Step must have an integer value.')); + } + } + } + break; + } + return TRUE; +} + +/** + * Generate select list options. + */ +function _webform_number_select_options($component) { + $options = array(); + $step = abs($component['extra']['step']); + + // Generate list in correct direction. + $min = $component['extra']['minimum']; + $max = $component['extra']['maximum']; + if ($max > $min) { + for ($f = $min; $f <= $max; $f += $step) { + $options[$f . ''] = $f; + } + } + else { + for ($f = $min; $f >= $max; $f -= $step) { + $options[$f . ''] = $f; + } + } + // Add end limit if it's been skipped due to step. + if (end($options) != $max) { + $options[$f] = $max; + } + + // Apply requisite number formatting. + if ($component['extra']['decimals'] != '') { + foreach ($options as $key => $value) { + $options[$key] = _webform_number_format($component, $value); + } + } + + return $options; +} + +/** + * Apply number format. + */ +function _webform_number_format($component, $value) { + $value = $value * 1.0; + return number_format($value, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); +} diff --git a/webform.module b/webform.module index b036c2a..03e4d2e 100644 --- a/webform.module +++ b/webform.module @@ -785,6 +785,13 @@ function webform_webform_component_info() { ), 'file' => 'components/markup.inc', ), + 'number' => array( + 'label' => t('Number'), + 'description' => t('A numeric input field (either as textfield or select list).'), + 'features' => array( + ), + 'file' => 'components/number.inc', + ), 'pagebreak' => array( 'label' => t('Page break'), 'description' => t('Organize forms into multiple pages.'),