? views_caching.patch
Index: views.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/views.install,v
retrieving revision 1.46
diff -u -p -r1.46 views.install
--- views.install	7 Apr 2009 20:39:51 -0000	1.46
+++ views.install	22 May 2009 13:26:08 -0000
@@ -126,6 +126,11 @@ function views_schema_1() {
   );
 
   $schema['cache_views'] = drupal_get_schema_unprocessed('system', 'cache');
+  
+  $schema['cache_views_data'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_views_data']['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.';
+  $schema['cache_views_data']['fields']['serialized']['default'] = 1;
+
 
   $schema['views_object_cache'] = array(
     'description' => 'A special cache used to store objects that are being edited; it serves to save state in an ordinarily stateless environment.',
@@ -239,3 +244,18 @@ function views_update_6004() {
 
   return $ret;
 }
+
+/**
+ * Add the cache_views_data table to support standard caching.
+ */
+function views_update_6005() {
+  $ret = array();
+  
+  $table = drupal_get_schema_unprocessed('system', 'cache');
+  $table['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.';
+  $table['fields']['serialized']['default'] = 1;
+
+  db_create_table($ret, 'cache_views_data', $table);
+
+  return $ret;
+}
Index: includes/plugins.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/plugins.inc,v
retrieving revision 1.152.2.1
diff -u -p -r1.152.2.1 plugins.inc
--- includes/plugins.inc	20 May 2009 03:03:38 -0000	1.152.2.1
+++ includes/plugins.inc	22 May 2009 13:26:09 -0000
@@ -247,6 +247,26 @@ function views_views_plugins() {
         'handler' => 'views_plugin_query_default'
       ),
     ),
+    'cache' => array(
+      'parent' => array(
+        'no ui' => TRUE,
+        'handler' => 'views_plugin_cache',
+        'parent' => '',
+      ),
+      'none' => array(
+        'title' => t('None'),
+        'help' => t('No caching of Views data.'),
+        'handler' => 'views_plugin_cache_none',
+        'help topic' => 'cache-none',
+      ),
+      'time' => array(
+        'title' => t('Time-based'),
+        'help' => t('Simple time-based caching of data.'),
+        'handler' => 'views_plugin_cache_time',
+        'uses options' => TRUE,
+        'help topic' => 'cache-time',
+      ),
+    ),
   );
 }
 
