diff --git a/plugins/views_plugin_style.inc b/plugins/views_plugin_style.inc index 4e0946e..0773476 100644 --- a/plugins/views_plugin_style.inc +++ b/plugins/views_plugin_style.inc @@ -43,8 +43,7 @@ class views_plugin_style extends views_plugin { } $this->options += array( - 'grouping' => '', - 'group_rendered' => TRUE, + 'grouping' => array(), ); $this->definition += array( @@ -152,8 +151,7 @@ class views_plugin_style extends views_plugin { function option_definition() { $options = parent::option_definition(); - $options['grouping'] = array('default' => ''); - $options['group_rendered'] = array('default' => TRUE); + $options['grouping'] = array('default' => array()); if ($this->uses_row_class()) { $options['row_class'] = array('default' => ''); } @@ -173,19 +171,35 @@ 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.'), - ); - $form['group_rendered'] = array( - '#type' => 'checkbox', - '#title' => t('Use rendered output to group rows'), - '#default_value' => $this->options['group_rendered'], - '#description' => t('If enabled the rendered output of the grouping field is used to group the rows.'), - ); + // 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; + } + if (is_string($this->options['group_rendered'])) { + $this->options['grouping'][0]['rendered'] = $this->options['group_rendered']; + } + + $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(); + $grouping += array('field' => '', 'rendered' => TRUE); + $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.'), + ); + $form['grouping'][$i]['rendered'] = array( + '#type' => 'checkbox', + '#title' => t('Use rendered output to group rows'), + '#default_value' => $grouping['rendered'], + '#description' => t('If enabled the rendered output of the grouping field is used to group the rows.'), + ); + } } } @@ -203,6 +217,15 @@ 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 @@ -237,36 +260,63 @@ 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'], - (bool) $this->options['group_rendered'] + TRUE ); - // Render each group separately and concatenate. Plugins may override this - // method if they wish some other way of handling grouping. + return $this->render_grouping_sets($sets); + } + + /** + * 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 $group) { - $title = $group['group']; - if ($this->uses_row_plugin()) { - $rows = array(); - foreach ($group['rows'] 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), + array( + 'view' => $this->view, + 'grouping' => $this->options['grouping'][$level], + 'grouping_level' => $level, + 'rows' => $set['rows'], + 'title' => $set['group']) + ); } + // Render as a record set. else { - $rows = $group['rows']; - } + 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(), - array( - 'view' => $this->view, - 'options' => $this->options, - 'rows' => $rows, - 'title' => $title) - ); + $output .= theme($this->theme_functions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'grouping_level' => $level, + 'rows' => $set['rows'], + 'title' => $set['group']) + ); + } } unset($this->view->row_index); return $output; @@ -277,42 +327,94 @@ 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. * @param $group_rendered - * Boolean value to switch whether to use the rendered or the raw field - * value for grouping. If set to NULL the return is structured as before - * Views 7.x-3.0-rc2. + * Boolean value whether to use the rendered or the raw field value for + * grouping. If set to NULL the return is structured as before + * Views 7.x-3.0-rc2. After Views 7.x-3.0 this boolean is only used if + * $groupings is an old-style string or if the rendered option is missing + * for a grouping instruction. * @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 = '', $group_rendered = NULL) { + function render_grouping($records, $groupings = array(), $group_rendered = NULL) { + // 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])) { - $group_content = $this->get_field($index, $grouping_field); - if ($this->view->field[$grouping_field]->options['label']) { - $group_content = $this->view->field[$grouping_field]->options['label'] . ': ' . $group_content; + // 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']; + $rendered = isset($info['rendered']) ? $info['rendered'] : $group_rendered; + $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; + } + if ($rendered) { + $grouping = $group_content; + } + else { + $grouping = $this->get_field_value($index, $field); + // Not all field handlers return a scalar value, + // e.g. views_handler_field_field. + if (!is_scalar($grouping)) { + $grouping = md5(serialize($grouping)); + } + } } - if ($group_rendered) { - $grouping = $group_content; - } - else { - $grouping = $this->get_field_value($index, $grouping_field); - } - if (empty($sets[$grouping]['group'])) { - $sets[$grouping]['group'] = $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]['rows'][$index] = $row; + // Add the row to the hierarchically positioned row set we just determined. + $set[$index] = $row; } } else { diff --git a/tests/styles/views_plugin_style.test b/tests/styles/views_plugin_style.test index 5bad590..37bb88b 100644 --- a/tests/styles/views_plugin_style.test +++ b/tests/styles/views_plugin_style.test @@ -14,9 +14,9 @@ class ViewsPluginStyleTestCase extends ViewsSqlTest { } /** - * Tests the groupby features of styles. + * Tests the grouping legacy features of styles. */ - function testGroupBy() { + function testGroupingLegacy() { $view = $this->getBasicView(); // Setup grouping by the job. $view->init_display(); @@ -69,7 +69,7 @@ class ViewsPluginStyleTestCase extends ViewsSqlTest { $expected['Job: Drummer'][2]->views_test_job = 'Drummer'; $expected['Job: Drummer'][2]->views_test_id = '3'; - $this->assertEqual($sets, $expected, t('The style plugin should proper groupby the results')); + $this->assertEqual($sets, $expected, t('The style plugin should proper group the results with grouping by the rendered output.')); $expected = array(); $expected['Job: Singer'] = array(); @@ -93,7 +93,7 @@ class ViewsPluginStyleTestCase extends ViewsSqlTest { $sets_new_rendered = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], TRUE); $sets_new_value = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], FALSE); - $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper groupby the results with grouped by the rendered output.')); + $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper group the results with grouping by the rendered output.')); // Reorder the group structure to group by value. $expected['Singer'] = $expected['Job: Singer']; @@ -101,6 +101,87 @@ class ViewsPluginStyleTestCase extends ViewsSqlTest { unset($expected['Job: Singer']); unset($expected['Job: Drummer']); - $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper groupby the results with grouped by the value.')); + $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper group the results with grouping by the value.')); + } + + /** + * Tests the grouping features of styles. + */ + function testGrouping() { + $view = $this->getBasicView(); + // Setup grouping by the job field twice. It doesn't really matter if we + // group on two different fields or on a single field twice. + $view->init_display(); + $view->init_style(); + $view->style_plugin->options['grouping'] = array( + array('field' => 'job'), + array('field' => 'job'), + ); + + // Reduce the amount of items to make the test a bit easier. + // Set up the pager. + $view->display['default']->handler->override_option('pager', array( + 'type' => 'some', + 'options' => array('items_per_page' => 3), + )); + + // Add the job field . + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + 'job' => array( + 'id' => 'job', + 'table' => 'views_test', + 'field' => 'job', + 'relationship' => 'none', + ), + )); + + // Now run the query and groupby the result. + $this->executeView($view); + + $expected = array(); + $expected['Job: Singer'] = array(); + $expected['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['rows']['Job: Singer'] = array(); + $expected['Job: Singer']['rows']['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['rows']['Job: Singer']['rows'][0] = new StdClass(); + $expected['Job: Singer']['rows']['Job: Singer']['rows'][0]->views_test_name = 'John'; + $expected['Job: Singer']['rows']['Job: Singer']['rows'][0]->views_test_job = 'Singer'; + $expected['Job: Singer']['rows']['Job: Singer']['rows'][0]->views_test_id = '1'; + $expected['Job: Singer']['rows']['Job: Singer']['rows'][1] = new StdClass(); + $expected['Job: Singer']['rows']['Job: Singer']['rows'][1]->views_test_name = 'George'; + $expected['Job: Singer']['rows']['Job: Singer']['rows'][1]->views_test_job = 'Singer'; + $expected['Job: Singer']['rows']['Job: Singer']['rows'][1]->views_test_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['rows']['Job: Drummer'] = array(); + $expected['Job: Drummer']['rows']['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['rows']['Job: Drummer']['rows'][2] = new StdClass(); + $expected['Job: Drummer']['rows']['Job: Drummer']['rows'][2]->views_test_name = 'Ringo'; + $expected['Job: Drummer']['rows']['Job: Drummer']['rows'][2]->views_test_job = 'Drummer'; + $expected['Job: Drummer']['rows']['Job: Drummer']['rows'][2]->views_test_id = '3'; + + // The newer api passes the value of the grouping as well. + $sets_new_rendered = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], TRUE); + $sets_new_value = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], FALSE); + + $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper group the results with grouping by the rendered output.')); + + // Reorder the group structure to grouping by value. + $expected['Singer'] = $expected['Job: Singer']; + $expected['Singer']['rows']['Singer'] = $expected['Job: Singer']['rows']['Job: Singer']; + $expected['Drummer'] = $expected['Job: Drummer']; + $expected['Drummer']['rows']['Drummer'] = $expected['Job: Drummer']['rows']['Job: Drummer']; + unset($expected['Job: Singer']); + unset($expected['Singer']['rows']['Job: Singer']); + unset($expected['Job: Drummer']); + unset($expected['Drummer']['rows']['Job: Drummer']); + + $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper group the results with grouping by the value.')); } } diff --git a/theme/theme.inc b/theme/theme.inc index 8ab2780..225e3e6 100644 --- a/theme/theme.inc +++ b/theme/theme.inc @@ -307,6 +307,29 @@ function template_preprocess_views_view_fields(&$vars) { } /** + * Display a single views grouping. + */ +function theme_views_view_grouping($vars) { + $view = $vars['view']; + $title = $vars['title']; + $content = $vars['content']; + + $output = '
'; + $output .= '
' . $title . '
'; + $output .= '
' . $content . '
' ; + $output .= '
'; + + return $output; +} + +/** + * Process a single grouping within a view. + */ +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: 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 @@ + +
+
+
+ +
+
diff --git a/views.module b/views.module index d347f92..3725321 100644 --- a/views.module +++ b/views.module @@ -91,6 +91,10 @@ function views_theme($existing, $type, $theme, $path) { 'pattern' => 'views_view_field__', 'variables' => array('view' => NULL, 'field' => NULL, 'row' => NULL), ); + $hooks['views_view_grouping'] = $base + array( + 'pattern' => 'views_view_grouping__', + 'variables' => array('view' => NULL, 'grouping' => NULL, 'grouping_level' => NULL, 'rows' => NULL, 'title' => NULL), + ); $plugins = views_fetch_plugin_data();