diff --git a/includes/webform.components.inc b/includes/webform.components.inc index 9addf1c..a2c5738 100644 --- a/includes/webform.components.inc +++ b/includes/webform.components.inc @@ -854,6 +854,7 @@ 'group' => FALSE, 'attachment' => FALSE, 'private' => TRUE, + 'views_range' => FALSE, ); return isset($components[$type]['features'][$feature]) ? $components[$type]['features'][$feature] : !empty($defaults[$feature]); } diff --git a/views/webform.views.inc b/views/webform.views.inc index dab3fe3..65ed8ca 100644 --- a/views/webform.views.inc +++ b/views/webform.views.inc @@ -47,7 +47,7 @@ 'help' => t('Submissions generated from Webform forms.'), ); - // sid + // SID. $data['webform_submissions']['sid'] = array( 'title' => t('Sid'), 'help' => t('The submission ID of the submission.'), @@ -67,7 +67,18 @@ ), ); - // nid + // Submission data as a field loads the entire submission. + $data['webform_submissions']['value'] = array( + 'title' => t('Value'), + 'help' => t('The value of a component as submitted by a user.'), + 'real field' => 'sid', + 'group' => t('Webform submission data'), + 'field' => array( + 'handler' => 'webform_handler_field_submission_data', + ), + ); + + // NID. $data['webform_submissions']['nid'] = array( 'title' => t('Node'), 'help' => t('The webform node this submission was generated from.'), @@ -79,7 +90,7 @@ ), ); - // uid + // UID. $data['webform_submissions']['uid'] = array( 'title' => t('User'), 'help' => t('The user who sent the webform submission.'), @@ -91,7 +102,7 @@ ), ); - // is_draft + // Is draft. $data['webform_submissions']['is_draft'] = array( 'title' => t('Draft'), 'help' => t('Whether or not the submission is a draft.'), @@ -107,7 +118,7 @@ ), ); - // submitted + // Submitted timestamp. $data['webform_submissions']['submitted'] = array( 'title' => t('Submitted'), 'help' => t('The date this submission was submitted.'), @@ -123,8 +134,12 @@ 'handler' => 'views_handler_sort_date', ), ); + // Add date module support, if present. + if (module_exists('date')) { + $data['webform_submissions']['submitted']['filter']['handler'] = 'date_views_filter_handler_simple'; + } - // remote_addr + // IP Address (remote_addr). $data['webform_submissions']['remote_addr'] = array( 'title' => t('Remote address'), 'help' => t('The remote IP address of the user that submitted this submission.'), @@ -141,7 +156,7 @@ ), ); - // view link + // View submission link. $data['webform_submissions']['view_submission'] = array( 'field' => array( 'title' => t('View link'), @@ -151,7 +166,7 @@ ), ); - // edit link + // Edit submission link. $data['webform_submissions']['edit_submission'] = array( 'field' => array( 'title' => t('Edit link'), @@ -161,13 +176,75 @@ ), ); - // delete link + // Delete submission link. $data['webform_submissions']['delete_submission'] = array( 'field' => array( 'title' => t('Delete link'), 'help' => t('Provide a simple link to delete the submission.'), 'handler' => 'webform_handler_field_submission_link', 'link_type' => 'delete', + ), + ); + + // Relation to webform_submitted_data table. + $data['webform_submissions']['data'] = array( + 'title' => t('Data'), + 'help' => t('Relates to a webform submission data'), + 'real field' => 'sid', + 'relationship' => array( + 'handler' => 'webform_handler_relationship_submission_data', + 'base' => 'webform_submitted_data', + 'base field' => 'sid', + 'label' => t('Submission Data'), + ), + ); + + /** + * Submission data table definitions. + */ + $data['webform_submitted_data']['table']['group'] = t('Webform submission data'); + + // Raw access to the submitted values. This usually will only be used for + // sorts and filters, since the 'value' field for the submission will often + // be faster and easier to configure than the raw values. + $data['webform_submitted_data']['data'] = array( + 'table' => 'webform_submitted_data', + 'title' => t('Data field'), + 'help' => t('The value of a component as submitted by a user.'), + 'real field' => 'data', + 'field' => array( + 'title' => t('Value (raw)'), // Distinguish from the normal value handler. + 'help' => t('The raw value from the database as submitted by a user. Use only when needing to sort on a field value.'), + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + 'filter' => array( + 'handler' => 'webform_handler_filter_submission_data', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // Number field for multi-value fields. + $data['webform_submitted_data']['no'] = array( + 'title' => t('Value delta'), + 'help' => t('The delta value of the submitted data in a multi value component (such as checkboxes).'), + 'real field' => 'no', + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', ), ); @@ -273,6 +350,10 @@ 'parent' => 'views_handler_field', 'file' => 'webform_handler_field_submission_link.inc', ), + 'webform_handler_field_submission_data' => array( + 'parent' => 'views_handler_field', + 'file' => 'webform_handler_field_submission_data.inc', + ), 'webform_handler_field_submission_count' => array( 'parent' => 'views_handler_field', 'file' => 'webform_handler_field_submission_count.inc', @@ -303,6 +384,115 @@ 'parent' => 'views_handler_filter_boolean_operator', 'file' => 'webform_handler_filter_webform_status.inc', ), + 'webform_handler_relationship_submission_data' => array( + 'parent' => 'views_handler_relationship', + 'file' => 'webform_handler_relationship_submission_data.inc', + ), ), ); -} \ No newline at end of file +} + +/** + * Menu callback; Provide a list of Webform nodes for use in autocomplete. + */ +function webform_views_autocomplete($string = '') { + $matches = array(); + if ($string) { + $or = db_or(); + + // Strings with nid: in them can be used as direct matches. + $matches = array(); + if (preg_match('/nid:([0-9]+)/', $string, $matches)) { + $or->condition('n.nid', (int) $matches[1]); + } + // Otherwise match on title and optionally indirect NIDs. + else { + $or->condition('n.title', '%' . db_like($string) . '%', 'LIKE'); + if (is_numeric($string)) { + $or->condition('n.nid', (int) $string); + } + } + + $options = array(); + $query = db_select('node', 'n') + ->fields('n', array('nid', 'title')) + ->condition($or); + $query->innerJoin('webform', 'w', 'w.nid = n.nid'); + $result = $query + ->range(0, 10) + ->execute(); + foreach ($result as $node) { + $options[$node->title . ' [nid:' . $node->nid . ']'] = check_plain($node->title) . ' [nid:' . $node->nid . ']'; + } + } + + drupal_json_output($options); +} + +/** + * Shared form for the Webform submission data field and relationship handler. + */ +function _webform_views_options_form(&$form, &$form_state, $nid, $cid) { + form_load_include($form_state, 'inc', 'webform', 'includes/webform.components'); + $node = $nid ? node_load($nid) : NULL; + + $form['webform_nid'] = array( + '#type' => 'textfield', + '#title' => t('Webform node'), + '#default_value' => isset($node) ? $node->title . ' [nid:' . $node->nid . ']' : '', + '#ajax' => array( + 'path' => views_ui_build_form_url($form_state), + 'event' => 'blur', + ), + '#autocomplete_path' => 'webform/autocomplete', + '#description' => t('Enter the title or NID of the Webform whose values should be made available.'), + '#submit' => array('views_ui_config_item_form_submit_temporary'), + '#executes_submit_callback' => TRUE, + ); + + $components = array(); + if (isset($node->webform['components'])) { + $components = $node->webform['components']; + } + + $type_options = array(); + foreach (webform_components() as $key => $component) { + $type_options[$key] = check_plain($component['label']); + } + + $options = webform_component_list($node, NULL, TRUE , TRUE); + $form['webform_cid'] = array( + '#title' => t('Component data'), + '#type' => 'select', + '#options' => $options, + '#default_value' => $cid, + '#dependency' => array('edit-options-component-type' => array($type)), + '#access' => count($components), + '#description' => t('Select the component whose values should be made available.'), + ); +} + +/** + * Validate handler for webform_views_options_form(). + */ +function _webform_views_options_validate(&$form, &$form_state) { + // Just store the checked components of the selected type. + if (empty($form_state['values']['options']['webform_nid'])) { + form_error($form['webform_nid'], t('Webform NID is required.')); + } + else { + $nid = preg_replace('/^.*?nid:([0-9]+).*?$/', '$1', $form_state['values']['options']['webform_nid']); + if (!($nid && ($node = node_load($nid)) && !empty($node->webform['components']))) { + form_error($form['webform_nid'], t('The specified node is not valid.')); + } + } +} + +/** + * Submit handler for webform_views_options_form(). + */ +function _webform_views_options_submit(&$form, &$form_state) { + // Save the NID as just the number instead of the title. + $nid = preg_replace('/^.*?nid:([0-9]+).*?$/', '$1', $form_state['values']['options']['webform_nid']); + $form_state['values']['options']['webform_nid'] = $nid; +} diff --git a/views/webform_handler_field_submission_data.inc b/views/webform_handler_field_submission_data.inc new file mode 100644 index 0000000..af9dd56 --- /dev/null +++ b/views/webform_handler_field_submission_data.inc @@ -0,0 +1,135 @@ + 'html'); + $options['custom_label'] = array('default' => 'default'); + $options['webform_nid'] = array('default' => NULL); + $options['webform_cid'] = array('default' => NULL); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + form_load_include($form_state, 'inc', 'webform', 'views/webform.views'); + + $form['custom_label']['#type'] = 'radios'; + $form['custom_label']['#options'] = array( + 'default' => t('Use component label'), + 'custom' => t('Custom label'), + 'none' => t('No label'), + ); + $form['custom_label']['#default_value'] = $this->options['custom_label']; + $form['label']['#dependency'] = array('radio:options[custom_label]' => array('custom')); + + $nid = (int) $this->options['webform_nid']; + $cid = (int) $this->options['webform_cid']; + + // Helper function provides webform_nid and webform_cid options. + _webform_views_options_form($form, $form_state, $nid, $cid); + + // Provide the selection for the display format. + $form['format'] = array( + '#type' => 'select', + '#title' => t('Display format'), + '#options' => array( + 'html' => t('HTML'), + 'plain' => t('Plain text'), + ), + '#default_value' => $this->options['format'], + ); + } + + function options_validate(&$form, &$form_state) { + parent::options_validate($form, $form_state); + _webform_views_options_validate($form, $form_state); + } + + function options_submit(&$form, &$form_state) { + parent::options_submit($form, $form_state); + _webform_views_options_submit($form, $form_state); + } + + /** + * Load the node and submissions needed for this components values. + */ + function pre_render(&$values) { + $nid = $this->options['webform_nid']; + $this->webform_node = node_load($nid); + // Load all the submissions needed for this page. This is stored at the + // view level to ensure it's available between fields so we don't load + // them twice. + if (!isset($this->view->_webform_submissions[$nid])) { + module_load_include('inc', 'webform', 'includes/webform.submissions'); + $this->view->_webform_submissions[$nid] = array(); + $sids = array(); + foreach ($values as $value) { + $sids[] = $value->{$this->field_alias}; + } + if ($sids) { + $this->view->_webform_submissions[$nid] = webform_get_submissions(array('sid' => $sids)); + } + } + } + + /** + * Get this field's label based on the selected component. + */ + function label() { + if ($this->options['custom_label'] === 'default' && isset($this->options['webform_cid'])) { + if (isset($this->webform_node)) { + $node = $this->webform_node; + } + else { + $node = node_load($this->options['webform_nid']); + } + if ($node && isset($node->webform['components'][$this->options['webform_cid']])) { + $component = $node->webform['components'][$this->options['webform_cid']]; + return $component['name']; + } + } + elseif (isset($this->options['label'])) { + return $this->options['label']; + } + return ''; + } + + /** + * Render the field using the loaded submissions from pre_render(). + */ + function render($row) { + $sid = $this->get_value($row); + if (isset($sid)) { + $nid = $this->options['webform_nid']; + $cid = $this->options['webform_cid']; + + $component = $this->webform_node->webform['components'][$cid]; + $submission = $this->view->_webform_submissions[$nid][$sid]; + + $render = array(); + $format = $this->options['format']; + + _webform_client_form_add_component($this->webform_node, $component, NULL, $render, $render, $submission->data, $format); + $render = $render[$component['form_key']]; + + // Remove display label. + if (empty($this->options['display_label'])) { + $render['#theme_wrappers'] = array(); + } + + return render($render); + } + } +} diff --git a/views/webform_handler_filter_submission_data.inc b/views/webform_handler_filter_submission_data.inc new file mode 100644 index 0000000..9643e07 --- /dev/null +++ b/views/webform_handler_filter_submission_data.inc @@ -0,0 +1,91 @@ +'] = array( + 'title' => t('Greater than'), + 'short' => t('>'), + 'method' => 'op_greater_than', + 'values' => 1, + ); + $operators['<'] = array( + 'title' => t('Less than'), + 'short' => t('>'), + 'method' => 'op_greater_than', + 'values' => 1, + ); + + return $operators; + } + + /** + * Build strings from the operators() for 'select' options + */ + function operator_options($which = 'title') { + $options = parent::operator_options($which); + + // Adjust the exposed filter options based on the component selected. + if ($which === 'title') { + $nid = $this->view->relationship[$this->options['relationship']]->options['webform_nid']; + $cid = $this->view->relationship[$this->options['relationship']]->options['webform_cid']; + + if ($nid && $node = $node = node_load($nid)) { + $component = $node->webform['components'][$cid]; + if (webform_component_feature($component['type'], 'views_range')) { + $options['='] = t('Is'); + $options['!='] = t('Is not'); + $options['>'] = t('After'); + $options['<'] = t('Before'); + $options = array_intersect_key($options, array('=' => '=', '!=' => '!=', '>' => '>', '<' => '<')); + } + } + } + + return $options; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if (isset($info['values']) && $info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + // TODO: Adjust the exposed filter form based on component form. + return parent::value_form(&$form, &$form_state); + } + + function op_greater_than($field) { + $this->query->add_where($this->options['group'], $field, $this->value, '>'); + } + + function op_less_than($field) { + $this->query->add_where($this->options['group'], $field, $this->value, '<'); + } +} diff --git a/views/webform_handler_relationship_submission_data.inc b/views/webform_handler_relationship_submission_data.inc new file mode 100644 index 0000000..43df8f4 --- /dev/null +++ b/views/webform_handler_relationship_submission_data.inc @@ -0,0 +1,59 @@ + NULL); + $options['webform_cid'] = array('default' => NULL); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + form_load_include($form_state, 'inc', 'webform', 'views/webform.views'); + + $nid = (int) $this->options['webform_nid']; + $cid = (int) $this->options['webform_cid']; + + // Helper function provides webform_nid and webform_cid options. + _webform_views_options_form($form, $form_state, $nid, $cid); + } + + function options_validate(&$form, &$form_state) { + parent::options_validate($form, $form_state); + _webform_views_options_validate($form, $form_state); + } + + function options_submit(&$form, &$form_state) { + parent::options_submit($form, $form_state); + _webform_views_options_submit($form, $form_state); + } + + /** + * Called to implement a relationship in a query. + * + * It respects the given component ids, provided via options form. + */ + function query() { + $this->definition['extra'] = array( + array( + 'table' => NULL, + 'field' => "%alias.nid", + 'value' => $this->options['webform_nid'], + ), + array( + 'table' => NULL, + 'field' => "%alias.cid", + 'value' => $this->options['webform_cid'], + ) + ); + + // The rest of building the join is performed by the parent. + parent::query(); + } +} diff --git a/webform.api.php b/webform.api.php index 149d869..985c0f2 100644 --- a/webform.api.php +++ b/webform.api.php @@ -408,6 +408,10 @@ // If this component saves a file that can be used as an e-mail // attachment. Defaults to FALSE. 'attachment' => FALSE, + + // If this component reflects a time range and should use labels such as + // "Before" and "After" when exposed as filters in Views module. + 'views_range' => FALSE, ), 'file' => 'components/textfield.inc', ); diff --git a/webform.info b/webform.info index 12ae28a..a11f1c0 100644 --- a/webform.info +++ b/webform.info @@ -13,10 +13,13 @@ files[] = views/webform_handler_field_node_link_edit.inc files[] = views/webform_handler_field_node_link_results.inc files[] = views/webform_handler_field_submission_count.inc +files[] = views/webform_handler_field_submission_data.inc files[] = views/webform_handler_field_submission_link.inc files[] = views/webform_handler_field_webform_status.inc files[] = views/webform_handler_filter_is_draft.inc +files[] = views/webform_handler_filter_submission_data.inc files[] = views/webform_handler_filter_webform_status.inc +files[] = views/webform_handler_relationship_submission_data.inc files[] = views/webform.views.inc files[] = tests/components.test diff --git a/webform.module b/webform.module index b716c90..973fd65 100644 --- a/webform.module +++ b/webform.module @@ -112,6 +112,15 @@ 'type' => MENU_NORMAL_ITEM, ); + // Autocomplete used in Views integration. + $items['webform/autocomplete'] = array( + 'title' => 'Webforms', + 'page callback' => 'webform_views_autocomplete', + 'access arguments' => 'administer views', + 'file' => 'views/webform.views.inc', + 'type' => MENU_CALLBACK, + ); + // Node page tabs. $items['node/%webform_menu/done'] = array( 'title' => 'Webform confirmation', @@ -838,6 +847,7 @@ 'label' => t('Date'), 'description' => t('Presents month, day, and year fields.'), 'features' => array( + 'views_range' => TRUE, ), 'file' => 'components/date.inc', 'conditional_type' => 'date', @@ -957,6 +967,7 @@ 'label' => t('Time'), 'description' => t('Presents the user with hour and minute fields. Optional am/pm fields.'), 'features' => array( + 'views_range' => TRUE, ), 'file' => 'components/time.inc', 'conditional_type' => 'time',