diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 97c8b3c64aea1aa7a9f14f70ee24e62ab6e930dd..14e0fd32d24d328652fb790e3e97cfaffe088981 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -79,6 +79,8 @@ Views 3.x-7.x-dev o #627402 by dereine: Aggregator description needed more controllable input filtering. o #699200 by xgretsch: Non-static functions cause strict warning messages. o #705668 by dereine: Fix glossary view, add tests for glossary view. + o #586668: Pagers turned into plugins to allow all kinds of new fun stuff with paging. + o #652712 by dagmar: Some pager settings were not getting properly stored with pluggable pagers. Views 6.x-3.x-dev o #396380 by merlinofchaos, dereine and dagmar: Initial support for GROUP BY queries!!!!!!!!!!!! diff --git a/handlers/views_handler_field_counter.inc b/handlers/views_handler_field_counter.inc index 01575c14693467bd8e8c3709a0c06c58c47345f8..54b5995f387391b7633e93f4759faf8d4242a4ee 100644 --- a/handlers/views_handler_field_counter.inc +++ b/handlers/views_handler_field_counter.inc @@ -29,10 +29,10 @@ class views_handler_field_counter extends views_handler_field { // Note: 1 is subtracted from the counter start value below because the // counter value is incremented by 1 at the end of this function. $count = is_numeric($this->options['counter_start']) ? $this->options['counter_start'] - 1 : 0; - $pager = $this->view->pager; + $pager = $this->view->query->pager; // Get the base count of the pager. - if ($pager['use_pager']) { - $count += ($pager['items_per_page'] * $pager['current_page']) + $pager['offset']; + if ($pager->use_pager()) { + $count += ($pager->get_items_per_page() * $pager->get_current_page() + $pager->get_offset()); } // Add the counter for the current site. $count += $this->view->row_index + 1; diff --git a/includes/plugins.inc b/includes/plugins.inc index 468af3e2def926d5758280ea845c940333ee6f3d..20769dcde313c3651658e8cb146fb4e4daf78bf0 100644 --- a/includes/plugins.inc +++ b/includes/plugins.inc @@ -259,6 +259,42 @@ function views_views_plugins() { 'help topic' => 'exposed-form-input-required', ), ), + 'pager' => array( + 'parent' => array( + 'no ui' => TRUE, + 'handler' => 'views_plugin_pager', + 'parent' => '', + ), + 'none' => array( + 'title' => t('Display all items'), + 'help' => t("Display all items that this view might find"), + 'handler' => 'views_plugin_pager_none', + 'help topic' => 'pager-none', + 'uses options' => TRUE, + ), + 'some' => array( + 'title' => t('Display a specified number of items'), + 'help' => t('Display a limited number items that this view might find.'), + 'handler' => 'views_plugin_pager_some', + 'help topic' => 'pager-some', + 'uses options' => TRUE, + ), + 'full' => array( + 'title' => t('Paged output, full pager'), + 'help' => t('Paged output, full Drupal style'), + 'handler' => 'views_plugin_pager_full', + 'help topic' => 'pager-full', + 'uses options' => TRUE, + ), + 'mini' => array( + 'title' => t('Paged output, mini pager'), + 'help' => t('Use the mini pager output.'), + 'handler' => 'views_plugin_pager_mini', + 'help topic' => 'pager-mini', + 'uses options' => TRUE, + 'parent' => 'full', + ), + ), ); } diff --git a/includes/view.inc b/includes/view.inc index e1f2af213863216c13711e83f45e90c87fa22bf1..e229c3a258e8c866a24293c9a5326e9ba32598fe 100644 --- a/includes/view.inc +++ b/includes/view.inc @@ -32,14 +32,8 @@ class view extends views_db_object { // Where the results of a query will go. var $result = array(); - // pager variables - var $pager = array( - 'use_pager' => FALSE, - 'items_per_page' => 10, - 'element' => 0, - 'offset' => 0, - 'current_page' => 0, - ); + // May be used to override the current page number. + var $current_page = NULL; // Places to put attached renderings: var $attachment_before = ''; @@ -93,43 +87,38 @@ class view extends views_db_object { $this->args = $args; } - /** - * Set the page size for ranged or pager queries - */ - function set_items_per_page($items_per_page) { - $this->pager['items_per_page'] = $items_per_page; - if (empty($items_per_page)) { - $this->pager['use_pager'] = FALSE; - } - } - /** * Change/Set the current page for the pager. */ function set_current_page($page) { - $this->pager['current_page'] = $page; + $this->current_page = $page; } /** - * Whether or not the pager should be used. + * Get the current page from the pager. */ - function set_use_pager($use_pager) { - $this->pager['use_pager'] = $use_pager; + function get_current_page() { + if (!empty($this->query->pager)) { + return $this->query->pager->get_current_page(); + } } /** - * The pager element id to use if use_apger is on + * Get the items per page from the pager. */ - function set_pager_element($pager_element) { - $this->pager['element'] = $pager_element; + function get_items_per_page() { + if (!empty($this->query->pager)) { + return $this->query->pager->get_items_per_page(); + } } /** - * How many records to skip. This does not function if use_pager is - * set. + * Get the pager offset from the pager. */ - function set_offset($offset) { - $this->pager['offset'] = $offset; + function get_offset() { + if (!empty($this->query->pager)) { + return $this->query->pager->get_offset(); + } } /** diff --git a/plugins/views_plugin_cache.inc b/plugins/views_plugin_cache.inc index 9031f98391f6fac24693735d0b661447a2352edc..4085b3cdb28c923d824aaa49daec4f39ae2f853e 100644 --- a/plugins/views_plugin_cache.inc +++ b/plugins/views_plugin_cache.inc @@ -68,7 +68,7 @@ class views_plugin_cache extends views_plugin { $data = array( 'result' => $this->view->result, 'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0, - 'pager' => $this->view->pager, + 'current_page' => $this->view->get_current_page(), ); cache_set($this->get_results_key(), $data, $this->table); break; @@ -94,12 +94,12 @@ class views_plugin_cache extends views_plugin { return FALSE; case 'results': // Values to set: $view->result, $view->total_rows, $view->execute_time, - // $view->pager['current_page']. + // $view->current_page. if ($cache = cache_get($this->get_results_key(), $this->table)) { if (!$cutoff || $cache->created > $cutoff) { $this->view->result = $cache->data['result']; $this->view->total_rows = $cache->data['total_rows']; - $this->view->pager = $cache->data['pager']; + $this->view->set_current_page = $cache->data['current_page']; $this->view->execute_time = 0; return TRUE; } diff --git a/plugins/views_plugin_display.inc b/plugins/views_plugin_display.inc index d5e85363f3445864157af253174aa17eab263b7e..3ba406a462056b37423a30b777ce8f0155ab89c2 100644 --- a/plugins/views_plugin_display.inc +++ b/plugins/views_plugin_display.inc @@ -41,6 +41,44 @@ class views_plugin_display extends views_plugin { } $this->unpack_options($this->options, $options); + + // Translate changed settings: + // Check if any of the previous values now managed by + // pluggable pagers have been changed. + // If yes, perform the conversion + $items_per_page = $this->get_option('items_per_page'); + $offset = $this->get_option('offset'); + $type = $this->get_option('use_pager'); + if ((!empty($items_per_page) && $items_per_page != 10) || !empty($offset) || !empty($type)) { + if (!$type) { + $type = $items_per_page ? 'some' : 'none'; + } + + if ($type == 1) { + $type = 'full'; + } + + $pager = array( + 'type' => $type, + 'options' => array( + 'offset' => $offset + ), + ); + + if ($items_per_page) { + $pager['options']['items_per_page'] = $items_per_page; + } + if ($id = $this->get_option('pager_element')) { + $pager['options']['id'] = $id; + } + + // Unset the previous options + // After edit and save the view they will be erased + $this->set_option('items_per_page', NULL); + $this->set_option('offset', NULL); + $this->set_option('use_pager', NULL); + $this->set_option('pager', $pager); + } } function destroy() { @@ -113,17 +151,10 @@ class views_plugin_display extends views_plugin { * Does the display have a pager enabled? */ function use_pager() { - if (!empty($this->definition['use pager'])) { - return $this->get_option('use_pager'); + $pager = $this->get_plugin('pager'); + if ($pager) { + return $pager->use_pager(); } - return FALSE; - } - - /** - * Does the display render the pager if it has it enabled? - */ - function render_pager() { - return $this->use_pager(); } /** @@ -189,7 +220,7 @@ class views_plugin_display extends views_plugin { 'empty' => array('empty', 'empty_format'), 'use_ajax' => array('use_ajax'), 'items_per_page' => array('items_per_page', 'offset', 'use_pager', 'pager_element'), - 'use_pager' => array('items_per_page', 'offset', 'use_pager', 'pager_element'), + 'pager' => array('pager'), 'use_more' => array('use_more', 'use_more_always', 'use_more_text'), 'link_display' => array('link_display'), 'distinct' => array('distinct'), @@ -261,9 +292,9 @@ class views_plugin_display extends views_plugin { 'use_ajax' => TRUE, 'items_per_page' => TRUE, - 'offset' => TRUE, 'use_pager' => TRUE, - 'pager_element' => TRUE, + 'offset' => TRUE, + 'pager' => TRUE, 'use_more' => TRUE, 'use_more_always' => TRUE, 'use_more_text' => TRUE, @@ -333,9 +364,6 @@ class views_plugin_display extends views_plugin { 'default' => FALSE, 'bool' => TRUE, ), - 'pager_element' => array( - 'default' => 0, - ), 'use_more' => array( 'default' => FALSE, 'bool' => TRUE, @@ -384,6 +412,12 @@ class views_plugin_display extends views_plugin { 'options' => array('default' => array(), 'export' => FALSE), ), ), + 'pager' => array( + 'contains' => array( + 'type' => array('default' => 'none', 'export' => 'export_plugin'), + 'options' => array('default' => array(), 'export' => FALSE), + ), + ), // Note that the styles have their options completely independent. // Like access and cache above, this is a legacy pattern and @@ -544,35 +578,42 @@ class views_plugin_display extends views_plugin { * Get the display or row plugin, if it exists. */ function get_plugin($type = 'style', $name = NULL) { - switch ($type) { - case 'style': - case 'row': - $option_name = $type . '_plugin'; - $options = $this->get_option($type . '_options'); - if (!$name) { - $name = $this->get_option($option_name); - } + static $cache = array(); + if (!isset($cache[$type][$name])) { + switch ($type) { + case 'style': + case 'row': + $option_name = $type . '_plugin'; + $options = $this->get_option($type . '_options'); + if (!$name) { + $name = $this->get_option($option_name); + } - break; - default: - $option_name = $type; - $options = $this->get_option($type); - if (!$name) { - $name = $options['type']; - } + break; + default: + $option_name = $type; + $options = $this->get_option($type); + if (!$name) { + $name = $options['type']; + } - // access & cache store their options as siblings with the - // type; all others use an 'options' array. - if ($type != 'access' && $type != 'cache') { - $options = $options['options']; - } - } + // access & cache store their options as siblings with the + // type; all others use an 'options' array. + if ($type != 'access' && $type != 'cache') { + $options = $options['options']; + } + } + + $plugin = views_get_plugin($type, $name); + if (!$plugin) { + return; + } - $plugin = views_get_plugin($type, $name); - if ($plugin) { $plugin->init($this->view, $this->display, $options); - return $plugin; + $cache[$type][$name] = $plugin; } + + return $cache[$type][$name]; } /** @@ -765,21 +806,26 @@ class views_plugin_display extends views_plugin { } if (!empty($this->definition['use pager'])) { - $options['use_pager'] = array( + + $pager_plugin = $this->get_plugin('pager'); + if (!$pager_plugin) { + // default to the no pager plugin. + $pager_plugin = views_get_plugin('pager', 'none'); + } + + $pager_str = $pager_plugin->summary_title(); + + $options['pager'] = array( 'category' => 'basic', 'title' => t('Use pager'), - 'value' => $this->get_option('use_pager') ? ($this->get_option('use_pager') === 'mini' ? t('Mini') : t('Yes')) : t('No'), + 'value' => $pager_str, 'desc' => t("Change this display's pager setting."), ); } - $items = intval($this->get_option('items_per_page')); - $options['items_per_page'] = array( - 'category' => 'basic', - 'title' => $this->use_pager() ? t('Items per page') : t('Items to display'), - 'value' => $items ? $items : t('Unlimited'), - 'desc' => t('Change how many items to display.'), - ); + if (!empty($pager_plugin->definition['uses options'])) { + $options['pager']['links']['pager_options'] = t('Change settings for this pager type.'); + } if (!empty($this->definition['use more'])) { $options['use_more'] = array( @@ -845,6 +891,10 @@ class views_plugin_display extends views_plugin { $options['cache']['links']['cache_options'] = t('Change settings for this caching type.'); } + if (!empty($access_plugin->definition['uses options'])) { + $options['access']['links']['access_options'] = t('Change settings for this access type.'); + } + if ($this->uses_link_display()) { // Only show the 'link display' if there is more than one option. $count = 0; @@ -988,35 +1038,6 @@ class views_plugin_display extends views_plugin { '#default_value' => $this->get_option('use_ajax') ? 1 : 0, ); break; - case 'use_pager': - $form['#title'] .= t('Use a pager for this view'); - $form['use_pager'] = array( - '#type' => 'radios', - '#options' => array(TRUE => t('Full pager'), 'mini' => t('Mini pager'), 0 => t('No')), - '#default_value' => $this->get_option('use_pager'), - ); - $form['pager_element'] = array( - '#type' => 'textfield', - '#title' => t('Pager element'), - '#description' => t("Unless you're experiencing problems with pagers related to this view, you should leave this at 0. If using multiple pagers on one page you may need to set this number to a higher value so as not to conflict within the ?page= array. Large values will add a lot of commas to your URLs, so avoid if possible."), - '#default_value' => intval($this->get_option('pager_element')), - ); - break; - case 'items_per_page': - $form['#title'] .= $this->use_pager() ? t('Items per page') : t('Items to display'); - - $form['items_per_page'] = array( - '#type' => 'textfield', - '#description' => t('The number of items to display per page. Enter 0 for no limit.'), - '#default_value' => intval($this->get_option('items_per_page')), - ); - $form['offset'] = array( - '#type' => 'textfield', - '#title' => t('Offset'), - '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed. Offset can not be used if items to display is 0; instead use a very large number there.'), - '#default_value' => intval($this->get_option('offset')), - ); - break; case 'use_more': $form['#title'] .= t('Add a more link to the bottom of the display.'); $form['use_more'] = array( @@ -1513,7 +1534,45 @@ class views_plugin_display extends views_plugin { $plugin->options_form($form['exposed_form_options'], $form_state); } break; + case 'pager': + $form['#title'] .= t('Select which pager, if any, to use for this view'); + $form['pager'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#tree' => TRUE, + ); + + $pager = $this->get_option('pager'); + $form['pager']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('pager'), + '#default_value' => $pager['type'], + ); + + $pager_plugin = views_fetch_plugin_data('pager', $pager['type']); + if (!empty($pager_plugin['uses options'])) { + $form['markup'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#value' => t('You may also adjust the !settings for the currently selected pager by clicking on the icon.', array('!settings' => $this->option_link(t('settings'), 'pager_options'))), + ); + } + + break; + case 'pager_options': + $plugin = $this->get_plugin('pager'); + $form['#title'] .= t('Pager options'); + if ($plugin) { + $form['#help_topic'] = $plugin->definition['help topic']; + + $form['pager_options'] = array( + '#tree' => TRUE, + ); + $plugin->options_form($form['pager_options'], $form_state); + } + break; } + } /** @@ -1594,6 +1653,12 @@ class views_plugin_display extends views_plugin { $plugin->options_validate($form['exposed_form_options'], $form_state); } break; + case 'pager_options': + $plugin = $this->get_plugin('pager'); + if ($plugin) { + $plugin->options_validate($form['pager_options'], $form_state); + } + break; } } @@ -1664,17 +1729,6 @@ class views_plugin_display extends views_plugin { case 'link_display': $this->set_option($section, $form_state['values'][$section]); break; - case 'use_ajax': - $this->set_option($section, (bool)$form_state['values'][$section]); - break; - case 'use_pager': - $this->set_option($section, $form_state['values'][$section]); - $this->set_option('pager_element', intval($form_state['values']['pager_element'])); - break; - case 'items_per_page': - $this->set_option($section, intval($form_state['values'][$section])); - $this->set_option('offset', intval($form_state['values']['offset'])); - break; case 'use_more': $this->set_option($section, intval($form_state['values'][$section])); $this->set_option('use_more_always', intval($form_state['values']['use_more_always'])); @@ -1761,6 +1815,33 @@ class views_plugin_display extends views_plugin { $this->set_option('exposed_form', $exposed_form); } break; + case 'pager': + $pager = $this->get_option('pager'); + if ($pager['type'] != $form_state['values']['pager']['type']) { + $plugin = views_get_plugin('pager', $form_state['values']['pager']['type']); + if ($plugin) { + // Because pagers have very similar options, let's allow pagers to + // try to carry the options over. + $plugin->init($this->view, $this->display, $pager['options']); + + $pager = array('type' => $form_state['values']['pager']['type'], 'options' => $plugin->options); + $this->set_option('pager', $pager); + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('pager_options')); + } + } + } + + break; + case 'pager_options': + $plugin = $this->get_plugin('pager'); + if ($plugin) { + $pager = $this->get_option('pager'); + $plugin->options_submit($form['pager_options'], $form_state); + $pager['options'] = $form_state['values'][$section]; + $this->set_option('pager', $pager); + } + break; } } @@ -1868,7 +1949,7 @@ class views_plugin_display extends views_plugin { * Render the 'more' link */ function render_more_link() { - if ($this->use_more() && ($this->view->total_rows > $this->view->pager['items_per_page'] || $this->use_more_always())) { + if ($this->use_more() && ($this->use_more_always() || $this->view->query->pager->has_more_records())) { $path = $this->get_path(); if ($path) { $path = $this->view->get_url(NULL, $path); @@ -1979,11 +2060,6 @@ class views_plugin_display extends views_plugin { */ function pre_execute() { $this->view->set_use_ajax($this->use_ajax()); - // Copy pager information from the display. - $this->view->set_use_pager($this->use_pager()); - $this->view->set_pager_element($this->get_option('pager_element')); - $this->view->set_items_per_page($this->get_option('items_per_page')); - $this->view->set_offset($this->get_option('offset')); if ($this->use_more()) { $this->view->get_total_rows = TRUE; } diff --git a/plugins/views_plugin_exposed_form.inc b/plugins/views_plugin_exposed_form.inc index 3f64dae8fee2c304a81d9efa33ce56f792b7fe12..8e8b31ad744620e5b4508d949216050d8025f067 100644 --- a/plugins/views_plugin_exposed_form.inc +++ b/plugins/views_plugin_exposed_form.inc @@ -14,12 +14,11 @@ class views_plugin_exposed_form extends views_plugin { * @param $display * The display handler. */ - function init(&$view, &$display) { + function init(&$view, &$display, $options = array()) { $this->view = &$view; $this->display = &$display; - $exposed_form = $display->handler->get_option('exposed_form'); - $this->unpack_options($this->options, $exposed_form['options']); + $this->unpack_options($this->options, $options); } /** @@ -108,9 +107,9 @@ class views_plugin_exposed_form extends views_plugin { ); } } - + function exposed_form_validate(&$form, &$form_state) { } - + /** * This function is executed when exposed form is submited. * @@ -119,9 +118,9 @@ class views_plugin_exposed_form extends views_plugin { * @param $form_state * A keyed array containing the current state of the form. * @param $exclude - * Nested array of keys to exclude of insert into + * Nested array of keys to exclude of insert into * $view->exposed_raw_input - */ + */ function exposed_form_submit(&$form, &$form_state, &$exclude) { if (isset($form_state['values']['op']) && $form_state['values']['op'] == t('Reset')) { $this->reset_form($form, $form_state); diff --git a/plugins/views_plugin_pager.inc b/plugins/views_plugin_pager.inc new file mode 100644 index 0000000000000000000000000000000000000000..03492b5d896d37c9b9888a10ee7ce1e042537ee0 --- /dev/null +++ b/plugins/views_plugin_pager.inc @@ -0,0 +1,173 @@ +view = &$view; + $this->display = &$display; + + $this->unpack_options($this->options, $options); + } + + /** + * Get how many items per page this pager will display. + * + * All but the leanest pagers should probably return a value here, so + * most pagers will not need to override this method. + */ + function get_items_per_page() { + return isset($this->options['items_per_page']) ? $this->options['items_per_page'] : 0; + } + + /** + * Get the page offset, or how many items to skip. + * + * Even pagers that don't actually page can skip items at the beginning, + * so few pagers will need to override this method. + */ + function get_offset() { + return isset($this->options['offset']) ? $this->options['offset'] : 0; + } + + /** + * Get the current page. + * + * If NULL, we do not know what the current page is. + */ + function get_current_page() { + return $this->current_page; + } + + /** + * Set the current page. + * + * @param $number + * If provided, the page number will be set to this. If NOT provided, + * the page number will be set from the global page array. + */ + function set_current_page($number = NULL) { + $this->current_page = $number; + } + + /** + * Get the total number of items. + * + * If NULL, we do not yet know what the total number of items are. + */ + function get_total_items() { + return $this->total_items; + } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * pager plugin. + */ + function summary_title() { + return t('Unknown'); + } + + /** + * Determine if this pager actually uses a pager. + * + * Only a couple of very specific pagers will set this to false. + */ + function use_pager() { + return TRUE; + } + + /** + * Determine if a pager needs a count query. + * + * If a pager needs a count query, a simple query + */ + function use_count_query() { + return TRUE; + } + + /** + * Execute the count query, which will be done just prior to the query + * itself being executed. + */ + function execute_count_query(&$count_query) { + $this->total_items = $count_query->execute(); + if (!empty($this->options['offset'])) { + $this->total_items -= $this->options['offset']; + } + + $this->update_page_info(); + return $this->total_items; + } + + /** + * If there are pagers that need global values set, this method can + * be used to set them. It will be called when the count query is run. + */ + function update_page_info() { + + } + + /** + * Modify the query for paging + * + * This is called during the build phase and can directly modify the query. + */ + function query() { } + + /** + * Perform any needed actions just prior to the query executing. + */ + function pre_execute(&$query) { } + + /** + * Perform any needed actions just after the query executing. + */ + function post_execute(&$result) { } + + /** + * Render the pager. + * + * Called during the view render process, this will render the + * pager. + * + * @param $input + * Any extra GET parameters that should be retained, such as exposed + * input. + */ + function render($input) { } + + /** + * Determine if there are more records available. + * + * This is primarily used to control the display of a more link. + */ + function has_more_records() { + return $this->get_items_per_page() + && $this->total_items > (intval($this->current_page) + 1) * $this->get_items_per_page(); + } +} diff --git a/plugins/views_plugin_pager_full.inc b/plugins/views_plugin_pager_full.inc new file mode 100644 index 0000000000000000000000000000000000000000..60c66adeb134da61557d1bc3882a996efb1e4485 --- /dev/null +++ b/plugins/views_plugin_pager_full.inc @@ -0,0 +1,119 @@ +options['offset'])) { + return format_plural($this->options['items_per_page'], 'Paged, @count item, skip @skip', 'Paged, @count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], 'Paged, @count item', 'Paged, @count items', array('@count' => $this->options['items_per_page'])); + } + + function option_definition() { + $options = parent::option_definition(); + $options['items_per_page'] = array('default' => 10); + $options['offset'] = array('default' => 0); + $options['id'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + $form['items_per_page'] = array( + '#title' => t('Items per page'), + '#type' => 'textfield', + '#description' => t('The number of items to display per page. Enter 0 for no limit.'), + '#default_value' => $this->options['items_per_page'], + ); + + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + + $form['id'] = array( + '#type' => 'textfield', + '#title' => t('Pager ID'), + '#description' => t("Unless you're experiencing problems with pagers related to this view, you should leave this at 0. If using multiple pagers on one page you may need to set this number to a higher value so as not to conflict within the ?page= array. Large values will add a lot of commas to your URLs, so avoid if possible."), + '#default_value' => $this->options['id'], + ); + } + + function query() { + $this->view->query->set_limit($this->options['items_per_page']); + $this->view->query->set_offset($this->current_page * $this->options['items_per_page'] + $this->options['offset']); + } + + function render($input) { + $pager_theme = views_theme_functions('pager', $this->view, $this->display); + $output = theme($pager_theme, array( + 'tags' => $input, 'quantity' => $this->options['items_per_page'], 'element' => $this->options['id'])); + return $output; + } + + /** + * Set the current page. + * + * @param $number + * If provided, the page number will be set to this. If NOT provided, + * the page number will be set from the global page array. + */ + function set_current_page($number = NULL) { + if (isset($number)) { + $this->current_page = $number; + return; + } + + // If the current page number was not prespecified, default to pulling it from 'page' + // based upon + global $pager_page_array; + // Extract the ['page'] info. + $pager_page_array = isset($_GET['page']) ? explode(',', $_GET['page']) : array(); + + $this->current_page = 0; + if (!empty($pager_page_array[$this->options['id']])) { + $this->current_page = intval($pager_page_array[$this->options['id']]); + } + } + + /** + * Update global paging info. + * + * This is called after the count query has been run to set the total + * items available and to update the current page if the requested + * page is out of range. + */ + function update_page_info() { + // Dump information about what we already know into the globals. + global $pager_page_array, $pager_total, $pager_total_items; + + // Set the item count for the pager. + $pager_total_items[$this->options['id']] = $this->total_items; + // Calculate and set the count of available pages. + $pager_total[$this->options['id']] = ceil($pager_total_items[$this->options['id']] / $this->get_items_per_page()); + + // See if the requested page was within range: + if ($this->current_page < 0) { + $this->current_page = 0; + } + else if ($this->current_page >= $pager_total[$this->options['id']]) { + // Pages are numbered from 0 so if there are 10 pages, the last page is 9. + $this->current_page = $pager_total[$this->options['id']] - 1; + } + + // Put this number in to guarantee that we do not generate notices when the pager + // goes to look for it later. + $pager_page_array[$this->options['id']] = $this->current_page; + } + +} diff --git a/plugins/views_plugin_pager_mini.inc b/plugins/views_plugin_pager_mini.inc new file mode 100644 index 0000000000000000000000000000000000000000..30ee9e84712bd0caf5f171f8453e0be2a96407b8 --- /dev/null +++ b/plugins/views_plugin_pager_mini.inc @@ -0,0 +1,22 @@ +options['offset'])) { + return format_plural($this->options['items_per_page'], 'Mini pager, @count item, skip @skip', 'Mini pager, @count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], 'Mini pager, @count item', 'Mini pager, @count items', array('@count' => $this->options['items_per_page'])); + } + + function render($input) { + $pager_theme = views_theme_functions('views_mini_pager', $this->view, $this->display); + return theme($pager_theme, array( + 'tags' => $input, 'quantity' => $this->options['items_per_page'], 'element' => $this->options['id'])); + } +} diff --git a/plugins/views_plugin_pager_none.inc b/plugins/views_plugin_pager_none.inc new file mode 100644 index 0000000000000000000000000000000000000000..3984e4db2707854c97776f5b83235c8b29b0dbbf --- /dev/null +++ b/plugins/views_plugin_pager_none.inc @@ -0,0 +1,62 @@ +options['offset'])) { + return t('All items, skip @skip', array('@skip' => $this->options['offset'])); + } + return t('All items'); + } + + function option_definition() { + $options = parent::option_definition(); + $options['offset'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + } + + function use_pager() { + return FALSE; + } + + function use_count_query() { + return FALSE; + } + + function get_items_per_page() { + return 0; + } + + function execute_count_query(&$count_query) { + // If we are displaying all items, never count. But we can update the count in post_execute. + } + + function post_execute($result) { + $this->total_items = count($result); + } + + function query() { + // The only query modifications we might do are offsets. + if (!empty($this->options['offset'])) { + $this->view->query->set_offset($this->options['offset']); + } + } +} diff --git a/plugins/views_plugin_pager_some.inc b/plugins/views_plugin_pager_some.inc new file mode 100644 index 0000000000000000000000000000000000000000..2079946abe255cd5a8a62e3e71db82b4c814ecd1 --- /dev/null +++ b/plugins/views_plugin_pager_some.inc @@ -0,0 +1,56 @@ +options['offset'])) { + return format_plural($this->options['items_per_page'], '@count item, skip @skip', '@count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], '@count item', '@count items', array('@count' => $this->options['items_per_page'])); + } + + function option_definition() { + $options = parent::option_definition(); + $options['items_per_page'] = array('default' => 10); + $options['offset'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + $form['items_per_page'] = array( + '#title' => t('Items to display'), + '#type' => 'textfield', + '#description' => t('The number of items to display per page. Enter 0 for no limit.'), + '#default_value' => $this->options['items_per_page'], + ); + + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + } + + function use_pager() { + return FALSE; + } + + function use_count_query() { + return FALSE; + } + + function query() { + $this->view->query->set_limit($this->options['items_per_page']); + $this->view->query->set_offset($this->options['offset']); + } +} diff --git a/plugins/views_plugin_query.inc b/plugins/views_plugin_query.inc index 183c0025c549e9948f85383994d5ac5b9a348e46..4aa75350b16078ee521f9f48c150d716b7572fcc 100644 --- a/plugins/views_plugin_query.inc +++ b/plugins/views_plugin_query.inc @@ -9,6 +9,10 @@ * Object used to create a SELECT query. */ class views_plugin_query extends views_plugin { + /** + * A pager plugin that should be provided by the display. + */ + var $pager = NULL; /** * Constructor; Create the basic query object and fill with default values. @@ -57,4 +61,37 @@ class views_plugin_query extends views_plugin { * If NULL, aggregation is not allowed. */ function get_aggregation_info() { } + + /** + * Set a LIMIT on the query, specifying a maximum number of results. + */ + function set_limit($limit) { + $this->limit = $limit; + } + + /** + * Set an OFFSET on the query, specifying a number of results to skip + */ + function set_offset($offset) { + $this->offset = $offset; + } + + function init_pager(&$view) { + // @todo -- probably this should be a method on the display rather than directly calling + // get plugin? + $this->pager = $view->display_handler->get_plugin('pager'); + } + + /** + * Render the pager, if necessary. + */ + function render_pager() { + if (!empty($this->pager) && $this->pager->use_pager()) { + $exposed_input = isset($this->view->exposed_data_raw) ? $this->view->exposed_data_raw : NULL; + return $this->pager->render($exposed_input); + } + return ''; + } + + } diff --git a/plugins/views_plugin_query_default.inc b/plugins/views_plugin_query_default.inc index 83dee821d077c5067da0d93ebf40533143c7d3de..16555a5e45b74f5503f0849ee871bb3cbe799c56 100644 --- a/plugins/views_plugin_query_default.inc +++ b/plugins/views_plugin_query_default.inc @@ -1058,6 +1058,14 @@ class views_plugin_query_default extends views_plugin_query { * Builds the necessary info to execute the query. */ function build(&$view) { + $this->init_pager($view); + if ($this->pager->use_pager()) { + $this->pager->set_current_page($view->current_page); + } + + // Let the pager modify the query to add limits. + $this->pager->query(); + $view->build_info['query'] = $this->query(); $view->build_info['count_query'] = $this->query(TRUE); } @@ -1067,7 +1075,7 @@ class views_plugin_query_default extends views_plugin_query { * values. * * Values to set: $view->result, $view->total_rows, $view->execute_time, - * $view->pager['current_page']. + * $view->current_page. */ function execute(&$view) { $external = FALSE; // Whether this query will run against an external database. @@ -1080,6 +1088,9 @@ class views_plugin_query_default extends views_plugin_query { // We have already build a working count query. Views optimeses it automatically. //$count_query = $count_query->countQuery(); + // against strict problems + $args = array(); + // Add additional arguments as a fake condition. // XXX: this doesn't work... because PDO mandates that all bound arguments // are used on the query. TODO: Find a better way to do this. @@ -1095,39 +1106,19 @@ class views_plugin_query_default extends views_plugin_query { } $start = microtime(TRUE); - if (!empty($view->pager['items_per_page'])) { - // We no longer use pager_query() here because pager_query() does not - // support an offset. This is fine as we don't actually need pager - // query; we've already been doing most of what it does, and we - // just need to do a little more playing with globals. - if (!empty($view->pager['use_pager']) || !empty($view->get_total_rows)) { - $view->total_rows = $count_query->execute()->fetchField() - $view->pager['offset']; - } - if (!empty($view->pager['use_pager'])) { - // Dump information about what we already know into the globals. - global $pager_page_array, $pager_total, $pager_total_items; - // Set the item count for the pager. - $pager_total_items[$view->pager['element']] = $view->total_rows; - // Calculate and set the count of available pages. - $pager_total[$view->pager['element']] = ceil($pager_total_items[$view->pager['element']] / $view->pager['items_per_page']); - - // What page was requested: - $pager_page_array = isset($_GET['page']) ? explode(',', $_GET['page']) : array(); - - // If the requested page was within range. $view->pager['current_page'] - // defaults to 0 so we don't need to set it in an out-of-range condition. - if (!empty($pager_page_array[$view->pager['element']])) { - $page = intval($pager_page_array[$view->pager['element']]); - if ($page > 0 && $page < $pager_total[$view->pager['element']]) { - $view->pager['current_page'] = $page; - } - } - $pager_page_array[$view->pager['element']] = $view->pager['current_page']; - } + if ($this->pager->use_count_query() || !empty($view->get_total_rows)) { + $this->pager->execute_count_query($count_query, $args); + } - $offset = $view->pager['current_page'] * $view->pager['items_per_page'] + $view->pager['offset']; - $query->range($offset, $view->pager['items_per_page']); + // Let the pager modify the query to add limits. + $this->pager->pre_execute($query, $args); + + if (!empty($this->limit) || !empty($this->offset)) { + // We can't have an offset without a limit, so provide a very large limit instead. + $limit = intval(!empty($this->limit) ? $this->limit : 999999); + $offset = intval(!empty($this->offset) ? $this->offset : 0); + $query->range($offset, $limit); } try { @@ -1138,10 +1129,10 @@ class views_plugin_query_default extends views_plugin_query { $view->result[] = $item; } - // If we already know how many items we have even if we did not run the - // count query, go ahead and set that value: - if (empty($this->pager['items_per_page'])) { - $this->total_rows = count($view->result); + $this->pager->post_execute($view->result); + + if ($this->pager->use_pager()) { + $view->total_rows = $this->pager->get_total_items(); } } catch (Exception $e) { diff --git a/tests/views_pager.test b/tests/views_pager.test index ea8d6389aea909512f71a982c7d180a08a1da822..1c4d575e292cb6fd3107a03e5eac13c36d8fdf01 100644 --- a/tests/views_pager.test +++ b/tests/views_pager.test @@ -28,13 +28,13 @@ class ViewsPagerTest extends DrupalWebTestCase { $this->drupalLogin($admin_user); // Test behaviour described in http://drupal.org/node/652712#comment-2354918. - $this->drupalGet('admin/build/views/edit/frontpage'); + $this->drupalGet('admin/structure/views/edit/frontpage'); $edit = array( 'pager_options[items_per_page]' => 20, ); - $this->drupalPost('admin/build/views/nojs/display/frontpage/default/pager_options', $edit, t('Update')); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/default/pager_options', $edit, t('Update')); $this->assertText('20 items'); // Change type and check whether the type is new type is stored. @@ -42,7 +42,8 @@ class ViewsPagerTest extends DrupalWebTestCase { $edit = array( 'pager[type]' => 'mini', ); - $this->drupalPost('admin/build/views/nojs/display/frontpage/default/pager', $edit, t('Update')); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/default/pager', $edit, t('Update')); + $this->drupalGet('admin/structure/views/edit/frontpage'); $this->assertText('Mini pager', 'Changed pager plugin, should change some text'); // Test behaviour described in http://drupal.org/node/652712#comment-2354400 @@ -50,19 +51,20 @@ class ViewsPagerTest extends DrupalWebTestCase { // Make it editable in the admin interface. $view->save(); - $this->drupalGet('admin/build/views/test_store_pager_settings'); + $this->drupalGet('admin/structure/views/test_store_pager_settings'); $edit = array(); $edit = array( 'pager[type]' => 'full', ); - $this->drupalPost('admin/build/views/nojs/display/test_store_pager_settings/default/pager', $edit, t('Update')); - $this->assert('Paged'); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager', $edit, t('Update')); + $this->drupalGet('admin/structure/views/edit/test_store_pager_settings'); + $this->assertText('Paged'); $edit = array( 'pager_options[items_per_page]' => 20, ); - $this->drupalPost('admin/build/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Update')); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Update')); $this->assertText('20 items'); // add new display and test the settings again, by override it. @@ -70,19 +72,20 @@ class ViewsPagerTest extends DrupalWebTestCase { 'display' => 'page', ); // Add a display and override the pager settings. - $this->drupalPost('admin/build/views/edit/test_store_pager_settings', $edit, t('Add display')); - $this->drupalPost('admin/build/views/nojs/display/test_store_pager_settings/page_1/pager', array(), t('Override')); + $this->drupalPost('admin/structure/views/edit/test_store_pager_settings', $edit, t('Add display')); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/page_1/pager', array(), t('Override')); $edit = array( 'pager[type]' => 'mini', ); - $this->drupalPost('admin/build/views/nojs/display/test_store_pager_settings/page_1/pager', array(), t('Update')); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/page_1/pager', $edit, t('Update')); + $this->drupalGet('admin/structure/views/edit/test_store_pager_settings'); $this->assertText('Mini pager', 'Changed pager plugin, should change some text'); $edit = array( 'pager_options[items_per_page]' => 10, ); - $this->drupalPost('admin/build/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Update')); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Update')); $this->assertText('20 items'); } @@ -105,7 +108,192 @@ class ViewsPagerTest extends DrupalWebTestCase { $handler->display->display_options['exposed_form']['type'] = 'basic'; $handler->display->display_options['pager']['type'] = 'none'; $handler->display->display_options['style_plugin'] = 'default'; - $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['row_plugin'] = 'node'; return $view; } + + /** + * Test the none-pager-query. + */ + public function testNoLimit() { + // Create 11 nodes and take sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerNoLimit(); + $view->set_display('default'); + $view->execute(); + $this->assertEqual(count($view->result), 11, t('Take sure that every item is returned in the result')); + + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerNoLimit(); + $view->set_display('default'); + + $pager = array( + 'type' => 'none', + 'options' => array( + 'offset' => 3, + ), + ); + $view->display_handler->set_option('pager', $pager); + $view->execute(); + + $this->assertEqual(count($view->result), 8, t('Take sure that every item beside the first three is returned in the result')); + + // Check some public functions. + $this->assertFalse($view->query->pager->use_pager()); + $this->assertFalse($view->query->pager->use_count_query()); + $this->assertEqual($view->query->pager->get_items_per_page(), 0); + } + + public function viewsPagerNoLimit() { + $view = new view; + $view->name = 'test_pager_none'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Defaults */ + $handler = $view->new_display('default', 'Defaults', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'none'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + return $view; + } + + /** + * Test the some pager plugin. + */ + public function testLimit() { + // Create 11 nodes and take sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerLimit(); + $view->set_display('default'); + $view->execute(); + $this->assertEqual(count($view->result), 5, t('Take sure that only a certain count of items is returned')); + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerLimit(); + $view->set_display('default'); + + $pager = array( + 'type' => 'none', + 'options' => array( + 'offset' => 8, + 'items_per_page' => 5, + ), + ); + $view->display_handler->set_option('pager', $pager); + $view->execute(); + $this->assertEqual(count($view->result), 3, t('Take sure that only a certain count of items is returned')); + + // Check some public functions. + $this->assertFalse($view->query->pager->use_pager()); + $this->assertFalse($view->query->pager->use_count_query()); + } + + public function viewsPagerLimit() { + $view = new view; + $view->name = 'test_pager_some'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Defaults */ + $handler = $view->new_display('default', 'Defaults', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['pager']['options']['offset'] = 0; + $handler->display->display_options['pager']['options']['items_per_page'] = 5; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + return $view; + } + + /** + * Test the normal pager. + */ + public function testNormalPager() { + // Create 11 nodes and take sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerFull(); + $view->set_display('default'); + $view->execute(); + $this->assertEqual(count($view->result), 5, t('Take sure that only a certain count of items is returned')); + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerFull(); + $view->set_display('default'); + + $pager = array( + 'type' => 'full', + 'options' => array( + 'offset' => 8, + 'items_per_page' => 5, + ), + ); + $view->display_handler->set_option('pager', $pager); + $view->execute(); + $this->assertEqual(count($view->result), 3, t('Take sure that only a certain count of items is returned')); + + // TODO test number of pages. + } + + function ViewsPagerFull() { + $view = new view; + $view->name = 'test_pager_full'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Defaults */ + $handler = $view->new_display('default', 'Defaults', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '5'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + + return $view; + } + + + /** + * Test the minipager. + */ + public function testMiniPager() { + // the functionality is the same as the normal pager, so i don't know what to test here. + } } diff --git a/theme/theme.inc b/theme/theme.inc index a8f7f9a8aea2e3926d01f9dbf953164ec54ebcc0..1a7fc97ac3b56971869d8a704699d56d923f49e3 100644 --- a/theme/theme.inc +++ b/theme/theme.inc @@ -72,24 +72,11 @@ function template_preprocess_views_view(&$vars) { } $vars['more'] = $view->display_handler->render_more_link(); $vars['feed_icon'] = !empty($view->feed_icon) ? $view->feed_icon : ''; + $vars['pager'] = $view->query->render_pager(); $vars['attachment_before'] = !empty($view->attachment_before) ? $view->attachment_before : ''; $vars['attachment_after'] = !empty($view->attachment_after) ? $view->attachment_after : ''; - $vars['pager'] = ''; - - $exposed_input = isset($view->exposed_data_raw) ? $view->exposed_data_raw : NULL; - if ($view->display_handler->render_pager()) { - $pager_type = ($view->pager['use_pager'] === 'mini' ? 'views_mini_pager' : 'pager'); - $pager_theme = views_theme_functions($pager_type, $view, $view->display_handler->display); - $vars['pager'] = theme($pager_theme, - array( - 'tags' => $exposed_input, - 'element' => $view->pager['element'], - ) - ); - } - // if administrator, add some links. These used to be tabs, but this is better. if (user_access('administer views') && module_exists('views_ui') && empty($view->hide_admin_links) && !variable_get('views_no_hover_links', FALSE)) { $vars['admin_links_raw'] = array( diff --git a/views.info b/views.info index ffb4c4ac9e741e1c7fe27e1fd02c3751dfbb88a2..c783eb192a060e802acd90f0a2d8c02af1c8e90e 100644 --- a/views.info +++ b/views.info @@ -205,6 +205,11 @@ files[] = plugins/views_plugin_exposed_form_basic.inc files[] = plugins/views_plugin_exposed_form.inc files[] = plugins/views_plugin_exposed_form_input_required.inc files[] = plugins/views_plugin_display_page.inc +files[] = plugins/views_plugin_pager.inc +files[] = plugins/views_plugin_pager_full.inc +files[] = plugins/views_plugin_pager_mini.inc +files[] = plugins/views_plugin_pager_none.inc +files[] = plugins/views_plugin_pager_some.inc files[] = plugins/views_plugin_query.inc files[] = plugins/views_plugin_query_default.inc files[] = plugins/views_plugin_row.inc