views_data_export_plugin_display_export.inc 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. <?php
  2. /**
  3. * @file
  4. * Contains the bulk export display plugin.
  5. *
  6. * This allows views to be rendered in parts by batch API.
  7. */
  8. /**
  9. * The plugin that batches its rendering.
  10. *
  11. * We are based on a feed display for compatibility.
  12. *
  13. * @ingroup views_display_plugins
  14. */
  15. class views_data_export_plugin_display_export extends views_plugin_display_feed {
  16. /**
  17. * The batched execution state of the view.
  18. */
  19. public $batched_execution_state;
  20. /**
  21. * The alias of the weight field in the index table.
  22. */
  23. var $weight_field_alias = '';
  24. /**
  25. * A map of the index column names to the expected views aliases.
  26. */
  27. var $field_aliases = array();
  28. /**
  29. * Private variable that stores the filename to save the results to.
  30. */
  31. var $_output_file = '';
  32. var $views_data_export_cached_view_loaded;
  33. var $errors = array();
  34. /**
  35. * Return the type of styles we require.
  36. */
  37. function get_style_type() { return 'data_export'; }
  38. /**
  39. * Return the sections that can be defaultable.
  40. */
  41. function defaultable_sections($section = NULL) {
  42. if (in_array($section, array('items_per_page', 'offset', 'use_pager', 'pager_element',))) {
  43. return FALSE;
  44. }
  45. return parent::defaultable_sections($section);
  46. }
  47. /**
  48. * Define the option for this view.
  49. */
  50. function option_definition() {
  51. $options = parent::option_definition();
  52. $options['use_batch'] = array('default' => 'no_batch');
  53. $options['items_per_page'] = array('default' => '0');
  54. $options['style_plugin']['default'] = 'views_data_export_csv';
  55. if (isset($options['defaults']['default']['items_per_page'])) {
  56. $options['defaults']['default']['items_per_page'] = FALSE;
  57. }
  58. return $options;
  59. }
  60. /**
  61. * Provide the summary for page options in the views UI.
  62. *
  63. * This output is returned as an array.
  64. */
  65. function options_summary(&$categories, &$options) {
  66. // It is very important to call the parent function here:
  67. parent::options_summary($categories, $options);
  68. $categories['page']['title'] = t('Data export settings');
  69. $options['use_batch'] = array(
  70. 'category' => 'page',
  71. 'title' => t('Batched export'),
  72. 'value' => ($this->get_option('use_batch') == 'batch' ? t('Yes') : t('No')),
  73. );
  74. if (!$this->is_compatible() && $this->get_option('use_batch')) {
  75. $options['use_batch']['value'] .= ' <strong>' . t('(Warning: incompatible)') . '</strong>';
  76. }
  77. }
  78. /**
  79. * Provide the default form for setting options.
  80. */
  81. function options_form(&$form, &$form_state) {
  82. // It is very important to call the parent function here:
  83. parent::options_form($form, $form_state);
  84. switch ($form_state['section']) {
  85. case 'use_batch':
  86. $form['#title'] .= t('Batched export');
  87. $form['use_batch'] = array(
  88. '#type' => 'radios',
  89. '#description' => t(''),
  90. '#default_value' => $this->get_option('use_batch'),
  91. '#options' => array(
  92. 'no_batch' => t('Export data all in one segment. Possible time and memory limit issues.'),
  93. 'batch' => t('Export data in small segments to build a complete export. Recommended for large exports sets (1000+ rows)'),
  94. ),
  95. );
  96. if (!$this->is_compatible()) {
  97. $form['use_batch']['#disabled'] = TRUE;
  98. $form['use_batch']['#default_value'] = 'no_batch';
  99. $form['use_batch']['message'] = array (
  100. '#type' => 'markup',
  101. '#markup' => theme('views_data_export_message', array('message' => t('The underlying database (!db_driver) is incompatible with the batched export option and it has been disabled.', array('!db_driver' => $this->_get_database_driver())), 'type' => 'warning')),
  102. '#weight' => -10,
  103. );
  104. }
  105. break;
  106. }
  107. }
  108. /**
  109. * Save the options from the options form.
  110. */
  111. function options_submit(&$form, &$form_state) {
  112. // It is very important to call the parent function here:
  113. parent::options_submit($form, $form_state);
  114. switch ($form_state['section']) {
  115. case 'use_batch':
  116. $this->set_option('use_batch', $form_state['values']['use_batch']);
  117. break;
  118. }
  119. }
  120. /**
  121. * Determine if this view should run as a batch or not.
  122. */
  123. function is_batched() {
  124. // The source of this option may change in the future.
  125. return ($this->get_option('use_batch') == 'batch') && empty($this->view->live_preview);
  126. }
  127. /**
  128. * Add HTTP headers for the file export.
  129. */
  130. function add_http_headers() {
  131. // Ask the style plugin to add any HTTP headers if it wants.
  132. if (method_exists($this->view->style_plugin, 'add_http_headers')) {
  133. $this->view->style_plugin->add_http_headers();
  134. }
  135. }
  136. /**
  137. * Execute this display handler.
  138. *
  139. * This is the main entry point for this display. We do different things based
  140. * on the stage in the rendering process.
  141. *
  142. * If we are being called for the very first time, the user has usually just
  143. * followed a link to our view. For this phase we:
  144. * - Register a new batched export with our parent module.
  145. * - Build and execute the view, redirecting the output into a temporary table.
  146. * - Set up the batch.
  147. *
  148. * If we are being called during batch processing we:
  149. * - Set up our variables from the context into the display.
  150. * - Call the rendering layer.
  151. * - Return with the appropriate progress value for the batch.
  152. *
  153. * If we are being called after the batch has completed we:
  154. * - Remove the index table.
  155. * - Show the complete page with a download link.
  156. * - Transfer the file if the download link was clicked.
  157. */
  158. function execute() {
  159. if (!$this->is_batched()) {
  160. return parent::execute();
  161. }
  162. // Try and get a batch context if possible.
  163. $eid = !empty($_GET['eid']) ? $_GET['eid'] :
  164. (!empty($this->batched_execution_state->eid) ? $this->batched_execution_state->eid : FALSE);
  165. if ($eid) {
  166. $this->batched_execution_state = views_data_export_get($eid);
  167. }
  168. // First time through
  169. if (empty($this->batched_execution_state)) {
  170. $output = $this->execute_initial();
  171. }
  172. // Call me on the cached version of this view please
  173. // This allows this view to be programatically executed with nothing
  174. // more than the eid in $_GET in order for it to execute the next chunk
  175. // TODO: What is going on here?
  176. /*
  177. Jamsilver tells me this might be useful one day.
  178. if (!$this->views_data_export_cached_view_loaded) {
  179. $view = views_data_export_view_retrieve($this->batched_execution_state->eid);
  180. $view->set_display($this->view->current_display);
  181. $view->display_handler->batched_execution_state->eid = $this->batched_execution_state->eid;
  182. $view->display_handler->views_data_export_cached_view_loaded = TRUE;
  183. $ret = $view->execute_display($this->view->current_display);
  184. $this->batched_execution_state = &$view->display_handler->batched_execution_state;
  185. return $ret;
  186. }*/
  187. // Last time through
  188. if ($this->batched_execution_state->batch_state == VIEWS_DATA_EXPORT_FINISHED) {
  189. $output = $this->execute_final();
  190. }
  191. // In the middle of processing
  192. else {
  193. $output = $this->execute_normal();
  194. }
  195. //Ensure any changes we made to the database sandbox are saved
  196. views_data_export_update($this->batched_execution_state);
  197. return $output;
  198. }
  199. /**
  200. * Initializes the whole export process and starts off the batch process.
  201. *
  202. * Page execution will be ended at the end of this function.
  203. */
  204. function execute_initial() {
  205. // Register this export with our central table - get a unique eid
  206. // Also store our view in a cache to be retrieved with each batch call
  207. $this->batched_execution_state = views_data_export_new($this->view->name, $this->view->current_display, $this->outputfile_create());
  208. views_data_export_view_store($this->batched_execution_state->eid, $this->view);
  209. // We need to build the index right now, before we lose $_GET etc.
  210. $this->initialize_index();
  211. //$this->batched_execution_state->fid = $this->outputfile_create();
  212. // Initialize the progress counter
  213. $this->batched_execution_state->sandbox['max'] = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
  214. // Record the time we started.
  215. list($usec, $sec) = explode(' ', microtime());
  216. $this->batched_execution_state->sandbox['started'] = (float) $usec + (float) $sec;
  217. // Build up our querystring for the final page callback.
  218. $querystring = array(
  219. 'eid' => $this->batched_execution_state->eid,
  220. );
  221. // If we were attached to another view, grab that for the final URL.
  222. if (!empty($_GET['attach']) && isset($this->view->display[$_GET['attach']])) {
  223. // Get the path of the attached display:
  224. $querystring['return-url'] = $this->view->display[$_GET['attach']]->handler->get_path();
  225. }
  226. //Set the batch off
  227. $batch = array(
  228. 'operations' => array (
  229. array('_views_data_export_batch_process', array($this->batched_execution_state->eid, $this->view->current_display)),
  230. ),
  231. 'title' => t('Building export'),
  232. 'init_message' => t('Export is starting up.'),
  233. 'progress_message' => t('Exporting @percentage% complete,'),
  234. 'error_message' => t('Export has encountered an error.'),
  235. );
  236. // We do not return, so update database sandbox now
  237. views_data_export_update($this->batched_execution_state);
  238. $final_destination = $this->view->get_url();
  239. // Provide a way in for others at this point
  240. // e.g. Drush to grab this batch and yet execute it in
  241. // it's own special way
  242. $batch['view_name'] = $this->view->name;
  243. $batch['display_id'] = $this->view->current_display;
  244. $batch['eid'] = $this->batched_execution_state->eid;
  245. $batch_redirect = array($final_destination, array('query' => $querystring));
  246. drupal_alter('views_data_export_batch', $batch, $batch_redirect);
  247. // Modules may have cleared out $batch, indicating that we shouldn't process further.
  248. if (!empty($batch)) {
  249. batch_set($batch);
  250. // batch_process exits
  251. batch_process($batch_redirect);
  252. }
  253. }
  254. /**
  255. * Compiles the next chunk of the output file
  256. */
  257. function execute_normal() {
  258. // Pass through to our render method,
  259. $output = $this->view->render();
  260. // Append what was rendered to the output file.
  261. $this->outputfile_write($output);
  262. // Store for convenience.
  263. $state = &$this->batched_execution_state;
  264. $sandbox = &$state->sandbox;
  265. // Update progress measurements & move our state forward
  266. switch ($state->batch_state) {
  267. case VIEWS_DATA_EXPORT_BODY:
  268. // Remove rendered results from our index
  269. if (count($this->view->result) && ($sandbox['weight_field_alias'])) {
  270. $last = end($this->view->result);
  271. db_delete($this->index_tablename())
  272. ->condition($sandbox['weight_field_alias'], $last->{$sandbox['weight_field_alias']}, '<=')
  273. ->execute();
  274. // Update progress.
  275. $progress = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
  276. // TODO: These next few lines are messy, clean them up.
  277. $progress = 0.99 - ($progress / $sandbox['max'] * 0.99);
  278. $progress = ((int)floor($progress * 100000));
  279. $progress = $progress / 100000;
  280. $sandbox['finished'] = $progress;
  281. }
  282. else {
  283. // No more results.
  284. $progress = 0.99;
  285. $state->batch_state = VIEWS_DATA_EXPORT_FOOTER;
  286. }
  287. break;
  288. case VIEWS_DATA_EXPORT_HEADER:
  289. $sandbox['finished'] = 0;
  290. $state->batch_state = VIEWS_DATA_EXPORT_BODY;
  291. break;
  292. case VIEWS_DATA_EXPORT_FOOTER:
  293. $sandbox['finished'] = 1;
  294. $state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
  295. break;
  296. }
  297. // Create a more helpful exporting message.
  298. $sandbox['message'] = $this->compute_time_remaining($sandbox['started'], $sandbox['finished']);
  299. }
  300. /**
  301. * Renders the final page
  302. * We should be free of the batch at this point
  303. */
  304. function execute_final() {
  305. // Should we download the file.
  306. if (!empty($_GET['download'])) {
  307. // This next method will exit.
  308. $this->transfer_file();
  309. }
  310. else {
  311. // Remove the index table.
  312. $this->remove_index();
  313. return $this->render_complete();
  314. }
  315. }
  316. /**
  317. * Render the display.
  318. *
  319. * We basically just work out if we should be rendering the header, body or
  320. * footer and call the appropriate functions on the style plugins.
  321. */
  322. function render() {
  323. if (!$this->is_batched()) {
  324. $result = parent::render();
  325. if (empty($this->view->live_preview)) {
  326. $this->add_http_headers();
  327. }
  328. return $result;
  329. }
  330. $this->view->build();
  331. switch ($this->batched_execution_state->batch_state) {
  332. case VIEWS_DATA_EXPORT_BODY:
  333. $output = $this->view->style_plugin->render_body();
  334. break;
  335. case VIEWS_DATA_EXPORT_HEADER:
  336. $output = $this->view->style_plugin->render_header();
  337. break;
  338. case VIEWS_DATA_EXPORT_FOOTER:
  339. $output = $this->view->style_plugin->render_footer();
  340. break;
  341. }
  342. return $output;
  343. }
  344. /**
  345. * Trick views into thinking that we have executed the query and got results.
  346. *
  347. * We are called in the build phase of the view, but short circuit straight to
  348. * getting the results and making the view think it has already executed the
  349. * query.
  350. */
  351. function query() {
  352. if (!$this->is_batched()) {
  353. return parent::query();
  354. }
  355. // Make the query distinct if the option was set.
  356. if ($this->get_option('distinct')) {
  357. $this->view->query->set_distinct();
  358. }
  359. if (!empty($this->batched_execution_state->batch_state) && !empty($this->batched_execution_state->sandbox['weight_field_alias'])) {
  360. switch ($this->batched_execution_state->batch_state) {
  361. case VIEWS_DATA_EXPORT_BODY:
  362. case VIEWS_DATA_EXPORT_HEADER:
  363. case VIEWS_DATA_EXPORT_FOOTER:
  364. // Tell views its been executed.
  365. $this->view->executed = TRUE;
  366. // Grab our results from the index, and push them into the view result.
  367. // TODO: Handle external databases.
  368. $result = db_query_range('SELECT * FROM {' . $this->index_tablename() . '} ORDER BY ' . $this->batched_execution_state->sandbox['weight_field_alias'] . ' ASC', 0, 100);
  369. $this->view->result = array();
  370. foreach ($result as $item_hashed) {
  371. $item = new stdClass();
  372. // We had to shorten some of the column names in the index, restore
  373. // those now.
  374. foreach ($item_hashed as $hash => $value) {
  375. if (isset($this->batched_execution_state->sandbox['field_aliases'][$hash])) {
  376. $item->{$this->batched_execution_state->sandbox['field_aliases'][$hash]} = $value;
  377. }
  378. else {
  379. $item->{$hash} = $value;
  380. }
  381. }
  382. // Push the restored $item in the views result array.
  383. $this->view->result[] = $item;
  384. }
  385. $this->view->_post_execute();
  386. break;
  387. }
  388. }
  389. }
  390. /**
  391. * Render the 'Export Finished' page with the link to the file on it.
  392. */
  393. function render_complete() {
  394. $return_path = empty($_GET['return-url']) ? '' : $_GET['return-url'];
  395. $query = array(
  396. 'download' => 1,
  397. 'eid' => $this->batched_execution_state->eid,
  398. );
  399. return theme('views_data_export_complete_page', array(
  400. 'file' => url($this->view->get_url(), array('query' => $query)),
  401. 'errors' => $this->errors,
  402. 'return_url' => $return_path));
  403. }
  404. /**
  405. * TBD - What does 'preview' mean for bulk exports?
  406. * According to doc:
  407. * "Fully render the display for the purposes of a live preview or
  408. * some other AJAXy reason. [views_plugin_display.inc:1877]"
  409. *
  410. * Not sure it makes sense for Bulk exports to be previewed in this manner?
  411. * We need the user's full attention to run the batch. Suggestions:
  412. * 1) Provide a link to execute the view?
  413. * 2) Provide a link to the last file we generated??
  414. * 3) Show a table of the first 20 results?
  415. */
  416. function preview() {
  417. if (!$this->is_batched()) {
  418. // Can replace with return parent::preview() when views 2.12 lands.
  419. if (!empty($this->view->live_preview)) {
  420. // Change the items per page.
  421. $this->view->set_items_per_page(20);
  422. // Force a pager to be used.
  423. $this->set_option('pager', array('type' => 'some'));
  424. return '<p>' . t('A maximum of 20 items will be shown here, all results will be shown on export.') . '</p><pre>' . check_plain($this->view->render()) . '</pre>';
  425. }
  426. return $this->view->render();
  427. }
  428. return '';
  429. }
  430. /**
  431. * Transfer the output file to the client.
  432. */
  433. function transfer_file() {
  434. // Build the view so we can set the headers.
  435. $this->view->build();
  436. // Arguments can cause the style to not get built.
  437. if (!$this->view->init_style()) {
  438. $this->view->build_info['fail'] = TRUE;
  439. }
  440. // Set the headers.
  441. $this->add_http_headers();
  442. file_transfer($this->outputfile_path(), array());
  443. }
  444. /**
  445. * Called on export initialization.
  446. *
  447. * Modifies the view query to insert the results into a table, which we call
  448. * the 'index', this means we essentially have a snapshot of the results,
  449. * which we can then take time over rendering.
  450. *
  451. * This method is essentially all the best bits of the view::execute() method.
  452. */
  453. protected function initialize_index() {
  454. $view = &$this->view;
  455. // Get views to build the query.
  456. $view->build();
  457. // Change the query object to use our custom one.
  458. $query = new views_data_export_plugin_query_default_batched();
  459. // Copy the query over:
  460. foreach ($view->query as $property => $value) {
  461. $query->$property = $value;
  462. }
  463. // Replace the query object.
  464. $view->query = $query;
  465. $view->execute();
  466. }
  467. /**
  468. * Given a view, construct an map of hashed aliases to aliases.
  469. *
  470. * The keys of the returned array will have a maximum length of 33 characters.
  471. */
  472. function field_aliases_create(&$view) {
  473. $all_aliases = array();
  474. foreach ($view->query->fields as $field) {
  475. if (strlen($field['alias']) > 32) {
  476. $all_aliases['a' . md5($field['alias'])] = $field['alias'];
  477. }
  478. else {
  479. $all_aliases[$field['alias']] = $field['alias'];
  480. }
  481. }
  482. return $all_aliases;
  483. }
  484. /**
  485. * Create an alias for the weight field in the index.
  486. *
  487. * This method ensures that it isn't the same as any other alias in the
  488. * supplied view's fields.
  489. */
  490. function _weight_alias_create(&$view) {
  491. $alias = 'vde_weight';
  492. $all_aliases = array();
  493. foreach ($view->query->fields as $field) {
  494. $all_aliases[] = $field['alias'];
  495. }
  496. // Keep appending '_' until we are unique.
  497. while (in_array($alias, $all_aliases)) {
  498. $alias .= '_';
  499. }
  500. return $alias;
  501. }
  502. /**
  503. * Remove the index.
  504. */
  505. function remove_index() {
  506. $ret = array();
  507. if (db_table_exists($this->index_tablename())) {
  508. db_drop_table($this->index_tablename());
  509. }
  510. }
  511. /**
  512. * Return the name of the unique table to store the index in.
  513. */
  514. function index_tablename() {
  515. return VIEWS_DATA_EXPORT_INDEX_TABLE_PREFIX . $this->batched_execution_state->eid;
  516. }
  517. /**
  518. * Get the output file path.
  519. */
  520. function outputfile_path() {
  521. if (empty($this->_output_file) && !empty($this->batched_execution_state->fid)) {
  522. // Return the filename associated with this file.
  523. $this->_output_file = $this->file_load($this->batched_execution_state->fid);
  524. }
  525. return $this->_output_file->uri;
  526. }
  527. /**
  528. * Called on export initialization
  529. * Creates the output file, registers it as a temporary file with Drupal
  530. * and returns the fid
  531. */
  532. protected function outputfile_create() {
  533. $dir = 'temporary://views_plugin_display';
  534. // Make sure the directory exists first.
  535. if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
  536. $this->abort_export(t('Could not create temporary directory for result export (@dir). Check permissions.', array ('@dir' => $dir)));
  537. }
  538. $path = drupal_tempnam($dir, 'views_data_export');
  539. // Save the file into the DB.
  540. $file = $this->file_save_file($path);
  541. return $file->fid;
  542. }
  543. /**
  544. * Write to the output file.
  545. */
  546. protected function outputfile_write($string) {
  547. $output_file = $this->outputfile_path();
  548. if (file_put_contents($output_file, $string, FILE_APPEND) === FALSE) {
  549. $this->abort_export(t('Could not write to temporary output file for result export (@file). Check permissions.', array ('@file' => $output_file)));
  550. }
  551. }
  552. function abort_export($errors) {
  553. // Just cause the next batch to do the clean-up
  554. if (!is_array($errors)) {
  555. $errors = array($errors);
  556. }
  557. foreach ($errors as $error) {
  558. drupal_set_message($error . ' ['. t('Export Aborted') . ']', 'error');
  559. }
  560. $this->batched_execution_state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
  561. }
  562. /**
  563. * Load a file from the database.
  564. *
  565. * @param $fid
  566. * A numeric file id or string containing the file path.
  567. * @return
  568. * A file object.
  569. */
  570. function file_load($fid) {
  571. return file_load($fid);
  572. }
  573. /**
  574. * Save a file into a file node after running all the associated validators.
  575. *
  576. * This function is usually used to move a file from the temporary file
  577. * directory to a permanent location. It may be used by import scripts or other
  578. * modules that want to save an existing file into the database.
  579. *
  580. * @param $filepath
  581. * The local file path of the file to be saved.
  582. * @return
  583. * An array containing the file information, or 0 in the event of an error.
  584. */
  585. function file_save_file($filepath) {
  586. return file_save_data('', $filepath);
  587. }
  588. /**
  589. * Helper function that computes the time remaining
  590. */
  591. function compute_time_remaining($started, $finished) {
  592. list($usec, $sec) = explode(' ', microtime());
  593. $now = (float) $usec + (float) $sec;
  594. $diff = round(($now - $started), 0);
  595. // So we've taken $diff seconds to get this far.
  596. if ($finished > 0) {
  597. $estimate_total = $diff / $finished;
  598. $stamp = max(1, $estimate_total - $diff);
  599. // Round up to nearest 30 seconds.
  600. $stamp = ceil($stamp / 30) * 30;
  601. // Set the message in the batch context.
  602. return t('Time remaining: about @interval.', array('@interval' => format_interval($stamp)));
  603. }
  604. }
  605. /**
  606. * Checks the driver of the database underlying
  607. * this query and returns FALSE if it is imcompatible
  608. * with the approach taken in this display.
  609. * Basically mysql & mysqli will be fine, pg will not
  610. */
  611. function is_compatible() {
  612. $incompatible_drivers = array (
  613. 'pgsql',
  614. );
  615. $db_driver = $this->_get_database_driver();
  616. return !in_array($db_driver, $incompatible_drivers);
  617. }
  618. function _get_database_driver() {
  619. $name = !empty($this->view->base_database) ? $this->view->base_database : 'default';
  620. $conn_info = Database::getConnectionInfo($name);
  621. return $conn_info[$name]['driver'];
  622. }
  623. }
  624. class views_data_export_plugin_query_default_batched extends views_plugin_query_default {
  625. /**
  626. * Executes the query and fills the associated view object with according
  627. * values.
  628. *
  629. * Values to set: $view->result, $view->total_rows, $view->execute_time,
  630. * $view->current_page.
  631. */
  632. function execute(&$view) {
  633. $display_handler = &$view->display_handler;
  634. $external = FALSE; // Whether this query will run against an external database.
  635. $query = $view->build_info['query'];
  636. $count_query = $view->build_info['count_query'];
  637. $query->addMetaData('view', $view);
  638. $count_query->addMetaData('view', $view);
  639. if (empty($this->options['disable_sql_rewrite'])) {
  640. $base_table_data = views_fetch_data($this->base_table);
  641. if (isset($base_table_data['table']['base']['access query tag'])) {
  642. $access_tag = $base_table_data['table']['base']['access query tag'];
  643. $query->addTag($access_tag);
  644. $count_query->addTag($access_tag);
  645. }
  646. }
  647. $items = array();
  648. if ($query) {
  649. $additional_arguments = module_invoke_all('views_query_substitutions', $view);
  650. // Count queries must be run through the preExecute() method.
  651. // If not, then hook_query_node_access_alter() may munge the count by
  652. // adding a distinct against an empty query string
  653. // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
  654. // See pager.inc > PagerDefault::execute()
  655. // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
  656. // See http://drupal.org/node/1046170.
  657. $count_query->preExecute();
  658. // Build the count query.
  659. $count_query = $count_query->countQuery();
  660. // Add additional arguments as a fake condition.
  661. // XXX: this doesn't work... because PDO mandates that all bound arguments
  662. // are used on the query. TODO: Find a better way to do this.
  663. if (!empty($additional_arguments)) {
  664. // $query->where('1 = 1', $additional_arguments);
  665. // $count_query->where('1 = 1', $additional_arguments);
  666. }
  667. $start = microtime(TRUE);
  668. if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
  669. $this->pager->execute_count_query($count_query);
  670. }
  671. // Let the pager modify the query to add limits.
  672. $this->pager->pre_execute($query);
  673. if (!empty($this->limit) || !empty($this->offset)) {
  674. // We can't have an offset without a limit, so provide a very large limit instead.
  675. $limit = intval(!empty($this->limit) ? $this->limit : 999999);
  676. $offset = intval(!empty($this->offset) ? $this->offset : 0);
  677. $query->range($offset, $limit);
  678. }
  679. try {
  680. // The $query is final and ready to go, we are going to redirect it to
  681. // become an insert into our table, sneaky!
  682. // Our query will look like:
  683. // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
  684. // (-query-) AS cl, (SELECT @row := 0) AS r
  685. // We do some magic to get the row count.
  686. $display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
  687. $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
  688. $select_aliases = array();
  689. foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
  690. $select_aliases[] = "cl.$alias AS $hash";
  691. }
  692. // TODO: this could probably be replaced with a query extender and new query type.
  693. $query->preExecute();
  694. $args = $query->getArguments();
  695. $insert_query = 'CREATE TABLE {' . $display_handler->index_tablename() . '} SELECT @row := @row + 1 AS ' . $display_handler->batched_execution_state->sandbox['weight_field_alias'] . ', ' . implode(', ', $select_aliases) . ' FROM (' . (string)$query . ') AS cl, (SELECT @row := 0) AS r';
  696. db_query($insert_query, $args);
  697. $view->result = array();
  698. $this->pager->post_execute($view->result);
  699. if ($this->pager->use_pager()) {
  700. $view->total_rows = $this->pager->get_total_items();
  701. }
  702. // Now create an index for the weight field, otherwise the queries on the
  703. // index will take a long time to execute.
  704. db_add_unique_key($display_handler->index_tablename(), $display_handler->batched_execution_state->sandbox['weight_field_alias'], array($display_handler->batched_execution_state->sandbox['weight_field_alias']));
  705. }
  706. catch (Exception $e) {
  707. $view->result = array();
  708. debug('Exception: ' . $e->getMessage());
  709. }
  710. }
  711. $view->execute_time = microtime(TRUE) - $start;
  712. }
  713. }