From 86011d4ea21ddc5a23c6eb23087c7ca7854546ad Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 3 Jul 2015 10:45:00 +0200 Subject: [PATCH 01/10] Initial commit for CSV import. --- app/Http/Controllers/CsvController.php | 35 +++++++++++++++++++++++ app/Http/routes.php | 6 ++++ resources/lang/en/firefly.php | 3 ++ resources/lang/nl/firefly.php | 3 ++ resources/twig/csv/index.twig | 12 ++++++++ resources/twig/partials/menu-sidebar.twig | 3 ++ 6 files changed, 62 insertions(+) create mode 100644 app/Http/Controllers/CsvController.php create mode 100644 resources/twig/csv/index.twig diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php new file mode 100644 index 0000000000..723b1a33e1 --- /dev/null +++ b/app/Http/Controllers/CsvController.php @@ -0,0 +1,35 @@ + 'CategoryController@update', 'as' => 'categories.update']); Route::post('/categories/destroy/{category}', ['uses' => 'CategoryController@destroy', 'as' => 'categories.destroy']); + + /** + * CSV controller + */ + Route::get('/csv', ['uses' => 'CsvController@index', 'as' => 'csv.index']); + /** * Currency Controller */ diff --git a/resources/lang/en/firefly.php b/resources/lang/en/firefly.php index d6feb0f661..f7d834653a 100644 --- a/resources/lang/en/firefly.php +++ b/resources/lang/en/firefly.php @@ -19,6 +19,9 @@ return [ 'never' => 'Never', 'search_results_for' => 'Search results for ":query"', + // csv import: + 'csv_import' => 'Import CSV file', + // create new stuff: 'create_new_withdrawal' => 'Create new withdrawal', 'create_new_deposit' => 'Create new deposit', diff --git a/resources/lang/nl/firefly.php b/resources/lang/nl/firefly.php index bdab21f817..6c03404832 100644 --- a/resources/lang/nl/firefly.php +++ b/resources/lang/nl/firefly.php @@ -19,6 +19,9 @@ return [ 'never' => 'Nooit', 'search_results_for' => 'Zoekresultaten voor ":query"', + // csv import: + 'csv_import' => 'Import CSV file', + // create new stuff: 'create_new_withdrawal' => 'Nieuwe uitgave', 'create_new_deposit' => 'Nieuwe inkomsten', diff --git a/resources/twig/csv/index.twig b/resources/twig/csv/index.twig new file mode 100644 index 0000000000..5dd98e4135 --- /dev/null +++ b/resources/twig/csv/index.twig @@ -0,0 +1,12 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + + Bla bla. + +{% endblock %} diff --git a/resources/twig/partials/menu-sidebar.twig b/resources/twig/partials/menu-sidebar.twig index ba080666f2..76bc4f4cfb 100644 --- a/resources/twig/partials/menu-sidebar.twig +++ b/resources/twig/partials/menu-sidebar.twig @@ -123,6 +123,9 @@
  • {{ 'currencies'|_ }}
  • +
  • + {{ 'csv_import'|_ }} +
  • From 16374bce9b19fe1db599d102007fc93d3a7ca064 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 3 Jul 2015 11:52:51 +0200 Subject: [PATCH 02/10] Fixed upload form, made a new form element, added some processing. --- app/Http/Controllers/CsvController.php | 61 +- app/Http/routes.php | 1 + app/Support/ExpandedForm.php | 18 + composer.json | 3 +- composer.lock | 2415 +++++++++++++++++++++++- config/twigbridge.php | 4 +- resources/lang/en/firefly.php | 7 + resources/lang/en/form.php | 2 + resources/lang/nl/firefly.php | 9 +- resources/lang/nl/form.php | 2 + resources/twig/csv/index.twig | 77 +- resources/twig/form/checkbox.twig | 1 + resources/twig/form/file.twig | 9 + storage/upload/.gitignore | 2 + 14 files changed, 2533 insertions(+), 78 deletions(-) create mode 100644 resources/twig/form/file.twig create mode 100644 storage/upload/.gitignore diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php index 723b1a33e1..3f2504a7a4 100644 --- a/app/Http/Controllers/CsvController.php +++ b/app/Http/Controllers/CsvController.php @@ -7,6 +7,12 @@ */ namespace FireflyIII\Http\Controllers; + +use Illuminate\Http\Request; +use Input; +use League\Csv\Reader; +use Redirect; +use Session; use View; /** @@ -23,13 +29,64 @@ class CsvController extends Controller public function __construct() { parent::__construct(); - View::share('title', 'CSV'); + View::share('title', trans('firefly.csv')); View::share('mainTitleIcon', 'fa-file-text-o'); } + /** + * @return View + */ public function index() { - return view('csv.index'); + $subTitle = trans('firefly.csv_import'); + + // can actually upload? + $uploadPossible = !is_writable(storage_path('upload')); + $path = storage_path('upload'); + + + return view('csv.index', compact('subTitle', 'uploadPossible', 'path')); + } + + /** + * + */ + public function upload(Request $request) + { + if (!$request->hasFile('csv')) { + Session::flash('warning', 'No file uploaded.'); + + + return Redirect::route('csv.index'); + } + $hasHeaders = intval(Input::get('has_headers')) === 1; + $reader = Reader::createFromPath($request->file('csv')->getRealPath()); + $data = $reader->query(); + $data->next(); // go to first row: + if ($hasHeaders) { + + // first row = headers. + $headers = $data->current(); + } else { + $count = count($data->current()); + $headers = []; + for ($i = 1; $i <= $count; $i++) { + $headers[] = trans('firefly.csv_row') . ' #' . $i; + } + } + + // example data is always the second row: + $data->next(); + $example = $data->current(); + + var_dump($headers); + var_dump($example); + + // store file somewhere temporary? + + + exit; + } } \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 0c5d9174ee..e0447eba0c 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -224,6 +224,7 @@ Route::group( * CSV controller */ Route::get('/csv', ['uses' => 'CsvController@index', 'as' => 'csv.index']); + Route::post('/csv/upload', ['uses' => 'CsvController@upload', 'as' => 'csv.upload']); /** * Currency Controller diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 4bae2bcbbb..62505adac6 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -347,6 +347,24 @@ class ExpandedForm return $html; } + /** + * @param $name + * @param null $value + * @param array $options + * + * @return string + */ + public function file($name, array $options = []) + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $html = View::make('form.file', compact('classes', 'name', 'label', 'options'))->render(); + + return $html; + + } + /** * @param $name * @param null $value diff --git a/composer.json b/composer.json index b1134d9cd1..54b352de13 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "illuminate/html": "~5.0", "league/commonmark": "0.7.*", "rcrowe/twigbridge": "0.7.x@dev", - "zizaco/entrust": "dev-laravel-5" + "zizaco/entrust": "dev-laravel-5", + "league/csv": "^7.1" }, "require-dev": { "barryvdh/laravel-debugbar": "@stable", diff --git a/composer.lock b/composer.lock index dbc3422a9f..87162d93ae 100644 --- a/composer.lock +++ b/composer.lock @@ -4,39 +4,33 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "c1cfabc3da3bdec86d8912a6878e0b0f", + "hash": "9747e35883b8d868d434b5d887963634", "packages": [ { "name": "classpreloader/classpreloader", - "version": "1.4.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/ClassPreloader/ClassPreloader.git", - "reference": "b76f3f4f603ebbe7e64351a7ef973431ddaf7b27" + "reference": "8c3c14b10309e3b40bce833913a6c0c0b8c8f962" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ClassPreloader/ClassPreloader/zipball/b76f3f4f603ebbe7e64351a7ef973431ddaf7b27", - "reference": "b76f3f4f603ebbe7e64351a7ef973431ddaf7b27", + "url": "https://api.github.com/repos/ClassPreloader/ClassPreloader/zipball/8c3c14b10309e3b40bce833913a6c0c0b8c8f962", + "reference": "8c3c14b10309e3b40bce833913a6c0c0b8c8f962", "shasum": "" }, "require": { "nikic/php-parser": "~1.3", - "php": ">=5.3.3", - "symfony/console": "~2.1", - "symfony/filesystem": "~2.1", - "symfony/finder": "~2.1" + "php": ">=5.5.9" }, "require-dev": { "phpunit/phpunit": "~4.0" }, - "bin": [ - "classpreloader.php" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -55,7 +49,7 @@ }, { "name": "Graham Campbell", - "email": "graham@cachethq.io" + "email": "graham@alt-three.com" } ], "description": "Helps class loading performance by generating a single PHP file containing all of the autoloaded files for a specific use case", @@ -64,7 +58,7 @@ "class", "preload" ], - "time": "2015-05-26 10:57:51" + "time": "2015-06-28 21:39:13" }, { "name": "danielstjules/stringy", @@ -902,20 +896,20 @@ }, { "name": "laravel/framework", - "version": "v5.1.3", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "a7ebd043ca178c05cc7946ece03d3a268a5a3f84" + "reference": "25d872ec1945b30083f3e348039c380307b42d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/a7ebd043ca178c05cc7946ece03d3a268a5a3f84", - "reference": "a7ebd043ca178c05cc7946ece03d3a268a5a3f84", + "url": "https://api.github.com/repos/laravel/framework/zipball/25d872ec1945b30083f3e348039c380307b42d5e", + "reference": "25d872ec1945b30083f3e348039c380307b42d5e", "shasum": "" }, "require": { - "classpreloader/classpreloader": "~1.2", + "classpreloader/classpreloader": "~2.0", "danielstjules/stringy": "~1.8", "doctrine/inflector": "~1.0", "ext-mbstring": "*", @@ -1026,7 +1020,7 @@ "framework", "laravel" ], - "time": "2015-06-24 19:38:07" + "time": "2015-07-02 02:28:22" }, { "name": "league/commonmark", @@ -1087,6 +1081,63 @@ ], "time": "2015-03-08 17:48:53" }, + { + "name": "league/csv", + "version": "7.1.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "2ee1760c262c41986f6371775907fc9e8603fd26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/2ee1760c262c41986f6371775907fc9e8603fd26", + "reference": "2ee1760c262c41986f6371775907fc9e8603fd26", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "scrutinizer/ocular": "~1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Csv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "Csv data manipulation made easy in PHP", + "homepage": "http://csv.thephpleague.com", + "keywords": [ + "csv", + "export", + "filter", + "import", + "read", + "write" + ], + "time": "2015-06-10 11:12:37" + }, { "name": "league/flysystem", "version": "1.0.4", @@ -1890,55 +1941,6 @@ "homepage": "https://symfony.com", "time": "2015-06-08 09:37:21" }, - { - "name": "symfony/filesystem", - "version": "v2.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/Filesystem.git", - "reference": "a0d43eb3e17d4f4c6990289805a488a0482a07f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Filesystem/zipball/a0d43eb3e17d4f4c6990289805a488a0482a07f3", - "reference": "a0d43eb3e17d4f4c6990289805a488a0482a07f3", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2015-06-08 09:37:21" - }, { "name": "symfony/finder", "version": "v2.7.1", @@ -2587,7 +2589,2278 @@ "time": "2015-05-25 00:17:51" } ], - "packages-dev": null, + "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "3edaea0e8056edde00f7d0af13ed0d406412182d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3edaea0e8056edde00f7d0af13ed0d406412182d", + "reference": "3edaea0e8056edde00f7d0af13ed0d406412182d", + "shasum": "" + }, + "require": { + "illuminate/support": "~5.0.17|5.1.*", + "maximebf/debugbar": "~1.10.2", + "php": ">=5.4.0", + "symfony/finder": "~2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "time": "2015-06-07 07:19:29" + }, + { + "name": "barryvdh/laravel-ide-helper", + "version": "v2.0.6", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-ide-helper.git", + "reference": "037386153630a7515a1542f29410d8c267651689" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/037386153630a7515a1542f29410d8c267651689", + "reference": "037386153630a7515a1542f29410d8c267651689", + "shasum": "" + }, + "require": { + "illuminate/console": "5.0.x|5.1.x", + "illuminate/filesystem": "5.0.x|5.1.x", + "illuminate/support": "5.0.x|5.1.x", + "php": ">=5.4.0", + "phpdocumentor/reflection-docblock": "2.0.4", + "symfony/class-loader": "~2.3" + }, + "require-dev": { + "doctrine/dbal": "~2.3" + }, + "suggest": { + "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\LaravelIdeHelper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", + "keywords": [ + "autocomplete", + "codeintel", + "helper", + "ide", + "laravel", + "netbeans", + "phpdoc", + "phpstorm", + "sublime" + ], + "time": "2015-06-25 08:58:59" + }, + { + "name": "codeception/codeception", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Codeception.git", + "reference": "b5af3aac061ffaeb65ed023534b3c50558e90d07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b5af3aac061ffaeb65ed023534b3c50558e90d07", + "reference": "b5af3aac061ffaeb65ed023534b3c50558e90d07", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "facebook/webdriver": "~0.4|~0.5", + "guzzlehttp/guzzle": ">=4.0|<7.0", + "guzzlehttp/psr7": "~1.0", + "php": ">=5.4.0", + "phpunit/phpunit": "~4.7.0", + "symfony/browser-kit": "~2.4", + "symfony/console": "~2.4", + "symfony/css-selector": "~2.4", + "symfony/dom-crawler": "~2.4,!=2.4.5", + "symfony/event-dispatcher": "~2.4", + "symfony/finder": "~2.4", + "symfony/yaml": "~2.4" + }, + "require-dev": { + "codeception/specify": "~0.3", + "facebook/php-sdk-v4": "~4.0", + "flow/jsonpath": "~0.2", + "monolog/monolog": "~1.8", + "pda/pheanstalk": "~2.0", + "videlalvaro/php-amqplib": "~2.4" + }, + "suggest": { + "codeception/phpbuiltinserver": "Extension to start and stop PHP built-in web server for your tests", + "codeception/specify": "BDD-style code blocks", + "codeception/verify": "BDD-style assertions", + "monolog/monolog": "Log test steps", + "phpseclib/phpseclib": "Extension required to use the SFTP option in the FTP Module." + }, + "bin": [ + "codecept" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Codeception\\": "src\\Codeception", + "Codeception\\Extension\\": "ext" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Bodnarchuk", + "email": "davert@mail.ua", + "homepage": "http://codegyre.com" + } + ], + "description": "BDD-style testing framework", + "homepage": "http://codeception.com/", + "keywords": [ + "BDD", + "TDD", + "acceptance testing", + "functional testing", + "unit testing" + ], + "time": "2015-06-30 03:38:01" + }, + { + "name": "codeclimate/php-test-reporter", + "version": "v0.1.2", + "source": { + "type": "git", + "url": "https://github.com/codeclimate/php-test-reporter.git", + "reference": "8ed24ff30f3663ecf40f1c12d6c97eb56c69e646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/8ed24ff30f3663ecf40f1c12d6c97eb56c69e646", + "reference": "8ed24ff30f3663ecf40f1c12d6c97eb56c69e646", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3", + "satooshi/php-coveralls": "0.6.*", + "symfony/console": ">=2.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*@stable" + }, + "bin": [ + "composer/bin/test-reporter" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "CodeClimate\\Component": "src/", + "CodeClimate\\Bundle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Code Climate", + "email": "hello@codeclimate.com", + "homepage": "https://codeclimate.com" + } + ], + "description": "PHP client for reporting test coverage to Code Climate", + "homepage": "https://github.com/codeclimate/php-test-reporter", + "keywords": [ + "codeclimate", + "coverage" + ], + "time": "2014-07-23 13:42:41" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "facebook/webdriver", + "version": "v0.6.0", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "2c5b305ea91b00ebbc433ad1663b7f16c1b31ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/2c5b305ea91b00ebbc433ad1663b7f16c1b31ec5", + "reference": "2c5b305ea91b00ebbc433ad1663b7f16c1b31ec5", + "shasum": "" + }, + "require": { + "php": ">=5.3.19" + }, + "require-dev": { + "phpdocumentor/phpdocumentor": "2.*", + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A php client for WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2015-02-09 19:39:34" + }, + { + "name": "fzaninotto/faker", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "010c7efedd88bf31141a02719f51fb44c732d5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/010c7efedd88bf31141a02719f51fb44c732d5a0", + "reference": "010c7efedd88bf31141a02719f51fb44c732d5a0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "extra": { + "branch-alias": [] + }, + "autoload": { + "psr-0": { + "Faker": "src/", + "Faker\\PHPUnit": "test/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "time": "2014-06-04 14:43:02" + }, + { + "name": "guzzle/guzzle", + "version": "v3.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": "~2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "~1.3", + "monolog/monolog": "~1.0", + "phpunit/phpunit": "3.7.*", + "psr/log": "~1.0", + "symfony/class-loader": "~2.1", + "zendframework/zend-cache": "2.*,<2.3", + "zendframework/zend-log": "2.*,<2.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.9-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2015-03-18 18:23:50" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f992b7b487a816c957d317442bed4966409873e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f992b7b487a816c957d317442bed4966409873e0", + "reference": "f992b7b487a816c957d317442bed4966409873e0", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0.0", + "guzzlehttp/psr7": "^1.0.0", + "php": ">=5.5.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2015-05-27 16:57:51" + }, + { + "name": "guzzlehttp/promises", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "2ee5bc7f1a92efecc90da7f6711a53a7be26b5b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/2ee5bc7f1a92efecc90da7f6711a53a7be26b5b7", + "reference": "2ee5bc7f1a92efecc90da7f6711a53a7be26b5b7", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2015-06-24 16:16:25" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "af0e1758de355eb113917ad79c3c0e3604bce4bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/af0e1758de355eb113917ad79c3c0e3604bce4bd", + "reference": "af0e1758de355eb113917ad79c3c0e3604bce4bd", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "PSR-7 message implementation", + "keywords": [ + "http", + "message", + "stream", + "uri" + ], + "time": "2015-06-24 19:55:15" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b37020aa976fa52d3de9aa904aa2522dc518f79c", + "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "1.3.3", + "satooshi/php-coveralls": "dev-master" + }, + "type": "library", + "autoload": { + "classmap": [ + "hamcrest" + ], + "files": [ + "hamcrest/Hamcrest.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "time": "2015-05-11 14:41:42" + }, + { + "name": "league/factory-muffin", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/factory-muffin.git", + "reference": "91f0adcdac6b5f7bf2277ac2c90f94352afe65de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/factory-muffin/zipball/91f0adcdac6b5f7bf2277ac2c90f94352afe65de", + "reference": "91f0adcdac6b5f7bf2277ac2c90f94352afe65de", + "shasum": "" + }, + "require": { + "fzaninotto/faker": "1.4.*", + "php": ">=5.3.3" + }, + "replace": { + "zizaco/factory-muff": "self.version" + }, + "require-dev": { + "illuminate/database": "~4.1", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "illuminate/database": "Factory Muffin works well with eloquent models." + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\FactoryMuffin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@mineuk.com" + }, + { + "name": "Zizaco Zizuini", + "email": "zizaco@gmail.com" + }, + { + "name": "Scott Robertson", + "email": "scottymeuk@gmail.com" + } + ], + "description": "The goal of this package is to enable the rapid creation of objects for the purpose of testing.", + "homepage": "http://factory-muffin.thephpleague.com/", + "keywords": [ + "factory", + "laravel", + "testing" + ], + "time": "2014-09-18 18:29:06" + }, + { + "name": "maximebf/debugbar", + "version": "v1.10.4", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "7b2006e6e095126b5a061ec33fca3d90ea8a8219" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/7b2006e6e095126b5a061ec33fca3d90ea8a8219", + "reference": "7b2006e6e095126b5a061ec33fca3d90ea8a8219", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0", + "symfony/var-dumper": "~2.6" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-0": { + "DebugBar": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug" + ], + "time": "2015-02-05 07:51:20" + }, + { + "name": "mockery/mockery", + "version": "0.9.4", + "source": { + "type": "git", + "url": "https://github.com/padraic/mockery.git", + "reference": "70bba85e4aabc9449626651f48b9018ede04f86b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/padraic/mockery/zipball/70bba85e4aabc9449626651f48b9018ede04f86b", + "reference": "70bba85e4aabc9449626651f48b9018ede04f86b", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "~1.1", + "lib-pcre": ">=7.0", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.9.x-dev" + } + }, + "autoload": { + "psr-0": { + "Mockery": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "http://davedevelopment.co.uk" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", + "homepage": "http://github.com/padraic/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "time": "2015-04-02 19:54:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03 12:10:50" + }, + { + "name": "phpspec/php-diff", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton", + "role": "Original developer" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "time": "2013-11-01 13:02:21" + }, + { + "name": "phpspec/phpspec", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/phpspec.git", + "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/e9a40577323e67f1de2e214abf32976a0352d8f8", + "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.1", + "php": ">=5.3.3", + "phpspec/php-diff": "~1.0.0", + "phpspec/prophecy": "~1.4", + "sebastian/exporter": "~1.0", + "symfony/console": "~2.3", + "symfony/event-dispatcher": "~2.1", + "symfony/finder": "~2.1", + "symfony/process": "~2.1", + "symfony/yaml": "~2.1" + }, + "require-dev": { + "behat/behat": "^3.0.11", + "bossa/phpspec2-expect": "~1.0", + "phpunit/phpunit": "~4.4", + "symfony/filesystem": "~2.1", + "symfony/process": "~2.1" + }, + "suggest": { + "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" + }, + "bin": [ + "bin/phpspec" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "PhpSpec": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + } + ], + "description": "Specification-oriented BDD framework for PHP 5.3+", + "homepage": "http://phpspec.net/", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification", + "testing", + "tests" + ], + "time": "2015-05-30 15:21:40" + }, + { + "name": "phpspec/prophecy", + "version": "v1.4.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", + "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "phpdocumentor/reflection-docblock": "~2.0", + "sebastian/comparator": "~1.1" + }, + "require-dev": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2015-04-27 22:15:08" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.1.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "07e27765596d72c378a6103e80da5d84e802f1e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/07e27765596d72c378a6103e80da5d84e802f1e4", + "reference": "07e27765596d72c378a6103e80da5d84e802f1e4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "~1.0", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-06-30 06:52:35" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a923bb15680d0089e2316f7a4af8f437046e96bb", + "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-04-02 05:19:05" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "83fe1bdc5d47658b727595c14da140da92b3d66d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/83fe1bdc5d47658b727595c14da140da92b3d66d", + "reference": "83fe1bdc5d47658b727595c14da140da92b3d66d", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2015-06-13 07:35:30" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/7a9b0969488c3c54fd62b4d504b3ec758fd005d9", + "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-06-19 03:43:16" + }, + { + "name": "phpunit/phpunit", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0ebabb4cda7d066be8391dfdbaf57fe70ac9a99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0ebabb4cda7d066be8391dfdbaf57fe70ac9a99b", + "reference": "0ebabb4cda7d066be8391dfdbaf57fe70ac9a99b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "~1.3,>=1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": ">=1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.2", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2015-06-30 06:53:57" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "92408bb1968a81b3217a6fdf6c1a198da83caa35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/92408bb1968a81b3217a6fdf6c1a198da83caa35", + "reference": "92408bb1968a81b3217a6fdf6c1a198da83caa35", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "~1.0,>=1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-06-11 15:55:48" + }, + { + "name": "psr/http-message", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2015-05-04 20:22:00" + }, + { + "name": "satooshi/php-coveralls", + "version": "v0.6.1", + "source": { + "type": "git", + "url": "https://github.com/satooshi/php-coveralls.git", + "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/satooshi/php-coveralls/zipball/dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", + "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-simplexml": "*", + "guzzle/guzzle": ">=3.0", + "php": ">=5.3", + "psr/log": "1.0.0", + "symfony/config": ">=2.0", + "symfony/console": ">=2.0", + "symfony/stopwatch": ">=2.2", + "symfony/yaml": ">=2.0" + }, + "require-dev": { + "apigen/apigen": "2.8.*@stable", + "pdepend/pdepend": "dev-master", + "phpmd/phpmd": "dev-master", + "phpunit/php-invoker": ">=1.1.0,<1.2.0", + "phpunit/phpunit": "3.7.*@stable", + "sebastian/finder-facade": "dev-master", + "sebastian/phpcpd": "1.4.*@stable", + "squizlabs/php_codesniffer": "1.4.*@stable", + "theseer/fdomdocument": "dev-master" + }, + "bin": [ + "composer/bin/coveralls" + ], + "type": "library", + "autoload": { + "psr-0": { + "Contrib\\Component": "src/", + "Contrib\\Bundle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/satooshi/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "time": "2013-05-04 08:07:33" + }, + { + "name": "sebastian/comparator", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "1dd8869519a225f7f2b9eb663e225298fade819e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dd8869519a225f7f2b9eb663e225298fade819e", + "reference": "1dd8869519a225f7f2b9eb663e225298fade819e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-01-29 16:28:08" + }, + { + "name": "sebastian/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "http://www.github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-02-22 15:13:53" + }, + { + "name": "sebastian/environment", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5a8c7d31914337b69923db26c4221b81ff5a196e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5a8c7d31914337b69923db26c4221b81ff5a196e", + "reference": "5a8c7d31914337b69923db26c4221b81ff5a196e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2015-01-01 10:01:08" + }, + { + "name": "sebastian/exporter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "84839970d05254c73cde183a721c7af13aede943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/84839970d05254c73cde183a721c7af13aede943", + "reference": "84839970d05254c73cde183a721c7af13aede943", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2015-01-27 07:23:06" + }, + { + "name": "sebastian/global-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2014-10-06 09:23:50" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "3989662bbb30a29d20d9faa04a846af79b276252" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/3989662bbb30a29d20d9faa04a846af79b276252", + "reference": "3989662bbb30a29d20d9faa04a846af79b276252", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-01-24 09:48:32" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "symfony/browser-kit", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/BrowserKit.git", + "reference": "d0a144a1a96d5dc90bed2814b2096a1322761672" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/BrowserKit/zipball/d0a144a1a96d5dc90bed2814b2096a1322761672", + "reference": "d0a144a1a96d5dc90bed2814b2096a1322761672", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/dom-crawler": "~2.0,>=2.0.5" + }, + "require-dev": { + "symfony/css-selector": "~2.0,>=2.0.5", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.0,>=2.0.5" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2015-06-04 20:11:48" + }, + { + "name": "symfony/class-loader", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/ClassLoader.git", + "reference": "84843730de48ca0feba91004a03c7c952f8ea1da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/84843730de48ca0feba91004a03c7c952f8ea1da", + "reference": "84843730de48ca0feba91004a03c7c952f8ea1da", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/finder": "~2.0,>=2.0.5", + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2015-06-08 09:37:21" + }, + { + "name": "symfony/config", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/Config.git", + "reference": "58ded81f1f582a87c528ef3dae9a859f78b5f374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Config/zipball/58ded81f1f582a87c528ef3dae9a859f78b5f374", + "reference": "58ded81f1f582a87c528ef3dae9a859f78b5f374", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/filesystem": "~2.3" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2015-06-11 14:06:56" + }, + { + "name": "symfony/filesystem", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/Filesystem.git", + "reference": "a0d43eb3e17d4f4c6990289805a488a0482a07f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Filesystem/zipball/a0d43eb3e17d4f4c6990289805a488a0482a07f3", + "reference": "a0d43eb3e17d4f4c6990289805a488a0482a07f3", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2015-06-08 09:37:21" + }, + { + "name": "symfony/stopwatch", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/Stopwatch.git", + "reference": "c653f1985f6c2b7dbffd04d48b9c0a96aaef814b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/c653f1985f6c2b7dbffd04d48b9c0a96aaef814b", + "reference": "c653f1985f6c2b7dbffd04d48b9c0a96aaef814b", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Stopwatch Component", + "homepage": "https://symfony.com", + "time": "2015-06-04 20:11:48" + }, + { + "name": "symfony/yaml", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "9808e75c609a14f6db02f70fccf4ca4aab53c160" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/9808e75c609a14f6db02f70fccf4ca4aab53c160", + "reference": "9808e75c609a14f6db02f70fccf4ca4aab53c160", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2015-06-10 15:30:22" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": { diff --git a/config/twigbridge.php b/config/twigbridge.php index 04d882d65e..8d5621c253 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -145,12 +145,12 @@ return [ 'ExpandedForm' => [ 'is_safe' => [ 'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location', - 'multiRadio' + 'multiRadio','file' ] ], 'Form' => [ 'is_safe' => [ - 'input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea' + 'input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea','file' ] ], ], diff --git a/resources/lang/en/firefly.php b/resources/lang/en/firefly.php index f7d834653a..d58d2cc632 100644 --- a/resources/lang/en/firefly.php +++ b/resources/lang/en/firefly.php @@ -21,6 +21,13 @@ return [ // csv import: 'csv_import' => 'Import CSV file', + 'csv' => 'CSV', + 'csv_index_text' => 'Here be explanation.', + 'csv_upload_form' => 'Upload form', + 'upload_csv_file' => 'Upload CSV file', + 'csv_header_help' => 'Check this when bla bla', + 'csv_row' => 'row', + 'upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', // create new stuff: 'create_new_withdrawal' => 'Create new withdrawal', diff --git a/resources/lang/en/form.php b/resources/lang/en/form.php index 4fbb9a44f3..8b9530ec10 100644 --- a/resources/lang/en/form.php +++ b/resources/lang/en/form.php @@ -45,6 +45,8 @@ return [ 'under' => 'Under', 'symbol' => 'Symbol', 'code' => 'Code', + 'csv' => 'CSV file', + 'has_headers' => 'Headers', 'store_new_withdrawal' => 'Store new withdrawal', 'store_new_deposit' => 'Store new deposit', diff --git a/resources/lang/nl/firefly.php b/resources/lang/nl/firefly.php index 6c03404832..aaa2a9a90e 100644 --- a/resources/lang/nl/firefly.php +++ b/resources/lang/nl/firefly.php @@ -20,7 +20,14 @@ return [ 'search_results_for' => 'Zoekresultaten voor ":query"', // csv import: - 'csv_import' => 'Import CSV file', + 'csv_import' => 'Importeer CSV-bestand', + 'csv' => 'CSV', + 'csv_index_text' => 'Hier komt uitleg.', + 'csv_upload_form' => 'Upload formulier', + 'upload_csv_file' => 'Upload CSV-bestand', + 'csv_header_help' => 'Check dit als bla bla', + 'csv_row' => 'rij', + 'upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', // create new stuff: 'create_new_withdrawal' => 'Nieuwe uitgave', diff --git a/resources/lang/nl/form.php b/resources/lang/nl/form.php index 5ab0cd308b..837ec1d3a8 100644 --- a/resources/lang/nl/form.php +++ b/resources/lang/nl/form.php @@ -45,6 +45,8 @@ return [ 'under' => 'Onder', 'symbol' => 'Symbool', 'code' => 'Code', + 'csv' => 'CSV-bestand', + 'has_headers' => 'Eerste rij zijn kolomnamen', 'store_new_withdrawal' => 'Nieuwe uitgave opslaan', 'store_new_deposit' => 'Nieuwe inkomsten opslaan', diff --git a/resources/twig/csv/index.twig b/resources/twig/csv/index.twig index 5dd98e4135..023d0b2fe0 100644 --- a/resources/twig/csv/index.twig +++ b/resources/twig/csv/index.twig @@ -7,6 +7,81 @@ {% block content %} - Bla bla. +
    +
    +
    +
    +

    {{ 'csv'|_ }}

    + + +
    + +
    + +
    +
    + {{ 'csv_index_text'|_ }} +
    +
    + +
    +
    +
    +
    +
    +
    +

    {{ 'csv_upload_form'|_ }}

    + + +
    + +
    + +
    +
    + +
    + + + {{ ExpandedForm.checkbox('has_headers',false,null,{helpText: 'csv_header_help'|_}) }} + + {{ ExpandedForm.file('csv') }} + + {% if uploadPossible %} +
    +
    +   +
    + +
    + +
    +
    + {% else %} +
    +
    +   +
    + +
    +
    {{ path }}
    +

    + {{ 'upload_not_writeable'|_ }} +

    +
    +
    + {% endif %} + + + +
    + + +
    +
    +
    +
    {% endblock %} diff --git a/resources/twig/form/checkbox.twig b/resources/twig/form/checkbox.twig index bea165b856..b2bc258cd0 100644 --- a/resources/twig/form/checkbox.twig +++ b/resources/twig/form/checkbox.twig @@ -7,6 +7,7 @@ {{ Form.checkbox(name, value, options.checked, options) }} + {% include 'form/help.twig' %} {% include 'form/feedback.twig' %} diff --git a/resources/twig/form/file.twig b/resources/twig/form/file.twig new file mode 100644 index 0000000000..0596ef7dbd --- /dev/null +++ b/resources/twig/form/file.twig @@ -0,0 +1,9 @@ +
    + + +
    + {{ Form.file(name, options) }} + {% include 'form/help.twig' %} + {% include 'form/feedback.twig' %} +
    +
    diff --git a/storage/upload/.gitignore b/storage/upload/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/storage/upload/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From 26c9b2c3531b6a69f34f485cd46d2803020b2921 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 3 Jul 2015 12:22:20 +0200 Subject: [PATCH 03/10] Initial process form. --- app/Http/Controllers/CsvController.php | 49 ++++++++++++------ resources/twig/csv/upload.twig | 71 ++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 resources/twig/csv/upload.twig diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php index 3f2504a7a4..4e40bbeed7 100644 --- a/app/Http/Controllers/CsvController.php +++ b/app/Http/Controllers/CsvController.php @@ -8,6 +8,8 @@ namespace FireflyIII\Http\Controllers; +use Auth; +use Crypt; use Illuminate\Http\Request; use Input; use League\Csv\Reader; @@ -42,7 +44,7 @@ class CsvController extends Controller $subTitle = trans('firefly.csv_import'); // can actually upload? - $uploadPossible = !is_writable(storage_path('upload')); + $uploadPossible = is_writable(storage_path('upload')); $path = storage_path('upload'); @@ -54,6 +56,21 @@ class CsvController extends Controller */ public function upload(Request $request) { + // possible column roles: + $roles = [ + '(ignore this column)', + 'Asset account name', + 'Expense or revenue account name', + 'Amount', + 'Date', + 'Currency', + 'Description', + 'Category', + 'Budget', + + ]; + + if (!$request->hasFile('csv')) { Session::flash('warning', 'No file uploaded.'); @@ -64,29 +81,31 @@ class CsvController extends Controller $reader = Reader::createFromPath($request->file('csv')->getRealPath()); $data = $reader->query(); $data->next(); // go to first row: - if ($hasHeaders) { - // first row = headers. + $count = count($data->current()); + $headers = []; + for ($i = 1; $i <= $count; $i++) { + $headers[] = trans('firefly.csv_row') . ' #' . $i; + } + if ($hasHeaders) { $headers = $data->current(); - } else { - $count = count($data->current()); - $headers = []; - for ($i = 1; $i <= $count; $i++) { - $headers[] = trans('firefly.csv_row') . ' #' . $i; - } } // example data is always the second row: $data->next(); $example = $data->current(); + // store file somewhere temporary (encrypted)? + $time = str_replace(' ', '-', microtime()); + $fileName = 'csv-upload-' . Auth::user()->id . '-' . $time . '.csv.encrypted'; + $fullPath = storage_path('upload') . DIRECTORY_SEPARATOR . $fileName; + $content = file_get_contents($request->file('csv')->getRealPath()); + $content = Crypt::encrypt($content); + file_put_contents($fullPath, $content); + Session::put('latestCSVUpload', $fullPath); - var_dump($headers); - var_dump($example); + $subTitle = trans('firefly.csv_process'); - // store file somewhere temporary? - - - exit; + return view('csv.upload', compact('headers', 'example', 'roles', 'subTitle')); } } \ No newline at end of file diff --git a/resources/twig/csv/upload.twig b/resources/twig/csv/upload.twig new file mode 100644 index 0000000000..4b0b34cd07 --- /dev/null +++ b/resources/twig/csv/upload.twig @@ -0,0 +1,71 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +
    +
    +
    +
    +

    {{ 'csv_process'|_ }}

    + + +
    + +
    + +
    +
    + {{ 'csv_process_text'|_ }} +
    +
    + +
    +
    +
    +
    +
    +
    +

    {{ 'csv_process_form'|_ }}

    + + +
    + +
    + +
    +
    + + + + + + + + + {% for index,header in headers %} + + + + + + + + {% endfor %} +
    {{ 'cvs_column_name'|_ }}{{ 'cvs_column_example'|_ }}{{ 'cvs_column_role'|_ }}{{ 'do_map_value'|_ }}
    {{ header }}{{ example[index] }} + {{ Form.select(('role_'~index), roles) }} + + {{ Form.checkbox(('map_'~index),false) }} +
    + + +
    +
    +
    +
    + +{% endblock %} From d2c018f7daa202885af37bbff6272b2047111632 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 4 Jul 2015 07:53:37 +0200 Subject: [PATCH 04/10] First (almost) functional CSV importer. --- app/Http/Controllers/CsvController.php | 468 ++++++++++++++++-- app/Http/routes.php | 5 + config/csv.php | 113 +++++ database/seeds/TestDataSeeder.php | 8 +- resources/lang/en/firefly.php | 5 +- resources/lang/en/form.php | 1 + resources/lang/nl/form.php | 1 + .../csv/{upload.twig => column-roles.twig} | 18 +- resources/twig/csv/index.twig | 3 + resources/twig/csv/map.twig | 83 ++++ resources/twig/form/help.twig | 2 +- resources/twig/form/text.twig | 1 + 12 files changed, 664 insertions(+), 44 deletions(-) create mode 100644 config/csv.php rename resources/twig/csv/{upload.twig => column-roles.twig} (73%) create mode 100644 resources/twig/csv/map.twig diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php index 4e40bbeed7..0302be11ff 100644 --- a/app/Http/Controllers/CsvController.php +++ b/app/Http/Controllers/CsvController.php @@ -9,10 +9,16 @@ namespace FireflyIII\Http\Controllers; use Auth; +use Carbon\Carbon; +use Config; use Crypt; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionCurrency; use Illuminate\Http\Request; use Input; use League\Csv\Reader; +use Log; use Redirect; use Session; use View; @@ -37,75 +43,465 @@ class CsvController extends Controller } /** + * Define column roles and mapping. + * + * + * STEP THREE + * + * @return View + */ + public function columnRoles() + { + $fields = ['csv-file', 'csv-date-format', 'csv-has-headers']; + foreach ($fields as $field) { + if (!Session::has($field)) { + Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + + return Redirect::route('csv.index'); + } + } + + $subTitle = trans('firefly.csv_process'); + $fullPath = Session::get('csv-file'); + $hasHeaders = Session::get('csv-has-headers'); + $content = file_get_contents($fullPath); + $contentDecrypted = Crypt::decrypt($content); + $reader = Reader::createFromString($contentDecrypted); + + + Log::debug('Get uploaded content from ' . $fullPath); + Log::debug('Strlen of original content is ' . strlen($contentDecrypted)); + Log::debug('MD5 of original content is ' . md5($contentDecrypted)); + + $firstRow = $reader->fetchOne(); + + $count = count($firstRow); + $headers = []; + for ($i = 1; $i <= $count; $i++) { + $headers[] = trans('firefly.csv_row') . ' #' . $i; + } + if ($hasHeaders) { + $headers = $firstRow; + } + + // example data is always the second row: + $example = $reader->fetchOne(); + $roles = []; + foreach (Config::get('csv.roles') as $name => $role) { + $roles[$name] = $role['name']; + } + ksort($roles); + + + return view('csv.column-roles', compact('roles', 'headers', 'example', 'subTitle')); + } + + /** + * This method shows the initial upload form. + * + * STEP ONE + * * @return View */ public function index() { $subTitle = trans('firefly.csv_import'); + Session::forget('csv-date-format'); + Session::forget('csv-has-headers'); + Session::forget('csv-file'); + + // can actually upload? $uploadPossible = is_writable(storage_path('upload')); $path = storage_path('upload'); - return view('csv.index', compact('subTitle', 'uploadPossible', 'path')); } /** + * Parse the file. * + * STEP FOUR + * + * @return \Illuminate\Http\RedirectResponse + */ + public function initialParse() + { + $fields = ['csv-file', 'csv-date-format', 'csv-has-headers']; + foreach ($fields as $field) { + if (!Session::has($field)) { + Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + + return Redirect::route('csv.index'); + } + } + $configRoles = Config::get('csv.roles'); + $roles = []; + + /* + * Store all rows for each column: + */ + if (is_array(Input::get('role'))) { + $roles = []; + foreach (Input::get('role') as $index => $role) { + if ($role != '_ignore') { + $roles[$index] = $role; + } + + } + } + /* + * Go back when no roles defined: + */ + if (count($roles) === 0) { + Session::flash('warning', 'Please select some roles.'); + + return Redirect::route('csv.column-roles'); + } + Session::put('csv-roles', $roles); + + /* + * Show user map thing: + */ + if (is_array(Input::get('map'))) { + $maps = []; + foreach (Input::get('map') as $index => $map) { + $name = $roles[$index]; + if ($configRoles[$name]['mappable']) { + $maps[$index] = $name; + } + } + // redirect to map routine. + Session::put('csv-map', $maps); + + return Redirect::route('csv.map'); + } + + var_dump($roles); + var_dump($_POST); + exit; + + } + + /** + * + * Map first if necessary, + * + * STEP FIVE. + * + * @return \Illuminate\Http\RedirectResponse|View + * @throws FireflyException + */ + public function map() + { + + /* + * Make sure all fields we need are accounted for. + */ + $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles']; + foreach ($fields as $field) { + if (!Session::has($field)) { + Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + + return Redirect::route('csv.index'); + } + } + + /* + * The $map array contains all columns + * the user wishes to map on to data already in the system. + */ + $map = Session::get('csv-map'); + + /* + * The "options" array contains all options the user has + * per column, where the key represents the column. + * + * For each key there is an array which in turn represents + * all the options available: grouped by ID. + */ + $options = []; + + /* + * Loop each field the user whishes to map. + */ + foreach ($map as $index => $columnRole) { + + /* + * Depending on the column role, get the relevant data from the database. + * This needs some work to be optimal. + */ + switch ($columnRole) { + default: + throw new FireflyException('Cannot map field of type "' . $columnRole . '".'); + break; + case 'account-iban': + // get content for this column. + $content = Auth::user()->accounts()->where('account_type_id', 3)->get(['accounts.*']); + $list = []; + // make user friendly list: + + foreach ($content as $account) { + $list[$account->id] = $account->name; + //if(!is_null($account->iban)) { + //$list[$account->id] .= ' ('.$account->iban.')'; + //} + } + $options[$index] = $list; + break; + case 'currency-code': + $currencies = TransactionCurrency::get(); + $list = []; + foreach ($currencies as $currency) { + $list[$currency->id] = $currency->name . ' (' . $currency->code . ')'; + } + $options[$index] = $list; + break; + case 'opposing-name': + // get content for this column. + $content = Auth::user()->accounts()->whereIn('account_type_id', [4, 5])->get(['accounts.*']); + $list = []; + // make user friendly list: + + foreach ($content as $account) { + $list[$account->id] = $account->name . ' (' . $account->accountType->type . ')'; + } + $options[$index] = $list; + break; + + } + + } + + + /* + * After these values are prepped, read the actual CSV file + */ + $content = file_get_contents(Session::get('csv-file')); + $hasHeaders = Session::get('csv-has-headers'); + $reader = Reader::createFromString(Crypt::decrypt($content)); + $values = []; + + /* + * Loop over the CSV and collect mappable data: + */ + foreach ($reader as $index => $row) { + if (($hasHeaders && $index > 1) || !$hasHeaders) { + // collect all map values + foreach ($map as $column => $irrelevant) { + // check if $irrelevant is mappable! + $values[$column][] = $row[$column]; + } + } + } + foreach ($values as $column => $found) { + $values[$column] = array_unique($found); + } + + return view('csv.map', compact('map', 'options', 'values')); + } + + /** + * Finally actually process the CSV file. + * + * STEP SEVEN + */ + public function process() + { + /* + * Make sure all fields we need are accounted for. + */ + $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles', 'csv-mapped']; + foreach ($fields as $field) { + if (!Session::has($field)) { + Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + + return Redirect::route('csv.index'); + } + } + + // loop the original file again: + $content = file_get_contents(Session::get('csv-file')); + $hasHeaders = Session::get('csv-has-headers'); + $reader = Reader::createFromString(Crypt::decrypt($content)); + + // dump stuff + $dateFormat = Session::get('csv-date-format'); + $roles = Session::get('csv-roles'); + $mapped = Session::get('csv-mapped'); + + var_dump($roles); + var_dump(Session::get('csv-mapped')); + + + /* + * Loop over the CSV and collect mappable data: + */ + foreach ($reader as $index => $row) { + if (($hasHeaders && $index > 1) || !$hasHeaders) { + // this is the data we need to store the new transaction: + $amount = 0; + $amountModifier = 1; + $description = ''; + $assetAccount = null; + $opposingAccount = null; + $currency = null; + $date = null; + + foreach ($row as $index => $value) { + if (isset($roles[$index])) { + switch ($roles[$index]) { + default: + throw new FireflyException('Cannot process role "' . $roles[$index] . '"'); + break; + case 'account-iban': + // find ID in "mapped" (if present). + if (isset($mapped[$index])) { + $searchID = $mapped[$index][$value]; + $assetAccount = Account::find($searchID); + } else { + // create account + } + break; + case 'opposing-name': + // don't know yet if its going to be a + // revenue or expense account. + $opposingAccount = $value; + break; + case 'currency-code': + // find ID in "mapped" (if present). + if (isset($mapped[$index])) { + $searchValue = $mapped[$index][$value]; + $currency = TransactionCurrency::whereCode($searchValue); + } else { + // create account + } + break; + case 'date-transaction': + // unmappable: + $date = Carbon::createFromFormat($dateFormat, $value); + + break; + case 'rabo-debet-credet': + if ($value == 'D') { + $amountModifier = -1; + } + break; + case 'amount': + $amount = $value; + break; + case 'description': + $description .= ' ' . $value; + break; + case 'sepa-ct-id': + $description .= ' ' . $value; + break; + + } + } + } + // do something with all this data: + + + // do something. + var_dump($row); + + } + } + + + } + + /** + * Store the mapping the user has made. This is + * + * STEP SIX + */ + public function saveMapping() + { + /* + * Make sure all fields we need are accounted for. + */ + $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles']; + foreach ($fields as $field) { + if (!Session::has($field)) { + Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + + return Redirect::route('csv.index'); + } + } + // save mapping to session. + $mapped = []; + if (!is_array(Input::get('mapping'))) { + Session::flash('warning', 'Invalid mapping.'); + + return Redirect::route('csv.map'); + } + + foreach (Input::get('mapping') as $index => $data) { + $mapped[$index] = []; + foreach ($data as $value => $mapping) { + $mapped[$index][$value] = $mapping; + } + } + Session::put('csv-mapped', $mapped); + + // proceed to process. + return Redirect::route('csv.process'); + + } + + /** + * + * This method processes the file, puts it away somewhere safe + * and sends you onwards. + * + * STEP TWO + * + * @param Request $request + * + * @return \Illuminate\Http\RedirectResponse */ public function upload(Request $request) { - // possible column roles: - $roles = [ - '(ignore this column)', - 'Asset account name', - 'Expense or revenue account name', - 'Amount', - 'Date', - 'Currency', - 'Description', - 'Category', - 'Budget', - - ]; - - if (!$request->hasFile('csv')) { Session::flash('warning', 'No file uploaded.'); return Redirect::route('csv.index'); } + + + $dateFormat = Input::get('date_format'); $hasHeaders = intval(Input::get('has_headers')) === 1; - $reader = Reader::createFromPath($request->file('csv')->getRealPath()); - $data = $reader->query(); - $data->next(); // go to first row: - - $count = count($data->current()); - $headers = []; - for ($i = 1; $i <= $count; $i++) { - $headers[] = trans('firefly.csv_row') . ' #' . $i; - } - if ($hasHeaders) { - $headers = $data->current(); - } - - // example data is always the second row: - $data->next(); - $example = $data->current(); // store file somewhere temporary (encrypted)? $time = str_replace(' ', '-', microtime()); $fileName = 'csv-upload-' . Auth::user()->id . '-' . $time . '.csv.encrypted'; $fullPath = storage_path('upload') . DIRECTORY_SEPARATOR . $fileName; $content = file_get_contents($request->file('csv')->getRealPath()); - $content = Crypt::encrypt($content); + + Log::debug('Stored uploaded content in ' . $fullPath); + Log::debug('Strlen of uploaded content is ' . strlen($content)); + Log::debug('MD5 of uploaded content is ' . md5($content)); + + $content = Crypt::encrypt($content); file_put_contents($fullPath, $content); - Session::put('latestCSVUpload', $fullPath); - $subTitle = trans('firefly.csv_process'); - return view('csv.upload', compact('headers', 'example', 'roles', 'subTitle')); + Session::put('csv-date-format', $dateFormat); + Session::put('csv-has-headers', $hasHeaders); + Session::put('csv-file', $fullPath); + + return Redirect::route('csv.column-roles'); + + + // + // + // + + // + // return view('csv.upload', compact('headers', 'example', 'roles', 'subTitle')); } } \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index e0447eba0c..55b967497e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -225,6 +225,11 @@ Route::group( */ Route::get('/csv', ['uses' => 'CsvController@index', 'as' => 'csv.index']); Route::post('/csv/upload', ['uses' => 'CsvController@upload', 'as' => 'csv.upload']); + Route::get('/csv/column_roles', ['uses' => 'CsvController@columnRoles', 'as' => 'csv.column-roles']); + Route::post('/csv/initial_parse', ['uses' => 'CsvController@initialParse', 'as' => 'csv.initial_parse']); + Route::get('/csv/map', ['uses' => 'CsvController@map', 'as' => 'csv.map']); + Route::post('/csv/save_mapping', ['uses' => 'CsvController@saveMapping', 'as' => 'csv.save_mapping']); + Route::get('/csv/process', ['uses' => 'CsvController@process', 'as' => 'csv.process']); /** * Currency Controller diff --git a/config/csv.php b/config/csv.php new file mode 100644 index 0000000000..6a198dc8c8 --- /dev/null +++ b/config/csv.php @@ -0,0 +1,113 @@ + [ + '_ignore' => [ + 'name' => '(ignore this column)', + 'mappable' => false, + ], + 'bill-id' => [ + 'name' => 'Bill ID (matching Firefly)', + 'mappable' => true, + ], + 'bill-name' => [ + 'name' => 'Bill name', + 'mappable' => true, + ], + 'currency-id' => [ + 'name' => 'Currency ID (matching Firefly)', + 'mappable' => true, + ], + 'currency-name' => [ + 'name' => 'Currency name (matching Firefly)', + 'mappable' => true, + ], + 'currency-code' => [ + 'name' => 'Currency code (ISO 4217)', + 'mappable' => true, + ], + 'currency-symbol' => [ + 'name' => 'Currency symbol (matching Firefly)', + 'mappable' => true, + ], + 'description' => [ + 'name' => 'Description', + 'mappable' => false, + ], + 'date-transaction' => [ + 'name' => 'Date', + 'mappable' => false, + ], + 'date-rent' => [ + 'name' => 'Rent calculation date', + 'mappable' => false, + ], + 'budget-id' => [ + 'name' => 'Budget ID (matching Firefly)', + 'mappable' => true, + ], + 'budget-name' => [ + 'name' => 'Budget name', + 'mappable' => true, + ], + 'rabo-debet-credet' => [ + 'name' => 'Rabobank specific debet/credet indicator', + 'mappable' => false, + ], + 'category-id' => [ + 'name' => 'Category ID (matching Firefly)', + 'mappable' => true, + ], + 'category-name' => [ + 'name' => 'Category name', + 'mappable' => true, + ], + 'tags-comma' => [ + 'name' => 'Tags (comma separated)', + 'mappable' => true, + ], + 'tags-space' => [ + 'name' => 'Tags (space separated)', + 'mappable' => true, + ], + 'account-id' => [ + 'name' => 'Asset account ID (matching Firefly)', + 'mappable' => true, + ], + 'account-name' => [ + 'name' => 'Asset account name', + 'mappable' => true, + ], + 'account-iban' => [ + 'name' => 'Asset account IBAN', + 'mappable' => true, + ], + 'opposing-id' => [ + 'name' => 'Expense or revenue account ID (matching Firefly)', + 'mappable' => true, + ], + 'opposing-name' => [ + 'name' => 'Expense or revenue account name', + 'mappable' => true, + ], + 'opposing-iban' => [ + 'name' => 'Expense or revenue account IBAN', + 'mappable' => true, + ], + 'amount' => [ + 'name' => 'Amount', + 'mappable' => false, + ], + 'sepa-ct-id' => [ + 'name' => 'SEPA Credit Transfer end-to-end ID', + 'mappable' => false, + ], + 'sepa-ct-op' => [ + 'name' => 'SEPA Credit Transfer opposing account', + 'mappable' => false, + ], + 'sepa-db' => [ + 'name' => 'SEPA Direct Debet', + 'mappable' => false, + ], + ] +]; \ No newline at end of file diff --git a/database/seeds/TestDataSeeder.php b/database/seeds/TestDataSeeder.php index 2ef6f90674..d8afef1045 100644 --- a/database/seeds/TestDataSeeder.php +++ b/database/seeds/TestDataSeeder.php @@ -98,7 +98,7 @@ class TestDataSeeder extends Seeder protected function createAssetAccounts() { - $assets = ['MyBank Checking Account', 'Savings', 'Shared', 'Creditcard']; + $assets = ['MyBank Checking Account', 'Savings', 'Shared', 'Creditcard', 'Emergencies', 'STE']; $assetMeta = [ [ 'accountRole' => 'defaultAsset', @@ -114,6 +114,12 @@ class TestDataSeeder extends Seeder 'ccMonthlyPaymentDate' => '2015-05-27', 'ccType' => 'monthlyFull' ], + [ + 'accountRole' => 'savingAsset', + ], + [ + 'accountRole' => 'savingAsset', + ], ]; diff --git a/resources/lang/en/firefly.php b/resources/lang/en/firefly.php index d58d2cc632..071f352e50 100644 --- a/resources/lang/en/firefly.php +++ b/resources/lang/en/firefly.php @@ -26,8 +26,11 @@ return [ 'csv_upload_form' => 'Upload form', 'upload_csv_file' => 'Upload CSV file', 'csv_header_help' => 'Check this when bla bla', + 'csv_date_help' => 'Date time format in your CSV. Follow the format like this' . + ' page indicates.', 'csv_row' => 'row', - 'upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', + 'upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', // create new stuff: 'create_new_withdrawal' => 'Create new withdrawal', diff --git a/resources/lang/en/form.php b/resources/lang/en/form.php index 8b9530ec10..ef9596096f 100644 --- a/resources/lang/en/form.php +++ b/resources/lang/en/form.php @@ -47,6 +47,7 @@ return [ 'code' => 'Code', 'csv' => 'CSV file', 'has_headers' => 'Headers', + 'date_format' => 'Date format', 'store_new_withdrawal' => 'Store new withdrawal', 'store_new_deposit' => 'Store new deposit', diff --git a/resources/lang/nl/form.php b/resources/lang/nl/form.php index 837ec1d3a8..82b5667a69 100644 --- a/resources/lang/nl/form.php +++ b/resources/lang/nl/form.php @@ -47,6 +47,7 @@ return [ 'code' => 'Code', 'csv' => 'CSV-bestand', 'has_headers' => 'Eerste rij zijn kolomnamen', + 'date_format' => 'Datumformaat', 'store_new_withdrawal' => 'Nieuwe uitgave opslaan', 'store_new_deposit' => 'Nieuwe inkomsten opslaan', diff --git a/resources/twig/csv/upload.twig b/resources/twig/csv/column-roles.twig similarity index 73% rename from resources/twig/csv/upload.twig rename to resources/twig/csv/column-roles.twig index 4b0b34cd07..6325ada631 100644 --- a/resources/twig/csv/upload.twig +++ b/resources/twig/csv/column-roles.twig @@ -20,7 +20,9 @@
    - {{ 'csv_process_text'|_ }} +

    {{ 'csv_process_text'|_ }}

    +

    {{ 'csv_more_information' }}

    +

    {{ 'csv_more_information_text'|_ }}

    @@ -39,7 +41,8 @@
    - +
    + @@ -52,16 +55,21 @@ {% endfor %}
    {{ 'cvs_column_name'|_ }}{{ header }} {{ example[index] }} - {{ Form.select(('role_'~index), roles) }} + {{ Form.select(('role['~index~']'), roles) }} - {{ Form.checkbox(('map_'~index),false) }} + {{ Form.checkbox(('map['~index~']'),1,false) }}
    - +

    + +

    +
    diff --git a/resources/twig/csv/index.twig b/resources/twig/csv/index.twig index 023d0b2fe0..79e3b53add 100644 --- a/resources/twig/csv/index.twig +++ b/resources/twig/csv/index.twig @@ -21,6 +21,8 @@
    {{ 'csv_index_text'|_ }} +

    {{ 'csv_more_information' }}

    + {{ 'csv_more_information_text'|_ }}
    @@ -44,6 +46,7 @@ {{ ExpandedForm.checkbox('has_headers',false,null,{helpText: 'csv_header_help'|_}) }} + {{ ExpandedForm.text('date_format','Ymd',{helpText: 'csv_date_help'|_}) }} {{ ExpandedForm.file('csv') }} diff --git a/resources/twig/csv/map.twig b/resources/twig/csv/map.twig new file mode 100644 index 0000000000..78ed891fa8 --- /dev/null +++ b/resources/twig/csv/map.twig @@ -0,0 +1,83 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +
    +
    +
    +
    +

    {{ 'csv_map'|_ }}

    + + +
    + +
    + +
    +
    +

    {{ 'csv_map_text'|_ }}

    +

    {{ 'csv_more_information' }}

    + +

    {{ 'csv_more_information_text'|_ }}

    +
    +
    + +
    +
    +
    + + + {% for index,columnName in map %} + +
    +
    +
    +
    +

    {{ columnName }}

    + + +
    + +
    + +
    +
    + + + + + + + {% for value in values[index] %} + + + + + {% endfor %} + + + +
    {{ 'cvs_field_value'|_ }}{{ 'csv_field_mapped_to'|_ }}
    {{ value }} + {{ Form.select('mapping['~index~']['~value~']',options[index]) }} +
    + + +
    +
    +
    +
    + {% endfor %} +

    + +

    +
    + + +{% endblock %} diff --git a/resources/twig/form/help.twig b/resources/twig/form/help.twig index e1e2d011ea..d402fb623b 100644 --- a/resources/twig/form/help.twig +++ b/resources/twig/form/help.twig @@ -1,3 +1,3 @@ {% if options.helpText %} -

    {{ options.helpText }}

    +

    {{ options.helpText|raw }}

    {% endif %} diff --git a/resources/twig/form/text.twig b/resources/twig/form/text.twig index 0b199f9117..c2f890c0fe 100644 --- a/resources/twig/form/text.twig +++ b/resources/twig/form/text.twig @@ -3,6 +3,7 @@
    {{ Form.input('text', name, value, options) }} + {% include 'form/help.twig' %} {% include 'form/feedback.twig' %}
    From 65122f014433dbf1549968929e7e42de30cc399f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 5 Jul 2015 06:18:02 +0200 Subject: [PATCH 05/10] Greatly expanded the CSV routine. --- app/Helpers/Csv/Converter/AccountIban.php | 46 +++ app/Helpers/Csv/Converter/Amount.php | 32 ++ app/Helpers/Csv/Converter/BasicConverter.php | 101 +++++ .../Csv/Converter/ConverterInterface.php | 51 +++ app/Helpers/Csv/Converter/CurrencyCode.php | 27 ++ app/Helpers/Csv/Converter/Date.php | 33 ++ app/Helpers/Csv/Converter/Ignore.php | 22 ++ .../Csv/Converter/RabobankDebetCredit.php | 27 ++ app/Helpers/Csv/Data.php | 235 ++++++++++++ app/Helpers/Csv/DataGrabber.php | 54 +++ app/Helpers/Csv/Importer.php | 175 +++++++++ app/Helpers/Csv/Wizard.php | 178 +++++++++ app/Helpers/Csv/WizardInterface.php | 58 +++ app/Http/Controllers/CsvController.php | 352 ++++++++---------- app/Http/routes.php | 3 + app/Providers/FireflyServiceProvider.php | 3 + config/csv.php | 34 +- resources/twig/csv/column-roles.twig | 5 +- resources/twig/csv/download-config.twig | 40 ++ resources/twig/csv/index.twig | 5 +- resources/twig/csv/map.twig | 7 +- resources/twig/error.twig | 4 +- 22 files changed, 1282 insertions(+), 210 deletions(-) create mode 100644 app/Helpers/Csv/Converter/AccountIban.php create mode 100644 app/Helpers/Csv/Converter/Amount.php create mode 100644 app/Helpers/Csv/Converter/BasicConverter.php create mode 100644 app/Helpers/Csv/Converter/ConverterInterface.php create mode 100644 app/Helpers/Csv/Converter/CurrencyCode.php create mode 100644 app/Helpers/Csv/Converter/Date.php create mode 100644 app/Helpers/Csv/Converter/Ignore.php create mode 100644 app/Helpers/Csv/Converter/RabobankDebetCredit.php create mode 100644 app/Helpers/Csv/Data.php create mode 100644 app/Helpers/Csv/DataGrabber.php create mode 100644 app/Helpers/Csv/Importer.php create mode 100644 app/Helpers/Csv/Wizard.php create mode 100644 app/Helpers/Csv/WizardInterface.php create mode 100644 resources/twig/csv/download-config.twig diff --git a/app/Helpers/Csv/Converter/AccountIban.php b/app/Helpers/Csv/Converter/AccountIban.php new file mode 100644 index 0000000000..6aa7494c62 --- /dev/null +++ b/app/Helpers/Csv/Converter/AccountIban.php @@ -0,0 +1,46 @@ +mapped[$this->index][$this->value])) { + $account = Auth::user()->accounts()->find($this->mapped[$this->index][$this->value]); + } else { + // find or create new account: + $accountType = AccountType::where('type', 'Asset account')->first(); + $account = Account::firstOrCreateEncrypted( + [ + 'name' => $this->value, + //'iban' => $this->value, + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id + ] + ); + } + + return $account; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Amount.php b/app/Helpers/Csv/Converter/Amount.php new file mode 100644 index 0000000000..de2e9d5b08 --- /dev/null +++ b/app/Helpers/Csv/Converter/Amount.php @@ -0,0 +1,32 @@ +value)) { + return $this->value; + } + + return 0; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/BasicConverter.php b/app/Helpers/Csv/Converter/BasicConverter.php new file mode 100644 index 0000000000..ff19555071 --- /dev/null +++ b/app/Helpers/Csv/Converter/BasicConverter.php @@ -0,0 +1,101 @@ +data; + } + + /** + * @param array $data + */ + public function setData($data) + { + $this->data = $data; + } + + /** + * @return mixed + */ + public function getIndex() + { + return $this->index; + } + + /** + * @param mixed $index + */ + public function setIndex($index) + { + $this->index = $index; + } + + /** + * @return array + */ + public function getMapped() + { + return $this->mapped; + } + + /** + * @param array $mapped + */ + public function setMapped($mapped) + { + $this->mapped = $mapped; + } + + /** + * @return mixed + */ + public function getRole() + { + return $this->role; + } + + /** + * @param mixed $role + */ + public function setRole($role) + { + $this->role = $role; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed $value + */ + public function setValue($value) + { + $this->value = $value; + } + + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/ConverterInterface.php b/app/Helpers/Csv/Converter/ConverterInterface.php new file mode 100644 index 0000000000..3efd70555c --- /dev/null +++ b/app/Helpers/Csv/Converter/ConverterInterface.php @@ -0,0 +1,51 @@ +mapped[$this->index][$this->value])) { + $currency = TransactionCurrency::find($this->mapped[$this->index][$this->value]); + } else { + $currency = TransactionCurrency::whereCode($this->value)->first(); + } + + return $currency; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Date.php b/app/Helpers/Csv/Converter/Date.php new file mode 100644 index 0000000000..df79825a62 --- /dev/null +++ b/app/Helpers/Csv/Converter/Date.php @@ -0,0 +1,33 @@ +value); + + return $date; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Ignore.php b/app/Helpers/Csv/Converter/Ignore.php new file mode 100644 index 0000000000..b9c7607d47 --- /dev/null +++ b/app/Helpers/Csv/Converter/Ignore.php @@ -0,0 +1,22 @@ +value == 'D') { + return -1; + } + + return 1; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Data.php b/app/Helpers/Csv/Data.php new file mode 100644 index 0000000000..149cbda6e5 --- /dev/null +++ b/app/Helpers/Csv/Data.php @@ -0,0 +1,235 @@ +sessionHasHeaders(); + $this->sessionDateFormat(); + $this->sessionCsvFileLocation(); + $this->sessionMap(); + $this->sessionRoles(); + $this->sessionMapped(); + } + + protected function sessionHasHeaders() + { + if (Session::has('csv-has-headers')) { + $this->hasHeaders = (bool)Session::get('csv-has-headers'); + } + } + + protected function sessionDateFormat() + { + if (Session::has('csv-date-format')) { + $this->dateFormat = (string)Session::get('csv-date-format'); + } + } + + protected function sessionCsvFileLocation() + { + if (Session::has('csv-file')) { + $this->csvFileLocation = (string)Session::get('csv-file'); + } + } + + protected function sessionMap() + { + if (Session::has('csv-map')) { + $this->map = (array)Session::get('csv-map'); + } + } + + protected function sessionRoles() + { + if (Session::has('csv-roles')) { + $this->roles = (array)Session::get('csv-roles'); + } + } + + protected function sessionMapped() + { + if (Session::has('csv-mapped')) { + $this->mapped = (array)Session::get('csv-mapped'); + } + } + + /** + * @return string + */ + public function getDateFormat() + { + return $this->dateFormat; + } + + /** + * @param mixed $dateFormat + */ + public function setDateFormat($dateFormat) + { + Session::put('csv-date-format', $dateFormat); + $this->dateFormat = $dateFormat; + } + + /** + * @return bool + */ + public function getHasHeaders() + { + return $this->hasHeaders; + } + + /** + * @param bool $hasHeaders + */ + public function setHasHeaders($hasHeaders) + { + Session::put('csv-has-headers', $hasHeaders); + $this->hasHeaders = $hasHeaders; + } + + /** + * @return array + */ + public function getMap() + { + return $this->map; + } + + /** + * @param array $map + */ + public function setMap(array $map) + { + Session::put('csv-map', $map); + $this->map = $map; + } + + /** + * @return array + */ + public function getMapped() + { + return $this->mapped; + } + + /** + * @param array $mapped + */ + public function setMapped(array $mapped) + { + Session::put('csv-mapped', $mapped); + $this->mapped = $mapped; + } + + /** + * @return Reader + */ + public function getReader() + { + + if (strlen($this->csvFileContent) === 0) { + $this->loadCsvFile(); + } + + if (is_null($this->reader)) { + $this->reader = Reader::createFromString($this->getCsvFileContent()); + } + + return $this->reader; + } + + protected function loadCsvFile() + { + $file = $this->getCsvFileLocation(); + $content = file_get_contents($file); + $contentDecrypted = Crypt::decrypt($content); + $this->setCsvFileContent($contentDecrypted); + } + + /** + * @return string + */ + public function getCsvFileLocation() + { + return $this->csvFileLocation; + } + + /** + * @param string $csvFileLocation + */ + public function setCsvFileLocation($csvFileLocation) + { + Session::put('csv-file', $csvFileLocation); + $this->csvFileLocation = $csvFileLocation; + } + + /** + * @return string + */ + public function getCsvFileContent() + { + return $this->csvFileContent; + } + + /** + * @param string $csvFileContent + */ + public function setCsvFileContent($csvFileContent) + { + $this->csvFileContent = $csvFileContent; + } + + /** + * @return array + */ + public function getRoles() + { + return $this->roles; + } + + /** + * @param array $roles + */ + public function setRoles(array $roles) + { + Session::put('csv-roles', $roles); + $this->roles = $roles; + } + + +} \ No newline at end of file diff --git a/app/Helpers/Csv/DataGrabber.php b/app/Helpers/Csv/DataGrabber.php new file mode 100644 index 0000000000..93260b82c4 --- /dev/null +++ b/app/Helpers/Csv/DataGrabber.php @@ -0,0 +1,54 @@ +accounts()->with( + ['accountmeta' => function (HasMany $query) { + $query->where('name', 'accountRole'); + }] + )->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + + $list = []; + /** @var Account $account */ + foreach ($result as $account) { + $list[$account->id] = $account->name; + } + + return $list; + } + + /** + * @return array + */ + public function getCurrencies() + { + $currencies = TransactionCurrency::get(); + $list = []; + foreach ($currencies as $currency) { + $list[$currency->id] = $currency->name . ' (' . $currency->code . ')'; + } + + return $list; + } + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php new file mode 100644 index 0000000000..eeda8892c5 --- /dev/null +++ b/app/Helpers/Csv/Importer.php @@ -0,0 +1,175 @@ +map = $this->data->getMap(); + $this->roles = $this->data->getRoles(); + $this->mapped = $this->data->getMapped(); + foreach ($this->data->getReader() as $row) { + $this->importRow($row); + } + } + + /** + * @param $row + * + * @throws FireflyException + */ + protected function importRow($row) + { + /* + * These fields are necessary to create a new transaction journal. Some are optional: + */ + $data = $this->getFiller(); + foreach ($row as $index => $value) { + $role = isset($this->roles[$index]) ? $this->roles[$index] : '_ignore'; + $class = Config::get('csv.roles.' . $role . '.converter'); + $field = Config::get('csv.roles.' . $role . '.field'); + + if (is_null($class)) { + throw new FireflyException('No converter for field of type "' . $role . '".'); + } + if (is_null($field)) { + throw new FireflyException('No place to store value of type "' . $role . '".'); + } + /** @var ConverterInterface $converter */ + $converter = App::make('FireflyIII\Helpers\Csv\Converter\\' . $class); + $converter->setData($data); // the complete array so far. + $converter->setIndex($index); + $converter->setValue($value); + $converter->setRole($role); + // if (is_array($field)) { + // $convertResult = $converter->convert(); + // foreach ($field as $fieldName) { + // $data[$fieldName] = $convertResult[$fieldName]; + // } + // } else { + $data[$field] = $converter->convert(); + // } + + + // case 'description': + // $data['description'] .= ' ' . $value; + // break; + // case '_ignore': + // ignore! (duh) + // break; + // case 'account-iban': + // $data['asset-account'] = $this->findAssetAccount($index, $value); + // break; + // case 'currency-code': + // $data['currency'] = $this->findCurrency($index, $value, $role); + // break; + // case 'date-transaction': + // $data['date'] = $this->parseDate($value); + // break; + // case 'rabo-debet-credit': + // $data['amount-modifier'] = $this->parseRaboDebetCredit($value); + // break; + // default: + // throw new FireflyException('Cannot process row of type "' . $role . '".'); + // break; + + + } + $data = $this->postProcess($data); + var_dump($data); + + + + exit; + + } + + /** + * @return array + */ + protected function getFiller() + { + return [ + 'description' => '', + 'asset-account' => null, + 'date' => null, + 'currency' => null, + 'amount' => null, + 'amount-modifier' => 1, + 'ignored' => null, + ]; + + } + + /** + * @param array $data + * + * @return array + */ + protected function postProcess(array $data) + { + $data['description'] = trim($data['description']); + + + return $data; + } + + /** + * @param Data $data + */ + public function setData($data) + { + $this->data = $data; + } + + /** + * @param $value + * + * @return Carbon + */ + protected function parseDate($value) + { + return Carbon::createFromFormat($this->data->getDateFormat(), $value); + } + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Wizard.php b/app/Helpers/Csv/Wizard.php new file mode 100644 index 0000000000..b08e10bc55 --- /dev/null +++ b/app/Helpers/Csv/Wizard.php @@ -0,0 +1,178 @@ + $row) { + if (($hasHeaders && $index > 1) || !$hasHeaders) { + // collect all map values + foreach ($map as $column => $irrelevant) { + // check if $irrelevant is mappable! + $values[$column][] = $row[$column]; + } + } + } + /* + * Make each one unique. + */ + foreach ($values as $column => $found) { + $values[$column] = array_unique($found); + } + + return $values; + } + + + /** + * @param array $roles + * @param mixed $map + * + * @return array + */ + public function processSelectedMapping(array $roles, $map) + { + $configRoles = Config::get('csv.roles'); + $maps = []; + + + if (is_array($map)) { + foreach ($map as $index => $field) { + if (isset($roles[$index])) { + $name = $roles[$index]; + if ($configRoles[$name]['mappable']) { + $maps[$index] = $name; + } + } + } + } + + return $maps; + + } + + /** + * @param mixed $input + * + * @return array + */ + public function processSelectedRoles($input) + { + $roles = []; + + + /* + * Store all rows for each column: + */ + if (is_array($input)) { + foreach ($input as $index => $role) { + if ($role != '_ignore') { + $roles[$index] = $role; + } + } + } + + return $roles; + } + + /** + * @param array $fields + * + * @return bool + */ + public function sessionHasValues(array $fields) + { + foreach ($fields as $field) { + if (!Session::has($field)) { + return false; + } + } + + return true; + } + + + /** + * @param array $map + * + * @return array + * @throws FireflyException + */ + public function showOptions(array $map) + { + $dataGrabber = new DataGrabber; + $options = []; + foreach ($map as $index => $columnRole) { + + /* + * Depending on the column role, get the relevant data from the database. + * This needs some work to be optimal. + */ + switch ($columnRole) { + default: + throw new FireflyException('Cannot map field of type "' . $columnRole . '".'); + break; + case 'account-iban': + $set = $dataGrabber->getAssetAccounts(); + break; + case 'currency-code': + $set = $dataGrabber->getCurrencies(); + break; + } + + /* + * Make select list kind of thing: + */ + + $options[$index] = $set; + } + + + return $options; + } + + /** + * @param $path + * + * @return string + */ + public function storeCsvFile($path) + { + $time = str_replace(' ', '-', microtime()); + $fileName = 'csv-upload-' . Auth::user()->id . '-' . $time . '.csv.encrypted'; + $fullPath = storage_path('upload') . DIRECTORY_SEPARATOR . $fileName; + $content = file_get_contents($path); + $contentEncrypted = Crypt::encrypt($content); + file_put_contents($fullPath, $contentEncrypted); + + return $fullPath; + + + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/WizardInterface.php b/app/Helpers/Csv/WizardInterface.php new file mode 100644 index 0000000000..bd938ad248 --- /dev/null +++ b/app/Helpers/Csv/WizardInterface.php @@ -0,0 +1,58 @@ +wizard = App::make('FireflyIII\Helpers\Csv\WizardInterface'); + $this->data = App::make('FireflyIII\Helpers\Csv\Data'); + } /** @@ -52,48 +62,81 @@ class CsvController extends Controller */ public function columnRoles() { - $fields = ['csv-file', 'csv-date-format', 'csv-has-headers']; - foreach ($fields as $field) { - if (!Session::has($field)) { - Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); - return Redirect::route('csv.index'); - } + $fields = ['csv-file', 'csv-date-format', 'csv-has-headers']; + if (!$this->wizard->sessionHasValues($fields)) { + Session::flash('warning', 'Could not recover upload.'); + + return Redirect::route('csv.index'); } - $subTitle = trans('firefly.csv_process'); - $fullPath = Session::get('csv-file'); - $hasHeaders = Session::get('csv-has-headers'); - $content = file_get_contents($fullPath); - $contentDecrypted = Crypt::decrypt($content); - $reader = Reader::createFromString($contentDecrypted); + $subTitle = trans('firefly.csv_process'); + $firstRow = $this->data->getReader()->fetchOne(); + $count = count($firstRow); + $headers = []; + $example = $this->data->getReader()->fetchOne(); + $availableRoles = []; + $roles = $this->data->getRoles(); + $map = $this->data->getMap(); - - Log::debug('Get uploaded content from ' . $fullPath); - Log::debug('Strlen of original content is ' . strlen($contentDecrypted)); - Log::debug('MD5 of original content is ' . md5($contentDecrypted)); - - $firstRow = $reader->fetchOne(); - - $count = count($firstRow); - $headers = []; for ($i = 1; $i <= $count; $i++) { $headers[] = trans('firefly.csv_row') . ' #' . $i; } - if ($hasHeaders) { + if ($this->data->getHasHeaders()) { $headers = $firstRow; } - // example data is always the second row: - $example = $reader->fetchOne(); - $roles = []; foreach (Config::get('csv.roles') as $name => $role) { - $roles[$name] = $role['name']; + $availableRoles[$name] = $role['name']; } - ksort($roles); + ksort($availableRoles); + return view('csv.column-roles', compact('availableRoles', 'map', 'roles', 'headers', 'example', 'subTitle')); + } - return view('csv.column-roles', compact('roles', 'headers', 'example', 'subTitle')); + /** + * Optional download of mapping. + * + * STEP FOUR THREE-A + */ + public function downloadConfig() + { + $fields = ['csv-date-format', 'csv-has-headers']; + if (!$this->wizard->sessionHasValues($fields)) { + Session::flash('warning', 'Could not recover upload.'); + + return Redirect::route('csv.index'); + } + $data = [ + 'date-format' => Session::get('date-format'), + 'has-headers' => Session::get('csv-has-headers') + ]; + // $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles', 'csv-mapped']; + if (Session::has('csv-map')) { + $data['map'] = Session::get('csv-map'); + } + if (Session::has('csv-roles')) { + $data['roles'] = Session::get('csv-roles'); + } + if (Session::has('csv-mapped')) { + $data['mapped'] = Session::get('csv-mapped'); + } + + $result = json_encode($data, JSON_PRETTY_PRINT); + $name = 'csv-configuration-' . date('Y-m-d') . '.json'; + + header('Content-disposition: attachment; filename=' . $name); + header('Content-type: application/json'); + echo $result; + exit; + } + + /** + * @return View + */ + public function downloadConfigPage() + { + return view('csv.download-config'); } /** @@ -110,6 +153,9 @@ class CsvController extends Controller Session::forget('csv-date-format'); Session::forget('csv-has-headers'); Session::forget('csv-file'); + Session::forget('csv-map'); + Session::forget('csv-roles'); + Session::forget('csv-mapped'); // can actually upload? @@ -129,28 +175,20 @@ class CsvController extends Controller public function initialParse() { $fields = ['csv-file', 'csv-date-format', 'csv-has-headers']; - foreach ($fields as $field) { - if (!Session::has($field)) { - Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + if (!$this->wizard->sessionHasValues($fields)) { + Session::flash('warning', 'Could not recover upload.'); - return Redirect::route('csv.index'); - } + return Redirect::route('csv.index'); } - $configRoles = Config::get('csv.roles'); - $roles = []; - /* - * Store all rows for each column: - */ - if (is_array(Input::get('role'))) { - $roles = []; - foreach (Input::get('role') as $index => $role) { - if ($role != '_ignore') { - $roles[$index] = $role; - } - } - } + // process given roles and mapping: + $roles = $this->wizard->processSelectedRoles(Input::get('role')); + $maps = $this->wizard->processSelectedMapping($roles, Input::get('map')); + + Session::put('csv-map', $maps); + Session::put('csv-roles', $roles); + /* * Go back when no roles defined: */ @@ -159,28 +197,19 @@ class CsvController extends Controller return Redirect::route('csv.column-roles'); } - Session::put('csv-roles', $roles); /* - * Show user map thing: + * Continue with map specification when necessary. */ - if (is_array(Input::get('map'))) { - $maps = []; - foreach (Input::get('map') as $index => $map) { - $name = $roles[$index]; - if ($configRoles[$name]['mappable']) { - $maps[$index] = $name; - } - } - // redirect to map routine. - Session::put('csv-map', $maps); - + if (count($maps) > 0) { return Redirect::route('csv.map'); } - var_dump($roles); - var_dump($_POST); - exit; + /* + * Or simply start processing. + */ + + return Redirect::route('csv.process'); } @@ -200,106 +229,42 @@ class CsvController extends Controller * Make sure all fields we need are accounted for. */ $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles']; - foreach ($fields as $field) { - if (!Session::has($field)) { - Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + if (!$this->wizard->sessionHasValues($fields)) { + Session::flash('warning', 'Could not recover upload.'); - return Redirect::route('csv.index'); - } + return Redirect::route('csv.index'); } - /* - * The $map array contains all columns - * the user wishes to map on to data already in the system. - */ - $map = Session::get('csv-map'); - /* * The "options" array contains all options the user has * per column, where the key represents the column. * * For each key there is an array which in turn represents * all the options available: grouped by ID. + * + * Aka: + * + * options[column index] = [ + * field id => field identifier. + * ] */ - $options = []; - - /* - * Loop each field the user whishes to map. - */ - foreach ($map as $index => $columnRole) { - - /* - * Depending on the column role, get the relevant data from the database. - * This needs some work to be optimal. - */ - switch ($columnRole) { - default: - throw new FireflyException('Cannot map field of type "' . $columnRole . '".'); - break; - case 'account-iban': - // get content for this column. - $content = Auth::user()->accounts()->where('account_type_id', 3)->get(['accounts.*']); - $list = []; - // make user friendly list: - - foreach ($content as $account) { - $list[$account->id] = $account->name; - //if(!is_null($account->iban)) { - //$list[$account->id] .= ' ('.$account->iban.')'; - //} - } - $options[$index] = $list; - break; - case 'currency-code': - $currencies = TransactionCurrency::get(); - $list = []; - foreach ($currencies as $currency) { - $list[$currency->id] = $currency->name . ' (' . $currency->code . ')'; - } - $options[$index] = $list; - break; - case 'opposing-name': - // get content for this column. - $content = Auth::user()->accounts()->whereIn('account_type_id', [4, 5])->get(['accounts.*']); - $list = []; - // make user friendly list: - - foreach ($content as $account) { - $list[$account->id] = $account->name . ' (' . $account->accountType->type . ')'; - } - $options[$index] = $list; - break; - - } - + try { + $options = $this->wizard->showOptions($this->data->getMap()); + } catch (FireflyException $e) { + return view('error', ['message' => $e->getMessage()]); } - /* * After these values are prepped, read the actual CSV file */ - $content = file_get_contents(Session::get('csv-file')); - $hasHeaders = Session::get('csv-has-headers'); - $reader = Reader::createFromString(Crypt::decrypt($content)); - $values = []; + $reader = $this->data->getReader(); + $map = $this->data->getMap(); + $hasHeaders = $this->data->getHasHeaders(); + $values = $this->wizard->getMappableValues($reader, $map, $hasHeaders); + $map = $this->data->getMap(); + $mapped = $this->data->getMapped(); - /* - * Loop over the CSV and collect mappable data: - */ - foreach ($reader as $index => $row) { - if (($hasHeaders && $index > 1) || !$hasHeaders) { - // collect all map values - foreach ($map as $column => $irrelevant) { - // check if $irrelevant is mappable! - $values[$column][] = $row[$column]; - } - } - } - foreach ($values as $column => $found) { - $values[$column] = array_unique($found); - } - - return view('csv.map', compact('map', 'options', 'values')); + return view('csv.map', compact('map', 'options', 'values', 'mapped')); } /** @@ -313,14 +278,24 @@ class CsvController extends Controller * Make sure all fields we need are accounted for. */ $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles', 'csv-mapped']; - foreach ($fields as $field) { - if (!Session::has($field)) { - Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + if (!$this->wizard->sessionHasValues($fields)) { + Session::flash('warning', 'Could not recover upload.'); - return Redirect::route('csv.index'); - } + return Redirect::route('csv.index'); } + // + $importer = new Importer; + $importer->setData($this->data); + try { + $importer->run(); + } catch (FireflyException $e) { + return view('error', ['message' => $e->getMessage()]); + } + + + exit; + // loop the original file again: $content = file_get_contents(Session::get('csv-file')); $hasHeaders = Session::get('csv-has-headers'); @@ -331,10 +306,6 @@ class CsvController extends Controller $roles = Session::get('csv-roles'); $mapped = Session::get('csv-mapped'); - var_dump($roles); - var_dump(Session::get('csv-mapped')); - - /* * Loop over the CSV and collect mappable data: */ @@ -424,13 +395,12 @@ class CsvController extends Controller * Make sure all fields we need are accounted for. */ $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles']; - foreach ($fields as $field) { - if (!Session::has($field)) { - Session::flash('warning', 'Could not recover upload (' . $field . ' missing).'); + if (!$this->wizard->sessionHasValues($fields)) { + Session::flash('warning', 'Could not recover upload.'); - return Redirect::route('csv.index'); - } + return Redirect::route('csv.index'); } + // save mapping to session. $mapped = []; if (!is_array(Input::get('mapping'))) { @@ -448,7 +418,7 @@ class CsvController extends Controller Session::put('csv-mapped', $mapped); // proceed to process. - return Redirect::route('csv.process'); + return Redirect::route('csv.download-config-page'); } @@ -468,40 +438,46 @@ class CsvController extends Controller if (!$request->hasFile('csv')) { Session::flash('warning', 'No file uploaded.'); - return Redirect::route('csv.index'); } - + /* + * Store CSV and put in session. + */ + $fullPath = $this->wizard->storeCsvFile($request->file('csv')->getRealPath()); $dateFormat = Input::get('date_format'); $hasHeaders = intval(Input::get('has_headers')) === 1; - // store file somewhere temporary (encrypted)? - $time = str_replace(' ', '-', microtime()); - $fileName = 'csv-upload-' . Auth::user()->id . '-' . $time . '.csv.encrypted'; - $fullPath = storage_path('upload') . DIRECTORY_SEPARATOR . $fileName; - $content = file_get_contents($request->file('csv')->getRealPath()); - - Log::debug('Stored uploaded content in ' . $fullPath); - Log::debug('Strlen of uploaded content is ' . strlen($content)); - Log::debug('MD5 of uploaded content is ' . md5($content)); - - $content = Crypt::encrypt($content); - file_put_contents($fullPath, $content); + $map = []; + $roles = []; + $mapped = []; - Session::put('csv-date-format', $dateFormat); - Session::put('csv-has-headers', $hasHeaders); - Session::put('csv-file', $fullPath); + /* + * Process config file if present. + */ + if ($request->hasFile('csv_config')) { + + $data = file_get_contents($request->file('csv_config')->getRealPath()); + $json = json_decode($data, true); + + if (!is_null($json)) { + $dateFormat = isset($json['date-format']) ? $json['date-format'] : $dateFormat; + $hasHeaders = isset($json['has-headers']) ? $json['has-headers'] : $hasHeaders; + $map = isset($json['map']) && is_array($json['map']) ? $json['map'] : []; + $mapped = isset($json['mapped']) && is_array($json['mapped']) ? $json['mapped'] : []; + $roles = isset($json['roles']) && is_array($json['roles']) ? $json['roles'] : []; + } + } + + $this->data->setCsvFileLocation($fullPath); + $this->data->setDateFormat($dateFormat); + $this->data->setHasHeaders($hasHeaders); + $this->data->setMap($map); + $this->data->setMapped($mapped); + $this->data->setRoles($roles); + return Redirect::route('csv.column-roles'); - - // - // - // - - // - // return view('csv.upload', compact('headers', 'example', 'roles', 'subTitle')); - } } \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 55b967497e..a391ead7e5 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -228,7 +228,10 @@ Route::group( Route::get('/csv/column_roles', ['uses' => 'CsvController@columnRoles', 'as' => 'csv.column-roles']); Route::post('/csv/initial_parse', ['uses' => 'CsvController@initialParse', 'as' => 'csv.initial_parse']); Route::get('/csv/map', ['uses' => 'CsvController@map', 'as' => 'csv.map']); + Route::get('/csv/download-config', ['uses' => 'CsvController@downloadConfig', 'as' => 'csv.download-config']); + Route::get('/csv/download', ['uses' => 'CsvController@downloadConfigPage', 'as' => 'csv.download-config-page']); Route::post('/csv/save_mapping', ['uses' => 'CsvController@saveMapping', 'as' => 'csv.save_mapping']); + Route::get('/csv/process', ['uses' => 'CsvController@process', 'as' => 'csv.process']); /** diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 685e9685e9..c528ab0bb6 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -91,6 +91,9 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind('FireflyIII\Repositories\Tag\TagRepositoryInterface', 'FireflyIII\Repositories\Tag\TagRepository'); $this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search'); + // CSV import + $this->app->bind('FireflyIII\Helpers\Csv\WizardInterface', 'FireflyIII\Helpers\Csv\Wizard'); + // make charts: // alternative is Google instead of ChartJs $this->app->bind('FireflyIII\Generator\Chart\Account\AccountChartGenerator', 'FireflyIII\Generator\Chart\Account\ChartJsAccountChartGenerator'); diff --git a/config/csv.php b/config/csv.php index 6a198dc8c8..7c65084ffd 100644 --- a/config/csv.php +++ b/config/csv.php @@ -4,6 +4,8 @@ return [ '_ignore' => [ 'name' => '(ignore this column)', 'mappable' => false, + 'converter' => 'Ignore', + 'field' => 'ignored', ], 'bill-id' => [ 'name' => 'Bill ID (matching Firefly)', @@ -22,8 +24,10 @@ return [ 'mappable' => true, ], 'currency-code' => [ - 'name' => 'Currency code (ISO 4217)', - 'mappable' => true, + 'name' => 'Currency code (ISO 4217)', + 'mappable' => true, + 'converter' => 'CurrencyCode', + 'field' => 'currency' ], 'currency-symbol' => [ 'name' => 'Currency symbol (matching Firefly)', @@ -34,8 +38,10 @@ return [ 'mappable' => false, ], 'date-transaction' => [ - 'name' => 'Date', - 'mappable' => false, + 'name' => 'Date', + 'mappable' => false, + 'converter' => 'Date', + 'field' => 'date', ], 'date-rent' => [ 'name' => 'Rent calculation date', @@ -49,9 +55,11 @@ return [ 'name' => 'Budget name', 'mappable' => true, ], - 'rabo-debet-credet' => [ - 'name' => 'Rabobank specific debet/credet indicator', - 'mappable' => false, + 'rabo-debet-credit' => [ + 'name' => 'Rabobank specific debet/credit indicator', + 'mappable' => false, + 'converter' => 'RabobankDebetCredit', + 'field' => 'amount-modifier', ], 'category-id' => [ 'name' => 'Category ID (matching Firefly)', @@ -78,8 +86,10 @@ return [ 'mappable' => true, ], 'account-iban' => [ - 'name' => 'Asset account IBAN', - 'mappable' => true, + 'name' => 'Asset account IBAN', + 'mappable' => true, + 'converter' => 'AccountIban', + 'field' => 'asset-account' ], 'opposing-id' => [ 'name' => 'Expense or revenue account ID (matching Firefly)', @@ -94,8 +104,10 @@ return [ 'mappable' => true, ], 'amount' => [ - 'name' => 'Amount', - 'mappable' => false, + 'name' => 'Amount', + 'mappable' => false, + 'converter' => 'Amount', + 'field' => 'amount', ], 'sepa-ct-id' => [ 'name' => 'SEPA Credit Transfer end-to-end ID', diff --git a/resources/twig/csv/column-roles.twig b/resources/twig/csv/column-roles.twig index 6325ada631..e3e0248288 100644 --- a/resources/twig/csv/column-roles.twig +++ b/resources/twig/csv/column-roles.twig @@ -55,16 +55,17 @@ {{ header }} {{ example[index] }} - {{ Form.select(('role['~index~']'), roles) }} + {{ Form.select(('role['~index~']'), availableRoles,roles[index]) }} - {{ Form.checkbox(('map['~index~']'),1,false) }} + {{ Form.checkbox(('map['~index~']'),1,map[index]) }} {% endfor %}

    + {{ 'go_back'|_ }} diff --git a/resources/twig/csv/download-config.twig b/resources/twig/csv/download-config.twig new file mode 100644 index 0000000000..e566d293ec --- /dev/null +++ b/resources/twig/csv/download-config.twig @@ -0,0 +1,40 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +

    +
    +
    +
    +

    {{ 'csv_download_config'|_ }}

    + + +
    + +
    + +
    +
    +

    + {{ 'csv_some_text'|_ }} +

    +

    + {{ 'csv_do_download_config'|_ }} +

    +

    + {{ 'csv_more_information_text'|_ }} +

    +

    + {{ 'csv_do_process'|_ }} +

    +
    +
    + +
    +
    +{% endblock %} diff --git a/resources/twig/csv/index.twig b/resources/twig/csv/index.twig index 79e3b53add..58b4c3f2b3 100644 --- a/resources/twig/csv/index.twig +++ b/resources/twig/csv/index.twig @@ -29,7 +29,7 @@
    -
    +

    {{ 'csv_upload_form'|_ }}

    @@ -50,6 +50,8 @@ {{ ExpandedForm.file('csv') }} + {{ ExpandedForm.file('csv_config') }} + {% if uploadPossible %}
    @@ -57,6 +59,7 @@
    + diff --git a/resources/twig/csv/map.twig b/resources/twig/csv/map.twig index 78ed891fa8..255abac3b0 100644 --- a/resources/twig/csv/map.twig +++ b/resources/twig/csv/map.twig @@ -20,10 +20,7 @@
    -

    {{ 'csv_map_text'|_ }}

    -

    {{ 'csv_more_information' }}

    - -

    {{ 'csv_more_information_text'|_ }}

    + Download config for use again
    @@ -57,7 +54,7 @@ {{ value }} - {{ Form.select('mapping['~index~']['~value~']',options[index]) }} + {{ Form.select('mapping['~index~']['~value~']',options[index], mapped[index][value]) }} {% endfor %} diff --git a/resources/twig/error.twig b/resources/twig/error.twig index e0621f17d8..4390f9c05c 100644 --- a/resources/twig/error.twig +++ b/resources/twig/error.twig @@ -4,9 +4,7 @@
    -

    Firefly
    - Error -

    +

    Sorry, an error occurred.

    From 1069db3c134be5bdbab731588752a56ac120f6b6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 5 Jul 2015 06:26:34 +0200 Subject: [PATCH 06/10] Some new converters. --- app/Helpers/Csv/Converter/BasicConverter.php | 35 +++++++++++++++---- .../Csv/Converter/ConverterInterface.php | 16 +++++---- app/Helpers/Csv/Converter/OpposingName.php | 24 +++++++++++++ app/Helpers/Csv/Importer.php | 1 + config/csv.php | 12 ++++--- 5 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 app/Helpers/Csv/Converter/OpposingName.php diff --git a/app/Helpers/Csv/Converter/BasicConverter.php b/app/Helpers/Csv/Converter/BasicConverter.php index ff19555071..ba2bffc864 100644 --- a/app/Helpers/Csv/Converter/BasicConverter.php +++ b/app/Helpers/Csv/Converter/BasicConverter.php @@ -11,10 +11,15 @@ class BasicConverter { /** @var array */ protected $data; + /** @var string */ + protected $field; + /** @var int */ protected $index; /** @var array */ protected $mapped; + /** @var string */ protected $role; + /** @var string */ protected $value; /** @@ -28,13 +33,29 @@ class BasicConverter /** * @param array $data */ - public function setData($data) + public function setData(array $data) { $this->data = $data; } /** - * @return mixed + * @return string + */ + public function getField() + { + return $this->field; + } + + /** + * @param string $field + */ + public function setField($field) + { + $this->field = $field; + } + + /** + * @return int */ public function getIndex() { @@ -42,7 +63,7 @@ class BasicConverter } /** - * @param mixed $index + * @param int $index */ public function setIndex($index) { @@ -66,7 +87,7 @@ class BasicConverter } /** - * @return mixed + * @return string */ public function getRole() { @@ -74,7 +95,7 @@ class BasicConverter } /** - * @param mixed $role + * @param string $role */ public function setRole($role) { @@ -82,7 +103,7 @@ class BasicConverter } /** - * @return mixed + * @return string */ public function getValue() { @@ -90,7 +111,7 @@ class BasicConverter } /** - * @param mixed $value + * @param string $value */ public function setValue($value) { diff --git a/app/Helpers/Csv/Converter/ConverterInterface.php b/app/Helpers/Csv/Converter/ConverterInterface.php index 3efd70555c..1e9eb275e3 100644 --- a/app/Helpers/Csv/Converter/ConverterInterface.php +++ b/app/Helpers/Csv/Converter/ConverterInterface.php @@ -22,30 +22,34 @@ interface ConverterInterface public function convert(); /** - * @param $index + * @param int $index */ public function setIndex($index); /** - * @param $mapped + * @param array $mapped */ public function setMapped($mapped); /** - * @param $role + * @param string $role */ public function setRole($role); /** - * @param $value + * @param string $value */ public function setValue($value); /** * @param array $data - * - * @return mixed */ public function setData(array $data); + /** + * @param string $field + * + */ + public function setField($field); + } \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/OpposingName.php b/app/Helpers/Csv/Converter/OpposingName.php new file mode 100644 index 0000000000..25ab7b00be --- /dev/null +++ b/app/Helpers/Csv/Converter/OpposingName.php @@ -0,0 +1,24 @@ +value; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php index eeda8892c5..644daaebea 100644 --- a/app/Helpers/Csv/Importer.php +++ b/app/Helpers/Csv/Importer.php @@ -78,6 +78,7 @@ class Importer /** @var ConverterInterface $converter */ $converter = App::make('FireflyIII\Helpers\Csv\Converter\\' . $class); $converter->setData($data); // the complete array so far. + $converter->setField($field); $converter->setIndex($index); $converter->setValue($value); $converter->setRole($role); diff --git a/config/csv.php b/config/csv.php index 7c65084ffd..dea1edb42f 100644 --- a/config/csv.php +++ b/config/csv.php @@ -2,8 +2,8 @@ return [ 'roles' => [ '_ignore' => [ - 'name' => '(ignore this column)', - 'mappable' => false, + 'name' => '(ignore this column)', + 'mappable' => false, 'converter' => 'Ignore', 'field' => 'ignored', ], @@ -46,6 +46,8 @@ return [ 'date-rent' => [ 'name' => 'Rent calculation date', 'mappable' => false, + 'converter' => 'Date', + 'field' => 'date-rent', ], 'budget-id' => [ 'name' => 'Budget ID (matching Firefly)', @@ -96,8 +98,10 @@ return [ 'mappable' => true, ], 'opposing-name' => [ - 'name' => 'Expense or revenue account name', - 'mappable' => true, + 'name' => 'Expense or revenue account name', + 'mappable' => true, + 'converter' => 'OpposingName', + 'field' => 'opposing-account' ], 'opposing-iban' => [ 'name' => 'Expense or revenue account IBAN', From 1dc6d8de40ce4664f27037863b4ef2598930221a Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 5 Jul 2015 06:59:05 +0200 Subject: [PATCH 07/10] First working version, very beta. --- app/Helpers/Csv/Converter/AccountIban.php | 7 +- app/Helpers/Csv/Converter/Description.php | 21 +++ app/Helpers/Csv/Importer.php | 180 +++++++++++++++++----- config/csv.php | 28 ++-- 4 files changed, 183 insertions(+), 53 deletions(-) create mode 100644 app/Helpers/Csv/Converter/Description.php diff --git a/app/Helpers/Csv/Converter/AccountIban.php b/app/Helpers/Csv/Converter/AccountIban.php index 6aa7494c62..bbca8ac0ee 100644 --- a/app/Helpers/Csv/Converter/AccountIban.php +++ b/app/Helpers/Csv/Converter/AccountIban.php @@ -11,6 +11,7 @@ namespace FireflyIII\Helpers\Csv\Converter; use Auth; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use Log; /** * Class AccountIban @@ -36,9 +37,13 @@ class AccountIban extends BasicConverter implements ConverterInterface 'name' => $this->value, //'iban' => $this->value, 'user_id' => Auth::user()->id, - 'account_type_id' => $accountType->id + 'account_type_id' => $accountType->id, + 'active' => true, ] ); + if ($account->getErrors()->count() > 0) { + Log::error('Create or find asset account: ' . json_encode($account->getErrors()->all())); + } } return $account; diff --git a/app/Helpers/Csv/Converter/Description.php b/app/Helpers/Csv/Converter/Description.php new file mode 100644 index 0000000000..19bede19a0 --- /dev/null +++ b/app/Helpers/Csv/Converter/Description.php @@ -0,0 +1,21 @@ +data['description'] . ' ' . $this->value); + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php index 644daaebea..4d8ac5c1b4 100644 --- a/app/Helpers/Csv/Importer.php +++ b/app/Helpers/Csv/Importer.php @@ -3,10 +3,18 @@ namespace FireflyIII\Helpers\Csv; use App; +use Auth; use Carbon\Carbon; use Config; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Csv\Converter\ConverterInterface; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Illuminate\Support\MessageBag; +use Log; /** * Class Importer @@ -18,13 +26,12 @@ class Importer /** @var Data */ protected $data; - + /** @var array */ + protected $errors; /** @var array */ protected $map; - /** @var array */ protected $mapped; - /** @var array */ protected $roles; @@ -48,22 +55,29 @@ class Importer $this->map = $this->data->getMap(); $this->roles = $this->data->getRoles(); $this->mapped = $this->data->getMapped(); - foreach ($this->data->getReader() as $row) { - $this->importRow($row); + foreach ($this->data->getReader() as $index => $row) { + $result = $this->importRow($row); + if (!($result === true)) { + $this->errors[$index] = $result; + Log::error('ImportRow: ' . $result); + } } + + return count($this->errors); } /** * @param $row * * @throws FireflyException + * @return string|bool */ protected function importRow($row) { /* * These fields are necessary to create a new transaction journal. Some are optional: */ - $data = $this->getFiller(); + $data = $this->getFiller(); foreach ($row as $index => $value) { $role = isset($this->roles[$index]) ? $this->roles[$index] : '_ignore'; $class = Config::get('csv.roles.' . $role . '.converter'); @@ -91,37 +105,19 @@ class Importer $data[$field] = $converter->convert(); // } - - // case 'description': - // $data['description'] .= ' ' . $value; - // break; - // case '_ignore': - // ignore! (duh) - // break; - // case 'account-iban': - // $data['asset-account'] = $this->findAssetAccount($index, $value); - // break; - // case 'currency-code': - // $data['currency'] = $this->findCurrency($index, $value, $role); - // break; - // case 'date-transaction': - // $data['date'] = $this->parseDate($value); - // break; - // case 'rabo-debet-credit': - // $data['amount-modifier'] = $this->parseRaboDebetCredit($value); - // break; - // default: - // throw new FireflyException('Cannot process row of type "' . $role . '".'); - // break; - - } - $data = $this->postProcess($data); - var_dump($data); + $data = $this->postProcess($data); + $result = $this->validateData($data); + if ($result === true) { + $result = $this->createTransactionJournal($data); + } else { + Log::error('Validator: ' . $result); + } + if ($result instanceof TransactionJournal) { + return true; + } - - - exit; + return 'Not a journal.'; } @@ -131,13 +127,16 @@ class Importer protected function getFiller() { return [ - 'description' => '', - 'asset-account' => null, - 'date' => null, - 'currency' => null, - 'amount' => null, - 'amount-modifier' => 1, - 'ignored' => null, + 'description' => '', + 'asset-account' => null, + 'opposing-account' => '', + 'opposing-account-object' => null, + 'date' => null, + 'currency' => null, + 'amount' => null, + 'amount-modifier' => 1, + 'ignored' => null, + 'date-rent' => null, ]; } @@ -149,12 +148,109 @@ class Importer */ protected function postProcess(array $data) { + bcscale(2); $data['description'] = trim($data['description']); + $data['amount'] = bcmul($data['amount'], $data['amount-modifier']); + if ($data['amount'] < 0) { + // create expense account: + $accountType = AccountType::where('type', 'Expense account')->first(); + } else { + // create revenue account: + $accountType = AccountType::where('type', 'Revenue account')->first(); + } + $data['opposing-account-object'] = Account::firstOrCreateEncrypted( + [ + 'user_id' => Auth::user()->id, + 'name' => ucwords($data['opposing-account']), + 'account_type_id' => $accountType->id, + 'active' => 1, + ] + ); return $data; } + /** + * @param $data + * + * @return bool|string + */ + protected function validateData($data) + { + if (is_null($data['date']) && is_null($data['date-rent'])) { + return 'No date value for this row.'; + } + if (strlen($data['description']) == 0) { + return 'No valid description'; + } + if (is_null($data['opposing-account-object'])) { + return 'Opposing account is null'; + } + + return true; + } + + /** + * @param array $data + * + * @return static + */ + protected function createTransactionJournal(array $data) + { + bcscale(2); + $date = $data['date']; + if (is_null($data['date'])) { + $date = $data['date-rent']; + } + if ($data['amount'] < 0) { + $transactionType = TransactionType::where('type', 'Withdrawal')->first(); + } else { + $transactionType = TransactionType::where('type', 'Deposit')->first(); + } + $errors = new MessageBag; + $journal = TransactionJournal::create( + [ + 'user_id' => Auth::user()->id, + 'transaction_type_id' => $transactionType->id, + 'bill_id' => null, + 'transaction_currency_id' => $data['currency']->id, + 'description' => $data['description'], + 'completed' => 0, + 'date' => $date, + ] + ); + $errors = $journal->getErrors()->merge($errors); + if ($journal->getErrors()->count() == 0) { + // create both transactions: + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $data['asset-account']->id, + 'amount' => $data['amount'] + ] + ); + $errors = $transaction->getErrors()->merge($errors); + + $transaction = Transaction::create( + [ + 'transaction_journal_id' => $journal->id, + 'account_id' => $data['opposing-account-object']->id, + 'amount' => bcmul($data['amount'], -1) + ] + ); + $errors = $transaction->getErrors()->merge($errors); + } + if($errors->count() == 0) { + $journal->completed = 1; + $journal->save(); + } + + return $journal; + + + } + /** * @param Data $data */ diff --git a/config/csv.php b/config/csv.php index dea1edb42f..78b4121dcb 100644 --- a/config/csv.php +++ b/config/csv.php @@ -34,8 +34,10 @@ return [ 'mappable' => true, ], 'description' => [ - 'name' => 'Description', - 'mappable' => false, + 'name' => 'Description', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', ], 'date-transaction' => [ 'name' => 'Date', @@ -44,8 +46,8 @@ return [ 'field' => 'date', ], 'date-rent' => [ - 'name' => 'Rent calculation date', - 'mappable' => false, + 'name' => 'Rent calculation date', + 'mappable' => false, 'converter' => 'Date', 'field' => 'date-rent', ], @@ -114,16 +116,22 @@ return [ 'field' => 'amount', ], 'sepa-ct-id' => [ - 'name' => 'SEPA Credit Transfer end-to-end ID', - 'mappable' => false, + 'name' => 'SEPA Credit Transfer end-to-end ID', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', ], 'sepa-ct-op' => [ - 'name' => 'SEPA Credit Transfer opposing account', - 'mappable' => false, + 'name' => 'SEPA Credit Transfer opposing account', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', ], 'sepa-db' => [ - 'name' => 'SEPA Direct Debet', - 'mappable' => false, + 'name' => 'SEPA Direct Debet', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', ], ] ]; \ No newline at end of file From f8936210cfa111fd8138f74f2742a6f311eab25a Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 5 Jul 2015 07:18:48 +0200 Subject: [PATCH 08/10] Small fixes. --- app/Helpers/Csv/Importer.php | 29 ++++++++++---- app/Helpers/Csv/Specifix.php | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 app/Helpers/Csv/Specifix.php diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php index 4d8ac5c1b4..94e6b75e5a 100644 --- a/app/Helpers/Csv/Importer.php +++ b/app/Helpers/Csv/Importer.php @@ -16,6 +16,8 @@ use FireflyIII\Models\TransactionType; use Illuminate\Support\MessageBag; use Log; +set_time_limit(0); + /** * Class Importer * @@ -106,7 +108,7 @@ class Importer // } } - $data = $this->postProcess($data); + $data = $this->postProcess($data, $row); $result = $this->validateData($data); if ($result === true) { $result = $this->createTransactionJournal($data); @@ -142,11 +144,14 @@ class Importer } /** + * Row denotes the original data. + * * @param array $data + * @param array $row * * @return array */ - protected function postProcess(array $data) + protected function postProcess(array $data, array $row) { bcscale(2); $data['description'] = trim($data['description']); @@ -159,6 +164,16 @@ class Importer $accountType = AccountType::where('type', 'Revenue account')->first(); } + // do bank specific fixes: + + $specifix = new Specifix(); + $specifix->setData($data); + $specifix->setRow($row); + $specifix->fix($data, $row); + + // get data back: + $data = $specifix->getData(); + $data['opposing-account-object'] = Account::firstOrCreateEncrypted( [ 'user_id' => Auth::user()->id, @@ -208,7 +223,7 @@ class Importer } else { $transactionType = TransactionType::where('type', 'Deposit')->first(); } - $errors = new MessageBag; + $errors = new MessageBag; $journal = TransactionJournal::create( [ 'user_id' => Auth::user()->id, @@ -220,7 +235,7 @@ class Importer 'date' => $date, ] ); - $errors = $journal->getErrors()->merge($errors); + $errors = $journal->getErrors()->merge($errors); if ($journal->getErrors()->count() == 0) { // create both transactions: $transaction = Transaction::create( @@ -230,7 +245,7 @@ class Importer 'amount' => $data['amount'] ] ); - $errors = $transaction->getErrors()->merge($errors); + $errors = $transaction->getErrors()->merge($errors); $transaction = Transaction::create( [ @@ -239,9 +254,9 @@ class Importer 'amount' => bcmul($data['amount'], -1) ] ); - $errors = $transaction->getErrors()->merge($errors); + $errors = $transaction->getErrors()->merge($errors); } - if($errors->count() == 0) { + if ($errors->count() == 0) { $journal->completed = 1; $journal->save(); } diff --git a/app/Helpers/Csv/Specifix.php b/app/Helpers/Csv/Specifix.php new file mode 100644 index 0000000000..0e530d4a5d --- /dev/null +++ b/app/Helpers/Csv/Specifix.php @@ -0,0 +1,73 @@ +rabobankFixEmptyOpposing(); + + } + + /** + * Fixes Rabobank specific thing. + */ + protected function rabobankFixEmptyOpposing() + { + if (strlen($this->data['opposing-account']) == 0) { + $this->data['opposing-account'] = $this->row[10]; + } + $this->data['description'] = trim(str_replace($this->row[10], '', $this->data['description'])); + } + + + /** + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * @param array $data + */ + public function setData($data) + { + $this->data = $data; + } + + /** + * @return array + */ + public function getRow() + { + return $this->row; + } + + /** + * @param array $row + */ + public function setRow($row) + { + $this->row = $row; + } + + +} \ No newline at end of file From 540dde135e0e313557983fdecfb8ad39b3bc9a4b Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 5 Jul 2015 08:45:05 +0200 Subject: [PATCH 09/10] CSV importer now indicates the problems it has. --- app/Helpers/Csv/Importer.php | 27 ++-- .../AssetAccount.php} | 34 ++--- app/Helpers/Csv/Mapper/MapperInterface.php | 16 +++ .../Csv/Mapper/TransactionCurrency.php | 28 +++++ app/Helpers/Csv/Wizard.php | 35 +++--- app/Http/Controllers/CsvController.php | 118 ++++-------------- config/csv.php | 6 +- resources/lang/en/firefly.php | 52 ++++++-- resources/lang/en/form.php | 1 + resources/twig/csv/column-roles.twig | 105 ++++++++-------- resources/twig/csv/download-config.twig | 58 +++++---- resources/twig/csv/index.twig | 101 ++++++++------- resources/twig/csv/map.twig | 28 +++-- 13 files changed, 312 insertions(+), 297 deletions(-) rename app/Helpers/Csv/{DataGrabber.php => Mapper/AssetAccount.php} (51%) create mode 100644 app/Helpers/Csv/Mapper/MapperInterface.php create mode 100644 app/Helpers/Csv/Mapper/TransactionCurrency.php diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php index 94e6b75e5a..3f7be24554 100644 --- a/app/Helpers/Csv/Importer.php +++ b/app/Helpers/Csv/Importer.php @@ -37,18 +37,6 @@ class Importer /** @var array */ protected $roles; - /** - * @param $value - */ - public function parseRaboDebetCredit($value) - { - if ($value == 'D') { - return -1; - } - - return 1; - } - /** * */ @@ -58,10 +46,11 @@ class Importer $this->roles = $this->data->getRoles(); $this->mapped = $this->data->getMapped(); foreach ($this->data->getReader() as $index => $row) { + Log::debug('Now at row ' . $index); $result = $this->importRow($row); if (!($result === true)) { + Log::error('Caught error at row #' . $index . ': ' . $result); $this->errors[$index] = $result; - Log::error('ImportRow: ' . $result); } } @@ -96,16 +85,10 @@ class Importer $converter->setData($data); // the complete array so far. $converter->setField($field); $converter->setIndex($index); + $converter->setMapped($this->mapped); $converter->setValue($value); $converter->setRole($role); - // if (is_array($field)) { - // $convertResult = $converter->convert(); - // foreach ($field as $fieldName) { - // $data[$fieldName] = $convertResult[$fieldName]; - // } - // } else { $data[$field] = $converter->convert(); - // } } $data = $this->postProcess($data, $row); @@ -164,6 +147,10 @@ class Importer $accountType = AccountType::where('type', 'Revenue account')->first(); } + if(strlen($data['description']) == 0) { + $data['description'] = trans('firefly.csv_empty_description'); + } + // do bank specific fixes: $specifix = new Specifix(); diff --git a/app/Helpers/Csv/DataGrabber.php b/app/Helpers/Csv/Mapper/AssetAccount.php similarity index 51% rename from app/Helpers/Csv/DataGrabber.php rename to app/Helpers/Csv/Mapper/AssetAccount.php index 93260b82c4..d8fe71cc2d 100644 --- a/app/Helpers/Csv/DataGrabber.php +++ b/app/Helpers/Csv/Mapper/AssetAccount.php @@ -1,26 +1,29 @@ accounts()->with( ['accountmeta' => function (HasMany $query) { @@ -36,19 +39,4 @@ class DataGrabber return $list; } - - /** - * @return array - */ - public function getCurrencies() - { - $currencies = TransactionCurrency::get(); - $list = []; - foreach ($currencies as $currency) { - $list[$currency->id] = $currency->name . ' (' . $currency->code . ')'; - } - - return $list; - } - } \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/MapperInterface.php b/app/Helpers/Csv/Mapper/MapperInterface.php new file mode 100644 index 0000000000..a01785f9a4 --- /dev/null +++ b/app/Helpers/Csv/Mapper/MapperInterface.php @@ -0,0 +1,16 @@ +id] = $currency->name . ' (' . $currency->code . ')'; + } + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Wizard.php b/app/Helpers/Csv/Wizard.php index b08e10bc55..92291e7f45 100644 --- a/app/Helpers/Csv/Wizard.php +++ b/app/Helpers/Csv/Wizard.php @@ -1,11 +1,14 @@ $columnRole) { - /* - * Depending on the column role, get the relevant data from the database. - * This needs some work to be optimal. - */ - switch ($columnRole) { - default: - throw new FireflyException('Cannot map field of type "' . $columnRole . '".'); - break; - case 'account-iban': - $set = $dataGrabber->getAssetAccounts(); - break; - case 'currency-code': - $set = $dataGrabber->getCurrencies(); - break; + $mapper = Config::get('csv.roles.' . $columnRole . '.mapper'); + if (is_null($mapper)) { + throw new FireflyException('Cannot map field of type "' . $columnRole . '".'); } - - /* - * Make select list kind of thing: - */ - + $class = 'FireflyIII\Helpers\Csv\Mapper\\' . $mapper; + try { + /** @var MapperInterface $mapObject */ + $mapObject = App::make($class); + } catch (ReflectionException $e) { + throw new FireflyException('Column "' . $columnRole . '" cannot be mapped because class ' . $mapper . ' does not exist.'); + } + $set = $mapObject->getMap(); $options[$index] = $set; } - return $options; } diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php index a8636dde63..03cfd5e63f 100644 --- a/app/Http/Controllers/CsvController.php +++ b/app/Http/Controllers/CsvController.php @@ -9,18 +9,14 @@ namespace FireflyIII\Http\Controllers; use App; -use Carbon\Carbon; use Config; -use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Csv\Data; use FireflyIII\Helpers\Csv\Importer; use FireflyIII\Helpers\Csv\WizardInterface; -use FireflyIII\Models\Account; -use FireflyIII\Models\TransactionCurrency; use Illuminate\Http\Request; use Input; -use League\Csv\Reader; +use Log; use Redirect; use Session; use View; @@ -80,7 +76,7 @@ class CsvController extends Controller $map = $this->data->getMap(); for ($i = 1; $i <= $count; $i++) { - $headers[] = trans('firefly.csv_row') . ' #' . $i; + $headers[] = trans('firefly.csv_column') . ' #' . $i; } if ($this->data->getHasHeaders()) { $headers = $firstRow; @@ -157,12 +153,24 @@ class CsvController extends Controller Session::forget('csv-roles'); Session::forget('csv-mapped'); + // get values which are yet unsaveable or unmappable: + $unsupported = []; + foreach (Config::get('csv.roles') as $role) { + if (!isset($role['converter'])) { + $unsupported[] = trans('firefly.csv_unsupported_value', ['columnRole' => $role['name']]); + } + if ($role['mappable'] === true && !isset($role['mapper'])) { + $unsupported[] = trans('firefly.csv_unsupported_map', ['columnRole' => $role['name']]); + } + } + sort($unsupported); + // can actually upload? $uploadPossible = is_writable(storage_path('upload')); $path = storage_path('upload'); - return view('csv.index', compact('subTitle', 'uploadPossible', 'path')); + return view('csv.index', compact('subTitle', 'uploadPossible', 'path','unsupported')); } /** @@ -209,7 +217,8 @@ class CsvController extends Controller * Or simply start processing. */ - return Redirect::route('csv.process'); + // proceed to download config + return Redirect::route('csv.download-config-page'); } @@ -284,104 +293,21 @@ class CsvController extends Controller return Redirect::route('csv.index'); } - // + Log::debug('Created importer'); $importer = new Importer; $importer->setData($this->data); try { $importer->run(); } catch (FireflyException $e) { + Log::error('Catch error: ' . $e->getMessage()); + return view('error', ['message' => $e->getMessage()]); } + Log::debug('Done importing!'); - + echo 'display result'; exit; - // loop the original file again: - $content = file_get_contents(Session::get('csv-file')); - $hasHeaders = Session::get('csv-has-headers'); - $reader = Reader::createFromString(Crypt::decrypt($content)); - - // dump stuff - $dateFormat = Session::get('csv-date-format'); - $roles = Session::get('csv-roles'); - $mapped = Session::get('csv-mapped'); - - /* - * Loop over the CSV and collect mappable data: - */ - foreach ($reader as $index => $row) { - if (($hasHeaders && $index > 1) || !$hasHeaders) { - // this is the data we need to store the new transaction: - $amount = 0; - $amountModifier = 1; - $description = ''; - $assetAccount = null; - $opposingAccount = null; - $currency = null; - $date = null; - - foreach ($row as $index => $value) { - if (isset($roles[$index])) { - switch ($roles[$index]) { - default: - throw new FireflyException('Cannot process role "' . $roles[$index] . '"'); - break; - case 'account-iban': - // find ID in "mapped" (if present). - if (isset($mapped[$index])) { - $searchID = $mapped[$index][$value]; - $assetAccount = Account::find($searchID); - } else { - // create account - } - break; - case 'opposing-name': - // don't know yet if its going to be a - // revenue or expense account. - $opposingAccount = $value; - break; - case 'currency-code': - // find ID in "mapped" (if present). - if (isset($mapped[$index])) { - $searchValue = $mapped[$index][$value]; - $currency = TransactionCurrency::whereCode($searchValue); - } else { - // create account - } - break; - case 'date-transaction': - // unmappable: - $date = Carbon::createFromFormat($dateFormat, $value); - - break; - case 'rabo-debet-credet': - if ($value == 'D') { - $amountModifier = -1; - } - break; - case 'amount': - $amount = $value; - break; - case 'description': - $description .= ' ' . $value; - break; - case 'sepa-ct-id': - $description .= ' ' . $value; - break; - - } - } - } - // do something with all this data: - - - // do something. - var_dump($row); - - } - } - - } /** diff --git a/config/csv.php b/config/csv.php index 78b4121dcb..e141c2f438 100644 --- a/config/csv.php +++ b/config/csv.php @@ -27,7 +27,8 @@ return [ 'name' => 'Currency code (ISO 4217)', 'mappable' => true, 'converter' => 'CurrencyCode', - 'field' => 'currency' + 'field' => 'currency', + 'mapper' => 'TransactionCurrency' ], 'currency-symbol' => [ 'name' => 'Currency symbol (matching Firefly)', @@ -93,7 +94,8 @@ return [ 'name' => 'Asset account IBAN', 'mappable' => true, 'converter' => 'AccountIban', - 'field' => 'asset-account' + 'field' => 'asset-account', + 'mapper' => 'AssetAccount' ], 'opposing-id' => [ 'name' => 'Expense or revenue account ID (matching Firefly)', diff --git a/resources/lang/en/firefly.php b/resources/lang/en/firefly.php index 071f352e50..1374379ef4 100644 --- a/resources/lang/en/firefly.php +++ b/resources/lang/en/firefly.php @@ -22,15 +22,53 @@ return [ // csv import: 'csv_import' => 'Import CSV file', 'csv' => 'CSV', - 'csv_index_text' => 'Here be explanation.', - 'csv_upload_form' => 'Upload form', - 'upload_csv_file' => 'Upload CSV file', - 'csv_header_help' => 'Check this when bla bla', + 'csv_index_title' => 'Upload and import a CSV file', + 'csv_index_text' => + 'This form allows you to import a CSV file with transactions into Firefly. It is based on the excellent CSV importer made by' . + ' the folks at Atlassian. Simply upload your CSV file and follow the instructions.', + 'csv_index_beta_warning' => 'This tool is very much in beta. Please proceed with caution', + 'csv_header_help' => 'Check this box when your CSV file\'s first row consists of column names, not actual data', 'csv_date_help' => 'Date time format in your CSV. Follow the format like this' . - ' page indicates.', - 'csv_row' => 'row', - 'upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', + ' page indicates. The default value will parse dates that look like this: ' . date('Ymd'), + 'csv_csv_file_help' => 'Select the CSV file here. You can only upload one file at a time', + 'csv_csv_config_file_help' => 'Select your CSV import configuration here. If you do not know what this is, ignore it. It will be explained later.', + 'csv_upload_button' => 'Start importing CSV', + 'csv_column_roles_title' => 'Define column roles', + 'csv_column_roles_text' => 'Firefly does not know what each column means. You need to indicate what every column is. Please check out the example ' + . 'data if you\'re not sure yourself. Click on the question mark (top right of the page) to learn what' + . ' each column means. If you want to map imported data onto existing data in Firefly, use the checkbox. ' + . 'The next step will show you what this button does.', + 'csv_column_roles_table' => 'Column roles', + 'csv_column' => 'CSV column', + 'cvs_column_name' => 'CSV column name', + 'cvs_column_example' => 'Column example data', + 'cvs_column_role' => 'Column contains?', + 'csv_do_map_value' => 'Map value?', + 'csv_continue' => 'Continue to the next step', + 'csv_go_back' => 'Go back to the previous step', + 'csv_map_title' => 'Map found values to existing values', + 'csv_map_text' => + 'This page allows you to map the values from the CSV file to existing entries in your database. This ensures that accounts and other' + . ' things won\'t be created twice.', + 'cvs_field_value' => 'Field value from CSV', + 'csv_field_mapped_to' => 'Must be mapped to...', + 'csv_download_config_title' => 'Download CSV configuration', + 'csv_download_config_text' => 'Everything you\'ve just set up can be downloaded as a configuration file. Click the button to do so.', + 'csv_more_information_text' => 'If the import fails, you can use this configuration file so you don\'t have to start all over again.', + 'csv_do_download_config' => 'Download configuration file.', + 'csv_empty_description' => '(empty description)', + 'csv_upload_form' => 'CSV upload form', + 'csv_index_unsupported_warning' => 'The CSV importer is yet incapable of doing the following:', + 'csv_unsupported_map' => 'The importer cannot map the column ":columnRole" to existing values in the database.', + 'csv_unsupported_value' => 'The importer does not know how to handle values in columns marked as ":columnRole".', + // 'csv_index_text' => 'Here be explanation.', + // 'csv_upload_form' => 'Upload form', + // 'upload_csv_file' => 'Upload CSV file', + // 'csv_header_help' => 'Check this when bla bla', + // 'csv_date_help' => + // 'csv_row' => 'row', + 'csv_upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', // create new stuff: 'create_new_withdrawal' => 'Create new withdrawal', diff --git a/resources/lang/en/form.php b/resources/lang/en/form.php index ef9596096f..67e7337be6 100644 --- a/resources/lang/en/form.php +++ b/resources/lang/en/form.php @@ -48,6 +48,7 @@ return [ 'csv' => 'CSV file', 'has_headers' => 'Headers', 'date_format' => 'Date format', + 'csv_config' => 'CSV import configuration', 'store_new_withdrawal' => 'Store new withdrawal', 'store_new_deposit' => 'Store new deposit', diff --git a/resources/twig/csv/column-roles.twig b/resources/twig/csv/column-roles.twig index e3e0248288..3480bdbd0f 100644 --- a/resources/twig/csv/column-roles.twig +++ b/resources/twig/csv/column-roles.twig @@ -7,32 +7,11 @@ {% block content %} -
    -
    -
    -
    -

    {{ 'csv_process'|_ }}

    - - -
    - -
    - -
    -
    -

    {{ 'csv_process_text'|_ }}

    -

    {{ 'csv_more_information' }}

    -

    {{ 'csv_more_information_text'|_ }}

    -
    -
    - -
    -
    -

    {{ 'csv_process_form'|_ }}

    +

    {{ 'csv_column_roles_title'|_ }}

    @@ -41,40 +20,68 @@
    -
    - - - +

    {{ 'csv_column_roles_text'|_ }}

    + + + + + + + + +
    +
    +
    +
    +

    {{ 'csv_column_roles_table'|_ }}

    + + +
    + +
    + +
    +
    + +
    + - - - {% for index,header in headers %} - - - - - + + + {% for index,header in headers %} + + + + + - - {% endfor %} -
    {{ 'cvs_column_name'|_ }} {{ 'cvs_column_example'|_ }} {{ 'cvs_column_role'|_ }}{{ 'do_map_value'|_ }}
    {{ header }}{{ example[index] }} - {{ Form.select(('role['~index~']'), availableRoles,roles[index]) }} - - {{ Form.checkbox(('map['~index~']'),1,map[index]) }} - {{ 'csv_do_map_value'|_ }}
    {{ header }}{{ example[index] }} + {{ Form.select(('role['~index~']'), availableRoles,roles[index]) }} + + {{ Form.checkbox(('map['~index~']'),1,map[index]) }} +
    -

    - {{ 'go_back'|_ }} - -

    -
    + + {% endfor %} + + +
    -
    +
    +
    +
    +
    + {{ 'csv_go_back'|_ }} + +
    +
    +
    +
    + {% endblock %} diff --git a/resources/twig/csv/download-config.twig b/resources/twig/csv/download-config.twig index e566d293ec..302670614d 100644 --- a/resources/twig/csv/download-config.twig +++ b/resources/twig/csv/download-config.twig @@ -7,34 +7,44 @@ {% block content %} -
    -
    -
    -
    -

    {{ 'csv_download_config'|_ }}

    +
    +
    +
    +
    +

    {{ 'csv_download_config_title'|_ }}

    + + +
    + +
    - -
    -
    +
    +

    + {{ 'csv_download_config_text'|_ }} +

    -
    -
    -

    - {{ 'csv_some_text'|_ }} -

    -

    - {{ 'csv_do_download_config'|_ }} -

    -

    - {{ 'csv_more_information_text'|_ }} -

    -

    - {{ 'csv_do_process'|_ }} -

    +

    + {{ 'csv_do_download_config'|_ }} +

    + +

    + {{ 'csv_more_information_text'|_ }} +

    +
    -
    -
    + + + {% endblock %} diff --git a/resources/twig/csv/index.twig b/resources/twig/csv/index.twig index 58b4c3f2b3..10f00c9cf2 100644 --- a/resources/twig/csv/index.twig +++ b/resources/twig/csv/index.twig @@ -7,32 +7,11 @@ {% block content %} -
    -
    -
    -
    -

    {{ 'csv'|_ }}

    - - -
    - -
    - -
    -
    - {{ 'csv_index_text'|_ }} -

    {{ 'csv_more_information' }}

    - {{ 'csv_more_information_text'|_ }} -
    -
    - -
    -
    -

    {{ 'csv_upload_form'|_ }}

    +

    {{ 'csv_index_title'|_ }}

    @@ -41,31 +20,48 @@
    + {{ 'csv_index_text'|_ }} +

    {{ 'csv_index_beta_warning'|_ }}

    + {% if unsupported|length > 0 %} +

    {{ 'csv_index_unsupported_warning'|_ }}

    +
      + {% for message in unsupported %} +
    • {{ message }}
    • + {% endfor %} +
    + {% endif %} +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +

    {{ 'csv_upload_form'|_ }}

    + + +
    + +
    + +
    +
    - - {{ ExpandedForm.checkbox('has_headers',false,null,{helpText: 'csv_header_help'|_}) }} {{ ExpandedForm.text('date_format','Ymd',{helpText: 'csv_date_help'|_}) }} - {{ ExpandedForm.file('csv') }} + {{ ExpandedForm.file('csv',{helpText: 'csv_csv_file_help'|_}) }} - {{ ExpandedForm.file('csv_config') }} + {{ ExpandedForm.file('csv_config',{helpText: 'csv_csv_config_file_help'|_}) }} - {% if uploadPossible %} -
    -
    -   -
    - -
    - - -
    -
    - {% else %} + {% if not uploadPossible %}
      @@ -74,20 +70,31 @@
    {{ path }}

    - {{ 'upload_not_writeable'|_ }} + {{ 'csv_upload_not_writeable'|_ }}

    {% endif %} - - - - - +
    -
    + +
    +
    +
    +
    + +
    +
    +
    +
    + + + + {% endblock %} diff --git a/resources/twig/csv/map.twig b/resources/twig/csv/map.twig index 255abac3b0..97ce18b582 100644 --- a/resources/twig/csv/map.twig +++ b/resources/twig/csv/map.twig @@ -11,7 +11,7 @@
    -

    {{ 'csv_map'|_ }}

    +

    {{ 'csv_map_title'|_ }}

    @@ -20,7 +20,9 @@
    - Download config for use again +

    + {{ 'csv_map_text'|_ }} +

    @@ -35,7 +37,7 @@
    -

    {{ columnName }}

    +

    {{ Config.get('csv.roles.'~columnName~'.name') }}

    @@ -69,11 +71,21 @@
    {% endfor %} -

    - -

    + + +
    +
    +
    +
    + {{ 'csv_go_back'|_ }} + +
    +
    +
    +
    + From 910510430357f45c780ae996d87a15f9fa3a4ab4 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 5 Jul 2015 08:47:16 +0200 Subject: [PATCH 10/10] Make CSV import a feature that can be turned on / off. --- app/Http/Controllers/CsvController.php | 6 +++++- config/firefly.php | 1 + resources/twig/partials/menu-sidebar.twig | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php index 03cfd5e63f..c56c7f1046 100644 --- a/app/Http/Controllers/CsvController.php +++ b/app/Http/Controllers/CsvController.php @@ -43,6 +43,10 @@ class CsvController extends Controller View::share('title', trans('firefly.csv')); View::share('mainTitleIcon', 'fa-file-text-o'); + if (Config::get('firefly.csv_import_enabled') === false) { + throw new FireflyException('CSV Import is not enabled.'); + } + $this->wizard = App::make('FireflyIII\Helpers\Csv\WizardInterface'); $this->data = App::make('FireflyIII\Helpers\Csv\Data'); @@ -170,7 +174,7 @@ class CsvController extends Controller $uploadPossible = is_writable(storage_path('upload')); $path = storage_path('upload'); - return view('csv.index', compact('subTitle', 'uploadPossible', 'path','unsupported')); + return view('csv.index', compact('subTitle', 'uploadPossible', 'path', 'unsupported')); } /** diff --git a/config/firefly.php b/config/firefly.php index e56f64e13e..81f741bd77 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -5,6 +5,7 @@ return [ 'version' => '3.4.6', 'index_periods' => ['1D', '1W', '1M', '3M', '6M', '1Y', 'custom'], 'budget_periods' => ['daily', 'weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], + 'csv_import_enabled' => false, 'piggy_bank_periods' => [ 'week' => 'Week', 'month' => 'Month', diff --git a/resources/twig/partials/menu-sidebar.twig b/resources/twig/partials/menu-sidebar.twig index 76bc4f4cfb..c3bfafa52c 100644 --- a/resources/twig/partials/menu-sidebar.twig +++ b/resources/twig/partials/menu-sidebar.twig @@ -123,9 +123,11 @@
  • {{ 'currencies'|_ }}
  • + {% if Config.get('firefly.csv_import_enabled') %}
  • {{ 'csv_import'|_ }}
  • + {% endif %}