diff --git a/handlers/views_handler_relationship_groupwise_max.inc b/handlers/views_handler_relationship_groupwise_max.inc new file mode 100644 index 0000000..b331760 --- /dev/null +++ b/handlers/views_handler_relationship_groupwise_max.inc @@ -0,0 +1,378 @@ + array(NULL)); // TODO: Correct structure? + $options['subquery_order'] = array('default' => 'DESC'); // Descending more useful. + $options['subquery_regenerate'] = array('default' => FALSE); + $options['subquery_view'] = array('default' => FALSE); + $options['subquery_namespace'] = array('default' => FALSE); + + return $options; + } + + /** + * Extends the relationship's basic options, allowing the user to pick + * a sort and an order for it. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Get the sorts that apply to our base. + $sorts = views_fetch_fields($this->definition['base'], 'sort'); + foreach ($sorts as $sort_id => $sort) { + $options[$sort_id] = "$sort[group]: $sort[title]"; + } + + $form['subquery_sort'] = array( + '#type' => 'select', + '#title' => t('Representative sort criterion'), + '#default_value' => $this->options['subquery_sort'], + '#options' => $options, + '#description' => theme('advanced_help_topic', 'views', 'relationship-representative') . + t('This sort determines how the representative item is chosen. Eg, to show the most recent node for each term in a term view, select "Node: Post date".'), + ); + + $form['subquery_order'] = array( + '#type' => 'radios', + '#title' => t('Representative sort order'), + '#options' => array('ASC' => t('Ascending'), 'DESC' => t('Descending')), + '#default_value' => $this->options['subquery_order'], + ); + + $form['subquery_namespace'] = array( + '#type' => 'textfield', + '#title' => t('Subquery namespace'), + '#default_value' => $this->options['subquery_namespace'], + ); + + + // WIP: This stuff doens't work yet: namespacing issues. + // A list of suitable views to pick one as the subview. + $views = array('' => ''); + $all_views = views_get_all_views(); + foreach ($all_views as $view) { + // Only get views that are suitable: + // - base must the base that our relationship joins towards + // - must have fields. + if ($view->base_table == $this->definition['base'] && !empty($view->display['default']->display_options['fields'])) { + // TODO: check the field is the correct sort? + // or let users hang themselves at this stage and check later? + if ($view->type == 'Default') { + $views[t('Default Views')][$view->name] = $view->name; + } + else { + $views[t('Existing Views')][$view->name] = $view->name; + } + } + } + + $form['subquery_view'] = array( + '#type' => 'select', + '#title' => t('Representative view'), + '#default_value' => $this->options['subquery_view'], + '#options' => $views, + '#description' => t('Advanced. Use another view to generate the relationship subquery. This allows you to use filtering and more than one sort. If you pick a view here, the sort options above are ignored. Your view must have the ID of its base as its only field, and should have some kind of sorting.'), + ); + + $form['subquery_regenerate'] = array( + '#type' => 'checkbox', + '#title' => t('Generate subquery each time view is run.'), + '#default_value' => $this->options['subquery_regenerate'], + '#description' => t('Will re-generate the subquery for this relationship every time the view is run, instead of only when these options are saved. Use for testing if you are making changes elsewhere. WARNING: seriously impairs performance.'), + ); + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + * + * Generate the subquery string when the user submits the options, and store + * it. This saves the expense of generating it when the view is run. + */ + function options_submit($form, &$form_state) { + // Get the new user options from the form values. + $new_options = $form_state['values']['options']; + $subquery = $this->left_query($new_options); + // Add the subquery string to the options we're about to store. + $this->options['subquery_string'] = $subquery; + } + + /** + * Helper function to create a pseudo view with a namespace added. + */ + function view_aliased() { + views_include('view'); + $view = new view(); + $view->vid = 'new'; // @todo: what's this? + $view->base_table = $this->definition['base']; + $view->add_display('default'); + return $view; + } + /** + * Generate a subquery given the user options, as set in the options. + * These are passed in rather than picked up from the object because we + * generate the subquery when the options are saved, rather than when the view + * is run. This saves considerable time. + * + * @param $options + * An array of options: + * - subquery_sort: the id of a views sort. + * - subquery_order: either ASC or DESC. + * @return + * The subquery SQL string, ready for use in the main query. + */ + function left_query($options) { + dsm($options); + // Either load another view, or create one on the fly. + if ($options['subquery_view']) { + // We don't use views_get_view because we want our own class of view. + views_include('view'); + $temp_view = view::load($options['subquery_view']); + + // Remove all fields from default display + unset($temp_view->display['default']->display_options['fields']); + } + else { + // Create a new view object on the fly. + // We use this to generate a query from the chosen sort. + // TODO-470258: this feature is almost certainly not working!!! + $temp_view = $this->view_aliased(); + + // Add the sort from the options to the default display. + // THIS IS BROKEN! See workaround below. + // TODO-470258 + $sort = $options['subquery_sort']; + list($sort_table, $sort_field) = explode('.', $sort); + $sort_options = array('order' => $options['subquery_order']); + $temp_view->add_item('default', 'sort', $sort_table, $sort_field, $sort_options); + } + $temp_view->namespace = (!empty($options['subquery_namespace'])) ? '_'. $options['subquery_namespace'] : '_INNER'; + $this->subquery_namespace = (!empty($options['subquery_namespace'])) ? '_'. $options['subquery_namespace'] : 'INNER'; + + // The value we add here does nothing, but doing this adds the right tables + // and puts in a WHERE clause with a placeholder we can grab later. + $temp_view->args[] = '**CORRELATED**'; + + // Add the base table ID field. + $views_data = views_fetch_data($this->definition['base']); + $base_field = $views_data['table']['base']['field']; + $temp_view->add_item('default', 'field', $this->definition['base'], $this->definition['field']); + + // Add the correct argument for our relationship's base + // ie ehe 'how to get back to base' argument. + // The relationship definition tells us which one to use. + $temp_view->add_item( + 'default', + 'argument', + $this->definition['argument table'], // eg 'term_node', + $this->definition['argument field'] // eg 'tid' + ); + + // Build the view. The creates the query object and produces the query + // string but does not run any queries. + $temp_view->build(); + + // Now collect the query SQL string.. + + $subquery = $temp_view->build_info['query']; + //dsm($temp_view, 'temp view'); + //dsm($subquery, 'subq'); + + // Workaround until http://drupal.org/node/844910 is fixed: + // Remove all fields from the SELECT except the base id. + $fields =& $subquery->getFields(); + foreach (array_keys($fields) as $field_name) { + // The base id for this subquery is stored in our definition. + if ($field_name != $this->definition['field']) { + unset($fields[$field_name]); + } + } + + // Make every alias in the subquery safe within the outer query by + // appending a namespace to it, '_inner' by default. + $tables =& $subquery->getTables(); + foreach (array_keys($tables) as $table_name) { + $tables[$table_name]['alias'] .= $this->subquery_namespace; + // Namespace the join on every table. + if (isset($tables[$table_name]['condition'])) { + $tables[$table_name]['condition'] = $this->condition_namespace($tables[$table_name]['condition']); + } + } + // Namespace fields. + foreach (array_keys($fields) as $field_name) { + $fields[$field_name]['table'] .= $this->subquery_namespace; + $fields[$field_name]['alias'] .= $this->subquery_namespace; + } + // Namespace conditions. + $where =& $subquery->conditions(); + $substitutions= array( + 'taxonomy_index' => 'foo', + ); + //_views_query_tag_alter_condition($subquery, $where, $substitutions); + $this->alter_subquery_condition($subquery, $where, $substitutions); + // Not sure why, but our sort order clause doesn't have a table. + // TODO: the call to add_item() above to add the sort handler is probably + // wrong -- needs attention from someone who understands it. + // In the meantime, this works, but with a leap of faith... + $orders =& $subquery->getOrderBy(); + foreach ($orders as $order_key => $order) { + + $orders[$sort_table . $this->subquery_namespace . '.' . $sort_field] = $order; + unset($orders[$order_key]); + } + //dsm($order, 'os'); + + // The query we get doesn't include the LIMIT. + $subquery->range(0, 1); + + // Extract the SQL the temporary view built. + $subquery_sql = $subquery->__toString(); + dsm($subquery_sql); + + // Replace the placeholder with the outer, correlated field. + // We have to use preg_replace here; putting a name of a field into a + // SelectQuery that it does not recognize (because it's outer) just + // makes it treat that as a string. + $subquery_sql = preg_replace('/:db_condition_placeholder_\d+/', $this->definition['outer field'], $subquery_sql); + + dsm($subquery_sql); + return $subquery_sql; + } + + /** + * Recursive helper to add a namespace to conditions. + * + * Similar to _views_query_tag_alter_condition(). + * + * (Though why is the condition we get in a simple query 3 levels deep???) + */ + function alter_subquery_condition(QueryAlterableInterface $query, &$conditions, $substitutions) { + foreach ($conditions as $condition_id => &$condition) { + // Skip the #conjunction element. + if (is_numeric($condition_id)) { + if (is_string($condition['field'])) { + $condition['field'] = $this->condition_namespace($condition['field']); + } + elseif (is_object($condition['field'])) { + $sub_conditions =& $condition['field']->conditions(); + $this->alter_subquery_condition($query, $sub_conditions, $substitutions); + } + } + } + } + + /** + * Helper function to namespace query pieces. + * + * Turns 'foo.bar' into 'foo_NAMESPACE.bar'. + */ + function condition_namespace($string) { + return str_replace('.', $this->subquery_namespace . '.', $string); + } + + /** + * Called to implement a relationship in a query. + * This is mostly a copy of our parent's query() except for this bit with + * the join class. + */ + function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensure_my_table(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->table_alias; + $def['left_field'] = $this->field; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + if ($this->options['subquery_regenerate']) { + // For testing only, regenerate the subquery each time. + $def['left_query'] = $this->left_query($this->options); + } + else { + // Get the stored subquery SQL string. + $def['left_query'] = $this->options['subquery_string']; + } + + if (!empty($def['join_handler']) && class_exists($def['join_handler'])) { + $join = new $def['join_handler']; + } + else { + $join = new views_join_subquery(); + } + + $join->definition = $def; + $join->construct(); + $join->adjusted = TRUE; + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + } +} + diff --git a/includes/handlers.inc b/includes/handlers.inc index 511093f..754f080 100644 --- a/includes/handlers.inc +++ b/includes/handlers.inc @@ -1266,6 +1266,87 @@ function views_date_sql_extract($extract_type, $field, $field_type = 'int', $set } /** + * Join handler for relationships that join with a subquery as the left field. + * eg: + * LEFT JOIN node node_term_data ON ([YOUR SUBQUERY HERE]) = node_term_data.nid + * + * join definition + * same as views_join class above, except: + * - left_query: The subquery to use in the left side of the join clause. + */ +class views_join_subquery extends views_join { + // PHP 4 doesn't call constructors of the base class automatically from a + // constructor of a derived class. It is your responsibility to propagate + // the call to constructors upstream where appropriate. + function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') { + parent::construct($table, $left_table, $left_field, $field, $extra, $type); + $this->left_query = $this->definition['left_query']; + } + + /** + * Build the SQL for the join this object represents. + */ + function join($table, &$query) { + $output = " $this->type JOIN {" . $this->table . "} $table[alias] ON ($this->left_query) = $table[alias].$this->field"; + + // Tack on the extra. + if (isset($this->extra)) { + if (is_array($this->extra)) { + $extras = array(); + foreach ($this->extra as $info) { + $extra = ''; + // Figure out the table name. Remember, only use aliases provided + // if at all possible. + $join_table = ''; + if (!array_key_exists('table', $info)) { + $join_table = $table['alias'] . '.'; + } + elseif (isset($info['table'])) { + $join_table = $info['table'] . '.'; + } + + // And now deal with the value and the operator. Set $q to + // a single-quote for non-numeric values and the + // empty-string for numeric values, then wrap all values in $q. + $raw_value = $this->db_safe($info['value']); + $q = (empty($info['numeric']) ? "'" : ''); + + if (is_array($raw_value)) { + $operator = !empty($info['operator']) ? $info['operator'] : 'IN'; + // Transform from IN() notation to = notation if just one value. + if (count($raw_value) == 1) { + $value = $q . array_shift($raw_value) . $q; + $operator = $operator == 'NOT IN' ? '!=' : '='; + } + else { + $value = "($q" . implode("$q, $q", $raw_value) . "$q)"; + } + } + else { + $operator = !empty($info['operator']) ? $info['operator'] : '='; + $value = "$q$raw_value$q"; + } + $extras[] = "$join_table$info[field] $operator $value"; + } + + if ($extras) { + if (count($extras) == 1) { + $output .= ' AND ' . array_shift($extras); + } + else { + $output .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')'; + } + } + } + else if ($this->extra && is_string($this->extra)) { + $output .= " AND ($this->extra)"; + } + } + return $output; + } +} + +/** * @} */ diff --git a/modules/comment.views.inc b/modules/comment.views.inc index 4f64845..65e4b45 100644 --- a/modules/comment.views.inc +++ b/modules/comment.views.inc @@ -79,6 +79,25 @@ function comment_views_data() { ), ); + $data['comments']['cid_plain'] = array( + 'title' => t('CID - Plain'), + 'help' => t('The comment ID of the field. Does not add additional fields.'), + 'real field' => 'cid', + 'field' => array( + 'handler' => 'views_handler_field', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument', + ), + ); + + // name (of comment author) $data['comment']['name'] = array( 'title' => t('Author'), @@ -300,6 +319,9 @@ function comment_views_data() { 'help' => t('Provide a simple link to reply to the comment.'), 'handler' => 'views_handler_field_comment_link_reply', ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), ); $data['comment']['thread'] = array( diff --git a/modules/node.views.inc b/modules/node.views.inc index 6c9fb5d..ed1db82 100644 --- a/modules/node.views.inc +++ b/modules/node.views.inc @@ -596,6 +596,24 @@ function node_views_data() { 'handler' => 'views_handler_filter_history_user_timestamp', ), ); + + // ---------------------------------------------------------------------- + // Representative relationships + $data['node']['cid_representative'] = array( + 'relationship' => array( + 'real field' => 'cid', + 'title' => t('Representative comment'), + 'label' => t('Representative comment'), + 'help' => t('Obtains a single representative comment for each node, acccording to a chosen sort criterion, or by embedding a comment view.'), + 'handler' => 'views_handler_relationship_groupwise_max', + 'base' => 'comments', + 'field' => 'cid_plain', + 'outer field' => 'node.nid', + 'argument table' => 'comments', + 'argument field' => 'nid', + ), + ); + return $data; } diff --git a/modules/taxonomy.views.inc b/modules/taxonomy.views.inc index c8fc168..585f349 100644 --- a/modules/taxonomy.views.inc +++ b/modules/taxonomy.views.inc @@ -144,6 +144,17 @@ function taxonomy_views_data() { 'hierarchy table' => 'taxonomy_term_hierarchy', 'numeric' => TRUE, ), + 'relationship' => array( + 'title' => t('Representative node'), + 'label' => t('Representative node'), + 'help' => t('Obtains a single representative node for each term, acccording to a chosen sort criterion.'), + 'handler' => 'views_handler_relationship_groupwise_max', + 'base' => 'node', + 'field' => 'nid', + 'outer field' => 'term_data.tid', + 'argument table' => 'term_node', + 'argument field' => 'tid', + ), ); // Term name field diff --git a/modules/user.views.inc b/modules/user.views.inc index aa605d3..fe126f9 100644 --- a/modules/user.views.inc +++ b/modules/user.views.inc @@ -74,6 +74,22 @@ function user_views_data() { ); // uid + $data['users']['uid_representative'] = array( + 'real field' => 'uid', // TODO-470258: This does not seem to be working! + 'relationship' => array( + 'title' => t('Representative node'), + 'label' => t('Representative node'), + 'help' => t('Obtains a single representative node for each user, acccording to a chosen sort criterion.'), + 'handler' => 'views_handler_relationship_groupwise_max', + 'base' => 'node', + 'field' => 'nid', + 'outer field' => 'users.uid', + 'argument table' => 'users', + 'argument field' => 'uid', + ), + ); + + // uid $data['users']['uid_current'] = array( 'real field' => 'uid', 'title' => t('Current'), diff --git a/views.info b/views.info index 66122eb..d2d1d78 100644 --- a/views.info +++ b/views.info @@ -44,6 +44,7 @@ files[] = handlers/views_handler_filter_many_to_one.inc files[] = handlers/views_handler_filter_numeric.inc files[] = handlers/views_handler_filter_string.inc files[] = handlers/views_handler_relationship.inc +files[] = handlers/views_handler_relationship_groupwise_max.inc files[] = handlers/views_handler_sort.inc files[] = handlers/views_handler_sort_group_by_numeric.inc files[] = handlers/views_handler_sort_date.inc