views_data_export_plugin_display_export.inc 36 KB

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