diff --git a/components/number.inc b/components/number.inc new file mode 100644 index 0000000..3db892b --- /dev/null +++ b/components/number.inc @@ -0,0 +1,687 @@ + '', + 'form_key' => NULL, + 'pid' => 0, + 'weight' => 0, + 'value' => '', + 'mandatory' => 0, + 'extra' => array( + 'width' => '', + 'maxlength' => '', + 'field_prefix' => '', + 'field_suffix' => '', + 'disabled' => 0, + 'unique' => 0, + 'title_display' => 0, + 'description' => '', + 'attributes' => array(), + 'private' => FALSE, + 'minimum' => '', + 'maximum' => '', + 'step' => '', + 'decimals' => '0', + 'point' => '.', + 'separator' => ',', + 'integer' => 0, + 'excludezero' => 0, + 'distribution' => 0, + ), + ); +} + +/** + * Implements _webform_theme_component(). + */ +function _webform_theme_number() { + return array( + 'webform_display_number' => array( + 'arguments' => array('element' => NULL), + '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 numeric value of the field.') . theme('webform_token_help'), + '#size' => 60, + '#maxlength' => 1024, + '#weight' => 0, + ); + $form['display']['width'] = array( + '#type' => 'textfield', + '#title' => t('Width'), + '#default_value' => $component['extra']['width'], + '#description' => t('Width of the textfield.') . ' ' . t('Leaving blank will use the default size.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 0, + '#parents' => array('extra', 'width'), + ); + $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']['point'] = array( + '#type' => 'textfield', + '#title' => t('Decimal point'), + '#default_value' => $component['extra']['point'], + '#description' => t('Specify the character/s to use as decimal point.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 1.1, + '#parents' => array('extra', 'point'), + '#element_validate' => array('_webform_edit_number_validate'), + ); + $form['display']['separator'] = array( + '#type' => 'textfield', + '#title' => t('Thousands separator'), + '#default_value' => $component['extra']['separator'], + '#description' => t('Specify the character/s to display as thousands separator.'), + '#size' => 5, + '#maxlength' => 10, + '#weight' => 1.2, + '#parents' => array('extra', 'separator'), + '#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.3, + '#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.4, + '#parents' => array('extra', 'field_suffix'), + ); + $form['display']['disabled'] = array( + '#type' => 'checkbox', + '#title' => t('Disabled'), + '#return_value' => 1, + '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'), + '#weight' => 11, + '#default_value' => $component['extra']['disabled'], + '#parents' => array('extra', 'disabled'), + ); + $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('Enter the increment value for options to render this field as a select list.'), + '#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'), + ); + $form['analysis']['distribution'] = array( + '#type' => 'checkbox', + '#title' => t('Normal distribution'), + '#return_value' => 1, + '#description' => t('Show standard deviation and normal distribution table in analysis.'), + '#weight' => 1.5, + '#default_value' => $component['extra']['distribution'], + '#parents' => array('extra', 'distribution'), + ); + return $form; +} + +/** + * Implements _webform_render_component(). + */ +function _webform_render_number($component, $value = NULL, $filter = TRUE) { + if ($component['extra']['step'] == '') { + // 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_wrapper'), + '#pre_render' => array('webform_element_title_display'), + '#post_render' => array('webform_element_wrapper'), + '#webform_component' => $component, + ); + + // Change the 'width' option to the correct 'size' option. + if ($component['extra']['width'] > 0) { + $element['#size'] = $component['extra']['width']; + } + if ($component['extra']['maxlength'] > 0) { + $element['#maxlength'] = $component['extra']['maxlength']; + } + + if (isset($value)) { + if ($component['extra']['decimals'] != '') { + $value[0] = _webform_format_number($value, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + } + $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_wrapper'), + '#pre_render' => array('webform_element_title_display'), + '#post_render' => array('webform_element_wrapper'), + '#webform_component' => $component, + '#type' => 'select', + ); + + // Create user-specified options list as an array. + $filter = TRUE; + $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 matches + // Drupal 7's handling of select lists. + if ($default_value === '') { + $options = array('' => ($component['mandatory'] ? t('- Select -') : t('- None -'))) + $options; + } + $element['#default_value'] = $default_value; + + // Set the component options. + $element['#options'] = $options; + } + + if ($component['extra']['disabled']) { + $element['#attributes']['readonly'] = 'readonly'; + } + + // 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_format_number($value[0], $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + } + return array( + '#title' => $component['name'], + '#weight' => $component['weight'], + '#theme' => 'webform_display_number', + '#theme_wrappers' => $format == 'html' ? array('webform_element', 'webform_element_wrapper') : array('webform_element_text'), + '#post_render' => array('webform_element_wrapper'), + '#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($element) { + $prefix = $element['#format'] == 'html' ? filter_xss($element['#field_prefix']) : $element['#field_prefix']; + $suffix = $element['#format'] == 'html' ? filter_xss($element['#field_suffix']) : $element['#field_suffix']; + if ($element['#webform_component']['extra']['decimals'] != ''){ + $element['#value'] = _webform_format_number($element['#value'], $element['#webform_component']['extra']['decimals'], $element['#webform_component']['extra']['point'], $element['#webform_component']['extra']['separator']); + } + $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()) { + $placeholders = count($sids) ? array_fill(0, count($sids), "'%s'") : array(); + $sidfilter = count($sids) ? " AND sid in (" . implode(",", $placeholders) . ")" : ""; + $query = 'SELECT data ' . + ' FROM {webform_submitted_data} ' . + ' WHERE nid = %d ' . + ' AND cid = %d ' . $sidfilter; + + $population = array(); + $submissions = 0; + $nonzero = 0; + $sum = 0; + + $result = db_query($query, array_merge(array($component['nid'], $component['cid']), $sids)); + while ($data = db_fetch_array($result)) { + $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' => 'μ')); + // 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' => 'μ')); + // Population. + $population_count = $submissions; + $sigma = 'σ'; + $description = t('population'); + } + } + + // Formatting. + if ($component['extra']['decimals'] != '') { + $average = _webform_format_number($average, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + $sum = _webform_format_number($sum, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + } + + $rows[0] = array(t('Zero/blank'), ($submissions - $nonzero)); + $rows[1] = array(t('User entered value'), $submissions); + $rows[2] = array(t('Sum (!Sigma)', array('!Sigma' => 'Σ')), $sum); + $rows[3] = array($average_title, $average); + + // Normal distribution information. + if ($component['extra']['distribution'] && $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'), + t('-4!sigma', array('!sigma' => $sigma)), + t('-3!sigma', array('!sigma' => $sigma)), + t('-2!sigma', array('!sigma' => $sigma)), + t('-1!sigma', array('!sigma' => $sigma)), + t('+1!sigma', array('!sigma' => $sigma)), + t('+2!sigma', array('!sigma' => $sigma)), + t('+3!sigma', array('!sigma' => $sigma)), + t('+4!sigma', array('!sigma' => $sigma)), + ); + + // Output distribution table. + if ($component['extra']['decimals'] != '') { + $stddev = _webform_format_number($stddev, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + $low = _webform_format_number($population[0], $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + $high = _webform_format_number(end($population), $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + foreach($limit as $key => $value){ + $limit[$key] = _webform_format_number($value, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + } + } + 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', $header, $distrows, array('class' => 'webform-grid')); + + $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_format_number($value[0], $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + } + 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(). + * + * Thousands separator is excluded in export and decimal point is set to period. + * + */ +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['maximum'] != ''){ + form_error($element, t('Minimum is required when Maximum value is specified.')); + } + if ($values['step'] != ''){ + form_error($element, t('Minimum is required when using Step.')); + } + } + 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['minimum'] != ''){ + form_error($element, t('Maximum is required when Minimum value is specified.')); + } + if ($values['step'] != ''){ + form_error($element, t('Maximum is required when using Step.')); + } + } + else{ + if (!is_numeric($values['maximum'])) { + form_error($element, t('Maximum must be numeric.')); + } + if ($values['maximum'] == $values['minimum']) { + form_error($element, t('Maximum may not be equal to Minimum.')); + } + 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; + case 'extra[decimals]': + if ($values['decimals'] == '' && ($values['point'] != '' || $values['separator'] != '')) { + form_error($element, t('Decimals field is required.')); + } + else{ + if (!is_numeric($values['decimals'])) { + form_error($element, t('Decimals must be numeric.')); + } + else{ + if (!is_int($values['decimals'] * 1)) { + form_error($element, t('Decimals 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_format_number($value, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']); + } + } + + return $options; +} + +/** + * Apply number format. + */ +function _webform_format_number($value, $decimals, $point, $separator) { + $value = $value * 1.0; + return number_format($value, $decimals, $point, $separator); +} diff --git a/webform.module b/webform.module index cebb4c7..9c78b24 100644 --- a/webform.module +++ b/webform.module @@ -744,6 +744,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.'),