Implement exporting function modeled after import function

This commit is contained in:
Samuel Georges 2015-07-23 19:44:19 +10:00
parent 5be21f6231
commit d0742653d1
13 changed files with 694 additions and 51 deletions

View File

@ -3,7 +3,6 @@
use Str;
use Lang;
use Backend\Classes\ControllerBehavior;
use League\Csv\Writer as CsvWriter;
use League\Csv\Reader as CsvReader;
use ApplicationException;
use Exception;
@ -44,10 +43,35 @@ class ImportExportController extends ControllerBehavior
protected $importUploadFormWidget;
/**
* @var Backend\Classes\WidgetBase Reference to the widget used for specifing import options.
* @var Backend\Classes\WidgetBase Reference to the widget used for specifying import options.
*/
protected $importOptionsFormWidget;
/**
* @var Model Export model
*/
public $exportModel;
/**
* @var array Export column configuration.
*/
public $exportColumns;
/**
* @var string File name used for export output.
*/
protected $exportFileName = 'export.csv';
/**
* @var Backend\Classes\WidgetBase Reference to the widget used for standard export options.
*/
protected $exportFormatFormWidget;
/**
* @var Backend\Classes\WidgetBase Reference to the widget used for custom export options.
*/
protected $exportOptionsFormWidget;
/**
* Behavior constructor
* @param Backend\Classes\Controller $controller
@ -57,13 +81,22 @@ class ImportExportController extends ControllerBehavior
parent::__construct($controller);
$this->addJs('js/october.import.js', 'core');
$this->addJs('js/october.export.js', 'core');
$this->addCss('css/import.css', 'core');
$this->addCss('css/export.css', 'core');
/*
* Build configuration
*/
$this->config = $this->makeConfig($controller->importExportConfig, $this->requiredConfig);
/*
* Process config
*/
if ($exportFileName = $this->getConfig('export[fileName]')) {
$this->exportFileName = $exportFileName;
}
/*
* Import form widgets
*/
@ -75,6 +108,16 @@ class ImportExportController extends ControllerBehavior
$this->importOptionsFormWidget->bindToController();
}
/*
* Export form widgets
*/
if ($this->exportFormatFormWidget = $this->makeExportFormatFormWidget()) {
$this->exportFormatFormWidget->bindToController();
}
if ($this->exportOptionsFormWidget = $this->makeExportOptionsFormWidget()) {
$this->exportOptionsFormWidget->bindToController();
}
}
//
@ -86,12 +129,23 @@ class ImportExportController extends ControllerBehavior
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('import[title]', 'Import records'));
$this->prepareVars();
$this->prepareImportVars();
}
public function export()
{
// TBA
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('export[title]', 'Export records'));
$this->prepareExportVars();
}
public function download($name, $outputName = null)
{
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('export[title]', 'Export records'));
return $this->exportGetModel()->download($name, $outputName);
}
//
@ -103,13 +157,13 @@ class ImportExportController extends ControllerBehavior
try {
$model = $this->importGetModel();
$matches = post('column_match', []);
$sessionKey = $this->importUploadFormWidget->getSessionKey();
if ($optionData = post('ImportOptions')) {
$model->fill($optionData);
}
$model->importDataFromColumnMatch($matches, $sessionKey, [
$model->import($matches, [
'sessionKey' => $this->importUploadFormWidget->getSessionKey(),
'firstRowTitles' => post('first_row_titles', false)
]);
@ -178,7 +232,7 @@ class ImportExportController extends ControllerBehavior
* Prepares the view data.
* @return void
*/
public function prepareVars()
public function prepareImportVars()
{
$this->vars['importUploadFormWidget'] = $this->importUploadFormWidget;
$this->vars['importOptionsFormWidget'] = $this->importOptionsFormWidget;
@ -196,12 +250,7 @@ class ImportExportController extends ControllerBehavior
public function importGetModel()
{
if ($this->importModel !== null) {
return $this->importModel;
}
$modelClass = $this->getConfig('import[modelClass]');
return $this->importModel = new $modelClass;
return $this->getModelForType('import');
}
protected function getImportDbColumns()
@ -233,6 +282,10 @@ class ImportExportController extends ControllerBehavior
protected function makeImportUploadFormWidget()
{
if (!$this->getConfig('import')) {
return null;
}
$widgetConfig = $this->makeConfig('~/modules/backend/behaviors/importexportcontroller/partials/fields_import.yaml');
$widgetConfig->model = $this->importGetModel();
$widgetConfig->alias = 'importUploadForm';
@ -248,21 +301,14 @@ class ImportExportController extends ControllerBehavior
protected function makeImportOptionsFormWidget()
{
if ($fieldConfig = $this->getConfig('import[form]')) {
$widgetConfig = $this->makeConfig($fieldConfig);
$widgetConfig->model = $this->importGetModel();
$widgetConfig->alias = 'importOptionsForm';
$widgetConfig->arrayName = 'ImportOptions';
$widget = $this->makeOptionsFormWidgetForType('import');
$widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
return $widget;
}
elseif ($this->importUploadFormWidget) {
if (!$widget && $this->importUploadFormWidget) {
$stepSection = $this->importUploadFormWidget->getField('step3_section');
$stepSection->hidden = true;
}
return null;
return $widget;
}
protected function getImportFilePath()
@ -302,10 +348,136 @@ class ImportExportController extends ControllerBehavior
}
}
//
// Exporting AJAX
//
public function onExport()
{
try {
$model = $this->exportGetModel();
$columns = $this->processExportColumnsFromPost();
$exportOptions = [
'sessionKey' => $this->exportFormatFormWidget->getSessionKey()
];
if ($optionData = post('ExportOptions')) {
$model->fill($optionData);
}
if (post('format_preset') == 'custom') {
$exportOptions['delimiter'] = post('format_delimiter');
$exportOptions['enclosure'] = post('format_enclosure');
$exportOptions['escape'] = post('format_escape');
}
$reference = $model->export($columns, $exportOptions);
$this->vars['fileUrl'] = $this->controller->actionUrl(
'download',
$reference.'/'.$this->exportFileName
);
}
catch (Exception $ex) {
$this->controller->handleError($ex);
}
return $this->importExportMakePartial('export_result_form');
}
public function onExportLoadForm()
{
return $this->importExportMakePartial('export_form');
}
//
// Exporting
//
/**
* Prepares the view data.
* @return void
*/
public function prepareExportVars()
{
$this->vars['exportFormatFormWidget'] = $this->exportFormatFormWidget;
$this->vars['exportOptionsFormWidget'] = $this->exportOptionsFormWidget;
$this->vars['exportColumns'] = $this->getExportColumns();
// Make these variables available to widgets
$this->controller->vars += $this->vars;
}
public function exportRender()
{
return $this->importExportMakePartial('export');
}
public function exportGetModel()
{
return $this->getModelForType('export');
}
protected function getExportColumns()
{
if ($this->exportColumns !== null) {
return $this->exportColumns;
}
$columnConfig = $this->getConfig('export[list]');
return $this->exportColumns = $this->makeListColumns($columnConfig);
}
protected function makeExportFormatFormWidget()
{
if (!$this->getConfig('export')) {
return null;
}
$widgetConfig = $this->makeConfig('~/modules/backend/behaviors/importexportcontroller/partials/fields_export.yaml');
$widgetConfig->model = $this->exportGetModel();
$widgetConfig->alias = 'exportUploadForm';
$widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
$widget->bindEvent('form.beforeRefresh', function($holder) {
$holder->data = [];
});
return $widget;
}
protected function makeExportOptionsFormWidget()
{
$widget = $this->makeOptionsFormWidgetForType('export');
if (!$widget && $this->exportFormatFormWidget) {
$stepSection = $this->exportFormatFormWidget->getField('step3_section');
$stepSection->hidden = true;
}
return $widget;
}
protected function processExportColumnsFromPost()
{
$visibleColumns = post('visible_columns', []);
$columns = post('export_columns', []);
foreach ($columns as $key => $columnName) {
if (!isset($visibleColumns[$columnName])) {
unset($columns[$key]);
}
}
$result = [];
$definitions = $this->getExportColumns();
foreach ($columns as $column) {
$result[$column] = array_get($definitions, $column, '???');
}
return $result;
}
//
// Helpers
@ -327,6 +499,41 @@ class ImportExportController extends ControllerBehavior
return $contents;
}
protected function makeOptionsFormWidgetForType($type)
{
if (!$this->getConfig($type)) {
return null;
}
if ($fieldConfig = $this->getConfig($type.'[form]')) {
$widgetConfig = $this->makeConfig($fieldConfig);
$widgetConfig->model = $this->getModelForType($type);
$widgetConfig->alias = $type.'OptionsForm';
$widgetConfig->arrayName = ucfirst($type).'Options';
$widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
return $widget;
}
return null;
}
protected function getModelForType($type)
{
$cacheProperty = $type.'Model';
if ($this->{$cacheProperty} !== null) {
return $this->{$cacheProperty};
}
$modelClass = $this->getConfig($type.'[modelClass]');
if (!$modelClass) {
throw new ApplicationException('Please specify the modelClass property for '.$type);
}
return $this->{$cacheProperty} = new $modelClass;
}
protected function makeListColumns($config)
{
$config = $this->makeConfig($config);

View File

@ -0,0 +1,7 @@
.export-behavior .export-columns {
max-height: 400px;
background: #f0f0f0;
padding: 15px;
padding-bottom: 0;
overflow: auto;
}

View File

@ -0,0 +1,22 @@
/*
* Scripts for the Export controller behavior.
*/
+function ($) { "use strict";
var ExportBehavior = function() {
this.processExport = function () {
var $form = $('#exportColumns').closest('form')
$form.request('onExport', {
success: function(data) {
$('#exportContainer').html(data.result)
$(document).trigger('render')
}
})
}
}
$.oc.exportBehavior = new ExportBehavior;
}(window.jQuery);

View File

@ -1,10 +1,34 @@
/*
* Scripts for the Import/Export controller behavior.
* Scripts for the Import controller behavior.
*/
+function ($) { "use strict";
var ImportBehavior = function() {
this.processImport = function () {
var $form = $('#importFileColumns').closest('form')
$form.request('onImport', {
success: function(data) {
$('#importContainer').html(data.result)
$(document).trigger('render')
}
})
}
this.loadFileColumnSample = function(el) {
var $el = $(el),
$column = $el.closest('[data-column-id]'),
columnId = $column.data('column-id')
$el.popup({
handler: 'onImportLoadColumnSampleForm',
extraData: {
file_column_id: columnId
}
})
}
this.bindColumnSorting = function() {
/*
* Unbind existing
@ -85,29 +109,6 @@
$('#showIgnoredColumnsButton').addClass('disabled')
}
this.loadFileColumnSample = function(el) {
var $el = $(el),
$column = $el.closest('[data-column-id]'),
columnId = $column.data('column-id')
$el.popup({
handler: 'onImportLoadColumnSampleForm',
extraData: {
file_column_id: columnId
}
})
}
this.processImport = function () {
var $form = $('#importFileColumns').closest('form')
$form.request('onImport', {
success: function(data) {
$('#importContainer').html(data.result)
$(document).trigger('render')
}
})
}
}
$.oc.importBehavior = new ImportBehavior;

View File

@ -0,0 +1,13 @@
@import "../../../../assets/less/core/boot.less";
.export-behavior {
.export-columns {
max-height: 400px;
background: #f0f0f0;
padding: 15px;
padding-bottom: 0;
overflow: auto;
}
}

View File

@ -0,0 +1,9 @@
<div class="export-behavior">
<?= $exportFormatFormWidget->render() ?>
<?php if ($exportOptionsFormWidget): ?>
<?= $exportOptionsFormWidget->render() ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,27 @@
<div class="export-columns" id="exportColumns">
<div class="control-simplelist with-checkboxes is-sortable" data-control="simplelist">
<ul>
<?php foreach ($exportColumns as $key => $column): ?>
<li>
<div class="checkbox custom-checkbox">
<input
type="hidden"
name="export_columns[]"
value="<?= $key ?>" />
<input
id="<?= $this->getId('exportCheckbox-'.$key) ?>"
name="visible_columns[<?= $key ?>]"
value="1"
checked="checked"
type="checkbox" />
<label
class="choice"
for="<?= $this->getId('exportCheckbox-'.$key) ?>">
<?= e(trans($column)) ?>
</label>
</div>
</li>
<?php endforeach ?>
</ul>
</div>
</div>

View File

@ -0,0 +1,51 @@
<div id="exportFormPopup">
<?php if (!$this->fatalError): ?>
<?= Form::open(['id' => 'exportForm']) ?>
<div class="modal-header">
<h4 class="modal-title">Export progress</h4>
</div>
<div id="exportContainer">
<div class="modal-body">
<div class="loading-indicator-container">
<p>&nbsp;</p>
<div class="loading-indicator transparent">
<div>Processing</div>
<span></span>
</div>
</div>
<p>&nbsp;</p>
</div>
</div>
<?= Form::close() ?>
<script>
$('#exportFormPopup').on('popupComplete', function() {
$.oc.exportBehavior.processExport()
})
</script>
<?php else: ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title">Export error</h4>
</div>
<div class="modal-body">
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.close')) ?>
</button>
</div>
<?php endif ?>
</div>

View File

@ -0,0 +1,34 @@
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<p>
File export process has completed successfully!
The browser should now redirect automatically to the file download.
</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-success"
data-dismiss="popup">
<?= e(trans('backend::lang.form.complete')) ?>
</button>
</div>
<script> window.location = '<?= $fileUrl ?>' </script>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.close')) ?>
</button>
</div>
<?php endif ?>

View File

@ -2,6 +2,8 @@
<?= $importUploadFormWidget->render() ?>
<?= $importOptionsFormWidget->render() ?>
<?php if ($importOptionsFormWidget): ?>
<?= $importOptionsFormWidget->render() ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,58 @@
# ===================================
# Field Definitions
# ===================================
fields:
step1_section:
label: 1. Export output format
type: section
format_preset:
label: File format
type: dropdown
default: standard
options:
standard: Standard format
custom: Custom format
span: left
format_delimiter:
label: Delimiter character
default: ','
span: left
trigger:
action: show
condition: value[custom]
field: format_preset
format_enclosure:
label: Enclosure character
span: auto
default: '"'
trigger:
action: show
condition: value[custom]
field: format_preset
format_escape:
label: Escape character
span: auto
default: '\\'
trigger:
action: show
condition: value[custom]
field: format_preset
step2_section:
label: 2. Select columns to export
type: section
export_columns:
label: Columns
type: partial
path: ~/modules/backend/behaviors/importexportcontroller/partials/_export_columns.htm
span: left
step3_section:
label: 3. Set export options
type: section

View File

@ -0,0 +1,186 @@
<?php namespace Backend\Models;
use File;
use Lang;
use Model;
use Response;
use League\Csv\Writer as CsvWriter;
use ApplicationException;
use SplTempFileObject;
/**
* Model used for exporting data
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
abstract class ExportModel extends Model
{
/**
* Called when data is being exported.
* The return value should be an array in the format of:
*
* [
* 'db_name1' => 'Some attribute value',
* 'db_name2' => 'Another attribute value'
* ],
* [...]
*
*/
abstract public function exportData($columns, $sessionKey = null);
/**
* Export data based on column names and labels.
* The $columns array should be in the format of:
*
* [
* 'db_name1' => 'Column label',
* 'db_name2' => 'Another label',
* ...
* ]
*
*/
public function export($columns, $options)
{
$sessionKey = array_get($options, 'sessionKey');
$data = $this->exportData(array_keys($columns), $sessionKey);
return $this->processExportData($columns, $data, $options);
}
/**
* Download a previously compiled export file.
* @return void
*/
public function download($name, $outputName = null)
{
if (!preg_match('/^oc[0-9a-z]*$/i', $name)) {
throw new ApplicationException('File not found');
}
$csvPath = temp_path() . '/' . $name;
if (!file_exists($csvPath)) {
throw new ApplicationException('File not found');
}
$headers = Response::download($csvPath, $outputName)->headers->all();
$result = Response::make(File::get($csvPath), 200, $headers);
@unlink($csvPath);
return $result;
}
/**
* Converts a data collection to a CSV file.
*/
protected function processExportData($columns, $results, $options)
{
/*
* Validate
*/
if (!$results) {
throw new ApplicationException('There was no data supplied to export');
}
/*
* Parse options
*/
$defaultOptions = [
'useOutput' => false,
'fileName' => 'export.csv',
'delimiter' => null,
'enclosure' => null,
'escape' => null
];
$options = array_merge($defaultOptions, $options);
$columns = $this->exportExtendColumns($columns);
/*
* Prepare CSV
*/
$csv = CsvWriter::createFromFileObject(new SplTempFileObject);
if ($options['delimiter'] !== null) {
$csv->setDelimiter($options['delimiter']);
}
if ($options['enclosure'] !== null) {
$csv->setEnclosure($options['enclosure']);
}
if ($options['escape'] !== null) {
$csv->setEscape($options['escape']);
}
/*
* Add headers
*/
$headers = $this->getColumnHeaders($columns);
$csv->insertOne($headers);
/*
* Add records
*/
foreach ($results as $result) {
$data = $this->matchDataToColumns($result, $columns);
$csv->insertOne($data);
}
/*
* Output
*/
if ($options['useOutput']) {
$csv->output($options['fileName']);
}
/*
* Save for download
*/
$csvName = uniqid('oc');
$csvPath = temp_path().'/'.$csvName;
$output = $csv->__toString();
File::put($csvPath, $output);
return $csvName;
}
/**
* Used to override column definitions at export time.
*/
protected function exportExtendColumns($columns)
{
return $columns;
}
/**
* Extracts the headers from the column definitions.
*/
protected function getColumnHeaders($columns)
{
$headers = [];
foreach ($columns as $column => $label) {
$headers[] = Lang::get($label);
}
return $headers;
}
/**
* Ensures the correct order of the column data.
*/
protected function matchDataToColumns($data, $columns)
{
$results = [];
foreach ($columns as $column => $label) {
$results[] = array_get($data, $column);
}
return $results;
}
}

View File

@ -26,6 +26,9 @@ abstract class ImportModel extends Model
'import_file' => ['System\Models\File']
];
/**
* @var array Import statistics store.
*/
protected $resultStats = [
'updated' => 0,
'created' => 0,
@ -36,11 +39,33 @@ abstract class ImportModel extends Model
/**
* Called when data is being imported.
* The $results array should be in the format of:
*
* [
* 'db_name1' => 'Some value',
* 'db_name2' => 'Another value'
* ],
* [...]
*
*/
abstract public function importData($results, $sessionKey = null);
public function importDataFromColumnMatch($matches, $sessionKey = null, $options = [])
/**
* Import data based on column names matching header indexes in the CSV.
* The $matches array should be in the format of:
*
* [
* 0 => [db_name1, db_name2],
* 1 => [db_name3],
* ...
* ]
*
* The key (0, 1) is the column index in the CSV and the value
* is another array of target database column names.
*/
public function import($matches, $options = [])
{
$sessionKey = array_get($options, 'sessionKey');
$path = $this->getImportFilePath($sessionKey);
$data = $this->processImportData($path, $matches, $options);
return $this->importData($data, $sessionKey);
@ -48,11 +73,12 @@ abstract class ImportModel extends Model
/**
* Converts column index to database column map to an array containing
* database column names and values pulled from the CSV file.
* Eg:
* database column names and values pulled from the CSV file. Eg:
*
* [0 => [first_name], 1 => [last_name]]
*
* Will return:
*
* [first_name => Joe, last_name => Blogs],
* [first_name => Harry, last_name => Potter],
* [...]