Merge branch 'release/4.6.13'

This commit is contained in:
James Cole 2018-01-07 12:40:54 +01:00
commit 3562ec1f79
196 changed files with 8584 additions and 2110 deletions

14
.htaccess Normal file
View File

@ -0,0 +1,14 @@
# Optional: force HTTPS:
# <IfModule mod_rewrite.c>
# RewriteEngine On
# RewriteCond %{HTTPS} off
# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
# </IfModule>
# To hide directory listing
Options All -Indexes
# To prevent access to .env and other files
<Files .*>
Deny from all
</Files>

View File

@ -1,3 +1,21 @@
# 4.6.13
- [Issue 1074](https://github.com/firefly-iii/firefly-iii/issues/1074), suggested by [MacPaille](https://github.com/MacPaille)
- [Issue 1077](https://github.com/firefly-iii/firefly-iii/issues/1077), suggested by [wtercato](https://github.com/wtercato)
- Bulk edit of transactions thanks to [vicmosin](https://github.com/vicmosin) ([issue 1078](https://github.com/firefly-iii/firefly-iii/issues/1078))
- Support for Turkish.
- [Issue 1090](https://github.com/firefly-iii/firefly-iii/issues/1090), suggested by [Findus23](https://github.com/Findus23)
- [Issue 1097](https://github.com/firefly-iii/firefly-iii/issues/1097), suggested by [kelvinhammond](https://github.com/kelvinhammond)
- [Issue 1093](https://github.com/firefly-iii/firefly-iii/issues/1093), suggested by [jinformatique](https://github.com/jinformatique)
- [Issue 1098](https://github.com/firefly-iii/firefly-iii/issues/1098), suggested by [Nik-vr](https://github.com/Nik-vr)
- [Issue 972](https://github.com/firefly-iii/firefly-iii/issues/972), reported by [pjotrvdh](https://github.com/pjotrvdh)
- [Issue 1079](https://github.com/firefly-iii/firefly-iii/issues/1079), reported by [gavu](https://github.com/gavu)
- [Issue 1080](https://github.com/firefly-iii/firefly-iii/issues/1080), reported by [zjean](https://github.com/zjean)
- [Issue 1083](https://github.com/firefly-iii/firefly-iii/issues/1083), reported by [skuzzle](https://github.com/skuzzle)
- [Issue 1085](https://github.com/firefly-iii/firefly-iii/issues/1085), reported by [nicoschreiner](https://github.com/nicoschreiner)
- [Issue 1087](https://github.com/firefly-iii/firefly-iii/issues/1087), reported by [4oo4](https://github.com/4oo4)
- [Issue 1089](https://github.com/firefly-iii/firefly-iii/issues/1089), reported by [robin5210](https://github.com/robin5210)
- [Issue 1092](https://github.com/firefly-iii/firefly-iii/issues/1092), reported by [kelvinhammond](https://github.com/kelvinhammond)
- [Issue 1096](https://github.com/firefly-iii/firefly-iii/issues/1096), reported by [wtercato](https://github.com/wtercato)
# 4.6.12
- Support for Indonesian.

View File

@ -200,14 +200,22 @@ lib/x86_64-linux-gnu/libwrap.so.0.7.6
lib/x86_64-linux-gnu/libz.so.1
lib/x86_64-linux-gnu/libz.so.1.2.8
lib64/ld-linux-x86-64.so.2
opt/app/.dockerignore
opt/app/.env
opt/app/.env.docker
opt/app/.env.example
opt/app/.env.heroku
opt/app/.env.sandstorm
opt/app/.gitattributes
opt/app/.htaccess
opt/app/.sandstorm/.gitattributes
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/action_provision
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/action_set_name
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/creator_uid
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/id
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/index_uuid
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/private_key
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/synced_folders
opt/app/.sandstorm/.vagrant/machines/default/virtualbox/vagrant_cwd
opt/app/.sandstorm/Vagrantfile
opt/app/.sandstorm/app-graphics/firefly-iii-128.png
opt/app/.sandstorm/app-graphics/firefly-iii-150.png
@ -231,9 +239,7 @@ opt/app/.sandstorm/setup.sh
opt/app/.sandstorm/stack
opt/app/CHANGELOG.md
opt/app/CODE_OF_CONDUCT.md
opt/app/Dockerfile
opt/app/LICENSE
opt/app/Procfile
opt/app/README.md
opt/app/app.json
opt/app/app/Console/Commands/CreateExport.php
@ -359,6 +365,7 @@ opt/app/app/Http/Controllers/Chart/ReportController.php
opt/app/app/Http/Controllers/Chart/TagReportController.php
opt/app/app/Http/Controllers/Controller.php
opt/app/app/Http/Controllers/CurrencyController.php
opt/app/app/Http/Controllers/DebugController.php
opt/app/app/Http/Controllers/ExportController.php
opt/app/app/Http/Controllers/HelpController.php
opt/app/app/Http/Controllers/HomeController.php
@ -389,6 +396,7 @@ opt/app/app/Http/Controllers/RuleController.php
opt/app/app/Http/Controllers/RuleGroupController.php
opt/app/app/Http/Controllers/SearchController.php
opt/app/app/Http/Controllers/TagController.php
opt/app/app/Http/Controllers/Transaction/BulkController.php
opt/app/app/Http/Controllers/Transaction/ConvertController.php
opt/app/app/Http/Controllers/Transaction/LinkController.php
opt/app/app/Http/Controllers/Transaction/MassController.php
@ -416,6 +424,7 @@ opt/app/app/Http/Requests/AttachmentFormRequest.php
opt/app/app/Http/Requests/BillFormRequest.php
opt/app/app/Http/Requests/BudgetFormRequest.php
opt/app/app/Http/Requests/BudgetIncomeRequest.php
opt/app/app/Http/Requests/BulkEditJournalRequest.php
opt/app/app/Http/Requests/CategoryFormRequest.php
opt/app/app/Http/Requests/ConfigurationRequest.php
opt/app/app/Http/Requests/CurrencyFormRequest.php
@ -625,10 +634,21 @@ opt/app/app/Services/Github/Request/GithubRequest.php
opt/app/app/Services/Github/Request/UpdateRequest.php
opt/app/app/Services/Password/PwndVerifier.php
opt/app/app/Services/Password/Verifier.php
opt/app/app/Services/Spectre/Exception/DuplicatedCustomerException.php
opt/app/app/Services/Spectre/Exception/SpectreException.php
opt/app/app/Services/Spectre/Object/Account.php
opt/app/app/Services/Spectre/Object/Attempt.php
opt/app/app/Services/Spectre/Object/Customer.php
opt/app/app/Services/Spectre/Object/Holder.php
opt/app/app/Services/Spectre/Object/Login.php
opt/app/app/Services/Spectre/Object/SpectreObject.php
opt/app/app/Services/Spectre/Object/Token.php
opt/app/app/Services/Spectre/Object/Transaction.php
opt/app/app/Services/Spectre/Request/CreateTokenRequest.php
opt/app/app/Services/Spectre/Request/ListAccountsRequest.php
opt/app/app/Services/Spectre/Request/ListCustomersRequest.php
opt/app/app/Services/Spectre/Request/ListLoginsRequest.php
opt/app/app/Services/Spectre/Request/ListTransactionsRequest.php
opt/app/app/Services/Spectre/Request/NewCustomerRequest.php
opt/app/app/Services/Spectre/Request/SpectreRequest.php
opt/app/app/Support/Amount.php
@ -657,7 +677,8 @@ opt/app/app/Support/Import/Configuration/ConfigurationInterface.php
opt/app/app/Support/Import/Configuration/File/Initial.php
opt/app/app/Support/Import/Configuration/File/Map.php
opt/app/app/Support/Import/Configuration/File/Roles.php
opt/app/app/Support/Import/Configuration/File/Upload.php
opt/app/app/Support/Import/Configuration/File/UploadConfig.php
opt/app/app/Support/Import/Configuration/Spectre/HaveAccounts.php
opt/app/app/Support/Import/Information/BunqInformation.php
opt/app/app/Support/Import/Information/InformationInterface.php
opt/app/app/Support/Models/TransactionJournalTrait.php
@ -763,7 +784,6 @@ opt/app/config/session.php
opt/app/config/twigbridge.php
opt/app/config/upgrade.php
opt/app/config/view.php
opt/app/crowdin.yml
opt/app/database/factories/ModelFactory.php
opt/app/database/migrations/2016_06_16_000000_create_support_tables.php
opt/app/database/migrations/2016_06_16_000001_create_users_table.php
@ -786,8 +806,7 @@ opt/app/database/seeds/PermissionSeeder.php
opt/app/database/seeds/TransactionCurrencySeeder.php
opt/app/database/seeds/TransactionTypeSeeder.php
opt/app/docker-compose.yml
opt/app/nginx_app.conf
opt/app/phpunit.coverage.xml
opt/app/index.php
opt/app/public/.htaccess
opt/app/public/android-chrome-192x192.png
opt/app/public/android-chrome-512x512.png
@ -920,6 +939,7 @@ opt/app/public/js/ff/tags/create-edit.js
opt/app/public/js/ff/tags/index.js
opt/app/public/js/ff/tags/show.js
opt/app/public/js/ff/transactions/list.js
opt/app/public/js/ff/transactions/mass/edit-bulk.js
opt/app/public/js/ff/transactions/mass/edit.js
opt/app/public/js/ff/transactions/show.js
opt/app/public/js/ff/transactions/single/common.js
@ -935,8 +955,6 @@ opt/app/public/js/lib/bootstrap-tagsinput.min.js.map
opt/app/public/js/lib/bootstrap3-typeahead.min.js
opt/app/public/js/lib/daterangepicker.js
opt/app/public/js/lib/html5shiv.min.js
opt/app/public/js/lib/jquery-3.1.1.min.js
opt/app/public/js/lib/jquery-3.1.1.min.map
opt/app/public/js/lib/jquery-3.2.1.min.js
opt/app/public/js/lib/jquery-3.2.1.min.map
opt/app/public/js/lib/jquery-ui.min.js
@ -1074,6 +1092,20 @@ opt/app/resources/lang/pl_PL/list.php
opt/app/resources/lang/pl_PL/pagination.php
opt/app/resources/lang/pl_PL/passwords.php
opt/app/resources/lang/pl_PL/validation.php
opt/app/resources/lang/tr_TR/auth.php
opt/app/resources/lang/tr_TR/bank.php
opt/app/resources/lang/tr_TR/breadcrumbs.php
opt/app/resources/lang/tr_TR/config.php
opt/app/resources/lang/tr_TR/csv.php
opt/app/resources/lang/tr_TR/demo.php
opt/app/resources/lang/tr_TR/firefly.php
opt/app/resources/lang/tr_TR/form.php
opt/app/resources/lang/tr_TR/import.php
opt/app/resources/lang/tr_TR/intro.php
opt/app/resources/lang/tr_TR/list.php
opt/app/resources/lang/tr_TR/pagination.php
opt/app/resources/lang/tr_TR/passwords.php
opt/app/resources/lang/tr_TR/validation.php
opt/app/resources/stubs/binary.bin
opt/app/resources/stubs/csv.csv
opt/app/resources/stubs/demo-configuration.json
@ -1194,13 +1226,11 @@ opt/app/resources/views/import/bunq/prerequisites.twig
opt/app/resources/views/import/file/initial.twig
opt/app/resources/views/import/file/map.twig
opt/app/resources/views/import/file/roles.twig
opt/app/resources/views/import/file/upload.twig
opt/app/resources/views/import/file/upload-config.twig
opt/app/resources/views/import/index.twig
opt/app/resources/views/import/spectre/input-fields.twig
opt/app/resources/views/import/spectre/accounts.twig
opt/app/resources/views/import/spectre/prerequisites.twig
opt/app/resources/views/import/spectre/redirect.twig
opt/app/resources/views/import/spectre/select-country.twig
opt/app/resources/views/import/spectre/select-provider.twig
opt/app/resources/views/import/status.twig
opt/app/resources/views/index.twig
opt/app/resources/views/javascript/accounts.twig
@ -1219,6 +1249,7 @@ opt/app/resources/views/list/piggy-bank-events.twig
opt/app/resources/views/list/piggy-banks.twig
opt/app/resources/views/new-user/index.twig
opt/app/resources/views/partials/boxes.twig
opt/app/resources/views/partials/breadcrumbs.twig
opt/app/resources/views/partials/control-bar.twig
opt/app/resources/views/partials/empty.twig
opt/app/resources/views/partials/favicons.twig
@ -1297,10 +1328,11 @@ opt/app/resources/views/tags/edit.twig
opt/app/resources/views/tags/index.twig
opt/app/resources/views/tags/show.twig
opt/app/resources/views/test/test.twig
opt/app/resources/views/transactions/bulk/edit.twig
opt/app/resources/views/transactions/convert.twig
opt/app/resources/views/transactions/index.twig
opt/app/resources/views/transactions/links/delete.twig
opt/app/resources/views/transactions/mass-delete.twig
opt/app/resources/views/transactions/mass/delete.twig
opt/app/resources/views/transactions/mass/edit.twig
opt/app/resources/views/transactions/show.twig
opt/app/resources/views/transactions/single/create.twig
@ -2280,10 +2312,8 @@ opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/boots
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/none-stubs/app.js
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/react-stubs/Example.js
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/react-stubs/app.js
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/react-stubs/webpack.mix.js
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/vue-stubs/ExampleComponent.vue
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/vue-stubs/app.js
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Presets/vue-stubs/webpack.mix.js
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/ProviderMakeCommand.php
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/QueuedCommand.php
opt/app/vendor/laravel/framework/src/Illuminate/Foundation/Console/RequestMakeCommand.php
@ -2801,6 +2831,7 @@ opt/app/vendor/league/commonmark/src/Block/Element/Heading.php
opt/app/vendor/league/commonmark/src/Block/Element/HtmlBlock.php
opt/app/vendor/league/commonmark/src/Block/Element/IndentedCode.php
opt/app/vendor/league/commonmark/src/Block/Element/InlineContainer.php
opt/app/vendor/league/commonmark/src/Block/Element/InlineContainerInterface.php
opt/app/vendor/league/commonmark/src/Block/Element/ListBlock.php
opt/app/vendor/league/commonmark/src/Block/Element/ListData.php
opt/app/vendor/league/commonmark/src/Block/Element/ListItem.php
@ -2833,7 +2864,6 @@ opt/app/vendor/league/commonmark/src/Context.php
opt/app/vendor/league/commonmark/src/ContextInterface.php
opt/app/vendor/league/commonmark/src/Converter.php
opt/app/vendor/league/commonmark/src/Cursor.php
opt/app/vendor/league/commonmark/src/CursorState.php
opt/app/vendor/league/commonmark/src/Delimiter/Delimiter.php
opt/app/vendor/league/commonmark/src/Delimiter/DelimiterStack.php
opt/app/vendor/league/commonmark/src/DocParser.php
@ -3336,10 +3366,10 @@ opt/app/vendor/pragmarx/google2fa-laravel/src/config/config.php
opt/app/vendor/pragmarx/google2fa-laravel/tests/spec/Support/AuthenticatorSpec.php
opt/app/vendor/pragmarx/google2fa-laravel/upgrading.md
opt/app/vendor/pragmarx/google2fa/LICENSE
opt/app/vendor/pragmarx/google2fa/README.md
opt/app/vendor/pragmarx/google2fa/changelog.md
opt/app/vendor/pragmarx/google2fa/composer.json
opt/app/vendor/pragmarx/google2fa/docs/playground.jpg
opt/app/vendor/pragmarx/google2fa/readme.md
opt/app/vendor/pragmarx/google2fa/src/Exceptions/IncompatibleWithGoogleAuthenticatorException.php
opt/app/vendor/pragmarx/google2fa/src/Exceptions/InvalidCharactersException.php
opt/app/vendor/pragmarx/google2fa/src/Exceptions/SecretKeyTooShortException.php
@ -3348,6 +3378,7 @@ opt/app/vendor/pragmarx/google2fa/src/Support/Base32.php
opt/app/vendor/pragmarx/google2fa/src/Support/Constants.php
opt/app/vendor/pragmarx/google2fa/src/Support/QRCode.php
opt/app/vendor/pragmarx/google2fa/src/Support/Url.php
opt/app/vendor/pragmarx/google2fa/tests/Constants.php
opt/app/vendor/pragmarx/google2fa/tests/Google2FATest.php
opt/app/vendor/pragmarx/google2fa/tests/bootstrap.php
opt/app/vendor/pragmarx/google2fa/upgrading.md
@ -4049,6 +4080,8 @@ opt/app/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_7
opt/app/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_8.txt
opt/app/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_9.txt
opt/app/vendor/symfony/console/Tests/Fixtures/TestCommand.php
opt/app/vendor/symfony/console/Tests/Fixtures/TestTiti.php
opt/app/vendor/symfony/console/Tests/Fixtures/TestToto.php
opt/app/vendor/symfony/console/Tests/Fixtures/application_1.json
opt/app/vendor/symfony/console/Tests/Fixtures/application_1.md
opt/app/vendor/symfony/console/Tests/Fixtures/application_1.txt
@ -4335,6 +4368,7 @@ opt/app/vendor/symfony/debug/Tests/Fixtures/reallyNotPsr0.php
opt/app/vendor/symfony/debug/Tests/Fixtures2/RequiredTwice.php
opt/app/vendor/symfony/debug/Tests/HeaderMock.php
opt/app/vendor/symfony/debug/Tests/MockExceptionHandler.php
opt/app/vendor/symfony/debug/Tests/phpt/debug_class_loader.phpt
opt/app/vendor/symfony/debug/Tests/phpt/exception_rethrown.phpt
opt/app/vendor/symfony/debug/Tests/phpt/fatal_with_nested_handlers.phpt
opt/app/vendor/symfony/debug/composer.json
@ -4751,6 +4785,7 @@ opt/app/vendor/symfony/http-kernel/Tests/EventListener/LocaleListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/ProfilerListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/ResponseListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/RouterListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/SaveSessionListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/SurrogateListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/TestSessionListenerTest.php
opt/app/vendor/symfony/http-kernel/Tests/EventListener/TranslatorListenerTest.php
@ -6161,7 +6196,6 @@ opt/app/vendor/watson/validating/src/ValidatingModel.php
opt/app/vendor/watson/validating/src/ValidatingObserver.php
opt/app/vendor/watson/validating/src/ValidatingTrait.php
opt/app/vendor/watson/validating/src/ValidationException.php
opt/app/webpack.mix.js
proc/cpuinfo
sandstorm-http-bridge
sandstorm-http-bridge-config

View File

@ -15,8 +15,8 @@ const pkgdef :Spk.PackageDefinition = (
manifest = (
appTitle = (defaultText = "Firefly III"),
appVersion = 6,
appMarketingVersion = (defaultText = "4.6.12"),
appVersion = 7,
appMarketingVersion = (defaultText = "4.6.13"),
actions = [
# Define your "new document" handlers here.

View File

@ -2,6 +2,28 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [4.6.13] - 2018-01-06
### Added
- [Issue 1074](https://github.com/firefly-iii/firefly-iii/issues/1074), suggested by [MacPaille](https://github.com/MacPaille)
- [Issue 1077](https://github.com/firefly-iii/firefly-iii/issues/1077), suggested by [wtercato](https://github.com/wtercato)
- Bulk edit of transactions thanks to [vicmosin](https://github.com/vicmosin) ([issue 1078](https://github.com/firefly-iii/firefly-iii/issues/1078))
- Support for Turkish.
- [Issue 1090](https://github.com/firefly-iii/firefly-iii/issues/1090), suggested by [Findus23](https://github.com/Findus23)
- [Issue 1097](https://github.com/firefly-iii/firefly-iii/issues/1097), suggested by [kelvinhammond](https://github.com/kelvinhammond)
- [Issue 1093](https://github.com/firefly-iii/firefly-iii/issues/1093), suggested by [jinformatique](https://github.com/jinformatique)
- [Issue 1098](https://github.com/firefly-iii/firefly-iii/issues/1098), suggested by [Nik-vr](https://github.com/Nik-vr)
### Fixed
- [Issue 972](https://github.com/firefly-iii/firefly-iii/issues/972), reported by [pjotrvdh](https://github.com/pjotrvdh)
- [Issue 1079](https://github.com/firefly-iii/firefly-iii/issues/1079), reported by [gavu](https://github.com/gavu)
- [Issue 1080](https://github.com/firefly-iii/firefly-iii/issues/1080), reported by [zjean](https://github.com/zjean)
- [Issue 1083](https://github.com/firefly-iii/firefly-iii/issues/1083), reported by [skuzzle](https://github.com/skuzzle)
- [Issue 1085](https://github.com/firefly-iii/firefly-iii/issues/1085), reported by [nicoschreiner](https://github.com/nicoschreiner)
- [Issue 1087](https://github.com/firefly-iii/firefly-iii/issues/1087), reported by [4oo4](https://github.com/4oo4)
- [Issue 1089](https://github.com/firefly-iii/firefly-iii/issues/1089), reported by [robin5210](https://github.com/robin5210)
- [Issue 1092](https://github.com/firefly-iii/firefly-iii/issues/1092), reported by [kelvinhammond](https://github.com/kelvinhammond)
- [Issue 1096](https://github.com/firefly-iii/firefly-iii/issues/1096), reported by [wtercato](https://github.com/wtercato)
## [4.6.12] - 2017-12-31
### Added
- Support for Indonesian.
@ -16,7 +38,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- [Issue 1047](https://github.com/firefly-iii/firefly-iii/issues/1047), as reported by [pkoziol](https://github.com/pkoziol)
- [Issue 1048](https://github.com/firefly-iii/firefly-iii/issues/1048), as reported by [webence](https://github.com/webence)
- [Issue 1049](https://github.com/firefly-iii/firefly-iii/issues/1049), as reported by [nicoschreiner](https://github.com/nicoschreiner)
- [Issue 1015](https://github.com/firefly-iii/firefly-iii/issues/1015), as reporterd by a user on Tweakers.net
- [Issue 1015](https://github.com/firefly-iii/firefly-iii/issues/1015), as reported by a user on Tweakers.net
- [Issue 1056](https://github.com/firefly-iii/firefly-iii/issues/1056), as reported by [repercussion](https://github.com/repercussion)
- [Issue 1061](https://github.com/firefly-iii/firefly-iii/issues/1061), as reported by [Meizikyn](https://github.com/Meizikyn)
- [Issue 1045](https://github.com/firefly-iii/firefly-iii/issues/1045), as reported by [gavu](https://github.com/gavu)
@ -53,7 +75,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Reconciliation of accounts ([issue 736](https://github.com/firefly-iii/firefly-iii/issues/736)), as requested by [kristophr](https://github.com/kristophr) and several others
### Changed
- Extended currency list, as suggested by @emuhendis in [issue 994](https://github.com/firefly-iii/firefly-iii/issues/994)
- Extended currency list, as suggested by [emuhendis](https://github.com/emuhendis) in [issue 994](https://github.com/firefly-iii/firefly-iii/issues/994)
- [Issue 996](https://github.com/firefly-iii/firefly-iii/issues/996) as suggested by [dp87](https://github.com/dp87)
### Removed
@ -78,7 +100,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- [Issue 1025](https://github.com/firefly-iii/firefly-iii/issues/1025), reported by [gavu](https://github.com/gavu)
## [4.6.10] - 2017-11-xx
## [4.6.10] - 2017-11-03
### Added
- Greatly expanded Docker support thanks to [alazare619](https://github.com/alazare619)
- [Issue 967](https://github.com/firefly-iii/firefly-iii/issues/967), thanks to [Aquariu](https://github.com/Aquariu)
@ -353,8 +375,8 @@ This will be the last release to support PHP 7.0.
- Can now make rules for attachments, see [issue 608](https://github.com/firefly-iii/firefly-iii/issues/608), as suggested by [dzaikos](https://github.com/dzaikos).
### Fixed
- Fixed [issue 629](https://github.com/firefly-iii/firefly-iii/issues/629), reported by @forcaeluz
- Fixed [issue 630](https://github.com/firefly-iii/firefly-iii/issues/630), reported by @welbert
- Fixed [issue 629](https://github.com/firefly-iii/firefly-iii/issues/629), reported by [forcaeluz](https://github.com/forcaeluz)
- Fixed [issue 630](https://github.com/firefly-iii/firefly-iii/issues/630), reported by [welbert](https://github.com/welbert)
- And more various bug fixes.
## [4.3.8] - 2017-04-08

View File

@ -37,6 +37,10 @@ Register for a free Heroku account and instantly run Firefly III on your very ow
You can find Firefly III in [the Sandstorm.io marketplace](https://apps.sandstorm.io/app/uws252ya9mep4t77tevn85333xzsgrpgth8q4y1rhknn1hammw70). You can run it on your own installation or on Oasis.
### Other options
Firefly III is also available as a package on [https://softaculous.com/](Softaculous) and [AMPPS](https://www.ampps.com/).
## More about Firefly III
Personal financial management is pretty difficult, and everybody has their own approach to it.

View File

@ -146,6 +146,7 @@ class UpgradeDatabase extends Command
// both 0? set to default currency:
if (0 === $accountCurrency && 0 === $obCurrency) {
AccountMeta::where('account_id', $account->id)->where('name', 'currency_id')->forceDelete();
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
@ -218,7 +219,7 @@ class UpgradeDatabase extends Command
}
// when mismatch in transaction:
if ($transaction->transaction_currency_id !== $currency->id) {
if (!(intval($transaction->transaction_currency_id) === intval($currency->id))) {
$transaction->foreign_currency_id = $transaction->transaction_currency_id;
$transaction->foreign_amount = $transaction->amount;
$transaction->transaction_currency_id = $currency->id;
@ -401,24 +402,26 @@ class UpgradeDatabase extends Command
// has no currency ID? Must have, so fill in using account preference:
if (null === $transaction->transaction_currency_id) {
$transaction->transaction_currency_id = $currency->id;
$transaction->transaction_currency_id = intval($currency->id);
Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $transaction->id, $currency->code));
$transaction->save();
}
// does not match the source account (see above)? Can be fixed
// when mismatch in transaction and NO foreign amount is set:
if ($transaction->transaction_currency_id !== $currency->id && null === $transaction->foreign_amount) {
if (!(intval($transaction->transaction_currency_id) === intval($currency->id)) && null === $transaction->foreign_amount) {
Log::debug(
sprintf(
'Transaction #%d has a currency setting (#%d) that should be #%d. Amount remains %s, currency is changed.',
'Transaction #%d has a currency setting (#%d) (%s) that should be #%d (%s). Amount remains %s, currency is changed.',
$transaction->id,
$transaction->transaction_currency_id,
$this->var_dump_ret(intval($transaction->transaction_currency_id)),
$currency->id,
$this->var_dump_ret(intval($currency->id)),
$transaction->amount
)
);
$transaction->transaction_currency_id = $currency->id;
$transaction->transaction_currency_id = intval($currency->id);
$transaction->save();
}
@ -436,7 +439,7 @@ class UpgradeDatabase extends Command
}
// if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions:
if ($opposingCurrency->id === $currency->id) {
if (intval($opposingCurrency->id) === intval($currency->id)) {
// update both transactions to match:
$transaction->foreign_amount = null;
$transaction->foreign_currency_id = null;
@ -450,7 +453,7 @@ class UpgradeDatabase extends Command
return;
}
// if destination account currency is different, both transactions must have this currency as foreign currency id.
if ($opposingCurrency->id !== $currency->id) {
if (!(intval($opposingCurrency->id) === intval($currency->id))) {
$transaction->foreign_currency_id = $opposingCurrency->id;
$opposing->foreign_currency_id = $opposingCurrency->id;
$transaction->save();
@ -500,4 +503,20 @@ class UpgradeDatabase extends Command
return;
}
/**
* @param null $mixed
*
* @return string
*/
private function var_dump_ret($mixed = null): string
{
ob_start();
var_dump($mixed);
$content = ob_get_contents();
ob_end_clean();
return trim($content);
}
}

View File

@ -773,7 +773,8 @@ class JournalCollector implements JournalCollectorInterface
$this->query->leftJoin('category_transaction', 'category_transaction.transaction_id', '=', 'transactions.id');
$this->query->leftJoin('categories as transaction_categories', 'transaction_categories.id', '=', 'category_transaction.category_id');
$this->query->whereNull('transaction_journal_categories.deleted_at');
$this->query->whereNull('transaction_categories.deleted_at');
$this->fields[] = 'category_transaction_journal.category_id as transaction_journal_category_id';
$this->fields[] = 'transaction_journal_categories.encrypted as transaction_journal_category_encrypted';
$this->fields[] = 'transaction_journal_categories.name as transaction_journal_category_name';

View File

@ -29,6 +29,7 @@ use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Requests\AccountFormRequest;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Note;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@ -156,14 +157,14 @@ class AccountController extends Controller
* @throws FireflyException
* @throws FireflyException
*/
public function edit(Request $request, Account $account)
public function edit(Request $request, Account $account, AccountRepositoryInterface $repository)
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
/** @var CurrencyRepositoryInterface $currencyRepos */
$currencyRepos = app(CurrencyRepositoryInterface::class);
$what = config('firefly.shortNamesByFullName')[$account->accountType->type];
$subTitle = trans('firefly.edit_' . $what . '_account', ['name' => $account->name]);
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $what);
$allCurrencies = $repository->get();
$allCurrencies = $currencyRepos->get();
$currencySelectList = ExpandedForm::makeSelectList($allCurrencies);
$roles = [];
foreach (config('firefly.accountRoles') as $role) {
@ -183,7 +184,7 @@ class AccountController extends Controller
$openingBalanceAmount = '0' === $account->getOpeningBalanceAmount() ? '' : $openingBalanceAmount;
$openingBalanceDate = $account->getOpeningBalanceDate();
$openingBalanceDate = 1900 === $openingBalanceDate->year ? null : $openingBalanceDate->format('Y-m-d');
$currency = $repository->find(intval($account->getMeta('currency_id')));
$currency = $currencyRepos->find(intval($account->getMeta('currency_id')));
$preFilled = [
'accountNumber' => $account->getMeta('accountNumber'),
@ -195,7 +196,15 @@ class AccountController extends Controller
'openingBalance' => $openingBalanceAmount,
'virtualBalance' => $account->virtual_balance,
'currency_id' => $currency->id,
'notes' => '',
];
/** @var Note $note */
$note = $repository->getNote($account);
if (null !== $note) {
$preFilled['notes'] = $note->text;
}
$request->session()->flash('preFilled', $preFilled);
return view(

View File

@ -22,8 +22,13 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Auth;
use FireflyConfig;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
/**
* Class ForgotPasswordController
@ -51,4 +56,57 @@ class ForgotPasswordController extends Controller
parent::__construct();
$this->middleware('guest');
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
*
* @param UserRepositoryInterface $repository
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function sendResetLinkEmail(Request $request, UserRepositoryInterface $repository)
{
$this->validateEmail($request);
// verify if the user is not a demo user. If so, we give him back an error.
$user = User::where('email', $request->get('email'))->first();
if (!is_null($user) && $repository->hasRole($user, 'demo')) {
return back()->withErrors(['email' => trans('firefly.cannot_reset_demo_user')]);
}
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
if ($response == Password::RESET_LINK_SENT) {
return back()->with('status', trans($response));
}
return back()->withErrors(['email' => trans($response)]); // @codeCoverageIgnore
}
/**
* @codeCoverageIgnore
* Display the form to request a password reset link.
*
* @return \Illuminate\Http\Response
*/
public function showLinkRequestForm()
{
// is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
$allowRegistration = true;
if (true === $singleUserMode && $userCount > 0) {
$allowRegistration = false;
}
return view('auth.passwords.email')->with(compact('allowRegistration'));
}
}

View File

@ -22,8 +22,11 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Auth;
use FireflyConfig;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\User;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
/**
* @codeCoverageIgnore
@ -52,4 +55,28 @@ class ResetPasswordController extends Controller
parent::__construct();
$this->middleware('guest');
}
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* @param Request $request
* @param string|null $token
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showResetForm(Request $request, $token = null)
{
// is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
$allowRegistration = true;
if (true === $singleUserMode && $userCount > 0) {
$allowRegistration = false;
}
return view('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email, 'allowRegistration' => $allowRegistration]
);
}
}

View File

@ -0,0 +1,159 @@
<?php
/**
* DebugController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Carbon\Carbon;
use DB;
use Illuminate\Http\Request;
use Log;
use Monolog\Handler\RotatingFileHandler;
/**
* Class DebugController
*/
class DebugController extends Controller
{
/**
* @param Request $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index(Request $request)
{
$search = ['~', '#'];
$replace = ['\~', '# '];
$phpVersion = str_replace($search, $replace, PHP_VERSION);
$phpOs = str_replace($search, $replace, php_uname());
$interface = PHP_SAPI;
$now = Carbon::create()->format('Y-m-d H:i:s e');
$extensions = join(', ', get_loaded_extensions());
$drivers = join(', ', DB::availableDrivers());
$currentDriver = DB::getDriverName();
$userAgent = $request->header('user-agent');
$isSandstorm = var_export(env('IS_SANDSTORM', 'unknown'), true);
$isDocker = var_export(env('IS_DOCKER', 'unknown'), true);
$trustedProxies = env('TRUSTED_PROXIES', '(none)');
$displayErrors = ini_get('display_errors');
$errorReporting = $this->errorReporting(intval(ini_get('error_reporting')));
$appEnv = env('APP_ENV', '');
$appDebug = var_export(env('APP_DEBUG', false), true);
$appLog = env('APP_LOG', '');
$appLogLevel = env('APP_LOG_LEVEL', '');
$packages = $this->collectPackages();
$cacheDriver = env('CACHE_DRIVER', 'unknown');
// get latest log file:
$logger = Log::getMonolog();
$handlers = $logger->getHandlers();
$logContent = '';
foreach ($handlers as $handler) {
if ($handler instanceof RotatingFileHandler) {
$logFile = $handler->getUrl();
if (null !== $logFile) {
$logContent = file_get_contents($logFile);
}
}
}
// last few lines
$logContent = 'Truncated from this point <----|' . substr($logContent, -4096);
return view(
'debug',
compact(
'phpVersion',
'extensions',
'carbon',
'appEnv',
'appDebug',
'appLog',
'appLogLevel',
'now',
'packages',
'drivers',
'currentDriver',
'userAgent',
'displayErrors',
'errorReporting',
'phpOs',
'interface',
'logContent',
'cacheDriver',
'isDocker',
'isSandstorm',
'trustedProxies'
)
);
}
/**
* Some common combinations.
*
* @param int $value
*
* @return string
*/
protected function errorReporting(int $value): string
{
$array = [
-1 => 'ALL errors',
E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED => 'E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED',
E_ALL => 'E_ALL',
E_ALL & ~E_DEPRECATED & ~E_STRICT => 'E_ALL & ~E_DEPRECATED & ~E_STRICT',
E_ALL & ~E_NOTICE => 'E_ALL & ~E_NOTICE',
E_ALL & ~E_NOTICE & ~E_STRICT => 'E_ALL & ~E_NOTICE & ~E_STRICT',
E_COMPILE_ERROR | E_RECOVERABLE_ERROR | E_ERROR | E_CORE_ERROR => 'E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR',
];
if (isset($array[$value])) {
return $array[$value];
}
return strval($value); // @codeCoverageIgnore
}
/**
* @return array
*/
private function collectPackages(): array
{
$packages = [];
$file = realpath(__DIR__ . '/../../../vendor/composer/installed.json');
if (!($file === false) && file_exists($file)) {
// file exists!
$content = file_get_contents($file);
$json = json_decode($content, true);
foreach ($json as $package) {
$packages[]
= [
'name' => $package['name'],
'version' => $package['version'],
];
}
}
return $packages;
}
}

View File

@ -24,7 +24,6 @@ namespace FireflyIII\Http\Controllers;
use Artisan;
use Carbon\Carbon;
use DB;
use Exception;
use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Exceptions\FireflyException;
@ -38,7 +37,6 @@ use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Collection;
use Log;
use Monolog\Handler\RotatingFileHandler;
use Preferences;
use Response;
use Route as RouteFacade;
@ -98,59 +96,6 @@ class HomeController extends Controller
return Response::json(['ok' => 'ok']);
}
/**
* @param Request $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function displayDebug(Request $request)
{
$phpVersion = str_replace('~', '\~', PHP_VERSION);
$phpOs = php_uname();
$interface = PHP_SAPI;
$now = Carbon::create()->format('Y-m-d H:i:s e');
$extensions = join(', ', get_loaded_extensions());
$drivers = join(', ', DB::availableDrivers());
$currentDriver = DB::getDriverName();
$userAgent = $request->header('user-agent');
$isSandstorm = var_export(env('IS_SANDSTORM', 'unknown'), true);
$isDocker = var_export(env('IS_DOCKER', 'unknown'), true);
$trustedProxies = env('TRUSTED_PROXIES', '(none)');
// get latest log file:
$logger = Log::getMonolog();
$handlers = $logger->getHandlers();
$logContent = '';
foreach ($handlers as $handler) {
if ($handler instanceof RotatingFileHandler) {
$logFile = $handler->getUrl();
if (null !== $logFile) {
$logContent = file_get_contents($logFile);
}
}
}
// last few lines
$logContent = 'Truncated from this point <----|' . substr($logContent, -4096);
return view(
'debug',
compact(
'phpVersion',
'extensions',
'carbon',
'now',
'drivers',
'currentDriver',
'userAgent',
'phpOs',
'interface',
'logContent',
'isDocker',
'isSandstorm',
'trustedProxies'
)
);
}
/**
* @throws FireflyException

View File

@ -78,7 +78,9 @@ class ConfigurationController extends Controller
return redirect(route('import.status', [$job->key]));
}
$this->repository->updateStatus($job, 'configuring');
$view = $configurator->getNextView();
$data = $configurator->getNextData();
$subTitle = trans('firefly.import_config_bread_crumb');
@ -135,6 +137,7 @@ class ConfigurationController extends Controller
if (null === $className || !class_exists($className)) {
throw new FireflyException(sprintf('Cannot find configurator class for job of type "%s".', $type)); // @codeCoverageIgnore
}
Log::debug(sprintf('Going to create class "%s"', $className));
/** @var ConfiguratorInterface $configurator */
$configurator = app($className);
$configurator->setJob($job);

View File

@ -0,0 +1,178 @@
<?php
/**
* BulkController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use ExpandedForm;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\BulkEditJournalRequest;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Log;
use Preferences;
use Session;
use View;
/**
* Class BulkController
*/
class BulkController extends Controller
{
/**
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
app('view')->share('title', trans('firefly.transactions'));
app('view')->share('mainTitleIcon', 'fa-repeat');
return $next($request);
}
);
}
/**
* @param Collection $journals
*
* @return View
*/
public function edit(Request $request, Collection $journals)
{
$subTitle = trans('firefly.mass_bulk_journals');
// skip transactions that have multiple destinations, multiple sources or are an opening balance.
$filtered = new Collection;
$messages = [];
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$sources = $journal->sourceAccountList();
$destinations = $journal->destinationAccountList();
if ($sources->count() > 1) {
$messages[] = trans('firefly.cannot_edit_multiple_source', ['description' => $journal->description, 'id' => $journal->id]);
continue;
}
if ($destinations->count() > 1) {
$messages[] = trans('firefly.cannot_edit_multiple_dest', ['description' => $journal->description, 'id' => $journal->id]);
continue;
}
if (TransactionType::OPENING_BALANCE === $journal->transactionType->type) {
$messages[] = trans('firefly.cannot_edit_opening_balance');
continue;
}
// cannot edit reconciled transactions / journals:
if ($journal->transactions->first()->reconciled) {
$messages[] = trans('firefly.cannot_edit_reconciled', ['description' => $journal->description, 'id' => $journal->id]);
continue;
}
$filtered->push($journal);
}
if (count($messages) > 0) {
$request->session()->flash('info', $messages);
}
// put previous url in session
$this->rememberPreviousUri('transactions.bulk-edit.uri');
// get list of budgets:
/** @var BudgetRepositoryInterface $repository */
$repository = app(BudgetRepositoryInterface::class);
$budgetList = ExpandedForm::makeSelectListWithEmpty($repository->getActiveBudgets());
// collect some useful meta data for the mass edit:
$filtered->each(
function (TransactionJournal $journal) {
$journal->transaction_count = $journal->transactions()->count();
}
);
if (0 === $filtered->count()) {
$request->session()->flash('error', trans('firefly.no_edit_multiple_left'));
}
$journals = $filtered;
return view('transactions.bulk.edit', compact('journals', 'subTitle', 'budgetList'));
}
/**
* @param BulkEditJournalRequest $request
* @param JournalRepositoryInterface $repository
*
* @return mixed
*/
public function update(BulkEditJournalRequest $request, JournalRepositoryInterface $repository)
{
$journalIds = $request->get('journals');
$ignoreCategory = intval($request->get('ignore_category')) === 1;
$ignoreBudget = intval($request->get('ignore_budget')) === 1;
$ignoreTags = intval($request->get('ignore_tags')) === 1;
$count = 0;
if (is_array($journalIds)) {
foreach ($journalIds as $journalId) {
$journal = $repository->find(intval($journalId));
if (!is_null($journal)) {
$count++;
Log::debug(sprintf('Found journal #%d', $journal->id));
// update category if not told to ignore
if ($ignoreCategory === false) {
Log::debug(sprintf('Set category to %s', $request->string('category')));
$repository->updateCategory($journal, $request->string('category'));
}
// update budget if not told to ignore (and is withdrawal)
if ($ignoreBudget === false) {
Log::debug(sprintf('Set budget to %d', $request->integer('budget_id')));
$repository->updateBudget($journal, $request->integer('budget_id'));
}
if ($ignoreTags === false) {
Log::debug(sprintf('Set tags to %s', $request->string('budget_id')));
$repository->updateTags($journal, explode(',', $request->string('tags')));
}
// update tags if not told to ignore (and is withdrawal)
}
}
}
Preferences::mark();
$request->session()->flash('success', trans('firefly.mass_edited_transactions_success', ['amount' => $count]));
// redirect to previous URL:
return redirect($this->getPreviousUri('transactions.bulk-edit.uri'));
}
}

View File

@ -26,6 +26,7 @@ use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\MassDeleteJournalRequest;
use FireflyIII\Http\Requests\MassEditJournalRequest;
use FireflyIII\Http\Requests\MassEditBulkJournalRequest;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
@ -71,7 +72,7 @@ class MassController extends Controller
// put previous url in session
$this->rememberPreviousUri('transactions.mass-delete.uri');
return view('transactions.mass-delete', compact('journals', 'subTitle'));
return view('transactions.mass.delete', compact('journals', 'subTitle'));
}
/**
@ -131,7 +132,7 @@ class MassController extends Controller
// skip transactions that have multiple destinations, multiple sources or are an opening balance.
$filtered = new Collection;
$messages = [];
// @var TransactionJournal
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$sources = $journal->sourceAccountList();
$destinations = $journal->destinationAccountList();
@ -213,7 +214,7 @@ class MassController extends Controller
if (is_array($journalIds)) {
foreach ($journalIds as $journalId) {
$journal = $repository->find(intval($journalId));
if ($journal) {
if (!is_null($journal)) {
// get optional fields:
$what = strtolower($journal->transactionTypeStr());
$sourceAccountId = $request->get('source_account_id')[$journal->id] ?? 0;
@ -264,4 +265,5 @@ class MassController extends Controller
// redirect to previous URL:
return redirect($this->getPreviousUri('transactions.mass-edit.uri'));
}
}

View File

@ -57,6 +57,7 @@ class AccountFormRequest extends Request
'openingBalanceDate' => $this->date('openingBalanceDate'),
'ccType' => $this->string('ccType'),
'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'),
'notes' => $this->string('notes'),
];
}

View File

@ -0,0 +1,50 @@
<?php
/**
* BulkEditJournalRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Requests;
/**
* Class MassEditBulkJournalRequest.
*/
class BulkEditJournalRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only allow logged in users
return auth()->check();
}
/**
* @return array
*/
public function rules()
{
// fixed
return [
'journals.*' => 'required|belongsToUser:transaction_journals,id',
];
}
}

View File

@ -91,33 +91,33 @@ class JournalFormRequest extends Request
{
$what = $this->get('what');
$rules = [
'what' => 'required|in:withdrawal,deposit,transfer',
'date' => 'required|date',
'what' => 'required|in:withdrawal,deposit,transfer',
'date' => 'required|date',
'amount_currency_id_amount' => 'exists:transaction_currencies,id|required',
// then, custom fields:
'interest_date' => 'date|nullable',
'book_date' => 'date|nullable',
'process_date' => 'date|nullable',
'due_date' => 'date|nullable',
'payment_date' => 'date|nullable',
'invoice_date' => 'date|nullable',
'internal_reference' => 'min:1,max:255|nullable',
'notes' => 'min:1,max:50000|nullable',
'interest_date' => 'date|nullable',
'book_date' => 'date|nullable',
'process_date' => 'date|nullable',
'due_date' => 'date|nullable',
'payment_date' => 'date|nullable',
'invoice_date' => 'date|nullable',
'internal_reference' => 'min:1,max:255|nullable',
'notes' => 'min:1,max:50000|nullable',
// and then transaction rules:
'description' => 'required|between:1,255',
'amount' => 'numeric|required|more:0',
'budget_id' => 'mustExist:budgets,id|belongsToUser:budgets,id|nullable',
'category' => 'between:1,255|nullable',
'source_account_id' => 'numeric|belongsToUser:accounts,id|nullable',
'source_account_name' => 'between:1,255|nullable',
'destination_account_id' => 'numeric|belongsToUser:accounts,id|nullable',
'destination_account_name' => 'between:1,255|nullable',
'piggy_bank_id' => 'between:1,255|nullable',
'description' => 'required|between:1,255',
'amount' => 'numeric|required|more:0',
'budget_id' => 'mustExist:budgets,id|belongsToUser:budgets,id|nullable',
'category' => 'between:1,255|nullable',
'source_account_id' => 'numeric|belongsToUser:accounts,id|nullable',
'source_account_name' => 'between:1,255|nullable',
'destination_account_id' => 'numeric|belongsToUser:accounts,id|nullable',
'destination_account_name' => 'between:1,255|nullable',
'piggy_bank_id' => 'between:1,255|nullable',
// foreign currency amounts
'native_amount' => 'numeric|more:0|nullable',
'source_amount' => 'numeric|more:0|nullable',
'destination_amount' => 'numeric|more:0|nullable',
'native_amount' => 'numeric|more:0|nullable',
'source_amount' => 'numeric|more:0|nullable',
'destination_amount' => 'numeric|more:0|nullable',
];
// some rules get an upgrade depending on the type of data:

View File

@ -143,7 +143,7 @@ class Request extends FormRequest
*
* @return int
*/
protected function integer(string $field): int
public function integer(string $field): int
{
return intval($this->get($field));
}

View File

@ -24,11 +24,12 @@ namespace FireflyIII\Import\Configuration;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use FireflyIII\Support\Import\Configuration\File\Initial;
use FireflyIII\Support\Import\Configuration\File\Map;
use FireflyIII\Support\Import\Configuration\File\Roles;
use FireflyIII\Support\Import\Configuration\File\Upload;
use FireflyIII\Support\Import\Configuration\File\UploadConfig;
use Log;
/**
@ -36,17 +37,41 @@ use Log;
*/
class FileConfigurator implements ConfiguratorInterface
{
/** @var array */
private $defaultConfig
= [
'stage' => 'initial',
'has-headers' => false, // assume
'date-format' => 'Ymd', // assume
'delimiter' => ',', // assume
'import-account' => 0, // none,
'specifics' => [], // none
'column-count' => 0, // unknown
'column-roles' => [], // unknown
'column-do-mapping' => [], // not yet set which columns must be mapped
'column-mapping-config' => [], // no mapping made yet.
'file-type' => 'csv', // assume
'has-config-file' => true,
'apply-rules' => true,
'match-bills' => false,
'auto-start' => false,
];
/** @var ImportJob */
private $job;
/** @var ImportJobRepositoryInterface */
private $repository;
// give job default config:
/** @var string */
private $warning = '';
/**
* ConfiguratorInterface constructor.
* FileConfigurator constructor.
*/
public function __construct()
{
Log::debug('Created FileConfigurator');
}
/**
@ -60,10 +85,13 @@ class FileConfigurator implements ConfiguratorInterface
*/
public function configureJob(array $data): bool
{
if (is_null($this->job)) {
throw new FireflyException('Cannot call configureJob() without a job.');
}
$class = $this->getConfigurationClass();
$job = $this->job;
/** @var ConfigurationInterface $object */
$object = new $class($this->job);
$object = app($class);
$object->setJob($job);
$result = $object->storeConfiguration($data);
$this->warning = $object->getWarningMessage();
@ -80,6 +108,10 @@ class FileConfigurator implements ConfiguratorInterface
*/
public function getNextData(): array
{
if (is_null($this->job)) {
throw new FireflyException('Cannot call getNextData() without a job.');
}
$class = $this->getConfigurationClass();
$job = $this->job;
/** @var ConfigurationInterface $object */
@ -96,52 +128,56 @@ class FileConfigurator implements ConfiguratorInterface
*/
public function getNextView(): string
{
if (!$this->job->configuration['has-file-upload']) {
return 'import.file.upload';
if (is_null($this->job)) {
throw new FireflyException('Cannot call getNextView() without a job.');
}
if (!$this->job->configuration['initial-config-complete']) {
return 'import.file.initial';
$config = $this->getConfig();
$stage = $config['stage'] ?? 'initial';
switch ($stage) {
case 'initial': // has nothing, no file upload or anything.
return 'import.file.initial';
case 'upload-config': // has file, needs file config.
return 'import.file.upload-config';
case 'roles': // has configured file, needs roles.
return 'import.file.roles';
case 'map': // has roles, needs mapping.
return 'import.file.map';
}
if (!$this->job->configuration['column-roles-complete']) {
return 'import.file.roles';
}
if (!$this->job->configuration['column-mapping-complete']) {
return 'import.file.map';
}
throw new FireflyException('No view for state');
throw new FireflyException(sprintf('No view for stage "%s"', $stage));
}
/**
* Return possible warning to user.
*
* @return string
* @throws FireflyException
*/
public function getWarningMessage(): string
{
if (is_null($this->job)) {
throw new FireflyException('Cannot call getWarningMessage() without a job.');
}
return $this->warning;
}
/**
* @return bool
* @throws FireflyException
*/
public function isJobConfigured(): bool
{
$config = $this->job->configuration;
$config['has-file-upload'] = $config['has-file-upload'] ?? false;
$config['initial-config-complete'] = $config['initial-config-complete'] ?? false;
$config['column-roles-complete'] = $config['column-roles-complete'] ?? false;
$config['column-mapping-complete'] = $config['column-mapping-complete'] ?? false;
$this->job->configuration = $config;
$this->job->save();
if (is_null($this->job)) {
throw new FireflyException('Cannot call isJobConfigured() without a job.');
}
$config = $this->getConfig();
$stage = $config['stage'] ?? 'initial';
if ($stage === 'ready') {
Log::debug('isJobConfigured returns true');
if ($config['initial-config-complete']
&& $config['column-roles-complete']
&& $config['column-mapping-complete']
&& $config['has-file-upload']
) {
return true;
}
Log::debug('isJobConfigured returns false');
return false;
}
@ -151,12 +187,24 @@ class FileConfigurator implements ConfiguratorInterface
*/
public function setJob(ImportJob $job)
{
$this->job = $job;
if (null === $this->job->configuration || 0 === count($this->job->configuration)) {
Log::debug(sprintf('Gave import job %s initial configuration.', $this->job->key));
$this->job->configuration = config('csv.default_config');
$this->job->save();
}
Log::debug(sprintf('FileConfigurator::setJob(#%d: %s)', $job->id, $job->key));
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($job->user);
$config = $this->getConfig();
$newConfig = array_merge($this->defaultConfig, $config);
$this->repository->setConfiguration($job, $newConfig);
}
/**
* Short hand method.
*
* @return array
*/
private function getConfig(): array
{
return $this->repository->getConfiguration($this->job);
}
/**
@ -166,18 +214,22 @@ class FileConfigurator implements ConfiguratorInterface
*/
private function getConfigurationClass(): string
{
$class = false;
switch (true) {
case !$this->job->configuration['has-file-upload']:
$class = Upload::class;
break;
case !$this->job->configuration['initial-config-complete']:
$config = $this->getConfig();
$stage = $config['stage'] ?? 'initial';
$class = false;
Log::debug(sprintf('Now in getConfigurationClass() for stage "%s"', $stage));
switch ($stage) {
case 'initial': // has nothing, no file upload or anything.
$class = Initial::class;
break;
case !$this->job->configuration['column-roles-complete']:
case 'upload-config': // has file, needs file config.
$class = UploadConfig::class;
break;
case 'roles': // has configured file, needs roles.
$class = Roles::class;
break;
case !$this->job->configuration['column-mapping-complete']:
case 'map': // has roles, needs mapping.
$class = Map::class;
break;
default:
@ -185,11 +237,12 @@ class FileConfigurator implements ConfiguratorInterface
}
if (false === $class || 0 === strlen($class)) {
throw new FireflyException('Cannot handle current job state in getConfigurationClass().');
throw new FireflyException(sprintf('Cannot handle job stage "%s" in getConfigurationClass().', $stage));
}
if (!class_exists($class)) {
throw new FireflyException(sprintf('Class %s does not exist in getConfigurationClass().', $class));
throw new FireflyException(sprintf('Class %s does not exist in getConfigurationClass().', $class)); // @codeCoverageIgnore
}
Log::debug(sprintf('Configuration class is "%s"', $class));
return $class;
}

View File

@ -22,7 +22,10 @@ declare(strict_types=1);
namespace FireflyIII\Import\Configuration;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ImportJob;
use FireflyIII\Support\Import\Configuration\Spectre\HaveAccounts;
use Log;
/**
* Class SpectreConfigurator.
@ -51,7 +54,29 @@ class SpectreConfigurator implements ConfiguratorInterface
*/
public function configureJob(array $data): bool
{
die('cannot store config');
$config = $this->job->configuration;
$stage = $config['stage'];
$status = $this->job->status;
Log::debug(sprintf('in getNextData(), for stage "%s".', $stage));
switch ($stage) {
case 'have-accounts':
/** @var HaveAccounts $class */
$class = app(HaveAccounts::class);
$class->setJob($this->job);
$class->storeConfiguration($data);
// update job for next step and set to "configured".
$config = $this->job->configuration;
$config['stage'] = 'have-account-mapping';
$this->job->configuration = $config;
$this->job->status = 'configured';
$this->job->save();
return true;
break;
default:
throw new FireflyException(sprintf('Cannot store configuration when job is in state "%s"', $stage));
break;
}
}
/**
@ -61,11 +86,34 @@ class SpectreConfigurator implements ConfiguratorInterface
*/
public function getNextData(): array
{
// update config to tell Firefly we've redirected the user.
$config = $this->job->configuration;
$config['is-redirected'] = true;
$config = $this->job->configuration;
$stage = $config['stage'];
$status = $this->job->status;
Log::debug(sprintf('in getNextData(), for stage "%s".', $stage));
switch ($stage) {
case 'has-token':
// simply redirect to Spectre.
$config['is-redirected'] = true;
$config['stage'] = 'user-logged-in';
$status = 'configured';
break;
case 'have-accounts':
// use special class:
/** @var HaveAccounts $class */
$class = app(HaveAccounts::class);
$class->setJob($this->job);
$data = $class->getData();
return $data;
default:
return [];
break;
}
// update config and status:
$this->job->configuration = $config;
$this->job->status = 'configured';
$this->job->status = $status;
$this->job->save();
return $this->job->configuration;
@ -76,7 +124,22 @@ class SpectreConfigurator implements ConfiguratorInterface
*/
public function getNextView(): string
{
return 'import.spectre.redirect';
$config = $this->job->configuration;
$stage = $config['stage'];
Log::debug(sprintf('in getNextView(), for stage "%s".', $stage));
switch ($stage) {
case 'has-token':
// redirect to Spectre.
return 'import.spectre.redirect';
break;
case 'have-accounts':
return 'import.spectre.accounts';
break;
default:
return '';
break;
}
}
/**
@ -94,13 +157,20 @@ class SpectreConfigurator implements ConfiguratorInterface
*/
public function isJobConfigured(): bool
{
// job is configured (and can start) when token is empty:
$config = $this->job->configuration;
if ($config['has-token'] === false) {
return true;
}
$stage = $config['stage'];
Log::debug(sprintf('in isJobConfigured(), for stage "%s".', $stage));
switch ($stage) {
case 'has-token':
case 'have-accounts':
Log::debug('isJobConfigured returns false');
return false;
return false;
default:
Log::debug('isJobConfigured returns true');
return true;
}
}
/**
@ -108,18 +178,27 @@ class SpectreConfigurator implements ConfiguratorInterface
*/
public function setJob(ImportJob $job)
{
$defaultConfig = [
'has-token' => false,
'token' => '',
'token-expires' => 0,
'token-url' => '',
'is-redirected' => false,
$defaultConfig = [
'has-token' => false,
'token' => '',
'token-expires' => 0,
'token-url' => '',
'is-redirected' => false,
'customer' => null,
'login' => null,
'stage' => 'initial',
'accounts' => '',
'accounts-mapped' => '',
'auto-start' => true,
];
$extendedStatus = $job->extended_status;
$extendedStatus['steps'] = 100;
$config = $job->configuration;
$finalConfig = array_merge($defaultConfig, $config);
$job->configuration = $finalConfig;
$config = $job->configuration;
$finalConfig = array_merge($defaultConfig, $config);
$job->configuration = $finalConfig;
$job->extended_status = $extendedStatus;
$job->save();
$this->job = $job;
}

View File

@ -45,8 +45,10 @@ class Amount implements ConverterInterface
if (null === $value) {
return '0';
}
$value = strval($value);
Log::debug(sprintf('Start with amount "%s"', $value));
$original = $value;
$value = strval($value);
$value = $this->stripAmount($value);
$len = strlen($value);
$decimalPosition = $len - 3;
$altPosition = $len - 2;
@ -81,27 +83,43 @@ class Amount implements ConverterInterface
// if decimal is dot, replace all comma's and spaces with nothing. then parse as float (round to 4 pos)
if ('.' === $decimal) {
$search = [',', ' '];
$oldValue = $value;
$value = str_replace($search, '', $value);
Log::debug(sprintf('Converted amount from "%s" to "%s".', $oldValue, $value));
Log::debug(sprintf('Converted amount from "%s" to "%s".', $original, $value));
}
if (',' === $decimal) {
$search = ['.', ' '];
$oldValue = $value;
$value = str_replace($search, '', $value);
$value = str_replace(',', '.', $value);
Log::debug(sprintf('Converted amount from "%s" to "%s".', $oldValue, $value));
Log::debug(sprintf('Converted amount from "%s" to "%s".', $original, $value));
}
if (null === $decimal) {
// replace all:
$search = ['.', ' ', ','];
$oldValue = $value;
$value = str_replace($search, '', $value);
Log::debug(sprintf('No decimal character found. Converted amount from "%s" to "%s".', $oldValue, $value));
Log::debug(sprintf('No decimal character found. Converted amount from "%s" to "%s".', $original, $value));
}
$number = strval(number_format(round(floatval($value), 12), 12, '.', ''));
return $number;
}
/**
* @param string $value
*
* @return string
*/
private function stripAmount(string $value): string
{
$str = preg_replace('/[^\-\(\)\.\,0-9 ]/', '', $value);
$len = strlen($str);
if ($str{0} === '(' && $str{$len - 1} === ')') {
$str = '-' . substr($str, 1, ($len - 2));
}
Log::debug(sprintf('Stripped "%s" away to "%s"', $value, $str));
return $str;
}
}

View File

@ -26,7 +26,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Object\ImportJournal;
use FireflyIII\Import\Specifics\SpecificInterface;
use FireflyIII\Models\ImportJob;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use Illuminate\Support\Collection;
use Iterator;
use League\Csv\Reader;
@ -43,6 +43,8 @@ class CsvProcessor implements FileProcessorInterface
private $job;
/** @var Collection */
private $objects;
/** @var ImportJobRepositoryInterface */
private $repository;
/** @var array */
private $validConverters = [];
/** @var array */
@ -60,9 +62,14 @@ class CsvProcessor implements FileProcessorInterface
/**
* @return Collection
* @throws FireflyException
*/
public function getObjects(): Collection
{
if (is_null($this->job)) {
throw new FireflyException('Cannot call getObjects() without a job.');
}
return $this->objects;
}
@ -73,9 +80,13 @@ class CsvProcessor implements FileProcessorInterface
*
* @throws \League\Csv\Exception
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
* @throws FireflyException
*/
public function run(): bool
{
if (is_null($this->job)) {
throw new FireflyException('Cannot call run() without a job.');
}
Log::debug('Now in CsvProcessor run(). Job is now running...');
$entries = new Collection($this->getImportArray());
@ -86,8 +97,8 @@ class CsvProcessor implements FileProcessorInterface
$row = array_values($row);
if ($this->rowAlreadyImported($row)) {
$message = sprintf('Row #%d has already been imported.', $index);
$this->job->addError($index, $message);
$this->job->addStepsDone(5); // all steps.
$this->repository->addStepsDone($this->job, 5);
$this->addError($index, $message);
Log::info($message);
return null;
@ -99,23 +110,34 @@ class CsvProcessor implements FileProcessorInterface
Log::debug(sprintf('Number of entries left: %d', $notImported->count()));
// set (new) number of steps:
$status = $this->job->extended_status;
$status['steps'] = $notImported->count() * 5;
$this->job->extended_status = $status;
$this->job->save();
Log::debug(sprintf('Number of steps: %d', $notImported->count() * 5));
$extended = $this->getExtendedStatus();
$steps = $notImported->count() * 5;
$extended['steps'] = $steps;
$this->setExtendedStatus($extended);
Log::debug(sprintf('Number of steps: %d', $steps));
$notImported->each(
function (array $row, int $index) {
$journal = $this->importRow($index, $row);
$this->objects->push($journal);
$this->job->addStepsDone(1);
$this->repository->addStepsDone($this->job, 1);
}
);
return true;
}
/**
* @codeCoverageIgnore
* Shorthand method
*
* @param array $array
*/
public function setExtendedStatus(array $array)
{
$this->repository->setExtendedStatus($this->job, $array);
}
/**
* Set import job for this processor.
*
@ -125,11 +147,30 @@ class CsvProcessor implements FileProcessorInterface
*/
public function setJob(ImportJob $job): FileProcessorInterface
{
$this->job = $job;
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($job->user);
return $this;
}
/**
* Shorthand method.
*
* @codeCoverageIgnore
*
* @param int $index
* @param string $message
*/
private function addError(int $index, string $message): void
{
$extended = $this->getExtendedStatus();
$extended['errors'][$index][] = $message;
$this->setExtendedStatus($extended);
return;
}
/**
* Add meta data to the individual value and verify that it can be handled in a later stage.
*
@ -142,7 +183,7 @@ class CsvProcessor implements FileProcessorInterface
*/
private function annotateValue(int $index, string $value)
{
$config = $this->job->configuration;
$config = $this->getConfig();
$role = $config['column-roles'][$index] ?? '_ignore';
$mapped = $config['column-mapping-config'][$index][$value] ?? null;
@ -160,6 +201,29 @@ class CsvProcessor implements FileProcessorInterface
return $entry;
}
/**
* @codeCoverageIgnore
* Shorthand method.
*
* @return array
*/
private function getConfig(): array
{
return $this->repository->getConfiguration($this->job);
}
/**
* @codeCoverageIgnore
* Shorthand method.
*
* @return array
*/
private function getExtendedStatus(): array
{
return $this->repository->getExtendedStatus($this->job);
}
/**
* @return Iterator
*
@ -169,16 +233,17 @@ class CsvProcessor implements FileProcessorInterface
*/
private function getImportArray(): Iterator
{
$content = $this->job->uploadFileContents();
$config = $this->job->configuration;
$reader = Reader::createFromString($content);
$delimiter = $config['delimiter'];
$content = $this->repository->uploadFileContents($this->job);
$config = $this->getConfig();
$reader = Reader::createFromString($content);
$delimiter = $config['delimiter'] ?? ',';
$hasHeaders = isset($config['has-headers']) ? $config['has-headers'] : false;
if ('tab' === $delimiter) {
$delimiter = "\t";
$delimiter = "\t"; // @codeCoverageIgnore
}
$reader->setDelimiter($delimiter);
if ($config['has-headers']) {
$reader->setHeaderOffset(0);
if ($hasHeaders) {
$reader->setHeaderOffset(0); // @codeCoverageIgnore
}
$results = $reader->getRecords();
Log::debug('Created a CSV reader.');
@ -191,6 +256,7 @@ class CsvProcessor implements FileProcessorInterface
*
* @param int $jsonError
*
* @codeCoverageIgnore
* @return string
*/
private function getJsonError(int $jsonError): string
@ -230,7 +296,7 @@ class CsvProcessor implements FileProcessorInterface
$jsonError = json_last_error();
if (false === $json) {
throw new FireflyException(sprintf('Error while encoding JSON for CSV row: %s', $this->getJsonError($jsonError)));
throw new FireflyException(sprintf('Error while encoding JSON for CSV row: %s', $this->getJsonError($jsonError))); // @codeCoverageIgnore
}
$hash = hash('sha256', $json);
@ -251,8 +317,9 @@ class CsvProcessor implements FileProcessorInterface
{
$row = array_values($row);
Log::debug(sprintf('Now at row %d', $index));
$row = $this->specifics($row);
$hash = $this->getRowHash($row);
$row = $this->specifics($row);
$hash = $this->getRowHash($row);
$config = $this->getConfig();
$journal = new ImportJournal;
$journal->setUser($this->job->user);
@ -271,7 +338,8 @@ class CsvProcessor implements FileProcessorInterface
}
}
// set some extra info:
$journal->asset->setDefaultAccountId($this->job->configuration['import-account']);
$importAccount = intval($config['import-account'] ?? 0);
$journal->asset->setDefaultAccountId($importAccount);
return $journal;
}
@ -288,12 +356,8 @@ class CsvProcessor implements FileProcessorInterface
private function rowAlreadyImported(array $array): bool
{
$hash = $this->getRowHash($array);
$json = json_encode($hash);
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('data', $json)
->where('name', 'importHash')
->first();
if (null !== $entry) {
$count = $this->repository->countByHash($hash);
if ($count > 0) {
return true;
}

View File

@ -26,6 +26,7 @@ use Illuminate\Console\Command;
use Monolog\Handler\AbstractProcessingHandler;
/**
* @codeCoverageIgnore
* Class CommandHandler.
*/
class CommandHandler extends AbstractProcessingHandler

View File

@ -44,18 +44,17 @@ class AssetAccountIbans implements MapperInterface
/** @var Account $account */
foreach ($set as $account) {
$iban = $account->iban ?? '';
$iban = $account->iban ?? '';
$accountId = intval($account->id);
if (strlen($iban) > 0) {
$topList[$account->id] = $account->iban . ' (' . $account->name . ')';
$topList[$accountId] = $account->iban . ' (' . $account->name . ')';
}
if (0 === strlen($iban)) {
$list[$account->id] = $account->name;
$list[$accountId] = $account->name;
}
}
asort($topList);
asort($list);
$list = $topList + $list;
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -43,16 +43,15 @@ class AssetAccounts implements MapperInterface
/** @var Account $account */
foreach ($set as $account) {
$name = $account->name;
$iban = $account->iban ?? '';
$accountId = intval($account->id);
$name = $account->name;
$iban = $account->iban ?? '';
if (strlen($iban) > 0) {
$name .= ' (' . $account->iban . ')';
$name .= ' (' . $iban . ')';
}
$list[$account->id] = $name;
$list[$accountId] = $name;
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -42,10 +42,10 @@ class Bills implements MapperInterface
/** @var Bill $bill */
foreach ($result as $bill) {
$list[$bill->id] = $bill->name . ' [' . $bill->match . ']';
$billId = intval($bill->id);
$list[$billId] = $bill->name . ' [' . $bill->match . ']';
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -37,15 +37,15 @@ class Budgets implements MapperInterface
{
/** @var BudgetRepositoryInterface $repository */
$repository = app(BudgetRepositoryInterface::class);
$result = $repository->getBudgets();
$result = $repository->getActiveBudgets();
$list = [];
/** @var Budget $budget */
foreach ($result as $budget) {
$list[$budget->id] = $budget->name;
$budgetId = intval($budget->id);
$list[$budgetId] = $budget->name;
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -42,10 +42,10 @@ class Categories implements MapperInterface
/** @var Category $category */
foreach ($result as $category) {
$list[$category->id] = $category->name;
$categoryId = intval($category->id);
$list[$categoryId] = $category->name;
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -50,18 +50,17 @@ class OpposingAccountIbans implements MapperInterface
/** @var Account $account */
foreach ($set as $account) {
$iban = $account->iban ?? '';
$iban = $account->iban ?? '';
$accountId = intval($account->id);
if (strlen($iban) > 0) {
$topList[$account->id] = $account->iban . ' (' . $account->name . ')';
$topList[$accountId] = $account->iban . ' (' . $account->name . ')';
}
if (0 === strlen($iban)) {
$list[$account->id] = $account->name;
$list[$accountId] = $account->name;
}
}
asort($topList);
asort($list);
$list = $topList + $list;
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -49,16 +49,15 @@ class OpposingAccounts implements MapperInterface
/** @var Account $account */
foreach ($set as $account) {
$name = $account->name;
$iban = $account->iban ?? '';
$accountId = intval($account->id);
$name = $account->name;
$iban = $account->iban ?? '';
if (strlen($iban) > 0) {
$name .= ' (' . $account->iban . ')';
$name .= ' (' . $iban . ')';
}
$list[$account->id] = $name;
$list[$accountId] = $name;
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -42,10 +42,10 @@ class Tags implements MapperInterface
/** @var Tag $tag */
foreach ($result as $tag) {
$list[$tag->id] = $tag->tag;
$tagId = intval($tag->id);
$list[$tagId] = $tag->tag;
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;
return $list;

View File

@ -22,7 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Import\Mapper;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
/**
* Class TransactionCurrencies.
@ -34,12 +34,14 @@ class TransactionCurrencies implements MapperInterface
*/
public function getMap(): array
{
$currencies = TransactionCurrency::get();
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$currencies = $repository->get();
$list = [];
foreach ($currencies as $currency) {
$list[$currency->id] = $currency->name . ' (' . $currency->code . ')';
$currencyId = intval($currency->id);
$list[$currencyId] = $currency->name . ' (' . $currency->code . ')';
}
asort($list);
$list = [0 => trans('import.map_do_not_map')] + $list;

View File

@ -34,6 +34,11 @@ class TagsComma implements PreProcessorInterface
*/
public function run(string $value): array
{
return explode(',', $value);
$set = explode(',', $value);
$set = array_map('trim', $set);
$set = array_filter($set, 'strlen');
$return = array_values($set);
return $return;
}
}

View File

@ -34,6 +34,11 @@ class TagsSpace implements PreProcessorInterface
*/
public function run(string $value): array
{
return explode(' ', $value);
$set = explode(' ', $value);
$set = array_map('trim', $set);
$set = array_filter($set, 'strlen');
$return = array_values($set);
return $return;
}
}

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Import\Object;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@ -74,6 +75,7 @@ class ImportAccount
/**
* @return Account
* @throws FireflyException
*/
public function getAccount(): Account
{
@ -85,6 +87,7 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
* @return string
*/
public function getExpectedType(): string
@ -93,6 +96,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param string $expectedType
*/
public function setExpectedType(string $expectedType)
@ -101,6 +106,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param array $accountIban
*/
public function setAccountIban(array $accountIban)
@ -109,6 +116,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param array $value
*/
public function setAccountId(array $value)
@ -117,6 +126,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param array $accountName
*/
public function setAccountName(array $accountName)
@ -125,6 +136,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param array $accountNumber
*/
public function setAccountNumber(array $accountNumber)
@ -133,6 +146,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param int $defaultAccountId
*/
public function setDefaultAccountId(int $defaultAccountId)
@ -141,6 +156,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param int $forbiddenAccountId
*/
public function setForbiddenAccountId(int $forbiddenAccountId)
@ -149,6 +166,8 @@ class ImportAccount
}
/**
* @codeCoverageIgnore
*
* @param User $user
*/
public function setUser(User $user)
@ -158,20 +177,21 @@ class ImportAccount
}
/**
* @return Account
* @return Account|null
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function findExistingObject(): Account
private function findExistingObject(): ?Account
{
Log::debug('In findExistingObject() for Account');
// 0: determin account type:
/** @var AccountType $accountType */
$accountType = AccountType::whereType($this->expectedType)->first();
$accountType = $this->repository->getAccountType($this->expectedType);
// 1: find by ID, iban or name (and type)
if (3 === count($this->accountId)) {
Log::debug(sprintf('Finding account of type %d and ID %d', $accountType->id, $this->accountId['value']));
/** @var Account $account */
$account = $this->user->accounts()->where('id', '!=', $this->forbiddenAccountId)->where('account_type_id', $accountType->id)->where(
'id',
$this->accountId['value']
@ -233,13 +253,13 @@ class ImportAccount
// 4: do not search by account number.
Log::debug('Found NO existing accounts.');
return new Account;
return null;
}
/**
* @return Account
* @return Account|null
*/
private function findMappedObject(): Account
private function findMappedObject(): ?Account
{
Log::debug('In findMappedObject() for Account');
$fields = ['accountId', 'accountIban', 'accountNumber', 'accountName'];
@ -248,7 +268,7 @@ class ImportAccount
Log::debug(sprintf('Find mapped account based on field "%s" with value', $field), $array);
// check if a pre-mapped object exists.
$mapped = $this->getMappedObject($array);
if (null !== $mapped->id) {
if (null !== $mapped) {
Log::debug(sprintf('Found account #%d!', $mapped->id));
return $mapped;
@ -256,38 +276,38 @@ class ImportAccount
}
Log::debug('Found no account on mapped data or no map present.');
return new Account;
return null;
}
/**
* @param array $array
*
* @return Account
* @return Account|null
*/
private function getMappedObject(array $array): Account
private function getMappedObject(array $array): ?Account
{
Log::debug('In getMappedObject() for Account');
if (0 === count($array)) {
Log::debug('Array is empty, nothing will come of this.');
return new Account;
return null;
}
if (array_key_exists('mapped', $array) && null === $array['mapped']) {
Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value']));
return new Account;
return null;
}
Log::debug('Finding a mapped account based on', $array);
$search = intval($array['mapped']);
$search = intval($array['mapped'] ?? 0);
$account = $this->repository->find($search);
if (null === $account->id) {
Log::error(sprintf('There is no account with id #%d. Invalid mapping will be ignored!', $search));
return new Account;
return null;
}
// must be of the same type
// except when mapped is an asset, then it's fair game.
@ -302,7 +322,7 @@ class ImportAccount
)
);
return new Account;
return null;
}
Log::debug(sprintf('Found account! #%d ("%s"). Return it', $account->id, $account->name));
@ -312,19 +332,26 @@ class ImportAccount
/**
* @return bool
* @throws FireflyException
*/
private function store(): bool
{
if (is_null($this->user)) {
throw new FireflyException('ImportAccount cannot continue without user.');
}
if ((is_null($this->defaultAccountId) || intval($this->defaultAccountId) === 0) && AccountType::ASSET === $this->expectedType) {
throw new FireflyException('ImportAccount cannot continue without a default account to fall back on.');
}
// 1: find mapped object:
$mapped = $this->findMappedObject();
if (null !== $mapped->id) {
if (null !== $mapped) {
$this->account = $mapped;
return true;
}
// 2: find existing by given values:
$found = $this->findExistingObject();
if (null !== $found->id) {
if (null !== $found) {
$this->account = $found;
return true;
@ -335,7 +362,7 @@ class ImportAccount
$oldExpectedType = $this->expectedType;
$this->expectedType = AccountType::ASSET;
$found = $this->findExistingObject();
if (null !== $found->id) {
if (null !== $found) {
Log::debug('Found asset account!');
$this->account = $found;

View File

@ -302,6 +302,10 @@ class ImportJournal
if (0 === count($info)) {
throw new FireflyException('No amount information for this row.');
}
$class = $info['class'] ?? '';
if (strlen($class) === 0) {
throw new FireflyException('No amount information (conversion class) for this row.');
}
Log::debug(sprintf('Converter class is %s', $info['class']));
/** @var ConverterInterface $amountConverter */

View File

@ -24,9 +24,18 @@ namespace FireflyIII\Import\Routine;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Services\Spectre\Exception\DuplicatedCustomerException;
use FireflyIII\Services\Spectre\Exception\SpectreException;
use FireflyIII\Services\Spectre\Object\Account;
use FireflyIII\Services\Spectre\Object\Customer;
use FireflyIII\Services\Spectre\Object\Login;
use FireflyIII\Services\Spectre\Object\Token;
use FireflyIII\Services\Spectre\Request\CreateTokenRequest;
use FireflyIII\Services\Spectre\Request\ListAccountsRequest;
use FireflyIII\Services\Spectre\Request\ListCustomersRequest;
use FireflyIII\Services\Spectre\Request\ListLoginsRequest;
use FireflyIII\Services\Spectre\Request\ListTransactionsRequest;
use FireflyIII\Services\Spectre\Request\NewCustomerRequest;
use Illuminate\Support\Collection;
use Log;
@ -46,6 +55,9 @@ class SpectreRoutine implements RoutineInterface
/** @var ImportJob */
private $job;
/** @var ImportJobRepositoryInterface */
private $repository;
/**
* ImportRoutine constructor.
*/
@ -80,57 +92,61 @@ class SpectreRoutine implements RoutineInterface
}
/**
* A Spectre job that ends up here is either "configured" or "running", and will be set to "running"
* when it is "configured".
*
* Job has several stages, stored in extended status key 'stage'
*
* initial: just begun, nothing happened. action: get a customer and a token. Next status: has-token
* has-token: redirect user to sandstorm, make user login. set job to: user-logged-in
* user-logged-in: customer has an attempt. action: analyse/get attempt and go for next status.
* if attempt failed: job status is error, save a warning somewhere?
* if success, try to get accounts. Save in config key 'accounts'. set status: have-accounts and "configuring"
*
* have-accounts: make user link accounts and select accounts to import from.
*
* If job is "configuring" and stage "have-accounts" then present the accounts and make user link them to
* own asset accounts. Store this mapping, set config to "have-account-mapping" and job status configured".
*
* have-account-mapping: start downloading transactions?
*
*
* @throws \FireflyIII\Exceptions\FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
public function run(): bool
{
if ('configured' !== $this->job->status) {
Log::error(sprintf('Job %s is in state "%s" so it cannot be started.', $this->job->key, $this->job->status));
return false;
if ('configured' === $this->job->status) {
$this->repository->updateStatus($this->job, 'running');
}
Log::info(sprintf('Start with import job %s using Spectre.', $this->job->key));
set_time_limit(0);
// check if job has token first!
$config = $this->job->configuration;
$hasToken = $config['has-token'] ?? false;
if ($hasToken === false) {
Log::debug('Job has no token');
// create customer if user does not have one:
$customer = $this->getCustomer();
Log::debug(sprintf('Customer ID is %s', $customer->getId()));
// use customer to request a token:
$uri = route('import.status', [$this->job->key]);
$token = $this->getToken($customer, $uri);
Log::debug(sprintf('Token is %s', $token->getToken()));
$config = $this->job->configuration;
$stage = $config['stage'];
// update job, give it the token:
$config = $this->job->configuration;
$config['has-token'] = true;
$config['token'] = $token->getToken();
$config['token-expires'] = $token->getExpiresAt()->format('U');
$config['token-url'] = $token->getConnectUrl();
$this->job->configuration = $config;
Log::debug('Job config is now', $config);
// update job, set status to "configuring".
$this->job->status = 'configuring';
$this->job->save();
Log::debug(sprintf('Job status is now %s', $this->job->status));
return true;
}
$isRedirected = $config['is-redirected'] ?? false;
if ($isRedirected === true) {
// assume user has "used" the token.
// ...
// now what?
throw new FireflyException('Application cannot handle this.');
switch ($stage) {
case 'initial':
// get customer and token:
$this->runStageInitial();
break;
case 'has-token':
// import routine does nothing at this point:
break;
case 'user-logged-in':
$this->runStageLoggedIn();
break;
case 'have-account-mapping':
$this->runStageHaveMapping();
break;
default:
throw new FireflyException(sprintf('Cannot handle stage %s', $stage));
}
var_dump($config);
exit;
throw new FireflyException('Application cannot handle this.');
}
@ -139,18 +155,39 @@ class SpectreRoutine implements RoutineInterface
*/
public function setJob(ImportJob $job)
{
$this->job = $job;
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($job->user);
}
/**
* @return Customer
* @throws \FireflyIII\Exceptions\FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
protected function createCustomer(): Customer
{
$newCustomerRequest = new NewCustomerRequest($this->job->user);
$newCustomerRequest->call();
$customer = $newCustomerRequest->getCustomer();
$customer = null;
try {
$newCustomerRequest->call();
$customer = $newCustomerRequest->getCustomer();
} catch (DuplicatedCustomerException $e) {
// already exists, must fetch customer instead.
Log::warning('Customer exists already for user, fetch it.');
}
if (is_null($customer)) {
$getCustomerRequest = new ListCustomersRequest($this->job->user);
$getCustomerRequest->call();
$customers = $getCustomerRequest->getCustomers();
/** @var Customer $current */
foreach ($customers as $current) {
if ($current->getIdentifier() === 'default_ff3_customer') {
$customer = $current;
break;
}
}
}
Preferences::setForUser($this->job->user, 'spectre_customer', $customer->toArray());
@ -160,15 +197,26 @@ class SpectreRoutine implements RoutineInterface
/**
* @return Customer
* @throws \FireflyIII\Exceptions\FireflyException
* @throws FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
protected function getCustomer(): Customer
{
$preference = Preferences::getForUser($this->job->user, 'spectre_customer', null);
if (is_null($preference)) {
return $this->createCustomer();
$config = $this->job->configuration;
if (!is_null($config['customer'])) {
$customer = new Customer($config['customer']);
return $customer;
}
$customer = new Customer($preference->data);
$customer = $this->createCustomer();
$config['customer'] = [
'id' => $customer->getId(),
'identifier' => $customer->getIdentifier(),
'secret' => $customer->getSecret(),
];
$this->job->configuration = $config;
$this->job->save();
return $customer;
}
@ -179,6 +227,7 @@ class SpectreRoutine implements RoutineInterface
*
* @return Token
* @throws \FireflyIII\Exceptions\FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
protected function getToken(Customer $customer, string $returnUri): Token
{
@ -191,4 +240,124 @@ class SpectreRoutine implements RoutineInterface
return $request->getToken();
}
/**
* @throws FireflyException
* @throws SpectreException
*/
protected function runStageInitial(): void
{
Log::debug('In runStageInitial()');
// create customer if user does not have one:
$customer = $this->getCustomer();
Log::debug(sprintf('Customer ID is %s', $customer->getId()));
// use customer to request a token:
$uri = route('import.status', [$this->job->key]);
$token = $this->getToken($customer, $uri);
Log::debug(sprintf('Token is %s', $token->getToken()));
// update job, give it the token:
$config = $this->job->configuration;
$config['has-token'] = true;
$config['token'] = $token->getToken();
$config['token-expires'] = $token->getExpiresAt()->format('U');
$config['token-url'] = $token->getConnectUrl();
$config['stage'] = 'has-token';
$this->job->configuration = $config;
Log::debug('Job config is now', $config);
// update job, set status to "configuring".
$this->job->status = 'configuring';
$this->job->save();
Log::debug(sprintf('Job status is now %s', $this->job->status));
}
/**
* @throws FireflyException
* @throws SpectreException
*/
protected function runStageLoggedIn(): void
{
Log::debug('In runStageLoggedIn');
// list all logins:
$customer = $this->getCustomer();
$request = new ListLoginsRequest($this->job->user);
$request->setCustomer($customer);
$request->call();
$logins = $request->getLogins();
/** @var Login $final */
$final = null;
// loop logins, find the latest with no error in it:
$time = 0;
/** @var Login $login */
foreach ($logins as $login) {
$attempt = $login->getLastAttempt();
$attemptTime = intval($attempt->getCreatedAt()->format('U'));
if ($attemptTime > $time && is_null($attempt->getFailErrorClass())) {
$time = $attemptTime;
$final = $login;
}
}
if (is_null($final)) {
throw new FireflyException('No valid login attempt found.');
}
// list the users accounts using this login.
$accountRequest = new ListAccountsRequest($this->job->user);
$accountRequest->setLogin($login);
$accountRequest->call();
$accounts = $accountRequest->getAccounts();
// store accounts in job:
$all = [];
/** @var Account $account */
foreach ($accounts as $account) {
$all[] = $account->toArray();
}
// update job:
$config = $this->job->configuration;
$config['accounts'] = $all;
$config['login'] = $login->toArray();
$config['stage'] = 'have-accounts';
$this->job->configuration = $config;
$this->job->status = 'configuring';
$this->job->save();
return;
}
/**
*
*/
private function runStageHaveMapping()
{
// for each spectre account id in 'account-mappings'.
// find FF account
// get transactions.
// import?!
$config = $this->job->configuration;
$accounts = $config['accounts'] ?? [];
/** @var array $accountArray */
foreach ($accounts as $accountArray) {
$account = new Account($accountArray);
$importId = intval($config['accounts-mapped'][$account->getid()] ?? 0);
$doImport = $importId !== 0 ? true : false;
if (!$doImport) {
continue;
}
// import into account
$listTransactionsRequest = new ListTransactionsRequest($this->job->user);
$listTransactionsRequest->setAccount($account);
$listTransactionsRequest->call();
$transactions = $listTransactionsRequest->getTransactions();
var_dump($transactions);exit;
}
var_dump($config);
exit;
}
}

View File

@ -50,7 +50,10 @@ class SnsDescription implements SpecificInterface
*/
public function run(array $row): array
{
$row = array_values($row);
$row = array_values($row);
if (!isset($row[17])) {
return $row;
}
$row[17] = ltrim($row[17], "'");
$row[17] = rtrim($row[17], "'");

View File

@ -94,8 +94,8 @@ class ImportStorage
$this->defaultCurrencyId = $currency->id;
$this->transfers = $this->getTransfers();
$config = $job->configuration;
$this->applyRules = $config['apply_rules'] ?? false;
$this->matchBills = $config['match_bills'] ?? false;
$this->applyRules = $config['apply-rules'] ?? false;
$this->matchBills = $config['match-bills'] ?? false;
if (true === $this->applyRules) {
Log::debug('applyRules seems to be true, get the rules.');
$this->rules = $this->getRules();

View File

@ -345,6 +345,15 @@ class Account extends Model
$this->attributes['iban'] = Crypt::encrypt($value);
}
/**
* @codeCoverageIgnore
* Get all of the notes.
*/
public function notes()
{
return $this->morphMany(Note::class, 'noteable');
}
/**
* @codeCoverageIgnore
*

View File

@ -132,7 +132,7 @@ class Bill extends Model
*/
public function notes()
{
return $this->morphMany('FireflyIII\Models\Note', 'noteable');
return $this->morphMany(Note::class, 'noteable');
}
/**

View File

@ -161,7 +161,7 @@ class ImportJob extends Model
*/
public function getExtendedStatusAttribute($value)
{
if (0 === strlen($value)) {
if (0 === strlen(strval($value))) {
return [];
}
@ -209,6 +209,7 @@ class ImportJob extends Model
$disk = Storage::disk('upload');
$encryptedContent = $disk->get($fileName);
$content = Crypt::decrypt($encryptedContent);
$content = trim($content);
Log::debug(sprintf('Content size is %d bytes.', strlen($content)));
return $content;

View File

@ -57,8 +57,8 @@ class Note extends Model
/**
* @codeCoverageIgnore
* Get all of the owning noteable models. Currently piggy bank and
* transaction journal.
*
* Get all of the owning noteable models.
*/
public function noteable()
{

View File

@ -29,6 +29,7 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Category;
use FireflyIII\Models\Note;
use FireflyIII\Models\Tag;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
@ -86,6 +87,28 @@ class AccountRepository implements AccountRepositoryInterface
return true;
}
/**
* Return account type by string.
*
* @param string $type
*
* @return AccountType|null
*/
public function getAccountType(string $type): ?AccountType
{
return AccountType::whereType($type)->first();
}
/**
* @param Account $account
*
* @return Note|null
*/
public function getNote(Account $account): ?Note
{
return $account->notes()->first();
}
/**
* Returns the date of the very last transaction in this account.
*
@ -176,6 +199,11 @@ class AccountRepository implements AccountRepositoryInterface
}
$this->deleteInitialBalance($newAccount);
// update note:
if (isset($data['notes'])) {
$this->updateNote($newAccount, $data['notes']);
}
return $newAccount;
}
@ -199,6 +227,12 @@ class AccountRepository implements AccountRepositoryInterface
$this->updateInitialBalance($account, $data);
}
// update note:
if (isset($data['notes']) && null !== $data['notes']) {
$this->updateNote($account, strval($data['notes']));
}
return $account;
}
@ -364,7 +398,7 @@ class AccountRepository implements AccountRepositoryInterface
'user_id' => $this->user->id,
'transaction_type_id' => $transactionType->id,
'transaction_currency_id' => $currencyId,
'description' => 'Initial balance for "' . $account->name . '"',
'description' => strval(trans('firefly.initial_balance_description', ['account' => $account->name])),
'completed' => true,
'date' => $data['openingBalanceDate'],
]
@ -507,6 +541,33 @@ class AccountRepository implements AccountRepositoryInterface
}
}
/**
* @param Account $account
* @param string $note
*
* @return bool
*/
protected function updateNote(Account $account, string $note): bool
{
if (0 === strlen($note)) {
$dbNote = $account->notes()->first();
if (null !== $dbNote) {
$dbNote->delete();
}
return true;
}
$dbNote = $account->notes()->first();
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($account);
}
$dbNote->text = trim($note);
$dbNote->save();
return true;
}
/**
* @param Account $account
* @param TransactionJournal $journal
@ -518,13 +579,15 @@ class AccountRepository implements AccountRepositoryInterface
*/
protected function updateOpeningBalanceJournal(Account $account, TransactionJournal $journal, array $data): bool
{
$date = $data['openingBalanceDate'];
$amount = strval($data['openingBalance']);
$currencyId = intval($data['currency_id']);
$date = $data['openingBalanceDate'];
$amount = strval($data['openingBalance']);
$negativeAmount = bcmul($amount, '-1');
$currencyId = intval($data['currency_id']);
Log::debug(sprintf('Submitted amount for opening balance to update is %s', $amount));
Log::debug(sprintf('Submitted amount for opening balance to update is "%s"', $amount));
if (0 === bccomp($amount, '0')) {
Log::notice(sprintf('Amount "%s" is zero, delete opening balance.', $amount));
$journal->delete();
return true;
@ -534,18 +597,18 @@ class AccountRepository implements AccountRepositoryInterface
$journal->date = $date;
$journal->transaction_currency_id = $currencyId;
$journal->save();
// update transactions:
/** @var Transaction $transaction */
foreach ($journal->transactions()->get() as $transaction) {
if ($account->id === $transaction->account_id) {
Log::debug(sprintf('Will change transaction #%d amount from %s to %s', $transaction->id, $transaction->amount, $amount));
if (intval($account->id) === intval($transaction->account_id)) {
Log::debug(sprintf('Will (eq) change transaction #%d amount from "%s" to "%s"', $transaction->id, $transaction->amount, $amount));
$transaction->amount = $amount;
$transaction->transaction_currency_id = $currencyId;
$transaction->save();
}
if ($account->id !== $transaction->account_id) {
$negativeAmount = bcmul($amount, '-1');
Log::debug(sprintf('Will change transaction #%d amount from %s to %s', $transaction->id, $transaction->amount, $negativeAmount));
if (!(intval($account->id) === intval($transaction->account_id))) {
Log::debug(sprintf('Will (neq) change transaction #%d amount from "%s" to "%s"', $transaction->id, $transaction->amount, $negativeAmount));
$transaction->amount = $negativeAmount;
$transaction->transaction_currency_id = $currencyId;
$transaction->save();

View File

@ -24,6 +24,8 @@ namespace FireflyIII\Repositories\Account;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Note;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\User;
use Illuminate\Support\Collection;
@ -83,6 +85,15 @@ interface AccountRepositoryInterface
*/
public function findByName(string $name, array $types): Account;
/**
* Return account type by string.
*
* @param string $type
*
* @return AccountType|null
*/
public function getAccountType(string $type): ?AccountType;
/**
* @param array $accountIds
*
@ -109,6 +120,13 @@ interface AccountRepositoryInterface
*/
public function getCashAccount(): Account;
/**
* @param Account $account
*
* @return Note|null
*/
public function getNote(Account $account): ?Note;
/**
* Find or create the opposing reconciliation account.
*

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\ImportJob;
use Crypt;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ImportJob;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Support\Str;
@ -41,6 +42,37 @@ class ImportJobRepository implements ImportJobRepositoryInterface
/** @var User */
private $user;
/**
* @param ImportJob $job
* @param int $steps
*
* @return ImportJob
*/
public function addStepsDone(ImportJob $job, int $steps = 1): ImportJob
{
$job->addStepsDone($steps);
return $job;
}
/**
* Return number of imported rows with this hash value.
*
* @param string $hash
*
* @return int
*/
public function countByHash(string $hash): int
{
$json = json_encode($hash);
$count = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('data', $json)
->where('name', 'importHash')
->count();
return intval($count);
}
/**
* @param string $type
*
@ -62,7 +94,7 @@ class ImportJobRepository implements ImportJobRepositoryInterface
$importJob->file_type = $type;
$importJob->key = Str::random(12);
$importJob->status = 'new';
$importJob->configuration = config(sprintf('import.default_config.%s', $type)) ?? [];
$importJob->configuration = [];
$importJob->extended_status = [
'steps' => 0,
'done' => 0,
@ -86,6 +118,7 @@ class ImportJobRepository implements ImportJobRepositoryInterface
*/
public function findByKey(string $key): ImportJob
{
/** @var ImportJob $result */
$result = $this->user->importJobs()->where('key', $key)->first(['import_jobs.*']);
if (null === $result) {
return new ImportJob;
@ -94,6 +127,40 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return $result;
}
/**
* Return configuration of job.
*
* @param ImportJob $job
*
* @return array
*/
public function getConfiguration(ImportJob $job): array
{
$config = $job->configuration;
if (is_array($config)) {
return $config;
}
return [];
}
/**
* Return extended status of job.
*
* @param ImportJob $job
*
* @return array
*/
public function getExtendedStatus(ImportJob $job): array
{
$status = $job->extended_status;
if (is_array($status)) {
return $status;
}
return [];
}
/**
* @param ImportJob $job
* @param UploadedFile $file
@ -186,6 +253,30 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return $job;
}
/**
* @param ImportJob $job
* @param array $array
*
* @return ImportJob
*/
public function setExtendedStatus(ImportJob $job, array $array): ImportJob
{
// remove 'errors' because it gets larger and larger and larger...
$display = $array;
unset($display['errors']);
Log::debug(sprintf('Incoming extended status for job "%s" is (except errors): ', $job->key), $display);
$currentStatus = $job->extended_status;
$newStatus = array_merge($currentStatus, $array);
$job->extended_status = $newStatus;
$job->save();
// remove 'errors' because it gets larger and larger and larger...
unset($newStatus['errors']);
Log::debug(sprintf('Set extended status of job "%s" to (except errors): ', $job->key), $newStatus);
return $job;
}
/**
* @param User $user
*/
@ -207,4 +298,17 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return $job;
}
/**
* Return import file content.
*
* @param ImportJob $job
*
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function uploadFileContents(ImportJob $job): string
{
return $job->uploadFileContents();
}
}

View File

@ -31,6 +31,23 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/
interface ImportJobRepositoryInterface
{
/**
* @param ImportJob $job
* @param int $steps
*
* @return ImportJob
*/
public function addStepsDone(ImportJob $job, int $steps = 1): ImportJob;
/**
* Return number of imported rows with this hash value.
*
* @param string $hash
*
* @return int
*/
public function countByHash(string $hash): int;
/**
* @param string $type
*
@ -45,6 +62,24 @@ interface ImportJobRepositoryInterface
*/
public function findByKey(string $key): ImportJob;
/**
* Return configuration of job.
*
* @param ImportJob $job
*
* @return array
*/
public function getConfiguration(ImportJob $job): array;
/**
* Return extended status of job.
*
* @param ImportJob $job
*
* @return array
*/
public function getExtendedStatus(ImportJob $job): array;
/**
* @param ImportJob $job
* @param UploadedFile $file
@ -69,6 +104,14 @@ interface ImportJobRepositoryInterface
*/
public function setConfiguration(ImportJob $job, array $configuration): ImportJob;
/**
* @param ImportJob $job
* @param array $array
*
* @return void
*/
public function setExtendedStatus(ImportJob $job, array $array): ImportJob;
/**
* @param User $user
*/
@ -81,4 +124,13 @@ interface ImportJobRepositoryInterface
* @return ImportJob
*/
public function updateStatus(ImportJob $job, string $status): ImportJob;
/**
* Return import file content.
*
* @param ImportJob $job
*
* @return string
*/
public function uploadFileContents(ImportJob $job): string;
}

View File

@ -22,12 +22,15 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Journal;
use DB;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Note;
use FireflyIII\Models\Tag;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Illuminate\Support\MessageBag;
@ -432,6 +435,43 @@ class JournalRepository implements JournalRepositoryInterface
return $journal;
}
/**
* @param TransactionJournal $journal
* @param int $budgetId
*
* @return TransactionJournal
*/
public function updateBudget(TransactionJournal $journal, int $budgetId): TransactionJournal
{
if ($budgetId === 0) {
$journal->budgets()->detach();
$journal->save();
return $journal;
}
$this->storeBudgetWithJournal($journal, $budgetId);
return $journal;
}
/**
* @param TransactionJournal $journal
* @param string $category
*
* @return TransactionJournal
*/
public function updateCategory(TransactionJournal $journal, string $category): TransactionJournal
{
Log::debug(sprintf('In updateCategory("%s")', $category));
$journal->categories()->detach();
if (strlen($category) === 0) {
return $journal;
}
$this->storeCategoryWithJournal($journal, $category);
return $journal;
}
/**
* Same as above but for transaction journal with multiple transactions.
*
@ -492,4 +532,48 @@ class JournalRepository implements JournalRepositoryInterface
return $journal;
}
/**
* Update tags.
*
* @param TransactionJournal $journal
* @param array $array
*
* @return bool
*/
public function updateTags(TransactionJournal $journal, array $array): bool
{
// create tag repository
/** @var TagRepositoryInterface $tagRepository */
$tagRepository = app(TagRepositoryInterface::class);
// find or create all tags:
$tags = [];
$ids = [];
foreach ($array as $name) {
if (strlen(trim($name)) > 0) {
$tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]);
$tags[] = $tag;
$ids[] = $tag->id;
}
}
// delete all tags connected to journal not in this array:
if (count($ids) > 0) {
DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete();
}
// if count is zero, delete them all:
if (0 === count($ids)) {
DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete();
}
// connect each tag to journal (if not yet connected):
/** @var Tag $tag */
foreach ($tags as $tag) {
Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id));
$tagRepository->connect($journal, $tag);
}
return true;
}
}

View File

@ -160,6 +160,22 @@ interface JournalRepositoryInterface
*/
public function update(TransactionJournal $journal, array $data): TransactionJournal;
/**
* @param TransactionJournal $journal
* @param int $budgetId
*
* @return TransactionJournal
*/
public function updateBudget(TransactionJournal $journal, int $budgetId): TransactionJournal;
/**
* @param TransactionJournal $journal
* @param string $category
*
* @return TransactionJournal
*/
public function updateCategory(TransactionJournal $journal, string $category): TransactionJournal;
/**
* @param TransactionJournal $journal
* @param array $data
@ -167,4 +183,12 @@ interface JournalRepositoryInterface
* @return TransactionJournal
*/
public function updateSplitJournal(TransactionJournal $journal, array $data): TransactionJournal;
/**
* @param TransactionJournal $journal
* @param array $tags
*
* @return bool
*/
public function updateTags(TransactionJournal $journal, array $tags): bool;
}

View File

@ -100,6 +100,7 @@ trait SupportJournalsTrait
$budget = Budget::find($budgetId);
$journal->budgets()->save($budget);
}
$journal->touch();
}
/**
@ -112,6 +113,7 @@ trait SupportJournalsTrait
$category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $journal->user_id]);
$journal->categories()->save($category);
}
$journal->touch();
}
/**
@ -247,6 +249,10 @@ trait SupportJournalsTrait
case TransactionType::WITHDRAWAL:
// continue:
$nativeCurrencyId = intval($accounts[$check]->getMeta('currency_id'));
if ($nativeCurrencyId === 0) {
// fall back to given ID (not everybody upgrades nicely).
$nativeCurrencyId = $submittedCurrencyId;
}
// does not match? Then user has submitted amount in a foreign currency:
if ($nativeCurrencyId !== $submittedCurrencyId) {

View File

@ -37,6 +37,7 @@ use Log;
*/
trait UpdateJournalsTrait
{
/**
* When the user edits a split journal, each line is missing crucial data:.
*
@ -121,47 +122,5 @@ trait UpdateJournalsTrait
$transaction->save();
}
/**
* Update tags.
*
* @param TransactionJournal $journal
* @param array $array
*
* @return bool
*/
protected function updateTags(TransactionJournal $journal, array $array): bool
{
// create tag repository
/** @var TagRepositoryInterface $tagRepository */
$tagRepository = app(TagRepositoryInterface::class);
// find or create all tags:
$tags = [];
$ids = [];
foreach ($array as $name) {
if (strlen(trim($name)) > 0) {
$tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]);
$tags[] = $tag;
$ids[] = $tag->id;
}
}
// delete all tags connected to journal not in this array:
if (count($ids) > 0) {
DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete();
}
// if count is zero, delete them all:
if (0 === count($ids)) {
DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete();
}
// connect each tag to journal (if not yet connected):
/** @var Tag $tag */
foreach ($tags as $tag) {
Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id));
$tagRepository->connect($journal, $tag);
}
return true;
}
}

View File

@ -32,7 +32,7 @@ use SimpleXMLElement;
/**
* Class UpdateRequest
*/
class UpdateRequest implements GitHubRequest
class UpdateRequest implements GithubRequest
{
/** @var array */
private $releases = [];

View File

@ -0,0 +1,32 @@
<?php
/**
* DuplicatedCustomerException.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Exception;
/**
* Class DuplicatedCustomerException
*/
class DuplicatedCustomerException extends SpectreException
{
}

View File

@ -0,0 +1,34 @@
<?php
/**
* SpectreException.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Exception;
use Exception;
/**
* Class SpectreException
*/
class SpectreException extends Exception
{
}

View File

@ -0,0 +1,101 @@
<?php
/**
* Account.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Object;
use Carbon\Carbon;
/**
* Class Account
*/
class Account extends SpectreObject
{
/** @var float */
private $balance;
/** @var Carbon */
private $createdAt;
/** @var string */
private $currencyCode;
/** @var array */
private $extra = [];
/** @var int */
private $id;
/** @var int */
private $loginId;
/** @var string */
private $name;
/** @var string */
private $nature;
/** @var Carbon */
private $updatedAt;
/**
* Account constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->id = $data['id'];
$this->loginId = $data['login_id'];
$this->currencyCode = $data['currency_code'];
$this->balance = $data['balance'];
$this->name = $data['name'];
$this->nature = $data['nature'];
$this->createdAt = new Carbon($data['created_at']);
$this->updatedAt = new Carbon($data['updated_at']);
foreach ($data['extra'] as $key => $value) {
$this->extra[$key] = $value;
}
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return array
*/
public function toArray(): array
{
$array = [
'balance' => $this->balance,
'created_at' => $this->createdAt->toIso8601String(),
'currency_code' => $this->currencyCode,
'extra' => $this->extra,
'id' => $this->id,
'login_id' => $this->loginId,
'name' => $this->name,
'nature' => $this->nature,
'updated_at' => $this->updatedAt->toIso8601String(),
];
return $array;
}
}

View File

@ -0,0 +1,208 @@
<?php
/**
* Attempt.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Object;
use Carbon\Carbon;
/**
* Class Attempt
*/
class Attempt extends SpectreObject
{
/** @var string */
private $apiMode;
/** @var int */
private $apiVersion;
/** @var bool */
private $automaticFetch;
/** @var bool */
private $categorize;
/** @var Carbon */
private $consentGivenAt;
/** @var array */
private $consentTypes = [];
/** @var Carbon */
private $createdAt;
/** @var array */
private $customFields = [];
/** @var bool */
private $dailyRefresh;
/** @var string */
private $deviceType;
/** @var array */
private $excludeAccounts = [];
/** @var Carbon */
private $failAt;
/** @var string */
private $failErrorClass;
/** @var string */
private $failMessage;
/** @var string */
private $fetchType;
/** @var bool */
private $finished;
/** @var bool */
private $finishedRecent;
/** @var Carbon */
private $fromDate;
/** @var int */
private $id;
/** @var bool */
private $interactive;
/** @var string */
private $locale;
/** @var bool */
private $partial;
/** @var string */
private $remoteIp;
/** @var bool */
private $showConsentInformation;
/** @var array */
private $stages = [];
/** @var bool */
private $storeCredentials;
/** @var Carbon */
private $successAt;
/** @var Carbon */
private $toDate;
/** @var Carbon */
private $updatedAt;
/** @var string */
private $userAgent; // undocumented
/**
* Attempt constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->apiMode = $data['api_mode'];
$this->apiVersion = $data['api_version'];
$this->automaticFetch = $data['automatic_fetch'];
$this->categorize = $data['categorize'];
$this->createdAt = new Carbon($data['created_at']);
$this->consentGivenAt = new Carbon($data['consent_given_at']);
$this->consentTypes = $data['consent_types'];
$this->customFields = $data['custom_fields'];
$this->dailyRefresh = $data['daily_refresh'];
$this->deviceType = $data['device_type'];
$this->userAgent = $data['user_agent'] ?? '';
$this->remoteIp = $data['remote_ip'];
$this->excludeAccounts = $data['exclude_accounts'];
$this->failAt = new Carbon($data['fail_at']);
$this->failErrorClass = $data['fail_error_class'];
$this->failMessage = $data['fail_message'];
$this->fetchType = $data['fetch_type'];
$this->finished = $data['finished'];
$this->finishedRecent = $data['finished_recent'];
$this->fromDate = new Carbon($data['from_date']);
$this->id = $data['id'];
$this->interactive = $data['interactive'];
$this->locale = $data['locale'];
$this->partial = $data['partial'];
$this->showConsentInformation = $data['show_consent_confirmation'];
$this->stages = $data['stages'] ?? [];
$this->storeCredentials = $data['store_credentials'];
$this->successAt = new Carbon($data['success_at']);
$this->toDate = new Carbon($data['to_date']);
$this->updatedAt = new Carbon($data['updated_at']);
}
/**
* @return Carbon
*/
public function getCreatedAt(): Carbon
{
return $this->createdAt;
}
/**
* @return Carbon
*/
public function getFailAt(): Carbon
{
return $this->failAt;
}
/**
* @return null|string
*/
public function getFailErrorClass(): ?string
{
return $this->failErrorClass;
}
/**
* @return null|string
*/
public function getFailMessage(): ?string
{
return $this->failMessage;
}
/**
* @return array
*/
public function toArray(): array
{
$array = [
'api_mode' => $this->apiMode,
'api_version' => $this->apiVersion,
'automatic_fetch' => $this->automaticFetch,
'categorize' => $this->categorize,
'created_at' => $this->createdAt->toIso8601String(),
'consent_given_at' => $this->consentGivenAt->toIso8601String(),
'consent_types' => $this->consentTypes,
'custom_fields' => $this->customFields,
'daily_refresh' => $this->dailyRefresh,
'device_type' => $this->deviceType,
'user_agent' => $this->userAgent,
'remote_ip' => $this->remoteIp,
'exclude_accounts' => $this->excludeAccounts,
'fail_at' => $this->failAt->toIso8601String(),
'fail_error_class' => $this->failErrorClass,
'fail_message' => $this->failMessage,
'fetch_type' => $this->fetchType,
'finished' => $this->finished,
'finished_recent' => $this->finishedRecent,
'from_date' => $this->fromDate->toIso8601String(),
'id' => $this->id,
'interactive' => $this->interactive,
'locale' => $this->locale,
'partial' => $this->partial,
'show_consent_confirmation' => $this->showConsentInformation,
'stages' => $this->stages,
'store_credentials' => $this->storeCredentials,
'success_at' => $this->successAt->toIso8601String(),
'to_date' => $this->toDate->toIso8601String(),
'updated_at' => $this->updatedAt->toIso8601String(),
];
return $array;
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Holder.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Object;
/**
* Class Holder
*/
class Holder extends SpectreObject
{
/**
* Holder constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
}
/**
* @return array
*/
public function toArray(): array
{
return [];
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* Login.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Object;
use Carbon\Carbon;
/**
* Class Login
*/
class Login extends SpectreObject
{
/** @var Carbon */
private $consentGivenAt;
/** @var array */
private $consentTypes;
/** @var string */
private $countryCode;
/** @var Carbon */
private $createdAt;
/** @var int */
private $customerId;
/** @var bool */
private $dailyRefresh;
/** @var Holder */
private $holderInfo;
/** @var int */
private $id;
/** @var Attempt */
private $lastAttempt;
/** @var Carbon */
private $lastSuccessAt;
/** @var Carbon */
private $nextRefreshPossibleAt;
/** @var string */
private $providerCode;
/** @var int */
private $providerId;
/** @var string */
private $providerName;
/** @var bool */
private $showConsentConfirmation;
/** @var string */
private $status;
/** @var bool */
private $storeCredentials;
/** @var Carbon */
private $updatedAt;
/**
* Login constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->consentGivenAt = new Carbon($data['consent_given_at']);
$this->consentTypes = $data['consent_types'];
$this->countryCode = $data['country_code'];
$this->createdAt = new Carbon($data['created_at']);
$this->updatedAt = new Carbon($data['updated_at']);
$this->customerId = $data['customer_id'];
$this->dailyRefresh = $data['daily_refresh'];
$this->holderInfo = new Holder($data['holder_info']);
$this->id = $data['id'];
$this->lastAttempt = new Attempt($data['last_attempt']);
$this->lastSuccessAt = new Carbon($data['last_success_at']);
$this->nextRefreshPossibleAt = new Carbon($data['next_refresh_possible_at']);
$this->providerCode = $data['provider_code'];
$this->providerId = $data['provider_id'];
$this->providerName = $data['provider_name'];
$this->showConsentConfirmation = $data['show_consent_confirmation'];
$this->status = $data['status'];
$this->storeCredentials = $data['store_credentials'];
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return Attempt
*/
public function getLastAttempt(): Attempt
{
return $this->lastAttempt;
}
/**
* @return array
*/
public function toArray(): array
{
$array = [
'consent_given_at' => $this->consentGivenAt->toIso8601String(),
'consent_types' => $this->consentTypes,
'country_code' => $this->countryCode,
'created_at' => $this->createdAt->toIso8601String(),
'updated_at' => $this->updatedAt->toIso8601String(),
'customer_id' => $this->customerId,
'daily_refresh' => $this->dailyRefresh,
'holder_info' => $this->holderInfo->toArray(),
'id' => $this->id,
'last_attempt' => $this->lastAttempt->toArray(),
'last_success_at' => $this->lastSuccessAt->toIso8601String(),
'next_refresh_possible_at' => $this->nextRefreshPossibleAt,
'provider_code' => $this->providerCode,
'provider_id' => $this->providerId,
'provider_name' => $this->providerName,
'show_consent_confirmation' => $this->showConsentConfirmation,
'status' => $this->status,
'store_credentials' => $this->storeCredentials,
];
return $array;
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Transaction.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Object;
/**
* Class Transaction
*/
class Transaction extends SpectreObject
{
/**
* Transaction constructor.
*
* @param array $data
*/
public function __construct(array $data) {
var_dump($data);
exit;
}
}

View File

@ -44,6 +44,7 @@ class CreateTokenRequest extends SpectreRequest
/**
*
* @throws \FireflyIII\Exceptions\FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
public function call(): void
{

View File

@ -0,0 +1,93 @@
<?php
/**
* ListAccountsRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Request;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Services\Spectre\Exception\SpectreException;
use FireflyIII\Services\Spectre\Object\Account;
use FireflyIII\Services\Spectre\Object\Login;
use Log;
/**
* Class ListAccountsRequest
*/
class ListAccountsRequest extends SpectreRequest
{
/** @var array */
private $accounts = [];
/** @var Login */
private $login;
/**
* @throws SpectreException
* @throws FireflyException
*/
public function call(): void
{
$hasNextPage = true;
$nextId = 0;
while ($hasNextPage) {
Log::debug(sprintf('Now calling ListAccountsRequest for next_id %d', $nextId));
$parameters = ['from_id' => $nextId, 'login_id' => $this->login->getId()];
$uri = '/api/v3/accounts?' . http_build_query($parameters);
$response = $this->sendSignedSpectreGet($uri, []);
// count entries:
Log::debug(sprintf('Found %d entries in data-array', count($response['data'])));
// extract next ID
$hasNextPage = false;
if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) {
$hasNextPage = true;
$nextId = $response['meta']['next_id'];
Log::debug(sprintf('Next ID is now %d.', $nextId));
} else {
Log::debug('No next page.');
}
// store customers:
foreach ($response['data'] as $accountArray) {
$this->accounts[] = new Account($accountArray);
}
}
}
/**
* @return array
*/
public function getAccounts(): array
{
return $this->accounts;
}
/**
* @param Login $login
*/
public function setLogin(Login $login): void
{
$this->login = $login;
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* ListCustomersRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Request;
use FireflyIII\Services\Spectre\Object\Customer;
use Log;
/**
* Class ListCustomersRequest
*/
class ListCustomersRequest extends SpectreRequest
{
/** @var array */
private $customers = [];
/**
*
* @throws \FireflyIII\Exceptions\FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
public function call(): void
{
$hasNextPage = true;
$nextId = 0;
while ($hasNextPage) {
Log::debug(sprintf('Now calling ListCustomersRequest for next_id %d', $nextId));
$parameters = ['from_id' => $nextId];
$uri = '/api/v3/customers/?' . http_build_query($parameters);
$response = $this->sendSignedSpectreGet($uri, []);
// count entries:
Log::debug(sprintf('Found %d entries in data-array', count($response['data'])));
// extract next ID
$hasNextPage = false;
if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) {
$hasNextPage = true;
$nextId = $response['meta']['next_id'];
Log::debug(sprintf('Next ID is now %d.', $nextId));
} else {
Log::debug('No next page.');
}
// store customers:
foreach ($response['data'] as $customerArray) {
$this->customers[] = new Customer($customerArray);
}
}
}
/**
* @return array
*/
public function getCustomers(): array
{
return $this->customers;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* ListLoginsRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Request;
use FireflyIII\Services\Spectre\Object\Customer;
use FireflyIII\Services\Spectre\Object\Login;
use Log;
/**
* Class ListLoginsRequest
*/
class ListLoginsRequest extends SpectreRequest
{
/** @var Customer */
private $customer;
/** @var array */
private $logins = [];
/**
* @return array
*/
public function getLogins(): array
{
return $this->logins;
}
/**
*
*/
public function call(): void
{
$hasNextPage = true;
$nextId = 0;
while ($hasNextPage) {
Log::debug(sprintf('Now calling ListLoginsRequest for next_id %d', $nextId));
$parameters = ['from_id' => $nextId, 'customer_id' => $this->customer->getId()];
$uri = '/api/v3/logins/?' . http_build_query($parameters);
$response = $this->sendSignedSpectreGet($uri, []);
// count entries:
Log::debug(sprintf('Found %d entries in data-array', count($response['data'])));
// extract next ID
$hasNextPage = false;
if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) {
$hasNextPage = true;
$nextId = $response['meta']['next_id'];
Log::debug(sprintf('Next ID is now %d.', $nextId));
} else {
Log::debug('No next page.');
}
// store logins:
/** @var array $loginArray */
foreach ($response['data'] as $loginArray) {
$this->logins[] = new Login($loginArray);
}
}
}
/**
* @param Customer $customer
*/
public function setCustomer(Customer $customer): void
{
$this->customer = $customer;
}
}

View File

@ -0,0 +1,90 @@
<?php
/**
* ListTransactionsRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Request;
use FireflyIII\Services\Spectre\Object\Account;
use FireflyIII\Services\Spectre\Object\Transaction;
use Log;
/**
* Class ListTransactionsRequest
*/
class ListTransactionsRequest extends SpectreRequest
{
/** @var Account */
private $account;
/** @var array */
private $transactions = [];
/**
*
*/
public function call(): void
{
$hasNextPage = true;
$nextId = 0;
while ($hasNextPage) {
Log::debug(sprintf('Now calling ListTransactionsRequest for next_id %d', $nextId));
$parameters = ['from_id' => $nextId,'account_id' => $this->account->getId()];
$uri = '/api/v3/transactions?' . http_build_query($parameters);
$response = $this->sendSignedSpectreGet($uri, []);
// count entries:
Log::debug(sprintf('Found %d entries in data-array', count($response['data'])));
// extract next ID
$hasNextPage = false;
if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) {
$hasNextPage = true;
$nextId = $response['meta']['next_id'];
Log::debug(sprintf('Next ID is now %d.', $nextId));
} else {
Log::debug('No next page.');
}
// store customers:
foreach ($response['data'] as $transactionArray) {
$this->transactions[] = new Transaction($transactionArray);
}
}
}
/**
* @return array
*/
public function getTransactions(): array
{
return $this->transactions;
}
/**
* @param Account $account
*/
public function setAccount(Account $account): void
{
$this->account = $account;
}
}

View File

@ -34,6 +34,7 @@ class NewCustomerRequest extends SpectreRequest
/**
* @throws \FireflyIII\Exceptions\FireflyException
* @throws \FireflyIII\Services\Spectre\Exception\SpectreException
*/
public function call(): void
{

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Services\Spectre\Request;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Services\Spectre\Exception\DuplicatedCustomerException;
use FireflyIII\Services\Spectre\Exception\SpectreException;
use FireflyIII\User;
use Log;
use Requests;
@ -179,6 +181,7 @@ abstract class SpectreRequest
* @return array
*
* @throws FireflyException
* @throws SpectreException
*/
protected function sendSignedSpectreGet(string $uri, array $data): array
{
@ -222,6 +225,7 @@ abstract class SpectreRequest
* @return array
*
* @throws FireflyException
* @throws SpectreException
*/
protected function sendSignedSpectrePost(string $uri, array $data): array
{
@ -255,15 +259,21 @@ abstract class SpectreRequest
* @param Requests_Response $response
*
* @throws FireflyException
* @throws SpectreException
*/
private function detectError(Requests_Response $response): void
{
$body = $response->body;
$array = json_decode($body, true);
if (isset($array['error_class'])) {
$message = $array['error_message'] ?? '(no message)';
$message = $array['error_message'] ?? '(no message)';
$errorClass = $array['error_class'];
$class = sprintf('\\FireflyIII\\Services\\Spectre\Exception\\%sException', $errorClass);
if (class_exists($class)) {
throw new $class($message);
}
throw new FireflyException(sprintf('Error of class %s: %s', $array['error_class'], $message));
throw new FireflyException(sprintf('Error of class %s: %s', $errorClass, $message));
}
$statusCode = intval($response->status_code);

View File

@ -22,62 +22,48 @@ declare(strict_types=1);
namespace FireflyIII\Support\Import\Configuration\File;
use ExpandedForm;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use Log;
/**
* Class CsvInitial.
* Class Initial.
*/
class Initial implements ConfigurationInterface
{
/**
* @var ImportJob
*/
/** @var ImportJob */
private $job;
/** @var ImportJobRepositoryInterface */
private $repository;
/** @var string */
private $warning = '';
public function __construct()
{
Log::debug('Constructed Initial.');
}
/**
* Get the data necessary to show the configuration screen.
*
* @return array
*/
public function getData(): array
{
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class);
$accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$delimiters = [
',' => trans('form.csv_comma'),
';' => trans('form.csv_semicolon'),
'tab' => trans('form.csv_tab'),
];
$importFileTypes = [];
$defaultImportType = config('import.options.file.default_import_format');
// update job with default date format:
$config = $this->job->configuration;
if (!isset($config['date-format'])) {
$config['date-format'] = 'Ymd';
$this->job->configuration = $config;
$this->job->save();
}
$specifics = [];
// collect specifics.
foreach (config('csv.import_specifics') as $name => $className) {
$specifics[$name] = [
'name' => $className::getName(),
'description' => $className::getDescription(),
];
foreach (config('import.options.file.import_formats') as $type) {
$importFileTypes[$type] = trans('import.import_file_type_' . $type);
}
$data = [
'accounts' => ExpandedForm::makeSelectList($accounts),
'specifix' => [],
'delimiters' => $delimiters,
'specifics' => $specifics,
return [
'default_type' => $defaultImportType,
'file_types' => $importFileTypes,
];
return $data;
}
/**
@ -87,7 +73,7 @@ class Initial implements ConfigurationInterface
*/
public function getWarningMessage(): string
{
return '';
return $this->warning;
}
/**
@ -97,7 +83,9 @@ class Initial implements ConfigurationInterface
*/
public function setJob(ImportJob $job): ConfigurationInterface
{
$this->job = $job;
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($job->user);
return $this;
}
@ -111,59 +99,44 @@ class Initial implements ConfigurationInterface
*/
public function storeConfiguration(array $data): bool
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$importId = $data['csv_import_account'] ?? 0;
$account = $repository->find(intval($importId));
Log::debug('Now in storeConfiguration for file Upload.');
$config = $this->getConfig();
$type = $data['import_file_type'] ?? 'csv'; // assume it's a CSV file.
$config['file-type'] = in_array($type, config('import.options.file.import_formats')) ? $type : 'csv';
$hasHeaders = isset($data['has_headers']) && 1 === intval($data['has_headers']) ? true : false;
$config = $this->job->configuration;
$config['initial-config-complete'] = true;
$config['has-headers'] = $hasHeaders;
$config['date-format'] = $data['date_format'];
$config['delimiter'] = $data['csv_delimiter'];
$config['delimiter'] = 'tab' === $config['delimiter'] ? "\t" : $config['delimiter'];
$config['apply_rules'] = isset($data['apply_rules']) && 1 === intval($data['apply_rules']) ? true : false;
$config['match_bills'] = isset($data['match_bills']) && 1 === intval($data['match_bills']) ? true : false;
// update config:
$this->repository->setConfiguration($this->job, $config);
Log::debug('Entered import account.', ['id' => $importId]);
// make repository process file:
$uploaded = $this->repository->processFile($this->job, $data['import_file'] ?? null);
Log::debug(sprintf('Result of upload is %s', var_export($uploaded, true)));
if (null !== $account->id) {
Log::debug('Found account.', ['id' => $account->id, 'name' => $account->name]);
$config['import-account'] = $account->id;
// process config, if present:
if (isset($data['configuration_file'])) {
$this->repository->processConfiguration($this->job, $data['configuration_file']);
}
if (null === $account->id) {
Log::error('Could not find anything for csv_import_account.', ['id' => $importId]);
if (false === $uploaded) {
$this->warning = 'No valid upload.';
return true;
}
$config = $this->storeSpecifics($data, $config);
$this->job->configuration = $config;
$this->job->save();
// if file was upload safely, assume we can go to the next stage:
$config = $this->getConfig();
$config['stage'] = 'upload-config';
$this->repository->setConfiguration($this->job, $config);
return true;
}
/**
* @param array $data
* @param array $config
* Short hand method.
*
* @return array
*/
private function storeSpecifics(array $data, array $config): array
private function getConfig(): array
{
// loop specifics.
if (isset($data['specifics']) && is_array($data['specifics'])) {
$names = array_keys($data['specifics']);
foreach ($names as $name) {
// verify their content.
$className = sprintf('FireflyIII\Import\Specifics\%s', $name);
if (class_exists($className)) {
$config['specifics'][$name] = 1;
}
}
}
return $config;
return $this->repository->getConfiguration($this->job);
}
}

View File

@ -27,6 +27,7 @@ use FireflyIII\Import\Mapper\MapperInterface;
use FireflyIII\Import\MapperPreProcess\PreProcessorInterface;
use FireflyIII\Import\Specifics\SpecificInterface;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use League\Csv\Reader;
use League\Csv\Statement;
@ -37,12 +38,12 @@ use Log;
*/
class Map implements ConfigurationInterface
{
/** @var array */
private $configuration = [];
/** @var array that holds each column to be mapped by the user */
private $data = [];
/** @var ImportJob */
private $job;
/** @var ImportJobRepositoryInterface */
private $repository;
/** @var array */
private $validSpecifics = [];
@ -55,16 +56,16 @@ class Map implements ConfigurationInterface
*/
public function getData(): array
{
$this->configuration = $this->job->configuration;
$this->getMappableColumns();
// in order to actually map we also need all possible values from the CSV file.
$content = $this->job->uploadFileContents();
$config = $this->getConfig();
$content = $this->repository->uploadFileContents($this->job);
$offset = 0;
/** @var Reader $reader */
$reader = Reader::createFromString($content);
$reader->setDelimiter($this->configuration['delimiter']);
if ($this->configuration['has-headers']) {
$reader->setDelimiter($config['delimiter']);
if ($config['has-headers']) {
$offset = 1;
}
$stmt = (new Statement)->offset($offset);
@ -106,6 +107,10 @@ class Map implements ConfigurationInterface
foreach ($setIndexes as $index) {
$this->data[$index]['values'] = array_unique($this->data[$index]['values']);
asort($this->data[$index]['values']);
// if the count of this array is zero, there is nothing to map.
if (count($this->data[$index]['values']) === 0) {
unset($this->data[$index]);
}
}
unset($setIndexes);
@ -136,7 +141,9 @@ class Map implements ConfigurationInterface
*/
public function setJob(ImportJob $job): ConfigurationInterface
{
$this->job = $job;
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($job->user);
return $this;
}
@ -150,7 +157,8 @@ class Map implements ConfigurationInterface
*/
public function storeConfiguration(array $data): bool
{
$config = $this->job->configuration;
$config = $this->getConfig();
if (isset($data['mapping'])) {
foreach ($data['mapping'] as $index => $data) {
$config['column-mapping-config'][$index] = [];
@ -164,9 +172,8 @@ class Map implements ConfigurationInterface
}
// set thing to be completed.
$config['column-mapping-complete'] = true;
$this->job->configuration = $config;
$this->job->save();
$config['stage'] = 'ready';
$this->saveConfig($config);
return true;
}
@ -181,11 +188,21 @@ class Map implements ConfigurationInterface
$mapperClass = config('csv.import_roles.' . $column . '.mapper');
$mapperName = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass);
/** @var MapperInterface $mapper */
$mapper = new $mapperName;
$mapper = app($mapperName);
return $mapper;
}
/**
* Short hand method.
*
* @return array
*/
private function getConfig(): array
{
return $this->repository->getConfiguration($this->job);
}
/**
* @return bool
*
@ -193,8 +210,7 @@ class Map implements ConfigurationInterface
*/
private function getMappableColumns(): bool
{
$config = $this->job->configuration;
$config = $this->getConfig();
/**
* @var int
* @var bool $mustBeMapped
@ -246,7 +262,9 @@ class Map implements ConfigurationInterface
{
// run specifics here:
// and this is the point where the specifix go to work.
$names = array_keys($this->job->configuration['specifics']);
$config = $this->getConfig();
$specifics = $config['specifics'] ?? [];
$names = array_keys($specifics);
foreach ($names as $name) {
if (!in_array($name, $this->validSpecifics)) {
throw new FireflyException(sprintf('"%s" is not a valid class name', $name));
@ -262,6 +280,14 @@ class Map implements ConfigurationInterface
return $row;
}
/**
* @param array $array
*/
private function saveConfig(array $array)
{
$this->repository->setConfiguration($this->job, $array);
}
/**
* @param string $column
* @param bool $mustBeMapped

View File

@ -24,6 +24,7 @@ namespace FireflyIII\Support\Import\Configuration\File;
use FireflyIII\Import\Specifics\SpecificInterface;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use League\Csv\Reader;
use League\Csv\Statement;
@ -40,7 +41,8 @@ class Roles implements ConfigurationInterface
private $data = [];
/** @var ImportJob */
private $job;
/** @var ImportJobRepositoryInterface */
private $repository;
/** @var string */
private $warning = '';
@ -54,8 +56,8 @@ class Roles implements ConfigurationInterface
*/
public function getData(): array
{
$config = $this->job->configuration;
$content = $this->job->uploadFileContents();
$content = $this->repository->uploadFileContents($this->job);
$config = $this->getConfig();
$headers = [];
$offset = 0;
// create CSV reader.
@ -112,7 +114,9 @@ class Roles implements ConfigurationInterface
*/
public function setJob(ImportJob $job): ConfigurationInterface
{
$this->job = $job;
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($job->user);
return $this;
}
@ -127,26 +131,40 @@ class Roles implements ConfigurationInterface
public function storeConfiguration(array $data): bool
{
Log::debug('Now in storeConfiguration of Roles.');
$config = $this->job->configuration;
$config = $this->getConfig();
$count = $config['column-count'];
for ($i = 0; $i < $count; ++$i) {
$role = $data['role'][$i] ?? '_ignore';
$mapping = isset($data['map'][$i]) && $data['map'][$i] === '1' ? true : false;
$config['column-roles'][$i] = $role;
$config['column-do-mapping'][$i] = $mapping;
Log::debug(sprintf('Column %d has been given role %s', $i, $role));
Log::debug(sprintf('Column %d has been given role %s (mapping: %s)', $i, $role, var_export($mapping, true)));
}
$this->job->configuration = $config;
$this->job->save();
$this->saveConfig($config);
$this->ignoreUnmappableColumns();
$this->setRolesComplete();
$config = $this->getConfig();
$config['stage'] = 'map';
$this->saveConfig($config);
$this->isMappingNecessary();
return true;
}
/**
* Short hand method.
*
* @return array
*/
private function getConfig(): array
{
return $this->repository->getConfiguration($this->job);
}
/**
* @return array
*/
@ -165,11 +183,12 @@ class Roles implements ConfigurationInterface
*/
private function ignoreUnmappableColumns(): bool
{
$config = $this->job->configuration;
$config = $this->getConfig();
$count = $config['column-count'];
for ($i = 0; $i < $count; ++$i) {
$role = $config['column-roles'][$i] ?? '_ignore';
$mapping = $config['column-do-mapping'][$i] ?? false;
Log::debug(sprintf('Role for column %d is %s, and mapping is %s', $i, $role, var_export($mapping, true)));
if ('_ignore' === $role && true === $mapping) {
$mapping = false;
@ -177,9 +196,7 @@ class Roles implements ConfigurationInterface
}
$config['column-do-mapping'][$i] = $mapping;
}
$this->job->configuration = $config;
$this->job->save();
$this->saveConfig($config);
return true;
}
@ -189,7 +206,7 @@ class Roles implements ConfigurationInterface
*/
private function isMappingNecessary()
{
$config = $this->job->configuration;
$config = $this->getConfig();
$count = $config['column-count'];
$toBeMapped = 0;
for ($i = 0; $i < $count; ++$i) {
@ -200,12 +217,11 @@ class Roles implements ConfigurationInterface
}
Log::debug(sprintf('Found %d columns that need mapping.', $toBeMapped));
if (0 === $toBeMapped) {
// skip setting of map, because none need to be mapped:
$config['column-mapping-complete'] = true;
$this->job->configuration = $config;
$this->job->save();
$config['stage'] = 'ready';
}
$this->saveConfig($config);
return true;
}
@ -248,7 +264,9 @@ class Roles implements ConfigurationInterface
*/
private function processSpecifics(array $row): array
{
$names = array_keys($this->job->configuration['specifics'] ?? []);
$config = $this->getConfig();
$specifics = $config['specifics'] ?? [];
$names = array_keys($specifics);
foreach ($names as $name) {
/** @var SpecificInterface $specific */
$specific = app('FireflyIII\Import\Specifics\\' . $name);
@ -258,12 +276,20 @@ class Roles implements ConfigurationInterface
return $row;
}
/**
* @param array $array
*/
private function saveConfig(array $array)
{
$this->repository->setConfiguration($this->job, $array);
}
/**
* @return bool
*/
private function setRolesComplete(): bool
{
$config = $this->job->configuration;
$config = $this->getConfig();
$count = $config['column-count'];
$assigned = 0;
$hasAmount = false;
@ -278,13 +304,12 @@ class Roles implements ConfigurationInterface
}
if ($assigned > 0 && $hasAmount) {
$config['column-roles-complete'] = true;
$this->job->configuration = $config;
$this->job->save();
$this->warning = '';
$this->warning = '';
}
if (0 === $assigned || !$hasAmount) {
$this->warning = strval(trans('import.roles_warning'));
}
$this->saveConfig($config);
return true;
}
@ -294,11 +319,10 @@ class Roles implements ConfigurationInterface
*/
private function updateColumCount(): bool
{
$config = $this->job->configuration;
$count = $this->data['total'];
$config['column-count'] = $count;
$this->job->configuration = $config;
$this->job->save();
$config = $this->getConfig();
$count = $this->data['total'];
$config['column-count'] = $count;
$this->saveConfig($config);
return true;
}

View File

@ -1,116 +0,0 @@
<?php
/**
* Upload.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\Configuration\File;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use Log;
/**
* Class Upload.
*/
class Upload implements ConfigurationInterface
{
/** @var ImportJob */
private $job;
/** @var string */
private $warning = '';
/**
* Get the data necessary to show the configuration screen.
*
* @return array
*/
public function getData(): array
{
$importFileTypes = [];
$defaultImportType = config('import.options.file.default_import_format');
foreach (config('import.options.file.import_formats') as $type) {
$importFileTypes[$type] = trans('import.import_file_type_' . $type);
}
return [
'default_type' => $defaultImportType,
'file_types' => $importFileTypes,
];
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return $this->warning;
}
/**
* @param ImportJob $job
*
* @return ConfigurationInterface
*/
public function setJob(ImportJob $job): ConfigurationInterface
{
$this->job = $job;
return $this;
}
/**
* Store the result.
*
* @param array $data
*
* @return bool
*/
public function storeConfiguration(array $data): bool
{
Log::debug('Now in storeConfiguration for file Upload.');
/** @var ImportJobRepositoryInterface $repository */
$repository = app(ImportJobRepositoryInterface::class);
$type = $data['import_file_type'] ?? 'unknown';
$config = $this->job->configuration;
$config['file-type'] = in_array($type, config('import.options.file.import_formats')) ? $type : 'unknown';
$repository->setConfiguration($this->job, $config);
$uploaded = $repository->processFile($this->job, $data['import_file'] ?? null);
$this->job->save();
Log::debug(sprintf('Result of upload is %s', var_export($uploaded, true)));
// process config, if present:
if (isset($data['configuration_file'])) {
$repository->processConfiguration($this->job, $data['configuration_file']);
}
$config = $this->job->configuration;
$config['has-file-upload'] = $uploaded;
$repository->setConfiguration($this->job, $config);
if (false === $uploaded) {
$this->warning = 'No valid upload.';
}
return true;
}
}

View File

@ -0,0 +1,191 @@
<?php
/**
* UploadConfig.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\Configuration\File;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use Log;
/**
* Class UploadConfig.
*/
class UploadConfig implements ConfigurationInterface
{
/** @var AccountRepositoryInterface */
private $accountRepository;
/**
* @var ImportJob
*/
private $job;
/** @var ImportJobRepositoryInterface */
private $repository;
/**
* @return array
*/
public function getData(): array
{
$accounts = $this->accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$delimiters = [
',' => trans('form.csv_comma'),
';' => trans('form.csv_semicolon'),
'tab' => trans('form.csv_tab'),
];
$config = $this->getConfig();
$config['date-format'] = $config['date-format'] ?? 'Ymd';
$specifics = [];
$this->saveConfig($config);
// collect specifics.
foreach (config('csv.import_specifics') as $name => $className) {
$specifics[$name] = [
'name' => $className::getName(),
'description' => $className::getDescription(),
];
}
$data = [
'accounts' => app('expandedform')->makeSelectList($accounts),
'specifix' => [],
'delimiters' => $delimiters,
'specifics' => $specifics,
];
return $data;
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return '';
}
/**
* @param ImportJob $job
*
* @return ConfigurationInterface
*/
public function setJob(ImportJob $job): ConfigurationInterface
{
$this->job = $job;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->repository->setUser($job->user);
$this->accountRepository->setUser($job->user);
return $this;
}
/**
* Store the result.
*
* @param array $data
*
* @return bool
*/
public function storeConfiguration(array $data): bool
{
Log::debug('Now in Initial::storeConfiguration()');
$config = $this->getConfig();
$importId = intval($data['csv_import_account'] ?? 0);
$account = $this->accountRepository->find($importId);
$delimiter = strval($data['csv_delimiter']);
// set "headers":
$config['has-headers'] = intval($data['has_headers'] ?? 0) === 1;
$config['date-format'] = strval($data['date_format']);
$config['delimiter'] = 'tab' === $delimiter ? "\t" : $config['delimiter'];
$config['apply-rules'] = intval($data['apply_rules'] ?? 0) === 1;
$config['match-bills'] = intval($data['match_bills'] ?? 0) === 1;
Log::debug('Entered import account.', ['id' => $importId]);
if (null !== $account->id) {
Log::debug('Found account.', ['id' => $account->id, 'name' => $account->name]);
$config['import-account'] = $account->id;
}
if (null === $account->id) {
Log::error('Could not find anything for csv_import_account.', ['id' => $importId]);
}
$config = $this->storeSpecifics($data, $config);
Log::debug('Final config is ', $config);
// onto the next stage!
$config['stage'] = 'roles';
$this->saveConfig($config);
return true;
}
/**
* Short hand method.
*
* @return array
*/
private function getConfig(): array
{
return $this->repository->getConfiguration($this->job);
}
/**
* @param array $array
*/
private function saveConfig(array $array)
{
$this->repository->setConfiguration($this->job, $array);
}
/**
* @param array $data
* @param array $config
*
* @return array
*/
private function storeSpecifics(array $data, array $config): array
{
// loop specifics.
if (isset($data['specifics']) && is_array($data['specifics'])) {
$names = array_keys($data['specifics']);
foreach ($names as $name) {
// verify their content.
$className = sprintf('FireflyIII\Import\Specifics\%s', $name);
if (class_exists($className)) {
$config['specifics'][$name] = 1;
}
}
}
return $config;
}
}

View File

@ -0,0 +1,157 @@
<?php
/**
* HaveAccounts.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\Configuration\Spectre;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\Import\Configuration\ConfigurationInterface;
use Illuminate\Support\Collection;
/**
* Class HaveAccounts
*/
class HaveAccounts implements ConfigurationInterface
{
/** @var ImportJob */
private $job;
/**
* Get the data necessary to show the configuration screen.
*
* @return array
*/
public function getData(): array
{
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class);
/** @var CurrencyRepositoryInterface $currencyRepository */
$currencyRepository = app(CurrencyRepositoryInterface::class);
$data = [];
$config = $this->job->configuration;
$collection = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$defaultCurrency = app('amount')->getDefaultCurrency();
$dbAccounts = [];
/** @var Account $dbAccount */
foreach ($collection as $dbAccount) {
$id = $dbAccount->id;
$currencyId = intval($dbAccount->getMeta('currency_id'));
$currency = $currencyRepository->find($currencyId);
$dbAccounts[$id] = [
'account' => $dbAccount,
'currency' => is_null($currency->id) ? $defaultCurrency : $currency,
];
}
// loop Spectre accounts:
/**
* @var int $index
* @var array $spectreAccount
*/
foreach ($config['accounts'] as $index => $spectreAccount) {
// find accounts with currency code
$code = $spectreAccount['currency_code'];
$selection = $this->filterAccounts($dbAccounts, $code);
$config['accounts'][$index]['options'] = app('expandedform')->makeSelectList($selection);
}
$data = [
'config' => $config,
];
return $data;
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return '';
}
/**
* @param ImportJob $job
*
* @return ConfigurationInterface
*/
public function setJob(ImportJob $job)
{
$this->job = $job;
return $this;
}
/**
* Store the result.
*
* @param array $data
*
* @return bool
*/
public function storeConfiguration(array $data): bool
{
$accounts = $data['spectre_account_id'] ?? [];
$mapping = [];
foreach ($accounts as $spectreId) {
$spectreId = intval($spectreId);
$doImport = intval($data['do_import'][$spectreId] ?? 0) === 1;
$account = intval($data['import'][$spectreId] ?? 0);
if ($doImport) {
$mapping[$spectreId] = $account;
}
}
$config = $this->job->configuration;
$config['accounts-mapped'] = $mapping;
$this->job->configuration = $config;
$this->job->save();
return true;
}
/**
* @param array $dbAccounts
* @param string $code
*
* @return Collection
*/
private function filterAccounts(array $dbAccounts, string $code): Collection
{
$collection = new Collection;
foreach ($dbAccounts as $accountId => $data) {
if ($data['currency']->code === $code) {
$collection->push($data['account']);
}
}
return $collection;
}
}

View File

@ -38,6 +38,7 @@ class SingleCacheProperties extends CacheProperties
$this->properties = new Collection;
if (auth()->check()) {
$this->addProperty(auth()->user()->id);
$this->addProperty(app('preferences')->lastActivity());
}
}
}

View File

@ -72,6 +72,7 @@ class Steam
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d'))
->where('transactions.foreign_currency_id', $currencyId)
->where('transactions.transaction_currency_id', '!=', $currencyId)
->sum('transactions.foreign_amount')
);
$balance = bcadd($nativeBalance, $foreignBalance);
@ -114,6 +115,7 @@ class Steam
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d'))
->where('transactions.foreign_currency_id', $currencyId)
->where('transactions.transaction_currency_id', '!=', $currencyId)
->sum('transactions.foreign_amount')
);
$balance = bcadd($nativeBalance, $foreignBalance);

View File

@ -99,7 +99,7 @@
},
"scripts": {
"pre-install-cmd": [
"if [ -z ${DYNO+x} ]; then echo Not in Heroku environment.; else php -r \"file_exists('.env') || copy('.env.heroku', '.env');\"; fi"
"@php -r \"if (!(getenv('DYNO'))===false){file_exists('.env') || copy('.env.heroku', '.env');}\""
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

206
composer.lock generated
View File

@ -947,34 +947,34 @@
},
{
"name": "league/commonmark",
"version": "0.16.0",
"version": "0.17.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "c0e41be0f80c51ad3170c9c713f74a0b8dec59ce"
"reference": "3b4c2224524776a584de663c7a04bc8eb2e1544d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c0e41be0f80c51ad3170c9c713f74a0b8dec59ce",
"reference": "c0e41be0f80c51ad3170c9c713f74a0b8dec59ce",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3b4c2224524776a584de663c7a04bc8eb2e1544d",
"reference": "3b4c2224524776a584de663c7a04bc8eb2e1544d",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.4.8"
"php": ">=5.6.5"
},
"replace": {
"colinodell/commonmark-php": "*"
},
"require-dev": {
"cebe/markdown": "~1.0",
"commonmark/commonmark.js": "0.28",
"erusev/parsedown": "~1.0",
"jgm/commonmark": "0.28",
"michelf/php-markdown": "~1.4",
"mikehaertl/php-shellcommand": "~1.2.0",
"phpunit/phpunit": "^4.8.35|~5.7",
"phpunit/phpunit": "~5.7|~6.5",
"scrutinizer/ocular": "~1.1",
"symfony/finder": "~2.3|~3.0"
"symfony/finder": "~3.0|~4.0"
},
"suggest": {
"league/commonmark-extras": "Library of useful extensions including smart punctuation"
@ -985,7 +985,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.17-dev"
"dev-master": "0.18-dev"
}
},
"autoload": {
@ -1012,7 +1012,7 @@
"markdown",
"parser"
],
"time": "2017-10-31T00:49:55+00:00"
"time": "2017-12-30T22:08:48+00:00"
},
{
"name": "league/csv",
@ -1448,16 +1448,16 @@
},
{
"name": "pragmarx/google2fa",
"version": "v2.0.6",
"version": "v2.0.7",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46"
"reference": "5a818bda62fab0c0a79060b06d50d50b5525d631"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/bc2d654305e4d09254125f8cd390a7fbc4742d46",
"reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/5a818bda62fab0c0a79060b06d50d50b5525d631",
"reference": "5a818bda62fab0c0a79060b06d50d50b5525d631",
"shasum": ""
},
"require": {
@ -1468,8 +1468,7 @@
},
"require-dev": {
"bacon/bacon-qr-code": "~1.0",
"phpspec/phpspec": "~2.1",
"phpunit/phpunit": "~4"
"phpunit/phpunit": "~4|~5|~6"
},
"suggest": {
"bacon/bacon-qr-code": "Required to generate inline QR Codes."
@ -1483,7 +1482,8 @@
},
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/"
"PragmaRX\\Google2FA\\": "src/",
"PragmaRX\\Google2FA\\Tests\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -1505,7 +1505,7 @@
"google2fa",
"laravel"
],
"time": "2017-09-12T06:55:05+00:00"
"time": "2018-01-06T16:21:07+00:00"
},
{
"name": "pragmarx/google2fa-laravel",
@ -1974,16 +1974,16 @@
},
{
"name": "symfony/console",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "9f21adfb92a9315b73ae2ed43138988ee4913d4e"
"reference": "8394c8ef121949e8f858f13bc1e34f05169e4e7d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/9f21adfb92a9315b73ae2ed43138988ee4913d4e",
"reference": "9f21adfb92a9315b73ae2ed43138988ee4913d4e",
"url": "https://api.github.com/repos/symfony/console/zipball/8394c8ef121949e8f858f13bc1e34f05169e4e7d",
"reference": "8394c8ef121949e8f858f13bc1e34f05169e4e7d",
"shasum": ""
},
"require": {
@ -2039,20 +2039,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:40:10+00:00"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/css-selector",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "eac760b414cf1f64362c3dd047b989e4db121332"
"reference": "e66394bc7610e69279bfdb3ab11b4fe65403f556"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/eac760b414cf1f64362c3dd047b989e4db121332",
"reference": "eac760b414cf1f64362c3dd047b989e4db121332",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/e66394bc7610e69279bfdb3ab11b4fe65403f556",
"reference": "e66394bc7610e69279bfdb3ab11b4fe65403f556",
"shasum": ""
},
"require": {
@ -2092,20 +2092,20 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:40:10+00:00"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/debug",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "543deab3ffff94402440b326fc94153bae2dfa7a"
"reference": "603b95dda8b00020e4e6e60dc906e7b715b1c245"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/543deab3ffff94402440b326fc94153bae2dfa7a",
"reference": "543deab3ffff94402440b326fc94153bae2dfa7a",
"url": "https://api.github.com/repos/symfony/debug/zipball/603b95dda8b00020e4e6e60dc906e7b715b1c245",
"reference": "603b95dda8b00020e4e6e60dc906e7b715b1c245",
"shasum": ""
},
"require": {
@ -2148,20 +2148,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2017-12-12T08:27:14+00:00"
"time": "2018-01-03T17:14:19+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v4.0.2",
"version": "v4.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "d4face19ed8002eec8280bc1c5ec18130472bf43"
"reference": "74d33aac36208c4d6757807d9f598f0133a3a4eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d4face19ed8002eec8280bc1c5ec18130472bf43",
"reference": "d4face19ed8002eec8280bc1c5ec18130472bf43",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/74d33aac36208c4d6757807d9f598f0133a3a4eb",
"reference": "74d33aac36208c4d6757807d9f598f0133a3a4eb",
"shasum": ""
},
"require": {
@ -2211,20 +2211,20 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:48:22+00:00"
"time": "2018-01-03T07:38:00+00:00"
},
{
"name": "symfony/finder",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "dac8d7db537bac7ad8143eb11360a8c2231f251a"
"reference": "613e26310776f49a1773b6737c6bd554b8bc8c6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/dac8d7db537bac7ad8143eb11360a8c2231f251a",
"reference": "dac8d7db537bac7ad8143eb11360a8c2231f251a",
"url": "https://api.github.com/repos/symfony/finder/zipball/613e26310776f49a1773b6737c6bd554b8bc8c6f",
"reference": "613e26310776f49a1773b6737c6bd554b8bc8c6f",
"shasum": ""
},
"require": {
@ -2260,20 +2260,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2017-11-05T16:10:10+00:00"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "59bf131b5460227a2f583a7dbe6b179f98f9e0a5"
"reference": "4a213be1cc8598089b8c7451529a2927b49b5d26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/59bf131b5460227a2f583a7dbe6b179f98f9e0a5",
"reference": "59bf131b5460227a2f583a7dbe6b179f98f9e0a5",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/4a213be1cc8598089b8c7451529a2927b49b5d26",
"reference": "4a213be1cc8598089b8c7451529a2927b49b5d26",
"shasum": ""
},
"require": {
@ -2314,20 +2314,20 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:40:10+00:00"
"time": "2018-01-03T17:14:19+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "48325096bbda77b983e642d21a4dd9bdde3ab73e"
"reference": "1c2a82d6a8ec9b354fe4ef48ad1ad3f1a4f7db0e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/48325096bbda77b983e642d21a4dd9bdde3ab73e",
"reference": "48325096bbda77b983e642d21a4dd9bdde3ab73e",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/1c2a82d6a8ec9b354fe4ef48ad1ad3f1a4f7db0e",
"reference": "1c2a82d6a8ec9b354fe4ef48ad1ad3f1a4f7db0e",
"shasum": ""
},
"require": {
@ -2402,7 +2402,7 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
"time": "2017-12-15T02:05:18+00:00"
"time": "2018-01-05T08:33:00+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@ -2632,16 +2632,16 @@
},
{
"name": "symfony/process",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "bb3ef65d493a6d57297cad6c560ee04e2a8f5098"
"reference": "ff69f110c6b33fd33cd2089ba97d6112f44ef0ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/bb3ef65d493a6d57297cad6c560ee04e2a8f5098",
"reference": "bb3ef65d493a6d57297cad6c560ee04e2a8f5098",
"url": "https://api.github.com/repos/symfony/process/zipball/ff69f110c6b33fd33cd2089ba97d6112f44ef0ba",
"reference": "ff69f110c6b33fd33cd2089ba97d6112f44ef0ba",
"shasum": ""
},
"require": {
@ -2677,20 +2677,20 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:40:10+00:00"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/routing",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "5f248dfac5e4660c74982eb3dadc71cf58595570"
"reference": "e2b6d6fe7b090c7af720b75c7722c6dfa7a52658"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/5f248dfac5e4660c74982eb3dadc71cf58595570",
"reference": "5f248dfac5e4660c74982eb3dadc71cf58595570",
"url": "https://api.github.com/repos/symfony/routing/zipball/e2b6d6fe7b090c7af720b75c7722c6dfa7a52658",
"reference": "e2b6d6fe7b090c7af720b75c7722c6dfa7a52658",
"shasum": ""
},
"require": {
@ -2755,20 +2755,20 @@
"uri",
"url"
],
"time": "2017-12-14T22:37:31+00:00"
"time": "2018-01-04T15:09:34+00:00"
},
{
"name": "symfony/translation",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "4c5d5582baf2829751a5207659329c1f52eedeb6"
"reference": "17b5962d252b2d6d1d37a2485ebb7ddc5b2bef0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/4c5d5582baf2829751a5207659329c1f52eedeb6",
"reference": "4c5d5582baf2829751a5207659329c1f52eedeb6",
"url": "https://api.github.com/repos/symfony/translation/zipball/17b5962d252b2d6d1d37a2485ebb7ddc5b2bef0a",
"reference": "17b5962d252b2d6d1d37a2485ebb7ddc5b2bef0a",
"shasum": ""
},
"require": {
@ -2823,20 +2823,20 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2017-12-12T08:27:14+00:00"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "757074cf71b952ce9e75b557538948811c2bf006"
"reference": "545be7e78ccbec43e599f10ff7500d0b09eda9d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/757074cf71b952ce9e75b557538948811c2bf006",
"reference": "757074cf71b952ce9e75b557538948811c2bf006",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/545be7e78ccbec43e599f10ff7500d0b09eda9d0",
"reference": "545be7e78ccbec43e599f10ff7500d0b09eda9d0",
"shasum": ""
},
"require": {
@ -2892,7 +2892,7 @@
"debug",
"dump"
],
"time": "2017-12-11T22:06:16+00:00"
"time": "2018-01-03T17:14:19+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@ -4598,16 +4598,16 @@
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "5.0.5",
"version": "5.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933"
"reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/283b9f4f670e3a6fd6c4ff95c51a952eb5c75933",
"reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
"reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
"shasum": ""
},
"require": {
@ -4653,7 +4653,7 @@
"mock",
"xunit"
],
"time": "2017-12-10T08:01:53+00:00"
"time": "2018-01-06T05:45:45+00:00"
},
{
"name": "psr/http-message",
@ -5266,16 +5266,16 @@
},
{
"name": "symfony/class-loader",
"version": "v3.4.2",
"version": "v3.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",
"reference": "e8d36a7b5568d232f5c3f8ef92665836b9f1e038"
"reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/class-loader/zipball/e8d36a7b5568d232f5c3f8ef92665836b9f1e038",
"reference": "e8d36a7b5568d232f5c3f8ef92665836b9f1e038",
"url": "https://api.github.com/repos/symfony/class-loader/zipball/e63c12699822bb3b667e7216ba07fbcc3a3e203e",
"reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e",
"shasum": ""
},
"require": {
@ -5318,20 +5318,20 @@
],
"description": "Symfony ClassLoader Component",
"homepage": "https://symfony.com",
"time": "2017-11-05T16:10:10+00:00"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/config",
"version": "v4.0.2",
"version": "v4.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "0356e6d5298e9e72212c0bad65c2f1b49e42d622"
"reference": "0e86d267db0851cf55f339c97df00d693fe8592f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/0356e6d5298e9e72212c0bad65c2f1b49e42d622",
"reference": "0356e6d5298e9e72212c0bad65c2f1b49e42d622",
"url": "https://api.github.com/repos/symfony/config/zipball/0e86d267db0851cf55f339c97df00d693fe8592f",
"reference": "0e86d267db0851cf55f339c97df00d693fe8592f",
"shasum": ""
},
"require": {
@ -5378,20 +5378,20 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:48:22+00:00"
"time": "2018-01-03T07:38:00+00:00"
},
{
"name": "symfony/filesystem",
"version": "v4.0.2",
"version": "v4.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "8c2868641d0c4885eee9c12a89c2b695eb1985cd"
"reference": "760e47a4ee64b4c48f4b30017011e09d4c0f05ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/8c2868641d0c4885eee9c12a89c2b695eb1985cd",
"reference": "8c2868641d0c4885eee9c12a89c2b695eb1985cd",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/760e47a4ee64b4c48f4b30017011e09d4c0f05ed",
"reference": "760e47a4ee64b4c48f4b30017011e09d4c0f05ed",
"shasum": ""
},
"require": {
@ -5427,20 +5427,20 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2017-12-14T19:48:22+00:00"
"time": "2018-01-03T07:38:00+00:00"
},
{
"name": "symfony/stopwatch",
"version": "v4.0.2",
"version": "v4.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "ac0e49150555c703fef6b696d8eaba1db7a3ca03"
"reference": "d52321f0e2b596bd03b5d1dd6eebe71caa925704"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/ac0e49150555c703fef6b696d8eaba1db7a3ca03",
"reference": "ac0e49150555c703fef6b696d8eaba1db7a3ca03",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/d52321f0e2b596bd03b5d1dd6eebe71caa925704",
"reference": "d52321f0e2b596bd03b5d1dd6eebe71caa925704",
"shasum": ""
},
"require": {
@ -5476,20 +5476,20 @@
],
"description": "Symfony Stopwatch Component",
"homepage": "https://symfony.com",
"time": "2017-11-09T12:45:29+00:00"
"time": "2018-01-03T07:38:00+00:00"
},
{
"name": "symfony/yaml",
"version": "v4.0.2",
"version": "v4.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "a5ee52d155f06ad23b19eb63c31228ff56ad1116"
"reference": "b84f646b9490d2101e2c25ddeec77ceefbda2eee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/a5ee52d155f06ad23b19eb63c31228ff56ad1116",
"reference": "a5ee52d155f06ad23b19eb63c31228ff56ad1116",
"url": "https://api.github.com/repos/symfony/yaml/zipball/b84f646b9490d2101e2c25ddeec77ceefbda2eee",
"reference": "b84f646b9490d2101e2c25ddeec77ceefbda2eee",
"shasum": ""
},
"require": {
@ -5534,7 +5534,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-12-12T08:41:51+00:00"
"time": "2018-01-03T07:38:00+00:00"
},
{
"name": "theseer/tokenizer",

View File

@ -18,7 +18,7 @@ return [
|
*/
'view' => 'breadcrumbs::bootstrap3',
'view' => 'partials/breadcrumbs',
/*
|--------------------------------------------------------------------------

View File

@ -320,18 +320,4 @@ return [
// number of example rows:
'example_rows' => 5,
'default_config' => [
'initial-config-complete' => false,
'has-headers' => false, // assume
'date-format' => 'Ymd', // assume
'delimiter' => ',', // assume
'import-account' => 0, // none,
'specifics' => [], // none
'column-count' => 0, // unknown
'column-roles' => [], // unknown
'column-do-mapping' => [], // not yet set which columns must be mapped
'column-roles-complete' => false, // not yet configured roles for columns
'column-mapping-config' => [], // no mapping made yet.
'column-mapping-complete' => false, // so mapping is not complete.
],
];

View File

@ -33,7 +33,7 @@ return [
'is_demo_site' => false,
],
'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true),
'version' => '4.6.12',
'version' => '4.6.13',
'maxUploadSize' => 15242880,
'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf', 'text/plain'],
'list_length' => 10,
@ -119,6 +119,7 @@ return [
'id_ID' => ['name_locale' => 'Bahasa Indonesia', 'name_english' => 'Indonesian'],
'nl_NL' => ['name_locale' => 'Nederlands', 'name_english' => 'Dutch'],
'pl_PL' => ['name_locale' => 'Polski', 'name_english' => 'Polish '],
'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'],
// incomplete languages:
//'pt_BR' => ['name_locale' => 'Português do Brasil', 'name_english' => 'Portuguese (Brazil)'],
@ -126,7 +127,7 @@ return [
//'es_ES' => ['name_locale' => 'Spanish', 'name_english' => 'Spanish'],
//'ru_RU' => ['name_locale' => 'Русский', 'name_english' => 'Russian'],
//'sl_SI' => ['name_locale' => 'Slovenščina', 'name_english' => 'Slovenian'],
//'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'],
//
],
'transactionTypesByWhat' => [
'expenses' => ['Withdrawal'],

54
index.php Normal file
View File

@ -0,0 +1,54 @@
<?php
/**
* index.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
echo '<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="8; url=./public/">
<script type="text/javascript">
setTimeout(function() {
window.location.href = "./public/";
}, 8000);
</script>
<title>Firefly III</title>
<style>
p {font-family:Arial,sans-serif;font-size:18px;color:#222;text-align:center;}
</style>
</head>
<body>
<p>
<strong style="color:red;">Danger!</strong> This directory should not be open to the public!
</p>
<p>
<span style="font-family:monospace;">/public/</span> should be the document root of your web server.
</p>
<p>
Leaving your web server configured like this is a <span style="color:red;">huge</span> security risk.
</p>
<p>
Please <a href="https://github.com/firefly-iii/help/wiki/Configure-your-webserver-correctly">read more on the Github help pages</a>.
</p>
</body>
</html>
';

View File

@ -31,6 +31,7 @@
*/
function formatLabel(str, maxwidth) {
var sections = [];
str = String(str);
var words = str.split(" ");
var temp = "";

View File

@ -174,14 +174,20 @@ function jobIsStalled(data) {
/**
* This function tells Firefly start the job. It will also initialize a re-check in 500ms time.
* Only when job is in "configured" state.
*/
function startJob() {
// disable the button, add loading thing.
$('.start-job').prop('disabled', true).text('...');
$.post(jobStartUri, {_token: token}).fail(reportOnSubmitError);
if (job.status === "configured") {
console.log("Job auto started!");
// disable the button, add loading thing.
$('.start-job').prop('disabled', true).text('...');
$.post(jobStartUri, {_token: token}).fail(reportOnSubmitError);
// check status, every 500 ms.
timeOutId = setTimeout(checkJobStatus, startInterval);
// check status, every 500 ms.
timeOutId = setTimeout(checkJobStatus, startInterval);
return;
}
console.log("Job not auto started because state is " + job.status);
}
/**

View File

@ -22,17 +22,10 @@
$(function () {
"use strict";
drawChart();
lineChart(accountChartUri, 'account-balances-chart');
loadAjaxPartial('categoryReport', categoryReportUri);
loadAjaxPartial('budgetReport', budgetReportUri);
loadAjaxPartial('balanceReport', balanceReportUri);
});
function drawChart() {
"use strict";
// month view:
// draw account chart
lineChart(accountChartUri, 'account-balances-chart');
}

View File

@ -22,18 +22,12 @@
$(function () {
"use strict";
drawChart();
lineChart(netWorthUri, 'net-worth');
columnChart(opChartUri, 'income-expenses-chart');
columnChart(sumChartUri, 'income-expenses-sum-chart');
loadAjaxPartial('budgetPeriodReport', budgetPeriodReportUri);
loadAjaxPartial('categoryExpense', categoryExpenseUri);
loadAjaxPartial('categoryIncome', categoryIncomeUri);
});
function drawChart() {
"use strict";
// income and expense over multi year:
lineChart(netWorthUri, 'net-worth');
columnChart(opChartUri, 'income-expenses-chart');
columnChart(sumChartUri, 'income-expenses-sum-chart');
}

View File

@ -22,19 +22,12 @@
$(function () {
"use strict";
drawChart();
lineChart(netWorthUri, 'net-worth');
columnChart(opChartUri, 'income-expenses-chart');
columnChart(sumChartUri, 'income-expenses-sum-chart');
loadAjaxPartial('budgetPeriodReport', budgetPeriodReportUri);
loadAjaxPartial('categoryExpense', categoryExpenseUri);
loadAjaxPartial('categoryIncome', categoryIncomeUri);
});
function drawChart() {
"use strict";
lineChart(netWorthUri, 'net-worth');
columnChart(opChartUri, 'income-expenses-chart');
columnChart(sumChartUri, 'income-expenses-sum-chart');
}

View File

@ -18,7 +18,7 @@
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
/** global: edit_selected_txt, delete_selected_txt, token */
/** global: edit_selected_txt, edit_bulk_selected_txt, delete_selected_txt, token */
/**
*
@ -43,43 +43,14 @@ $(document).ready(function () {
countChecked();
});
// click the edit button:
// click the mass edit button:
$('.mass_edit').click(goToMassEdit);
// click the bulk edit button:
$('.bulk_edit').click(goToBulkEdit);
// click the delete button:
$('.mass_delete').click(goToMassDelete);
// click reconcile button
// $('.mass_reconcile').click(goToReconcile);
});
/**
*
* @returns {boolean}
*/
function goToReconcile() {
var checked = $('.select_all_single:checked');
var ids = [];
$.each(checked, function (i, v) {
ids.push(parseInt($(v).data('transaction')));
});
// go to specially crafted URL:
var bases = document.getElementsByTagName('base');
var baseHref = null;
if (bases.length > 0) {
baseHref = bases[0].href;
}
$.post(baseHref + 'transactions/reconcile', {transactions: ids, _token: token}).done(function () {
window.location.reload(true);
}).fail(function () {
alert('Could not reconcile transactions: please check the logs and try again later.');
});
return false;
}
/**
*
* @returns {boolean}
@ -100,6 +71,26 @@ function goToMassEdit() {
return false;
}
/**
*
* @returns {boolean}
*/
function goToBulkEdit() {
"use strict";
var checkedArray = getCheckboxes();
// go to specially crafted URL:
var bases = document.getElementsByTagName('base');
var baseHref = null;
if (bases.length > 0) {
baseHref = bases[0].href;
}
window.location.href = baseHref + '/transactions/bulk/edit/' + checkedArray;
return false;
}
/**
*
* @returns {boolean}
@ -144,6 +135,7 @@ function countChecked() {
var checked = $('.select_all_single:checked').length;
if (checked > 0) {
$('.mass_edit span').text(edit_selected_txt + ' (' + checked + ')');
$('.bulk_edit span').text(edit_bulk_selected_txt + ' (' + checked + ')');
$('.mass_delete span').text(delete_selected_txt + ' (' + checked + ')');
// get amount for the transactions:

View File

@ -0,0 +1,43 @@
/*
* edit.js
* Copyright (c) 2017 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
/** global: what */
$(document).ready(function () {
"use strict";
$.getJSON('json/categories').done(function (data) {
$('input[name="category"]').typeahead({source: data});
});
$.getJSON('json/tags').done(function (data) {
var opt = {
typeahead: {
source: data,
afterSelect: function () {
this.$element.val("");
}
}
};
$('input[name="tags"]').tagsinput(
opt
);
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
/**
* @version: 2.1.25
* @version: 2.1.30
* @author: Dan Grossman http://www.dangrossman.info/
* @copyright: Copyright (c) 2012-2017 Dan Grossman. All rights reserved.
* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php
@ -11,7 +11,7 @@
// AMD. Make globaly available as well
define(['moment', 'jquery'], function (moment, jquery) {
if (!jquery.fn) jquery.fn = {}; // webpack server rendering
return (root.daterangepicker = factory(moment, jquery));
return factory(moment, jquery);
});
} else if (typeof module === 'object' && module.exports) {
// Node / Browserify
@ -27,8 +27,8 @@
// Browser globals
root.daterangepicker = factory(root.moment, root.jQuery);
}
}(this, function (moment, $) {
var DateRangePicker = function (element, options, cb) {
}(this, function(moment, $) {
var DateRangePicker = function(element, options, cb) {
//default settings for options
this.parentEl = 'body';
@ -78,8 +78,7 @@
firstDay: moment.localeData().firstDayOfWeek()
};
this.callback = function () {
};
this.callback = function() { };
//some state information
this.isShowing = false;
@ -163,7 +162,7 @@
if (typeof options.locale.weekLabel === 'string')
this.locale.weekLabel = options.locale.weekLabel;
if (typeof options.locale.customRangeLabel === 'string') {
if (typeof options.locale.customRangeLabel === 'string'){
//Support unicode chars in the custom range name.
var elem = document.createElement('textarea');
elem.innerHTML = options.locale.customRangeLabel;
@ -288,7 +287,7 @@
//if no start/end dates set, check if an input element contains initial values
if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
if ($(this.element).is('input[type=text]')) {
var val = $(this.element).val(),
var val = $(this.element).val(),
split = val.split(this.locale.separator);
start = end = null;
@ -397,7 +396,7 @@
//swap the position of the predefined ranges if opens right
if (typeof options.ranges !== 'undefined' && this.opens == 'right') {
this.container.find('.ranges').prependTo(this.container.find('.calendar.left').parent());
this.container.find('.ranges').prependTo( this.container.find('.calendar.left').parent() );
}
//apply CSS classes and labels to buttons
@ -425,7 +424,8 @@
.on('click.daterangepicker', '.daterangepicker_input input', $.proxy(this.showCalendars, this))
.on('focus.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsFocused, this))
.on('blur.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsBlurred, this))
.on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this));
.on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this))
.on('keydown.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsKeydown, this));
this.container.find('.ranges')
.on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this))
@ -439,10 +439,11 @@
'click.daterangepicker': $.proxy(this.show, this),
'focus.daterangepicker': $.proxy(this.show, this),
'keyup.daterangepicker': $.proxy(this.elementChanged, this),
'keydown.daterangepicker': $.proxy(this.keydown, this)
'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility
});
} else {
this.element.on('click.daterangepicker', $.proxy(this.toggle, this));
this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this));
}
//
@ -463,7 +464,7 @@
constructor: DateRangePicker,
setStartDate: function (startDate) {
setStartDate: function(startDate) {
if (typeof startDate === 'string')
this.startDate = moment(startDate, this.locale.format);
@ -494,7 +495,7 @@
this.updateMonthsInView();
},
setEndDate: function (endDate) {
setEndDate: function(endDate) {
if (typeof endDate === 'string')
this.endDate = moment(endDate, this.locale.format);
@ -502,7 +503,7 @@
this.endDate = moment(endDate);
if (!this.timePicker)
this.endDate = this.endDate.endOf('day');
this.endDate = this.endDate.add(1,'d').startOf('day').subtract(1,'second');
if (this.timePicker && this.timePickerIncrement)
this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
@ -524,15 +525,15 @@
this.updateMonthsInView();
},
isInvalidDate: function () {
isInvalidDate: function() {
return false;
},
isCustomDate: function () {
isCustomDate: function() {
return false;
},
updateView: function () {
updateView: function() {
if (this.timePicker) {
this.renderTimePicker('left');
this.renderTimePicker('right');
@ -554,7 +555,7 @@
this.updateFormInputs();
},
updateMonthsInView: function () {
updateMonthsInView: function() {
if (this.endDate) {
//if both dates are visible already, do nothing
@ -585,7 +586,7 @@
}
},
updateCalendars: function () {
updateCalendars: function() {
if (this.timePicker) {
var hour, minute, second;
@ -626,7 +627,7 @@
this.calculateChosenLabel();
},
renderCalendar: function (side) {
renderCalendar: function(side) {
//
// Build the matrix of dates that will populate the calendar
@ -763,7 +764,7 @@
if (this.showWeekNumbers || this.showISOWeekNumbers)
html += '<th class="week">' + this.locale.weekLabel + '</th>';
$.each(this.locale.daysOfWeek, function (index, dayOfWeek) {
$.each(this.locale.daysOfWeek, function(index, dayOfWeek) {
html += '<th>' + dayOfWeek + '</th>';
});
@ -860,7 +861,7 @@
},
renderTimePicker: function (side) {
renderTimePicker: function(side) {
// Don't bother updating the time picker if it's currently disabled
// because an end date hasn't been clicked yet
@ -1021,7 +1022,7 @@
},
updateFormInputs: function () {
updateFormInputs: function() {
//ignore mouse movements while an above-calendar text input has focus
if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus"))
@ -1039,8 +1040,8 @@
},
move: function () {
var parentOffset = {top: 0, left: 0},
move: function() {
var parentOffset = { top: 0, left: 0 },
containerTop;
var parentRightEdge = $(window).width();
if (!this.parentEl.is('body')) {
@ -1097,13 +1098,11 @@
}
},
show: function (e) {
show: function(e) {
if (this.isShowing) return;
// Create a click proxy that is private to this instance of datepicker, for unbinding
this._outsideClickProxy = $.proxy(function (e) {
this.outsideClick(e);
}, this);
this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this);
// Bind global datepicker mousedown for hiding and
$(document)
@ -1116,9 +1115,7 @@
.on('focusin.daterangepicker', this._outsideClickProxy);
// Reposition the picker if the window is resized while it's open
$(window).on('resize.daterangepicker', $.proxy(function (e) {
this.move(e);
}, this));
$(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this));
this.oldStartDate = this.startDate.clone();
this.oldEndDate = this.endDate.clone();
@ -1131,7 +1128,7 @@
this.isShowing = true;
},
hide: function (e) {
hide: function(e) {
if (!this.isShowing) return;
//incomplete date selection, revert to last values
@ -1154,7 +1151,7 @@
this.isShowing = false;
},
toggle: function (e) {
toggle: function(e) {
if (this.isShowing) {
this.hide();
} else {
@ -1162,7 +1159,7 @@
}
},
outsideClick: function (e) {
outsideClick: function(e) {
var target = $(e.target);
// if the page is clicked anywhere except within the daterangerpicker/button
// itself then call this.hide()
@ -1177,18 +1174,18 @@
this.element.trigger('outsideClick.daterangepicker', this);
},
showCalendars: function () {
showCalendars: function() {
this.container.addClass('show-calendar');
this.move();
this.element.trigger('showCalendar.daterangepicker', this);
},
hideCalendars: function () {
hideCalendars: function() {
this.container.removeClass('show-calendar');
this.element.trigger('hideCalendar.daterangepicker', this);
},
hoverRange: function (e) {
hoverRange: function(e) {
//ignore mouse movements while an above-calendar text input has focus
if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus"))
@ -1206,7 +1203,7 @@
},
clickRange: function (e) {
clickRange: function(e) {
var label = e.target.getAttribute('data-range-key');
this.chosenLabel = label;
if (label == this.locale.customRangeLabel) {
@ -1227,7 +1224,7 @@
}
},
clickPrev: function (e) {
clickPrev: function(e) {
var cal = $(e.target).parents('.calendar');
if (cal.hasClass('left')) {
this.leftCalendar.month.subtract(1, 'month');
@ -1239,7 +1236,7 @@
this.updateCalendars();
},
clickNext: function (e) {
clickNext: function(e) {
var cal = $(e.target).parents('.calendar');
if (cal.hasClass('left')) {
this.leftCalendar.month.add(1, 'month');
@ -1251,7 +1248,7 @@
this.updateCalendars();
},
hoverDate: function (e) {
hoverDate: function(e) {
//ignore mouse movements while an above-calendar text input has focus
//if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus"))
@ -1278,7 +1275,7 @@
var rightCalendar = this.rightCalendar;
var startDate = this.startDate;
if (!this.endDate) {
this.container.find('.calendar tbody td').each(function (index, el) {
this.container.find('.calendar tbody td').each(function(index, el) {
//skip week numbers, only look at dates
if ($(el).hasClass('week')) return;
@ -1300,7 +1297,7 @@
},
clickDate: function (e) {
clickDate: function(e) {
if (!$(e.target).hasClass('available')) return;
@ -1378,7 +1375,9 @@
var i = 0;
for (var range in this.ranges) {
if (this.timePicker) {
if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) {
var format = this.timePickerSeconds ? "YYYY-MM-DD hh:mm:ss" : "YYYY-MM-DD hh:mm";
//ignore times when comparing dates if time picker seconds is not enabled
if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) {
customRange = false;
this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html();
break;
@ -1403,22 +1402,22 @@
}
},
clickApply: function (e) {
clickApply: function(e) {
this.hide();
this.element.trigger('apply.daterangepicker', this);
},
clickCancel: function (e) {
clickCancel: function(e) {
this.startDate = this.oldStartDate;
this.endDate = this.oldEndDate;
this.hide();
this.element.trigger('cancel.daterangepicker', this);
},
monthOrYearChanged: function (e) {
var isLeft = $(e.target).closest('.calendar').hasClass('left'),
monthOrYearChanged: function(e) {
var isLeft = $(e.target).closest('.calendar').hasClass('left'),
leftOrRight = isLeft ? 'left' : 'right',
cal = this.container.find('.calendar.' + leftOrRight);
cal = this.container.find('.calendar.'+leftOrRight);
// Month must be Number for new moment versions
var month = parseInt(cal.find('.monthselect').val(), 10);
@ -1457,9 +1456,9 @@
this.updateCalendars();
},
timeChanged: function (e) {
timeChanged: function(e) {
var cal = $(e.target).closest('.calendar'),
var cal = $(e.target).closest('.calendar'),
isLeft = cal.hasClass('left');
var hour = parseInt(cal.find('.hourselect').val(), 10);
@ -1505,7 +1504,7 @@
},
formInputsChanged: function (e) {
formInputsChanged: function(e) {
var isRight = $(e.target).closest('.calendar').hasClass('right');
var start = moment(this.container.find('input[name="daterangepicker_start"]').val(), this.locale.format);
var end = moment(this.container.find('input[name="daterangepicker_end"]').val(), this.locale.format);
@ -1529,13 +1528,13 @@
this.updateView();
},
formInputsFocused: function (e) {
formInputsFocused: function(e) {
// Highlight the focused input
this.container.find('input[name="daterangepicker_start"], input[name="daterangepicker_end"]').removeClass('active');
$(e.target).addClass('active');
// Set the state such that if the user goes back to using a mouse,
// Set the state such that if the user goes back to using a mouse,
// the calendars are aware we're selecting the end of the range, not
// the start. This allows someone to edit the end of a date range without
// re-selecting the beginning, by clicking on the end date input then
@ -1549,7 +1548,7 @@
},
formInputsBlurred: function (e) {
formInputsBlurred: function(e) {
// this function has one purpose right now: if you tab from the first
// text input to the second in the UI, the endDate is nulled so that
@ -1567,14 +1566,26 @@
},
elementChanged: function () {
formInputsKeydown: function(e) {
// This function ensures that if the 'enter' key was pressed in the input, then the calendars
// are updated with the startDate and endDate.
// This behaviour is automatic in Chrome/Firefox/Edge but not in IE 11 hence why this exists.
// Other browsers and versions of IE are untested and the behaviour is unknown.
if (e.keyCode === 13) {
// Prevent the calendar from being updated twice on Chrome/Firefox/Edge
e.preventDefault();
this.formInputsChanged(e);
}
},
elementChanged: function() {
if (!this.element.is('input')) return;
if (!this.element.val().length) return;
if (this.element.val().length < this.locale.format.length) return;
var dateString = this.element.val().split(this.locale.separator),
start = null,
end = null;
start = null,
end = null;
if (dateString.length === 2) {
start = moment(dateString[0], this.locale.format);
@ -1593,14 +1604,22 @@
this.updateView();
},
keydown: function (e) {
keydown: function(e) {
//hide on tab or enter
if ((e.keyCode === 9) || (e.keyCode === 13)) {
this.hide();
}
//hide on esc and prevent propagation
if (e.keyCode === 27) {
e.preventDefault();
e.stopPropagation();
this.hide();
}
},
updateElement: function () {
updateElement: function() {
if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) {
this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format));
this.element.trigger('change');
@ -1610,7 +1629,7 @@
}
},
remove: function () {
remove: function() {
this.container.remove();
this.element.off('.daterangepicker');
this.element.removeData();
@ -1618,16 +1637,17 @@
};
$.fn.daterangepicker = function (options, callback) {
this.each(function () {
$.fn.daterangepicker = function(options, callback) {
var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options);
this.each(function() {
var el = $(this);
if (el.data('daterangepicker'))
el.data('daterangepicker').remove();
el.data('daterangepicker', new DateRangePicker(el, options, callback));
el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback));
});
return this;
};
return DateRangePicker;
}));
}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
/*! modernizr 3.3.1 (Custom Build) | MIT *
/*! modernizr 3.5.0 (Custom Build) | MIT *
* https://modernizr.com/download/?-inputtypes-setclasses !*/
!function(e,t,n){function a(e,t){return typeof e===t}function s(){var e,t,n,s,i,o,c;for(var u in r)if(r.hasOwnProperty(u)){if(e=[],t=r[u],t.name&&(e.push(t.name.toLowerCase()),t.options&&t.options.aliases&&t.options.aliases.length))for(n=0;n<t.options.aliases.length;n++)e.push(t.options.aliases[n].toLowerCase());for(s=a(t.fn,"function")?t.fn():t.fn,i=0;i<e.length;i++)o=e[i],c=o.split("."),1===c.length?Modernizr[c[0]]=s:(!Modernizr[c[0]]||Modernizr[c[0]]instanceof Boolean||(Modernizr[c[0]]=new Boolean(Modernizr[c[0]])),Modernizr[c[0]][c[1]]=s),l.push((s?"":"no-")+c.join("-"))}}function i(e){var t=u.className,n=Modernizr._config.classPrefix||"";if(f&&(t=t.baseVal),Modernizr._config.enableJSClass){var a=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(a,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(t+=" "+n+e.join(" "+n),f?u.className.baseVal=t:u.className=t)}function o(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):f?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}var l=[],r=[],c={_version:"3.3.1",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,t){var n=this;setTimeout(function(){t(n[e])},0)},addTest:function(e,t,n){r.push({name:e,fn:t,options:n})},addAsyncTest:function(e){r.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=c,Modernizr=new Modernizr;var u=t.documentElement,f="svg"===u.nodeName.toLowerCase(),p=o("input"),d="search tel url email datetime date month week time datetime-local number range color".split(" "),m={};Modernizr.inputtypes=function(e){for(var a,s,i,o=e.length,l="1)",r=0;o>r;r++)p.setAttribute("type",a=e[r]),i="text"!==p.type&&"style"in p,i&&(p.value=l,p.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(a)&&p.style.WebkitAppearance!==n?(u.appendChild(p),s=t.defaultView,i=s.getComputedStyle&&"textfield"!==s.getComputedStyle(p,null).WebkitAppearance&&0!==p.offsetHeight,u.removeChild(p)):/^(search|tel)$/.test(a)||(i=/^(url|email)$/.test(a)?p.checkValidity&&p.checkValidity()===!1:p.value!=l)),m[e[r]]=!!i;return m}(d),s(),i(l),delete c.addTest,delete c.addAsyncTest;for(var h=0;h<Modernizr._q.length;h++)Modernizr._q[h]();e.Modernizr=Modernizr}(window,document);
!function(e,t,n){function a(e,t){return typeof e===t}function s(){var e,t,n,s,i,o,c;for(var u in r)if(r.hasOwnProperty(u)){if(e=[],t=r[u],t.name&&(e.push(t.name.toLowerCase()),t.options&&t.options.aliases&&t.options.aliases.length))for(n=0;n<t.options.aliases.length;n++)e.push(t.options.aliases[n].toLowerCase());for(s=a(t.fn,"function")?t.fn():t.fn,i=0;i<e.length;i++)o=e[i],c=o.split("."),1===c.length?Modernizr[c[0]]=s:(!Modernizr[c[0]]||Modernizr[c[0]]instanceof Boolean||(Modernizr[c[0]]=new Boolean(Modernizr[c[0]])),Modernizr[c[0]][c[1]]=s),l.push((s?"":"no-")+c.join("-"))}}function i(e){var t=u.className,n=Modernizr._config.classPrefix||"";if(f&&(t=t.baseVal),Modernizr._config.enableJSClass){var a=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(a,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(t+=" "+n+e.join(" "+n),f?u.className.baseVal=t:u.className=t)}function o(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):f?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}var l=[],r=[],c={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,t){var n=this;setTimeout(function(){t(n[e])},0)},addTest:function(e,t,n){r.push({name:e,fn:t,options:n})},addAsyncTest:function(e){r.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=c,Modernizr=new Modernizr;var u=t.documentElement,f="svg"===u.nodeName.toLowerCase(),p=o("input"),d="search tel url email datetime date month week time datetime-local number range color".split(" "),m={};Modernizr.inputtypes=function(e){for(var a,s,i,o=e.length,l="1)",r=0;o>r;r++)p.setAttribute("type",a=e[r]),i="text"!==p.type&&"style"in p,i&&(p.value=l,p.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(a)&&p.style.WebkitAppearance!==n?(u.appendChild(p),s=t.defaultView,i=s.getComputedStyle&&"textfield"!==s.getComputedStyle(p,null).WebkitAppearance&&0!==p.offsetHeight,u.removeChild(p)):/^(search|tel)$/.test(a)||(i=/^(url|email)$/.test(a)?p.checkValidity&&p.checkValidity()===!1:p.value!=l)),m[e[r]]=!!i;return m}(d),s(),i(l),delete c.addTest,delete c.addAsyncTest;for(var h=0;h<Modernizr._q.length;h++)Modernizr._q[h]();e.Modernizr=Modernizr}(window,document);

File diff suppressed because one or more lines are too long

View File

@ -1,49 +1,54 @@
(function(C,n){"object"===typeof exports?n(exports):"function"===typeof define&&define.amd?define(["exports"],n):n(C)})(this,function(C){function n(a){this._targetElement=a;this._introItems=[];this._options={nextLabel:"Next &rarr;",prevLabel:"&larr; Back",skipLabel:"Skip",doneLabel:"Done",hidePrev:!1,hideNext:!1,tooltipPosition:"bottom",tooltipClass:"",highlightClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,showProgress:!1,scrollToElement:!0,
overlayOpacity:0.8,scrollPadding:30,positionPrecedence:["bottom","top","right","left"],disableInteraction:!1,hintPosition:"top-middle",hintButtonLabel:"Got it",hintAnimation:!0}}function V(a){var b=[],c=this;if(this._options.steps)for(var d=0,e=this._options.steps.length;d<e;d++){var f=y(this._options.steps[d]);f.step=b.length+1;"string"===typeof f.element&&(f.element=document.querySelector(f.element));if("undefined"===typeof f.element||null==f.element){var g=document.querySelector(".introjsFloatingElement");
null==g&&(g=document.createElement("div"),g.className="introjsFloatingElement",document.body.appendChild(g));f.element=g;f.position="floating"}null!=f.element&&b.push(f)}else{e=a.querySelectorAll("*[data-intro]");if(1>e.length)return!1;d=0;for(f=e.length;d<f;d++)if(g=e[d],"none"!=g.style.display){var k=parseInt(g.getAttribute("data-step"),10);0<k&&(b[k-1]={element:g,intro:g.getAttribute("data-intro"),step:parseInt(g.getAttribute("data-step"),10),tooltipClass:g.getAttribute("data-tooltipClass"),highlightClass:g.getAttribute("data-highlightClass"),
position:g.getAttribute("data-position")||this._options.tooltipPosition})}d=k=0;for(f=e.length;d<f;d++)if(g=e[d],null==g.getAttribute("data-step")){for(;"undefined"!=typeof b[k];)k++;b[k]={element:g,intro:g.getAttribute("data-intro"),step:k+1,tooltipClass:g.getAttribute("data-tooltipClass"),highlightClass:g.getAttribute("data-highlightClass"),position:g.getAttribute("data-position")||this._options.tooltipPosition}}}d=[];for(e=0;e<b.length;e++)b[e]&&d.push(b[e]);b=d;b.sort(function(a,b){return a.step-
b.step});c._introItems=b;W.call(c,a)&&(x.call(c),a.querySelector(".introjs-skipbutton"),a.querySelector(".introjs-nextbutton"),c._onKeyDown=function(b){if(27===b.keyCode&&!0==c._options.exitOnEsc)z.call(c,a);else if(37===b.keyCode)E.call(c);else if(39===b.keyCode)x.call(c);else if(13===b.keyCode){var d=b.target||b.srcElement;d&&0<d.className.indexOf("introjs-prevbutton")?E.call(c):d&&0<d.className.indexOf("introjs-skipbutton")?(c._introItems.length-1==c._currentStep&&"function"===typeof c._introCompleteCallback&&
c._introCompleteCallback.call(c),z.call(c,a)):x.call(c);b.preventDefault?b.preventDefault():b.returnValue=!1}},c._onResize=function(a){t.call(c,document.querySelector(".introjs-helperLayer"));t.call(c,document.querySelector(".introjs-tooltipReferenceLayer"))},window.addEventListener?(this._options.keyboardNavigation&&window.addEventListener("keydown",c._onKeyDown,!0),window.addEventListener("resize",c._onResize,!0)):document.attachEvent&&(this._options.keyboardNavigation&&document.attachEvent("onkeydown",
c._onKeyDown),document.attachEvent("onresize",c._onResize)));return!1}function y(a){if(null==a||"object"!=typeof a||"undefined"!=typeof a.nodeType)return a;var b={},c;for(c in a)b[c]="undefined"!=typeof jQuery&&a[c]instanceof jQuery?a[c]:y(a[c]);return b}function x(){this._direction="forward";if("undefined"!==typeof this._currentStepNumber)for(var a=0,b=this._introItems.length;a<b;a++)this._introItems[a].step===this._currentStepNumber&&(this._currentStep=a-1,this._currentStepNumber=void 0);"undefined"===
typeof this._currentStep?this._currentStep=0:++this._currentStep;this._introItems.length<=this._currentStep?("function"===typeof this._introCompleteCallback&&this._introCompleteCallback.call(this),z.call(this,this._targetElement)):(a=this._introItems[this._currentStep],"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element),N.call(this,a))}function E(){this._direction="backward";if(0===this._currentStep)return!1;var a=this._introItems[--this._currentStep];
"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element);N.call(this,a)}function z(a){var b=a.querySelectorAll(".introjs-overlay");if(b&&0<b.length)for(var c=b.length-1;0<=c;c--){var d=b[c];d.style.opacity=0;setTimeout(function(){this.parentNode&&this.parentNode.removeChild(this)}.bind(d),500)}(c=a.querySelector(".introjs-helperLayer"))&&c.parentNode.removeChild(c);(c=a.querySelector(".introjs-tooltipReferenceLayer"))&&c.parentNode.removeChild(c);
(a=a.querySelector(".introjs-disableInteraction"))&&a.parentNode.removeChild(a);(a=document.querySelector(".introjsFloatingElement"))&&a.parentNode.removeChild(a);O();if((a=document.querySelectorAll(".introjs-fixParent"))&&0<a.length)for(c=a.length-1;0<=c;c--)a[c].className=a[c].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");window.removeEventListener?window.removeEventListener("keydown",this._onKeyDown,!0):document.detachEvent&&document.detachEvent("onkeydown",this._onKeyDown);
void 0!=this._introExitCallback&&this._introExitCallback.call(self);this._currentStep=void 0}function F(a,b,c,d,e){var f="",g,k;e=e||!1;b.style.top=null;b.style.right=null;b.style.bottom=null;b.style.left=null;b.style.marginLeft=null;b.style.marginTop=null;c.style.display="inherit";"undefined"!=typeof d&&null!=d&&(d.style.top=null,d.style.left=null);if(this._introItems[this._currentStep]){f=this._introItems[this._currentStep];f="string"===typeof f.tooltipClass?f.tooltipClass:this._options.tooltipClass;
b.className=("introjs-tooltip "+f).replace(/^\s+|\s+$/g,"");k=this._introItems[this._currentStep].position;if(("auto"==k||"auto"==this._options.tooltipPosition)&&"floating"!=k){f=k;g=this._options.positionPrecedence.slice();k=G();var w=u(b).height+10,m=u(b).width+20,h=u(a),l="floating";h.left+m>k.width||0>h.left+h.width/2-m?(s(g,"bottom"),s(g,"top")):(h.height+h.top+w>k.height&&s(g,"bottom"),0>h.top-w&&s(g,"top"));h.width+h.left+m>k.width&&s(g,"right");0>h.left-m&&s(g,"left");0<g.length&&(l=g[0]);
f&&"auto"!=f&&-1<g.indexOf(f)&&(l=f);k=l}f=u(a);a=u(b);g=G();switch(k){case "top":c.className="introjs-arrow bottom";H(f,e?0:15,a,g,b);b.style.bottom=f.height+20+"px";break;case "right":b.style.left=f.width+20+"px";f.top+a.height>g.height?(c.className="introjs-arrow left-bottom",b.style.top="-"+(a.height-f.height-20)+"px"):c.className="introjs-arrow left";break;case "left":e||!0!=this._options.showStepNumbers||(b.style.top="15px");f.top+a.height>g.height?(b.style.top="-"+(a.height-f.height-20)+"px",
c.className="introjs-arrow right-bottom"):c.className="introjs-arrow right";b.style.right=f.width+20+"px";break;case "floating":c.style.display="none";b.style.left="50%";b.style.top="50%";b.style.marginLeft="-"+a.width/2+"px";b.style.marginTop="-"+a.height/2+"px";"undefined"!=typeof d&&null!=d&&(d.style.left="-"+(a.width/2+18)+"px",d.style.top="-"+(a.height/2+18)+"px");break;case "bottom-right-aligned":c.className="introjs-arrow top-right";P(f,0,a,b);b.style.top=f.height+20+"px";break;case "bottom-middle-aligned":c.className=
"introjs-arrow top-middle";c=f.width/2-a.width/2;e&&(c+=5);P(f,c,a,b)&&(b.style.right=null,H(f,c,a,g,b));b.style.top=f.height+20+"px";break;default:c.className="introjs-arrow top",H(f,0,a,g,b),b.style.top=f.height+20+"px"}}}function H(a,b,c,d,e){if(a.left+b+c.width>d.width)return e.style.left=d.width-c.width-a.left+"px",!1;e.style.left=b+"px";return!0}function P(a,b,c,d){if(0>a.left+a.width-b-c.width)return d.style.left=-a.left+"px",!1;d.style.right=b+"px";return!0}function s(a,b){-1<a.indexOf(b)&&
a.splice(a.indexOf(b),1)}function t(a){if(a&&this._introItems[this._currentStep]){var b=this._introItems[this._currentStep],c=u(b.element),d=10;I(b.element)?a.className+=" introjs-fixedTooltip":a.className=a.className.replace(" introjs-fixedTooltip","");"floating"==b.position&&(d=0);a.setAttribute("style","width: "+(c.width+d)+"px; height:"+(c.height+d)+"px; top:"+(c.top-5)+"px;left: "+(c.left-5)+"px;")}}function X(){var a=document.querySelector(".introjs-disableInteraction");null===a&&(a=document.createElement("div"),
a.className="introjs-disableInteraction",this._targetElement.appendChild(a));t.call(this,a)}function D(a){a.setAttribute("role","button");a.tabIndex=0}function N(a){"undefined"!==typeof this._introChangeCallback&&this._introChangeCallback.call(this,a.element);var b=this,c=document.querySelector(".introjs-helperLayer"),d=document.querySelector(".introjs-tooltipReferenceLayer"),e="introjs-helperLayer";u(a.element);"string"===typeof a.highlightClass&&(e+=" "+a.highlightClass);"string"===typeof this._options.highlightClass&&
(e+=" "+this._options.highlightClass);if(null!=c){var f=d.querySelector(".introjs-helperNumberLayer"),g=d.querySelector(".introjs-tooltiptext"),k=d.querySelector(".introjs-arrow"),w=d.querySelector(".introjs-tooltip"),m=d.querySelector(".introjs-skipbutton"),h=d.querySelector(".introjs-prevbutton"),l=d.querySelector(".introjs-nextbutton");c.className=e;w.style.opacity=0;w.style.display="none";if(null!=f){var p=this._introItems[0<=a.step-2?a.step-2:0];if(null!=p&&"forward"==this._direction&&"floating"==
p.position||"backward"==this._direction&&"floating"==a.position)f.style.opacity=0}t.call(b,c);t.call(b,d);if((p=document.querySelectorAll(".introjs-fixParent"))&&0<p.length)for(e=p.length-1;0<=e;e--)p[e].className=p[e].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");O();b._lastShowElementTimer&&clearTimeout(b._lastShowElementTimer);b._lastShowElementTimer=setTimeout(function(){null!=f&&(f.innerHTML=a.step);g.innerHTML=a.intro;w.style.display="block";F.call(b,a.element,w,k,f);b._options.showBullets&&
(d.querySelector(".introjs-bullets li > a.active").className="",d.querySelector('.introjs-bullets li > a[data-stepnumber="'+a.step+'"]').className="active");d.querySelector(".introjs-progress .introjs-progressbar").setAttribute("style","width:"+Q.call(b)+"%;");w.style.opacity=1;f&&(f.style.opacity=1);-1===l.tabIndex?m.focus():l.focus()},350)}else{var n=document.createElement("div"),h=document.createElement("div"),c=document.createElement("div"),q=document.createElement("div"),r=document.createElement("div"),
s=document.createElement("div"),v=document.createElement("div"),A=document.createElement("div");n.className=e;h.className="introjs-tooltipReferenceLayer";t.call(b,n);t.call(b,h);this._targetElement.appendChild(n);this._targetElement.appendChild(h);c.className="introjs-arrow";r.className="introjs-tooltiptext";r.innerHTML=a.intro;s.className="introjs-bullets";!1===this._options.showBullets&&(s.style.display="none");for(var n=document.createElement("ul"),e=0,C=this._introItems.length;e<C;e++){var y=
document.createElement("li"),B=document.createElement("a");B.onclick=function(){b.goToStep(this.getAttribute("data-stepnumber"))};e===a.step-1&&(B.className="active");D(B);B.innerHTML="&nbsp;";B.setAttribute("data-stepnumber",this._introItems[e].step);y.appendChild(B);n.appendChild(y)}s.appendChild(n);v.className="introjs-progress";!1===this._options.showProgress&&(v.style.display="none");e=document.createElement("div");e.className="introjs-progressbar";e.setAttribute("style","width:"+Q.call(this)+
"%;");v.appendChild(e);A.className="introjs-tooltipbuttons";!1===this._options.showButtons&&(A.style.display="none");q.className="introjs-tooltip";q.appendChild(r);q.appendChild(s);q.appendChild(v);!0==this._options.showStepNumbers&&(p=document.createElement("span"),p.className="introjs-helperNumberLayer",p.innerHTML=a.step,h.appendChild(p));q.appendChild(c);h.appendChild(q);l=document.createElement("a");l.onclick=function(){b._introItems.length-1!=b._currentStep&&x.call(b)};D(l);l.innerHTML=this._options.nextLabel;
h=document.createElement("a");h.onclick=function(){0!=b._currentStep&&E.call(b)};D(h);h.innerHTML=this._options.prevLabel;m=document.createElement("a");m.className="introjs-button introjs-skipbutton";D(m);m.innerHTML=this._options.skipLabel;m.onclick=function(){b._introItems.length-1==b._currentStep&&"function"===typeof b._introCompleteCallback&&b._introCompleteCallback.call(b);z.call(b,b._targetElement)};A.appendChild(m);1<this._introItems.length&&(A.appendChild(h),A.appendChild(l));q.appendChild(A);
F.call(b,a.element,q,c,p)}!0===this._options.disableInteraction&&X.call(b);h.removeAttribute("tabIndex");l.removeAttribute("tabIndex");0==this._currentStep&&1<this._introItems.length?(m.className="introjs-button introjs-skipbutton",l.className="introjs-button introjs-nextbutton",!0==this._options.hidePrev?(h.className="introjs-button introjs-prevbutton introjs-hidden",l.className+=" introjs-fullbutton"):h.className="introjs-button introjs-prevbutton introjs-disabled",h.tabIndex="-1",m.innerHTML=this._options.skipLabel):
this._introItems.length-1==this._currentStep||1==this._introItems.length?(m.innerHTML=this._options.doneLabel,m.className+=" introjs-donebutton",h.className="introjs-button introjs-prevbutton",!0==this._options.hideNext?(l.className="introjs-button introjs-nextbutton introjs-hidden",h.className+=" introjs-fullbutton"):l.className="introjs-button introjs-nextbutton introjs-disabled",l.tabIndex="-1"):(m.className="introjs-button introjs-skipbutton",h.className="introjs-button introjs-prevbutton",l.className=
"introjs-button introjs-nextbutton",m.innerHTML=this._options.skipLabel);l.focus();Y(a);Z(a.element)||!0!==this._options.scrollToElement||(q=a.element.getBoundingClientRect(),p=G().height,c=q.bottom-(q.bottom-q.top),q=q.bottom-p,0>c||a.element.clientHeight>p?window.scrollBy(0,c-this._options.scrollPadding):window.scrollBy(0,q+70+this._options.scrollPadding));"undefined"!==typeof this._introAfterChangeCallback&&this._introAfterChangeCallback.call(this,a.element)}function O(){for(var a=document.querySelectorAll(".introjs-showElement"),
b=0,c=a.length;b<c;b++){var d=a[b],e=/introjs-[a-zA-Z]+/g;if(d instanceof SVGElement){var f=d.getAttribute("class")||"";d.setAttribute("class",f.replace(e,"").replace(/^\s+|\s+$/g,""))}else d.className=d.className.replace(e,"").replace(/^\s+|\s+$/g,"")}}function Y(a){if(a.element instanceof SVGElement)for(var b=a.element.parentNode;null!=a.element.parentNode&&b.tagName&&"body"!==b.tagName.toLowerCase();)"svg"===b.tagName.toLowerCase()&&J(b,"introjs-showElement introjs-relativePosition"),b=b.parentNode;
J(a.element,"introjs-showElement");b=r(a.element,"position");"absolute"!==b&&("relative"!==b&&"fixed"!==b)&&J(a.element,"introjs-relativePosition");for(b=a.element.parentNode;null!=b&&b.tagName&&"body"!==b.tagName.toLowerCase();){a=r(b,"z-index");var c=parseFloat(r(b,"opacity")),d=r(b,"transform")||r(b,"-webkit-transform")||r(b,"-moz-transform")||r(b,"-ms-transform")||r(b,"-o-transform");if(/[0-9]+/.test(a)||1>c||"none"!==d&&void 0!==d)b.className+=" introjs-fixParent";b=b.parentNode}}function J(a,
b){if(a instanceof SVGElement){var c=a.getAttribute("class")||"";a.setAttribute("class",c+" "+b)}else a.className+=" "+b}function r(a,b){var c="";a.currentStyle?c=a.currentStyle[b]:document.defaultView&&document.defaultView.getComputedStyle&&(c=document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return c&&c.toLowerCase?c.toLowerCase():c}function I(a){var b=a.parentNode;return b&&"HTML"!==b.nodeName?"fixed"==r(a,"position")?!0:I(b):!1}function G(){if(void 0!=window.innerWidth)return{width:window.innerWidth,
height:window.innerHeight};var a=document.documentElement;return{width:a.clientWidth,height:a.clientHeight}}function Z(a){a=a.getBoundingClientRect();return 0<=a.top&&0<=a.left&&a.bottom+80<=window.innerHeight&&a.right<=window.innerWidth}function W(a){var b=document.createElement("div"),c="",d=this;b.className="introjs-overlay";if(a.tagName&&"body"!==a.tagName.toLowerCase()){var e=u(a);e&&(c+="width: "+e.width+"px; height:"+e.height+"px; top:"+e.top+"px;left: "+e.left+"px;",b.setAttribute("style",
c))}else c+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;",b.setAttribute("style",c);a.appendChild(b);b.onclick=function(){!0==d._options.exitOnOverlayClick&&z.call(d,a)};setTimeout(function(){c+="opacity: "+d._options.overlayOpacity.toString()+";";b.setAttribute("style",c)},10);return!0}function v(){var a=this._targetElement.querySelector(".introjs-hintReference");if(a){var b=a.getAttribute("data-step");a.parentNode.removeChild(a);return b}}function R(a){this._introItems=[];if(this._options.hints){a=
0;for(var b=this._options.hints.length;a<b;a++){var c=y(this._options.hints[a]);"string"===typeof c.element&&(c.element=document.querySelector(c.element));c.hintPosition=c.hintPosition||this._options.hintPosition;c.hintAnimation=c.hintAnimation||this._options.hintAnimation;null!=c.element&&this._introItems.push(c)}}else{c=a.querySelectorAll("*[data-hint]");if(1>c.length)return!1;a=0;for(b=c.length;a<b;a++){var d=c[a],e=d.getAttribute("data-hintAnimation"),e=e?"true"==e:this._options.hintAnimation;
this._introItems.push({element:d,hint:d.getAttribute("data-hint"),hintPosition:d.getAttribute("data-hintPosition")||this._options.hintPosition,hintAnimation:e,tooltipClass:d.getAttribute("data-tooltipClass"),position:d.getAttribute("data-position")||this._options.tooltipPosition})}}$.call(this);document.addEventListener?(document.addEventListener("click",v.bind(this),!1),window.addEventListener("resize",K.bind(this),!0)):document.attachEvent&&(document.attachEvent("onclick",v.bind(this)),document.attachEvent("onresize",
K.bind(this)))}function K(){for(var a=0,b=this._introItems.length;a<b;a++){var c=this._introItems[a];"undefined"!=typeof c.targetElement&&S.call(this,c.hintPosition,c.element,c.targetElement)}}function L(a){v.call(this);var b=this._targetElement.querySelector('.introjs-hint[data-step="'+a+'"]');b&&(b.className+=" introjs-hidehint");"undefined"!==typeof this._hintCloseCallback&&this._hintCloseCallback.call(this,a)}function T(a){if(a=this._targetElement.querySelector('.introjs-hint[data-step="'+a+'"]'))a.className=
a.className.replace(/introjs\-hidehint/g,"")}function U(a){(a=this._targetElement.querySelector('.introjs-hint[data-step="'+a+'"]'))&&a.parentNode.removeChild(a)}function $(){var a=this,b=document.querySelector(".introjs-hints");null==b&&(b=document.createElement("div"),b.className="introjs-hints");for(var c=0,d=this._introItems.length;c<d;c++){var e=this._introItems[c];if(!document.querySelector('.introjs-hint[data-step="'+c+'"]')){var f=document.createElement("a");D(f);(function(b,c,d){b.onclick=
function(e){e=e?e:window.event;e.stopPropagation&&e.stopPropagation();null!=e.cancelBubble&&(e.cancelBubble=!0);aa.call(a,b,c,d)}})(f,e,c);f.className="introjs-hint";e.hintAnimation||(f.className+=" introjs-hint-no-anim");I(e.element)&&(f.className+=" introjs-fixedhint");var g=document.createElement("div");g.className="introjs-hint-dot";var k=document.createElement("div");k.className="introjs-hint-pulse";f.appendChild(g);f.appendChild(k);f.setAttribute("data-step",c);e.targetElement=e.element;e.element=
f;S.call(this,e.hintPosition,f,e.targetElement);b.appendChild(f)}}document.body.appendChild(b);"undefined"!==typeof this._hintsAddedCallback&&this._hintsAddedCallback.call(this)}function S(a,b,c){c=u.call(this,c);switch(a){default:case "top-left":b.style.left=c.left+"px";b.style.top=c.top+"px";break;case "top-right":b.style.left=c.left+c.width-20+"px";b.style.top=c.top+"px";break;case "bottom-left":b.style.left=c.left+"px";b.style.top=c.top+c.height-20+"px";break;case "bottom-right":b.style.left=
c.left+c.width-20+"px";b.style.top=c.top+c.height-20+"px";break;case "middle-left":b.style.left=c.left+"px";b.style.top=c.top+(c.height-20)/2+"px";break;case "middle-right":b.style.left=c.left+c.width-20+"px";b.style.top=c.top+(c.height-20)/2+"px";break;case "middle-middle":b.style.left=c.left+(c.width-20)/2+"px";b.style.top=c.top+(c.height-20)/2+"px";break;case "bottom-middle":b.style.left=c.left+(c.width-20)/2+"px";b.style.top=c.top+c.height-20+"px";break;case "top-middle":b.style.left=c.left+(c.width-
20)/2+"px",b.style.top=c.top+"px"}}function aa(a,b,c){"undefined"!==typeof this._hintClickCallback&&this._hintClickCallback.call(this,a,b,c);var d=v.call(this);if(parseInt(d,10)!=c){var d=document.createElement("div"),e=document.createElement("div"),f=document.createElement("div"),g=document.createElement("div");d.className="introjs-tooltip";d.onclick=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0};e.className="introjs-tooltiptext";var k=document.createElement("p");k.innerHTML=
b.hint;b=document.createElement("a");b.className="introjs-button";b.innerHTML=this._options.hintButtonLabel;b.onclick=L.bind(this,c);e.appendChild(k);e.appendChild(b);f.className="introjs-arrow";d.appendChild(f);d.appendChild(e);this._currentStep=a.getAttribute("data-step");g.className="introjs-tooltipReferenceLayer introjs-hintReference";g.setAttribute("data-step",a.getAttribute("data-step"));t.call(this,g);g.appendChild(d);document.body.appendChild(g);F.call(this,a,d,f,null,!0)}}function u(a){var b=
{},c=document.body,d=document.documentElement,e=window.pageYOffset||d.scrollTop||c.scrollTop,c=window.pageXOffset||d.scrollLeft||c.scrollLeft;if(a instanceof SVGElement)a=a.getBoundingClientRect(),b.top=a.top+e,b.width=a.width,b.height=a.height,b.left=a.left+c;else{b.width=a.offsetWidth;b.height=a.offsetHeight;for(c=e=0;a&&!isNaN(a.offsetLeft)&&!isNaN(a.offsetTop);)e+=a.offsetLeft,c+=a.offsetTop,a=a.offsetParent;b.top=c;b.left=e}return b}function Q(){return 100*(parseInt(this._currentStep+1,10)/this._introItems.length)}
var M=function(a){if("object"===typeof a)return new n(a);if("string"===typeof a){if(a=document.querySelector(a))return new n(a);throw Error("There is no element with given selector.");}return new n(document.body)};M.version="2.5.0";M.fn=n.prototype={clone:function(){return new n(this)},setOption:function(a,b){this._options[a]=b;return this},setOptions:function(a){var b=this._options,c={},d;for(d in b)c[d]=b[d];for(d in a)c[d]=a[d];this._options=c;return this},start:function(){V.call(this,this._targetElement);
return this},goToStep:function(a){this._currentStep=a-2;"undefined"!==typeof this._introItems&&x.call(this);return this},addStep:function(a){this._options.steps||(this._options.steps=[]);this._options.steps.push(a);return this},addSteps:function(a){if(a.length){for(var b=0;b<a.length;b++)this.addStep(a[b]);return this}},goToStepNumber:function(a){this._currentStepNumber=a;"undefined"!==typeof this._introItems&&x.call(this);return this},nextStep:function(){x.call(this);return this},previousStep:function(){E.call(this);
return this},exit:function(){z.call(this,this._targetElement);return this},refresh:function(){t.call(this,document.querySelector(".introjs-helperLayer"));t.call(this,document.querySelector(".introjs-tooltipReferenceLayer"));K.call(this);return this},onbeforechange:function(a){if("function"===typeof a)this._introBeforeChangeCallback=a;else throw Error("Provided callback for onbeforechange was not a function");return this},onchange:function(a){if("function"===typeof a)this._introChangeCallback=a;else throw Error("Provided callback for onchange was not a function.");
return this},onafterchange:function(a){if("function"===typeof a)this._introAfterChangeCallback=a;else throw Error("Provided callback for onafterchange was not a function");return this},oncomplete:function(a){if("function"===typeof a)this._introCompleteCallback=a;else throw Error("Provided callback for oncomplete was not a function.");return this},onhintsadded:function(a){if("function"===typeof a)this._hintsAddedCallback=a;else throw Error("Provided callback for onhintsadded was not a function.");
return this},onhintclick:function(a){if("function"===typeof a)this._hintClickCallback=a;else throw Error("Provided callback for onhintclick was not a function.");return this},onhintclose:function(a){if("function"===typeof a)this._hintCloseCallback=a;else throw Error("Provided callback for onhintclose was not a function.");return this},onexit:function(a){if("function"===typeof a)this._introExitCallback=a;else throw Error("Provided callback for onexit was not a function.");return this},addHints:function(){R.call(this,
this._targetElement);return this},hideHint:function(a){L.call(this,a);return this},hideHints:function(){var a=this._targetElement.querySelectorAll(".introjs-hint");if(a&&0<a.length)for(var b=0;b<a.length;b++)L.call(this,a[b].getAttribute("data-step"));return this},showHint:function(a){T.call(this,a);return this},showHints:function(){var a=this._targetElement.querySelectorAll(".introjs-hint");if(a&&0<a.length)for(var b=0;b<a.length;b++)T.call(this,a[b].getAttribute("data-step"));else R.call(this,this._targetElement);
return this},removeHints:function(){var a=this._targetElement.querySelectorAll(".introjs-hint");if(a&&0<a.length)for(var b=0;b<a.length;b++)U.call(this,a[b].getAttribute("data-step"));return this},removeHint:function(a){U.call(this,a);return this}};return C.introJs=M});
(function(C,n){"object"===typeof exports?n(exports):"function"===typeof define&&define.amd?define(["exports"],n):n(C)})(this,function(C){function n(a){this._targetElement=a;this._introItems=[];this._options={nextLabel:"Next \x26rarr;",prevLabel:"\x26larr; Back",skipLabel:"Skip",doneLabel:"Done",hidePrev:!1,hideNext:!1,tooltipPosition:"bottom",tooltipClass:"",highlightClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,showProgress:!1,
scrollToElement:!0,scrollTo:"element",scrollPadding:30,overlayOpacity:.8,positionPrecedence:["bottom","top","right","left"],disableInteraction:!1,hintPosition:"top-middle",hintButtonLabel:"Got it",hintAnimation:!0}}function Z(a){var b,c=[],d=this;if(this._options.steps){var f=0;for(b=this._options.steps.length;f<b;f++){var e=y(this._options.steps[f]);e.step=c.length+1;"string"===typeof e.element&&(e.element=document.querySelector(e.element));if("undefined"===typeof e.element||null==e.element){var g=
document.querySelector(".introjsFloatingElement");null==g&&(g=document.createElement("div"),g.className="introjsFloatingElement",document.body.appendChild(g));e.element=g;e.position="floating"}e.scrollTo=e.scrollTo||this._options.scrollTo;"undefined"===typeof e.disableInteraction&&(e.disableInteraction=this._options.disableInteraction);null!=e.element&&c.push(e)}}else{g=a.querySelectorAll("*[data-intro]");if(1>g.length)return!1;for(var f=0,r=g.length;f<r;f++)if(e=g[f],"none"!=e.style.display){var q=
parseInt(e.getAttribute("data-step"),10);b=this._options.disableInteraction;"undefined"!=typeof e.getAttribute("data-disable-interaction")&&(b=!!e.getAttribute("data-disable-interaction"));0<q&&(c[q-1]={element:e,intro:e.getAttribute("data-intro"),step:parseInt(e.getAttribute("data-step"),10),tooltipClass:e.getAttribute("data-tooltipClass"),highlightClass:e.getAttribute("data-highlightClass"),position:e.getAttribute("data-position")||this._options.tooltipPosition,scrollTo:e.getAttribute("data-scrollTo")||
this._options.scrollTo,disableInteraction:b})}f=q=0;for(r=g.length;f<r;f++)if(e=g[f],null==e.getAttribute("data-step")){for(;"undefined"!=typeof c[q];)q++;b=this._options.disableInteraction;"undefined"!=typeof e.getAttribute("data-disable-interaction")&&(b=!!e.getAttribute("data-disable-interaction"));c[q]={element:e,intro:e.getAttribute("data-intro"),step:q+1,tooltipClass:e.getAttribute("data-tooltipClass"),highlightClass:e.getAttribute("data-highlightClass"),position:e.getAttribute("data-position")||
this._options.tooltipPosition,scrollTo:e.getAttribute("data-scrollTo")||this._options.scrollTo,disableInteraction:b}}}f=[];for(b=0;b<c.length;b++)c[b]&&f.push(c[b]);c=f;c.sort(function(a,b){return a.step-b.step});d._introItems=c;aa.call(d,a)&&(x.call(d),a.querySelector(".introjs-skipbutton"),a.querySelector(".introjs-nextbutton"),d._onKeyDown=function(b){if(27===b.keyCode&&1==d._options.exitOnEsc)z.call(d,a);else if(37===b.keyCode)E.call(d);else if(39===b.keyCode)x.call(d);else if(13===b.keyCode){var c=
b.target||b.srcElement;c&&0<c.className.indexOf("introjs-prevbutton")?E.call(d):c&&0<c.className.indexOf("introjs-skipbutton")?(d._introItems.length-1==d._currentStep&&"function"===typeof d._introCompleteCallback&&d._introCompleteCallback.call(d),z.call(d,a)):x.call(d);b.preventDefault?b.preventDefault():b.returnValue=!1}},d._onResize=function(a){d.refresh.call(d)},window.addEventListener?(this._options.keyboardNavigation&&window.addEventListener("keydown",d._onKeyDown,!0),window.addEventListener("resize",
d._onResize,!0)):document.attachEvent&&(this._options.keyboardNavigation&&document.attachEvent("onkeydown",d._onKeyDown),document.attachEvent("onresize",d._onResize)));return!1}function y(a){if(null==a||"object"!=typeof a||"undefined"!=typeof a.nodeType)return a;var b={},c;for(c in a)b[c]="undefined"!=typeof jQuery&&a[c]instanceof jQuery?a[c]:y(a[c]);return b}function x(){this._direction="forward";if("undefined"!==typeof this._currentStepNumber)for(var a=0,b=this._introItems.length;a<b;a++)this._introItems[a].step===
this._currentStepNumber&&(this._currentStep=a-1,this._currentStepNumber=void 0);"undefined"===typeof this._currentStep?this._currentStep=0:++this._currentStep;this._introItems.length<=this._currentStep?("function"===typeof this._introCompleteCallback&&this._introCompleteCallback.call(this),z.call(this,this._targetElement)):(a=this._introItems[this._currentStep],"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element),O.call(this,a))}function E(){this._direction=
"backward";if(0===this._currentStep)return!1;var a=this._introItems[--this._currentStep];"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element);O.call(this,a)}function z(a,b){var c=!0;void 0!=this._introBeforeExitCallback&&(c=this._introBeforeExitCallback.call(self));if(b||!1!==c){if((c=a.querySelectorAll(".introjs-overlay"))&&0<c.length)for(b=c.length-1;0<=b;b--){var d=c[b];d.style.opacity=0;setTimeout(function(){this.parentNode&&this.parentNode.removeChild(this)}.bind(d),
500)}(b=a.querySelector(".introjs-helperLayer"))&&b.parentNode.removeChild(b);(b=a.querySelector(".introjs-tooltipReferenceLayer"))&&b.parentNode.removeChild(b);(a=a.querySelector(".introjs-disableInteraction"))&&a.parentNode.removeChild(a);(a=document.querySelector(".introjsFloatingElement"))&&a.parentNode.removeChild(a);P();if((a=document.querySelectorAll(".introjs-fixParent"))&&0<a.length)for(b=a.length-1;0<=b;b--)a[b].className=a[b].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,
"");window.removeEventListener?window.removeEventListener("keydown",this._onKeyDown,!0):document.detachEvent&&document.detachEvent("onkeydown",this._onKeyDown);void 0!=this._introExitCallback&&this._introExitCallback.call(self);this._currentStep=void 0}}function F(a,b,c,d,f){f=f||!1;b.style.top=null;b.style.right=null;b.style.bottom=null;b.style.left=null;b.style.marginLeft=null;b.style.marginTop=null;c.style.display="inherit";"undefined"!=typeof d&&null!=d&&(d.style.top=null,d.style.left=null);if(this._introItems[this._currentStep]){var e=
this._introItems[this._currentStep];e="string"===typeof e.tooltipClass?e.tooltipClass:this._options.tooltipClass;b.className=("introjs-tooltip "+e).replace(/^\s+|\s+$/g,"");var g=this._introItems[this._currentStep].position;"floating"!=g&&(g="auto"===g?Q.call(this,a,b):Q.call(this,a,b,g));e=u(a);a=u(b);var r=H();switch(g){case "top":c.className="introjs-arrow bottom";I(e,f?0:15,a,r,b);b.style.bottom=e.height+20+"px";break;case "right":b.style.left=e.width+20+"px";e.top+a.height>r.height?(c.className=
"introjs-arrow left-bottom",b.style.top="-"+(a.height-e.height-20)+"px"):c.className="introjs-arrow left";break;case "left":f||1!=this._options.showStepNumbers||(b.style.top="15px");e.top+a.height>r.height?(b.style.top="-"+(a.height-e.height-20)+"px",c.className="introjs-arrow right-bottom"):c.className="introjs-arrow right";b.style.right=e.width+20+"px";break;case "floating":c.style.display="none";b.style.left="50%";b.style.top="50%";b.style.marginLeft="-"+a.width/2+"px";b.style.marginTop="-"+a.height/
2+"px";"undefined"!=typeof d&&null!=d&&(d.style.left="-"+(a.width/2+18)+"px",d.style.top="-"+(a.height/2+18)+"px");break;case "bottom-right-aligned":c.className="introjs-arrow top-right";R(e,0,a,b);b.style.top=e.height+20+"px";break;case "bottom-middle-aligned":c.className="introjs-arrow top-middle";c=e.width/2-a.width/2;f&&(c+=5);R(e,c,a,b)&&(b.style.right=null,I(e,c,a,r,b));b.style.top=e.height+20+"px";break;default:c.className="introjs-arrow top",I(e,0,a,r,b),b.style.top=e.height+20+"px"}}}function I(a,
b,c,d,f){if(a.left+b+c.width>d.width)return f.style.left=d.width-c.width-a.left+"px",!1;f.style.left=b+"px";return!0}function R(a,b,c,d){if(0>a.left+a.width-b-c.width)return d.style.left=-a.left+"px",!1;d.style.right=b+"px";return!0}function Q(a,b,c){var d=this._options.positionPrecedence.slice(),f=H(),e=u(b).height+10;b=u(b).width+20;a=u(a);var g="floating";a.left+b>f.width||0>a.left+a.width/2-b?(t(d,"bottom"),t(d,"top")):(a.height+a.top+e>f.height&&t(d,"bottom"),0>a.top-e&&t(d,"top"));a.width+a.left+
b>f.width&&t(d,"right");0>a.left-b&&t(d,"left");0<d.length&&(g=d[0]);c&&"auto"!=c&&-1<d.indexOf(c)&&(g=c);return g}function t(a,b){-1<a.indexOf(b)&&a.splice(a.indexOf(b),1)}function w(a){if(a&&this._introItems[this._currentStep]){var b=this._introItems[this._currentStep],c=u(b.element),d=10;J(b.element)?a.className+=" introjs-fixedTooltip":a.className=a.className.replace(" introjs-fixedTooltip","");"floating"==b.position&&(d=0);a.setAttribute("style","width: "+(c.width+d)+"px; height:"+(c.height+
d)+"px; top:"+(c.top-5)+"px;left: "+(c.left-5)+"px;")}}function ba(){var a=document.querySelector(".introjs-disableInteraction");null===a&&(a=document.createElement("div"),a.className="introjs-disableInteraction",this._targetElement.appendChild(a));w.call(this,a)}function D(a){a.setAttribute("role","button");a.tabIndex=0}function O(a){"undefined"!==typeof this._introChangeCallback&&this._introChangeCallback.call(this,a.element);var b=this,c=document.querySelector(".introjs-helperLayer"),d=document.querySelector(".introjs-tooltipReferenceLayer"),
f="introjs-helperLayer";u(a.element);"string"===typeof a.highlightClass&&(f+=" "+a.highlightClass);"string"===typeof this._options.highlightClass&&(f+=" "+this._options.highlightClass);if(null!=c){var e=d.querySelector(".introjs-helperNumberLayer"),g=d.querySelector(".introjs-tooltiptext"),r=d.querySelector(".introjs-arrow"),q=d.querySelector(".introjs-tooltip");var l=d.querySelector(".introjs-skipbutton");var h=d.querySelector(".introjs-prevbutton");var k=d.querySelector(".introjs-nextbutton");c.className=
f;q.style.opacity=0;q.style.display="none";if(null!=e){var p=this._introItems[0<=a.step-2?a.step-2:0];if(null!=p&&"forward"==this._direction&&"floating"==p.position||"backward"==this._direction&&"floating"==a.position)e.style.opacity=0}w.call(b,c);w.call(b,d);if((p=document.querySelectorAll(".introjs-fixParent"))&&0<p.length)for(f=p.length-1;0<=f;f--)p[f].className=p[f].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");P();b._lastShowElementTimer&&clearTimeout(b._lastShowElementTimer);
b._lastShowElementTimer=setTimeout(function(){null!=e&&(e.innerHTML=a.step);g.innerHTML=a.intro;q.style.display="block";F.call(b,a.element,q,r,e);b._options.showBullets&&(d.querySelector(".introjs-bullets li \x3e a.active").className="",d.querySelector('.introjs-bullets li \x3e a[data-stepnumber\x3d"'+a.step+'"]').className="active");d.querySelector(".introjs-progress .introjs-progressbar").setAttribute("style","width:"+S.call(b)+"%;");q.style.opacity=1;e&&(e.style.opacity=1);"undefined"!==typeof l&&
null!=l&&/introjs-donebutton/gi.test(l.className)?l.focus():"undefined"!==typeof k&&null!=k&&k.focus();T.call(b,a.scrollTo,a,g)},350)}else{var n=document.createElement("div");h=document.createElement("div");var c=document.createElement("div"),m=document.createElement("div"),t=document.createElement("div"),v=document.createElement("div"),G=document.createElement("div"),A=document.createElement("div");n.className=f;h.className="introjs-tooltipReferenceLayer";w.call(b,n);w.call(b,h);this._targetElement.appendChild(n);
this._targetElement.appendChild(h);c.className="introjs-arrow";t.className="introjs-tooltiptext";t.innerHTML=a.intro;v.className="introjs-bullets";!1===this._options.showBullets&&(v.style.display="none");for(var n=document.createElement("ul"),f=0,C=this._introItems.length;f<C;f++){var y=document.createElement("li"),B=document.createElement("a");B.onclick=function(){b.goToStep(this.getAttribute("data-stepnumber"))};f===a.step-1&&(B.className="active");D(B);B.innerHTML="\x26nbsp;";B.setAttribute("data-stepnumber",
this._introItems[f].step);y.appendChild(B);n.appendChild(y)}v.appendChild(n);G.className="introjs-progress";!1===this._options.showProgress&&(G.style.display="none");f=document.createElement("div");f.className="introjs-progressbar";f.setAttribute("style","width:"+S.call(this)+"%;");G.appendChild(f);A.className="introjs-tooltipbuttons";!1===this._options.showButtons&&(A.style.display="none");m.className="introjs-tooltip";m.appendChild(t);m.appendChild(v);m.appendChild(G);1==this._options.showStepNumbers&&
(p=document.createElement("span"),p.className="introjs-helperNumberLayer",p.innerHTML=a.step,h.appendChild(p));m.appendChild(c);h.appendChild(m);k=document.createElement("a");k.onclick=function(){b._introItems.length-1!=b._currentStep&&x.call(b)};D(k);k.innerHTML=this._options.nextLabel;h=document.createElement("a");h.onclick=function(){0!=b._currentStep&&E.call(b)};D(h);h.innerHTML=this._options.prevLabel;l=document.createElement("a");l.className="introjs-button introjs-skipbutton";D(l);l.innerHTML=
this._options.skipLabel;l.onclick=function(){b._introItems.length-1==b._currentStep&&"function"===typeof b._introCompleteCallback&&b._introCompleteCallback.call(b);z.call(b,b._targetElement)};A.appendChild(l);1<this._introItems.length&&(A.appendChild(h),A.appendChild(k));m.appendChild(A);F.call(b,a.element,m,c,p);T.call(this,a.scrollTo,a,m)}(p=b._targetElement.querySelector(".introjs-disableInteraction"))&&p.parentNode.removeChild(p);a.disableInteraction&&ba.call(b);"undefined"!==typeof k&&null!=
k&&k.removeAttribute("tabIndex");"undefined"!==typeof h&&null!=h&&h.removeAttribute("tabIndex");0==this._currentStep&&1<this._introItems.length?("undefined"!==typeof l&&null!=l&&(l.className="introjs-button introjs-skipbutton"),"undefined"!==typeof k&&null!=k&&(k.className="introjs-button introjs-nextbutton"),1==this._options.hidePrev?("undefined"!==typeof h&&null!=h&&(h.className="introjs-button introjs-prevbutton introjs-hidden"),"undefined"!==typeof k&&null!=k&&(k.className+=" introjs-fullbutton")):
"undefined"!==typeof h&&null!=h&&(h.className="introjs-button introjs-prevbutton introjs-disabled"),"undefined"!==typeof h&&null!=h&&(h.tabIndex="-1"),"undefined"!==typeof l&&null!=l&&(l.innerHTML=this._options.skipLabel)):this._introItems.length-1==this._currentStep||1==this._introItems.length?("undefined"!==typeof l&&null!=l&&(l.innerHTML=this._options.doneLabel,l.className+=" introjs-donebutton"),"undefined"!==typeof h&&null!=h&&(h.className="introjs-button introjs-prevbutton"),1==this._options.hideNext?
("undefined"!==typeof k&&null!=k&&(k.className="introjs-button introjs-nextbutton introjs-hidden"),"undefined"!==typeof h&&null!=h&&(h.className+=" introjs-fullbutton")):"undefined"!==typeof k&&null!=k&&(k.className="introjs-button introjs-nextbutton introjs-disabled"),"undefined"!==typeof k&&null!=k&&(k.tabIndex="-1")):("undefined"!==typeof l&&null!=l&&(l.className="introjs-button introjs-skipbutton"),"undefined"!==typeof h&&null!=h&&(h.className="introjs-button introjs-prevbutton"),"undefined"!==
typeof k&&null!=k&&(k.className="introjs-button introjs-nextbutton"),"undefined"!==typeof l&&null!=l&&(l.innerHTML=this._options.skipLabel));"undefined"!==typeof k&&null!=k&&k.focus();ca(a);"undefined"!==typeof this._introAfterChangeCallback&&this._introAfterChangeCallback.call(this,a.element)}function T(a,b,c){this._options.scrollToElement&&(a="tooltip"===a?c.getBoundingClientRect():b.element.getBoundingClientRect(),c=b.element.getBoundingClientRect(),0<=c.top&&0<=c.left&&c.bottom+80<=window.innerHeight&&
c.right<=window.innerWidth||(c=H().height,0>a.bottom-(a.bottom-a.top)||b.element.clientHeight>c?window.scrollBy(0,a.top-(c/2-a.height/2)-this._options.scrollPadding):window.scrollBy(0,a.top-(c/2-a.height/2)+this._options.scrollPadding)))}function P(){for(var a=document.querySelectorAll(".introjs-showElement"),b=0,c=a.length;b<c;b++){var d=a[b],f=/introjs-[a-zA-Z]+/g;if(d instanceof SVGElement){var e=d.getAttribute("class")||"";d.setAttribute("class",e.replace(f,"").replace(/^\s+|\s+$/g,""))}else d.className=
d.className.replace(f,"").replace(/^\s+|\s+$/g,"")}}function ca(a){var b;if(a.element instanceof SVGElement)for(b=a.element.parentNode;null!=a.element.parentNode&&b.tagName&&"body"!==b.tagName.toLowerCase();)"svg"===b.tagName.toLowerCase()&&K(b,"introjs-showElement introjs-relativePosition"),b=b.parentNode;K(a.element,"introjs-showElement");b=m(a.element,"position");"absolute"!==b&&"relative"!==b&&"fixed"!==b&&K(a.element,"introjs-relativePosition");for(b=a.element.parentNode;null!=b&&b.tagName&&
"body"!==b.tagName.toLowerCase();){a=m(b,"z-index");var c=parseFloat(m(b,"opacity")),d=m(b,"transform")||m(b,"-webkit-transform")||m(b,"-moz-transform")||m(b,"-ms-transform")||m(b,"-o-transform");if(/[0-9]+/.test(a)||1>c||"none"!==d&&void 0!==d)b.className+=" introjs-fixParent";b=b.parentNode}}function K(a,b){if(a instanceof SVGElement){var c=a.getAttribute("class")||"";a.setAttribute("class",c+" "+b)}else a.className+=" "+b}function m(a,b){var c="";a.currentStyle?c=a.currentStyle[b]:document.defaultView&&
document.defaultView.getComputedStyle&&(c=document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return c&&c.toLowerCase?c.toLowerCase():c}function J(a){var b=a.parentNode;return b&&"HTML"!==b.nodeName?"fixed"==m(a,"position")?!0:J(b):!1}function H(){if(void 0!=window.innerWidth)return{width:window.innerWidth,height:window.innerHeight};var a=document.documentElement;return{width:a.clientWidth,height:a.clientHeight}}function aa(a){var b=document.createElement("div"),c="",d=this;b.className=
"introjs-overlay";if(a.tagName&&"body"!==a.tagName.toLowerCase()){var f=u(a);f&&(c+="width: "+f.width+"px; height:"+f.height+"px; top:"+f.top+"px;left: "+f.left+"px;",b.setAttribute("style",c))}else c+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;",b.setAttribute("style",c);a.appendChild(b);b.onclick=function(){1==d._options.exitOnOverlayClick&&z.call(d,a)};setTimeout(function(){c+="opacity: "+d._options.overlayOpacity.toString()+";";b.setAttribute("style",c)},10);return!0}function v(){var a=
this._targetElement.querySelector(".introjs-hintReference");if(a){var b=a.getAttribute("data-step");a.parentNode.removeChild(a);return b}}function U(a){this._introItems=[];if(this._options.hints){a=0;for(var b=this._options.hints.length;a<b;a++){var c=y(this._options.hints[a]);"string"===typeof c.element&&(c.element=document.querySelector(c.element));c.hintPosition=c.hintPosition||this._options.hintPosition;c.hintAnimation=c.hintAnimation||this._options.hintAnimation;null!=c.element&&this._introItems.push(c)}}else{c=
a.querySelectorAll("*[data-hint]");if(1>c.length)return!1;a=0;for(b=c.length;a<b;a++){var d=c[a],f=d.getAttribute("data-hintAnimation"),f=f?"true"==f:this._options.hintAnimation;this._introItems.push({element:d,hint:d.getAttribute("data-hint"),hintPosition:d.getAttribute("data-hintPosition")||this._options.hintPosition,hintAnimation:f,tooltipClass:d.getAttribute("data-tooltipClass"),position:d.getAttribute("data-position")||this._options.tooltipPosition})}}da.call(this);document.addEventListener?
(document.addEventListener("click",v.bind(this),!1),window.addEventListener("resize",L.bind(this),!0)):document.attachEvent&&(document.attachEvent("onclick",v.bind(this)),document.attachEvent("onresize",L.bind(this)))}function L(){for(var a=0,b=this._introItems.length;a<b;a++){var c=this._introItems[a];"undefined"!=typeof c.targetElement&&V.call(this,c.hintPosition,c.element,c.targetElement)}}function M(a){v.call(this);var b=this._targetElement.querySelector('.introjs-hint[data-step\x3d"'+a+'"]');
b&&(b.className+=" introjs-hidehint");"undefined"!==typeof this._hintCloseCallback&&this._hintCloseCallback.call(this,a)}function W(a){if(a=this._targetElement.querySelector('.introjs-hint[data-step\x3d"'+a+'"]'))a.className=a.className.replace(/introjs\-hidehint/g,"")}function X(a){(a=this._targetElement.querySelector('.introjs-hint[data-step\x3d"'+a+'"]'))&&a.parentNode.removeChild(a)}function da(){var a=this;var b=document.querySelector(".introjs-hints");null==b&&(b=document.createElement("div"),
b.className="introjs-hints");for(var c=0,d=this._introItems.length;c<d;c++){var f=this._introItems[c];if(!document.querySelector('.introjs-hint[data-step\x3d"'+c+'"]')){var e=document.createElement("a");D(e);(function(b,c,d){b.onclick=function(b){b=b?b:window.event;b.stopPropagation&&b.stopPropagation();null!=b.cancelBubble&&(b.cancelBubble=!0);Y.call(a,d)}})(e,f,c);e.className="introjs-hint";f.hintAnimation||(e.className+=" introjs-hint-no-anim");J(f.element)&&(e.className+=" introjs-fixedhint");
var g=document.createElement("div");g.className="introjs-hint-dot";var m=document.createElement("div");m.className="introjs-hint-pulse";e.appendChild(g);e.appendChild(m);e.setAttribute("data-step",c);f.targetElement=f.element;f.element=e;V.call(this,f.hintPosition,e,f.targetElement);b.appendChild(e)}}document.body.appendChild(b);"undefined"!==typeof this._hintsAddedCallback&&this._hintsAddedCallback.call(this)}function V(a,b,c){c=u.call(this,c);switch(a){default:case "top-left":b.style.left=c.left+
"px";b.style.top=c.top+"px";break;case "top-right":b.style.left=c.left+c.width-20+"px";b.style.top=c.top+"px";break;case "bottom-left":b.style.left=c.left+"px";b.style.top=c.top+c.height-20+"px";break;case "bottom-right":b.style.left=c.left+c.width-20+"px";b.style.top=c.top+c.height-20+"px";break;case "middle-left":b.style.left=c.left+"px";b.style.top=c.top+(c.height-20)/2+"px";break;case "middle-right":b.style.left=c.left+c.width-20+"px";b.style.top=c.top+(c.height-20)/2+"px";break;case "middle-middle":b.style.left=
c.left+(c.width-20)/2+"px";b.style.top=c.top+(c.height-20)/2+"px";break;case "bottom-middle":b.style.left=c.left+(c.width-20)/2+"px";b.style.top=c.top+c.height-20+"px";break;case "top-middle":b.style.left=c.left+(c.width-20)/2+"px",b.style.top=c.top+"px"}}function Y(a){var b=document.querySelector('.introjs-hint[data-step\x3d"'+a+'"]'),c=this._introItems[a];"undefined"!==typeof this._hintClickCallback&&this._hintClickCallback.call(this,b,c,a);var d=v.call(this);if(parseInt(d,10)!=a){var d=document.createElement("div"),
f=document.createElement("div"),e=document.createElement("div"),g=document.createElement("div");d.className="introjs-tooltip";d.onclick=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0};f.className="introjs-tooltiptext";var m=document.createElement("p");m.innerHTML=c.hint;c=document.createElement("a");c.className="introjs-button";c.innerHTML=this._options.hintButtonLabel;c.onclick=M.bind(this,a);f.appendChild(m);f.appendChild(c);e.className="introjs-arrow";d.appendChild(e);d.appendChild(f);
this._currentStep=b.getAttribute("data-step");g.className="introjs-tooltipReferenceLayer introjs-hintReference";g.setAttribute("data-step",b.getAttribute("data-step"));w.call(this,g);g.appendChild(d);document.body.appendChild(g);F.call(this,b,d,e,null,!0)}}function u(a){var b={},c=document.body,d=document.documentElement,f=window.pageYOffset||d.scrollTop||c.scrollTop,c=window.pageXOffset||d.scrollLeft||c.scrollLeft;if(a instanceof SVGElement)a=a.getBoundingClientRect(),b.top=a.top+f,b.width=a.width,
b.height=a.height,b.left=a.left+c;else{b.width=a.offsetWidth;b.height=a.offsetHeight;for(c=f=0;a&&!isNaN(a.offsetLeft)&&!isNaN(a.offsetTop);)f+=a.offsetLeft,c+=a.offsetTop,a=a.offsetParent;b.top=c;b.left=f}return b}function S(){return parseInt(this._currentStep+1,10)/this._introItems.length*100}var N=function(a){if("object"===typeof a)return new n(a);if("string"===typeof a){if(a=document.querySelector(a))return new n(a);throw Error("There is no element with given selector.");}return new n(document.body)};
N.version="2.7.0";N.fn=n.prototype={clone:function(){return new n(this)},setOption:function(a,b){this._options[a]=b;return this},setOptions:function(a){var b=this._options,c={},d;for(d in b)c[d]=b[d];for(d in a)c[d]=a[d];this._options=c;return this},start:function(){Z.call(this,this._targetElement);return this},goToStep:function(a){this._currentStep=a-2;"undefined"!==typeof this._introItems&&x.call(this);return this},addStep:function(a){this._options.steps||(this._options.steps=[]);this._options.steps.push(a);
return this},addSteps:function(a){if(a.length){for(var b=0;b<a.length;b++)this.addStep(a[b]);return this}},goToStepNumber:function(a){this._currentStepNumber=a;"undefined"!==typeof this._introItems&&x.call(this);return this},nextStep:function(){x.call(this);return this},previousStep:function(){E.call(this);return this},exit:function(a){z.call(this,this._targetElement,a);return this},refresh:function(){w.call(this,document.querySelector(".introjs-helperLayer"));w.call(this,document.querySelector(".introjs-tooltipReferenceLayer"));
if(void 0!==this._currentStep&&null!==this._currentStep){var a=document.querySelector(".introjs-helperNumberLayer"),b=document.querySelector(".introjs-arrow"),c=document.querySelector(".introjs-tooltip");F.call(this,this._introItems[this._currentStep].element,c,b,a)}L.call(this);return this},onbeforechange:function(a){if("function"===typeof a)this._introBeforeChangeCallback=a;else throw Error("Provided callback for onbeforechange was not a function");return this},onchange:function(a){if("function"===
typeof a)this._introChangeCallback=a;else throw Error("Provided callback for onchange was not a function.");return this},onafterchange:function(a){if("function"===typeof a)this._introAfterChangeCallback=a;else throw Error("Provided callback for onafterchange was not a function");return this},oncomplete:function(a){if("function"===typeof a)this._introCompleteCallback=a;else throw Error("Provided callback for oncomplete was not a function.");return this},onhintsadded:function(a){if("function"===typeof a)this._hintsAddedCallback=
a;else throw Error("Provided callback for onhintsadded was not a function.");return this},onhintclick:function(a){if("function"===typeof a)this._hintClickCallback=a;else throw Error("Provided callback for onhintclick was not a function.");return this},onhintclose:function(a){if("function"===typeof a)this._hintCloseCallback=a;else throw Error("Provided callback for onhintclose was not a function.");return this},onexit:function(a){if("function"===typeof a)this._introExitCallback=a;else throw Error("Provided callback for onexit was not a function.");
return this},onbeforeexit:function(a){if("function"===typeof a)this._introBeforeExitCallback=a;else throw Error("Provided callback for onbeforeexit was not a function.");return this},addHints:function(){U.call(this,this._targetElement);return this},hideHint:function(a){M.call(this,a);return this},hideHints:function(){var a=this._targetElement.querySelectorAll(".introjs-hint");if(a&&0<a.length)for(var b=0;b<a.length;b++)M.call(this,a[b].getAttribute("data-step"));return this},showHint:function(a){W.call(this,
a);return this},showHints:function(){var a=this._targetElement.querySelectorAll(".introjs-hint");if(a&&0<a.length)for(var b=0;b<a.length;b++)W.call(this,a[b].getAttribute("data-step"));else U.call(this,this._targetElement);return this},removeHints:function(){var a=this._targetElement.querySelectorAll(".introjs-hint");if(a&&0<a.length)for(var b=0;b<a.length;b++)X.call(this,a[b].getAttribute("data-step"));return this},removeHint:function(a){X.call(this,a);return this},showHintDialog:function(a){Y.call(this,
a);return this}};return C.introJs=N});

View File

@ -1 +1 @@
.introjs-tooltipbuttons{text-align:left}.introjs-skipbutton{margin-left:5px}.introjs-tooltip{direction:rtl}.introjs-prevbutton{border:1px solid #d4d4d4;border-left:none;-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-nextbutton{border:1px solid #d4d4d4;-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em}
.introjs-tooltipbuttons{text-align:left}.introjs-skipbutton{margin-left:5px}.introjs-tooltip{direction:rtl}.introjs-prevbutton{border:1px solid #d4d4d4;border-left:none;-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-nextbutton{border:1px solid #d4d4d4;-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em}.introjs-bullets ul li{float:right}

File diff suppressed because one or more lines are too long

View File

@ -21,13 +21,12 @@
declare(strict_types=1);
return [
'locale' => 'de, Deutsch, de_DE, de_DE.utf8, de_DE.UTF-8',
'month' => '%B %Y',
'month_and_day' => '%e. %B %Y',
'date_time' => '%e %B %Y, @ %T',
'specific_day' => '%e. %B %Y',
'week_in_year' => 'Woche %W, %Y',
'quarter_of_year' => '%B %Y',
'year' => '%Y',
'half_year' => '%B %Y',
'locale' => 'de, Deutsch, de_DE, de_DE.utf8, de_DE.UTF-8',
'month' => '%B %Y',
'month_and_day' => '%e. %B %Y',
'date_time' => '%e %B %Y, @ %T',
'specific_day' => '%e. %B %Y',
'week_in_year' => 'Woche %W, %Y',
'year' => '%Y',
'half_year' => '%B %Y',
];

Some files were not shown because too many files have changed in this diff Show More