From 4235569a2667b8a225de724e48928d34145a77f7 Mon Sep 17 00:00:00 2001
From: Fabian Sorqvist <fabian.sorqvist@gmail.com>
Date: Wed, 2 May 2012 14:26:39 +0200
Subject: [PATCH] Issue #477338 by casey, dawehner | Flying Drupalist: Added
 Multiple group: group by more than one field.

---
 plugins/views_plugin_style.inc    |  163 ++++++++++++++++++++++++++++---------
 theme/theme.inc                   |   22 +++++-
 theme/views-view-grouping.tpl.php |   22 +++++
 views.module                      |    4 +
 4 files changed, 173 insertions(+), 38 deletions(-)
 create mode 100644 theme/views-view-grouping.tpl.php

diff --git a/plugins/views_plugin_style.inc b/plugins/views_plugin_style.inc
index 385f3a5..e9e41bd 100644
--- a/plugins/views_plugin_style.inc
+++ b/plugins/views_plugin_style.inc
@@ -52,7 +52,7 @@ class views_plugin_style extends views_plugin {
     }
 
     $this->options += array(
-      'grouping' => '',
+      'grouping' => array(),
     );
 
     $this->definition += array(
@@ -156,7 +156,7 @@ class views_plugin_style extends views_plugin {
 
   function option_definition() {
     $options = parent::option_definition();
-    $options['grouping'] = array('default' => '');
+    $options['grouping'] = array('default' => array());
     if ($this->uses_row_class()) {
       $options['row_class'] = array('default' => '');
     }
@@ -175,13 +175,24 @@ class views_plugin_style extends views_plugin {
 
       // If there are no fields, we can't group on them.
       if (count($options) > 1) {
-        $form['grouping'] = array(
-          '#type' => 'select',
-          '#title' => t('Grouping field'),
-          '#options' => $options,
-          '#default_value' => $this->options['grouping'],
-          '#description' => t('You may optionally specify a field by which to group the records. Leave blank to not group.'),
-        );
+        // This is for backward compability, when there was just a single select form.
+        if (is_string($this->options['grouping'])) {
+          $grouping = $this->options['grouping'];
+          $this->options['grouping'] = array();
+          $this->options['grouping'][0]['field'] = $grouping;
+        }
+        $c = count($this->options['grouping']);
+        // Add a form for every grouping, plus one.
+        for ($i = 0; $i <= $c; $i++) {
+          $grouping = !empty($this->options['grouping'][$i]) ? $this->options['grouping'][$i] : array('field' => '');
+          $form['grouping'][$i]['field'] = array(
+            '#type' => 'select',
+            '#title' => t('Grouping field Nr.@number', array('@number' => $i + 1)),
+            '#options' => $options,
+            '#default_value' => $grouping['field'],
+            '#description' => t('You may optionally specify a field by which to group the records. Leave blank to not group.'),
+          );
+        }
       }
     }
 
@@ -199,6 +210,17 @@ class views_plugin_style extends views_plugin {
     }
   }
 
+  function options_validate(&$form, &$form_state) {
+    // Don't save grouping if no field is specified.
+    foreach ($form_state['values']['style_options']['grouping'] as $index => $grouping) {
+      if (empty($grouping['field'])) {
+        unset($form_state['values']['style_options']['grouping'][$index]);
+      }
+    }
+  }
+
+
+
   /**
    * Called by the view builder to see if this style handler wants to
    * interfere with the sorts. If so it should build; if it returns
@@ -233,25 +255,43 @@ class views_plugin_style extends views_plugin {
       return;
     }
 
-    // Group the rows according to the grouping field, if specified.
+    // Group the rows according to the grouping instructions, if specified.
     $sets = $this->render_grouping($this->view->result, $this->options['grouping']);
+    return $this->render_grouping_sets($sets);
+  }
 
-    // Render each group separately and concatenate.  Plugins may override this
-    // method if they wish some other way of handling grouping.
+  /**
+   * Render the grouping sets.
+   *
+   * Plugins may override this method if they wish some other way of handling
+   * grouping.
+   *
+   * @param $sets
+   *   Array containing the grouping sets to render.
+   * @param $level
+   *   Integer indicating the hierarchical level of the grouping.
+   *
+   * @return string
+   *   Rendered output of given grouping sets.
+   */
+  function render_grouping_sets($sets, $level = 0) {
     $output = '';
-    foreach ($sets as $title => $records) {
-      if ($this->uses_row_plugin()) {
-        $rows = array();
-        foreach ($records as $row_index => $row) {
-          $this->view->row_index = $row_index;
-          $rows[$row_index] = $this->row_plugin->render($row);
-        }
+    foreach ($sets as $set) {
+      $row = reset($set['rows']);
+      // Render as a grouping set.
+      if (is_array($row) && isset($row['group'])) {
+        $output .= theme(views_theme_functions('views_view_grouping', $this->view, $this->display), $this->view, $this->options['grouping'][$level], $level, $set['rows'], $set['group']);
       }
+      // Render as a record set.
       else {
-        $rows = $records;
+        if ($this->uses_row_plugin()) {
+          foreach ($set['rows'] as $index => $row) {
+            $this->view->row_index = $index;
+            $set['rows'][$index] = $this->row_plugin->render($row);
+          }
+        }
+        $output .= theme($this->theme_functions(), $this->view, $this->options, $set['rows'], $set['group'], $level);
       }
-
-      $output .= theme($this->theme_functions(), $this->view, $this->options, $rows, $title);
     }
     unset($this->view->row_index);
     return $output;
@@ -262,34 +302,83 @@ class views_plugin_style extends views_plugin {
    *
    * @param $records
    *   An array of records from the view to group.
-   * @param $grouping_field
-   *   The field id on which to group.  If empty, the result set will be given
-   *   a single group with an empty string as a label.
+   * @param $groupings
+   *   An array of grouping instructions on which fields to group. If empty, the
+   *   result set will be given a single group with an empty string as a label.
    * @return
    *   The grouped record set.
+   *   A nested set structure is generated if multiple grouping fields are used.
+   *
+   *   @code
+   *   array(
+   *     'grouping_field_1:grouping_1' => array(
+   *       'group' => 'grouping_field_1:content_1',
+   *       'rows' => array(
+   *         'grouping_field_2:grouping_a' => array(
+   *           'group' => 'grouping_field_2:content_a',
+   *           'rows' => array(
+   *             $row_index_1 => $row_1,
+   *             $row_index_2 => $row_2,
+   *             // ...
+   *           )
+   *         ),
+   *       ),
+   *     ),
+   *     'grouping_field_1:grouping_2' => array(
+   *       // ...
+   *     ),
+   *   )
+   *   @endcode
    */
-  function render_grouping($records, $grouping_field = '') {
+  function render_grouping($records, $groupings = array()) {
+    // This is for backward compability, when $groupings was a string containing
+    // the ID of a single field.
+    if (is_string($groupings)) {
+      $rendered = $group_rendered === NULL ? TRUE : $group_rendered;
+      $groupings = array(array('field' => $groupings, 'rendered' => $rendered));
+    }
+
     // Make sure fields are rendered
     $this->render_fields($this->view->result);
     $sets = array();
-    if ($grouping_field) {
+    if ($groupings) {
       foreach ($records as $index => $row) {
-        $grouping = '';
-        // Group on the rendered version of the field, not the raw.  That way,
-        // we can control any special formatting of the grouping field through
-        // the admin or theme layer or anywhere else we'd like.
-        if (isset($this->view->field[$grouping_field])) {
-          $grouping = $this->get_field($index, $grouping_field);
-          if ($this->view->field[$grouping_field]->options['label']) {
-            $grouping = $this->view->field[$grouping_field]->options['label'] . ': ' . $grouping;
+        // Iterate through configured grouping fields to determine the
+        // hierarchically positioned set where the current row belongs to.
+        // While iterating, parent groups, that do not exist yet, are added.
+        $set = &$sets;
+        foreach ($groupings as $info) {
+          $field = $info['field'];
+          $grouping = '';
+          $group_content = '';
+          // Group on the rendered version of the field, not the raw.  That way,
+          // we can control any special formatting of the grouping field through
+          // the admin or theme layer or anywhere else we'd like.
+          if (isset($this->view->field[$field])) {
+            $group_content = $this->get_field($index, $field);
+            if ($this->view->field[$field]->options['label']) {
+              $group_content = $this->view->field[$field]->options['label'] . ': ' . $group_content;
+            }
           }
+          $grouping = $group_content;
+          // Create the group if it does not exist yet.
+          if (empty($set[$grouping])) {
+            $set[$grouping]['group'] = $group_content;
+            $set[$grouping]['rows'] = array();
+          }
+          // Move the set reference into the row set of the group we just determined.
+          $set = &$set[$grouping]['rows'];
         }
-        $sets[$grouping][$index] = $row;
+        // Add the row to the hierarchically positioned row set we just determined.
+        $set[$index] = $row;
       }
     }
     else {
       // Create a single group with an empty grouping field.
-      $sets[''] = $records;
+      $sets = array(
+        'group' => '',
+        'rows' => $records,
+      );
     }
     return $sets;
   }
diff --git a/theme/theme.inc b/theme/theme.inc
index 3282fcb..2f12ce2 100644
--- a/theme/theme.inc
+++ b/theme/theme.inc
@@ -293,6 +293,27 @@ function template_preprocess_views_view_fields(&$vars) {
 }
 
 /**
+ * Display a single views grouping.
+ */
+function theme_views_view_grouping($view, $grouping, $grouping_level, $rows, $title, $content) {
+  $content = $view->style_plugin->render_grouping_sets($rows, $grouping_level);
+  $output = '<div class="view-grouping">';
+  $output .= '<div class="view-grouping-header">' . $title . '</div>';
+  $output .= '<div class="view-grouping-content">' . $content . '</div>' ;
+  $output .= '</div>';
+  return $output;
+}
+
+/**
+ * Process a single grouping within a view.
+ * This function isn't normally run,
+ * except if you replace the theme function with a template.
+ */
+function template_preprocess_views_view_grouping(&$vars) {
+  $vars['content'] = $vars['view']->style_plugin->render_grouping_sets($vars['rows'], $vars['grouping_level']);
+}
+
+/**
  * Display a single views field.
  *
  * Interesting bits of info:
@@ -662,7 +683,6 @@ function template_preprocess_views_view_grid(&$vars) {
 function template_preprocess_views_view_unformatted(&$vars) {
   $view     = $vars['view'];
   $rows     = $vars['rows'];
-
   $vars['classes'] = array();
   // Set up striping values.
   $count = 0;
diff --git a/theme/views-view-grouping.tpl.php b/theme/views-view-grouping.tpl.php
new file mode 100644
index 0000000..caffc5f
--- /dev/null
+++ b/theme/views-view-grouping.tpl.php
@@ -0,0 +1,22 @@
+<?php
+ /**
+  * This template is used to print a single grouping in a view. It is not
+  * actually used in default Views, as this is registered as a theme
+  * function which has better performance. For single overrides, the
+  * template is perfectly okay.
+  *
+  * Variables available:
+  * - $view: The view object
+  * - $grouping: The grouping instruction.
+  * - $grouping_level: Integer indicating the hierarchical level of the grouping.
+  * - $rows: The rows contained in this grouping.
+  * - $title: The title of this grouping.
+  * - $content: The processed content output that will normally be used.
+  */
+?>
+<div class="view-grouping">
+  <div class="view-grouping-header"><?php print $title; ?></div>
+  <div class="view-grouping-content">
+    <?php print $content; ?>
+  </div>
+</div>
diff --git a/views.module b/views.module
index b540411..f459e63 100644
--- a/views.module
+++ b/views.module
@@ -95,6 +95,10 @@ function views_theme($existing, $type, $theme, $path) {
     'pattern' => 'views_view_field__',
     'arguments' => array('view' => NULL, 'field' => NULL, 'row' => NULL),
   );
+  $hooks['views_view_grouping'] = $base + array(
+    'pattern' => 'views_view_grouping__',
+    'arguments' => array('view' => NULL, 'grouping' => NULL, 'grouping_level' => NULL, 'rows' => NULL, 'title' => NULL),
+  );
 
   $plugins = views_fetch_plugin_data();
 
-- 
1.7.5.4

