? .DS_Store
? views_groupwise_max_1.patch
Index: handlers/views_handler_relationship_groupwise_max.inc
===================================================================
RCS file: handlers/views_handler_relationship_groupwise_max.inc
diff -N handlers/views_handler_relationship_groupwise_max.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ handlers/views_handler_relationship_groupwise_max.inc	15 Jun 2009 21:14:50 -0000
@@ -0,0 +1,266 @@
+<?php
+// $Id$
+/*
+ * @file
+ * Relationship for groupwise maximum handler.
+ */
+
+/**
+ * Relationship handler that allows a groupwise maximum of the linked in table. 
+ * For a definition, see: 
+ * http://dev.mysql.com/doc/refman/5.0/en/example-maximum-column-group-row.html
+ * In lay terms, instead of joining to get all matching records in the linked 
+ * table, we get only one record, a 'representative record' picked according
+ * to a given criterion.
+ * 
+ * Example:
+ * Suppose we have a term view that gives us the terms: Horse, Cat, Aardvark.
+ * We wish to show for each term the most recent node of that term.
+ * We want a relationship from term to node.
+ * However a regular relationship will give us all the nodes for each term, 
+ * adding rows to the view. We want one representative node per term, 
+ * the node that is the 'best' in some way:
+ * eg, the most recent, the most commented on, the first in alphabetical order.
+ * 
+ * This handler gives us that kind of relationship from term to node.
+ * The method of choosing the 'best' implemented with a sort
+ * that the user selects in the relationship settings. 
+ * 
+ * So if we want our term view to show the most commented node for each term, 
+ * add the relationship and in its options, pick the 'Comment count' sort.
+ * 
+ * Relationship definition
+ *  - 'outer field': The field to substitute into the correlated subquery.
+ *       This must be the full field name, not the alias. 
+ *       Eg: 'term_data.tid'.
+ *  - 'argument table',
+ *    'argument field': These options define the argument that the subquery
+ *     must add to itself to filter by the main view.
+ *     Example: the main view shows terms, this handler is being used to get to
+ *     the nodes base table. Your argument must be 'term_node', 'tid', as this 
+ *     is the argument that should be added to a node view to filter on terms.
+ * 
+ * A note on performance:
+ * This relationship uses a correlated subquery, which is expensive.
+ * Subsequent versions of this handler could also implement the alternative way 
+ * of doing this, with a join -- though this looks like it could be pretty messy
+ * to implement. This is also an expensive method, so providing both methods and
+ * allowing the user to choose which one works fastest for their data might be 
+ * the best way.
+ * If your use of this relationship handler is likely to result in large 
+ * data sets, you might want to consider storing statistics in a separate table,
+ * in the same way as node_comment_statistics.
+ */
+class views_handler_relationship_groupwise_max extends views_handler_relationship {
+  
+  /**
+   * Defines default values for options.
+   */
+  function option_definition() {
+    $options = parent::option_definition();
+
+    $options['subquery_sort'] = array('default' => array(NULL)); // TODO: Correct structure?
+    $options['subquery_order'] = array('default' => 'DESC'); // Descending more useful.
+
+    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'],
+    );        
+  } 
+   
+  /**
+   * 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;
+  }
+   
+  /**
+   * 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) {
+    // Create a new view object on the fly.
+    // We use this to generate a query from the chosen sort.
+    $temp_view = views_new_view();
+    // Add the sort from the options to the default display.
+    $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);
+        
+    // Add the correct argument for our relationship's base
+    // ie the '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'
+    );
+    
+    // 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**';
+
+    //dsm($temp_view);
+
+    
+    // Build the view. The creates the query object and produces the query 
+    // string but does not run any queries.
+    $temp_view->build();
+    
+    //dsm($temp_view);
+    
+    // Now collect the query SQL string.
+    $subquery = $temp_view->build_info['query'];
+    //dsm($subquery);
+    
+    return $this->subquery_process($subquery);    
+  }
+  
+  /**
+   * Takes a query string generated by a view and alters it so it can be used
+   * as a valid subquery.
+   */
+  function subquery_process($subquery) {
+    /**
+     * We have a query of the form:
+     * SELECT node.nid AS nid,
+     *    comments.timestamp AS comments_timestamp
+     *  FROM {node} node 
+     *  INNER JOIN {term_node} term_node ON node.vid = term_node.vid
+     *  LEFT JOIN {comments} comments ON node.nid = comments.nid
+     *  WHERE term_node.tid = %d
+     *    ORDER BY comments_timestamp ASC
+     */   
+     
+    // Subqueries may only have one column, so we must remove the second column 
+    // in the subquery. Because the second column's alias is used by the ORDER
+    // clause, we extract the second column in the callback so we can use it
+    // to change the ORDER clause.
+    $subquery = preg_replace_callback('/,\s+(\w+)\.(\w+) AS (\w+)(?=\s+FROM)/', array(&$this,
+'subquery_extract'), $subquery);
+    // Replace the alias for the sort field with the real name.
+    if ($this->subquery[alias]) {
+      $subquery = preg_replace(
+        '/' . $this->subquery[alias] . '/',
+        $this->subquery[table] . '.' . $this->subquery[field],
+        $subquery
+      );
+    }
+    
+    // Add in the outer field.
+    $subquery = preg_replace('/%d/', $this->definition['outer field'], $subquery);
+
+    // The query we get doesn't include the LIMIT.
+    // TODO: Is there a better way than adding it by hand?
+    $subquery .= ' LIMIT 1';
+        
+    //dsm($subquery);    
+    return $subquery;
+  }
+  
+  /**
+   * Preg_replace callback to extract and process the second column from the 
+   * subquery.
+   */
+  function subquery_extract($matches) {
+    // Store the things we want to extract in this handler for retrieval 
+    // back at the ranch.
+    $this->subquery['table'] = $matches[1];
+    $this->subquery['field'] = $matches[2];
+    $this->subquery['alias'] = $matches[3];
+    //dsm($matches);
+    // Return nothing as we want to remove everything we've matched.
+    return '';
+  }
+    
+  /**
+   * 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';
+    }
+    
+    // Get the stored subquery SQL string.
+    $def['left_query'] = $this->options['subquery_string'];
+    
+    // For testing only, regenerate the subquery each time.
+    $def['left_query'] = $this->left_query($this->options);
+
+    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);
+  }
+}
Index: help/relationship-representative.html
===================================================================
RCS file: help/relationship-representative.html
diff -N help/relationship-representative.html
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ help/relationship-representative.html	15 Jun 2009 21:14:50 -0000
@@ -0,0 +1,15 @@
+<!-- $Id$ -->
+A representative relationship obtains just one object from the linked in objects. 
+
+This is best explained with an example. Suppose you have a term view that shows you the terms in your vocabulary: Horse, Cat, Aardvark.
+In addition to the term names, you want to show the title of the most recent node in that term. This would give you a view that shows: Horse - latest horse node title, Cat - latest cat node title, Aardvark - latest aardvark node title.
+Each of these is the title of the <em>representative node</em> for that term, chosen by creation time.
+
+Any sort criterion can be used to choose the representative node. You might instead want to show the node with the most comments for each term, the first node in alphabetical order, or if you have a voting module installed, the most popular node.
+
+The options for a representative relationship let you choose a sort criterion and a sort order. This determines how the representative object is chosen: the first object returned by the sort is shown. For example, choose 'Node: title' and 'Ascending' to get the first node by title as the representative; or 'Node: comment count' and 'Descending' to get the node with the most comments.
+
+<h2>Performance</h2>
+These relationships require a correlated subquery. This can be slow to run on large amounts of data, as the subquery must be run for every row of the main query.
+
+For more on this topic, see the <a href="http://dev.mysql.com/doc/refman/5.0/en/example-maximum-column-group-row.html">MySQL tutorial on group-wise maximum queries</a>.
Index: help/views.help.ini
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/help/views.help.ini,v
retrieving revision 1.17
diff -u -p -r1.17 views.help.ini
--- help/views.help.ini	22 Apr 2009 07:03:35 -0000	1.17
+++ help/views.help.ini	15 Jun 2009 21:14:50 -0000
@@ -80,6 +80,10 @@ parent = about
 title = "Relationships"
 parent = about
 
+[relationship-representative]
+title = "Representative relationships"
+parent = relationship
+
 [style]
 title = "Output styles (View styles)"
 weight = -20 
Index: includes/handlers.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/handlers.inc,v
retrieving revision 1.114
diff -u -p -r1.114 handlers.inc
--- includes/handlers.inc	4 Jun 2009 20:20:45 -0000	1.114
+++ includes/handlers.inc	15 Jun 2009 21:14:52 -0000
@@ -1105,6 +1105,9 @@ function views_views_handlers() {
       'views_handler_relationship' => array(
         'parent' => 'views_handler',
       ),
+      'views_handler_relationship_groupwise_max' => array(
+        'parent' => 'views_handler_relationship',
+      ),
 
 
       // sort handlers
@@ -1310,6 +1313,88 @@ class views_join {
 }
 
 /**
+ * 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;
+  }
+}
+
+/**
  * @}
  */
 
Index: modules/taxonomy.views.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/modules/taxonomy.views.inc,v
retrieving revision 1.54
diff -u -p -r1.54 taxonomy.views.inc
--- modules/taxonomy.views.inc	5 Jun 2009 01:26:35 -0000	1.54
+++ modules/taxonomy.views.inc	15 Jun 2009 21:14:52 -0000
@@ -113,8 +113,19 @@ function taxonomy_views_data() {
       'numeric' => TRUE,
       'skip base' => array('node', 'node_revision'),
     ),
+    '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
   $data['term_data']['name'] = array(
     'title' => t('Term'),
