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
|
|
242
|
if (!empty($_GET['eid']) && !empty($_GET['token']) && drupal_valid_token($_GET['token'], 'views_data_export/' . $_GET['eid'])) {
|
243
|
$eid = $_GET['eid'];
|
244
|
}
|
245
|
elseif (!empty($this->batched_execution_state->eid)) {
|
246
|
$eid = $this->batched_execution_state->eid;
|
247
|
}
|
248
|
else {
|
249
|
$eid = FALSE;
|
250
|
}
|
251
|
|
252
|
if ($eid) {
|
253
|
$this->batched_execution_state = views_data_export_get($eid);
|
254
|
}
|
255
|
|
256
|
// First time through
|
257
|
if (empty($this->batched_execution_state)) {
|
258
|
$output = $this->execute_initial();
|
259
|
}
|
260
|
|
261
|
// Call me on the cached version of this view please
|
262
|
// This allows this view to be programatically executed with nothing
|
263
|
// more than the eid in $_GET in order for it to execute the next chunk
|
264
|
// TODO: What is going on here?
|
265
|
/*
|
266
|
Jamsilver tells me this might be useful one day.
|
267
|
if (!$this->views_data_export_cached_view_loaded) {
|
268
|
$view = views_data_export_view_retrieve($this->batched_execution_state->eid);
|
269
|
$view->set_display($this->view->current_display);
|
270
|
$view->display_handler->batched_execution_state->eid = $this->batched_execution_state->eid;
|
271
|
$view->display_handler->views_data_export_cached_view_loaded = TRUE;
|
272
|
$ret = $view->execute_display($this->view->current_display);
|
273
|
$this->batched_execution_state = &$view->display_handler->batched_execution_state;
|
274
|
return $ret;
|
275
|
}*/
|
276
|
|
277
|
// Last time through
|
278
|
if ($this->batched_execution_state->batch_state == VIEWS_DATA_EXPORT_FINISHED) {
|
279
|
$output = $this->execute_final();
|
280
|
}
|
281
|
// In the middle of processing
|
282
|
else {
|
283
|
$output = $this->execute_normal();
|
284
|
}
|
285
|
|
286
|
//Ensure any changes we made to the database sandbox are saved
|
287
|
views_data_export_update($this->batched_execution_state);
|
288
|
|
289
|
return $output;
|
290
|
}
|
291
|
|
292
|
|
293
|
/**
|
294
|
* Initializes the whole export process and starts off the batch process.
|
295
|
*
|
296
|
* Page execution will be ended at the end of this function.
|
297
|
*/
|
298
|
function execute_initial() {
|
299
|
|
300
|
// Register this export with our central table - get a unique eid
|
301
|
// Also store our view in a cache to be retrieved with each batch call
|
302
|
$this->batched_execution_state = views_data_export_new($this->view->name, $this->view->current_display, $this->outputfile_create());
|
303
|
views_data_export_view_store($this->batched_execution_state->eid, $this->view);
|
304
|
|
305
|
// Record a usage of our file, so we can identify our exports later.
|
306
|
file_usage_add(file_load($this->batched_execution_state->fid), 'views_data_export', 'eid', $this->batched_execution_state->eid);
|
307
|
|
308
|
// We need to build the index right now, before we lose $_GET etc.
|
309
|
$this->initialize_index();
|
310
|
//$this->batched_execution_state->fid = $this->outputfile_create();
|
311
|
|
312
|
// Initialize the progress counter.
|
313
|
if (db_table_exists($this->index_tablename())) {
|
314
|
$this->batched_execution_state->sandbox['max'] = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
|
315
|
}
|
316
|
|
317
|
// Record the time we started.
|
318
|
list($usec, $sec) = explode(' ', microtime());
|
319
|
$this->batched_execution_state->sandbox['started'] = (float) $usec + (float) $sec;
|
320
|
|
321
|
// Pop something into the session to ensure it stays aorund.
|
322
|
$_SESSION['views_data_export'][$this->batched_execution_state->eid] = TRUE;
|
323
|
|
324
|
// Build up our querystring for the final page callback.
|
325
|
$querystring = array(
|
326
|
'eid' => $this->batched_execution_state->eid,
|
327
|
'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
|
328
|
'return-url' => NULL,
|
329
|
);
|
330
|
|
331
|
// If we have a configured return path, use that.
|
332
|
if ($this->get_option('return_path')) {
|
333
|
$querystring['return-url'] = $this->get_option('return_path');
|
334
|
}
|
335
|
// Else if we were attached to another view, grab that for the final URL.
|
336
|
else if (!empty($_GET['attach']) && isset($this->view->display[$_GET['attach']])) {
|
337
|
// Get the path of the attached display:
|
338
|
$querystring['return-url'] = $this->view->get_url(NULL, $this->view->display[$_GET['attach']]->handler->get_path());
|
339
|
}
|
340
|
|
341
|
//Set the batch off
|
342
|
$batch = array(
|
343
|
'operations' => array (
|
344
|
array('_views_data_export_batch_process', array($this->batched_execution_state->eid, $this->view->current_display, $this->view->get_exposed_input())),
|
345
|
),
|
346
|
'title' => t('Building export'),
|
347
|
'init_message' => t('Export is starting up.'),
|
348
|
'progress_message' => t('Exporting @percentage% complete,'),
|
349
|
'error_message' => t('Export has encountered an error.'),
|
350
|
);
|
351
|
|
352
|
// We do not return, so update database sandbox now
|
353
|
views_data_export_update($this->batched_execution_state);
|
354
|
|
355
|
$final_destination = $this->view->get_url();
|
356
|
|
357
|
// Provide a way in for others at this point
|
358
|
// e.g. Drush to grab this batch and yet execute it in
|
359
|
// it's own special way
|
360
|
$batch['view_name'] = $this->view->name;
|
361
|
$batch['exposed_filters'] = $this->view->get_exposed_input();
|
362
|
$batch['display_id'] = $this->view->current_display;
|
363
|
$batch['eid'] = $this->batched_execution_state->eid;
|
364
|
$batch_redirect = array($final_destination, array('query' => $querystring));
|
365
|
drupal_alter('views_data_export_batch', $batch, $batch_redirect);
|
366
|
|
367
|
// Modules may have cleared out $batch, indicating that we shouldn't process further.
|
368
|
if (!empty($batch)) {
|
369
|
batch_set($batch);
|
370
|
// batch_process exits
|
371
|
batch_process($batch_redirect);
|
372
|
}
|
373
|
}
|
374
|
|
375
|
|
376
|
/**
|
377
|
* Compiles the next chunk of the output file
|
378
|
*/
|
379
|
function execute_normal() {
|
380
|
|
381
|
// Pass through to our render method,
|
382
|
$output = $this->view->render();
|
383
|
|
384
|
// Append what was rendered to the output file.
|
385
|
$this->outputfile_write($output);
|
386
|
|
387
|
// Store for convenience.
|
388
|
$state = &$this->batched_execution_state;
|
389
|
$sandbox = &$state->sandbox;
|
390
|
|
391
|
// Update progress measurements & move our state forward
|
392
|
switch ($state->batch_state) {
|
393
|
|
394
|
case VIEWS_DATA_EXPORT_BODY:
|
395
|
// Remove rendered results from our index
|
396
|
if (count($this->view->result) && ($sandbox['weight_field_alias'])) {
|
397
|
$last = end($this->view->result);
|
398
|
db_delete($this->index_tablename())
|
399
|
->condition($sandbox['weight_field_alias'], $last->{$sandbox['weight_field_alias']}, '<=')
|
400
|
->execute();
|
401
|
|
402
|
// Update progress.
|
403
|
$progress = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
|
404
|
// TODO: These next few lines are messy, clean them up.
|
405
|
$progress = 0.99 - ($progress / $sandbox['max'] * 0.99);
|
406
|
$progress = ((int)floor($progress * 100000));
|
407
|
$progress = $progress / 100000;
|
408
|
$sandbox['finished'] = $progress;
|
409
|
}
|
410
|
else {
|
411
|
// No more results.
|
412
|
$progress = 0.99;
|
413
|
$state->batch_state = VIEWS_DATA_EXPORT_FOOTER;
|
414
|
}
|
415
|
break;
|
416
|
|
417
|
case VIEWS_DATA_EXPORT_HEADER:
|
418
|
$sandbox['finished'] = 0;
|
419
|
$state->batch_state = VIEWS_DATA_EXPORT_BODY;
|
420
|
break;
|
421
|
|
422
|
case VIEWS_DATA_EXPORT_FOOTER:
|
423
|
// Update the temporary file size, otherwise we would get a problematic
|
424
|
// "Content-Length: 0" HTTP header, that may break the export download.
|
425
|
$this->outputfile_update_size();
|
426
|
$sandbox['finished'] = 1;
|
427
|
$state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
|
428
|
break;
|
429
|
}
|
430
|
|
431
|
// Create a more helpful exporting message.
|
432
|
$sandbox['message'] = $this->compute_time_remaining($sandbox['started'], $sandbox['finished']);
|
433
|
}
|
434
|
|
435
|
|
436
|
/**
|
437
|
* Renders the final page
|
438
|
* We should be free of the batch at this point
|
439
|
*/
|
440
|
function execute_final() {
|
441
|
// Should we download the file.
|
442
|
if (!empty($_GET['download'])) {
|
443
|
// Clean up our session, if we need to.
|
444
|
if (isset($_SESSION)) {
|
445
|
unset($_SESSION['views_data_export'][$this->batched_execution_state->eid]);
|
446
|
if (empty($_SESSION['views_data_export'])) {
|
447
|
unset($_SESSION['views_data_export']);
|
448
|
}
|
449
|
}
|
450
|
// This next method will exit.
|
451
|
$this->transfer_file();
|
452
|
}
|
453
|
else {
|
454
|
// Remove the index table.
|
455
|
$this->remove_index();
|
456
|
return $this->render_complete();
|
457
|
}
|
458
|
}
|
459
|
|
460
|
|
461
|
/**
|
462
|
* Render the display.
|
463
|
*
|
464
|
* We basically just work out if we should be rendering the header, body or
|
465
|
* footer and call the appropriate functions on the style plugins.
|
466
|
*/
|
467
|
function render() {
|
468
|
|
469
|
if (!$this->is_batched()) {
|
470
|
$result = parent::render();
|
471
|
if (empty($this->view->live_preview)) {
|
472
|
$this->add_http_headers();
|
473
|
}
|
474
|
return $result;
|
475
|
}
|
476
|
|
477
|
$this->view->build();
|
478
|
|
479
|
switch ($this->batched_execution_state->batch_state) {
|
480
|
case VIEWS_DATA_EXPORT_BODY:
|
481
|
$output = $this->view->style_plugin->render_body();
|
482
|
break;
|
483
|
case VIEWS_DATA_EXPORT_HEADER:
|
484
|
$output = $this->view->style_plugin->render_header();
|
485
|
break;
|
486
|
case VIEWS_DATA_EXPORT_FOOTER:
|
487
|
$output = $this->view->style_plugin->render_footer();
|
488
|
break;
|
489
|
}
|
490
|
|
491
|
return $output;
|
492
|
}
|
493
|
|
494
|
|
495
|
|
496
|
/**
|
497
|
* Trick views into thinking that we have executed the query and got results.
|
498
|
*
|
499
|
* We are called in the build phase of the view, but short circuit straight to
|
500
|
* getting the results and making the view think it has already executed the
|
501
|
* query.
|
502
|
*/
|
503
|
function query() {
|
504
|
|
505
|
if (!$this->is_batched()) {
|
506
|
return parent::query();
|
507
|
}
|
508
|
|
509
|
// Make the query distinct if the option was set.
|
510
|
if ($this->get_option('distinct')) {
|
511
|
$this->view->query->set_distinct();
|
512
|
}
|
513
|
|
514
|
if (!empty($this->batched_execution_state->batch_state) && !empty($this->batched_execution_state->sandbox['weight_field_alias'])) {
|
515
|
|
516
|
switch ($this->batched_execution_state->batch_state) {
|
517
|
case VIEWS_DATA_EXPORT_BODY:
|
518
|
case VIEWS_DATA_EXPORT_HEADER:
|
519
|
case VIEWS_DATA_EXPORT_FOOTER:
|
520
|
// Tell views its been executed.
|
521
|
$this->view->executed = TRUE;
|
522
|
|
523
|
// Grab our results from the index, and push them into the view result.
|
524
|
// TODO: Handle external databases.
|
525
|
$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'));
|
526
|
$this->view->result = array();
|
527
|
foreach ($result as $item_hashed) {
|
528
|
$item = new stdClass();
|
529
|
// We had to shorten some of the column names in the index, restore
|
530
|
// those now.
|
531
|
foreach ($item_hashed as $hash => $value) {
|
532
|
if (isset($this->batched_execution_state->sandbox['field_aliases'][$hash])) {
|
533
|
$item->{$this->batched_execution_state->sandbox['field_aliases'][$hash]} = $value;
|
534
|
}
|
535
|
else {
|
536
|
$item->{$hash} = $value;
|
537
|
}
|
538
|
}
|
539
|
// Push the restored $item in the views result array.
|
540
|
$this->view->result[] = $item;
|
541
|
}
|
542
|
$this->view->_post_execute();
|
543
|
break;
|
544
|
}
|
545
|
}
|
546
|
}
|
547
|
|
548
|
|
549
|
/**
|
550
|
* Render the 'Export Finished' page with the link to the file on it.
|
551
|
*/
|
552
|
function render_complete() {
|
553
|
$return_path = empty($_GET['return-url']) ? '' : $_GET['return-url'];
|
554
|
|
555
|
$query = array(
|
556
|
'download' => 1,
|
557
|
'eid' => $this->batched_execution_state->eid,
|
558
|
'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
|
559
|
);
|
560
|
|
561
|
return theme('views_data_export_complete_page', array(
|
562
|
'file' => url($this->view->get_url(), array('query' => $query)),
|
563
|
'errors' => $this->errors,
|
564
|
'return_url' => $return_path));
|
565
|
}
|
566
|
|
567
|
/**
|
568
|
* TBD - What does 'preview' mean for bulk exports?
|
569
|
* According to doc:
|
570
|
* "Fully render the display for the purposes of a live preview or
|
571
|
* some other AJAXy reason. [views_plugin_display.inc:1877]"
|
572
|
*
|
573
|
* Not sure it makes sense for Bulk exports to be previewed in this manner?
|
574
|
* We need the user's full attention to run the batch. Suggestions:
|
575
|
* 1) Provide a link to execute the view?
|
576
|
* 2) Provide a link to the last file we generated??
|
577
|
* 3) Show a table of the first 20 results?
|
578
|
*/
|
579
|
function preview() {
|
580
|
if (!$this->is_batched()) {
|
581
|
// Can replace with return parent::preview() when views 2.12 lands.
|
582
|
if (!empty($this->view->live_preview)) {
|
583
|
// Change the items per page.
|
584
|
$this->view->set_items_per_page(20);
|
585
|
// Force a pager to be used.
|
586
|
$this->set_option('pager', array('type' => 'some', 'options' => array()));
|
587
|
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>';
|
588
|
}
|
589
|
return $this->view->render();
|
590
|
}
|
591
|
return '';
|
592
|
}
|
593
|
|
594
|
/**
|
595
|
* Transfer the output file to the client.
|
596
|
*/
|
597
|
function transfer_file() {
|
598
|
// Build the view so we can set the headers.
|
599
|
$this->view->build();
|
600
|
// Arguments can cause the style to not get built.
|
601
|
if (!$this->view->init_style()) {
|
602
|
$this->view->build_info['fail'] = TRUE;
|
603
|
}
|
604
|
// Set the headers.
|
605
|
$this->add_http_headers();
|
606
|
$headers = array(
|
607
|
'Content-Length' => $this->outputfile_entity()->filesize,
|
608
|
);
|
609
|
file_transfer($this->outputfile_path(), $headers);
|
610
|
}
|
611
|
|
612
|
/**
|
613
|
* Called on export initialization.
|
614
|
*
|
615
|
* Modifies the view query to insert the results into a table, which we call
|
616
|
* the 'index', this means we essentially have a snapshot of the results,
|
617
|
* which we can then take time over rendering.
|
618
|
*
|
619
|
* This method is essentially all the best bits of the view::execute() method.
|
620
|
*/
|
621
|
protected function initialize_index() {
|
622
|
$view = &$this->view;
|
623
|
// Get views to build the query.
|
624
|
$view->build();
|
625
|
|
626
|
// Change the query object to use our custom one.
|
627
|
switch ($this->_get_database_driver()) {
|
628
|
case 'pgsql':
|
629
|
$query_class = 'views_data_export_plugin_query_pgsql_batched';
|
630
|
break;
|
631
|
|
632
|
default:
|
633
|
$query_class = 'views_data_export_plugin_query_default_batched';
|
634
|
break;
|
635
|
}
|
636
|
$query = new $query_class();
|
637
|
// Copy the query over:
|
638
|
foreach ($view->query as $property => $value) {
|
639
|
$query->$property = $value;
|
640
|
}
|
641
|
// Replace the query object.
|
642
|
$view->query = $query;
|
643
|
|
644
|
$view->execute();
|
645
|
}
|
646
|
|
647
|
/**
|
648
|
* Given a view, construct an map of hashed aliases to aliases.
|
649
|
*
|
650
|
* The keys of the returned array will have a maximum length of 33 characters.
|
651
|
*/
|
652
|
function field_aliases_create(&$view) {
|
653
|
$all_aliases = array();
|
654
|
foreach ($view->query->fields as $field) {
|
655
|
if (strlen($field['alias']) > 32) {
|
656
|
$all_aliases['a' . md5($field['alias'])] = $field['alias'];
|
657
|
}
|
658
|
else {
|
659
|
$all_aliases[$field['alias']] = $field['alias'];
|
660
|
}
|
661
|
}
|
662
|
return $all_aliases;
|
663
|
}
|
664
|
|
665
|
/**
|
666
|
* Create an alias for the weight field in the index.
|
667
|
*
|
668
|
* This method ensures that it isn't the same as any other alias in the
|
669
|
* supplied view's fields.
|
670
|
*/
|
671
|
function _weight_alias_create(&$view) {
|
672
|
$alias = 'vde_weight';
|
673
|
$all_aliases = array();
|
674
|
foreach ($view->query->fields as $field) {
|
675
|
$all_aliases[] = $field['alias'];
|
676
|
}
|
677
|
// Keep appending '_' until we are unique.
|
678
|
while (in_array($alias, $all_aliases)) {
|
679
|
$alias .= '_';
|
680
|
}
|
681
|
return $alias;
|
682
|
}
|
683
|
|
684
|
/**
|
685
|
* Remove the index.
|
686
|
*/
|
687
|
function remove_index() {
|
688
|
$ret = array();
|
689
|
if (db_table_exists($this->index_tablename())) {
|
690
|
db_drop_table($this->index_tablename());
|
691
|
}
|
692
|
}
|
693
|
|
694
|
/**
|
695
|
* Return the name of the unique table to store the index in.
|
696
|
*/
|
697
|
function index_tablename() {
|
698
|
return VIEWS_DATA_EXPORT_INDEX_TABLE_PREFIX . $this->batched_execution_state->eid;
|
699
|
}
|
700
|
|
701
|
/**
|
702
|
* Get the output file entity.
|
703
|
*/
|
704
|
public function outputfile_entity() {
|
705
|
if (empty($this->_output_file)) {
|
706
|
if (!empty($this->batched_execution_state->fid)) {
|
707
|
// Return the filename associated with this file.
|
708
|
$this->_output_file = $this->file_load($this->batched_execution_state->fid);
|
709
|
}
|
710
|
else {
|
711
|
return NULL;
|
712
|
}
|
713
|
}
|
714
|
return $this->_output_file;
|
715
|
}
|
716
|
|
717
|
/**
|
718
|
* Get the output file path.
|
719
|
*/
|
720
|
public function outputfile_path() {
|
721
|
if ($file = $this->outputfile_entity()) {
|
722
|
return $file->uri;
|
723
|
}
|
724
|
}
|
725
|
|
726
|
/**
|
727
|
* Called on export initialization
|
728
|
* Creates the output file, registers it as a temporary file with Drupal
|
729
|
* and returns the fid
|
730
|
*/
|
731
|
protected function outputfile_create() {
|
732
|
|
733
|
$dir = variable_get('views_data_export_directory', 'temporary://views_plugin_display');
|
734
|
|
735
|
// Make sure the directory exists first.
|
736
|
if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
|
737
|
$this->abort_export(t('Could not create temporary directory for result export (@dir). Check permissions.', array ('@dir' => $dir)));
|
738
|
}
|
739
|
|
740
|
$path = drupal_tempnam($dir, 'views_data_export');
|
741
|
|
742
|
// Save the file into the DB.
|
743
|
$file = $this->file_save_file($path);
|
744
|
|
745
|
// Make sure the file is marked as temporary.
|
746
|
// There is no FILE_STATUS_TEMPORARY constant.
|
747
|
$file->status = 0;
|
748
|
file_save($file);
|
749
|
|
750
|
return $file->fid;
|
751
|
}
|
752
|
|
753
|
/**
|
754
|
* Write to the output file.
|
755
|
*/
|
756
|
protected function outputfile_write($string) {
|
757
|
$output_file = $this->outputfile_path();
|
758
|
if (file_put_contents($output_file, $string, FILE_APPEND) === FALSE) {
|
759
|
$this->abort_export(t('Could not write to temporary output file for result export (@file). Check permissions.', array ('@file' => $output_file)));
|
760
|
}
|
761
|
}
|
762
|
|
763
|
/**
|
764
|
* Updates the file size in the file entity.
|
765
|
*/
|
766
|
protected function outputfile_update_size() {
|
767
|
if ($file = $this->outputfile_entity()) {
|
768
|
$file->filesize = filesize($file->uri);
|
769
|
file_save($file);
|
770
|
}
|
771
|
}
|
772
|
|
773
|
function abort_export($errors) {
|
774
|
// Just cause the next batch to do the clean-up
|
775
|
if (!is_array($errors)) {
|
776
|
$errors = array($errors);
|
777
|
}
|
778
|
foreach ($errors as $error) {
|
779
|
drupal_set_message($error . ' ['. t('Export Aborted') . ']', 'error');
|
780
|
}
|
781
|
$this->batched_execution_state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
|
782
|
}
|
783
|
|
784
|
/**
|
785
|
* Load a file from the database.
|
786
|
*
|
787
|
* @param $fid
|
788
|
* A numeric file id or string containing the file path.
|
789
|
* @return
|
790
|
* A file object.
|
791
|
*/
|
792
|
function file_load($fid) {
|
793
|
return file_load($fid);
|
794
|
}
|
795
|
|
796
|
/**
|
797
|
* Save a file into a file node after running all the associated validators.
|
798
|
*
|
799
|
* This function is usually used to move a file from the temporary file
|
800
|
* directory to a permanent location. It may be used by import scripts or other
|
801
|
* modules that want to save an existing file into the database.
|
802
|
*
|
803
|
* @param $filepath
|
804
|
* The local file path of the file to be saved.
|
805
|
* @return
|
806
|
* An array containing the file information, or 0 in the event of an error.
|
807
|
*/
|
808
|
function file_save_file($filepath) {
|
809
|
return file_save_data('', $filepath, FILE_EXISTS_REPLACE);
|
810
|
}
|
811
|
|
812
|
/**
|
813
|
* Helper function that computes the time remaining
|
814
|
*/
|
815
|
function compute_time_remaining($started, $finished) {
|
816
|
list($usec, $sec) = explode(' ', microtime());
|
817
|
$now = (float) $usec + (float) $sec;
|
818
|
$diff = round(($now - $started), 0);
|
819
|
// So we've taken $diff seconds to get this far.
|
820
|
if ($finished > 0) {
|
821
|
$estimate_total = $diff / $finished;
|
822
|
$stamp = max(1, $estimate_total - $diff);
|
823
|
// Round up to nearest 30 seconds.
|
824
|
$stamp = ceil($stamp / 30) * 30;
|
825
|
// Set the message in the batch context.
|
826
|
return t('Time remaining: about @interval.', array('@interval' => format_interval($stamp)));
|
827
|
}
|
828
|
}
|
829
|
|
830
|
/**
|
831
|
* Checks the driver of the database underlying
|
832
|
* this query and returns FALSE if it is imcompatible
|
833
|
* with the approach taken in this display.
|
834
|
* Basically mysql & mysqli will be fine, pg will not
|
835
|
*/
|
836
|
function is_compatible() {
|
837
|
$incompatible_drivers = array (
|
838
|
//'pgsql',
|
839
|
);
|
840
|
$db_driver = $this->_get_database_driver();
|
841
|
return !in_array($db_driver, $incompatible_drivers);
|
842
|
}
|
843
|
|
844
|
function _get_database_driver() {
|
845
|
$name = !empty($this->view->base_database) ? $this->view->base_database : 'default';
|
846
|
$conn_info = Database::getConnectionInfo($name);
|
847
|
return $conn_info['default']['driver'];
|
848
|
}
|
849
|
}
|
850
|
|
851
|
class views_data_export_plugin_query_default_batched extends views_plugin_query_default {
|
852
|
|
853
|
|
854
|
/**
|
855
|
* Executes the query and fills the associated view object with according
|
856
|
* values.
|
857
|
*
|
858
|
* Values to set: $view->result, $view->total_rows, $view->execute_time,
|
859
|
* $view->current_page.
|
860
|
*/
|
861
|
function execute(&$view) {
|
862
|
$display_handler = &$view->display_handler;
|
863
|
$external = FALSE; // Whether this query will run against an external database.
|
864
|
$query = $view->build_info['query'];
|
865
|
$count_query = $view->build_info['count_query'];
|
866
|
|
867
|
$query->addMetaData('view', $view);
|
868
|
$count_query->addMetaData('view', $view);
|
869
|
|
870
|
if (empty($this->options['disable_sql_rewrite'])) {
|
871
|
$base_table_data = views_fetch_data($this->base_table);
|
872
|
if (isset($base_table_data['table']['base']['access query tag'])) {
|
873
|
$access_tag = $base_table_data['table']['base']['access query tag'];
|
874
|
$query->addTag($access_tag);
|
875
|
$count_query->addTag($access_tag);
|
876
|
}
|
877
|
}
|
878
|
|
879
|
$items = array();
|
880
|
if ($query) {
|
881
|
$additional_arguments = module_invoke_all('views_query_substitutions', $view);
|
882
|
|
883
|
// Count queries must be run through the preExecute() method.
|
884
|
// If not, then hook_query_node_access_alter() may munge the count by
|
885
|
// adding a distinct against an empty query string
|
886
|
// (e.g. COUNT DISTINCT(1) ...) and no pager will return.
|
887
|
// See pager.inc > PagerDefault::execute()
|
888
|
// http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
|
889
|
// See http://drupal.org/node/1046170.
|
890
|
$count_query->preExecute();
|
891
|
|
892
|
// Build the count query.
|
893
|
$count_query = $count_query->countQuery();
|
894
|
|
895
|
// Add additional arguments as a fake condition.
|
896
|
// XXX: this doesn't work... because PDO mandates that all bound arguments
|
897
|
// are used on the query. TODO: Find a better way to do this.
|
898
|
if (!empty($additional_arguments)) {
|
899
|
// $query->where('1 = 1', $additional_arguments);
|
900
|
// $count_query->where('1 = 1', $additional_arguments);
|
901
|
}
|
902
|
|
903
|
$start = microtime(TRUE);
|
904
|
|
905
|
if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
|
906
|
$this->pager->execute_count_query($count_query);
|
907
|
}
|
908
|
|
909
|
// Let the pager modify the query to add limits.
|
910
|
$this->pager->pre_execute($query);
|
911
|
|
912
|
if (!empty($this->limit) || !empty($this->offset)) {
|
913
|
// We can't have an offset without a limit, so provide a very large limit instead.
|
914
|
$limit = intval(!empty($this->limit) ? $this->limit : 999999);
|
915
|
$offset = intval(!empty($this->offset) ? $this->offset : 0);
|
916
|
$query->range($offset, $limit);
|
917
|
}
|
918
|
|
919
|
try {
|
920
|
// The $query is final and ready to go, we are going to redirect it to
|
921
|
// become an insert into our table, sneaky!
|
922
|
// Our query will look like:
|
923
|
// CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
|
924
|
// (-query-) AS cl, (SELECT @row := 0) AS r
|
925
|
// We do some magic to get the row count.
|
926
|
|
927
|
$display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
|
928
|
|
929
|
$display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
|
930
|
$select_aliases = array();
|
931
|
foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
|
932
|
$select_aliases[] = "cl.$alias AS $hash";
|
933
|
}
|
934
|
|
935
|
// TODO: this could probably be replaced with a query extender and new query type.
|
936
|
$query->preExecute();
|
937
|
$args = $query->getArguments();
|
938
|
$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';
|
939
|
db_query($insert_query, $args);
|
940
|
|
941
|
|
942
|
$view->result = array();
|
943
|
|
944
|
$this->pager->post_execute($view->result);
|
945
|
|
946
|
if ($this->pager->use_pager()) {
|
947
|
$view->total_rows = $this->pager->get_total_items();
|
948
|
}
|
949
|
|
950
|
// Now create an index for the weight field, otherwise the queries on the
|
951
|
// index will take a long time to execute.
|
952
|
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']));
|
953
|
}
|
954
|
catch (Exception $e) {
|
955
|
$view->result = array();
|
956
|
debug('Exception: ' . $e->getMessage());
|
957
|
}
|
958
|
|
959
|
}
|
960
|
$view->execute_time = microtime(TRUE) - $start;
|
961
|
}
|
962
|
}
|
963
|
|
964
|
class views_data_export_plugin_query_pgsql_batched extends views_data_export_plugin_query_default_batched {
|
965
|
|
966
|
|
967
|
/**
|
968
|
* Executes the query and fills the associated view object with according
|
969
|
* values.
|
970
|
*
|
971
|
* Values to set: $view->result, $view->total_rows, $view->execute_time,
|
972
|
* $view->current_page.
|
973
|
*/
|
974
|
function execute(&$view) {
|
975
|
$display_handler = &$view->display_handler;
|
976
|
$external = FALSE; // Whether this query will run against an external database.
|
977
|
$query = $view->build_info['query'];
|
978
|
$count_query = $view->build_info['count_query'];
|
979
|
|
980
|
$query->addMetaData('view', $view);
|
981
|
$count_query->addMetaData('view', $view);
|
982
|
|
983
|
if (empty($this->options['disable_sql_rewrite'])) {
|
984
|
$base_table_data = views_fetch_data($this->base_table);
|
985
|
if (isset($base_table_data['table']['base']['access query tag'])) {
|
986
|
$access_tag = $base_table_data['table']['base']['access query tag'];
|
987
|
$query->addTag($access_tag);
|
988
|
$count_query->addTag($access_tag);
|
989
|
}
|
990
|
}
|
991
|
|
992
|
$items = array();
|
993
|
if ($query) {
|
994
|
$additional_arguments = module_invoke_all('views_query_substitutions', $view);
|
995
|
|
996
|
// Count queries must be run through the preExecute() method.
|
997
|
// If not, then hook_query_node_access_alter() may munge the count by
|
998
|
// adding a distinct against an empty query string
|
999
|
// (e.g. COUNT DISTINCT(1) ...) and no pager will return.
|
1000
|
// See pager.inc > PagerDefault::execute()
|
1001
|
// http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
|
1002
|
// See http://drupal.org/node/1046170.
|
1003
|
$count_query->preExecute();
|
1004
|
|
1005
|
// Build the count query.
|
1006
|
$count_query = $count_query->countQuery();
|
1007
|
|
1008
|
// Add additional arguments as a fake condition.
|
1009
|
// XXX: this doesn't work... because PDO mandates that all bound arguments
|
1010
|
// are used on the query. TODO: Find a better way to do this.
|
1011
|
if (!empty($additional_arguments)) {
|
1012
|
// $query->where('1 = 1', $additional_arguments);
|
1013
|
// $count_query->where('1 = 1', $additional_arguments);
|
1014
|
}
|
1015
|
|
1016
|
$start = microtime(TRUE);
|
1017
|
|
1018
|
if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
|
1019
|
$this->pager->execute_count_query($count_query);
|
1020
|
}
|
1021
|
|
1022
|
// Let the pager modify the query to add limits.
|
1023
|
$this->pager->pre_execute($query);
|
1024
|
|
1025
|
if (!empty($this->limit) || !empty($this->offset)) {
|
1026
|
// We can't have an offset without a limit, so provide a very large limit instead.
|
1027
|
$limit = intval(!empty($this->limit) ? $this->limit : 999999);
|
1028
|
$offset = intval(!empty($this->offset) ? $this->offset : 0);
|
1029
|
$query->range($offset, $limit);
|
1030
|
}
|
1031
|
|
1032
|
try {
|
1033
|
// The $query is final and ready to go, we are going to redirect it to
|
1034
|
// become an insert into our table, sneaky!
|
1035
|
// Our query will look like:
|
1036
|
// CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
|
1037
|
// (-query-) AS cl, (SELECT @row := 0) AS r
|
1038
|
// We do some magic to get the row count.
|
1039
|
|
1040
|
$display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
|
1041
|
|
1042
|
$display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
|
1043
|
$select_aliases = array();
|
1044
|
foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
|
1045
|
$select_aliases[] = "cl.$alias AS $hash";
|
1046
|
}
|
1047
|
|
1048
|
// TODO: this could probably be replaced with a query extender and new query type.
|
1049
|
$query->preExecute();
|
1050
|
$args = $query->getArguments();
|
1051
|
// Create temporary sequence
|
1052
|
$seq_name = $display_handler->index_tablename() . '_seq';
|
1053
|
db_query('CREATE TEMP sequence ' . $seq_name);
|
1054
|
// Query uses sequence to create row number
|
1055
|
$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';
|
1056
|
db_query($insert_query, $args);
|
1057
|
|
1058
|
|
1059
|
$view->result = array();
|
1060
|
|
1061
|
$this->pager->post_execute($view->result);
|
1062
|
|
1063
|
if ($this->pager->use_pager()) {
|
1064
|
$view->total_rows = $this->pager->get_total_items();
|
1065
|
}
|
1066
|
|
1067
|
// Now create an index for the weight field, otherwise the queries on the
|
1068
|
// index will take a long time to execute.
|
1069
|
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']));
|
1070
|
}
|
1071
|
catch (Exception $e) {
|
1072
|
$view->result = array();
|
1073
|
debug('Exception: ' . $e->getMessage());
|
1074
|
}
|
1075
|
|
1076
|
}
|
1077
|
$view->execute_time = microtime(TRUE) - $start;
|
1078
|
}
|
1079
|
}
|