diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b81a4caed677fa2aaf7a2cc70cca5f114b252041..9f3ea1e06e0932003abc4689c7144cda232bf39c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,6 @@ CHANGELOG for Views 3 for Drupal 7 +Views 6.x-3.x-dev + o #396380 by merlinofchaos, dereine and dagmar: Initial support for GROUP BY queries!!!!!!!!!!!! Views 6.x-3.0-alpha1 Bugs fixed diff --git a/handlers/views_handler_argument.inc b/handlers/views_handler_argument.inc index 14774ee6ca968ffc0f0a6b915f60b8a789b6ed5b..11ac9310497b69a819ba2cbfbed5d61f2b65b7b3 100644 --- a/handlers/views_handler_argument.inc +++ b/handlers/views_handler_argument.inc @@ -121,7 +121,7 @@ class views_handler_argument extends views_handler { '#default_value' => $this->options['title'], '#description' => t('The title to use when this argument is present. It will override the title of the view and titles from previous arguments. You can use percent substitution here to replace with argument titles. Use "%1" for the first argument, "%2" for the second, etc.'), ); - + $form['breadcrumb'] = array( '#prefix' => '
', '#suffix' => '
', @@ -714,7 +714,7 @@ class views_handler_argument extends views_handler { if (isset($this->argument)) { return $this->argument; } - + // Otherwise, we have to pretend to process ourself to find the value. $value = NULL; // Find the position of this argument within the view. diff --git a/handlers/views_handler_field.inc b/handlers/views_handler_field.inc index 62b30795ebfb52e163ac6d31d44297f359dc8fcd..e53df5a5beccccaafec34a9f59701e2f7c545ccf 100644 --- a/handlers/views_handler_field.inc +++ b/handlers/views_handler_field.inc @@ -94,7 +94,12 @@ class views_handler_field extends views_handler { else { $table_alias = $this->table_alias; } - $this->aliases[$identifier] = $this->query->add_field($table_alias, $info['field']); + $params = array(); + if (!empty($info['params'])) { + $params = $info['params']; + } + + $this->aliases[$identifier] = $this->query->add_field($table_alias, $info['field'], NULL, $params); } else { $this->aliases[$info] = $this->query->add_field($this->table_alias, $info); @@ -173,6 +178,8 @@ class views_handler_field extends views_handler { * should have. */ function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['label'] = array( '#type' => 'textfield', '#title' => t('Label'), @@ -272,7 +279,7 @@ class views_handler_field extends views_handler { 'edit-options-alter-make-link' => array(1) ), ); - + // Get a list of the available fields and arguments for token replacement. $options = array(); @@ -458,7 +465,7 @@ class views_handler_field extends views_handler { $this->original_value = $this->last_render; $alter = $item + $this->options['alter']; - $items[] = $this->render_text($alter); + $items[] = $this->render_text($alter); } $value = $this->render_items($items); @@ -511,7 +518,7 @@ class views_handler_field extends views_handler { return $value; } - + /** * Render this field as altered text, from a fieldset set by the user. */ diff --git a/handlers/views_handler_filter.inc b/handlers/views_handler_filter.inc index afb70fc4ce1728f564b9721f69b6f3e8ec768c6d..50056f286cbd61fe929b929e19fc313d1631becb 100644 --- a/handlers/views_handler_filter.inc +++ b/handlers/views_handler_filter.inc @@ -271,7 +271,7 @@ class views_handler_filter extends views_handler { if (!empty($form_state['values']['options']['expose']['identifier']) && $form_state['values']['options']['expose']['identifier'] == 'value') { form_error($form['expose']['identifier'], t('This identifier is not allowed.')); } - + if (!$this->view->display_handler->is_identifier_unique($form_state['id'], $form_state['values']['options']['expose']['identifier'])) { form_error($form['expose']['identifier'], t('This identifier is used by another handler.')); } diff --git a/handlers/views_handler_filter_string.inc b/handlers/views_handler_filter_string.inc index e518c9f7ea490690a3d21a73bd2759b1e1cff4bb..994d1649ec4f2398b876d4636123108e925498f9 100644 --- a/handlers/views_handler_filter_string.inc +++ b/handlers/views_handler_filter_string.inc @@ -300,7 +300,7 @@ class views_handler_filter_string extends views_handler_filter { function op_not($field, $upper) { $this->query->add_where($this->options['group'], "$upper(%s) NOT LIKE $upper('%%%s%%')", $field, $this->value); } - + function op_shorter($field, $upper) { $this->query->add_where($this->options['group'], "LENGTH($upper(%s)) < %d", $field, $this->value); } diff --git a/includes/admin.inc b/includes/admin.inc index 9c95b4aafe10f5c8d1c838ccc41d0a396a021ec6..6885c2f3c34960debb4a5895262b7569a95313bf 100644 --- a/includes/admin.inc +++ b/includes/admin.inc @@ -1226,28 +1226,21 @@ function template_preprocess_views_ui_edit_item(&$vars) { // Get relationship labels $relationships = array(); // @todo: get_handlers() - foreach ($display->handler->get_option('relationships') as $id => $relationship) { - $handler = views_get_handler($relationship['table'], $relationship['field'], 'relationship'); - if (empty($handler)) { - continue; - } - $handler->init($view, $relationship); + foreach ($display->handler->get_handlers('relationship') as $id => $handler) { $relationships[$id] = $handler->label(); } } - // @todo: get_handlers() foreach ($display->handler->get_option($types[$type]['plural']) as $id => $field) { $fields[$id] = array(); - $handler = views_get_handler($field['table'], $field['field'], $type); + $handler = $display->handler->get_handler($type, $id); if (empty($handler)) { $fields[$id]['class'] = 'broken'; $fields[$id]['title'] = t("Error: handler for @table > @field doesn't exist!", array('@table' => $field['table'], '@field' => $field['field'])); $fields[$id]['info'] = ''; continue; } - $handler->init($view, $field); $field_name = $handler->ui_name(TRUE); if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) { @@ -1261,6 +1254,9 @@ function template_preprocess_views_ui_edit_item(&$vars) { } $fields[$id]['info'] = $handler->admin_summary(); + if ($display->handler->use_group_by()) { + $fields[$id]['links'] = l('' . t('Group settings') . '', "admin/build/views/nojs/config-item-group/$view->name/$display->id/$type/$id", array('attributes' => array('class' => 'views-button-configure views-ajax-link', 'title' => t('Group settings')), 'html' => true)); + } if ($handler->has_extra_options()) { $fields[$id]['links'] = l('' . t('Settings') . '', "admin/build/views/nojs/config-item-extra/$view->name/$display->id/$type/$id", array('attributes' => array('class' => 'views-button-configure views-ajax-link', 'title' => t('Settings')), 'html' => true)); } @@ -1455,6 +1451,10 @@ function views_ui_ajax_forms($key = NULL) { 'form_id' => 'views_ui_config_item_form', 'args' => array('type', 'id'), ), + 'config-item-group' => array( + 'form_id' => 'views_ui_config_item_group_form', + 'args' => array('type', 'id'), + ), 'config-item-extra' => array( 'form_id' => 'views_ui_config_item_extra_form', 'args' => array('type', 'id'), @@ -2014,17 +2014,10 @@ function views_ui_rearrange_form(&$form_state) { // Get relationship labels $relationships = array(); - // @todo: get_handlers() - foreach ($display->handler->get_option('relationships') as $id => $relationship) { - $handler = views_get_handler($relationship['table'], $relationship['field'], 'relationship'); - if (empty($handler)) { - continue; - } - $handler->init($view, $relationship); + foreach ($display->handler->get_handlers('relationship') as $id => $handler) { $relationships[$id] = $handler->label(); } - // @todo: get_handlers() foreach ($display->handler->get_option($types[$type]['plural']) as $id => $field) { $form[$id] = array('#tree' => TRUE); $form[$id]['weight'] = array( @@ -2032,9 +2025,8 @@ function views_ui_rearrange_form(&$form_state) { '#delta' => 25, '#default_value' => ++$count, ); - $handler = views_get_handler($field['table'], $field['field'], $type); + $handler = $display->handler->get_handler($type, $id); if ($handler) { - $handler->init($view, $field); $name = $handler->ui_name() . ' ' . $handler->admin_summary(); if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) { $name = '(' . $relationships[$field['relationship']] . ') ' . $name; @@ -2155,7 +2147,7 @@ function views_ui_add_item_form(&$form_state) { // Figure out all the base tables allowed based upon what the relationships provide. $base_tables = $view->get_base_tables(); - $options = views_fetch_fields(array_keys($base_tables), $type); + $options = views_fetch_fields(array_keys($base_tables), $type, $display->handler->use_group_by()); if (!empty($options)) { $groups = array('all' => t('')); @@ -2220,6 +2212,11 @@ function views_ui_add_item_form_submit($form, &$form_state) { list($table, $field) = explode('.', $field, 2); $id = $form_state['view']->add_item($form_state['display_id'], $type, $table, $field); + // check to see if we have group by settings + if ($form_state['view']->display_handler->use_group_by()) { + views_ui_add_form_to_stack('config-item-group', $form_state['view'], $form_state['display_id'], array($type, $id)); + } + // check to see if this type has settings, if so add the settings form first $handler = views_get_handler($table, $field, $type); if ($handler && $handler->has_extra_options()) { @@ -2251,12 +2248,11 @@ function views_ui_config_item_form(&$form_state) { $item = $view->get_item($display_id, $type, $id); if ($item) { - $handler = views_get_handler($item['table'], $item['field'], $type); + $handler = $view->display_handler->get_handler($type, $id); if (empty($handler)) { $form['markup'] = array('#value' => t("Error: handler for @table > @field doesn't exist!", array('@table' => $item['table'], '@field' => $item['field']))); } else { - $handler->init($view, $item); $types = views_object_types(); if ($view->display_handler->defaultable_sections($types[$type]['plural'])) { @@ -2284,7 +2280,7 @@ function views_ui_config_item_form(&$form_state) { // If this relationship is valid for this type, add it to the list. $data = views_fetch_data($relationship['table']); $base = $data[$relationship['field']]['relationship']['base']; - $base_fields = views_fetch_fields($base, $form_state['type']); + $base_fields = views_fetch_fields($base, $form_state['type'], $view->display_handler->use_group_by()); if (isset($base_fields[$item['table'] . '.' . $item['field']])) { $relationship_handler->init($view, $relationship); $relationship_options[$relationship['id']] = $relationship_handler->label(); @@ -2294,7 +2290,7 @@ function views_ui_config_item_form(&$form_state) { if (!empty($relationship_options)) { // Make sure the existing relationship is even valid. If not, force // it to none. - $base_fields = views_fetch_fields($view->base_table, $form_state['type']); + $base_fields = views_fetch_fields($view->base_table, $form_state['type'], $view->display_handler->use_group_by()); if (isset($base_fields[$item['table'] . '.' . $item['field']])) { $relationship_options = array_merge(array('none' => t('Do not use a relationship')), $relationship_options); } @@ -2369,9 +2365,7 @@ function views_ui_config_item_form_submit($form, &$form_state) { // Store the item back on the view $form_state['view']->set_item($form_state['display_id'], $form_state['type'], $form_state['id'], $item); - $handler = views_get_handler($item['table'], $item['field'], $form_state['type']); - $handler->init($form_state['view'], $item); - if ($handler && $handler->needs_style_plugin()) { + if ($form_state['handler'] && $form_state['handler']->needs_style_plugin()) { views_ui_add_form_to_stack('change-style', $form_state['view'], $form_state['display_id'], array($form_state['type'], $form_state['id']), TRUE); } @@ -2379,6 +2373,73 @@ function views_ui_config_item_form_submit($form, &$form_state) { views_ui_cache_set($form_state['view']); } +/** + * Form to config_item items in the views UI. + */ +function views_ui_config_item_group_form(&$form_state) { + $view = &$form_state['view']; + $display_id = $form_state['display_id']; + $type = $form_state['type']; + $id = $form_state['id']; + + $view->init_query(); + + $form = array('options' => array('#tree' => TRUE)); + if (!$view->set_display($display_id)) { + views_ajax_render(t('Invalid display id @display', array('@display' => $display_id))); + } + + $item = $view->get_item($display_id, $type, $id); + + if ($item) { + $handler = $view->display_handler->get_handler($type, $id); + if (empty($handler)) { + $form['markup'] = array('#value' => t("Error: handler for @table > @field doesn't exist!", array('@table' => $item['table'], '@field' => $item['field']))); + } + else { + $handler->init($view, $item); + $types = views_object_types(); + + $form['#title'] = check_plain($view->display[$display_id]->display_title) . ': '; + $form['#title'] .= t('Configure group settings for @type %item', array('@type' => $types[$type]['lstitle'], '%item' => $handler->ui_name())); + + $form['#section'] = $display_id . '-' . $type . '-' . $id; + + $info = $view->query->get_aggregation_info(); + foreach ($info as $id => $aggregate) { + $group_types[$id] = $aggregate['title']; + } + + $form['group_type'] = array( + '#type' => 'select', + '#title' => t('Group type'), + '#default_value' => $handler->options['group_type'], + '#description' => t('Grouping is enabled for this display. You must select what function to use on this field.'), + '#options' => $group_types, + ); + $form_state['handler'] = &$handler; + } + + views_ui_standard_form_buttons($form, $form_state, 'views_ui_config_item_group_form'); + } + return $form; +} + +/** + * Submit handler for configing group settings on a view. + */ +function views_ui_config_item_group_form_submit($form, &$form_state) { + $item = $form_state['handler']->options; + + $item['group_type'] = $form_state['values']['group_type']; + + // Store the item back on the view + $form_state['view']->set_item($form_state['display_id'], $form_state['type'], $form_state['id'], $item); + + // Write to cache + views_ui_cache_set($form_state['view']); +} + /** * Submit handler for removing an item from a view */ @@ -2443,8 +2504,7 @@ function views_ui_config_item_extra_form(&$form_state) { // Get form from the handler. $handler->extra_options_form($form['options'], $form_state); - $form_state['handler'] = &$handler; - + $form_state['handler'] = &$handler; } views_ui_standard_form_buttons($form, $form_state, 'views_ui_config_item_extra_form'); @@ -2885,7 +2945,7 @@ function _views_sort_types($a, $b) { * @return * A keyed array of in the form of 'base_table' => 'Description'. */ -function views_fetch_fields($base, $type) { +function views_fetch_fields($base, $type, $grouping = FALSE) { static $fields = array(); if (empty($fields)) { $data = views_fetch_data(); @@ -2917,6 +2977,9 @@ function views_fetch_fields($base, $type) { } foreach (array('field', 'sort', 'filter', 'argument', 'relationship') as $key) { if (!empty($info[$key])) { + if ($grouping && !empty($info[$key]['no group by'])) { + continue; + } if (!empty($info[$key]['skip base'])) { foreach ((array) $info[$key]['skip base'] as $base_name) { $skip_bases[$field][$key][$base_name] = TRUE; diff --git a/includes/handlers.inc b/includes/handlers.inc index 95f9dc50d65b0eeafe8f0e419f3c920dfcaa00d0..065cb98062198d43b7bdf3d4d4ce116dcdf84d69 100644 --- a/includes/handlers.inc +++ b/includes/handlers.inc @@ -14,11 +14,24 @@ function _views_create_handler($definition, $type = 'handler') { return; } - if (!class_exists($definition['handler']) && !views_include_handler($definition, $type)) { + if (!empty($definition['override handler']) && + !class_exists($definition['override handler']) && + !views_include_handler($definition['override handler'], $definition, $type)) { return; } - $handler = new $definition['handler']; + if (!class_exists($definition['handler']) && + !views_include_handler($definition['handler'], $definition, $type)) { + return; + } + + if (!empty($definition['override handler'])) { + $handler = new $definition['override handler']; + } + else { + $handler = new $definition['handler']; + } + $handler->set_definition($definition); // let the handler have something like a constructor. $handler->construct(); @@ -32,24 +45,24 @@ function _views_create_handler($definition, $type = 'handler') { * This will also attempt to include all parents, though we're maxing the * parent chain to 10 to prevent infinite loops. */ -function views_include_handler($definition, $type, $count = 0) { +function views_include_handler($handler, $definition, $type, $count = 0) { // Do not proceed if the class already exists. - if (isset($definition['handler']) && class_exists($definition['handler'])) { + if (isset($handler) && class_exists($handler)) { return TRUE; } // simple infinite loop prevention. if ($count > 10) { - vpr(t('Handler @handler include tried to loop infinitely!', array('@handler' => $definition['handler']))); + vpr(t('Handler @handler include tried to loop infinitely!', array('@handler' => $handler))); return FALSE; } if (!isset($definition['path'])) { if ($type == 'handler') { - $definition += views_fetch_handler_data($definition['handler']); + $definition += views_fetch_handler_data($handler); } else { - $definition += views_fetch_plugin_data($type, $definition['handler']); + $definition += views_fetch_plugin_data($type, $handler); } } @@ -62,7 +75,7 @@ function views_include_handler($definition, $type, $count = 0) { } if ($parent) { - $rc = views_include_handler($parent, $type, $count + 1); + $rc = views_include_handler($parent['handler'], $parent, $type, $count + 1); // If the parent chain cannot be included, don't try; this will // help alleviate problems with modules with cross dependencies. if (!$rc) { @@ -78,7 +91,7 @@ function views_include_handler($definition, $type, $count = 0) { } } - return class_exists($definition['handler']); + return class_exists($handler); } /** @@ -262,6 +275,14 @@ class views_handler extends views_object { $this->query = &$view->query; } + function option_definition() { + $options = parent::option_definition(); + + $options['group_type'] = array('default' => 'group'); + + return $options; + } + /** * Return a string representing this handler's name in the UI. */ @@ -271,15 +292,39 @@ class views_handler extends views_object { } /** - * Provide a form for setting options. + * Shortcut to get a handler's raw field value. + * + * This should be overridden for handlers with formulae or other + * non-standard fields. Because this takes an argument, fields + * overriding this can just call return parent::get_field($formula) */ - function options_form(&$form, &$form_state) { } + function get_field($field = NULL) { + if (!isset($field)) { + if (!empty($this->formula)) { + $field = $this->get_formula(); + } + else { + $field = $this->table_alias . '.' . $this->real_field; + } + } + + // If grouping, check to see if the aggregation method needs to modify the field. + if ($this->view->display_handler->use_group_by()) { + $info = $this->query->get_aggregation_info(); + if (!empty($info[$this->options['group_type']]['method']) && function_exists($info[$this->options['group_type']]['method'])) { + return $info[$this->options['group_type']]['method']($this->options['group_type'], $field); + } + } + + return $field; + } /** * Validate the options form. */ function options_validate($form, &$form_state) { } + function options_form(&$form, &$form_state) { } /** * Perform any necessary changes to the form values prior to storage. * There is no need for this function to actually store the data. @@ -510,7 +555,7 @@ class views_handler extends views_object { * * If we were using PHP5, this would be abstract. */ - function query() { } + function query($group_by = FALSE) { } /** * Ensure the main table for this handler is in the query. This is used @@ -1248,6 +1293,20 @@ function views_views_handlers() { 'views_handler_sort_random' => array( 'parent' => 'views_handler_sort', ), + + // group by handlers + 'views_handler_argument_group_by_numeric' => array( + 'parent' => 'views_handler_argument', + ), + 'views_handler_field_group_by_numeric' => array( + 'parent' => 'views_handler_field', + ), + 'views_handler_filter_group_by_numeric' => array( + 'parent' => 'views_handler_filter_numeric', + ), + 'views_handler_sort_group_by_numeric' => array( + 'parent' => 'views_handler_sort', + ), ), ); } @@ -1457,7 +1516,8 @@ class views_join { */ function views_views_api() { return array( - 'api' => 2, + // in your modules do *not* use views_api_version()!!! + 'api' => views_api_version(), 'path' => drupal_get_path('module', 'views') . '/modules', ); } diff --git a/includes/view.inc b/includes/view.inc index 715db704b33f86ef6da6299ddbbf4923d1aa6a23..e24393b5d788197eb35c809d3d28e9c2f8159f6c 100644 --- a/includes/view.inc +++ b/includes/view.inc @@ -51,17 +51,18 @@ class view extends views_db_object { // Used to store views that were previously running if we recurse. var $old_view = array(); + + // Where the $query object will reside: + var $query = NULL; /** * Constructor */ - function view() { + function __construct() { parent::init(); // Make sure all of our sub objects are arrays. foreach ($this->db_objects() as $object) { $this->$object = array(); } - - $this->query = new stdClass(); } /** @@ -474,7 +475,7 @@ class view extends views_db_object { } else { $arg_title = $argument->get_title(); - $argument->query(); + $argument->query($this->display_handler->use_group_by()); } // Add this argument's substitution @@ -525,6 +526,14 @@ class view extends views_db_object { * Do some common building initialization. */ function init_query() { + if (!empty($this->query)) { + $class = get_class($this->query); + if ($class && $class != 'stdClass') { + // return if query is already initialized. + return; + } + } + // Create and initialize the query object. $views_data = views_fetch_data($this->base_table); $this->base_field = $views_data['table']['base']['field']; @@ -629,10 +638,10 @@ class view extends views_db_object { } // Allow display handler to affect the query: - $this->display_handler->query(); + $this->display_handler->query($this->display_handler->use_group_by()); // Allow style handler to affect the query: - $this->style_plugin->query(); + $this->style_plugin->query($this->display_handler->use_group_by()); if (variable_get('views_sql_signature', FALSE)) { $this->query->add_signature($this); @@ -668,7 +677,7 @@ class view extends views_db_object { } } $handlers[$id]->set_relationship(); - $handlers[$id]->query(); + $handlers[$id]->query($this->display_handler->use_group_by()); } } } diff --git a/modules/comment.views.inc b/modules/comment.views.inc index 3bc8521689ced69aee61144e8d09a1072b89a3b5..99cf961afcb9fb258554ed5161b5c29b5989de9b 100644 --- a/modules/comment.views.inc +++ b/modules/comment.views.inc @@ -389,6 +389,7 @@ function comment_views_data_alter(&$data) { 'help' => t('The number of new comments on the node.'), 'field' => array( 'handler' => 'views_handler_field_node_new_comments', + 'no group by' => TRUE, ), ); diff --git a/modules/node/views_handler_field_node.inc b/modules/node/views_handler_field_node.inc index 8cffbb5542fb68b2da71148b0132b5b0b0a80bf3..04953c8a2be7c48c6d99aa1ab63235fc977e5e6b 100644 --- a/modules/node/views_handler_field_node.inc +++ b/modules/node/views_handler_field_node.inc @@ -9,14 +9,14 @@ * Field handler to provide simple renderer that allows linking to a node. */ class views_handler_field_node extends views_handler_field { - /** - * Constructor to provide additional field to add. - */ - function construct() { - parent::construct(); - $this->additional_fields['nid'] = 'nid'; - if (module_exists('translation')) { - $this->additional_fields['language'] = array('table' => 'node', 'field' => 'language'); + + function init(&$view, $options) { + parent::init($view, $options); + if (!empty($this->options['link_to_node'])) { + $this->additional_fields['nid'] = 'nid'; + if (module_exists('translation')) { + $this->additional_fields['language'] = array('table' => 'node', 'field' => 'language'); + } } } diff --git a/modules/profile.views.inc b/modules/profile.views.inc index e1255b0b780465a1858422822bf8529e84d1d3ca..edc2677d86e97c4d9f1f849bc864e25e7f33a03f 100644 --- a/modules/profile.views.inc +++ b/modules/profile.views.inc @@ -195,6 +195,7 @@ function profile_views_fetch_field($field) { 'help' => t('Profile freeform list %field-name.', array('%field-name' => $field->title)), 'field' => array( 'handler' => 'views_handler_field_profile_list', + 'no group by' => TRUE, ), 'filter' => array( 'handler' => 'views_handler_filter_string', diff --git a/modules/taxonomy.views.inc b/modules/taxonomy.views.inc index 5877d152f6f44861cf946bc771561db6777ddeff..4ec4ecc74db922f2b72f30cfa486205c1822499a 100644 --- a/modules/taxonomy.views.inc +++ b/modules/taxonomy.views.inc @@ -203,6 +203,7 @@ function taxonomy_views_data() { 'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'), 'handler' => 'views_handler_field_term_node_tid', 'skip base' => 'term_data', + 'no group by' => TRUE, ), 'argument' => array( 'handler' => 'views_handler_argument_term_node_tid', @@ -265,7 +266,7 @@ function taxonomy_views_data() { 'argument' => array( 'help' => t('The parent term of the term.'), 'handler' => 'views_handler_argument_numeric', - ), + ), ); // ---------------------------------------------------------------------- diff --git a/modules/upload.views.inc b/modules/upload.views.inc index 06bbaedf964d3718cc5c8ee68e29a50e4a023ab0..bc4d4502edff23f39084d8c7189e1964d8c4265a 100644 --- a/modules/upload.views.inc +++ b/modules/upload.views.inc @@ -116,6 +116,7 @@ function upload_views_data_alter(&$data) { 'real field' => 'vid', 'field' => array( 'handler' => 'views_handler_field_upload_fid', + 'no group by' => TRUE, ), 'filter' => array( 'handler' => 'views_handler_filter_upload_fid', diff --git a/modules/user.views.inc b/modules/user.views.inc index a14a5f40f1e38f127cf9c60fa7a7f94de3c4bbfa..fa53e1253ee86d7542d8ee0170a5c4bb8312085e 100644 --- a/modules/user.views.inc +++ b/modules/user.views.inc @@ -275,6 +275,7 @@ function user_views_data() { 'help' => t('Roles that a user belongs to.'), 'field' => array( 'handler' => 'views_handler_field_user_roles', + 'no group by' => TRUE, ), 'filter' => array( 'handler' => 'views_handler_filter_user_roles', diff --git a/plugins/views_plugin_display.inc b/plugins/views_plugin_display.inc index 53aab1ac03f4c70cc129ddfb6d0aa3422fbaec67..86613a5d6099e6174f5254310433a43bb2a1251e 100644 --- a/plugins/views_plugin_display.inc +++ b/plugins/views_plugin_display.inc @@ -129,6 +129,13 @@ class views_plugin_display extends views_plugin { return FALSE; } + /** + * Does the display have a more link enabled? + */ + function use_group_by() { + return $this->get_option('group_by'); + } + /** * Should the enabled display more link be shown when no more items? */ @@ -256,6 +263,7 @@ class views_plugin_display extends views_plugin { 'exposed_block' => TRUE, 'link_display' => TRUE, + 'group_by' => TRUE, 'style_plugin' => TRUE, 'style_options' => TRUE, @@ -361,6 +369,9 @@ class views_plugin_display extends views_plugin { 'distinct' => array( 'default' => FALSE, ), + 'group_by' => array( + 'default' => FALSE, + ), 'style_plugin' => array( 'default' => 'default', @@ -559,7 +570,21 @@ class views_plugin_display extends views_plugin { $types = views_object_types(); $plural = $types[$type]['plural']; foreach ($this->get_option($plural) as $id => $info) { - $handler = views_get_handler($info['table'], $info['field'], $type); + // If aggregation is on, the group type might override the actual + // handler that is in use. This piece of code checks that and, + // if necessary, sets the override handler. + $override = NULL; + if ($this->use_group_by() && !empty($info['group_type'])) { + if (empty($this->view->query)) { + $this->view->init_query(); + } + $aggregate = $this->view->query->get_aggregation_info(); + if (!empty($aggregate[$info['group_type']]['handler'][$type])) { + $override = $aggregate[$info['group_type']]['handler'][$type]; + } + } + + $handler = views_get_handler($info['table'], $info['field'], $type, $override); if ($handler) { $handler->init($this->view, $info); $this->handlers[$type][$id] = &$handler; @@ -731,6 +756,16 @@ class views_plugin_display extends views_plugin { 'desc' => t('Display only distinct items, without duplicates.'), ); + $this->view->init_query(); + if ($this->view->query->get_aggregation_info()) { + $options['group_by'] = array( + 'category' => 'advanced', + 'title' => t('Use grouping'), + 'value' => $this->get_option('group_by') ? t('Yes') : t('No'), + 'desc' => t('Allow grouping and aggregation (calculation) of fields.'), + ); + } + $access_plugin = $this->get_access_plugin(); if (!$access_plugin) { // default to the no access control plugin. @@ -946,6 +981,15 @@ class views_plugin_display extends views_plugin { '#default_value' => $this->get_option('distinct'), ); break; + case 'group_by': + $form['#title'] .= t('Allow grouping and aggregation (calculation) of fields.'); + $form['group_by'] = array( + '#type' => 'checkbox', + '#title' => t('Group by'), + '#description' => t('If enabled, some fields may become unavailable. All fields that are selected for grouping will be collapsed to one record per distinct value. Other fields which are selected for aggregation will have the function run on them. For example, you can group nodes on title and count the number of nids in order to get a list of duplicate titles.'), + '#default_value' => $this->get_option('distinct'), + ); + break; case 'access': $form['#title'] .= t('Access restrictions'); $form['access'] = array( @@ -1526,6 +1570,9 @@ class views_plugin_display extends views_plugin { case 'distinct': $this->set_option($section, $form_state['values'][$section]); break; + case 'group_by': + $this->set_option($section, $form_state['values'][$section]); + break; case 'row_plugin': // This if prevents resetting options to default if they don't change // the plugin. diff --git a/plugins/views_plugin_query.inc b/plugins/views_plugin_query.inc index 37483186eb858247aeefd80cd6c5339b1b52776e..183c0025c549e9948f85383994d5ac5b9a348e46 100644 --- a/plugins/views_plugin_query.inc +++ b/plugins/views_plugin_query.inc @@ -23,12 +23,12 @@ class views_plugin_query extends views_plugin { * Provide a countquery if this is true, otherwise provide a normal query. */ function query($get_count = FALSE) { } - + /** * Let modules modify the query just prior to finalizing it. */ function alter(&$view) { } - + /** * Builds the necessary info to execute the query. */ @@ -37,7 +37,7 @@ class views_plugin_query extends views_plugin { /** * Executes the query and fills the associated view object with according * values. - * + * * Values to set: $view->result, $view->total_rows, $view->execute_time, * $view->pager['current_page']. */ @@ -50,4 +50,11 @@ class views_plugin_query extends views_plugin { * discern where particular queries might be coming from. */ function add_signature(&$view) { } + + /** + * Get aggregation info for group by queries. + * + * If NULL, aggregation is not allowed. + */ + function get_aggregation_info() { } } diff --git a/plugins/views_plugin_query_default.inc b/plugins/views_plugin_query_default.inc index 9fcb6e596fae398d9b1cd679a4240d85de80bdb7..5d15087cae5190e6a5fab73b65fa6ae5cd784ebe 100644 --- a/plugins/views_plugin_query_default.inc +++ b/plugins/views_plugin_query_default.inc @@ -93,6 +93,8 @@ class views_plugin_query_default extends views_plugin_query{ 'alias' => $base_table, ); +/** + * -- we no longer want the base field to appear automatigically. if ($base_field) { $this->fields[$base_field] = array( 'table' => $base_table, @@ -100,6 +102,7 @@ class views_plugin_query_default extends views_plugin_query{ 'alias' => $base_field, ); } + */ $this->count_field = array( 'table' => $base_table, @@ -612,11 +615,14 @@ class views_plugin_query_default extends views_plugin_query{ * The alias to create. If not specified, the alias will be $table_$field * unless $table is NULL. When adding formulae, it is recommended that an * alias be used. + * @param $params + * An array of parameters additional to the field that will control items + * such as aggregation functions and DISTINCT. * * @return $name * The name that this field can be referred to as. Usually this is the alias. */ - function add_field($table, $field, $alias = '', $params = NULL) { + function add_field($table, $field, $alias = '', $params = array()) { // We check for this specifically because it gets a special alias. if ($table == $this->base_table && $field == $this->base_field && empty($alias)) { $alias = $this->base_field; @@ -630,22 +636,30 @@ class views_plugin_query_default extends views_plugin_query{ $alias = $table . '_' . $field; } - $name = $alias ? $alias : $field; + // Make sure an alias is assigned + $alias = $alias ? $alias : $field; - // @todo FIXME -- $alias, then $name is inconsistent - if (empty($this->fields[$alias])) { - $this->fields[$name] = array( - 'field' => $field, - 'table' => $table, - 'alias' => $alias, - ); + // Create a field info array. + $field_info = array( + 'field' => $field, + 'table' => $table, + 'alias' => $alias, + ) + $params; + + // Test to see if the field is actually the same or not. Due to + // differing parameters changing the aggregation function, we need + // to do some automatic alias collision detection: + $base = $alias; + $counter = 0; + while (!empty($this->fields[$alias]) && $this->fields[$alias] != $field_info) { + $field_info['alias'] = $alias = $base . '_' . ++$counter; } - foreach ((array)$params as $key => $value) { - $this->fields[$name][$key] = $value; + if (empty($this->fields[$alias])) { + $this->fields[$alias] = $field_info; } - return $name; + return $alias; } /** @@ -806,8 +820,10 @@ class views_plugin_query_default extends views_plugin_query{ * must also be in the SELECT portion. If an $alias isn't specified * one will be generated for from the $field; however, if the * $field is a formula, this alias will likely fail. + * @param $params + * Any params that should be passed through to the add_field. */ - function add_orderby($table, $field, $order, $alias = '') { + function add_orderby($table, $field, $order, $alias = '', $params = array()) { if ($table) { $this->ensure_table($table); } @@ -822,17 +838,21 @@ class views_plugin_query_default extends views_plugin_query{ } if ($field) { - $this->add_field($table, $field, $as); + $this->add_field($table, $field, $as, $params); } $this->orderby[] = "$as " . strtoupper($order); +/** + * -- removing, this should be taken care of by field adding now. + * -- leaving commented because I am unsure. // If grouping, all items in the order by must also be in the // group by clause. Check $table to ensure that this is not a // formula. if ($this->groupby && $table) { $this->add_groupby($as); } + */ } /** @@ -885,10 +905,8 @@ class views_plugin_query_default extends views_plugin_query{ function query($get_count = FALSE) { // Check query distinct value. if (empty($this->no_distinct) && $this->distinct && !empty($this->fields)) { - if (!empty($this->fields[$this->base_field])) { - $this->fields[$this->base_field]['distinct'] = TRUE; - $this->add_groupby($this->base_field); - } + $base_field_alias = $this->add_field($this->base_table, $this->base_field, NULL, array('distinct' => TRUE)); + $this->add_groupby($base_field_alias); } /** @@ -898,7 +916,7 @@ class views_plugin_query_default extends views_plugin_query{ $fields_array = $this->fields; if ($get_count && !$this->groupby) { foreach ($fields_array as $field) { - if (!empty($field['distinct'])) { + if (!empty($field['distinct']) || !empty($field['function'])) { $get_count_optimized = FALSE; break; } @@ -911,7 +929,8 @@ class views_plugin_query_default extends views_plugin_query{ $get_count_optimized = TRUE; } - $joins = $fields = $where = $having = $orderby = $groupby = ''; + $joins = $where = $having = $orderby = $groupby = ''; + $fields = $distinct = array(); // Add all the tables to the query via joins. We assume all LEFT joins. foreach ($this->table_queue as $table) { if (is_object($table['join'])) { @@ -923,9 +942,6 @@ class views_plugin_query_default extends views_plugin_query{ $non_aggregates = array(); foreach ($fields_array as $field) { - if ($fields) { - $fields .= ",\n "; - } $string = ''; if (!empty($field['table'])) { $string .= $field['table'] . '.'; @@ -937,20 +953,37 @@ class views_plugin_query_default extends views_plugin_query{ if (!empty($field['distinct'])) { $string = "DISTINCT($string)"; } + if (!empty($field['count'])) { - $string = "COUNT($string)"; + // Retained for compatibility + $field['function'] = 'COUNT'; + } + + if (!empty($field['function'])) { + $info = $this->get_aggregation_info(); + if (!empty($info[$field['function']]['method']) && function_exists($info[$field['function']]['method'])) { + $string = $info[$field['function']]['method']($field['function'], $string); + } + $has_aggregate = TRUE; } elseif ($this->distinct && !in_array($fieldname, $this->groupby)) { $string = $GLOBALS['db_type'] == 'pgsql' ? "FIRST($string)" : $string; } - else { + elseif (empty($field['aggregate'])) { $non_aggregates[] = $fieldname; } + if ($field['alias']) { $string .= " AS $field[alias]"; } - $fields .= $string; + + if (!empty($field['distinct']) && empty($field['function'])) { + $distinct[] = $string; + } + else { + $fields[] = $string; + } if ($get_count_optimized) { // We only want the first field in this case. @@ -958,8 +991,8 @@ class views_plugin_query_default extends views_plugin_query{ } } - if ($has_aggregate || $this->groupby) { - $groupby = "GROUP BY " . implode(', ', array_unique(array_merge($this->groupby, $non_aggregates))) . "\n"; + if ($has_aggregate || $this->groupby) { + $groupby = "GROUP BY " . implode(', ', array_unique(array_merge($this->groupby, $non_aggregates))) . "\n"; if ($this->having) { $having = $this->condition_sql('having'); } @@ -974,7 +1007,7 @@ class views_plugin_query_default extends views_plugin_query{ $where = $this->condition_sql(); - $query = "SELECT $fields\n FROM {" . $this->base_table . "} $this->base_table \n$joins $where $groupby $having $orderby"; + $query = "SELECT " . implode(",\n", array_merge($distinct, $fields)) . "\n FROM {" . $this->base_table . "} $this->base_table \n$joins $where $groupby $having $orderby"; $replace = array('>' => '>', '<' => '<'); $query = strtr($query, $replace); @@ -1109,4 +1142,68 @@ class views_plugin_query_default extends views_plugin_query{ $view->query->add_field(NULL, "'" . $view->name . ':' . $view->current_display . "'", 'view_name'); } + function get_aggregation_info() { + // @todo -- need a way to get database specific and customized aggregation + // functions into here. + return array( + 'group' => array( + 'title' => t('Group results together'), + 'is aggregate' => FALSE, + ), + 'count' => array( + 'title' => t('Count'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'sum' => array( + 'title' => t('Sum'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'avg' => array( + 'title' => t('Average'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'min' => array( + 'title' => t('Minimum'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'max' => array( + 'title' => t('Maximum'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + ); + } +} + +function views_query_default_aggregation_method_simple($group_type, $field) { + return strtoupper($group_type) . '(' . $field . ')'; } diff --git a/views.module b/views.module index 238d83444f950c1768423c8b65ff5bfef9e2c069..1361a830293d4fe1d659177c761c953a7823bc6b 100644 --- a/views.module +++ b/views.module @@ -13,7 +13,14 @@ * Advertise the current views api version */ function views_api_version() { - return 2.0; + return '3.0-alpha1'; +} + +/** + * Views will not load plugins advertising a version older than this. + */ +function views_api_minimum_version() { + return '2'; } /** @@ -535,7 +542,8 @@ function views_get_module_apis() { foreach (module_implements('views_api') as $module) { $function = $module . '_views_api'; $info = $function(); - if (isset($info['api']) && $info['api'] == 2.000) { + if (version_compare($info['api'], views_api_minimum_version(), '>=') && + version_compare($info['api'], views_api_version(), '<=')) { if (!isset($info['path'])) { $info['path'] = drupal_get_path('module', $module); } @@ -624,19 +632,35 @@ function views_include_default_views() { * The name of the field this handler is from. * @param $key * The type of handler. i.e, sort, field, argument, filter, relationship + * @param $override + * Override the actual handler object with this class. Used for + * aggregation when the handler is redirected to the aggregation + * handler. * * @return * An instance of a handler object. May be views_handler_broken. */ -function views_get_handler($table, $field, $key) { +function views_get_handler($table, $field, $key, $override = NULL) { $data = views_fetch_data($table); + $handler = NULL; + if (isset($data[$field][$key])) { // Set up a default handler: if (empty($data[$field][$key]['handler'])) { $data[$field][$key]['handler'] = 'views_handler_' . $key; } - return _views_prepare_handler($data[$field][$key], $data, $field); + + if ($override) { + $data[$field][$key]['override handler'] = $override; + } + + $handler = _views_prepare_handler($data[$field][$key], $data, $field); + } + + if ($handler) { + return $handler; } + // DEBUG -- identify missing handlers vpr("Missing handler: $table $field $key"); $broken = array( @@ -967,7 +991,7 @@ function views_exposed_form(&$form_state) { } } } - + $form['submit'] = array( '#name' => '', // prevent from showing up in $_GET. '#type' => 'submit', diff --git a/views_ui.module b/views_ui.module index 49fe6138be179e88ab77216d8c7aefdb790e1a33..ca3745b13eaaa62a46ec138078dee55fd3cf9474 100644 --- a/views_ui.module +++ b/views_ui.module @@ -265,6 +265,7 @@ function views_ui_cache_set(&$view) { unset($view->display_handler); unset($view->current_display); unset($view->default_display); + $view->query = NULL; foreach (array_keys($view->display) as $id) { unset($view->display[$id]->handler); unset($view->display[$id]->default_display);