@@ -256,7 +276,7 @@ function views_views_plugins() {
  * @return Nested array of plugins, grouped by type.
  */
 function views_discover_plugins() {
-  $cache = array('display' => array(), 'style' => array(), 'row' => array(), 'argument default' => array(), 'argument validator' => array(), 'access' => array());
+  $cache = array('display' => array(), 'style' => array(), 'row' => array(), 'argument default' => array(), 'argument validator' => array(), 'access' => array(), 'cache' => array());
   // Get plugins from all mdoules.
   foreach (module_implements('views_plugins') as $module) {
     $function = $module . '_views_plugins';
Index: includes/view.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/view.inc,v
retrieving revision 1.151.2.3
diff -u -p -r1.151.2.3 view.inc
--- includes/view.inc	20 May 2009 03:09:36 -0000	1.151.2.3
+++ includes/view.inc	22 May 2009 13:26:09 -0000
@@ -685,7 +685,23 @@ class view extends views_db_object {
       $function($this);
     }
 
-    $this->query->execute($this);
+    // Check for already-cached results.
+    if ($this->preview) {
+      $cache = FALSE;
+    }
+    else {
+      $cache = $this->display_handler->get_cache_plugin();
+    }
+    if ($cache && $cache->cache_get('results')) {
+      vpr('Used cached results');
+    }
+    else {
+      $this->query->execute($this);
+      if ($cache) {
+        $cache->cache_set('results');
+      }
+    }
+    
     $this->executed = TRUE;
   }
 
@@ -693,9 +709,6 @@ class view extends views_db_object {
    * Render this view for display.
    */
   function render($display_id = NULL) {
-    // Check for cached output.
-    // @todo: Implement this
-
     $this->execute($display_id);
 
     // Check to see if the build failed.
@@ -707,34 +720,50 @@ class view extends views_db_object {
     if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) {
       $this->start_query_capture();
     }
-
-    // Initialize the style plugin.
-    $this->init_style();
-
-    $this->style_plugin->pre_render($this->result);
-
-    // Let modules modify the view just prior to executing it.
-    foreach (module_implements('views_pre_render') as $module) {
-      $function = $module . '_views_pre_render';
-      $function($this);
+  
+    // Check for already-cached output.
+    if ($this->preview) {
+      $cache = FALSE;
     }
-
-    // Give field handlers the opportunity to perform additional queries
-    // using the entire resultset prior to rendering.
-    if ($this->style_plugin->uses_fields()) {
-      foreach ($this->field as $id => $handler) {
-        if (!empty($this->field[$id])) {
-          $this->field[$id]->pre_render($this->result);
+    else {
+      $cache = $this->display_handler->get_cache_plugin();
+    }
+    if ($cache && $cache->cache_get('output')) {
+      vpr('Used cached output');
+    }
+    else {
+      // Initialize the style plugin.
+      $this->init_style();
+  
+      $this->style_plugin->pre_render($this->result);
+  
+      // Let modules modify the view just prior to executing it.
+      foreach (module_implements('views_pre_render') as $module) {
+        $function = $module . '_views_pre_render';
+        $function($this);
+      }
+  
+      // Give field handlers the opportunity to perform additional queries
+      // using the entire resultset prior to rendering.
+      if ($this->style_plugin->uses_fields()) {
+        foreach ($this->field as $id => $handler) {
+          if (!empty($this->field[$id])) {
+            $this->field[$id]->pre_render($this->result);
+          }
         }
       }
+      $this->display_handler->output = $this->display_handler->render();
+      if ($cache) {
+        $cache->cache_set('output');
+      }
     }
 
-    $output = $this->display_handler->render();
     if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) {
       $this->end_query_capture();
     }
     $this->render_time = views_microtime() - $start;
-    return $output;
+
+    return $this->display_handler->output;
   }
 
   /**
Index: plugins/views_plugin_cache.inc
===================================================================
RCS file: plugins/views_plugin_cache.inc
diff -N plugins/views_plugin_cache.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ plugins/views_plugin_cache.inc	22 May 2009 13:26:09 -0000
@@ -0,0 +1,74 @@
+<?php
+// $Id$
+
+/**
+ * The base plugin to handle caching.
+ *
+ * @ingroup views_cache_plugins
+ */
+class views_plugin_cache extends views_plugin {
+  /**
+   * Initialize the plugin.
+   *
+   * @param $view
+   *   The view object.
+   * @param $display
+   *   The display handler.
+   */
+  function init(&$view, &$display) {
+    $this->view = &$view;
+    $this->display = &$display;
+    $this->options = array();
+
+    if (is_object($display->handler)) {
+    // Note: The below is read only.
+      $this->options = $display->handler->get_option('cache');
+    }
+  }
+
+  /**
+   * Retrieve the default options when this is a new access
+   * control plugin
+   */
+  function option_defaults(&$options) { }
+
+  /**
+   * Provide the default form for setting options.
+   */
+  function options_form(&$form, &$form_state) { }
+
+  /**
+   * 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
+   * access control.
+   */
+  function summary_title() {
+    return t('Unknown');
+  }
+
+  /**
+   * Save data to the cache.
+   */
+  function cache_set($type, $data = NULL) { }
+
+  /**
+   * Retrieve data from the cache.
+   */
+  function cache_get($type) {
+    return FALSE;
+  }
+
+  /**
+   * Clear out cached data for the view.
+   */
+  function cache_flush() { }
+}
Index: plugins/views_plugin_cache_none.inc
===================================================================
RCS file: plugins/views_plugin_cache_none.inc
diff -N plugins/views_plugin_cache_none.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ plugins/views_plugin_cache_none.inc	22 May 2009 13:26:09 -0000
@@ -0,0 +1,11 @@
+<?php
+// $Id$
+
+/**
+ * Caching plugin that provides no caching at all.
+ */
+class views_plugin_cache_none extends views_plugin_cache {
+  function summary_title() {
+    return t('None');
+  }
+}
Index: plugins/views_plugin_cache_time.inc
===================================================================
RCS file: plugins/views_plugin_cache_time.inc
diff -N plugins/views_plugin_cache_time.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ plugins/views_plugin_cache_time.inc	22 May 2009 13:26:09 -0000
@@ -0,0 +1,171 @@
+<?php
+// $Id$
+
+/**
+ * Simple caching of query results for Views displays.
+ */
+class views_plugin_cache_time extends views_plugin_cache {
+  /**
+   * Initialize the plugin.
+   *
+   * @param $view
+   *   The view object.
+   * @param $display
+   *   The display handler.
+   */
+  function init(&$view, &$display) {
+    $this->view = &$view;
+    $this->display = &$display;
+    $this->options = array();
+
+    if (is_object($display->handler)) {
+    // Note: The below is read only.
+      $this->options = $display->handler->get_option('cache');
+    }
+  }
+
+  /**
+   * Retrieve the default options when this is a new access
+   * control plugin
+   */
+  function option_defaults(&$options) {
+    $options['results_lifespan'] = 3600;
+    $options['output_lifespan'] = 3600;
+  }
+
+  /**
+   * Provide the default form for setting options.
+   */
+  function options_form(&$form, &$form_state) {
+    $options = array(60, 300, 1800, 3600, 21600, 518400);
+    $options = drupal_map_assoc($options, 'format_interval');
+    $options = array(-1 => t('Never cache')) + $options;
+                  
+    $form['results_lifespan'] = array(
+      '#type' => 'select',
+      '#title' => t('Query results'),
+      '#description' => t('The length of time raw query results should be cached.'),
+      '#options' => $options,
+      '#default_value' => $this->options['results_lifespan'],
+    );
+    $form['output_lifespan'] = array(
+      '#type' => 'select',
+      '#title' => t('Rendered output'),
+      '#description' => t('The length of time rendered HTML output should be cached.'),
+      '#options' => $options,
+      '#default_value' => $this->options['output_lifespan'],
+    );
+  }
+
+  /**
+   * 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
+   * access control.
+   */
+  function summary_title() {
+    return format_interval($this->options['results_lifespan'], 1) .'/'. format_interval($this->options['output_lifespan'], 1);
+  }
+
+  /**
+   * Save data to the cache.
+   */
+  function cache_set($type) {
+    if ($lifespan = $this->options[$type .'_lifespan']) {
+      $cutoff = time() - $lifespan;
+    }
+    else {
+      return FALSE;
+    }
+
+    switch ($type) {
+      case 'query':
+        // Not supported currently, but this is certainly where we'd put it.
+        break;
+      case 'results':
+        $data = array(
+          'result' => $this->view->result,
+          'total_rows' => $this->view->total_rows,
+          'pager' => $this->view->pager,
+        );
+        cache_set($this->_results_key(), $data, 'cache_views_data');
+        break;
+      case 'output':
+        cache_set($this->_output_key(), $this->view->display_handler->output, 'cache_views_data');
+        break;
+    }
+  }
+
+  /**
+   * Retrieve data from the cache.
+   */
+  function cache_get($type) {
+    if ($lifespan = $this->options[$type .'_lifespan']) {
+      $cutoff = time() - $lifespan;
+    }
+    else {
+      return FALSE;
+    }
+
+    switch ($type) {
+      case 'query':
+        // Not supported currently, but this is certainly where we'd put it.
+        return FALSE;
+      case 'results':
+        // Values to set: $view->result, $view->total_rows, $view->execute_time,
+        // $view->pager['current_page'].
+        if ($cache = cache_get($this->_results_key(), 'cache_views_data')) {
+          if ($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->execute_time = 0;
+            return TRUE;
+          }
+        }
+        return FALSE;
+      case 'output':
+        if ($cache = cache_get($this->_output_key(), 'cache_views_data')) {
+          if ($cache->created > $cutoff) {
+            $this->view->display_handler->output = $cache->data;
+            return TRUE;
+          }
+        }
+        return FALSE;
+    }
+  }
+  
+  /**
+   * Clear out cached data for a view.
+   *
+   * We're jsut going to nuke anything related to the view, regardless of display,
+   * to be sure that we catch everything. Maybe that's a bad idea.
+   */
+  function cache_flush() {
+    cache_clear_all($this->view->name .':', 'cache_views_data', TRUE);
+  }
+  
+  function _results_key() {
+    $key_data = array(
+      'build_info' => $this->view->build_info,
+      'exposed_info' => $this->view->exposed_info,
+      'page' => $_GET['page'],
+    );
+    return $this->view->name .':'. $this->display->id .':results:'. md5(serialize($key_data));
+  }
+  
+  function _output_key() {
+    $key_data = array(
+      'result' => $this->view->result,
+    );
+    return $this->view->name .':'. $this->display->id .':output:'. md5(serialize($key_data));
+  }
+}
Index: plugins/views_plugin_display.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/views/plugins/views_plugin_display.inc,v
retrieving revision 1.20.2.2
diff -u -p -r1.20.2.2 views_plugin_display.inc
--- plugins/views_plugin_display.inc	20 May 2009 03:09:37 -0000	1.20.2.2
+++ plugins/views_plugin_display.inc	22 May 2009 13:26:09 -0000
@@ -148,6 +148,7 @@ class views_plugin_display extends views
   function defaultable_sections($section = NULL) {
     $sections = array(
       'access' => array('access'),
+      'cache' => array('cache'),
       'title' => array('title'),
       'header' => array('header', 'header_format', 'header_empty'),
       'footer' => array('footer', 'footer_format', 'footer_empty'),
@@ -212,6 +213,7 @@ class views_plugin_display extends views
       'defaults' => array(
         'default' => array(
           'access' => TRUE,
+          'cache' => TRUE,
           'title' => TRUE,
           'header' => TRUE,
           'header_format' => TRUE,
@@ -270,6 +272,11 @@ class views_plugin_display extends views
           'type' => array('default' => 'none'),
          ),
       ),
+      'cache' => array(
+        'contains' => array(
+          'type' => array('default' => 'none'),
+         ),
+      ),
       'title' => array(
         'default' => '',
         'translatable' => TRUE,
@@ -482,6 +489,22 @@ class views_plugin_display extends views
   }
 
   /**
+   * Get the cache plugin
+   */
+  function get_cache_plugin($name = NULL) {
+    if (!$name) {
+      $cache = $this->get_option('cache');
+      $name = $cache['type'];
+    }
+
+    $plugin = views_get_plugin('cache', $name);
+    if ($plugin) {
+      $plugin->init($this->view, $this->display);
+      return $plugin;
+    }
+  }
+
+  /**
    * Get the handler object for a single handler.
    */
   function &get_handler($type, $id) {
@@ -687,6 +710,25 @@ class views_plugin_display extends views
       $options['access']['links']['access_options'] = t('Change settings for this access type.');
     }
 
+    $cache_plugin = $this->get_cache_plugin();
+    if (!$cache_plugin) {
+      // default to the no cache control plugin.
+      $cache_plugin = views_get_plugin('cache', 'none');
+    }
+
+    $cache_str = $cache_plugin->summary_title();
+
+    $options['cache'] = array(
+      'category' => 'basic',
+      'title' => t('Caching'),
+      'value' => $cache_str,
+      'desc' => t('Specify caching type for this display.'),
+    );
+
+    if (!empty($cache_plugin->definition['uses options'])) {
+      $options['cache']['links']['cache_options'] = t('Change settings for this caching type.');
+    }
+
     if ($this->uses_link_display()) {
       // Only show the 'link display' if there is more than one option.
       $count = 0;
@@ -893,6 +935,47 @@ class views_plugin_display extends views
           $plugin->options_form($form['access_options'], $form_state);
         }
         break;
+      case 'cache':
+        $form['#title'] .= t('Caching');
+        $form['cache'] = array(
+          '#prefix' => '<div class="clear-block">',
+          '#suffix' => '</div>',
+          '#tree' => TRUE,
+        );
+
+        $cache = $this->get_option('cache');
+        $form['cache']['type'] =  array(
+          '#type' => 'radios',
+          '#options' => views_fetch_plugin_names('cache'),
+          '#default_value' => $cache['type'],
+        );
+
+        $cache_plugin = views_fetch_plugin_data('cache', $cache['type']);
+        if (!empty($cache_plugin['uses options'])) {
+          $form['markup'] = array(
+            '#prefix' => '<div class="form-item description">',
+            '#suffix' => '</div>',
+            '#value' => t('You may also adjust the !settings for the currently selected style by clicking on the icon.', array('!settings' => $this->option_link(t('settings'), 'cache_options'))),
+          );
+        }
+        break;
+      case 'cache_options':
+        $cache = $this->get_option('cache');
+        $plugin = $this->get_cache_plugin();
+        $form['#title'] .= t('Caching options');
+        if ($plugin) {
+          $form['#help_topic'] = $plugin->definition['help topic'];
+
+          $form['cache_options'] = array(
+            '#tree' => TRUE,
+          );
+          $form['cache_options']['type'] = array(
+            '#type' => 'value',
+            '#value' => $cache['type'],
+          );
+          $plugin->options_form($form['cache_options'], $form_state);
+        }
+        break;
       case 'header':
         $form['#title'] .= t('Header');
         $form['header_empty'] = array(
@@ -1269,6 +1352,12 @@ class views_plugin_display extends views
           $plugin->options_validate($form['access_options'], $form_state);
         }
         break;
+      case 'cache_options':
+        $plugin = $this->get_cache_plugin();
+        if ($plugin) {
+          $plugin->options_validate($form['cache_options'], $form_state);
+        }
+        break;
     }
   }
 
@@ -1277,6 +1366,12 @@ class views_plugin_display extends views
    * There is no need for this function to actually store the data.
    */
   function options_submit(&$form, &$form_state) {
+    // Not sure I like this being here, but it seems (?) like a logical place.
+    $cache_plugin = $this->get_cache_plugin();
+    if ($cache_plugin) {
+      $cache_plugin->cache_flush();
+    }
+    
     $section = $form_state['section'];
     switch ($section) {
       case 'display_title':
@@ -1304,6 +1399,28 @@ class views_plugin_display extends views
           $this->set_option('access', $form_state['values'][$section]);
         }
         break;
+      case 'cache':
+        $cache = $this->get_option('cache');
+        if ($cache['type'] != $form_state['values']['cache']['type']) {
+          $plugin = views_get_plugin('cache', $form_state['values']['cache']['type']);
+          if ($plugin) {
+            $cache = array('type' => $form_state['values']['cache']['type']);
+            $plugin->option_defaults($cache);
+            $this->set_option('cache', $cache);
+            if (!empty($plugin->definition['uses options'])) {
+              views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('cache_options'));
+            }
+          }
+        }
+
+        break;
+      case 'cache_options':
+        $plugin = views_get_plugin('cache', $form_state['values'][$section]['type']);
+        if ($plugin) {
+          $plugin->options_submit($form['cache_options'], $form_state);
+          $this->set_option('cache', $form_state['values'][$section]);
+        }
+        break;
       case 'title':
       case 'link_display':
         $this->set_option($section, $form_state['values'][$section]);
