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