From 5d0cdc4ffa12ece600ab78d70eacefc9f3903068 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 17 Feb 2017 06:42:36 +0100 Subject: [PATCH 001/244] Various code cleanup. --- app/Console/Commands/CreateImport.php | 2 +- .../Commands/UpgradeFireflyInstructions.php | 31 ++++--- app/Export/Collector/AttachmentCollector.php | 1 - app/Export/Collector/CollectorInterface.php | 14 +-- app/Helpers/Attachments/AttachmentHelper.php | 2 +- app/Helpers/Collector/JournalCollector.php | 1 + app/Helpers/Report/BalanceReportHelper.php | 4 +- app/Helpers/Report/ReportHelper.php | 3 - app/Helpers/Report/ReportHelperInterface.php | 1 - app/Http/Controllers/AccountController.php | 26 +++--- app/Http/Controllers/HomeController.php | 3 +- app/Http/Controllers/ReportController.php | 72 +++++++-------- app/Http/Middleware/Range.php | 1 - app/Http/Middleware/StartFireflySession.php | 1 - app/Http/Middleware/VerifyCsrfToken.php | 8 +- app/Http/Requests/Request.php | 2 +- app/Http/Requests/TagFormRequest.php | 1 - app/Import/ImportStorage.php | 4 +- app/Import/Setup/CsvSetup.php | 4 +- app/Models/Budget.php | 2 +- app/Providers/FireflyServiceProvider.php | 6 +- app/Providers/PiggyBankServiceProvider.php | 2 +- app/Providers/RuleServiceProvider.php | 1 + .../Currency/CurrencyRepository.php | 16 ++-- .../Journal/JournalRepository.php | 16 ++-- app/Repositories/Tag/TagRepository.php | 92 +++++++++---------- app/Rules/Actions/SetBudget.php | 6 +- app/Rules/Actions/SetDestinationAccount.php | 2 +- app/Rules/Actions/SetSourceAccount.php | 2 +- app/Rules/Triggers/AbstractTrigger.php | 1 - app/Support/Search/SearchInterface.php | 10 +- 31 files changed, 168 insertions(+), 169 deletions(-) diff --git a/app/Console/Commands/CreateImport.php b/app/Console/Commands/CreateImport.php index 00ff5094c6..d9b51a3b6d 100644 --- a/app/Console/Commands/CreateImport.php +++ b/app/Console/Commands/CreateImport.php @@ -81,7 +81,7 @@ class CreateImport extends Command /** @var ImportJobRepositoryInterface $jobRepository */ $jobRepository = app(ImportJobRepositoryInterface::class); $jobRepository->setUser($user); - $job = $jobRepository->create($type); + $job = $jobRepository->create($type); $this->line(sprintf('Created job "%s"...', $job->key)); Artisan::call('firefly:encrypt', ['file' => $file, 'key' => $job->key]); diff --git a/app/Console/Commands/UpgradeFireflyInstructions.php b/app/Console/Commands/UpgradeFireflyInstructions.php index a88c8ab79c..03e7761172 100644 --- a/app/Console/Commands/UpgradeFireflyInstructions.php +++ b/app/Console/Commands/UpgradeFireflyInstructions.php @@ -84,21 +84,8 @@ class UpgradeFireflyInstructions extends Command } } - /** - * Show a line - */ - private function showLine() + private function installInstructions() { - $line = '+'; - for ($i = 0; $i < 78; $i++) { - $line .= '-'; - } - $line .= '+'; - $this->line($line); - - } - - private function installInstructions() { /** @var string $version */ $version = config('firefly.version'); $config = config('upgrade.text.install'); @@ -120,6 +107,7 @@ class UpgradeFireflyInstructions extends Command $this->boxed('Firefly III should be ready for use.'); $this->boxed(''); $this->showLine(); + return; } @@ -129,6 +117,20 @@ class UpgradeFireflyInstructions extends Command $this->showLine(); } + /** + * Show a line + */ + private function showLine() + { + $line = '+'; + for ($i = 0; $i < 78; $i++) { + $line .= '-'; + } + $line .= '+'; + $this->line($line); + + } + private function updateInstructions() { /** @var string $version */ @@ -152,6 +154,7 @@ class UpgradeFireflyInstructions extends Command $this->boxed('Firefly III should be ready for use.'); $this->boxed(''); $this->showLine(); + return; } diff --git a/app/Export/Collector/AttachmentCollector.php b/app/Export/Collector/AttachmentCollector.php index 810dc1db85..f65cb72ae3 100644 --- a/app/Export/Collector/AttachmentCollector.php +++ b/app/Export/Collector/AttachmentCollector.php @@ -16,7 +16,6 @@ namespace FireflyIII\Export\Collector; use Carbon\Carbon; use Crypt; use FireflyIII\Models\Attachment; -use FireflyIII\Models\ExportJob; use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Support\Collection; diff --git a/app/Export/Collector/CollectorInterface.php b/app/Export/Collector/CollectorInterface.php index 54a3fa88a1..6cbbac9c2a 100644 --- a/app/Export/Collector/CollectorInterface.php +++ b/app/Export/Collector/CollectorInterface.php @@ -33,13 +33,6 @@ interface CollectorInterface */ public function run(): bool; - /** - * @param ExportJob $job - * - * @return mixed - */ - public function setJob(ExportJob $job); - /** * @param Collection $entries * @@ -48,4 +41,11 @@ interface CollectorInterface */ public function setEntries(Collection $entries); + /** + * @param ExportJob $job + * + * @return mixed + */ + public function setJob(ExportJob $job); + } diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index c7f7902109..74b189469b 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -45,7 +45,7 @@ class AttachmentHelper implements AttachmentHelperInterface public function __construct() { $this->maxUploadSize = intval(config('firefly.maxUploadSize')); - $this->allowedMimes = (array) config('firefly.allowedMimes'); + $this->allowedMimes = (array)config('firefly.allowedMimes'); $this->errors = new MessageBag; $this->messages = new MessageBag; $this->uploadDisk = Storage::disk('upload'); diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index f709eff9ba..3a146310fd 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -503,6 +503,7 @@ class JournalCollector implements JournalCollectorInterface public function withOpposingAccount(): JournalCollectorInterface { $this->joinOpposingTables(); + return $this; } diff --git a/app/Helpers/Report/BalanceReportHelper.php b/app/Helpers/Report/BalanceReportHelper.php index ca0667cf66..2c6dae78a8 100644 --- a/app/Helpers/Report/BalanceReportHelper.php +++ b/app/Helpers/Report/BalanceReportHelper.php @@ -158,7 +158,9 @@ class BalanceReportHelper implements BalanceReportHelperInterface foreach ($accounts as $account) { $balanceEntry = new BalanceEntry; $balanceEntry->setAccount($account); - $spent = $this->budgetRepository->spentInPeriod(new Collection([$budgetLimit->budget]), new Collection([$account]), $budgetLimit->start_date, $budgetLimit->end_date); + $spent = $this->budgetRepository->spentInPeriod( + new Collection([$budgetLimit->budget]), new Collection([$account]), $budgetLimit->start_date, $budgetLimit->end_date + ); $balanceEntry->setSpent($spent); $line->addBalanceEntry($balanceEntry); } diff --git a/app/Helpers/Report/ReportHelper.php b/app/Helpers/Report/ReportHelper.php index 2dfb605ee3..20a008e87a 100644 --- a/app/Helpers/Report/ReportHelper.php +++ b/app/Helpers/Report/ReportHelper.php @@ -16,15 +16,12 @@ namespace FireflyIII\Helpers\Report; use Carbon\Carbon; use FireflyIII\Helpers\Collection\Bill as BillCollection; use FireflyIII\Helpers\Collection\BillLine; -use FireflyIII\Helpers\Collection\Category as CategoryCollection; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\FiscalHelperInterface; use FireflyIII\Models\Bill; -use FireflyIII\Models\Category; use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use Illuminate\Support\Collection; /** diff --git a/app/Helpers/Report/ReportHelperInterface.php b/app/Helpers/Report/ReportHelperInterface.php index 9124821593..fb14625126 100644 --- a/app/Helpers/Report/ReportHelperInterface.php +++ b/app/Helpers/Report/ReportHelperInterface.php @@ -15,7 +15,6 @@ namespace FireflyIII\Helpers\Report; use Carbon\Carbon; use FireflyIII\Helpers\Collection\Bill as BillCollection; -use FireflyIII\Helpers\Collection\Category as CategoryCollection; use FireflyIII\Helpers\Collection\Expense; use FireflyIII\Helpers\Collection\Income; use Illuminate\Support\Collection; diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 66b2ff26a0..47febc0f84 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -116,9 +116,9 @@ class AccountController extends Controller } /** - * @param Request $request - * @param AccountRepositoryInterface $repository - * @param Account $account + * @param Request $request + * @param AccountRepositoryInterface $repository + * @param Account $account * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ @@ -190,8 +190,8 @@ class AccountController extends Controller } /** - * @param AccountRepositoryInterface $repository - * @param string $what + * @param AccountRepositoryInterface $repository + * @param string $what * * @return View */ @@ -260,9 +260,9 @@ class AccountController extends Controller } /** - * @param Request $request - * @param AccountRepositoryInterface $repository - * @param Account $account + * @param Request $request + * @param AccountRepositoryInterface $repository + * @param Account $account * * @return View */ @@ -323,8 +323,8 @@ class AccountController extends Controller } /** - * @param AccountFormRequest $request - * @param AccountRepositoryInterface $repository + * @param AccountFormRequest $request + * @param AccountRepositoryInterface $repository * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @@ -356,9 +356,9 @@ class AccountController extends Controller } /** - * @param AccountFormRequest $request - * @param AccountRepositoryInterface $repository - * @param Account $account + * @param AccountFormRequest $request + * @param AccountRepositoryInterface $repository + * @param Account $account * * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 9dbce2de32..460a3e98cd 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -23,7 +23,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Log; use Preferences; -use Route; use Session; use View; @@ -91,7 +90,7 @@ class HomeController extends Controller public function flush(Request $request) { Preferences::mark(); - $request->session()->forget(['start', 'end','_previous', 'viewRange', 'range', 'is_custom_range']); + $request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range']); Artisan::call('cache:clear'); return redirect(route('index')); diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 5c32e086f9..a3ac8ab74a 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -98,42 +98,6 @@ class ReportController extends Controller } - /** - * @param Collection $accounts - * @param Collection $tags - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function tagReport(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) - { - if ($end < $start) { - return view('error')->with('message', trans('firefly.end_after_start_date')); - } - if ($start < session('first')) { - $start = session('first'); - } - - View::share( - 'subTitle', trans( - 'firefly.report_tag', - [ - 'start' => $start->formatLocalized($this->monthFormat), - 'end' => $end->formatLocalized($this->monthFormat), - ] - ) - ); - - $generator = ReportGeneratorFactory::reportGenerator('Tag', $start, $end); - $generator->setAccounts($accounts); - $generator->setTags($tags); - $result = $generator->generate(); - - return $result; - - } - /** * @param Collection $accounts * @param Collection $budgets @@ -357,6 +321,42 @@ class ReportController extends Controller return redirect($uri); } + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function tagReport(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + if ($end < $start) { + return view('error')->with('message', trans('firefly.end_after_start_date')); + } + if ($start < session('first')) { + $start = session('first'); + } + + View::share( + 'subTitle', trans( + 'firefly.report_tag', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + + $generator = ReportGeneratorFactory::reportGenerator('Tag', $start, $end); + $generator->setAccounts($accounts); + $generator->setTags($tags); + $result = $generator->generate(); + + return $result; + + } + /** * @return string */ diff --git a/app/Http/Middleware/Range.php b/app/Http/Middleware/Range.php index fdf5322a4d..7bc60818c9 100644 --- a/app/Http/Middleware/Range.php +++ b/app/Http/Middleware/Range.php @@ -17,7 +17,6 @@ use Amount; use App; use Carbon\Carbon; use Closure; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request; diff --git a/app/Http/Middleware/StartFireflySession.php b/app/Http/Middleware/StartFireflySession.php index ba6c518a08..f72e13aed3 100644 --- a/app/Http/Middleware/StartFireflySession.php +++ b/app/Http/Middleware/StartFireflySession.php @@ -14,7 +14,6 @@ namespace FireflyIII\Http\Middleware; use Illuminate\Http\Request; use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\SessionManager; -use Log; /** * Class StartFireflySession diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 11ec0455c9..0c7c3ed591 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -12,9 +12,10 @@ declare(strict_types = 1); namespace FireflyIII\Http\Middleware; +use Carbon\Carbon; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier; use Symfony\Component\HttpFoundation\Cookie; -use Carbon\Carbon; + /** * Class VerifyCsrfToken * @@ -35,8 +36,9 @@ class VerifyCsrfToken extends BaseVerifier /** * Add the CSRF token to the response cookies. * - * @param \Illuminate\Http\Request $request - * @param \Symfony\Component\HttpFoundation\Response $response + * @param \Illuminate\Http\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response + * * @return \Symfony\Component\HttpFoundation\Response */ protected function addCookieToResponse($request, $response) diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index e73b17ac60..cf8bbae71e 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -87,7 +87,7 @@ class Request extends FormRequest */ protected function string(string $field): string { - $string = $this->get($field) ?? ''; + $string = $this->get($field) ?? ''; $search = [ "\u{0001}", // start of heading "\u{0002}", // start of text diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index 544b53c0b0..ce85aaa94f 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -12,7 +12,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; use FireflyIII\Repositories\Tag\TagRepositoryInterface; /** diff --git a/app/Import/ImportStorage.php b/app/Import/ImportStorage.php index ec68385e0d..89b5e76ded 100644 --- a/app/Import/ImportStorage.php +++ b/app/Import/ImportStorage.php @@ -162,7 +162,7 @@ class ImportStorage /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); $repository->setUser($this->user); - $data = [ + $data = [ 'tag' => trans('firefly.import_with_key', ['key' => $this->job->key]), 'date' => new Carbon, 'description' => null, @@ -171,7 +171,7 @@ class ImportStorage 'zoomLevel' => null, 'tagMode' => 'nothing', ]; - $tag = $repository->store($data); + $tag = $repository->store($data); return $tag; } diff --git a/app/Import/Setup/CsvSetup.php b/app/Import/Setup/CsvSetup.php index 3d0fc10ed3..01fcdc756a 100644 --- a/app/Import/Setup/CsvSetup.php +++ b/app/Import/Setup/CsvSetup.php @@ -182,8 +182,8 @@ class CsvSetup implements SetupInterface { /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); - $importId = $data['csv_import_account'] ?? 0; - $account = $repository->find(intval($importId)); + $importId = $data['csv_import_account'] ?? 0; + $account = $repository->find(intval($importId)); $hasHeaders = isset($data['has_headers']) && intval($data['has_headers']) === 1 ? true : false; $config = $this->job->configuration; diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 504802e05a..84ef039aa0 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -44,7 +44,7 @@ class Budget extends Model 'encrypted' => 'boolean', ]; /** @var array */ - protected $dates = ['created_at', 'updated_at', 'deleted_at']; + protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $fillable = ['user_id', 'name', 'active']; protected $hidden = ['encrypted']; protected $rules = ['name' => 'required|between:1,200',]; diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 2ef97e48d2..40f456b4d0 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -122,14 +122,14 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind(MetaPieChartInterface::class, MetaPieChart::class); // other generators - $this->app->bind(ProcessorInterface::class,Processor::class); - $this->app->bind(ImportProcedureInterface::class,ImportProcedure::class); + $this->app->bind(ProcessorInterface::class, Processor::class); + $this->app->bind(ImportProcedureInterface::class, ImportProcedure::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class); $this->app->bind(HelpInterface::class, Help::class); $this->app->bind(ReportHelperInterface::class, ReportHelper::class); - $this->app->bind(FiscalHelperInterface::class,FiscalHelper::class); + $this->app->bind(FiscalHelperInterface::class, FiscalHelper::class); $this->app->bind(BalanceReportHelperInterface::class, BalanceReportHelper::class); $this->app->bind(BudgetReportHelperInterface::class, BudgetReportHelper::class); } diff --git a/app/Providers/PiggyBankServiceProvider.php b/app/Providers/PiggyBankServiceProvider.php index fb1aaf7d5f..d91f362114 100644 --- a/app/Providers/PiggyBankServiceProvider.php +++ b/app/Providers/PiggyBankServiceProvider.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Providers; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Repositories\PiggyBank\PiggyBankRepository; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Foundation\Application; @@ -53,6 +52,7 @@ class PiggyBankServiceProvider extends ServiceProvider if ($app->auth->check()) { $repository->setUser(auth()->user()); } + return $repository; } ); diff --git a/app/Providers/RuleServiceProvider.php b/app/Providers/RuleServiceProvider.php index 5d694411b3..9d047d9e74 100644 --- a/app/Providers/RuleServiceProvider.php +++ b/app/Providers/RuleServiceProvider.php @@ -51,6 +51,7 @@ class RuleServiceProvider extends ServiceProvider if ($app->auth->check()) { $repository->setUser(auth()->user()); } + return $repository; } ); diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index bb1444d73c..b053780cac 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -30,14 +30,6 @@ class CurrencyRepository implements CurrencyRepositoryInterface /** @var User */ private $user; - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - /** * @param TransactionCurrency $currency * @@ -186,6 +178,14 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $preferred; } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param array $data * diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 12ce898ff1..31ac868afa 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -43,14 +43,6 @@ class JournalRepository implements JournalRepositoryInterface /** @var array */ private $validMetaFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes']; - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - /** * @param TransactionJournal $journal * @param TransactionType $type @@ -143,6 +135,14 @@ class JournalRepository implements JournalRepositoryInterface return TransactionType::orderBy('type', 'ASC')->get(); } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param array $data * diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 9f91816252..57bade58b5 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -34,14 +34,6 @@ class TagRepository implements TagRepositoryInterface /** @var User */ private $user; - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - /** * * @param TransactionJournal $journal @@ -88,6 +80,25 @@ class TagRepository implements TagRepositoryInterface return true; } + /** + * @param Tag $tag + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAllAssetAccounts()->setTag($tag); + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); + + return $sum; + } + /** * @param int $tagId * @@ -167,6 +178,33 @@ class TagRepository implements TagRepositoryInterface return new Carbon; } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * @param Tag $tag + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAllAssetAccounts()->setTag($tag); + $set = $collector->getJournals(); + $sum = strval($set->sum('transaction_amount')); + + return $sum; + } + /** * @param array $data * @@ -383,42 +421,4 @@ class TagRepository implements TagRepositoryInterface return false; } - - /** - * @param Tag $tag - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string - { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setUser($this->user); - $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAllAssetAccounts()->setTag($tag); - $set = $collector->getJournals(); - $sum = strval($set->sum('transaction_amount')); - - return $sum; - } - - /** - * @param Tag $tag - * @param Carbon $start - * @param Carbon $end - * - * @return string - */ - public function spentInPeriod(Tag $tag, Carbon $start, Carbon $end): string - { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setUser($this->user); - $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAllAssetAccounts()->setTag($tag); - $set = $collector->getJournals(); - $sum = strval($set->sum('transaction_amount')); - - return $sum; - } } diff --git a/app/Rules/Actions/SetBudget.php b/app/Rules/Actions/SetBudget.php index e9f8c1fae9..7797501e2b 100644 --- a/app/Rules/Actions/SetBudget.php +++ b/app/Rules/Actions/SetBudget.php @@ -52,9 +52,9 @@ class SetBudget implements ActionInterface /** @var BudgetRepositoryInterface $repository */ $repository = app(BudgetRepositoryInterface::class); $repository->setUser($journal->user); - $search = $this->action->action_value; - $budgets = $repository->getActiveBudgets(); - $budget = $budgets->filter( + $search = $this->action->action_value; + $budgets = $repository->getActiveBudgets(); + $budget = $budgets->filter( function (Budget $current) use ($search) { return $current->name == $search; } diff --git a/app/Rules/Actions/SetDestinationAccount.php b/app/Rules/Actions/SetDestinationAccount.php index 25677e546b..33ca3d27c3 100644 --- a/app/Rules/Actions/SetDestinationAccount.php +++ b/app/Rules/Actions/SetDestinationAccount.php @@ -62,7 +62,7 @@ class SetDestinationAccount implements ActionInterface $this->journal = $journal; $this->repository = app(AccountRepositoryInterface::class); $this->repository->setUser($journal->user); - $count = $journal->transactions()->count(); + $count = $journal->transactions()->count(); if ($count > 2) { Log::error(sprintf('Cannot change destination account of journal #%d because it is a split journal.', $journal->id)); diff --git a/app/Rules/Actions/SetSourceAccount.php b/app/Rules/Actions/SetSourceAccount.php index fc8067856c..07f0125d4a 100644 --- a/app/Rules/Actions/SetSourceAccount.php +++ b/app/Rules/Actions/SetSourceAccount.php @@ -62,7 +62,7 @@ class SetSourceAccount implements ActionInterface $this->journal = $journal; $this->repository = app(AccountRepositoryInterface::class); $this->repository->setUser($journal->user); - $count = $journal->transactions()->count(); + $count = $journal->transactions()->count(); if ($count > 2) { Log::error(sprintf('Cannot change source account of journal #%d because it is a split journal.', $journal->id)); diff --git a/app/Rules/Triggers/AbstractTrigger.php b/app/Rules/Triggers/AbstractTrigger.php index 0fd9a3bfd8..d38e1dcd9f 100644 --- a/app/Rules/Triggers/AbstractTrigger.php +++ b/app/Rules/Triggers/AbstractTrigger.php @@ -75,7 +75,6 @@ class AbstractTrigger } - /** * @param RuleTrigger $trigger * @param TransactionJournal $journal diff --git a/app/Support/Search/SearchInterface.php b/app/Support/Search/SearchInterface.php index ed9c3c6c88..918f3caee3 100644 --- a/app/Support/Search/SearchInterface.php +++ b/app/Support/Search/SearchInterface.php @@ -30,11 +30,6 @@ interface SearchInterface */ public function searchAccounts(array $words): Collection; - /** - * @param User $user - */ - public function setUser(User $user); - /** * @param array $words * @@ -63,4 +58,9 @@ interface SearchInterface * @return Collection */ public function searchTransactions(array $words): Collection; + + /** + * @param User $user + */ + public function setUser(User $user); } From 65a899bf253ce04b4af6ffef43237f1ae20071ba Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 17 Feb 2017 20:14:22 +0100 Subject: [PATCH 002/244] Clean up session code --- app/Handlers/Events/UserEventHandler.php | 4 +- app/Http/Controllers/AccountController.php | 44 ++++++++------- app/Http/Controllers/AttachmentController.php | 25 +++++---- .../Controllers/Auth/TwoFactorController.php | 4 +- app/Http/Controllers/BillController.php | 55 ++++++++++--------- app/Http/Controllers/BudgetController.php | 45 ++++++++------- app/Http/Controllers/CategoryController.php | 40 ++++++++------ app/Http/Controllers/CurrencyController.php | 53 +++++++++--------- .../Transaction/SingleController.php | 2 +- .../RedirectIfTwoFactorAuthenticated.php | 2 +- .../RuleGroup/RuleGroupRepository.php | 16 +++--- 11 files changed, 155 insertions(+), 135 deletions(-) diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 5144843bf7..5f86d60a57 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -62,8 +62,8 @@ class UserEventHandler public function logoutUser(): bool { // dump stuff from the session: - Session::forget('twofactor-authenticated'); - Session::forget('twofactor-authenticated-date'); + Session::forget('twoFactorAuthenticated'); + Session::forget('twoFactorAuthenticatedDate'); return true; } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 47febc0f84..2c12693bcd 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -31,7 +31,6 @@ use Illuminate\Support\Collection; use Log; use Navigation; use Preferences; -use Session; use Steam; use View; @@ -61,11 +60,12 @@ class AccountController extends Controller } /** - * @param string $what + * @param Request $request + * @param string $what * - * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory|View + * @return View */ - public function create(string $what = 'asset') + public function create(Request $request, string $what = 'asset') { /** @var CurrencyRepositoryInterface $repository */ $repository = app(CurrencyRepositoryInterface::class); @@ -80,27 +80,28 @@ class AccountController extends Controller // pre fill some data - Session::flash('preFilled', ['currency_id' => $defaultCurrency->id,]); + $request->session()->flash('preFilled', ['currency_id' => $defaultCurrency->id,]); // put previous url in session if not redirect from store (not "create another"). if (session('accounts.create.fromStore') !== true) { $this->rememberPreviousUri('accounts.create.uri'); } - Session::forget('accounts.create.fromStore'); - Session::flash('gaEventCategory', 'accounts'); - Session::flash('gaEventAction', 'create-' . $what); + $request->session()->forget('accounts.create.fromStore'); + $request->session()->flash('gaEventCategory', 'accounts'); + $request->session()->flash('gaEventAction', 'create-' . $what); return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'currencies', 'roles')); } /** + * @param Request $request * @param AccountRepositoryInterface $repository * @param Account $account * * @return View */ - public function delete(AccountRepositoryInterface $repository, Account $account) + public function delete(Request $request, AccountRepositoryInterface $repository, Account $account) { $typeName = config('firefly.shortNamesByFullName.' . $account->accountType->type); $subTitle = trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]); @@ -109,8 +110,8 @@ class AccountController extends Controller // put previous url in session $this->rememberPreviousUri('accounts.delete.uri'); - Session::flash('gaEventCategory', 'accounts'); - Session::flash('gaEventAction', 'delete-' . $typeName); + $request->session()->flash('gaEventCategory', 'accounts'); + $request->session()->flash('gaEventAction', 'delete-' . $typeName); return view('accounts.delete', compact('account', 'subTitle', 'accountList')); } @@ -131,18 +132,19 @@ class AccountController extends Controller $repository->destroy($account, $moveTo); - Session::flash('success', strval(trans('firefly.' . $typeName . '_deleted', ['name' => $name]))); + $request->session()->flash('success', strval(trans('firefly.' . $typeName . '_deleted', ['name' => $name]))); Preferences::mark(); return redirect($this->getPreviousUri('accounts.delete.uri')); } /** + * @param Request $request * @param Account $account * * @return View */ - public function edit(Account $account) + public function edit(Request $request, Account $account) { $what = config('firefly.shortNamesByFullName')[$account->accountType->type]; @@ -161,7 +163,7 @@ class AccountController extends Controller if (session('accounts.edit.fromUpdate') !== true) { $this->rememberPreviousUri('accounts.edit.uri'); } - Session::forget('accounts.edit.fromUpdate'); + $request->session()->forget('accounts.edit.fromUpdate'); // pre fill some useful values. @@ -182,9 +184,9 @@ class AccountController extends Controller 'virtualBalance' => $account->virtual_balance, 'currency_id' => $account->getMeta('currency_id'), ]; - Session::flash('preFilled', $preFilled); - Session::flash('gaEventCategory', 'accounts'); - Session::flash('gaEventAction', 'edit-' . $what); + $request->session()->flash('preFilled', $preFilled); + $request->session()->flash('gaEventCategory', 'accounts'); + $request->session()->flash('gaEventAction', 'edit-' . $what); return view('accounts.edit', compact('currencies', 'account', 'subTitle', 'subTitleIcon', 'openingBalance', 'what', 'roles')); } @@ -334,7 +336,7 @@ class AccountController extends Controller $data = $request->getAccountData(); $account = $repository->store($data); - Session::flash('success', strval(trans('firefly.stored_new_account', ['name' => $account->name]))); + $request->session()->flash('success', strval(trans('firefly.stored_new_account', ['name' => $account->name]))); Preferences::mark(); // update preferences if necessary: @@ -346,7 +348,7 @@ class AccountController extends Controller if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: - Session::put('accounts.create.fromStore', true); + $request->session()->put('accounts.create.fromStore', true); return redirect(route('accounts.create', [$request->input('what')]))->withInput(); } @@ -367,12 +369,12 @@ class AccountController extends Controller $data = $request->getAccountData(); $repository->update($account, $data); - Session::flash('success', strval(trans('firefly.updated_account', ['name' => $account->name]))); + $request->session()->flash('success', strval(trans('firefly.updated_account', ['name' => $account->name]))); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: - Session::put('accounts.edit.fromUpdate', true); + $request->session()->put('accounts.edit.fromUpdate', true); return redirect(route('accounts.edit', [$account->id]))->withInput(['return_to_edit' => 1]); } diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index a6532309f1..51b24e4d93 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -18,10 +18,10 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Requests\AttachmentFormRequest; use FireflyIII\Models\Attachment; use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; +use Illuminate\Http\Request; use Illuminate\Http\Response as LaravelResponse; use Preferences; use Response; -use Session; use View; /** @@ -53,35 +53,37 @@ class AttachmentController extends Controller } /** + * @param Request $request * @param Attachment $attachment * * @return View */ - public function delete(Attachment $attachment) + public function delete(Request $request, Attachment $attachment) { $subTitle = trans('firefly.delete_attachment', ['name' => $attachment->filename]); // put previous url in session $this->rememberPreviousUri('attachments.delete.uri'); - Session::flash('gaEventCategory', 'attachments'); - Session::flash('gaEventAction', 'delete-attachment'); + $request->session()->flash('gaEventCategory', 'attachments'); + $request->session()->flash('gaEventAction', 'delete-attachment'); return view('attachments.delete', compact('attachment', 'subTitle')); } /** + * @param Request $request * @param AttachmentRepositoryInterface $repository * @param Attachment $attachment * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(AttachmentRepositoryInterface $repository, Attachment $attachment) + public function destroy(Request $request, AttachmentRepositoryInterface $repository, Attachment $attachment) { $name = $attachment->filename; $repository->destroy($attachment); - Session::flash('success', strval(trans('firefly.attachment_deleted', ['name' => $name]))); + $request->session()->flash('success', strval(trans('firefly.attachment_deleted', ['name' => $name]))); Preferences::mark(); return redirect($this->getPreviousUri('attachments.delete.uri')); @@ -119,11 +121,12 @@ class AttachmentController extends Controller } /** + * @param Request $request * @param Attachment $attachment * * @return View */ - public function edit(Attachment $attachment) + public function edit(Request $request, Attachment $attachment) { $subTitleIcon = 'fa-pencil'; $subTitle = trans('firefly.edit_attachment', ['name' => $attachment->filename]); @@ -132,7 +135,7 @@ class AttachmentController extends Controller if (session('attachments.edit.fromUpdate') !== true) { $this->rememberPreviousUri('attachments.edit.uri'); } - Session::forget('attachments.edit.fromUpdate'); + $request->session()->forget('attachments.edit.fromUpdate'); return view('attachments.edit', compact('attachment', 'subTitleIcon', 'subTitle')); } @@ -169,12 +172,12 @@ class AttachmentController extends Controller $data = $request->getAttachmentData(); $repository->update($attachment, $data); - Session::flash('success', strval(trans('firefly.attachment_updated', ['name' => $attachment->filename]))); + $request->session()->flash('success', strval(trans('firefly.attachment_updated', ['name' => $attachment->filename]))); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: - Session::put('attachments.edit.fromUpdate', true); + $request->session()->put('attachments.edit.fromUpdate', true); return redirect(route('attachments.edit', [$attachment->id]))->withInput(['return_to_edit' => 1]); } diff --git a/app/Http/Controllers/Auth/TwoFactorController.php b/app/Http/Controllers/Auth/TwoFactorController.php index bf3b425161..66df10f66e 100644 --- a/app/Http/Controllers/Auth/TwoFactorController.php +++ b/app/Http/Controllers/Auth/TwoFactorController.php @@ -86,8 +86,8 @@ class TwoFactorController extends Controller */ public function postIndex(TokenFormRequest $request) { - Session::put('twofactor-authenticated', true); - Session::put('twofactor-authenticated-date', new Carbon); + Session::put('twoFactorAuthenticated', true); + Session::put('twoFactorAuthenticatedDate', new Carbon); return redirect(route('home')); } diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php index ae95ba23ef..b05ede6e52 100644 --- a/app/Http/Controllers/BillController.php +++ b/app/Http/Controllers/BillController.php @@ -22,7 +22,6 @@ use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Preferences; -use Session; use URL; use View; @@ -53,9 +52,11 @@ class BillController extends Controller } /** + * @param Request $request + * * @return View */ - public function create() + public function create(Request $request) { $periods = []; foreach (config('firefly.bill_periods') as $current) { @@ -68,52 +69,55 @@ class BillController extends Controller if (session('bills.create.fromStore') !== true) { $this->rememberPreviousUri('bills.create.uri'); } - Session::forget('bills.create.fromStore'); - Session::flash('gaEventCategory', 'bills'); - Session::flash('gaEventAction', 'create'); + $request->session()->forget('bills.create.fromStore'); + $request->session()->flash('gaEventCategory', 'bills'); + $request->session()->flash('gaEventAction', 'create'); return view('bills.create', compact('periods', 'subTitle')); } /** - * @param Bill $bill + * @param Request $request + * @param Bill $bill * * @return View */ - public function delete(Bill $bill) + public function delete(Request $request, Bill $bill) { // put previous url in session $this->rememberPreviousUri('bills.delete.uri'); - Session::flash('gaEventCategory', 'bills'); - Session::flash('gaEventAction', 'delete'); + $request->session()->flash('gaEventCategory', 'bills'); + $request->session()->flash('gaEventAction', 'delete'); $subTitle = trans('firefly.delete_bill', ['name' => $bill->name]); return view('bills.delete', compact('bill', 'subTitle')); } /** + * @param Request $request * @param BillRepositoryInterface $repository * @param Bill $bill * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(BillRepositoryInterface $repository, Bill $bill) + public function destroy(Request $request, BillRepositoryInterface $repository, Bill $bill) { $name = $bill->name; $repository->destroy($bill); - Session::flash('success', strval(trans('firefly.deleted_bill', ['name' => $name]))); + $request->session()->flash('success', strval(trans('firefly.deleted_bill', ['name' => $name]))); Preferences::mark(); return redirect($this->getPreviousUri('bills.delete.uri')); } /** - * @param Bill $bill + * @param Request $request + * @param Bill $bill * * @return View */ - public function edit(Bill $bill) + public function edit(Request $request, Bill $bill) { $periods = []; foreach (config('firefly.bill_periods') as $current) { @@ -125,9 +129,9 @@ class BillController extends Controller if (session('bills.edit.fromUpdate') !== true) { $this->rememberPreviousUri('bills.edit.uri'); } - Session::forget('bills.edit.fromUpdate'); - Session::flash('gaEventCategory', 'bills'); - Session::flash('gaEventAction', 'edit'); + $request->session()->forget('bills.edit.fromUpdate'); + $request->session()->flash('gaEventCategory', 'bills'); + $request->session()->flash('gaEventAction', 'edit'); return view('bills.edit', compact('subTitle', 'periods', 'bill')); } @@ -163,15 +167,16 @@ class BillController extends Controller } /** + * @param Request $request * @param BillRepositoryInterface $repository * @param Bill $bill * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function rescan(BillRepositoryInterface $repository, Bill $bill) + public function rescan(Request $request, BillRepositoryInterface $repository, Bill $bill) { if (intval($bill->active) == 0) { - Session::flash('warning', strval(trans('firefly.cannot_scan_inactive_bill'))); + $request->session()->flash('warning', strval(trans('firefly.cannot_scan_inactive_bill'))); return redirect(URL::previous()); } @@ -183,7 +188,7 @@ class BillController extends Controller } - Session::flash('success', strval(trans('firefly.rescanned_bill'))); + $request->session()->flash('success', strval(trans('firefly.rescanned_bill'))); Preferences::mark(); return redirect(URL::previous()); @@ -231,12 +236,12 @@ class BillController extends Controller { $billData = $request->getBillData(); $bill = $repository->store($billData); - Session::flash('success', strval(trans('firefly.stored_new_bill', ['name' => e($bill->name)]))); + $request->session()->flash('success', strval(trans('firefly.stored_new_bill', ['name' => e($bill->name)]))); Preferences::mark(); if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: - Session::put('bills.create.fromStore', true); + $request->session()->put('bills.create.fromStore', true); return redirect(route('bills.create'))->withInput(); } @@ -258,12 +263,12 @@ class BillController extends Controller $billData = $request->getBillData(); $bill = $repository->update($bill, $billData); - Session::flash('success', strval(trans('firefly.updated_bill', ['name' => e($bill->name)]))); + $request->session()->flash('success', strval(trans('firefly.updated_bill', ['name' => e($bill->name)]))); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: - Session::put('bills.edit.fromUpdate', true); + $request->session()->put('bills.edit.fromUpdate', true); return redirect(route('bills.edit', [$bill->id]))->withInput(['return_to_edit' => 1]); } diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index f21bc904fa..c7c73030f7 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -29,7 +29,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Preferences; use Response; -use Session; use View; /** @@ -87,17 +86,19 @@ class BudgetController extends Controller } /** + * @param Request $request + * * @return View */ - public function create() + public function create(Request $request) { // put previous url in session if not redirect from store (not "create another"). if (session('budgets.create.fromStore') !== true) { $this->rememberPreviousUri('budgets.create.uri'); } - Session::forget('budgets.create.fromStore'); - Session::flash('gaEventCategory', 'budgets'); - Session::flash('gaEventAction', 'create'); + $request->session()->forget('budgets.create.fromStore'); + $request->session()->flash('gaEventCategory', 'budgets'); + $request->session()->flash('gaEventAction', 'create'); $subTitle = (string)trans('firefly.create_new_budget'); return view('budgets.create', compact('subTitle')); @@ -108,40 +109,42 @@ class BudgetController extends Controller * * @return View */ - public function delete(Budget $budget) + public function delete(Request $request, Budget $budget) { $subTitle = trans('firefly.delete_budget', ['name' => $budget->name]); // put previous url in session $this->rememberPreviousUri('budgets.delete.uri'); - Session::flash('gaEventCategory', 'budgets'); - Session::flash('gaEventAction', 'delete'); + $request->session()->flash('gaEventCategory', 'budgets'); + $request->session()->flash('gaEventAction', 'delete'); return view('budgets.delete', compact('budget', 'subTitle')); } /** - * @param Budget $budget + * @param Request $request + * @param Budget $budget * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(Budget $budget) + public function destroy(Request $request, Budget $budget) { $name = $budget->name; $this->repository->destroy($budget); - Session::flash('success', strval(trans('firefly.deleted_budget', ['name' => e($name)]))); + $request->session()->flash('success', strval(trans('firefly.deleted_budget', ['name' => e($name)]))); Preferences::mark(); return redirect($this->getPreviousUri('budgets.delete.uri')); } /** - * @param Budget $budget + * @param Request $request + * @param Budget $budget * * @return View */ - public function edit(Budget $budget) + public function edit(Request $request, Budget $budget) { $subTitle = trans('firefly.edit_budget', ['name' => $budget->name]); @@ -149,9 +152,9 @@ class BudgetController extends Controller if (session('budgets.edit.fromUpdate') !== true) { $this->rememberPreviousUri('budgets.edit.uri'); } - Session::forget('budgets.edit.fromUpdate'); - Session::flash('gaEventCategory', 'budgets'); - Session::flash('gaEventAction', 'edit'); + $request->session()->forget('budgets.edit.fromUpdate'); + $request->session()->flash('gaEventCategory', 'budgets'); + $request->session()->flash('gaEventAction', 'edit'); return view('budgets.edit', compact('budget', 'subTitle')); @@ -305,12 +308,12 @@ class BudgetController extends Controller $data = $request->getBudgetData(); $budget = $this->repository->store($data); - Session::flash('success', strval(trans('firefly.stored_new_budget', ['name' => e($budget->name)]))); + $request->session()->flash('success', strval(trans('firefly.stored_new_budget', ['name' => e($budget->name)]))); Preferences::mark(); if (intval($request->get('create_another')) === 1) { // set value so create routine will not overwrite URL: - Session::put('budgets.create.fromStore', true); + $request->session()->put('budgets.create.fromStore', true); return redirect(route('budgets.create'))->withInput(); } @@ -329,12 +332,12 @@ class BudgetController extends Controller $data = $request->getBudgetData(); $this->repository->update($budget, $data); - Session::flash('success', strval(trans('firefly.updated_budget', ['name' => e($budget->name)]))); + $request->session()->flash('success', strval(trans('firefly.updated_budget', ['name' => e($budget->name)]))); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: - Session::put('budgets.edit.fromUpdate', true); + $request->session()->put('budgets.edit.fromUpdate', true); return redirect(route('budgets.edit', [$budget->id]))->withInput(['return_to_edit' => 1]); } diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 2224cb3512..24a391ec4b 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -25,7 +25,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Navigation; use Preferences; -use Session; use View; /** @@ -55,63 +54,68 @@ class CategoryController extends Controller } /** + * @param Request $request + * * @return View */ - public function create() + public function create(Request $request) { if (session('categories.create.fromStore') !== true) { $this->rememberPreviousUri('categories.create.uri'); } - Session::forget('categories.create.fromStore'); - Session::flash('gaEventCategory', 'categories'); - Session::flash('gaEventAction', 'create'); + $request->session()->forget('categories.create.fromStore'); + $request->session()->flash('gaEventCategory', 'categories'); + $request->session()->flash('gaEventAction', 'create'); $subTitle = trans('firefly.create_new_category'); return view('categories.create', compact('subTitle')); } /** + * @param Request $request * @param Category $category * * @return View */ - public function delete(Category $category) + public function delete(Request $request, Category $category) { $subTitle = trans('firefly.delete_category', ['name' => $category->name]); // put previous url in session $this->rememberPreviousUri('categories.delete.uri'); - Session::flash('gaEventCategory', 'categories'); - Session::flash('gaEventAction', 'delete'); + $request->session()->flash('gaEventCategory', 'categories'); + $request->session()->flash('gaEventAction', 'delete'); return view('categories.delete', compact('category', 'subTitle')); } /** + * @param Request $request * @param CategoryRepositoryInterface $repository * @param Category $category * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(CategoryRepositoryInterface $repository, Category $category) + public function destroy(Request $request, CategoryRepositoryInterface $repository, Category $category) { $name = $category->name; $repository->destroy($category); - Session::flash('success', strval(trans('firefly.deleted_category', ['name' => e($name)]))); + $request->session()->flash('success', strval(trans('firefly.deleted_category', ['name' => e($name)]))); Preferences::mark(); return redirect($this->getPreviousUri('categories.delete.uri')); } /** + * @param Request $request * @param Category $category * * @return View */ - public function edit(Category $category) + public function edit(Request $request, Category $category) { $subTitle = trans('firefly.edit_category', ['name' => $category->name]); @@ -119,9 +123,9 @@ class CategoryController extends Controller if (session('categories.edit.fromUpdate') !== true) { $this->rememberPreviousUri('categories.edit.uri'); } - Session::forget('categories.edit.fromUpdate'); - Session::flash('gaEventCategory', 'categories'); - Session::flash('gaEventAction', 'edit'); + $request->session()->forget('categories.edit.fromUpdate'); + $request->session()->flash('gaEventCategory', 'categories'); + $request->session()->flash('gaEventAction', 'edit'); return view('categories.edit', compact('category', 'subTitle')); @@ -269,11 +273,11 @@ class CategoryController extends Controller $data = $request->getCategoryData(); $category = $repository->store($data); - Session::flash('success', strval(trans('firefly.stored_category', ['name' => e($category->name)]))); + $request->session()->flash('success', strval(trans('firefly.stored_category', ['name' => e($category->name)]))); Preferences::mark(); if (intval($request->get('create_another')) === 1) { - Session::put('categories.create.fromStore', true); + $request->session()->put('categories.create.fromStore', true); return redirect(route('categories.create'))->withInput(); } @@ -294,11 +298,11 @@ class CategoryController extends Controller $data = $request->getCategoryData(); $repository->update($category, $data); - Session::flash('success', strval(trans('firefly.updated_category', ['name' => e($category->name)]))); + $request->session()->flash('success', strval(trans('firefly.updated_category', ['name' => e($category->name)]))); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { - Session::put('categories.edit.fromUpdate', true); + $request->session()->put('categories.edit.fromUpdate', true); return redirect(route('categories.edit', [$category->id])); } diff --git a/app/Http/Controllers/CurrencyController.php b/app/Http/Controllers/CurrencyController.php index 01cb6abd3e..d38e964c62 100644 --- a/app/Http/Controllers/CurrencyController.php +++ b/app/Http/Controllers/CurrencyController.php @@ -17,9 +17,9 @@ use Cache; use FireflyIII\Http\Requests\CurrencyFormRequest; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use Illuminate\Http\Request; use Log; use Preferences; -use Session; use View; /** @@ -52,7 +52,7 @@ class CurrencyController extends Controller /** * @return View */ - public function create() + public function create(Request $request) { $subTitleIcon = 'fa-plus'; $subTitle = trans('firefly.create_currency'); @@ -61,25 +61,26 @@ class CurrencyController extends Controller if (session('currencies.create.fromStore') !== true) { $this->rememberPreviousUri('currencies.create.uri'); } - Session::forget('currencies.create.fromStore'); - Session::flash('gaEventCategory', 'currency'); - Session::flash('gaEventAction', 'create'); + $request->session()->forget('currencies.create.fromStore'); + $request->session()->flash('gaEventCategory', 'currency'); + $request->session()->flash('gaEventAction', 'create'); return view('currencies.create', compact('subTitleIcon', 'subTitle')); } /** + * @param Request $request * @param TransactionCurrency $currency * - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function defaultCurrency(TransactionCurrency $currency) + public function defaultCurrency(Request $request, TransactionCurrency $currency) { Preferences::set('currencyPreference', $currency->code); Preferences::mark(); - Session::flash('success', trans('firefly.new_default_currency', ['name' => $currency->name])); + $request->session()->flash('success', trans('firefly.new_default_currency', ['name' => $currency->name])); Cache::forget('FFCURRENCYSYMBOL'); Cache::forget('FFCURRENCYCODE'); @@ -94,10 +95,10 @@ class CurrencyController extends Controller * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View */ - public function delete(CurrencyRepositoryInterface $repository, TransactionCurrency $currency) + public function delete(Request $request, CurrencyRepositoryInterface $repository, TransactionCurrency $currency) { if (!$repository->canDeleteCurrency($currency)) { - Session::flash('error', trans('firefly.cannot_delete_currency', ['name' => $currency->name])); + $request->session()->flash('error', trans('firefly.cannot_delete_currency', ['name' => $currency->name])); return redirect(route('currencies.index')); } @@ -105,8 +106,8 @@ class CurrencyController extends Controller // put previous url in session $this->rememberPreviousUri('currencies.delete.uri'); - Session::flash('gaEventCategory', 'currency'); - Session::flash('gaEventAction', 'delete'); + $request->session()->flash('gaEventCategory', 'currency'); + $request->session()->flash('gaEventAction', 'delete'); $subTitle = trans('form.delete_currency', ['name' => $currency->name]); @@ -119,26 +120,27 @@ class CurrencyController extends Controller * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function destroy(CurrencyRepositoryInterface $repository, TransactionCurrency $currency) + public function destroy(Request $request, CurrencyRepositoryInterface $repository, TransactionCurrency $currency) { if (!$repository->canDeleteCurrency($currency)) { - Session::flash('error', trans('firefly.cannot_delete_currency', ['name' => $currency->name])); + $request->session()->flash('error', trans('firefly.cannot_delete_currency', ['name' => $currency->name])); return redirect(route('currencies.index')); } $repository->destroy($currency); - Session::flash('success', trans('firefly.deleted_currency', ['name' => $currency->name])); + $request->session()->flash('success', trans('firefly.deleted_currency', ['name' => $currency->name])); return redirect($this->getPreviousUri('currencies.delete.uri')); } /** + * @param Request $request * @param TransactionCurrency $currency * * @return View */ - public function edit(TransactionCurrency $currency) + public function edit(Request $request, TransactionCurrency $currency) { $subTitleIcon = 'fa-pencil'; $subTitle = trans('breadcrumbs.edit_currency', ['name' => $currency->name]); @@ -148,27 +150,28 @@ class CurrencyController extends Controller if (session('currencies.edit.fromUpdate') !== true) { $this->rememberPreviousUri('currencies.edit.uri'); } - Session::forget('currencies.edit.fromUpdate'); - Session::flash('gaEventCategory', 'currency'); - Session::flash('gaEventAction', 'edit'); + $request->session()->forget('currencies.edit.fromUpdate'); + $request->session()->flash('gaEventCategory', 'currency'); + $request->session()->flash('gaEventAction', 'edit'); return view('currencies.edit', compact('currency', 'subTitle', 'subTitleIcon')); } /** + * @param Request $request * @param CurrencyRepositoryInterface $repository * * @return View */ - public function index(CurrencyRepositoryInterface $repository) + public function index(Request $request, CurrencyRepositoryInterface $repository) { $currencies = $repository->get(); $defaultCurrency = $repository->getCurrencyByPreference(Preferences::get('currencyPreference', config('firefly.default_currency', 'EUR'))); if (!auth()->user()->hasRole('owner')) { - Session::flash('info', trans('firefly.ask_site_owner', ['owner' => env('SITE_OWNER')])); + $request->session()->flash('info', trans('firefly.ask_site_owner', ['owner' => env('SITE_OWNER')])); } @@ -192,10 +195,10 @@ class CurrencyController extends Controller $data = $request->getCurrencyData(); $currency = $repository->store($data); - Session::flash('success', trans('firefly.created_currency', ['name' => $currency->name])); + $request->session()->flash('success', trans('firefly.created_currency', ['name' => $currency->name])); if (intval($request->get('create_another')) === 1) { - Session::put('currencies.create.fromStore', true); + $request->session()->put('currencies.create.fromStore', true); return redirect(route('currencies.create'))->withInput(); } @@ -216,12 +219,12 @@ class CurrencyController extends Controller if (auth()->user()->hasRole('owner')) { $currency = $repository->update($currency, $data); } - Session::flash('success', trans('firefly.updated_currency', ['name' => $currency->name])); + $request->session()->flash('success', trans('firefly.updated_currency', ['name' => $currency->name])); Preferences::mark(); if (intval($request->get('return_to_edit')) === 1) { - Session::put('currencies.edit.fromUpdate', true); + $request->session()->put('currencies.edit.fromUpdate', true); return redirect(route('currencies.edit', [$currency->id])); } diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index 4a70121724..830c32eb78 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -190,7 +190,7 @@ class SingleController extends Controller if ($this->isOpeningBalance($transactionJournal)) { return $this->redirectToAccount($transactionJournal); } - $type = TransactionJournal::transactionTypeStr($transactionJournal); + $type = TransactionJournal::transactionTypeStr($transactionJournal); Session::flash('success', strval(trans('firefly.deleted_' . strtolower($type), ['description' => e($transactionJournal->description)]))); $repository->delete($transactionJournal); diff --git a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php index 43cc865dc7..0b2894ef19 100644 --- a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php +++ b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php @@ -40,7 +40,7 @@ class RedirectIfTwoFactorAuthenticated $is2faEnabled = Preferences::get('twoFactorAuthEnabled', false)->data; $has2faSecret = !is_null(Preferences::get('twoFactorAuthSecret')); - $is2faAuthed = Session::get('twofactor-authenticated'); + $is2faAuthed = Session::get('twoFactorAuthenticated'); if ($is2faEnabled && $has2faSecret && $is2faAuthed) { return redirect('/'); } diff --git a/app/Repositories/RuleGroup/RuleGroupRepository.php b/app/Repositories/RuleGroup/RuleGroupRepository.php index e413987334..404e2f9d2f 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepository.php +++ b/app/Repositories/RuleGroup/RuleGroupRepository.php @@ -30,14 +30,6 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface /** @var User */ private $user; - /** - * @param User $user - */ - public function setUser(User $user) - { - $this->user = $user; - } - /** * @return int */ @@ -228,6 +220,14 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * @param array $data * From 48c26c58372b952d5a0839872dc622c551094577 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 17 Feb 2017 20:14:38 +0100 Subject: [PATCH 003/244] Update test coverage --- tests/Feature/Controllers/AccountControllerTest.php | 1 + tests/Feature/Controllers/Admin/ConfigurationControllerTest.php | 1 + tests/Feature/Controllers/Admin/UserControllerTest.php | 1 + tests/Feature/Controllers/AttachmentControllerTest.php | 1 + tests/Feature/Controllers/BillControllerTest.php | 1 + tests/Feature/Controllers/BudgetControllerTest.php | 1 + tests/Feature/Controllers/CategoryControllerTest.php | 1 + tests/Feature/Controllers/Chart/AccountControllerTest.php | 1 + tests/Feature/Controllers/Chart/BillControllerTest.php | 1 + tests/Feature/Controllers/Chart/BudgetControllerTest.php | 1 + tests/Feature/Controllers/Chart/CategoryControllerTest.php | 1 + .../Feature/Controllers/Chart/CategoryReportControllerTest.php | 1 + tests/Feature/Controllers/Chart/PiggyBankControllerTest.php | 1 + tests/Feature/Controllers/Chart/ReportControllerTest.php | 1 + tests/Feature/Controllers/CurrencyControllerTest.php | 1 + tests/Feature/Controllers/ExportControllerTest.php | 1 + tests/Feature/Controllers/HomeControllerTest.php | 1 + tests/Feature/Controllers/ImportControllerTest.php | 1 + tests/Feature/Controllers/NewUserControllerTest.php | 1 + tests/Feature/Controllers/PiggyBankControllerTest.php | 2 ++ tests/Feature/Controllers/PreferencesControllerTest.php | 1 + tests/Feature/Controllers/ProfileControllerTest.php | 1 + tests/Feature/Controllers/ReportControllerTest.php | 1 + tests/Feature/Controllers/RuleControllerTest.php | 1 + tests/Feature/Controllers/RuleGroupControllerTest.php | 1 + tests/Feature/Controllers/TagControllerTest.php | 1 + tests/Feature/Controllers/Transaction/ConvertControllerTest.php | 1 + tests/Feature/Controllers/Transaction/MassControllerTest.php | 1 + tests/Feature/Controllers/Transaction/SingleControllerTest.php | 1 + tests/Feature/Controllers/Transaction/SplitControllerTest.php | 1 + tests/Feature/Controllers/TransactionControllerTest.php | 1 + 31 files changed, 32 insertions(+) diff --git a/tests/Feature/Controllers/AccountControllerTest.php b/tests/Feature/Controllers/AccountControllerTest.php index b283f3e5d6..2f7ca99259 100644 --- a/tests/Feature/Controllers/AccountControllerTest.php +++ b/tests/Feature/Controllers/AccountControllerTest.php @@ -77,6 +77,7 @@ class AccountControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\AccountController::index + * @covers \FireflyIII\Http\Controllers\AccountController::__construct * @covers \FireflyIII\Http\Controllers\AccountController::isInArray * @dataProvider dateRangeProvider * diff --git a/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php b/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php index 9cc639b5ad..5d7c0e9b4e 100644 --- a/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php +++ b/tests/Feature/Controllers/Admin/ConfigurationControllerTest.php @@ -21,6 +21,7 @@ class ConfigurationControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Admin\ConfigurationController::index + * @covers \FireflyIII\Http\Controllers\Admin\ConfigurationController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/Admin/UserControllerTest.php b/tests/Feature/Controllers/Admin/UserControllerTest.php index 1fb73886d1..0d6701be4a 100644 --- a/tests/Feature/Controllers/Admin/UserControllerTest.php +++ b/tests/Feature/Controllers/Admin/UserControllerTest.php @@ -30,6 +30,7 @@ class UserControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Admin\UserController::index + * @covers \FireflyIII\Http\Controllers\Admin\UserController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/AttachmentControllerTest.php b/tests/Feature/Controllers/AttachmentControllerTest.php index 9c47c9dadc..4608209dbc 100644 --- a/tests/Feature/Controllers/AttachmentControllerTest.php +++ b/tests/Feature/Controllers/AttachmentControllerTest.php @@ -74,6 +74,7 @@ class AttachmentControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\AttachmentController::preview + * @covers \FireflyIII\Http\Controllers\AttachmentController::__construct */ public function testPreview() { diff --git a/tests/Feature/Controllers/BillControllerTest.php b/tests/Feature/Controllers/BillControllerTest.php index 840fdfea50..f8d3c20d32 100644 --- a/tests/Feature/Controllers/BillControllerTest.php +++ b/tests/Feature/Controllers/BillControllerTest.php @@ -71,6 +71,7 @@ class BillControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\BillController::index + * @covers \FireflyIII\Http\Controllers\BillController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/BudgetControllerTest.php b/tests/Feature/Controllers/BudgetControllerTest.php index b10387cac6..47e03cc0a0 100644 --- a/tests/Feature/Controllers/BudgetControllerTest.php +++ b/tests/Feature/Controllers/BudgetControllerTest.php @@ -88,6 +88,7 @@ class BudgetControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\BudgetController::index + * @covers \FireflyIII\Http\Controllers\BudgetController::__construct * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/CategoryControllerTest.php b/tests/Feature/Controllers/CategoryControllerTest.php index c755e13d4a..2d4737df42 100644 --- a/tests/Feature/Controllers/CategoryControllerTest.php +++ b/tests/Feature/Controllers/CategoryControllerTest.php @@ -74,6 +74,7 @@ class CategoryControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\CategoryController::index + * @covers \FireflyIII\Http\Controllers\CategoryController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/Chart/AccountControllerTest.php b/tests/Feature/Controllers/Chart/AccountControllerTest.php index 9e4ebadd29..e94470dd4b 100644 --- a/tests/Feature/Controllers/Chart/AccountControllerTest.php +++ b/tests/Feature/Controllers/Chart/AccountControllerTest.php @@ -60,6 +60,7 @@ class AccountControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Chart\AccountController::frontpage + * @covers \FireflyIII\Http\Controllers\Chart\AccountController::__construct * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/Chart/BillControllerTest.php b/tests/Feature/Controllers/Chart/BillControllerTest.php index 3e590e8963..eb0f99733f 100644 --- a/tests/Feature/Controllers/Chart/BillControllerTest.php +++ b/tests/Feature/Controllers/Chart/BillControllerTest.php @@ -19,6 +19,7 @@ class BillControllerTest extends TestCase { /** * @covers \FireflyIII\Http\Controllers\Chart\BillController::frontpage + * @covers \FireflyIII\Http\Controllers\Chart\BillController::__construct * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/Chart/BudgetControllerTest.php b/tests/Feature/Controllers/Chart/BudgetControllerTest.php index 3f1c8461ae..713962639c 100644 --- a/tests/Feature/Controllers/Chart/BudgetControllerTest.php +++ b/tests/Feature/Controllers/Chart/BudgetControllerTest.php @@ -20,6 +20,7 @@ class BudgetControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Chart\BudgetController::budget + * @covers \FireflyIII\Http\Controllers\Chart\BudgetController::__construct * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/Chart/CategoryControllerTest.php b/tests/Feature/Controllers/Chart/CategoryControllerTest.php index 909acf3bbe..3b5a88f52a 100644 --- a/tests/Feature/Controllers/Chart/CategoryControllerTest.php +++ b/tests/Feature/Controllers/Chart/CategoryControllerTest.php @@ -23,6 +23,7 @@ class CategoryControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Chart\CategoryController::all + * @covers \FireflyIII\Http\Controllers\Chart\CategoryController::__construct * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/Chart/CategoryReportControllerTest.php b/tests/Feature/Controllers/Chart/CategoryReportControllerTest.php index 7a0b53b337..8626bfa981 100644 --- a/tests/Feature/Controllers/Chart/CategoryReportControllerTest.php +++ b/tests/Feature/Controllers/Chart/CategoryReportControllerTest.php @@ -19,6 +19,7 @@ class CategoryReportControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Chart\CategoryReportController::accountExpense + * @covers \FireflyIII\Http\Controllers\Chart\CategoryReportController::__construct */ public function testAccountExpense() { diff --git a/tests/Feature/Controllers/Chart/PiggyBankControllerTest.php b/tests/Feature/Controllers/Chart/PiggyBankControllerTest.php index 8d14ef24d3..68a2d0f887 100644 --- a/tests/Feature/Controllers/Chart/PiggyBankControllerTest.php +++ b/tests/Feature/Controllers/Chart/PiggyBankControllerTest.php @@ -18,6 +18,7 @@ class PiggyBankControllerTest extends TestCase { /** * @covers \FireflyIII\Http\Controllers\Chart\PiggyBankController::history + * @covers \FireflyIII\Http\Controllers\Chart\PiggyBankController::__construct */ public function testHistory() { diff --git a/tests/Feature/Controllers/Chart/ReportControllerTest.php b/tests/Feature/Controllers/Chart/ReportControllerTest.php index 8b556a6c2e..d42aa62364 100644 --- a/tests/Feature/Controllers/Chart/ReportControllerTest.php +++ b/tests/Feature/Controllers/Chart/ReportControllerTest.php @@ -19,6 +19,7 @@ class ReportControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Chart\ReportController::netWorth + * @covers \FireflyIII\Http\Controllers\Chart\ReportController::__construct */ public function testNetWorth() { diff --git a/tests/Feature/Controllers/CurrencyControllerTest.php b/tests/Feature/Controllers/CurrencyControllerTest.php index 5d8426ae80..f58d0a2ab0 100644 --- a/tests/Feature/Controllers/CurrencyControllerTest.php +++ b/tests/Feature/Controllers/CurrencyControllerTest.php @@ -82,6 +82,7 @@ class CurrencyControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\CurrencyController::index + * @covers \FireflyIII\Http\Controllers\CurrencyController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/ExportControllerTest.php b/tests/Feature/Controllers/ExportControllerTest.php index 2f9d131671..35ef4061de 100644 --- a/tests/Feature/Controllers/ExportControllerTest.php +++ b/tests/Feature/Controllers/ExportControllerTest.php @@ -48,6 +48,7 @@ class ExportControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\ExportController::index + * @covers \FireflyIII\Http\Controllers\ExportController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/HomeControllerTest.php b/tests/Feature/Controllers/HomeControllerTest.php index 32f0be8912..0b80182db1 100644 --- a/tests/Feature/Controllers/HomeControllerTest.php +++ b/tests/Feature/Controllers/HomeControllerTest.php @@ -56,6 +56,7 @@ class HomeControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\HomeController::index + * @covers \FireflyIII\Http\Controllers\HomeController::__construct * @covers \FireflyIII\Http\Controllers\Controller::__construct * @dataProvider dateRangeProvider * diff --git a/tests/Feature/Controllers/ImportControllerTest.php b/tests/Feature/Controllers/ImportControllerTest.php index c45758fe4e..bb6b2229e2 100644 --- a/tests/Feature/Controllers/ImportControllerTest.php +++ b/tests/Feature/Controllers/ImportControllerTest.php @@ -64,6 +64,7 @@ class ImportControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\ImportController::index + * @covers \FireflyIII\Http\Controllers\ImportController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/NewUserControllerTest.php b/tests/Feature/Controllers/NewUserControllerTest.php index 510f813268..042dd0587a 100644 --- a/tests/Feature/Controllers/NewUserControllerTest.php +++ b/tests/Feature/Controllers/NewUserControllerTest.php @@ -18,6 +18,7 @@ class NewUserControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\NewUserController::index + * @covers \FireflyIII\Http\Controllers\NewUserController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/PiggyBankControllerTest.php b/tests/Feature/Controllers/PiggyBankControllerTest.php index eeda5f5b4a..ef44d4b2a2 100644 --- a/tests/Feature/Controllers/PiggyBankControllerTest.php +++ b/tests/Feature/Controllers/PiggyBankControllerTest.php @@ -12,6 +12,7 @@ declare(strict_types = 1); namespace Tests\Feature\Controllers; use FireflyIII\Models\PiggyBank; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Tests\TestCase; class PiggyBankControllerTest extends TestCase @@ -89,6 +90,7 @@ class PiggyBankControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\PiggyBankController::index + * @covers \FireflyIII\Http\Controllers\PiggyBankController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/PreferencesControllerTest.php b/tests/Feature/Controllers/PreferencesControllerTest.php index c3ddf6a33f..34c49aefb8 100644 --- a/tests/Feature/Controllers/PreferencesControllerTest.php +++ b/tests/Feature/Controllers/PreferencesControllerTest.php @@ -42,6 +42,7 @@ class PreferencesControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\PreferencesController::index + * @covers \FireflyIII\Http\Controllers\PreferencesController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/ProfileControllerTest.php b/tests/Feature/Controllers/ProfileControllerTest.php index e063012f3c..8c351fd51d 100644 --- a/tests/Feature/Controllers/ProfileControllerTest.php +++ b/tests/Feature/Controllers/ProfileControllerTest.php @@ -41,6 +41,7 @@ class ProfileControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\ProfileController::index + * @covers \FireflyIII\Http\Controllers\ProfileController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/ReportControllerTest.php b/tests/Feature/Controllers/ReportControllerTest.php index 640a754f50..b7e58cb560 100644 --- a/tests/Feature/Controllers/ReportControllerTest.php +++ b/tests/Feature/Controllers/ReportControllerTest.php @@ -62,6 +62,7 @@ class ReportControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\ReportController::index + * @covers \FireflyIII\Http\Controllers\ReportController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/RuleControllerTest.php b/tests/Feature/Controllers/RuleControllerTest.php index 2cf8bf56e7..333c1923c3 100644 --- a/tests/Feature/Controllers/RuleControllerTest.php +++ b/tests/Feature/Controllers/RuleControllerTest.php @@ -80,6 +80,7 @@ class RuleControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\RuleController::index + * @covers \FireflyIII\Http\Controllers\RuleController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/RuleGroupControllerTest.php b/tests/Feature/Controllers/RuleGroupControllerTest.php index 514389d3eb..c3038bf15f 100644 --- a/tests/Feature/Controllers/RuleGroupControllerTest.php +++ b/tests/Feature/Controllers/RuleGroupControllerTest.php @@ -99,6 +99,7 @@ class RuleGroupControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\RuleGroupController::selectTransactions + * @covers \FireflyIII\Http\Controllers\RuleGroupController::__construct */ public function testSelectTransactions() { diff --git a/tests/Feature/Controllers/TagControllerTest.php b/tests/Feature/Controllers/TagControllerTest.php index 99e159039f..ff47501801 100644 --- a/tests/Feature/Controllers/TagControllerTest.php +++ b/tests/Feature/Controllers/TagControllerTest.php @@ -67,6 +67,7 @@ class TagControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\TagController::index + * @covers \FireflyIII\Http\Controllers\TagController::__construct */ public function testIndex() { diff --git a/tests/Feature/Controllers/Transaction/ConvertControllerTest.php b/tests/Feature/Controllers/Transaction/ConvertControllerTest.php index 48384606d3..47f8fb0070 100644 --- a/tests/Feature/Controllers/Transaction/ConvertControllerTest.php +++ b/tests/Feature/Controllers/Transaction/ConvertControllerTest.php @@ -24,6 +24,7 @@ class ConvertControllerTest extends TestCase { /** * @covers \FireflyIII\Http\Controllers\Transaction\ConvertController::index + * @covers \FireflyIII\Http\Controllers\Transaction\ConvertController::__construct */ public function testIndexDepositTransfer() { diff --git a/tests/Feature/Controllers/Transaction/MassControllerTest.php b/tests/Feature/Controllers/Transaction/MassControllerTest.php index 1af0ccbd3c..09b1d6347e 100644 --- a/tests/Feature/Controllers/Transaction/MassControllerTest.php +++ b/tests/Feature/Controllers/Transaction/MassControllerTest.php @@ -19,6 +19,7 @@ class MassControllerTest extends TestCase { /** * @covers \FireflyIII\Http\Controllers\Transaction\MassController::delete + * @covers \FireflyIII\Http\Controllers\Transaction\MassController::__construct */ public function testDelete() { diff --git a/tests/Feature/Controllers/Transaction/SingleControllerTest.php b/tests/Feature/Controllers/Transaction/SingleControllerTest.php index 48b00fc2d4..29d2b5681f 100644 --- a/tests/Feature/Controllers/Transaction/SingleControllerTest.php +++ b/tests/Feature/Controllers/Transaction/SingleControllerTest.php @@ -26,6 +26,7 @@ class SingleControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::create + * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::__construct */ public function testCreate() { diff --git a/tests/Feature/Controllers/Transaction/SplitControllerTest.php b/tests/Feature/Controllers/Transaction/SplitControllerTest.php index 93a7953f77..1164571ddb 100644 --- a/tests/Feature/Controllers/Transaction/SplitControllerTest.php +++ b/tests/Feature/Controllers/Transaction/SplitControllerTest.php @@ -24,6 +24,7 @@ class SplitControllerTest extends TestCase { /** * @covers \FireflyIII\Http\Controllers\Transaction\SplitController::edit + * @covers \FireflyIII\Http\Controllers\Transaction\SplitController::__construct * Implement testEdit(). */ public function testEdit() diff --git a/tests/Feature/Controllers/TransactionControllerTest.php b/tests/Feature/Controllers/TransactionControllerTest.php index b9ac046c69..25171bfd85 100644 --- a/tests/Feature/Controllers/TransactionControllerTest.php +++ b/tests/Feature/Controllers/TransactionControllerTest.php @@ -18,6 +18,7 @@ class TransactionControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\TransactionController::index + * @covers \FireflyIII\Http\Controllers\TransactionController::__construct */ public function testIndex() { From f7642beb7c3dd07ce6ec60eeacbb9af1e4d31cf2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 17 Feb 2017 20:15:17 +0100 Subject: [PATCH 004/244] Better 2fa handling --- app/Handlers/Events/UserEventHandler.php | 15 --------------- app/Http/Controllers/Auth/LoginController.php | 12 ++++++++---- app/Http/Controllers/Auth/TwoFactorController.php | 11 +++++------ app/Http/Middleware/AuthenticateTwoFactor.php | 9 ++++++++- .../RedirectIfTwoFactorAuthenticated.php | 8 ++++++-- app/Providers/EventServiceProvider.php | 6 ------ 6 files changed, 27 insertions(+), 34 deletions(-) diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 5f86d60a57..836f7b11b5 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -19,7 +19,6 @@ use FireflyIII\Repositories\User\UserRepositoryInterface; use Illuminate\Mail\Message; use Log; use Mail; -use Session; use Swift_TransportException; /** @@ -54,20 +53,6 @@ class UserEventHandler return true; } - /** - * Handle user logout events. - * - * @return bool - */ - public function logoutUser(): bool - { - // dump stuff from the session: - Session::forget('twoFactorAuthenticated'); - Session::forget('twoFactorAuthenticatedDate'); - - return true; - } - /** * @param RequestedNewPassword $event * diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 04e8abcc06..0072229023 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -16,6 +16,7 @@ use Config; use FireflyConfig; use FireflyIII\Http\Controllers\Controller; use FireflyIII\User; +use Illuminate\Cookie\CookieJar; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Lang; @@ -74,23 +75,26 @@ class LoginController extends Controller } /** - * @param Request $request + * @param Request $request + * @param CookieJar $cookieJar * - * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @return $this */ - public function logout(Request $request) + public function logout(Request $request, CookieJar $cookieJar) { if (intval(getenv('SANDSTORM')) === 1) { return view('error')->with('message', strval(trans('firefly.sandstorm_not_available'))); } + $cookie = $cookieJar->forever('twoFactorAuthenticated', 'false'); + $this->guard()->logout(); $request->session()->flush(); $request->session()->regenerate(); - return redirect('/'); + return redirect('/')->withCookie($cookie); } /** diff --git a/app/Http/Controllers/Auth/TwoFactorController.php b/app/Http/Controllers/Auth/TwoFactorController.php index 66df10f66e..9b74cad644 100644 --- a/app/Http/Controllers/Auth/TwoFactorController.php +++ b/app/Http/Controllers/Auth/TwoFactorController.php @@ -13,14 +13,13 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Auth; -use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\TokenFormRequest; +use Illuminate\Cookie\CookieJar; use Illuminate\Http\Request; use Log; use Preferences; -use Session; /** * Class TwoFactorController @@ -84,12 +83,12 @@ class TwoFactorController extends Controller * * @return mixed */ - public function postIndex(TokenFormRequest $request) + public function postIndex(TokenFormRequest $request, CookieJar $cookieJar) { - Session::put('twoFactorAuthenticated', true); - Session::put('twoFactorAuthenticatedDate', new Carbon); + // set cookie! + $cookie = $cookieJar->forever('twoFactorAuthenticated', 'true'); - return redirect(route('home')); + return redirect(route('home'))->withCookie($cookie); } } diff --git a/app/Http/Middleware/AuthenticateTwoFactor.php b/app/Http/Middleware/AuthenticateTwoFactor.php index 4f9f1f5542..5325150bcc 100644 --- a/app/Http/Middleware/AuthenticateTwoFactor.php +++ b/app/Http/Middleware/AuthenticateTwoFactor.php @@ -14,8 +14,10 @@ declare(strict_types = 1); namespace FireflyIII\Http\Middleware; use Closure; +use Cookie; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Log; use Preferences; use Session; @@ -55,8 +57,13 @@ class AuthenticateTwoFactor } $is2faEnabled = Preferences::get('twoFactorAuthEnabled', false)->data; $has2faSecret = !is_null(Preferences::get('twoFactorAuthSecret')); - $is2faAuthed = Session::get('twofactor-authenticated'); + + // grab 2auth information from cookie, not from session. + $is2faAuthed = Cookie::get('twoFactorAuthenticated') === 'true'; + if ($is2faEnabled && $has2faSecret && !$is2faAuthed) { + Log::debug('Does not seem to be 2 factor authed, redirect.'); + return redirect(route('two-factor.index')); } diff --git a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php index 0b2894ef19..53dcaa619e 100644 --- a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php +++ b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php @@ -17,7 +17,8 @@ use Closure; use Illuminate\Support\Facades\Auth; use Preferences; use Session; - +use Log; +use Cookie; /** * Class RedirectIfTwoFactorAuthenticated * @@ -40,7 +41,10 @@ class RedirectIfTwoFactorAuthenticated $is2faEnabled = Preferences::get('twoFactorAuthEnabled', false)->data; $has2faSecret = !is_null(Preferences::get('twoFactorAuthSecret')); - $is2faAuthed = Session::get('twoFactorAuthenticated'); + + // grab 2auth information from cookie + $is2faAuthed = Cookie::get('twoFactorAuthenticated') === 'true'; + if ($is2faEnabled && $has2faSecret && $is2faAuthed) { return redirect('/'); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 215fc5978e..3fcdd8da06 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -56,12 +56,6 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@scanBills', 'FireflyIII\Handlers\Events\UpdatedJournalEventHandler@processRules', ], - - // LARAVEL EVENTS: - 'Illuminate\Auth\Events\Logout' => - [ - 'FireflyIII\Handlers\Events\UserEventHandler@logoutUser', - ], ]; /** From f0cb63fd48c1c44da85e60f673cf3b2c18f65082 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 18 Feb 2017 09:32:10 +0100 Subject: [PATCH 005/244] Some small optimisations. --- app/Http/Controllers/HelpController.php | 8 ++++---- database/seeds/AccountTypeSeeder.php | 3 +-- database/seeds/TransactionCurrencySeeder.php | 2 -- database/seeds/TransactionTypeSeeder.php | 3 --- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/HelpController.php b/app/Http/Controllers/HelpController.php index 24d2d75254..82846210cf 100644 --- a/app/Http/Controllers/HelpController.php +++ b/app/Http/Controllers/HelpController.php @@ -58,7 +58,8 @@ class HelpController extends Controller return Response::json($content); } - $content = $help->getFromGithub($language, $route); + $content = $help->getFromGithub($language, $route); + $notYourLanguage = '

' . strval(trans('firefly.help_may_not_be_your_language')) . '

'; // get backup language content (try English): if (strlen($content) === 0) { @@ -68,12 +69,11 @@ class HelpController extends Controller $content = $help->getFromCache($route, $language); } if (!$help->inCache($route, $language)) { - $content = $help->getFromGithub($language, $route); - $content = '

' . strval(trans('firefly.help_may_not_be_your_language')) . '

' . $content; + $content = trim($notYourLanguage . $help->getFromGithub($language, $route)); } } - if (strlen($content) === 0) { + if ($content === $notYourLanguage) { $content = '

' . strval(trans('firefly.route_has_no_help')) . '

'; } diff --git a/database/seeds/AccountTypeSeeder.php b/database/seeds/AccountTypeSeeder.php index 64422f3a5d..8f8690f33a 100644 --- a/database/seeds/AccountTypeSeeder.php +++ b/database/seeds/AccountTypeSeeder.php @@ -21,8 +21,6 @@ class AccountTypeSeeder extends Seeder { public function run() { - DB::table('account_types')->delete(); - AccountType::create(['type' => 'Default account']); AccountType::create(['type' => 'Cash account']); AccountType::create(['type' => 'Asset account']); @@ -31,6 +29,7 @@ class AccountTypeSeeder extends Seeder AccountType::create(['type' => 'Initial balance account']); AccountType::create(['type' => 'Beneficiary account']); AccountType::create(['type' => 'Import account']); + AccountType::create(['type' => 'Loan']); } diff --git a/database/seeds/TransactionCurrencySeeder.php b/database/seeds/TransactionCurrencySeeder.php index f1204569a3..5d8fe07845 100644 --- a/database/seeds/TransactionCurrencySeeder.php +++ b/database/seeds/TransactionCurrencySeeder.php @@ -21,8 +21,6 @@ class TransactionCurrencySeeder extends Seeder { public function run() { - DB::table('transaction_currencies')->delete(); - TransactionCurrency::create(['code' => 'EUR', 'name' => 'Euro', 'symbol' => '€', 'decimal_places' => 2]); TransactionCurrency::create(['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$', 'decimal_places' => 2]); TransactionCurrency::create(['code' => 'HUF', 'name' => 'Hungarian forint', 'symbol' => 'Ft', 'decimal_places' => 2]); diff --git a/database/seeds/TransactionTypeSeeder.php b/database/seeds/TransactionTypeSeeder.php index 690d333aba..06ff214776 100644 --- a/database/seeds/TransactionTypeSeeder.php +++ b/database/seeds/TransactionTypeSeeder.php @@ -21,9 +21,6 @@ class TransactionTypeSeeder extends Seeder { public function run() { - - DB::table('transaction_types')->delete(); - TransactionType::create(['type' => TransactionType::WITHDRAWAL]); TransactionType::create(['type' => TransactionType::DEPOSIT]); TransactionType::create(['type' => TransactionType::TRANSFER]); From 5073fd937c1ae789387a905af1cc89c3cf6691cd Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 18 Feb 2017 20:10:03 +0100 Subject: [PATCH 006/244] Expand search with a bunch of keywords for #510 --- app/Http/Controllers/SearchController.php | 68 ++++++--- app/Support/Search/Modifier.php | 108 ++++++++++++++ app/Support/Search/Search.php | 173 ++++++++++++++++++---- app/Support/Search/SearchInterface.php | 47 +++--- config/firefly.php | 2 + resources/views/search/index.twig | 161 ++++++++++++-------- 6 files changed, 426 insertions(+), 133 deletions(-) create mode 100644 app/Support/Search/Modifier.php diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 4b47777c62..17e868a2fb 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -15,6 +15,8 @@ namespace FireflyIII\Http\Controllers; use FireflyIII\Support\Search\SearchInterface; use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use View; /** * Class SearchController @@ -30,44 +32,66 @@ class SearchController extends Controller { parent::__construct(); + $this->middleware( + function ($request, $next) { + View::share('mainTitleIcon', 'fa-search'); + View::share('title', trans('firefly.search')); + + return $next($request); + } + ); + } /** - * Results always come in the form of an array [results, count, fullCount] - * * @param Request $request * @param SearchInterface $searcher * - * @return $this + * @return View */ public function index(Request $request, SearchInterface $searcher) { - $minSearchLen = 1; - $subTitle = null; - $query = null; - $result = []; - $title = trans('firefly.search'); - $limit = 20; - $mainTitleIcon = 'fa-search'; + // yes, hard coded values: + $minSearchLen = 1; + $limit = 20; - // set limit for search: - $searcher->setLimit($limit); + // ui stuff: + $subTitle = ''; + + // query stuff + $query = null; + $result = []; + $rawQuery = $request->get('q'); + $hasModifiers = true; + $modifiers = []; if (!is_null($request->get('q')) && strlen($request->get('q')) >= $minSearchLen) { - $query = trim(strtolower($request->get('q'))); - $words = explode(' ', $query); - $subTitle = trans('firefly.search_results_for', ['query' => $query]); + // parse query, find modifiers: + // set limit for search + $searcher->setLimit($limit); + $searcher->parseQuery($request->get('q')); - $transactions = $searcher->searchTransactions($words); - $accounts = $searcher->searchAccounts($words); - $categories = $searcher->searchCategories($words); - $budgets = $searcher->searchBudgets($words); - $tags = $searcher->searchTags($words); - $result = ['transactions' => $transactions, 'accounts' => $accounts, 'categories' => $categories, 'budgets' => $budgets, 'tags' => $tags]; + $transactions = $searcher->searchTransactions(); + $accounts = new Collection; + $categories = new Collection; + $tags = new Collection; + $budgets = new Collection; + + // no special search thing? + if (!$searcher->hasModifiers()) { + $hasModifiers = false; + $accounts = $searcher->searchAccounts(); + $categories = $searcher->searchCategories(); + $budgets = $searcher->searchBudgets(); + $tags = $searcher->searchTags(); + } + $query = $searcher->getWordsAsString(); + $subTitle = trans('firefly.search_results_for', ['query' => $query]); + $result = ['transactions' => $transactions, 'accounts' => $accounts, 'categories' => $categories, 'budgets' => $budgets, 'tags' => $tags]; } - return view('search.index', compact('title', 'subTitle', 'limit', 'mainTitleIcon', 'query', 'result')); + return view('search.index', compact('rawQuery', 'hasModifiers', 'modifiers', 'subTitle', 'limit', 'query', 'result')); } } diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php new file mode 100644 index 0000000000..821e34c0fe --- /dev/null +++ b/app/Support/Search/Modifier.php @@ -0,0 +1,108 @@ +transaction_amount); + + $compare = bccomp($amount, $transactionAmount); + Log::debug(sprintf('%s vs %s is %d', $amount, $transactionAmount, $compare)); + + return $compare === $expected; + } + + /** + * @param Transaction $transaction + * @param string $amount + * + * @return bool + */ + public static function amountIs(Transaction $transaction, string $amount): bool + { + return self::amountCompare($transaction, $amount, 0); + } + + /** + * @param Transaction $transaction + * @param string $amount + * + * @return bool + */ + public static function amountLess(Transaction $transaction, string $amount): bool + { + return self::amountCompare($transaction, $amount, 1); + } + + /** + * @param Transaction $transaction + * @param string $amount + * + * @return bool + */ + public static function amountMore(Transaction $transaction, string $amount): bool + { + return self::amountCompare($transaction, $amount, -1); + } + + /** + * @param Transaction $transaction + * @param string $destination + * + * @return bool + */ + public static function destination(Transaction $transaction, string $destination): bool + { + return self::stringCompare($transaction->opposing_account_name, $destination); + } + + /** + * @param Transaction $transaction + * @param string $source + * + * @return bool + */ + public static function source(Transaction $transaction, string $source): bool + { + return self::stringCompare($transaction->account_name, $source); + } + + /** + * @param string $haystack + * @param string $needle + * + * @return bool + */ + public static function stringCompare(string $haystack, string $needle): bool + { + $res = !(strpos(strtolower($haystack), strtolower($needle)) === false); + Log::debug(sprintf('"%s" is in "%s"? %s', $needle, $haystack, var_export($res, true))); + + return $res; + + } +} \ No newline at end of file diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index b37af85daa..2063252ae3 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -14,6 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Support\Search; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -34,19 +35,64 @@ class Search implements SearchInterface { /** @var int */ private $limit = 100; + /** @var Collection */ + private $modifiers; /** @var User */ private $user; + /** @var array */ + private $validModifiers = []; + /** @var array */ + private $words = []; + + /** + * Search constructor. + */ + public function __construct() + { + $this->modifiers = new Collection; + $this->validModifiers = config('firefly.search_modifiers'); + } + + /** + * @return string + */ + public function getWordsAsString(): string + { + return join(' ', $this->words); + } + + /** + * @return bool + */ + public function hasModifiers(): bool + { + return $this->modifiers->count() > 0; + } + + /** + * @param string $query + */ + public function parseQuery(string $query) + { + $filteredQuery = $query; + $pattern = '/[a-z_]*:[0-9a-z.]*/i'; + $matches = []; + preg_match_all($pattern, $query, $matches); + + foreach ($matches[0] as $match) { + $this->extractModifier($match); + $filteredQuery = str_replace($match, '', $filteredQuery); + } + $filteredQuery = trim(str_replace(['"', "'"], '', $filteredQuery)); + $this->words = array_map('trim', explode(' ', $filteredQuery)); + } /** - * The search will assume that the user does not have so many accounts - * that this search should be paginated. - * - * @param array $words - * * @return Collection */ - public function searchAccounts(array $words): Collection + public function searchAccounts(): Collection { + $words = $this->words; $accounts = $this->user->accounts() ->accountTypeIn([AccountType::DEFAULT, AccountType::ASSET, AccountType::EXPENSE, AccountType::REVENUE, AccountType::BENEFICIARY]) ->get(['accounts.*']); @@ -67,14 +113,13 @@ class Search implements SearchInterface } /** - * @param array $words - * * @return Collection */ - public function searchBudgets(array $words): Collection + public function searchBudgets(): Collection { /** @var Collection $set */ - $set = auth()->user()->budgets()->get(); + $set = auth()->user()->budgets()->get(); + $words = $this->words; /** @var Collection $result */ $result = $set->filter( function (Budget $budget) use ($words) { @@ -92,14 +137,11 @@ class Search implements SearchInterface } /** - * Search assumes the user does not have that many categories. So no paginated search. - * - * @param array $words - * * @return Collection */ - public function searchCategories(array $words): Collection + public function searchCategories(): Collection { + $words = $this->words; $categories = $this->user->categories()->get(); /** @var Collection $result */ $result = $categories->filter( @@ -117,15 +159,12 @@ class Search implements SearchInterface } /** - * - * @param array $words - * * @return Collection */ - public function searchTags(array $words): Collection + public function searchTags(): Collection { - $tags = $this->user->tags()->get(); - + $words = $this->words; + $tags = $this->user->tags()->get(); /** @var Collection $result */ $result = $tags->filter( function (Tag $tag) use ($words) { @@ -142,11 +181,9 @@ class Search implements SearchInterface } /** - * @param array $words - * * @return Collection */ - public function searchTransactions(array $words): Collection + public function searchTransactions(): Collection { $pageSize = 100; $processed = 0; @@ -156,20 +193,17 @@ class Search implements SearchInterface /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setUser($this->user); - $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page); - $set = $collector->getPaginatedJournals(); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->withOpposingAccount(); + $set = $collector->getPaginatedJournals(); + $words = $this->words; + Log::debug(sprintf('Found %d journals to check. ', $set->count())); // Filter transactions that match the given triggers. $filtered = $set->filter( function (Transaction $transaction) use ($words) { - // check descr of journal: - if ($this->strpos_arr(strtolower(strval($transaction->description)), $words)) { - return $transaction; - } - // check descr of transaction - if ($this->strpos_arr(strtolower(strval($transaction->transaction_description)), $words)) { + if ($this->matchModifiers($transaction)) { return $transaction; } @@ -201,6 +235,8 @@ class Search implements SearchInterface } while (!$reachedEndOfList && !$foundEnough); $result = $result->slice(0, $this->limit); + var_dump($result->toArray()); + exit; return $result; } @@ -221,6 +257,79 @@ class Search implements SearchInterface $this->user = $user; } + /** + * @param string $string + */ + private function extractModifier(string $string) + { + $parts = explode(':', $string); + if (count($parts) === 2 && strlen(trim(strval($parts[0]))) > 0 && strlen(trim(strval($parts[1])))) { + $type = trim(strval($parts[0])); + $value = trim(strval($parts[1])); + if (in_array($type, $this->validModifiers)) { + // filter for valid type + $this->modifiers->push(['type' => $type, 'value' => $value,]); + } + } + } + + /** + * @param Transaction $transaction + * + * @return bool + * @throws FireflyException + */ + private function matchModifiers(Transaction $transaction): bool + { + Log::debug(sprintf('Now at transaction #%d', $transaction->id)); + // first "modifier" is always the text of the search: + // check descr of journal: + if (!$this->strpos_arr(strtolower(strval($transaction->description)), $this->words) + && !$this->strpos_arr(strtolower(strval($transaction->transaction_description)), $this->words) + ) { + Log::debug('Description does not match', $this->words); + + return false; + } + + + // then a for-each and a switch for every possible other thingie. + foreach ($this->modifiers as $modifier) { + switch ($modifier['type']) { + default: + throw new FireflyException(sprintf('Search modifier "%s" is not (yet) supported. Sorry!', $modifier['type'])); + break; + case 'amount_is': + $res = Modifier::amountIs($transaction, $modifier['value']); + Log::debug(sprintf('Amount is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'amount_less': + $res = Modifier::amountLess($transaction, $modifier['value']); + Log::debug(sprintf('Amount less than %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'amount_more': + $res = Modifier::amountMore($transaction, $modifier['value']); + Log::debug(sprintf('Amount more than %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'source': + $res = Modifier::source($transaction, $modifier['value']); + Log::debug(sprintf('Source is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'destination': + $res = Modifier::destination($transaction, $modifier['value']); + Log::debug(sprintf('Destination is %s? %s', $modifier['value'], var_export($res, true))); + break; + + } + if ($res === false) { + return $res; + } + } + + return true; + + } + /** * @param string $haystack * @param array $needle diff --git a/app/Support/Search/SearchInterface.php b/app/Support/Search/SearchInterface.php index 918f3caee3..e3962fcfea 100644 --- a/app/Support/Search/SearchInterface.php +++ b/app/Support/Search/SearchInterface.php @@ -24,40 +24,49 @@ use Illuminate\Support\Collection; interface SearchInterface { /** - * @param array $words - * - * @return Collection + * @return string */ - public function searchAccounts(array $words): Collection; + public function getWordsAsString(): string; /** - * @param array $words - * - * @return Collection + * @return bool */ - public function searchBudgets(array $words): Collection; + public function hasModifiers(): bool; /** - * @param array $words - * - * @return Collection + * @param string $query */ - public function searchCategories(array $words): Collection; + public function parseQuery(string $query); /** - * - * @param array $words - * * @return Collection */ - public function searchTags(array $words): Collection; + public function searchAccounts(): Collection; /** - * @param array $words - * * @return Collection */ - public function searchTransactions(array $words): Collection; + public function searchBudgets(): Collection; + + /** + * @return Collection + */ + public function searchCategories(): Collection; + + /** + * @return Collection + */ + public function searchTags(): Collection; + + /** + * @return Collection + */ + public function searchTransactions(): Collection; + + /** + * @param int $limit + */ + public function setLimit(int $limit); /** * @param User $user diff --git a/config/firefly.php b/config/firefly.php index 5ffebe0c43..6844fa7a10 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -209,4 +209,6 @@ return [ ], 'default_currency' => 'EUR', 'default_language' => 'en_US', + 'search_modifiers' => ['amount_is', 'amount_less', 'amount_more', 'source', 'destination', 'category', 'budget', 'tag', 'bill', 'type', 'date_on', + 'date_before', 'date_after', 'has_attachments', 'notes',], ]; diff --git a/resources/views/search/index.twig b/resources/views/search/index.twig index e166a612c7..fc82a50940 100644 --- a/resources/views/search/index.twig +++ b/resources/views/search/index.twig @@ -16,15 +16,38 @@
+
+
+

{{ 'advanced_search'|_ }}

+
+
+

+ There are several modifiers that you can use in your search to narrow down the results. + If you use any of these, the search will only return transactions. + Please click the -icon for more information. +

+ {# search form #} +
+
+ +
+ +
+
+
+
+ +
+
+
+
+

{{ trans('firefly.results_limited', {count: limit}) }}

- -
- - - {% if result.transactions|length > 0 %} -
+ {% if hasModifiers %} +
+

{{ 'transactions'|_ }}

@@ -37,68 +60,86 @@
- {% endif %} - {% if result.categories|length > 0 %} -
-
-
-

{{ 'categories'|_ }}

-
-
-

- {{ trans('firefly.search_found_categories', {count: result.categories|length}) }} -

- {% include 'search.partials.categories' %} +
+ {% else %} +
+ {% if result.transactions|length > 0 %} +
+
+
+

{{ 'transactions'|_ }}

+
+
+

+ {{ trans('firefly.search_found_transactions', {count: result.transactions|length}) }} +

+ {% include 'search.partials.transactions' with {'transactions' : result.transactions} %} +
-
- {% endif %} - {% if result.tags|length > 0 %} -
-
-
-

{{ 'tags'|_ }}

-
-
-

- {{ trans('firefly.search_found_tags', {count: result.tags|length}) }} -

- {% include 'search.partials.tags' %} + {% endif %} + {% if result.categories|length > 0 %} +
+
+
+

{{ 'categories'|_ }}

+
+
+

+ {{ trans('firefly.search_found_categories', {count: result.categories|length}) }} +

+ {% include 'search.partials.categories' %} +
-
- {% endif %} - {% if result.accounts|length > 0 %} -
-
-
-

{{ 'accounts'|_ }}

-
-
-

- {{ trans('firefly.search_found_accounts', {count: result.accounts|length}) }} -

- {% include 'search.partials.accounts' %} + {% endif %} + {% if result.tags|length > 0 %} +
+
+
+

{{ 'tags'|_ }}

+
+
+

+ {{ trans('firefly.search_found_tags', {count: result.tags|length}) }} +

+ {% include 'search.partials.tags' %} +
-
- {% endif %} - {% if result.budgets|length > 0 %} -
-
-
-

{{ 'budgets'|_ }}

-
-
-

- {{ trans('firefly.search_found_budgets', {count: result.budgets|length}) }} -

- {% include 'search.partials.budgets' %} + {% endif %} + {% if result.accounts|length > 0 %} +
+
+
+

{{ 'accounts'|_ }}

+
+
+

+ {{ trans('firefly.search_found_accounts', {count: result.accounts|length}) }} +

+ {% include 'search.partials.accounts' %} +
-
- {% endif %} -
+ {% endif %} + {% if result.budgets|length > 0 %} +
+
+
+

{{ 'budgets'|_ }}

+
+
+

+ {{ trans('firefly.search_found_budgets', {count: result.budgets|length}) }} +

+ {% include 'search.partials.budgets' %} +
+
+
+ {% endif %} +
+ {% endif %} {% endif %} From b5032a7597c654235304964ac143e6fb85a87e80 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 07:34:39 +0100 Subject: [PATCH 007/244] Added a new helper function. --- .../Collector/JournalExportCollector.php | 9 +++--- app/Export/Entry/Entry.php | 21 +++--------- app/Helpers/Collector/JournalCollector.php | 4 +-- app/Repositories/Journal/JournalTasker.php | 7 ++-- app/Support/Steam.php | 16 ++++++++++ app/Support/Twig/Transaction.php | 32 ++++++------------- 6 files changed, 39 insertions(+), 50 deletions(-) diff --git a/app/Export/Collector/JournalExportCollector.php b/app/Export/Collector/JournalExportCollector.php index e9bfbca879..551880ce4f 100644 --- a/app/Export/Collector/JournalExportCollector.php +++ b/app/Export/Collector/JournalExportCollector.php @@ -19,6 +19,7 @@ use DB; use FireflyIII\Models\Transaction; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; +use Steam; /** * Class JournalExportCollector @@ -118,7 +119,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac ); $set->each( function ($obj) { - $obj->name = $obj->encrypted === 1 ? Crypt::decrypt($obj->name) : $obj->name; + $obj->name = Steam::decrypt(intval($obj->encrypted), $obj->name); } ); $array = []; @@ -159,7 +160,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac ); $set->each( function ($obj) { - $obj->name = $obj->encrypted === 1 ? Crypt::decrypt($obj->name) : $obj->name; + $obj->name = Steam::decrypt(intval($obj->encrypted), $obj->name); } ); $array = []; @@ -202,7 +203,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac ); $set->each( function ($obj) { - $obj->name = $obj->encrypted === 1 ? Crypt::decrypt($obj->name) : $obj->name; + $obj->name = Steam::decrypt(intval($obj->encrypted), $obj->name); } ); $array = []; @@ -243,7 +244,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac ); $set->each( function ($obj) { - $obj->name = $obj->encrypted === 1 ? Crypt::decrypt($obj->name) : $obj->name; + $obj->name = Steam::decrypt(intval($obj->encrypted), $obj->name); } ); $array = []; diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php index 0afc40ada4..2fb30850d0 100644 --- a/app/Export/Entry/Entry.php +++ b/app/Export/Entry/Entry.php @@ -14,6 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Export\Entry; use Crypt; +use Steam; /** * To extend the exported object, in case of new features in Firefly III for example, @@ -73,15 +74,15 @@ final class Entry { $entry = new self; $entry->journal_id = $object->transaction_journal_id; - $entry->description = self::decrypt(intval($object->journal_encrypted), $object->journal_description); + $entry->description = Steam::decrypt(intval($object->journal_encrypted), $object->journal_description); $entry->amount = $object->amount; $entry->date = $object->date; $entry->transaction_type = $object->transaction_type; $entry->currency_code = $object->transaction_currency_code; $entry->source_account_id = $object->account_id; - $entry->source_account_name = self::decrypt(intval($object->account_name_encrypted), $object->account_name); + $entry->source_account_name = Steam::decrypt(intval($object->account_name_encrypted), $object->account_name); $entry->destination_account_id = $object->opposing_account_id; - $entry->destination_account_name = self::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name); + $entry->destination_account_name = Steam::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name); $entry->category_id = $object->category_id ?? ''; $entry->category_name = $object->category_name ?? ''; $entry->budget_id = $object->budget_id ?? ''; @@ -95,19 +96,5 @@ final class Entry return $entry; } - /** - * @param int $isEncrypted - * @param $value - * - * @return string - */ - protected static function decrypt(int $isEncrypted, $value) - { - if ($isEncrypted === 1) { - return Crypt::decrypt($value); - } - - return $value; - } } diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index 3a146310fd..1603b61256 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -170,10 +170,10 @@ class JournalCollector implements JournalCollectorInterface $set->each( function (Transaction $transaction) { $transaction->date = new Carbon($transaction->date); - $transaction->description = $transaction->encrypted ? Crypt::decrypt($transaction->description) : $transaction->description; + $transaction->description = Steam::decrypt(intval($transaction->encrypted), $transaction->description); if (!is_null($transaction->bill_name)) { - $transaction->bill_name = $transaction->bill_name_encrypted ? Crypt::decrypt($transaction->bill_name) : $transaction->bill_name; + $transaction->bill_name = Steam::decrypt(intval($transaction->bill_name_encrypted), $transaction->bill_name); } try { diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index c0424242c8..7a75d8a871 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Journal; -use Crypt; use DB; use FireflyIII\Models\AccountType; use FireflyIII\Models\PiggyBankEvent; @@ -23,6 +22,7 @@ use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; +use Steam; /** * Class JournalTasker @@ -113,7 +113,7 @@ class JournalTasker implements JournalTaskerInterface 'source_amount' => $entry->amount, 'description' => $entry->description, 'source_account_id' => $entry->account_id, - 'source_account_name' => intval($entry->account_encrypted) === 1 ? Crypt::decrypt($entry->account_name) : $entry->account_name, + 'source_account_name' => Steam::decrypt(intval($entry->account_encrypted), $entry->account_name), 'source_account_type' => $entry->account_type, 'source_account_before' => $sourceBalance, 'source_account_after' => bcadd($sourceBalance, $entry->amount), @@ -121,8 +121,7 @@ class JournalTasker implements JournalTaskerInterface 'destination_amount' => bcmul($entry->amount, '-1'), 'destination_account_id' => $entry->destination_account_id, 'destination_account_type' => $entry->destination_account_type, - 'destination_account_name' => - intval($entry->destination_account_encrypted) === 1 ? Crypt::decrypt($entry->destination_account_name) : $entry->destination_account_name, + 'destination_account_name' => Steam::decrypt(intval($entry->destination_account_encrypted), $entry->destination_account_name), 'destination_account_before' => $destinationBalance, 'destination_account_after' => bcadd($destinationBalance, bcmul($entry->amount, '-1')), 'budget_id' => is_null($budget) ? 0 : $budget->id, diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 870b55566c..7693cac164 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -14,6 +14,7 @@ declare(strict_types = 1); namespace FireflyIII\Support; use Carbon\Carbon; +use Crypt; use DB; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; @@ -180,6 +181,21 @@ class Steam return $result; } + /** + * @param int $isEncrypted + * @param $value + * + * @return string + */ + public function decrypt(int $isEncrypted, string $value) + { + if ($isEncrypted === 1) { + return Crypt::decrypt($value); + } + + return $value; + } + /** * @param array $accounts * diff --git a/app/Support/Twig/Transaction.php b/app/Support/Twig/Transaction.php index 5285a8ee90..f1a0b98dd5 100644 --- a/app/Support/Twig/Transaction.php +++ b/app/Support/Twig/Transaction.php @@ -19,6 +19,7 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction as TransactionModel; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; +use Steam; use Twig_Extension; use Twig_SimpleFilter; use Twig_SimpleFunction; @@ -195,7 +196,7 @@ class Transaction extends Twig_Extension return new Twig_SimpleFunction( 'transactionDestinationAccount', function (TransactionModel $transaction): string { - $name = intval($transaction->account_encrypted) === 1 ? Crypt::decrypt($transaction->account_name) : $transaction->account_name; + $name = Steam::decrypt(intval($transaction->account_encrypted), $transaction->account_name); $id = intval($transaction->account_id); $type = $transaction->account_type; @@ -217,7 +218,7 @@ class Transaction extends Twig_Extension ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); - $name = intval($other->encrypted) === 1 ? Crypt::decrypt($other->name) : $other->name; + $name = Steam::decrypt(intval($other->encrypted), $other->name); $id = $other->account_id; $type = $other->type; } @@ -269,7 +270,7 @@ class Transaction extends Twig_Extension 'transactionSourceAccount', function (TransactionModel $transaction): string { // if the amount is negative, assume that the current account (the one in $transaction) is indeed the source account. - $name = intval($transaction->account_encrypted) === 1 ? Crypt::decrypt($transaction->account_name) : $transaction->account_name; + $name = Steam::decrypt(intval($transaction->account_encrypted), $transaction->account_name); $id = intval($transaction->account_id); $type = $transaction->account_type; @@ -289,7 +290,7 @@ class Transaction extends Twig_Extension ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); - $name = intval($other->encrypted) === 1 ? Crypt::decrypt($other->name) : $other->name; + $name = Steam::decrypt(intval($other->encrypted), $other->name); $id = $other->account_id; $type = $other->type; } @@ -337,21 +338,6 @@ class Transaction extends Twig_Extension ); } - /** - * @param int $isEncrypted - * @param string $value - * - * @return string - */ - private function encrypted(int $isEncrypted, string $value): string - { - if ($isEncrypted === 1) { - return Crypt::decrypt($value); - } - - return $value; - } - /** * @param TransactionModel $transaction * @@ -361,14 +347,14 @@ class Transaction extends Twig_Extension { // journal has a budget: if (isset($transaction->transaction_journal_budget_id)) { - $name = $this->encrypted(intval($transaction->transaction_journal_budget_encrypted), $transaction->transaction_journal_budget_name); + $name = Steam::decrypt(intval($transaction->transaction_journal_budget_encrypted), $transaction->transaction_journal_budget_name); return sprintf('%s', route('budgets.show', [$transaction->transaction_journal_budget_id]), $name, $name); } // transaction has a budget if (isset($transaction->transaction_budget_id)) { - $name = $this->encrypted(intval($transaction->transaction_budget_encrypted), $transaction->transaction_budget_name); + $name = Steam::decrypt(intval($transaction->transaction_budget_encrypted), $transaction->transaction_budget_name); return sprintf('%s', route('budgets.show', [$transaction->transaction_budget_id]), $name, $name); } @@ -400,14 +386,14 @@ class Transaction extends Twig_Extension { // journal has a category: if (isset($transaction->transaction_journal_category_id)) { - $name = $this->encrypted(intval($transaction->transaction_journal_category_encrypted), $transaction->transaction_journal_category_name); + $name = Steam::decrypt(intval($transaction->transaction_journal_category_encrypted), $transaction->transaction_journal_category_name); return sprintf('%s', route('categories.show', [$transaction->transaction_journal_category_id]), $name, $name); } // transaction has a category: if (isset($transaction->transaction_category_id)) { - $name = $this->encrypted(intval($transaction->transaction_category_encrypted), $transaction->transaction_category_name); + $name = Steam::decrypt(intval($transaction->transaction_category_encrypted), $transaction->transaction_category_name); return sprintf('%s', route('categories.show', [$transaction->transaction_category_id]), $name, $name); } From 1f6180ce5d8e17bea46d440fbe150161e5e24c9b Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 07:38:51 +0100 Subject: [PATCH 008/244] Updated search modifiers. --- app/Support/Search/Modifier.php | 102 ++++++++++++++++---------------- app/Support/Search/Search.php | 33 ++--------- 2 files changed, 56 insertions(+), 79 deletions(-) diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index 821e34c0fe..b4e3cd5fd8 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -12,6 +12,7 @@ declare(strict_types = 1); namespace FireflyIII\Support\Search; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Transaction; use Log; use Steam; @@ -36,59 +37,40 @@ class Modifier return $compare === $expected; } - /** - * @param Transaction $transaction - * @param string $amount - * - * @return bool - */ - public static function amountIs(Transaction $transaction, string $amount): bool + public static function apply(array $modifier, Transaction $transaction): bool { - return self::amountCompare($transaction, $amount, 0); - } + switch ($modifier['type']) { + default: + throw new FireflyException(sprintf('Search modifier "%s" is not (yet) supported. Sorry!', $modifier['type'])); + break; + case 'amount_is': + $res = Modifier::amountCompare($transaction, $modifier['value'], 0); + Log::debug(sprintf('Amount is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'amount_less': + $res = Modifier::amountCompare($transaction, $modifier['value'], 1); + Log::debug(sprintf('Amount less than %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'amount_more': + $res = Modifier::amountCompare($transaction, $modifier['value'], -1); + Log::debug(sprintf('Amount more than %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'source': + $res = Modifier::stringCompare($transaction->account_name, $modifier['value']); + Log::debug(sprintf('Source is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'destination': + $res = Modifier::stringCompare($transaction->opposing_account_name, $modifier['value']); + Log::debug(sprintf('Destination is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'category': + $res = Modifier::category($transaction, $modifier['value']); + Log::debug(sprintf('Category is %s? %s', $modifier['value'], var_export($res, true))); + break; - /** - * @param Transaction $transaction - * @param string $amount - * - * @return bool - */ - public static function amountLess(Transaction $transaction, string $amount): bool - { - return self::amountCompare($transaction, $amount, 1); - } + } - /** - * @param Transaction $transaction - * @param string $amount - * - * @return bool - */ - public static function amountMore(Transaction $transaction, string $amount): bool - { - return self::amountCompare($transaction, $amount, -1); - } - - /** - * @param Transaction $transaction - * @param string $destination - * - * @return bool - */ - public static function destination(Transaction $transaction, string $destination): bool - { - return self::stringCompare($transaction->opposing_account_name, $destination); - } - - /** - * @param Transaction $transaction - * @param string $source - * - * @return bool - */ - public static function source(Transaction $transaction, string $source): bool - { - return self::stringCompare($transaction->account_name, $source); + return $res; } /** @@ -105,4 +87,24 @@ class Modifier return $res; } + + /** + * @param Transaction $transaction + * @param string $search + * + * @return bool + */ + private static function category(Transaction $transaction, string $search): bool + { + $journalCategory = ''; + if (!is_null($transaction->transaction_journal_category_name)) { + $journalCategory = Steam::decrypt(intval($transaction->transaction_journal_category_encrypted), $transaction->transaction_journal_category_name); + } + $transactionCategory = ''; + if (!is_null($transaction->transaction_category_name)) { + $journalCategory = Steam::decrypt(intval($transaction->transaction_category_encrypted), $transaction->transaction_category_name); + } + + return self::stringCompare($journalCategory, $search) || self::stringCompare($transactionCategory, $search); + } } \ No newline at end of file diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index 2063252ae3..0a0e80f566 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -193,8 +193,9 @@ class Search implements SearchInterface /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setUser($this->user); - $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->withOpposingAccount(); - $set = $collector->getPaginatedJournals(); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page) + ->withOpposingAccount()->withCategoryInformation(); + $set = $collector->getPaginatedJournals()->getCollection(); $words = $this->words; Log::debug(sprintf('Found %d journals to check. ', $set->count())); @@ -292,35 +293,9 @@ class Search implements SearchInterface return false; } - // then a for-each and a switch for every possible other thingie. foreach ($this->modifiers as $modifier) { - switch ($modifier['type']) { - default: - throw new FireflyException(sprintf('Search modifier "%s" is not (yet) supported. Sorry!', $modifier['type'])); - break; - case 'amount_is': - $res = Modifier::amountIs($transaction, $modifier['value']); - Log::debug(sprintf('Amount is %s? %s', $modifier['value'], var_export($res, true))); - break; - case 'amount_less': - $res = Modifier::amountLess($transaction, $modifier['value']); - Log::debug(sprintf('Amount less than %s? %s', $modifier['value'], var_export($res, true))); - break; - case 'amount_more': - $res = Modifier::amountMore($transaction, $modifier['value']); - Log::debug(sprintf('Amount more than %s? %s', $modifier['value'], var_export($res, true))); - break; - case 'source': - $res = Modifier::source($transaction, $modifier['value']); - Log::debug(sprintf('Source is %s? %s', $modifier['value'], var_export($res, true))); - break; - case 'destination': - $res = Modifier::destination($transaction, $modifier['value']); - Log::debug(sprintf('Destination is %s? %s', $modifier['value'], var_export($res, true))); - break; - - } + $res = Modifier::apply($modifier, $transaction); if ($res === false) { return $res; } From a27b686446d7fc9fbf6dd92949dd5843d73c17b5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 07:41:12 +0100 Subject: [PATCH 009/244] Add budget keyword. --- app/Support/Search/Modifier.php | 24 ++++++++++++++++++++++++ app/Support/Search/Search.php | 6 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index b4e3cd5fd8..a79744097a 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -67,6 +67,10 @@ class Modifier $res = Modifier::category($transaction, $modifier['value']); Log::debug(sprintf('Category is %s? %s', $modifier['value'], var_export($res, true))); break; + case 'budget': + $res = Modifier::budget($transaction, $modifier['value']); + Log::debug(sprintf('Budget is %s? %s', $modifier['value'], var_export($res, true))); + break; } @@ -88,6 +92,26 @@ class Modifier } + /** + * @param Transaction $transaction + * @param string $search + * + * @return bool + */ + private static function budget(Transaction $transaction, string $search): bool + { + $journalBudget = ''; + if (!is_null($transaction->transaction_journal_budget_name)) { + $journalBudget = Steam::decrypt(intval($transaction->transaction_journal_budget_encrypted), $transaction->transaction_journal_budget_name); + } + $transactionBudget = ''; + if (!is_null($transaction->transaction_budget_name)) { + $journalBudget = Steam::decrypt(intval($transaction->transaction_budget_encrypted), $transaction->transaction_budget_name); + } + + return self::stringCompare($journalBudget, $search) || self::stringCompare($transactionBudget, $search); + } + /** * @param Transaction $transaction * @param string $search diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index 0a0e80f566..add2d3fc7d 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -193,8 +193,10 @@ class Search implements SearchInterface /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); $collector->setUser($this->user); - $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page) - ->withOpposingAccount()->withCategoryInformation(); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page); + if($this->hasModifiers()) { + $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); + } $set = $collector->getPaginatedJournals()->getCollection(); $words = $this->words; From 711a1a1d4fe31ccbc03f6b4c937220cb60418f04 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 09:07:14 +0100 Subject: [PATCH 010/244] Final modifiers. --- app/Support/Search/Modifier.php | 74 ++++++++++++++++++++++++++++++++- app/Support/Search/Search.php | 12 ++++-- config/firefly.php | 5 ++- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index a79744097a..fe6bb2d851 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -12,6 +12,8 @@ declare(strict_types = 1); namespace FireflyIII\Support\Search; +use Carbon\Carbon; +use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Transaction; use Log; @@ -71,12 +73,82 @@ class Modifier $res = Modifier::budget($transaction, $modifier['value']); Log::debug(sprintf('Budget is %s? %s', $modifier['value'], var_export($res, true))); break; - + case 'bill': + $res = Modifier::stringCompare(strval($transaction->bill_name), $modifier['value']); + Log::debug(sprintf('Bill is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'type': + $res = Modifier::stringCompare($transaction->transaction_type_type, $modifier['value']); + Log::debug(sprintf('Transaction type is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'date': + $res = Modifier::sameDate($transaction->date, $modifier['value']); + Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'date_before': + $res = Modifier::dateBefore($transaction->date, $modifier['value']); + Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); + break; + case 'date_after': + $res = Modifier::dateAfter($transaction->date, $modifier['value']); + Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); + break; } return $res; } + /** + * @param Carbon $date + * @param string $compare + * + * @return bool + */ + public static function dateAfter(Carbon $date, string $compare): bool + { + try { + $compareDate = new Carbon($compare); + } catch (Exception $e) { + return false; + } + + return $date->greaterThanOrEqualTo($compareDate); + } + + /** + * @param Carbon $date + * @param string $compare + * + * @return bool + */ + public static function dateBefore(Carbon $date, string $compare): bool + { + try { + $compareDate = new Carbon($compare); + } catch (Exception $e) { + return false; + } + + return $date->lessThanOrEqualTo($compareDate); + } + + /** + * @param Carbon $date + * @param string $compare + * + * @return bool + */ + public static function sameDate(Carbon $date, string $compare): bool + { + try { + $compareDate = new Carbon($compare); + } catch (Exception $e) { + return false; + } + + return $compareDate->isSameDay($date); + } + /** * @param string $haystack * @param string $needle diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index add2d3fc7d..4e96e3cb5b 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -75,7 +75,7 @@ class Search implements SearchInterface public function parseQuery(string $query) { $filteredQuery = $query; - $pattern = '/[a-z_]*:[0-9a-z.]*/i'; + $pattern = '/[a-z_]*:[0-9a-z-.]*/i'; $matches = []; preg_match_all($pattern, $query, $matches); @@ -84,7 +84,9 @@ class Search implements SearchInterface $filteredQuery = str_replace($match, '', $filteredQuery); } $filteredQuery = trim(str_replace(['"', "'"], '', $filteredQuery)); - $this->words = array_map('trim', explode(' ', $filteredQuery)); + if (strlen($filteredQuery) > 0) { + $this->words = array_map('trim', explode(' ', $filteredQuery)); + } } /** @@ -194,9 +196,10 @@ class Search implements SearchInterface $collector = app(JournalCollectorInterface::class); $collector->setUser($this->user); $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page); - if($this->hasModifiers()) { + if ($this->hasModifiers()) { $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); } + $collector->disableInternalFilter(); $set = $collector->getPaginatedJournals()->getCollection(); $words = $this->words; @@ -287,7 +290,8 @@ class Search implements SearchInterface Log::debug(sprintf('Now at transaction #%d', $transaction->id)); // first "modifier" is always the text of the search: // check descr of journal: - if (!$this->strpos_arr(strtolower(strval($transaction->description)), $this->words) + if (count($this->words) > 0 + && !$this->strpos_arr(strtolower(strval($transaction->description)), $this->words) && !$this->strpos_arr(strtolower(strval($transaction->transaction_description)), $this->words) ) { Log::debug('Description does not match', $this->words); diff --git a/config/firefly.php b/config/firefly.php index 6844fa7a10..3d4d28d6d7 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -209,6 +209,7 @@ return [ ], 'default_currency' => 'EUR', 'default_language' => 'en_US', - 'search_modifiers' => ['amount_is', 'amount_less', 'amount_more', 'source', 'destination', 'category', 'budget', 'tag', 'bill', 'type', 'date_on', - 'date_before', 'date_after', 'has_attachments', 'notes',], + 'search_modifiers' => ['amount_is', 'amount_less', 'amount_more', 'source', 'destination', 'category', 'budget', 'bill', 'type', 'date', + 'date_before', 'date_after'], + // tag notes has_attachments ]; From bf35ecc07ada1370589af1708e1fddea7485a214 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 09:17:02 +0100 Subject: [PATCH 011/244] Fixed tests --- tests/Feature/Controllers/SearchControllerTest.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Controllers/SearchControllerTest.php b/tests/Feature/Controllers/SearchControllerTest.php index babcefa82c..e0eeb52d6b 100644 --- a/tests/Feature/Controllers/SearchControllerTest.php +++ b/tests/Feature/Controllers/SearchControllerTest.php @@ -26,11 +26,14 @@ class SearchControllerTest extends TestCase { $search = $this->mock(SearchInterface::class); $search->shouldReceive('setLimit')->once(); - $search->shouldReceive('searchTransactions')->andReturn(new Collection)->withArgs([['test']])->once(); - $search->shouldReceive('searchBudgets')->andReturn(new Collection)->withArgs([['test']])->once(); - $search->shouldReceive('searchTags')->andReturn(new Collection)->withArgs([['test']])->once(); - $search->shouldReceive('searchCategories')->andReturn(new Collection)->withArgs([['test']])->once(); - $search->shouldReceive('searchAccounts')->andReturn(new Collection)->withArgs([['test']])->once(); + $search->shouldReceive('parseQuery')->once(); + $search->shouldReceive('hasModifiers')->once()->andReturn(false); + $search->shouldReceive('getWordsAsString')->once()->andReturn('test'); + $search->shouldReceive('searchTransactions')->andReturn(new Collection)->once(); + $search->shouldReceive('searchBudgets')->andReturn(new Collection)->once(); + $search->shouldReceive('searchTags')->andReturn(new Collection)->once(); + $search->shouldReceive('searchCategories')->andReturn(new Collection)->once(); + $search->shouldReceive('searchAccounts')->andReturn(new Collection)->once(); $this->be($this->user()); $response = $this->get(route('search.index') . '?q=test'); $response->assertStatus(200); From b149a816dd5e3840b7d188925dcd392a2fe85ac2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 09:36:51 +0100 Subject: [PATCH 012/244] Make search work. [skip ci] --- app/Support/Search/Search.php | 2 -- resources/lang/en_US/firefly.php | 2 ++ resources/views/search/index.twig | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index 4e96e3cb5b..cc6bd5e9f3 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -241,8 +241,6 @@ class Search implements SearchInterface } while (!$reachedEndOfList && !$foundEnough); $result = $result->slice(0, $this->limit); - var_dump($result->toArray()); - exit; return $result; } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 23d501fdb0..b0d2efd35f 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -26,6 +26,8 @@ return [ 'showEverything' => 'Show everything', 'never' => 'Never', 'search_results_for' => 'Search results for ":query"', + 'advanced_search' => 'Advanced search', + 'advanced_search_intro' => 'There are several modifiers that you can use in your search to narrow down the results. If you use any of these, the search will only return transactions. Please click the -icon for more information.', 'bounced_error' => 'The message sent to :email bounced, so no access for you.', 'deleted_error' => 'These credentials do not match our records.', 'general_blocked_error' => 'Your account has been disabled, so you cannot login.', diff --git a/resources/views/search/index.twig b/resources/views/search/index.twig index fc82a50940..5459b7720f 100644 --- a/resources/views/search/index.twig +++ b/resources/views/search/index.twig @@ -22,9 +22,7 @@

- There are several modifiers that you can use in your search to narrow down the results. - If you use any of these, the search will only return transactions. - Please click the -icon for more information. + {{ 'advanced_search_intro'|_ }}

{# search form #}
From 26751e10eec00ed732bfb5f2013c605ec8abb93c Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 12:12:24 +0100 Subject: [PATCH 013/244] Expand modifiers --- app/Support/Search/Modifier.php | 3 +++ config/firefly.php | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index fe6bb2d851..3f1e514686 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -45,14 +45,17 @@ class Modifier default: throw new FireflyException(sprintf('Search modifier "%s" is not (yet) supported. Sorry!', $modifier['type'])); break; + case 'amount': case 'amount_is': $res = Modifier::amountCompare($transaction, $modifier['value'], 0); Log::debug(sprintf('Amount is %s? %s', $modifier['value'], var_export($res, true))); break; + case 'amount_min': case 'amount_less': $res = Modifier::amountCompare($transaction, $modifier['value'], 1); Log::debug(sprintf('Amount less than %s? %s', $modifier['value'], var_export($res, true))); break; + case 'amount_max': case 'amount_more': $res = Modifier::amountCompare($transaction, $modifier['value'], -1); Log::debug(sprintf('Amount more than %s? %s', $modifier['value'], var_export($res, true))); diff --git a/config/firefly.php b/config/firefly.php index 3d4d28d6d7..0172e2d6ea 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -209,7 +209,7 @@ return [ ], 'default_currency' => 'EUR', 'default_language' => 'en_US', - 'search_modifiers' => ['amount_is', 'amount_less', 'amount_more', 'source', 'destination', 'category', 'budget', 'bill', 'type', 'date', - 'date_before', 'date_after'], + 'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', + 'budget', 'bill', 'type', 'date', 'date_before', 'date_after'], // tag notes has_attachments ]; From b13a87892762fcb039b279761eb115f54f4b7f12 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 12:17:07 +0100 Subject: [PATCH 014/244] Slightly expanded modifiers [skip ci] --- app/Support/Search/Modifier.php | 3 +++ config/firefly.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index 3f1e514686..d61b64f020 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -85,14 +85,17 @@ class Modifier Log::debug(sprintf('Transaction type is %s? %s', $modifier['value'], var_export($res, true))); break; case 'date': + case 'on': $res = Modifier::sameDate($transaction->date, $modifier['value']); Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); break; case 'date_before': + case 'before': $res = Modifier::dateBefore($transaction->date, $modifier['value']); Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); break; case 'date_after': + case 'after': $res = Modifier::dateAfter($transaction->date, $modifier['value']); Log::debug(sprintf('Date is %s? %s', $modifier['value'], var_export($res, true))); break; diff --git a/config/firefly.php b/config/firefly.php index 0172e2d6ea..a0c66d2b4a 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -210,6 +210,6 @@ return [ 'default_currency' => 'EUR', 'default_language' => 'en_US', 'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', - 'budget', 'bill', 'type', 'date', 'date_before', 'date_after'], + 'budget', 'bill', 'type', 'date', 'date_before', 'date_after','on','before','after'], // tag notes has_attachments ]; From 283ee076c7b58f012ba096869e0d711fcc654321 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 12:19:30 +0100 Subject: [PATCH 015/244] Expand view [skip ci] --- resources/views/search/index.twig | 2 +- .../search/partials/transactions-large.twig | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 resources/views/search/partials/transactions-large.twig diff --git a/resources/views/search/index.twig b/resources/views/search/index.twig index 5459b7720f..5bfb623b9a 100644 --- a/resources/views/search/index.twig +++ b/resources/views/search/index.twig @@ -54,7 +54,7 @@

{{ trans('firefly.search_found_transactions', {count: result.transactions|length}) }}

- {% include 'search.partials.transactions' with {'transactions' : result.transactions} %} + {% include 'search.partials.transactions-large' with {'journals' : result.transactions} %}
diff --git a/resources/views/search/partials/transactions-large.twig b/resources/views/search/partials/transactions-large.twig new file mode 100644 index 0000000000..7502719e27 --- /dev/null +++ b/resources/views/search/partials/transactions-large.twig @@ -0,0 +1,139 @@ +{{ journals.render|raw }} + + + + + + + + + + + + + {% if not hideBudgets %} + + {% endif %} + + + {% if not hideCategories %} + + {% endif %} + + + {% if not hideBills %} + + {% endif %} + + + + {% for transaction in journals %} + + + + + + + + + + + + {% if not hideBudgets %} + + {% endif %} + + + {% if not hideCategories %} + + {% endif %} + + + {% if not hideBills %} + + {% endif %} + + {% endfor %} + +
{{ trans('list.description') }}{{ trans('list.amount') }}
+ + + {% if transaction.transaction_description|length > 0 %} + {{ transaction.transaction_description }} ({{ transaction.description }}) + {% else %} + {{ transaction.description }} + {% endif %} + + {{ splitJournalIndicator(transaction.journal_id) }} + + {% if transaction.transactionJournal.attachments|length > 0 %} + + {% endif %} + + + + {% if transaction.transaction_type_type == 'Transfer' %} + + {{ formatByCode(transaction.transaction_currency_code, steam_positive(transaction.transaction_amount)) }} + + {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {% else %} + + {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} + + {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {% endif %} + + +
+ +
+
+ {{ journals.render|raw }} +
+
+ From 5b267c0e954f6ba9aecb647c68af9ad960ca2011 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 16:36:03 +0100 Subject: [PATCH 016/244] Update to version 4.3.5 --- CHANGELOG.md | 1 + config/firefly.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c018a7db..e926fc7b54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Beta support for Sandstorm.IO - Docker support by [@schoentoon](https://github.com/schoentoon), [@elohmeier](https://github.com/elohmeier), [@patrickkostjens](https://github.com/patrickkostjens) and [@crash7](https://github.com/crash7)! +- Can now use special keywords in the search to search for specic dates, categories, etc. ### Changed - Updated to laravel 5.4! diff --git a/config/firefly.php b/config/firefly.php index a0c66d2b4a..7e28edf8dd 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -24,7 +24,7 @@ return [ ], 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), 'chart' => 'chartjs', - 'version' => '4.3.4', + 'version' => '4.3.5', 'csv_import_enabled' => true, 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], From cf1813b4134014a743797b247316295aa59239df Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 16:48:49 +0100 Subject: [PATCH 017/244] Updated composer.lock --- composer.lock | 110 +++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/composer.lock b/composer.lock index d7fca941ad..c67ced8a26 100644 --- a/composer.lock +++ b/composer.lock @@ -1637,16 +1637,16 @@ }, { "name": "symfony/console", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7a8405a9fc175f87fed8a3c40856b0d866d61936" + "reference": "0e5e6899f82230fcb1153bcaf0e106ffaa44b870" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7a8405a9fc175f87fed8a3c40856b0d866d61936", - "reference": "7a8405a9fc175f87fed8a3c40856b0d866d61936", + "url": "https://api.github.com/repos/symfony/console/zipball/0e5e6899f82230fcb1153bcaf0e106ffaa44b870", + "reference": "0e5e6899f82230fcb1153bcaf0e106ffaa44b870", "shasum": "" }, "require": { @@ -1696,7 +1696,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-02-06T12:04:21+00:00" + "time": "2017-02-16T14:07:22+00:00" }, { "name": "symfony/css-selector", @@ -1753,16 +1753,16 @@ }, { "name": "symfony/debug", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "b4d9818f127c60ce21ed62c395da7df868dc8477" + "reference": "9b98854cb45bc59d100b7d4cc4cf9e05f21026b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/b4d9818f127c60ce21ed62c395da7df868dc8477", - "reference": "b4d9818f127c60ce21ed62c395da7df868dc8477", + "url": "https://api.github.com/repos/symfony/debug/zipball/9b98854cb45bc59d100b7d4cc4cf9e05f21026b9", + "reference": "9b98854cb45bc59d100b7d4cc4cf9e05f21026b9", "shasum": "" }, "require": { @@ -1806,11 +1806,11 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-01-28T02:37:08+00:00" + "time": "2017-02-16T16:34:18+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -1870,7 +1870,7 @@ }, { "name": "symfony/finder", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -1919,16 +1919,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e192b04de44aa1ed0e39d6793f7e06f5e0b672a0" + "reference": "a90da6dd679605d88c9803a57a6fc1fb7a19a6e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e192b04de44aa1ed0e39d6793f7e06f5e0b672a0", - "reference": "e192b04de44aa1ed0e39d6793f7e06f5e0b672a0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a90da6dd679605d88c9803a57a6fc1fb7a19a6e0", + "reference": "a90da6dd679605d88c9803a57a6fc1fb7a19a6e0", "shasum": "" }, "require": { @@ -1968,20 +1968,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-02-02T13:47:35+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "96443239baf674b143604fb87cb27cb01672ab77" + "reference": "4cd0d4bc31819095c6ef13573069f621eb321081" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/96443239baf674b143604fb87cb27cb01672ab77", - "reference": "96443239baf674b143604fb87cb27cb01672ab77", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4cd0d4bc31819095c6ef13573069f621eb321081", + "reference": "4cd0d4bc31819095c6ef13573069f621eb321081", "shasum": "" }, "require": { @@ -2050,7 +2050,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-02-06T13:15:19+00:00" + "time": "2017-02-16T23:59:56+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2221,16 +2221,16 @@ }, { "name": "symfony/process", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32646a7cf53f3956c76dcb5c82555224ae321858" + "reference": "0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32646a7cf53f3956c76dcb5c82555224ae321858", - "reference": "32646a7cf53f3956c76dcb5c82555224ae321858", + "url": "https://api.github.com/repos/symfony/process/zipball/0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856", + "reference": "0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856", "shasum": "" }, "require": { @@ -2266,11 +2266,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-02-03T12:11:38+00:00" + "time": "2017-02-16T14:07:22+00:00" }, { "name": "symfony/routing", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -2345,16 +2345,16 @@ }, { "name": "symfony/translation", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ca032cc56976d88b85e7386b17020bc6dc95dbc5" + "reference": "d6825c6bb2f1da13f564678f9f236fe8242c0029" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ca032cc56976d88b85e7386b17020bc6dc95dbc5", - "reference": "ca032cc56976d88b85e7386b17020bc6dc95dbc5", + "url": "https://api.github.com/repos/symfony/translation/zipball/d6825c6bb2f1da13f564678f9f236fe8242c0029", + "reference": "d6825c6bb2f1da13f564678f9f236fe8242c0029", "shasum": "" }, "require": { @@ -2405,20 +2405,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-01-21T17:06:35+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "5bb4435a03a4f05c211f4a9a8ee2756965924511" + "reference": "cb50260b674ee1c2d4ab49f2395a42e0b4681e20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5bb4435a03a4f05c211f4a9a8ee2756965924511", - "reference": "5bb4435a03a4f05c211f4a9a8ee2756965924511", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cb50260b674ee1c2d4ab49f2395a42e0b4681e20", + "reference": "cb50260b674ee1c2d4ab49f2395a42e0b4681e20", "shasum": "" }, "require": { @@ -2468,7 +2468,7 @@ "debug", "dump" ], - "time": "2017-01-24T12:58:58+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -3619,16 +3619,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.13", + "version": "5.7.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "60ebeed87a35ea46fd7f7d8029df2d6f013ebb34" + "reference": "4906b8faf23e42612182fd212eb6f4c0f2954b57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60ebeed87a35ea46fd7f7d8029df2d6f013ebb34", - "reference": "60ebeed87a35ea46fd7f7d8029df2d6f013ebb34", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4906b8faf23e42612182fd212eb6f4c0f2954b57", + "reference": "4906b8faf23e42612182fd212eb6f4c0f2954b57", "shasum": "" }, "require": { @@ -3652,7 +3652,7 @@ "sebastian/global-state": "^1.1", "sebastian/object-enumerator": "~2.0", "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0|~2.0", + "sebastian/version": "~1.0.3|~2.0", "symfony/yaml": "~2.1|~3.0" }, "conflict": { @@ -3697,7 +3697,7 @@ "testing", "xunit" ], - "time": "2017-02-10T09:05:10+00:00" + "time": "2017-02-19T07:22:16+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -4089,16 +4089,16 @@ }, { "name": "sebastian/object-enumerator", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35" + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", - "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", "shasum": "" }, "require": { @@ -4131,7 +4131,7 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-11-19T07:35:10+00:00" + "time": "2017-02-18T15:18:39+00:00" }, { "name": "sebastian/recursion-context", @@ -4273,7 +4273,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -4385,16 +4385,16 @@ }, { "name": "symfony/yaml", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e1718c6bf57e1efbb8793ada951584b2ab27775b" + "reference": "9724c684646fcb5387d579b4bfaa63ee0b0c64c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e1718c6bf57e1efbb8793ada951584b2ab27775b", - "reference": "e1718c6bf57e1efbb8793ada951584b2ab27775b", + "url": "https://api.github.com/repos/symfony/yaml/zipball/9724c684646fcb5387d579b4bfaa63ee0b0c64c8", + "reference": "9724c684646fcb5387d579b4bfaa63ee0b0c64c8", "shasum": "" }, "require": { @@ -4436,7 +4436,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-01-21T17:06:35+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "webmozart/assert", From b9309bc7b1aef651631a58b3bdaaa0e44ef154a9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 19 Feb 2017 17:01:29 +0100 Subject: [PATCH 018/244] Stop Travis from building weird branches. --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 040654a669..df725e203a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,4 +20,10 @@ install: - mv storage/database/databasecopy.sqlite storage/database/database.sqlite script: - - phpunit \ No newline at end of file + - phpunit + +# safelist +branches: + only: + - develop + - master \ No newline at end of file From a8ac69f0086fd16e133a1bbdbb0066f8721535e9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 20 Feb 2017 05:41:43 +0100 Subject: [PATCH 019/244] Fix #578 [skip ci] --- app/Providers/AppServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d8dd3843f5..43d6df3be4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -39,7 +39,7 @@ class AppServiceProvider extends ServiceProvider // force https urls if (env('APP_FORCE_SSL', false)) { - URL::forceSchema('https'); + URL::forceScheme('https'); } } From ca5c8450641d9728e51419be53d18556c11b7d8d Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 20 Feb 2017 05:43:05 +0100 Subject: [PATCH 020/244] Update to 4.3.6 --- CHANGELOG.md | 5 +++++ config/firefly.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e926fc7b54..81697a14cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). + +## [4.3.6] - 2017-02-20 +### Fixed +- #578, reported by [xpfgsyb](https://github.com/xpfgsyb). + ## [4.3.5] - 2017-02-19 ### Added - Beta support for Sandstorm.IO diff --git a/config/firefly.php b/config/firefly.php index 7e28edf8dd..043b94c7c0 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -24,7 +24,7 @@ return [ ], 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), 'chart' => 'chartjs', - 'version' => '4.3.5', + 'version' => '4.3.6', 'csv_import_enabled' => true, 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], From adcddb09cd99edaaab7daa696bdd21be028ce8af Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 20 Feb 2017 19:41:33 +0100 Subject: [PATCH 021/244] Updated link to installation guide. [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5013f18eab..06ab81a3e7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Try out Firefly III on the [demo site](https://firefly-iii.nder.be/). ## Installation -To install Firefly III, you'll need a web server (preferrably on Linux) and access to the command line. Then, please read the [installation guide](https://firefly-iii.github.io/installation-guide/). +To install Firefly III, you'll need a web server (preferrably on Linux) and access to the command line. Then, please read the [installation guide](https://firefly-iii.github.io/using-installing.html). ## More about Firefly III From 35aeb7e04ac349e53452fe8a4d62d564f81453ea Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 22 Feb 2017 17:15:54 +0100 Subject: [PATCH 022/244] Update tag view --- app/Http/Controllers/TagController.php | 79 ++++++++++++++---- resources/views/tags/show.twig | 111 +++++++++++++++++-------- routes/web.php | 4 +- 3 files changed, 141 insertions(+), 53 deletions(-) diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index bdb3160ea7..93d8236629 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -226,24 +226,71 @@ class TagController extends Controller * * @return View */ - public function show(Request $request, JournalCollectorInterface $collector, Tag $tag, string $moment = '') + public function show(Request $request, JournalCollectorInterface $collector, Tag $tag) + { + $start = clone session('start', Carbon::now()->startOfMonth()); + $end = clone session('end', Carbon::now()->endOfMonth()); + $subTitle = $tag->tag; + $subTitleIcon = 'fa-tag'; + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $periods = $this->getPeriodOverview($tag); + + // use collector: + $collector->setAllAssetAccounts() + ->setLimit($pageSize)->setPage($page)->setTag($tag)->withOpposingAccount()->disableInternalFilter() + ->withBudgetInformation()->withCategoryInformation()->setRange($start, $end); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('tags/show/' . $tag->id); + + $sum = $journals->sum( + function (Transaction $transaction) { + return $transaction->transaction_amount; + } + ); + + return view('tags.show', compact('tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end')); + } + + /** + * @param Request $request + * @param JournalCollectorInterface $collector + * @param Tag $tag + * + * @return View + */ + public function showAll(Request $request, JournalCollectorInterface $collector, Tag $tag) + { + $subTitle = $tag->tag; + $subTitleIcon = 'fa-tag'; + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTag($tag) + ->withOpposingAccount()->disableInternalFilter() + ->withBudgetInformation()->withCategoryInformation(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('tags/show/' . $tag->id . '/all'); + + $sum = $journals->sum( + function (Transaction $transaction) { + return $transaction->transaction_amount; + } + ); + + return view('tags.show', compact('tag', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end')); + + } + + public function showByDate(Request $request, JournalCollectorInterface $collector, Tag $tag, string $date) { $range = Preferences::get('viewRange', '1M')->data; - $start = new Carbon; - $end = new Carbon; - if (strlen($moment) > 0) { - try { - $start = new Carbon($moment); - $end = Navigation::endOfPeriod($start, $range); - } catch (Exception $e) { - $start = Navigation::startOfPeriod($this->repository->firstUseDate($tag), $range); - $end = Navigation::startOfPeriod($this->repository->lastUseDate($tag), $range); - } - } - if (strlen($moment) === 0) { - $start = clone session('start', Carbon::now()->startOfMonth()); - $end = clone session('end', Carbon::now()->endOfMonth()); + try { + $start = new Carbon($date); + $end = Navigation::endOfPeriod($start, $range); + } catch (Exception $e) { + $start = Navigation::startOfPeriod($this->repository->firstUseDate($tag), $range); + $end = Navigation::startOfPeriod($this->repository->lastUseDate($tag), $range); } $subTitle = $tag->tag; @@ -254,7 +301,7 @@ class TagController extends Controller // use collector: $collector->setAllAssetAccounts() - ->setLimit($pageSize)->setPage($page)->setTag($tag) + ->setLimit($pageSize)->setPage($page)->setTag($tag)->withOpposingAccount()->disableInternalFilter() ->withBudgetInformation()->withCategoryInformation()->setRange($start, $end); $journals = $collector->getPaginatedJournals(); $journals->setPath('tags/show/' . $tag->id); diff --git a/resources/views/tags/show.twig b/resources/views/tags/show.twig index 720934948e..460247a426 100644 --- a/resources/views/tags/show.twig +++ b/resources/views/tags/show.twig @@ -75,13 +75,15 @@
-
-
-

{{ 'showEverything'|_ }}

+ {% if periods %} + -
+ {% endif %}
-
+

{{ 'transactions'|_ }}

@@ -100,45 +102,82 @@
+ + {% if periods %} +

+ + + {{ 'show_all_no_filter'|_ }} + +

+ {% else %} +

+ + + {{ 'show_the_current_period_and_overview'|_ }} + +

+ {% endif %} + {% include 'list/journals-tasker' %} + + {% if periods %} +

+ + + {{ 'show_all_no_filter'|_ }} + +

+ {% else %} +

+ + + {{ 'show_the_current_period_and_overview'|_ }} + +

+ {% endif %}
-
- {% for period in periods %} - {% if period.spent != 0 or period.earned != 0 %} -
-
-

{{ period.date_name }} -

+ {% if periods %} +
+ {% for period in periods %} + {% if period.spent != 0 or period.earned != 0 %} +
+ +
+ + {% if period.spent != 0 %} + + + + + {% endif %} + {% if period.earned != 0 %} + + + + + {% endif %} +
{{ 'spent'|_ }}{{ period.spent|formatAmount }}
{{ 'earned'|_ }}{{ period.earned|formatAmount }}
+
-
- - {% if period.spent != 0 %} - - - - - {% endif %} - {% if period.earned != 0 %} - - - - - {% endif %} -
{{ 'spent'|_ }}{{ period.spent|formatAmount }}
{{ 'earned'|_ }}{{ period.earned|formatAmount }}
-
-
- {% endif %} + {% endif %} - {% endfor %} -
+ {% endfor %} +
+ {% endif %}
-
-
-

{{ 'showEverything'|_ }}

+ {% if periods %} + -
+ {% endif %} {% endblock %} {% block scripts %} diff --git a/routes/web.php b/routes/web.php index c446a58ff9..92a108e5fe 100755 --- a/routes/web.php +++ b/routes/web.php @@ -601,7 +601,9 @@ Route::group( Route::get('', ['uses' => 'TagController@index', 'as' => 'index']); Route::get('create', ['uses' => 'TagController@create', 'as' => 'create']); - Route::get('show/{tag}/{date?}', ['uses' => 'TagController@show', 'as' => 'show']); + Route::get('show/{tag}/all', ['uses' => 'TagController@showAll', 'as' => 'show.all']); + Route::get('show/{tag}/{date}', ['uses' => 'TagController@showByDate', 'as' => 'show.date']); + Route::get('show/{tag}', ['uses' => 'TagController@show', 'as' => 'show']); Route::get('edit/{tag}', ['uses' => 'TagController@edit', 'as' => 'edit']); Route::get('delete/{tag}', ['uses' => 'TagController@delete', 'as' => 'delete']); From bfc95cfc57b980c69312772f2c06f80ec51ffa74 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 22 Feb 2017 17:17:22 +0100 Subject: [PATCH 023/244] Update composer file. --- composer.json | 2 +- composer.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index aa09eb9b44..a61e815c44 100755 --- a/composer.json +++ b/composer.json @@ -88,7 +88,7 @@ "php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ - "php artisan key:generate" + "php artisan key:generate --force" ], "post-install-cmd": [ "Illuminate\\Foundation\\ComposerScripts::postInstall", diff --git a/composer.lock b/composer.lock index c67ced8a26..011e407e00 100644 --- a/composer.lock +++ b/composer.lock @@ -665,16 +665,16 @@ }, { "name": "laravel/framework", - "version": "v5.4.12", + "version": "v5.4.13", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "707f32d32dce58232f7a860e0a1d62caf6f9dbfc" + "reference": "3eebfaa759156e06144892b00bb95304aa5a71c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/707f32d32dce58232f7a860e0a1d62caf6f9dbfc", - "reference": "707f32d32dce58232f7a860e0a1d62caf6f9dbfc", + "url": "https://api.github.com/repos/laravel/framework/zipball/3eebfaa759156e06144892b00bb95304aa5a71c1", + "reference": "3eebfaa759156e06144892b00bb95304aa5a71c1", "shasum": "" }, "require": { @@ -790,7 +790,7 @@ "framework", "laravel" ], - "time": "2017-02-15T14:31:32+00:00" + "time": "2017-02-22T16:07:04+00:00" }, { "name": "laravelcollective/html", @@ -2736,16 +2736,16 @@ }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.3.0", + "version": "v2.3.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "555d3e37009bdb78f5d8bcea6eb8a816529a5cfa" + "reference": "e82de98cef0d6597b1b686be0b5813a3a4bb53c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/555d3e37009bdb78f5d8bcea6eb8a816529a5cfa", - "reference": "555d3e37009bdb78f5d8bcea6eb8a816529a5cfa", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/e82de98cef0d6597b1b686be0b5813a3a4bb53c5", + "reference": "e82de98cef0d6597b1b686be0b5813a3a4bb53c5", "shasum": "" }, "require": { @@ -2768,7 +2768,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -2798,7 +2798,7 @@ "phpstorm", "sublime" ], - "time": "2017-02-13T19:20:12+00:00" + "time": "2017-02-22T12:27:33+00:00" }, { "name": "barryvdh/reflection-docblock", From 2082e8d46297d13fcee118c802b1a3dc2949b211 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 22 Feb 2017 17:58:57 +0100 Subject: [PATCH 024/244] Small updates to read me [skip ci] --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ab81a3e7..c2c71351aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Firefly III: A personal finances manager -[![Requires PHP7](https://img.shields.io/badge/php-7.0-red.svg)](https://secure.php.net/downloads.php#v7.0.4) [![Latest Stable Version](https://poser.pugx.org/grumpydictator/firefly-iii/v/stable)](https://packagist.org/packages/grumpydictator/firefly-iii) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/firefly-iii/firefly-iii/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/firefly-iii/firefly-iii/?branch=master) [![Build Status](https://travis-ci.org/firefly-iii/firefly-iii.svg?branch=master)](https://travis-ci.org/firefly-iii/firefly-iii) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA) +[![Requires PHP7](https://img.shields.io/badge/php-7.0-red.svg)](https://secure.php.net/downloads.php) [![Latest Stable Version](https://poser.pugx.org/grumpydictator/firefly-iii/v/stable)](https://packagist.org/packages/grumpydictator/firefly-iii) [![License](https://img.shields.io/badge/license-CC%20BY--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA) [![The index of Firefly III](https://i.nder.be/hurdhgyg/400)](https://i.nder.be/h2b37243) [![The account overview of Firefly III](https://i.nder.be/hnkfkdpr/400)](https://i.nder.be/hv70pbwc) @@ -34,3 +34,5 @@ Firefly is pretty awesome. [You can read more about Firefly III, and its feature If you like Firefly and if it helps you save lots of money, why not send me [a dime for every dollar saved](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA) (this is a joke, although the Paypal form works just fine, try it!) If you want to contact me, please open an issue or [email me](mailto:thegrumpydictator@gmail.com). + +[![Build Status](https://travis-ci.org/firefly-iii/firefly-iii.svg?branch=master)](https://travis-ci.org/firefly-iii/firefly-iii) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/firefly-iii/firefly-iii/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/firefly-iii/firefly-iii/?branch=master) From 0d32f16041e74590542d0bd80e6a66378cebcee0 Mon Sep 17 00:00:00 2001 From: Joris de Vries Date: Wed, 22 Feb 2017 20:07:30 +0100 Subject: [PATCH 025/244] =?UTF-8?q?Fix=20saving=20a=20tag=E2=80=99s=20date?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `date` function takes the fieldname where a date is stored, not the literal date. --- app/Http/Requests/TagFormRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index ce85aaa94f..41a0f579ee 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -49,7 +49,7 @@ class TagFormRequest extends Request $data = [ 'tag' => $this->string('tag'), - 'date' => $this->date($date), + 'date' => $this->date('date'), 'description' => $this->string('description'), 'latitude' => $latitude, 'longitude' => $longitude, From e21188169130e1a82ef82f65c4d0d85f2a267617 Mon Sep 17 00:00:00 2001 From: Joris de Vries Date: Wed, 22 Feb 2017 20:20:00 +0100 Subject: [PATCH 026/244] Remove superfluous declaration of `$date` The null check is already part of the `$this->date()` function and `$date` is never used. --- app/Http/Requests/TagFormRequest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index 41a0f579ee..06c976e516 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -45,7 +45,6 @@ class TagFormRequest extends Request $longitude = null; $zoomLevel = null; } - $date = $this->get('date') ?? ''; $data = [ 'tag' => $this->string('tag'), From 47709dfc7cd8140b43c576b36699c5c1607465ae Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 22 Feb 2017 20:35:31 +0100 Subject: [PATCH 027/244] Test catches some exceptions. --- storage/database/databasecopy.sqlite | Bin 230400 -> 235520 bytes .../Controllers/AccountControllerTest.php | 40 ++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) mode change 100644 => 100755 storage/database/databasecopy.sqlite diff --git a/storage/database/databasecopy.sqlite b/storage/database/databasecopy.sqlite old mode 100644 new mode 100755 index 03d527000f45cc571c24ed510e1075447ad69f7d..e7bb5c4dec83e149695645ec27c7f62c663350a4 GIT binary patch literal 235520 zcmeFa2Vfh=l`y=!L+lb%w@lGig(%4qEm1;Y1uYq*09FAcL4p)0SriC>BuEqr040)c zSx|CG?}^hL+o^UECzqaFkbX|PCAm25lJ9c)lH4ViT$2C(H#>txAt>*VjAJ=~ z<(=W~zBe;(r@Z&(y*C5>r$Wh~VLTR(_>u+-SH|%?XEPW$Zruksj{7P6pZVD!Q*y=!3G^BC3G@;4e)Jyn zPV^S^AbK5o6#-ZVap`u60?%FwI2)mDJ@s(uw7|t-hD)0fF1Dj^IZ+FjmLqU!x*0BY zhu~s70GHZoxZG@j%c0$HxoH<%_U(j=VLM!Q+yIx#>*2C#6I}Eaa4FYqfNO=|<6s=V z9o>zFks0yAcZA1<`{8EkSE9gbDR5Io8P~5j8%+(PCiAGdY1C}An2q(L{z+eSBA6J3 zm1p!~oyByMKJPvAsHxrvw+r7h-c+7_&opYT8#SAZ_4Ve0_sqtdr0jdfQL}l}*jV3W ztSflWWIQI8aei7qXB8u5jqq}0SAq1c;1AM)mGA{eI?Jj|DIXO`eJX5LK9&dkK(Ns1Pa|2_O89QqZk z#-Bv*KyO4ZNB5v43Zb*;6xxG~s9gBH@Jr!egeQbg2;;(lAPYv}&B7i*;{SvHF8?L| zV*p4K zvBajGXdfSnsDDd0>_GLrA3D}VEIyyxwRdmZj!Znkn|r@v(~W?_mrVNnlaXLFnGp6? zA%{>51(Y}&kNU#=&$m>f148b&DcgVJ^{77gR_5h|px?6%wF*m1&b+~Yb;k{;g-`k> za_g0O>MGxgD)GaFZu52M03V+X2S@2>6}D_a2g#jeJTx%@;{%m%Mpb(JQexvKpq29a zL_9V-lc?MXB%XoS=10eT(dp6PMciEdruESN?Cr#c?Hf>Y&W+J{a3+`xDbvG-jqA|f z40%JrMESZ3w1W~gq0{S86+adVhZDl)a;U1$?~lz!lcSMf(udYdXjkS=qFOJ)tIB{z zdL3lrF{rRmSq3%CA?s*zekPbG+m4_@xwrkZaoioC0@4-%D3B>gd0oGehdM?=6LFtX z=L$Uj_59B{;J}}U`cWmU{jU%_!gjb-`aPQ{a7?e`ES5@Gh#PB-=32AaU~0Bn>a1p1 zjhEay74t=#%P~Mrk*7s&bt$uqf%df%5(X`T*t`_f+!l-m$3y;*FFv1vZxb&s}AEhTm+!VEjJ$Svsu_O23W`3OfZ^IVVe+!Jy@iWBDY!- ziC{8Q<3L4Le;umf^XJ9#DrDh{%y-fbU~!h2$HcAZFkdh~>F~XR zd8Z6>J9+a7+6-()?mQt}hYsrt#y?*FCBcf<|10POLcrr+f)rLdWp+L0(=FlK$cMQ>3(ts7PhM9OMf~CpnAV`$6Nmy_|Ns851M-0_O zT^bnmC94eq(1ayJk)Q$v&t_-vo#M{|!EkW7XZ~0qSZ%oIi-UGITMa-M^+k$5NX$pZ zV&S4NfuJ91A0C|v`#_~vq;&x*z#TLnthuX9s=D1;8l8=XF3bk= z9_T1-J+7{PkHgUAvO7*2s`FmSg{?Nkq6LpJ;juQUx<-cnRCVwmpXyOQaUPPqKg#ky zXx_7@^*bu{Uh=JK`UJ7+@L}GoG$U~#4140Ei6D>;>s9mrM0rgqcz6hZ@*@CQBk$*> zs=a&p)K&#&R^^t>33-^Rvkz885f!{>w;J;${gX?3QwS^iox2C4ad_`2u*;0sp@~pU(K!ibxCNZ?^aZI^&xU$9ypPCxde^>(iMZrd-&o z2u5a-MJ7&o7!SfO*~MUyH6!PtvNWh`3alp>tGf4s*A4Yyw%FWGQ24k;zOh288m{E! z0p+t{a*QfXgWUsCp=`#N993W1K*>m_XiaeBlVvDQ2$<8j>YO}os51V`kby)0fL?}f zgG}jHqCklPDg|1~M6Bf~vX4+q#nB5}0iozbW-p=+DZsn5=|={!sR8WeS_B1Zhc+mC za3+=rB^!7JXsY} z*?O=j$jm5c9T+0ei9*-SR)D${r^um?kD^`f?jw&x^ImTKa%~C{U9BpGjY%RF@J3o?Z%+*8iuM-lft?6nG|4 z0I&Z7@^I*3xRib+3X~|YdI~rN35*6S_Y^Y`EMlM+2u5Od_}}0GYhe7pA(Sxq4EVMI zhXEPkF}kX~T}5Dv+=^X_2IB)bs$Vb8DG1}U(S#v633x+MP=ez)39)fQd%K0`{~_L3 zBfpoIgJ~i3bk4a|%r^>~e*@al z;?KY#H5i(RV#ldMOf`mVoFPL_DT!*u39ANs2@xMDgWs3%`vTCxq6VTt&)hkJJzlc~ zWGa%6nvjPDX|B6hVYPGGTT^p*_+6uH|Bqv%#h`-GEeQ5; zC~%EZ0I&aL+&4MyoBS<8qwv9Nv>64}4pd52rd_-&5mhhM)Pk;+8d7JJv)hX*?lVub z?-R{^<~~*F*2r_)l;~HB#}tLIKNcMiP0W(W>Un?c%vTNPJ{-{RUGHHfdaOxL zD3{&&q*m@dtt4LFIV9tIk)zbl5*Y`mi;RJCztDw*==X7;AW-=i>;{a5WB%!4;f!D_ zhD7;3Xgmm8AoM*Y!d5;KCH_yz|F>ZJDE(QYz~2Z3O8o!d$cHX9vqXU+6u|sH5(YRl zEBqLe(yv5;)lxv-h}MC4bLu{jtb?^Zb63asOOo%;=tL^R_n6b@Yz8O%36ZMDx+O=~zS1-#6WE@!9?LmWimx zGwgIubxlT^`g{8u3;e#Wgh6&=n83-O-?2&f^N5Tor|uRHb?Nw6AT-eEL#i`eg5HXM@K$ z*le7QoN{)Kk4*(HUg)|I2z5=zTZgA+F2u*$8!W^2=rBH;7olI{Z$3!Q=3VXQFTs~6 zP@=#ynF0fQpmy%9aOqdh{}s5)9Cumx*k7y8m+Nm-3@h(Dt|)V8aCkU%RXHIsu_s@p zlezu0YIUv_{(q?%XI8C~8|XgY)HD0evWmBhp?Go@jsl=3+f~Ck1(9Gp%pgAUWB08$xw72bc%;eHEMY5tX9Z`wL%cvK0ZGKhZ;P^%UFe!XSkbbaK7O# zQMD~+xLbgtkS_VCa-S}5U|f@G-7ZxfIKW?a zy25rT_TJc>_{eqWLES&P*t7Y*GTAb2qU&!a!$ z7M~+Yk;a#9l_*f6z_XA7x2?nLzrc-h+^CTF3pDWZmK{n+w|hehiI3OT7jx5946S$@KA!=bTzSNB? z%h@aYy_#eIgZ}~2h_Zjkw&;JtHXHUWZZ{Yrn&;Pv_^`OndH-y&+ zqr!gv=lr|)1m6UYp1r^34ds}r+sl;~QAroMwE^zc*Ba|;ji%=HY@q9NK-aN>uFCT^KL*+4NhGh~w3K&2c|kquPL0oAdA>T*EK*g(s&pjJddi=4t+j3-A~ z(76_Y!U}M+vDRp=HJc5lW~-&nYAyzgU!x@EomL(L>-TaRfwQo}zbfm$5aCcWTuQ$Z z1^$v0xW84Xgr3Ujd5?3^v3MN*gDa1x8ZCXkJp?fb!t-5SU1yze@?z zWoFvde(B<1baJxaGjnllXlTwAzH~9LU~KLRT?z#v?TNs!C)6A2HoDC1vu?9#b`WUv z{rhj>Nn7;L7TOZ+ZPC({rn)9`ps~4byuR6D8aLKA1p{?vv#F`E&eYg2-fYP>W%A;P zV=mzece`B1$@UTRw9$7uV0I3*NB!-N=DvZhL|1fVa;$SG+%**o`Hc1b!_mOWA!x?; z@c8HXGzb4mziW^JmyhpK1g!g7i4HuO^!X?8S;q-D2{V&Dkk2;fJb7Aq%C1q0FuDDblAh&66r7;ka{DR}?~ zk>WF(b8ki9fax3r*>V%11$a2`ohUc9CQo(LLlmvk_pC-%!FqXN7TDVCY9Ib zxaSkr+ynw|eiAJ^R zR~2QfQVAxIJYC?5*MFVxa}IqDy%?QE)sQXyN)&kZQJ_($SC;TynwRcaI27>BCz{F# z;2yokHNPW1DHM*%$k1%4aHZxdeqx7YgRTAaFjFGT%6#$O@F8-zoVP%_uV zY{`uy`GUlf=TEChL4=!8QVFeAX<4OOFd8VXZPRFzR#<$#Ra)-TtJtNbg4I&h-o5;l zN=0n*gN5Wo3=6E;+|hiRXCFMR#9ry9=CT^pr&lV&OXOrt z`D|j&lc$wW%tUBpzta zGyM55EWaD%JbA`(_ z|7&apC{?#aff5B)LV+KN0QEy2$N!Yj8#we6^eyxT^iK3q^ab>rl^`s^lqgW5z}iw^ zJ=(>OhoYgxWH2CYLi_lc+6-$K6gQ&1@HfN-2dl-2gbu%(`J-$D+QX}FBK!zqD9(g~ z$)K=e@)3-Z7)gf66j)p?Am)#`iM=o@tl6Z_pSzb+X&+9iB6d41oV(wAJGAbS=0n zGofL}+=w^Q-r(vOxe%~SI$dTzaoUTRCcX3U?v!h?chFNmGVBVuB3_GY$}#U89+`9w zJB+R=r_r_G3XRxZldg__6Fi@D+NbNCW_Sng_YSv*ybBI95j*!9;5~Y?z0LNCxFEB?5dXYVa9mAlhWeKMR&h*aT>H<|rQj0=Jvv zcJrTGTFJ!&HHsvA&lVz)0qRi&E`jmVY-52`mVNTHO0p|8#4HT)lHEmLHq6T!M&VqW zC@z;9Z&m^f+~-uklk$ZU%}c)xSPVQvf~sa@*rFo4+KZMuHsP=T-5|7QY-V%@o6;o` zpo9M_oTHE(hL}wG+Lr9ItLR+F`oGAb#igUabf-jt5(WO^6u6yVuPpWYmJ1MANZUq) z7q9K(p?D&x1~b9KeQ+|AP?mfHq`s&jIOoH=en$=PzQGSS629>unE#Ts(hy3L^&g$) z&?9gu{Yn%lQD99d&?8{}U4Nj4*E*zO?|zi}FwLLBVdG)+&IF@y=)B-ngaN&!S@Zk= z+erW$7|mf}+hVg5lSu=dv}m{#i-!Z?iV=$#2>*YE<6 zqa;=B3av_i{&)65Ic?vIMcKSCQh z^Z|4RlG3k4fxlr2^dM!`KDSgw?lDn~;JI~>s!Fto)kg@VXSSb&;V&B$_A zYrr)KE434N_TkBxVIT$zHeR}i@zO1H5{6*kLCp+-qHCcmE*!5Mteqn9(y{$NJ^$-` zX%Fykxa*gi{x?X0QKabqgjNpzm3}1(uv1|1mRhN*vXZ~&j3VVFeG}lWHjy}>E5p<&byr-wPvt*9AUP%S!$_CmPvO0)^-5fc6^{5L6ry?d1)N)%XY z3jCkTxYvJcaRIL1Sd78-pB4jf{l`TwT)({Ng6kI+Pr~&d7ID11&n?!%^;3)0a7`~( z!S&;d>*4y5MINpnN?nBO`%}|!eP8N4Tpv$);QFpqH(cMDx)rW(OW}YzkECkg`cMjX z+qpNTw!-xdsdBi!PQDYa56E%2zDk~e>&xY{aDB1d57!sUIB?(d=D=6g8SAVJk%RHR5Gi`=`lU_B`E%C)gRC?xV}DR&P0C$&vQYR$nK<}&jpjZkg^$Hz(?xF69qb}cw5cs&Z5&ku z;H9DV!8~1SJS!z=e#T2JkiXc`)~S142iL@GqQGY%_tRuPZN7rE@Qvk_r3I)ny&)eR zzFPzZYy{9_VDpyu+`@BvPCm{eZ$Z$sYQ^HC2Ky?|tv|mHBB=#}%K1N~_@A0)zS2u2 z3Ows5faQOI`w7SWL`XgB8kcF%;`U?8PUMwK3eQ6Ml+g%;4a&{8=JP1IkDgZEBzF$a zRAQNpNj4X~>PcT>vbYEv#J=HyQ6HRAj`@ZpoLcu(i%?G0mS2PDOIKNeQnwqWs+Jc1 z%7%;rVYCPVFpp8>_R0KDw#-{A`g$1GGE^^8k!4=ZU%?5+0%Bp!$a2eb0UNPQxdjMw zIMk=6J|6!r9_)YqfPRgBj-Eu{L0< zg&v_zXcmmZL1DL0DXbR||3Cb%`JeIs%72spGXEL=qx^gMxASk}U&X(Wzrx=E=Z{bE zKB(L^_>){^5LZFqe=j*n=6BL5lKHLFLoz>>x=H4TQWwcQDRq*}SEUY;`HIv|GGCS) zB=a%JPBI^nPLj-rr8bgzo774&Z;@;y^N4gS$-G=TK{79uj+4wwq+3YlIZ_MB+$C8_ z=1!@ZWWrJt$xKO&BomSvNanOuPclPN9m#kk3(43eGs(0`CX#8Dj3i@}j*-k!=_tw6 zO0^`jQ#wL2+oi)KbE9-K$&^bqBqK_PNJb|eR5ILuNjH(qZ=?ex^J{58$^1atM>5}+ zs!8U1(q5AJl4Ky6FGzbx<_T#x$$UuKMKT|dc9P8dr5z;mu(X|I-YnfnG7m~Okj#sv zDw27jw2fq5AXY+#7w!tCG%(*D2|wxJ60F#m!0@6gMg9ptw;<-Qor%?HAW8 zsZCs`q$k7*B|R?cmGp>MuB11Ml9JYlqLSVq>XdYwSf-?vB2v;aQBYDr$3vbcOOe(4dW|QJK%bkY=`TeQ-uHj zI>&uoz&gWc!*6keNvdjX<*yu27)ly&z6kHM{1baVTeUH%nCx2Y;ne`5qY?*JAAa)t;HnX{_FW4aj?sOFYMrN6230H zN;o6zfm@~Dvx5RweL1+I@Z4Trn{OF{*Bami`dVZ2A**uK2ODgCt;t+#Y&v9B&iG)4 zg>oAYS(T$cw8IKeXDHWLYix*FmD4_0VU5kT#`>65IrM`WwyD-w7qcoyf9ScrxH1}o z2`W`K!iyH}w^rrg4^~+CN@ni2ElLnWsI$g$39XFv@QJzKV*Z~OZsg!!=~tpamI8|b zi*f|oJ!H>h2K;>HN5_28=~1vngQIGe+~~;X&zC%38B-n)!fr&-eTnMX8N5?b{8>)v zD`n@2$waOZMb^eb<4I`wwaDlGv(Nq-vRTH)K(YU86K==f{mBCRzi+`#z!%YH(I;Rp z;C<*Z^j6pncrAJbdJ*ggETTKnCD;*|MnNwPys1HXG8wx;{eeYmaPzIO($-@NxcT)%#AA6&n7uMMtWy%$@)edS&QT)%Yh zX1IRwUbt{i+=Ji${5|L4`q_K%`=7qY2G>vCgWvzeJqEab>>m97NAH0P_u(t}{SRI_ z57!S|!SDaw6&qaNdj-G$o+}2pzWWM(|IsUO;ofl>zyH?D=i&O6%lQ3=FWcby=F9l~ z2QM4o`o_!n{nuZH3-{VZ{Qhee&%^cAi}?LlEZX4uvPJy|?u8@w-t-@hkigX@(Pet$7#fUBIs@87NL&)*^A_vhsEaJ?ww_h)4rToa0A;04(L z`IwB~kIHc2!ua*6^KhBOFHhLu5;VeP%m5ePX1xAapm%WS_vlw}?)Uf6H^Jujv*=?m z;@<&}O1}~XN)&iTP(Z>Pe*!AoDB+zy$ZU}C)}J7ht(WlLA7s|y%|E?RR)IJFAk|BF z^A8@BOL+GWG7{eYgD0X0+kcSii2fg@3R!s5U!|Fsch)JkIrrX9%s;Z$3%Pd0iLQL^ zGDn;L%<(Hx-9W*56;GB~TVwKEcWT710|?&Bv8DM3yr!QtW?s3%}>kf1_W*89?7f|BSu_y1vI|1fs(-Kk14i4oydk7(NS~&?L=Eq1>ym?^ea)|8lwQQ_bDr@CiXsMWqXOe z4=OVddmmJ`huZs;?WXoVWxJ@oPuWgt?^Cuz1bZJ?N4H~hA4qSMu(=N`&NoQd-Ul*O z5;pjOrFWZzEq)+ViA{ds-RrT*52RZqZ1MxIUMFFjAINNxu+a~^x>>?jKakn9^3@+@ zR)16`N!a)Y(1{YZ{(%fx|M^*tpA~+1jeWvvx4NmR2E~W!GSRK6Tf{k2cCmY>@)x#Z zudHZkQxO$kxvF!jrNzuO7hV36M4zm1$9shJZxcnRLX?H`LEzx1y{wqyX60K?am^lnwKcBY6|E8 z1C9Ul(^CAORg<%XphSUdhyp*^K=gl~=lGv-pMM%F&SOiYtNi$Zt2`|7D7Cv;iA}jk zw1`x{5)MT|$wb~Chx3@eyhnyZ5W20)HR$MYxDBq}L4(V4>eLZ~U*+x$_SQkiV3*Tj zushmYJ*P5nWH+LhdxMr!6hG1DL19jZyX)j3lktebSd&vegwu%=XMP2H&J!iTD{O(2 z`}uY1b9Xsg-CjeF!wb!-skvO)q-^Kk>s7d5-$h?2tninlYKbK`dh-ZXz2x~bhEu-U zDFV5YORmp+F`esG^s3oUK)fS!wYr&)u;fXe!f&~XUWH7Xn(2VLatd+i)YT? zK2bm7aFAnHqa%}JokQWSsaR;Z{nACRG3f}NcE??V-4>s@IT?&}cML>a;lYK;$kp$&wj~OQ{!H$?^I6U3l6}l7}8*ZP%cwB?-kTU|uv)bE2y>{m%r@6o0 zH8?%znsQC`4!S2@Q=YnB`?P5!;+%I5PMCU!o%7y>*7}jb$!R#gb)k3AJ2wKjM(q6- z=YqrJoN}0ZPj)w__6U?phszfo+NlI#D&frNCDe(8FZP)Gji*mG&x{S5db~rIE(W6E zJ|~2r9E{9``Uj0ls{)bsL}1tx>J4?D9&}9h3`QN7e4XyoW1TazL&lMjk*IsvGw3x2 zPCFNdEJI#@`{cCi^vGPWbF$y*3Dh|q$%}mhU5PIBfZNu2@8Hnn$W)ib3kTc6VYpM6 z3NCZkrQX5rsgVwEy?0^4zu|ppSwLU;BKFpbcWk!o#B|V#}eon z=t$&sAWWKGyF1bgByid%>U%r-@fpBVE_-X8%ibOKPW9t6eT`0gf4#G#Jp%7T=b3=B zem!QF-DB<@bO34mW*|?UH{z)WaxHiVx|{L(zmesDM2&-}q^GW<(6=%`58!}m(WK#241%Th#wUEy&;o>6 zi$$BZtYGpPm<)!)_RSRX7)w23ksw4^35TLVgVLJzO%#iT0ZV7l7fwzZ`ogmbLsv90 z8xO{<8!4V040u|@p)uc>@74_z_5cIyNjN3Ka1v-5_l1wIrwH~kAn-+e3$f@?h~{&8 z9R(j|03Yx@RNM%?*7J2&+973m69Q+F-wd zVq)p~Gc(-#I|AdoPegk$&mVCW~h zD=C^52C8(#gAhw?G6r*>!4VGn;eY~vC^>)2^%NJ&_i_5|v8XQ`NZ|301S2Q>AK@}2 zrC*5xSEYcvn)bFH?LAj?2O;h)NM&8oP%`8T8`^wfUle*VwJ#dnOL6ux;Pgb}SI3&# z2Z!5$0Lk+v7SRY5a{h+XS>e$Yb`H;-^mlYmyF7EAp^l_62qMzRLO2}k2w0Zz{*f7v zD-uWqIs;zMwDFQBGIz;6eW`BHF%%maZ1;Ey zTb_6nae=tg76Gvc`2Y5>%j_{arzR(RJGv~+4o9797zCh&wka$cdCmQEARo;2c8r9* zk?u*S*$YBt`!tAB^&=gwY3G!GzSj=0qBK&_1=KquEsPHr4%@C=livKEqKYx<@ zGxq^5#^Io)9B1FCc#-jY_iFQQDhm+*CU6BaWpd^w#|8z9pcM<^{viXLhh{w+8Ug*R z7#jOJj7G1yHpMGruUHvnz|oCT##C!Df$)yK^fcE+rAz@D;;^R~jY^q(GVh4-*DFFD0A#pZx+GIDcj4~?I zZi265bbxAWB|{E?GXz%97}?OMl*vb92W{Uz7~oSF%T~4~sgx-|V`D?3QYIgb{nnLHrU1){l~IOH(?A(>t-0B8oUKVJ zWeU*T!iGkrOg= zN{_Q6?XYI{vS^{*SGQd>M|gp_VX~KDKse$XAHwJR6z}`3lh3M^{9?LM-Q2 zMj6V40c8}%@;0_6sgx-|bCwN_N|}5#_A@J^OaYdW6;URm{D<+Y=#0E#f|PSy zU@0dQHN@inkYPL&4UK_CZNdkqEGI*W%uYT=T`{C5s2iD3chLwx6LB!65BLmlLUSm2 z^at50>qp87-dmaQhGr785x^D=M!||Xk);OueinE6S&862$%K1gHZcPZE1^UpXn^oU zNkbq8MtVwCIGx2Vb&CZ19wzLr;3Wf`1L~jTZCPC6S%@)#I|y4%{jq2QOx%LW*|@VBudo-kq{lqMv|^uOI%+-2Rw1BG8H6>{o{x{060 z3UJL}6Vu$UBXkpw27WJm9lhour+gh4VsT%_7zTHKJpKh_=g=GAQu>uBP@(`c1x|w( zEuP_yGtEJL;AaBv48g>O*`Og1@+V=|$J5*G*%@fV`Sk>I57RuHnacoO5PM7jVlv5r z>l!S%xpN8N2m4>{bDZ!4{-=;!qrcQHv=M&-{o?T*dvP0ph6;Opbv8sb>;$9Bk+9!1 z=t%bX!J!MxEfqYrkgC_)i*Od9j^CG1ww-z z-N_-3+dCXd7~MnN-EQ+_`-sEk7>G>TM;45sQzyHV-eD*B&UJx@pLgCh?6~CY7?}d& z&Pi8C`=ocuUk{F47T1Ck9JZX7ToKpQ$W*`485s(JL)Y9$#9?wxP1lWdcq}8so=aZE zF|4a@#O|Gkd|g-6cpQSmOu}jjL;#T!cv!Q1Nuc!6;lrPp=@V&p`aFR)uQ}P~afICi zM(TdIyy8Fh=OX(9>Xpgxf&Aslh-xv%f?H<#J$v5npb9Ok*K!ZHTHt>Q32emL5 zusSW^^9Np#?bxpm3|#&5F1rI@y3F7kI6X2Io^lNjMO+J$Q_kTolXI|b$~71WyB7TO zUUQepJM0R(?A|)(F!&3CXC$RTY8St%o)Mkt>^&Ve1=_m@M?B#~&@(waJUm_JvrmqU zfev0V)6MZ0b+yk}MozoBy{7iIK?~GxVcHlRbPf6~?vWA8Oni8HXsUmzJJxRuoEox^ z7zf$|9RZ`!I5?QsGwNIm?Gfi7Hr4GnInCIe54;+u{1)%jMBT_Bxcx2o=UfZWE2dg6 zIqj`R@DDY*?EX3DU^wiGIKV8}Yj$;vgrJ8Qo#w7N@6^cTlAeKOBxU{Q|Be&>1umuE zH9>*YeqE+d*Q{i=n;Pl&54*i?^GssIL2x_&4+(l+3j zx!?qE-+_qJIP7q{`lsAcw|UO#8w$+z8^gVB&ve`s=`syi+D{HSnrDXd`gHfS6Go~F zO6i6E3x3YHAA?zXdkAJb@Z9wpy~AEh?{GJmx_jzf!(DYwFkc@5KW8V*jJ?BNnBk_4 zUYPwx?AQ$710KGvDcq-hfs5FipwBmt7dvIr!&=&=RCQT~Ob1p%rl==A7@4rRrregH z_Hh4j80LV%Nu%2|dD2^C=!2K9**!hf;557K{vnss*D;qEF#Am&Xo`P&sB3U~#N~=k zwhy;=SpuGs8E5Crz~JfbKBqlz=!0jlw;sHD=SJ*)Gq`)tIn7QJ3}?CX!eTKr-3#vGVCfIjhsWfN428kl7Z#^}3ntT;;&cJaLaUz= z39k%z{BP!lIEecBDs&bZgr5j+7ovih|8F>r_c^?s*K=RyUJ5TT{j8fS6po1JB#jz@ zE#fnldgfw9TW4d% z@tKFd)x3_iSTRjOtre`rZqnHXU-m61_u*zeYq7dC1+|v57F$_n8~oKnQ0|3hiM80W zGzGPati@I#oAdgGCp+GCeY1|WSd^xq)-u*&MZso(G8dlgY(}ib3TX;z5lFFmPS7}j zwj-yRC*^4e`qTT!`0wN(=KuZ3g{~96CA=0qfcNu1=O5$ad;|AK?i1W)uJfseztzA> zr;5$C#-DU~mRH!09U?t>D{HY8X$oq+j2kD(z9{R zkeQm#Y*3Lg&VcG#qQn?dmO%1_R2tUztyhsNnjh`!s}puSmo9xKMgO;%JD=tMzX|;R zxM1Y}$bXi9F7M*Ea^K=!2QRGfXEn02HI+Bm2EI;s$78H)%?)V^YCXzYY*m%5yB8Sc zPrazQmbKWbGzGODVJ%kNW;+OK04y6HW-WGGnu6BdoGDhbv{;FpmYPg~TA?cc^uD@7 ztVIdebI-RNWG(9Y|4laElmXJJMA~Y5^-mQ>+UVz<~%_=qICm$ld}X$ngC|8~s(dxYN!ZxfvSANWUk5BFQ{A+BBPpg*%$ zotM^j(lghZ>iY~T@->+B0Hf*L9u?!7iz&dUG8?R^Npo)kog*>In=($0y zEbI5r$|;3tjZN+<6)oc@%Lpn~MME#GZx*hJWi7S>{Srzxn_%37?n%hqU9X7T1`)?#<1DX6uHwb(5? zZ9M`}J2tWwyE9Eetqsh@?yxziMy#H-*d1vKYOP}~cDt?p(}ZKSuok;LO+l?@)?zo^ zXdAjkVRxEMti|4#rl4f}4`%uQLjC{$fd2ov+@MxY0KZq)muZSt#!@&0JYsLA5n7=R z0gvd)0BL3P4%!~5RmP#!nW?2#&>aWU8Q4>-up4f&9UoVI%{{DDz9~&Xt=-JU9&}7P~i1K?(ow=it2Wade~bNx?6y=ideP z|K;2VIX}y&U-n)1k(IJ7Ia*tYdF-DgiV*NKZ|Xg);ylQvq{^^D`3M`_H>(I4d8-_i zWj@Zjz8Z>CuV)loRDESWM)TQ21S2Rb8Re{{%F04s(A?xXsA6Z-TIXmhm!PdXdy`69 zMrQk|)RiD}Gz=Y3v9FoB5(ExQ&wdr}YK1LOSMFOoWR{-lHA7}n;{R`k!Mn)QP!!UbM#vB%OB)Owb=*rT?NDq{U}hPBwEX$oo`VJ&u3t*9j zbAKK0Qd^pbS&KcArl8g#)?%v;+qxbht@W@LdpJ!&t%Iz^iZ|O1{*>@}1FXf~oTi|4 z?o6>-S>f$9pwI2k6sQ%dhS>kC<3>32Fd7p62xEUlK>R~|KlfkUTjBO9em#wAG)-A* z&6b{qHA4nlH1#Sn##QtwwrDI^TUnaTZam}@rn3wXHRs5{ls^yF- z0Bu8$NkzN51r(5(-9{A|BYRd{TM2!t(R)nAxt7{W_+*Wzj;g3vsjY;@H;&X&Y`mVa zqD8coc^sw5xMuplnaq@>Rhq77vK^%sak0$Ar4_a*O+l?u)?)RIwx%urer5NE+|3cz zVjI&G)EZ_jw!FdCco<6U|3&jOYq1S!3TmBVEmo?x8P|UaeqU}5u@+mOrl8hI)?zE_ zZ1qO6t2x11Y+agyTE|(7)mvHEdQSo{tUMNF8)va+xTG~!TRrA#qxiuZLOw2a8qs^X`8ezPj#Hc~ z*H7eQG<$C$7(qYL%%+xEs-MVPsLW0778Nz4m0pf=A|GenS*uFaHP%c3f6*}1tYTj? z%>*zPmYyaR?`jzrm$9-I{az(dfn7& z^DGcyN2ZQ{+JRfs6qfM+u>L=y*#AEU_J4om-@x}n{I3tfY2d5<4LMa_WzBdX;Mpxb zr&PSFd4F8+sfn+SPU@&{W!0WQ>AZaZM$3&_?e`6(^$t@T%@#@W~p<`VaBH zmRtY-1J?iBxvRzotCJ0xSyL+~Df9tTD64+ITEY5&E0l@LFGGBQUfTmd#%tjot4v&e z!MRB@U)Y;gftj6tH@IvTWrl{;klE|km8P&(nc3}kgVWZ6wSJ)XlbQW~ooNbdmDwGC zPuYxZ#PCmM_WV7Srm$9--SxM}W>$p8nr4~V_qQiaVXZQ|^KZAU>6gR`M`rf^?M_oz ztIY2H+hsGopE4fI{=Z#m3aje>-i%zre+mx?{er;1haUp_pT}0E|8t*K=d3lE2kY@X zIIQAa%XttE(rG+3q@rHsjt#gfH=g!TY`ngaZQU-qV?$kFp`OORLA9QYi*}B((&of0{2VcpkeR*wPNylXRc3bc+cIoxe>Ly|yFb({GyC}srzxyeW_I-3 zGGyx%X^WXX{f5#M)+(#6ep)%0O&;687OMSFJ^r);d(srvDj&-5cv@jA2W^8dCi_#e z>i?$|crZ<2tuk=~WZ>DV25indsS<)X0x~d*18E9tmG`r@SajQ}sUlEj_5^gNDQumr zx&mtDi?{cKI#*V`0JVbkXY7B@;r+j}!k>h92u^|H-^Y(b)Wr92=T@PJN0)x>o%pQN|Rbv#H-Q{o8R+pkeDYMVtXqv)WWp<~(bGF7Gk&Pjl+3W9In!;LT zcDKLVZ00Xe-7vG?-)(6MYn7QD|0>Sfn$}ar1GDGf*))Z<%FM2R`ZKor=P1inv&`)K zcP33?tunLoU-^j5@_gE2X79g|G=;Uw%Us- z{Qqwt{^#9b|1WY+a4+Ecv~mJgS#|!?%IZ~4+J>kVzO4EHY6YH5Q&_7^Tmf15#0lG} z3)nc4ksgU@#2`-)n-S!VYC^Q9>)(fkU$rdOf4utEw#X5SoVr1R>}w83of~hLu@)x{0j+)c?;5CxHC`_5db? zoB3bzkMpy96Za=}|9^{oKC@Qv8lx9i%v^7lRo6eQP&2=<%FIrG`k2jBPxnojz5Zfp z3Tu^_-Tum>w#LkEAhX|JG)-ZxGPC2K6tNjIEoS!oi=-*6Rc3bm+Y+|*G*bH~X5YVX zn!;M;p-gwu$`x*zw%Ms8fXwXuH=U-iR+-uTZ_AXe^8x&OO|#7G|2LJUuohW$0Mt4Z zHii1kvg!b+6)N-|vi?K-|2M(@->-#-g<+wBe=k1>?9*FWqyA^PfZq8vnE%3bn!*S9 zoSswXKgJEUrSo6z%xZ4xzocSjR4e7EDDzO(oxMm~&#^|AdMbx^;3^u1W>xHrOF=R0 z>Pl_tNve2P%T9rgGO>2ZEIsixLk2p^3o5d;)lmYsQ{OkEB4_MO#dMT_(R41RVq9|_ zC9rGunrCqDr(8Nr)5v05izXg(rpY2wxMPfLJ0Q79IzWq6dZ72rmk>oCA137f=Q?m_6j!&TZ9VmJpL2J9rz{xZ(x1=9sXiml>t zoJuzF7Ul0-#TMo76QWf~kBiMXEo%{*a9VB^8FROPsH?+CgU*7JdYu_3 zbvhGHEIK1j%(`PZF^NYpykHb-mGqc+L`jc|hjA*_iZ|m_IwIC6e;*bPDSzKA9#qmA z@g|&>9TX4XwEQM#A|GPq!B*)j9)C_UiWF#Gu=alRdg!IN7b+iIZL8 z4h%2s6t^qs4)I1M-7emMQ}ITz3a8Qy;x^^)DzQ@edz*N@l2(daaay)jybh=3*NIz{ zbc=2?M!#9N2`8I$8*#Exw*e;`bn9`lUbhY>>vR=3snF?hq8H0CyihJmN-Bw>l8T}Z zr=m_Q!>LpzBIR!+3d-Lg1j(&{3hrWTn45xv79K9bs2IK$r z5EtYH=rXzkB~cg!;EbRF)D5G*5!J%jzY%RhI*1$aYvJF7?+ITQz5t{CL&Cd-w+OEn zUM@UOkYU6}Af}KP&J}P7Eil>-3cG~sg>_(M^auV|Fw(!ze}n%bjPnn|c_R`WX@Rj^J7~TKP{RfQg?{NPJmCpPm8G14=2;5gB89FmB zh}@SY8TvCXNZgks8Fy*8_eDv@of__aL6UK|hTId9j5{_E<@1t^yS7lyeNK{b=Z4&8 zMH#xcUf@0>%Fw?d^J!6r4i1@5i8AzX$b3?ip^HN%Ey}o$3*09}8TWF5`+HHw{aoNa zF3Hf-aScBv$O_E_q;M`j!8Ab)py+x8?VBi`)BFQi| zaJ?RuWIR0J-b0d%2MFAIvn1mo0=Wkz84nVu^&2G_4-=^C8zdPI6v(|^lwqhqv9A+l z7%Px@tti7_fy`?}8Ac0a9uQ?1E|7V(DC6-WaIX?&JYodym7WdDWbPGZPRZOO-mRop#JiOAvUsPGE{b<3X-d3Z zNo8pP;}W>%i1U!)g}cQ$CA~|$gj4ZO@gh#8JH%P#@7u+s^7n$6P||rZj?=PB;suU=;Y+p$-lu5Wtuk@>XVDE`N~^%kPX8M&+L|+aL(E?P7t<8hDl@zNZ@NrdqKC`|Z&G)?$_G)4G-Aw>9pVUqCw!i0Dh&$+_5ct%Ns;s{R5#ze1@`iKm0 zp2z^BLb_D5;Z(0H=ru&_hIkZXyD75fPwM2mBJ23xHX|ashBkST4Y9 z5|8YJ|3@bY|Bu=T|1Y!>{$H>W{$IG2@c+UI!v71$Mc|Eq9##=pBS@Qx1kgkzfJPz# zG!O}(o=5<7L;|qrfDyv-0B}KA9>6RRkBo%>N5=^NkB$=lU#KPgzi@=`|H5Iy{|h%0 z{$Hpe{D0X^B5*xWj{`&i*iQt2eMA7LCIY}-A^;eO0I-J$0K0X-;b1ucvo}22N%()X zgYf@oJK_I@8wvj}+(7t$p^EVT!ZyPH3zdZbFQfc_*(&*eMEQS2`F}+De}VG<0_Fb& z%KrZF`x1Hl6Lew+ir z0vLas1Hl6KZpeXPf%`Y)K(GMbAG3`hPAJTO85Spi`G>+loB-Y*G9XR>>kkY!F#Wi$AWi_+kLv*91hD-$ z2jT?q{Wu5W1Tg+M2jT=C1CRr8q8jJQun@7Ac;~WSP!1v=E2ob>e;~WSP!1?34g6IIOKhA;ZKzM%;9f0?TQbBY8)*mt;IsoSn z84w+?+^mQW!1qH2LWk{Mby9nzq+(}q};SLd4e^`!g7lHSObU_5> zAJTadxPO@TE{eeZLpn?N|27U?&Hn#6TpJl{#jY(Wj&O;BW~aI>+;eO_dlj2>t4xD@ zXou#WlU9M5!+dPH+t!}h3uBJ+ad(=+T4m-yA6xFSIetp*|Cu9w+?A%VR#^@8p>;28 z)19^fKQ%&BBYtQFzB5f>t+E>ULo00M9kxM%nq8{#KePhhk*2U#`68xt&W&RT2nOOk9KJ8 zzBC1HNzsTu+JW`ly*A6A6&EmTiiZ5r4$a-0rob&J8uLdxa5;C6t&v2cv8HIyAMMcG zJ!uNulA=+6v;#}r6`SdGiWj^!MZ^ARhvu%NDR4`Q#{JO_T)|zo)sgt7)|5mPf7+qB z%V`SSlA@7+v;*t8MVo05pR9hpHKk)NdNECbTT*4Ld~7+FvNaJst2Kp~i%z8}a7&6t z|IyxRiIZ)OTd)ba+nS=`f3!n$a+(6;@xKno|9>+`_9E=vje`H*qY(f5&tU(lCuY_uNoWFEqkW=YO#UAlAJkHo%zt0rw9TCbgU=qzb(7|e+kzA z(}DqX@Q?FJ@c;h<_fhUH&XyhB`MH)9js2s2@~PxrY#aQ5BA;1PGz5@#Xzs;n3fz(+ zk$`yZz*XFfY~4RpBs6P^!~z1XidLDFdr_JKx1?w+Anm{+_d?rEN}L#2rf4uA?apz_T`wnyp;(tFVoZ|mK{w=(N`wjPUPV@P{ zJ#zMV*452;O|^K&%N)7-!TPQkS zf1E8+Q4v3?e8$pb9FRAwm1Sgsa}*T$Sc(1bW=@OzPh{n4sx$>|Nzphz+JVctSK7=Oag+x7(GJbM zGEIS7QZ&+!c3_Eng{={rqRc#OP0>(4+M&5uq$zMqipKiU4!nhXxve`z8LNZLOZdyv z6u2ctqy4ZH{W4pJOg58J2bha~S(*a3r1mSo4BW#O?xi-zR}`ksn%c))^h?texFtm+ z{%CLYChjG+p>-hs_y5A0qJe+3Lvt@lQ((gX*Jb(tLjB(lK>ru!>T2E_J?-ha_%*@x=hhD z_=k3A?lox&+>)ZvKePi&+yl0HvdL*p(eNMIp}7ar6qxY;%^Z3!*!Hgz9uQb4{RhogsdsO6%l0c58vH&aSDtD_rVvTi`AZfrC+@+#t{DRASR)UxT`oNtk;?+u7 zSXa4Y?XX$Y_5TU<64(LQ2EKc*70!YF???P2{3L%fc-y?2i|4QZEh!q~NBe}cg?pQ= z%S0z>8stYiH21bN1#U^vC_mbPw{UN@IewtbUe**1^P?S_duy5kx1?yCAML}vO z_pr_L7~OpBVCIS+PE+8P6o~@Fz`9j&581k1lp%L87yVG00=J~>tZazLz1emU3qYTG zku`Oax#%~iDR6y?Mgr2#ByZ;)#QI$t1V}p^_h82U2jYJ|jy&MO`(9xL*8jKgoe=l; z(bdQQ?2)tQwytjGYsP^uAr%`Vr&9d94(O<$vplFGW#p}{ zdSC~9H_%z$q~czs&Jw6e&;Mx1m;b#&HUAU-t^5>!1nlh}=aO7QKKI{}I?p;kR&ejh zMB%iiXrv$QYYO+COc>6V6pi?!9azu3+h(?61CiUUDH`%eJ2dz1GzD%+(U?Elfy=pf z*%~r7P&DX|c4+QhX$stuqEUae154avwt5m}(3+xQf3!n$kEJPaU5dv20mBfj(VVzP zt1KxR@kcut_vp`CQZ&|&_L}OsciJq6sV0mD`_T@~y)#XLTT(RIk9Od4?j5$Ky;Sx1 z|F`!gU~-hz+VxcpRWp$xju@jsA%tB?=3P&oDC~$slT1 z^)eUtdsP-!To47l3NE;Rii)CuyMTfUilS_?Dxm)7TdI1tggV`Q@BjbL1NT01=;WOK zy4G{v^S$p;azDYnwNEkSY&%NtC%ChveUe6a*R%dJQAg?i1ozfH$&|CK|2J{{zXttL z=>10_^M42Ee%+;Arfph$*K1l&LQP>UK^KjOd$5H=;=z`k#+u)Q4Z?-0cO|^>tEA-Y zyzz;a{@F-srH}l$Q?-@HnB8jRY#)AkBMCB0%T%o z*owA9r3zb8f_!uOxWv++k@FH2GAN)D$P#}!DzOaph-~gkl>70KNy!3WVtHV|CL;bp zMlO6M@L+x4-oD`p4-DhjMA#2sFl++S3042+v;RkN|ARk#)@V1;F3CuVI!Xc{xVLr_ zQ_i*{l>kun;g@KvLV@#A3jotq_!3jjwxhHFg0I`uzDU=9Q0TQr9VG@3+*|u1Q_hu+ zQUeHH=dIJe!0SUL1Q6U$`vTwpqci}5yXo5Jd9O#&|AYH!pXa?E$^6Hhl^f}mFA=BI zb_TIo=`q@k%=aF^dY5W9&`nVAL!T+?^rLJ38<=vo9mW0+enFIKpQ914!>#E5!M(N5 zG36}rzX1JzSHm8iRT`rquSJ1p`7x%PZAY>Eg8#k}?VH{>xTvFOe!;!9Z+i3KY)A3@ zf;$&$-=O8!sdQ8k{epXI-(bqwb`;YuxU;2womTw4m7}atM^XKPduv~3%Gq`l*Dttp znf5i>c%H)i6xlDhxArxroNY(3{enA}X-c_ z|J_WhRTAzVflk72X3E)ii?A7PvG#3R@m;yi)W!4vzRi@g?dHJ$`xY%1-+Wxu#q!2)0_>>-a&UYy-k1;;lW>P&3+Un1C7 z-Q@nk>GivZ$Hvw#jT_qBmT30lBF~!#TrLY*)tPAWgCZBZ0J&Tix1v4KsN$BCAa{WI z(B-nIk#iFbGAdw~$nF2jvCAdMXk>FoqTY{=T=N3ta%ot_|A+TKX8(`79{Z#$(|%0r z{w2{b7ti_oF;mXAi{|{5X?M^%OV)=jp7VDHQ_i-F=KPgux6_6b+}DDI(X*KRdOXG~;RGA>)4IZczXKzVS`ti=YSeG2<#w{!fGQ_p~u&obN0DZ3NBF zV~m#>C9q%U8T}Fc5BlBukC!R`?bjz@j`%LUU2oF2>YMdV`iUzj|9y+x1aro(As-;` zCT}4QIk2Mg-%H>U_=onG_9yLL?HAgQK>6<`%Mp^(#pJb?1JZLK#Y|2!9icr(PBk5& zJx5M49nhXD(aFiC1JZL~oMbw$5XOn71HyCg$qA+dx^rM`FddMc1LJthk*afKz2!*N zIdYuk2-P{A9BVm3bq%YzE;ypfC3%2j<6D-K?kCB#IO>eLdVytTM5vi;}$U!phO2>3-XII+|d4Q zCZ>h)FEeqGF#c&KK!+Os=y@{%YII;cXC^?74vc5b1nAL$@eeD(1?hBM`@5Ckl5}7_ zV+og!W#c%Ce9Vc17@OE7=N@9 zJv^eW{lQA?l-B)LqFY+`S&1%gncDBIL?^c_?RQq9Lw@#KE72}LyVpvzN$Vaf(aNnN z?blXf2e*o~yRAfvwC*w!aq+dFa2XQ@C|qtA1}I!M3j-7`n}h)hmyN;zh06wEfWl?H zFhJq5jz`qF!ey}8&9{Yb>ihXLX(SAoee%4W^J-j~j8NosQw0mIxkLz=R zgSzQ3P7ZoKJ_|UgpY|Kx>yi9l%vt$0y`o)o9&A_Ae-RhNG1{-0?~UgFmTGs?t=lEP z%f<75?`F!`cG3LbQtd9fRnd)HJpcDDrkwfx-wh*vMg4ytsQ=$?dtBIse@m7Bcj|Gy2IhW6V9MWmeXV{J%pF*& z{C_?0jjw<`$7ymYD2|Pi-7wi>sq()DGd&)HxnFl{KY>o-H~nnHk8U@#3FbMpge8 zaSTG#|3w^wfchU61Q-OA|6l+HA#~?C1|f9kIR+tg=LujCvh)WGLYDr3LCDe{FbJtS z&oKz0I&W|c0xCqlR=^;n>O99Fr0P6i5MKR%j9>q+^XvatsrtVNkOx`+7Xk9X>wj3{ z%S~SXgK@OUtA8+FX7b`6j53qg{$RY+p!DY!AP-Rc za|@6ME=3Q=6_5umMh_Ms4^aMd3y=p{|8wL)*8d!N5cPi%#~VcbU&QeSsQ=;X0dIiv zA07{YH$e3d2H*`){DT2_1JwRt0NwzlKNx^FK;;hx;0?0y2fP6ae^?jC8=&&%7T^s~ z`g05L2B`gET^w(K;-6c9H$e5zEx;R~{O1#`%f8epXWOv?n6co<05-6e{h3xRCq~`f>A1 zr6C&Z%oy;aA)8+-@63pZre(h$w16bCa6~jMcl)vPY+Azaho)tp3Rj4xWzAs!f~F-b z7n+tA`pd;$1=*(M(zv{7d4V4ndEUIH<+7mAv^?Jrid^i1rscA@(6l^H#m&8Gxh!f# z=Kn$V|DrkntF%AUO?{Fb<>ER2e`dziMn{s{{u`p+b){(Z)ty|)dGXt6?O5P|35P2Y`bXAf0_0NT7RG1j_TH3eKki6m$Fw8Sx&_a@ zroM@NebWbF4<$?|2Y>=-X5PT|+-YV@gGP2G)S&i40GCT2svNjfQ`_?bE|4$|Db-_V;mEbjDIMJ0zcm#rC06cdqPjMT}k*y zVM32G-y6;NFV!BQ75A$>KX}IfBTPBlE}HRQsy$3Az1Ab1@&7PW&bI3_{(~OyRoX+e zx?6R^eByslZ|xzboNYG;{{LTTH50F-sEgC3e2cGl$G*iyD>vMjC-cqZzr)b*`B@NN_nZH53wWpYJwq2k9 z8`OD?_9X2RkAhWEcQ3jYc#VV_9z>hQKTe#tnR2$>9QgnL$}-{b{QrMtnQ%Gq|NoiYj)v#||1-NC4bA^wr9Drp zf5Mdq+M+I=|NlHw&bEu@|Ceab(TX#r^ni=!|3Al+v+bh!|Hay~G~!VY@%;a1nR2#W zH2>ey{z1#V1CHna|AQ%KiT}l={`V{NuY>llMLr4p|DMyX(c&uteTVt5)5%U{!@m8p zjhec_-P2hRkhcNb9Ls<5A>UVCqh?vqs!qrBgCZBZ0I*yZx1!xSLdDI!QL`*+eUZ+6*|*8s4~9K(;EJG*RY*os|FcBFD40ZW;2RCPJo$;yQREM>Y;-t1(DEa#hT zalkS=UODe*09a-xD(4$5yhYakdF=m7`dR>i>qAmZ+n<-arjouQKu~W|%#^d8l)3;2 zXm2gC==L)Cno3Gx07QLAET)|8Bw821BqlxOT7gMMlW1K4Vlw4ine=r5g4TKK$PrK< zCVfSKpnl{C*#DRGwE%*;>99BHtrbbjtNerdks`o)lcwhu_<}I#RSEI_x090okGLR? zAqMlk(ft2XqSG@uyaR$uNi_eT=uA1=N!_dYgZ}DLLg-maST;$}$BZ!LEZ_gzHRHoZ zEAan6thd4b|0_wI_Gg&?vHdWw`W61D*~yM-%kRkq-6(hlXNR-p8x0yWjyIdF&`5=7 z%sAQEYz06ncw^>#Cp$#B;7A3}sXCqPNM&UDb4a8D;#M^~-caSd^dR6(4ACNOjvA+2 z5IqPH6p#u>4N@+o9)ySuNQILfsGKKK0l&ZcJjWZLT!^hzOVi45NWTAr)c>1I_ECefUKvYIJpJBjA}7n7st8T^d^gV~a3&ObSd zDQ7#0=KNcvlvdp+x1%J{oPScvl(U^gbN#WqXw8qM z^iUGb`6n-7%Gpk$Isav36|K8SrITpRKUu|;vz_!g|3TmXt4Rs%7!j9vH0d+{gL;z^ zrkv&dkL>>^eJ*g&+)gL2TBs0Y1#izO8PG2A{zvLB6IonAz|F@K^p;R?llW6`gS;LgGoy7Bh$;)Zz{gTs_JO!PO zzMLs%JBjE2lA~$mZ`4<#`M>07rkw30p8rc;M#Ekbj^_W8moepRCpTd8e~XmS(4`Xf zOdgLu;AKoX%lyBA<9|KC=lO_!4(NZq59a?psa>u$tdQg%;-l?Ob~am{X-_$%J-v=M zi;ZBX)2$$b73-VsACg-N%V9fto0FZ#hC~w=v}YCt4Q+05vSZm!!Oa`+Aj-uNtJ<9G zP<7-o7vPhNgI4TtyxHm{CCEy}m%dsYGP2821Jw($nR01{3Y#e%HC#Qf&6LaH0ydeQ zsa{}gQf#JlvZK`thD}0Y2z7CG%6h(aaYXh1kosRqH0QUBtf!4{QTZX7^Gnt< zoZm8X9Br6USuvXPOO9j8*-oN4zh&fD+K`Y8h$NcxOO9pA*-rYL-=G=ZYVsP|`IvZE zN0UDDH>fvx4O7l`(&zsMbzVbWP22aYy3c0;2lXbeX3E)4N-i)Wbz4i0p-qYlkd$O# zOxAb|Q_gl0%>_2eI(pJJ$$&_rxxi!{Q_hu1p9>r`NnS@@#qnLA1sv3myvp1E)1iUd z7vKN?Nzngzid+uw|L3$%YaNGp*}uTYT~2mFTfrWE@ZyF?Uv^kqeviI#E`Z@=r?llA z4c2zD-%ZRn8iy-S@}r%N%O+l6V-+07${kL2v@$XYh)qrpE|R(i*b71eTzP`O;DW=I zkP$)}ke#nwD7X^h!c}m*zwms|8;2{^ROLd!m2jZ~T&boh7Z|RD^QQE_gW!L7{vSD& zhP{p#n*T>mWy;x3;`x8%6dLgwGid%FIfW@_JBjE2k&|h4NWoEP{vSD+DQ7#0=l_wD zXmwQXeodnJf8->lob4o<|F?>qNH_gTz4PnQ<>-k_IonAz|F49cK*QpjS4ER({vSDk zDQ7#0=KmFw4Yc|ixd}gs=KqlmOgY=hZTR~Ccv^Xc6j@DfMIZ3vnR1r+Uq1c+3$aUI zLqfFkGjdma(q{w(&JROOIonCe3qs|GXVBP$oSK-F#2`$5cm`9>b`s4CGRY=-iX!JE z(YzqCi7DsGq|XZqn&+(}8+m@{GlGKpk&V3n?{k5Iy6G@A7O-BQ1r*eeoX)Xc$^XHe zmDkZL>P1FsCnfy{aX}nIUdMcIH2bJYzH+TuK)iuxti2yPia?bHR7ke;QzEaYG`AjlAa2Y^r%UV z1x|V@xcr5a9oCp9SXm*!N^dx0A;8M5Q`5s^60C%^0$7`@dWJ+DiIgA28l+yv1;ggT|DxcY*%*>Ba`*mBvv< zaQ&~S9@bCQkJDeFmx7-3bL4UIXL2w3Ik}ZE@W?Go}LyRG?%vZ8}1Fnp|W# zLVrppQ%GG;kKf664ImLv41EHYv_LVwC4!t0|GH5zNf{F~7j*y@t{iY)%sK{>95fW6S&vb+Y6}ixKg!DAI zz;r-?3RZo->3{$g80T4z)Sn`|EJx~3k#j9a>Q9kg%MtohCh4&pp+9Ajot7i?r%cjq zIYNKRB3+hK!;hv#IxXiMZWWPs%h@K6aGT|9l~$|iQ1P`pOy_K2w3yCW!ibyBnZk&f z&g+G--E?eWG@DLT7)_?LMHr2ivzbTKNrUB7ORL^;s-#tCIhEWpNv-8naLXb!mQyZ2 zJI8V&^0RH06PDIi%L#F-h@5RXXK<^SoMkziq;;n0Y!qMndeb>w7`ExWP8dty~Pr2a>9epivrbQ8}|U_@aO&G{vpnR2$1XwGj5 zsix(xRy|=f=a*D7oZn(nmF@qdIlrVT+y6&%el1c-E4|&#XwEOGWXjo2qB*~1 zq=MExq6#ZC=a*D4oZm81P8&R|2hI5<@Q%Gpl( zoZp~j@M;pK?H;(+hpzv_OgYQ^ALf7FVD#$$(63SY-{+Gffd7-r{LeN=4QtFR30jo3 zMIZ^v4rnZJ5(Kpd@JOpf;TLnDujkbmH!2WYFI#AxC$!$xcOdFA*KI^-2a!- zJj^dBwxbiuF(ID?9Mq3cjtNQrF9s7jn_lrL@iwxPlKzXrgwAHZH=6%jO3tETZ(|mk z|4Ytd%Gpk$`M;&)OuFR(1xKU#zvN7&ob4o@|4Uv^EAD_)@7wQ)Cei#~@_MG6?IfQ6 zOKci$QafSM{9j@-uVMbzEymZ48(|*khm7|?{l8HEyV=-gY%$I-PJ;PgWrhWM!B6N9>c3s2 z|NSADA@Vle)eq{Epe1;&-m2H@XX@pP^uM1Ve<8mozaqDho5`19{@2H0$Kkt3W|97P zJvob1lGl;r$y!oM44D7*D9j}N^)mf$S@~aO^2#5|(koTrZ;&fg;ct-3RpD=t_o~8Q zC+|^(zfRt*3V)samn!^q@}H{k*U7t7;a^1Ft_uGm@;0yVf2&vczr`#3FY^lj|L_X` zd9U!#c!fW}6|(RLxIzj~bGSkj{sxCDMB#4`fGec%G>0ps@HB@jr0_I{D@5V1bGSkj z{yK*%RN)UGg(&=sI7lI+r#VO=>wW+!WZe%Sg{=Dlq>yz#fE2Rs2arP6{Qy$Px*tFa zDLlV9qk zG$9IqgF_Rl@CRr@75)HCh{9jz(1a-bbq-C4!e8gmged%r2mlhI@Gl|&NI>BaO9Vgy z>V7Z)kbtru3;-l#)enG#toi|vkX1hb60+(CKtfji07yX9&sPY51eE>U0zd-ler^FE zp$dNhBvj!KfP^ai0gw=dzfJ&<5QV=^0FV%ce-VcvMB!gV0EU3VA65o11l0Xt01WYJ zVMJyAzl-nxX@_}!pViL;l;K7|;B@Wt+O8Lb{tx>XI-Q)yw;qyVRw*(dZ+vUM3^RxZ zTQ=0#))_=;Ptm3AwvG_=F1 z@mGpW+ZXgMVtszd(B_zPjvw*_+-kGith4c|b~xMoc*rF#=vypYVny89szR2OoP=BA zf-TnBHA2sV%7BIFSuC!sH%t5vx&JqX=KPnD2HLnzZcRzyIsXkzIol~T=f8~9(;AT| z?TV)God0^Job42v^It~lXx;Tv=^%yY{MRw%Y^QwAe{g)LmbSlDeYMa059&>7nR2#M zKL0gnew#&f)>f^$X3|jks{ug2K6Ib<^JE7Y60;7)OxSx4|*n7 zksUP5Z~3|3M^kwI?+&J%?Nko@-xgYamz?vJ!t;Mym~ysLIq-kuv|R0kOX2yyai*N@ zR1W;#7~Q1m{}i788)M4ZPNDg~tH^d*r8eQE@ciHHOgY;rH2=4RG}DS_)vE>1|7~W< z*-oMPzs019p7EI6>zTsyf18+cwo_>SuSFW^8KaU=lfv_V8<}#J`F~7;^nUptwm9C{#*z}`wh}+{Y1%h2D0jbbL4|HdjcSC{2Z_wDv|Ynb zsv?_rIBH@eL`JT4(PoWoNa#{zXEYYLO93?nbSYZ>R26wF>L-UM_O74YH@kk%EVgd8F`Q)O@~gv4nJ^yh%#Shv(r+zXt1_;!J_dlMa+*@*e(SeN9a;)_oE|giIr#- z5E8l+&3?#&cPXIyQ~5vW|J`h)j0R&BO!9k&zEfXM9weWGX?#_%^X3QITZ1YA+9_Wd zAZV7bhU}#69~RG&Xv)_I2z7G(UZ5UgdDF6(8ZK*(B%=bp~|4T_5 zt^Vf}PXMB+6TI|0@U`2B>y2{((Z5dL z1vkQV2xiB$75pIxSq8Lfa~|1tNT3qp!mH35*_JO* z$#bDLM@?HV6i`_ufJ!xEy}&>voHy8Jot?9uCr}B$A82siTy=uoy1N2UxiHTRhgzNN zXm#Y)-33VH!kD4WaVI-q9f<>aQ01b4Rjp2T#yWD53$V&XF)Jkg7o`5D`1=1sy6J#>rkw2*n)7dw z^XMk89L00~&tuBjPN6yfWn>p^7Lea@(G;HZzl$knJB8-_myvU6!_iXTA%*AspUaf9 zokDZ|%SbP6xJGWcNZ~pEy-YdVDWCHnGR7cd}#Ja(?`csqz}Kd3gL~C$;7C_*)ROqQ&v% zwdI3YEQlD{>8KfPfGZ%|Y0D9dh0p59=2k~dTZYic)M^1_u_z{B7TE#I1;#8Oy#UN2 zJ7>9Im<8k(fD2?tF6W0?@MShT{eIG#cQjaAcDi!D(Ku$&=dZ1>m<1fiO8NfB?*H?( z0D{(bIvL_Mp|1iE)Q=4Dno#opF*V^{dd0lxJlH8o|3}q?dztTz=Kq(HJ#?#wgyZ@D zdzf;zQ)vEwDH)_^DT;9l&;K7}%Gplk!2chhmB*`I51#)&z?8F{%7OpiPpi*YX(pck z-_Mk@oyvj#zng{?;UaYoy8hqIl(U`6f&bq}BOdgC=l}OHWw_>@g}qtTCX3XL#HM? zpbeQxuhgl5kkF~g4rePkND1W+fRx$!>V@Ky@Up7e@dm8t3sUlyOPix6s}~BStQH`p znx|f1kP^-tfRx#p>Un~c@cRLz^k%3R2BgfEmK>zaj!{Pzz>7f2Y)r5J7X<&yf&Y7e zZaz|VyzuQ_glO2mbFq zTH&P|c>eD`rkw2*n*Y0s%+k%>qU?#L@ciFdrkw2*n*UouX6WWtmEYm{zcWlZ+bJ~v zx0p=Rs*@G|*N@KsrBl(WSDVE)ey#(?n>{d#>T`6r+M z2YvrfVdwvx>k@@j5bTJ$6&f^K_wC&`Gb?wgY=+I`?XKqIAjlDQ3!Z+9qlRD`P4Gh>cbX2&e&9j#o}|9R;DrF|}N z&9nD33||CW+Dy7k9u|1X~ZJI9o>okH_}OUXeR^+* zOgYgLnH^{+rEic6wuexyokV zrFFAo8}p7<-t1-vHRc-)@JiR4(^zP{5^A5y9d356INcDo_m76(U z>E=9M{g9g~Aue16-gxzV@k)pmX>-+B^+Ms5aG_TBxQ1V<+BgPq-SF7h;K2IXp~3b2 zebejrO|9?S+c!Mnm%-KBUEPnJCteA^U+DjLYs}OC_>J*1<5q(iUxa?ZM~o|sw;M@g z4)j0w7`s3ppvl;3YzF<0la1FHFE>`f^Wtg!A^kr6PW^WMJNj2ZJ>VKp5PrA*7Tp2; zk12hxe!kwJH|bQb(ofen=&#g|0(Q}}Fk$RZp#S*`@*~jy_zLh0t|cEN?iIOnre;!9(0s0?BFpuPMm`QxE_H*r4Sg!w%&#qL|!kxk7@XnA~H!AVC2d2){91P@n+g*QN^s6kyzKx>A4QF4L9z6L*@f z)Svj3=}P^HUz)DepZJC4g8l^8Wc<11f&c}#erCC#K*6n_TCR|wFv(9WS4dD;SpH0g9VVR|-&k+jOM>#kVXM1St3lndO241-HIwxk7?M zC*QDKAwgl1uUoE=ps>i-ELTWSnB=RLDBsvgHa1iX!qQ%M}t7#pH{Y zDmQEO(8xK4rNt=axx6X}L#p%OclW?#tw7pRn9A`Pns=`%-Cr+;UfQ ztB8Ema*yIxG5Lt)mP+et(>+pr?T1bGCBpcS>8=vS2Tiv`7#}d*Vqv`BbS+`L&vZ>; zTxGgP2;)l2E#eV%kfL!7XZKsoWLQRe9V!F+Et|F@L9 zou2KL9(exm+nI8<(`f#0DR~=>H1YI*RWyy~|GteWXFHt(|M#u*47K$rjpzTql__UC zodf^(Ewo%<@Oz?ZJpcDCOgY=>9QePN(a>e;faCeUmoepRr*q){{)az$B#r0){)abu zMCSjm)r?OV4f^Byd-Su&AIUpNmG%oQt-aQN&4SV6syU1WZo+QiO;|TOi!slnW(y=W z=eyqQMN)DOF8^Ag5z#(4H9I)IdvIDNVX)p%mz$lzh=ht4^dgo84sC9CkM^@WWbeRh zM=Xz8)#bj-kBVIGf{w)Uz!e>CnF?G|62={9zbUaiY^2wHsSFFv17!ZZd=K^Frbh%0 z@oGOfa#F#9UDQjWR_t<*@(*!=dlS6$?Q%=~@W?ZGXzn+ZfeQcMrWuFY|M#T!b?wdC zwuMiE#kQSBbAHRn6|~_Vg%;sCzgIBjT#-g|e#^+^;qo+|^LzOnb{ft3EhF!x^%u!@ zLmJQdeJ@kacG~Cs21R&QllRcB)8*!iw9otv>P_Cml(U`o`M*J(*N}J9F2PK>B%1bF zz(KvqyP0yf(EpfM8`O`y%iI44{NL-0^Nb_(kLj)C3DD#0CMDWubK?K*ayR%- zsscX~Qsbqj9Lxo2_1Gs1lAN zK$Y3y>IHx*AtXSR+0p6+hbkdo1gJ7QX}wTeDUgre4$E+22iD%vtB5u5-!x> z^FPx$@P9u{EANmJ#c4eM_rpv%+vyzmzaOHT)Mo57p8xwHrkw3`4*cH_(kj75Esv)0 z{NE2UcHxK%M%!B?P^`QSpOb-16to^Xbk$*5AGCA-M#$Qd2 z`-AZplf(XCJg88AgZx>c{s#GzLj4W$fI|Is@<)aG>*Nm#_1DS$7DxU03Zc6pgr{}h z-2mL5uMm0~0Q_?cdKwb>hn|K+{-LKKk$>oENaP=S8d7>1dKv)y!^)ti0k}UH(9;0e z9}MVeDB)@7X$aKc;5`k2`Ww8bAy9vv_cR3Ruk)UUK>c;z(-5e?&U+d{cv|N@4S@am z3Zat$xIed`lOchB=wwLXA37Nl_=ir01pc9uA%TDBWB~9FD}zo3;QnAhCj(%AFrbqm zP=AAWG6d>x@J@z6{SDs95U9V-I~fA?*Lf#Hp#D1VWC+w>=ba3J`s=)t0jNJ;A@nZ* z_U9J#FC^{{{R@fvL;phJ{?NaWxIgqSB<>IW3yJ$f{{nD-SR(W<0QLt1`WFKAHwg4E z1nO@P=wArb-yok-sJ{;V3yJ#c(7%wVzfL}(P=B3VqfmdHd|aXaMbNe2s6Sup)gJ8s zVGs8IkO%vJ(1ZOy;KBay_eSTvPvZai?Emw*z(Fg#HRM{__JG_qo%R{QLA}YfOgY^aaXq~r? ze4N*ZJ|j4&ANe@!|4aK^;Gk|g`55o@_$=U{e&l1k*Hh)UF((3 zL7k5wA7#FG4c5Dqe1x8H3m3SB_dlNh`w^y`?esSMS6@xbUnPEaG`$sl%&un2S>pfa zaQwd>@c;Me_0XMv7ulvgpuI~wXED-ui2rJKy(z7ERypC2sB+YR)&f;I5R`AoP?K2; z+q;0+ymyfu(3)TGg8%;NUG8ZLhae?q;~@1FdKWGIvy)!ug0D8z;hyTl97x1|LFZyg z*wE%y_Y^-YlA7lAEtbZt>Tpl?Vk0Z1wLVoo8TNFGbv?$K-Ll(M40bjsd6r21nKqdf(+D*;l zR>}GwyZ_JE0ti~$>EtF}6Z$FuLH)>0ye5?Ve@vzRC3?kM1PE`ZCH)@(5*$Om#C&fw z|G$)ck=kp;OCg%Z^Z&odl(U^i^Z!f97ifjog2wazzrd8Uoz8*(|9M(@ElmI1by_ry z=l_46DQ7#q2U`i3kQ-^GmwF7ME8!cNa<Z@;_P1M~MW3PhA{e#o%cLS?t{nEIh&24UWpgIx?@KB3o zL9065>|Ax^Vi&*`%i>nFyWWKL5^OT;;}*-JX6Jt&YX9HE+J9@)TE)Wq-_D>p|7C>H zdT%=#p7YO`a<ivM{{C|TfXFKh4 z{)1L{tI5}CSDk9I_{{&H-sI~{IooNU{~y$O4fz`FI#KNf@KpeUdXuj)g&xH5SD|INM#SEdO2)y3r7^bGGZ!}I^Y&6Km9LG%AD@-14)c&G@v@0{Wjf>Vx``Y8J`9m)Q#NwH#>vo|CW*;&=Bvfw>}ij;Q7Bl zV9Hs(|836yY1f|s{!fG8|HQP1v@13Hu}tALpEt3F1+!)xyt#Xl@JZkN;OltP-rF0S%n=|YMgR`(MmXP zu)``lS2<6#5`I6RmEIiXLZFpPlS__PX2&UGuYxRExil_EE3<=@b3`kb1y%jO0Q!F! zG|P7txsz51@awc_2G8=nlPPCA`yJ5?p7Z-lrkw2zn)7RsU(m4HVw}Nqet*G~vz0kcl&}%89e{*ZeMUI zlLP7gNqM|KB0?|6ieh9T@WA`HRis*d}&Ys zEY(!?LI9Ravp^15stN0P0hY_+0$`aPtX^Qi5{eE0EVJ{~3kFz1#Q~sTcGP-)5F=k^ zvzwiG8dZYyk z6FbG1fx<+$FhF6VOBkRq(J2g2nCK7&C`_~q0~99OgaHZ@t-=6>i5)zm&J`wFqy-8S zacP0VM2uS|SD4t&EsHBmG|SKKm-&B8Gp>gHzfbE|>+R$@*#BFvJ*d4;iy=>6vzwjM zm`7?Na{dS;I@!sL`9|ZNS#L07LAw=Vt-Ldv9nF|;w?c-5ZpElyo)orQp=zp5cf|h! zEKWV#KME@KpiaNnqucnk8l=0!e%yS|8}C*O`O%Q5cYJVg09LuT54b=|L|;7bb2a7gC=LGb?^_`i?Q@^01H$Mb(5 zW6Igix`mXN>FGZf7{gXjPLl__UCgXaGhlfTgF z9x1|+!SjFr!j!X}LGynt@*s_PHy57&`yf-!^8F9}{~L@R{aO8^!2fxaTn_plf6*>` zf&Tw?H#?3o&j+1s%X;G%3(=OX8{9peg%APLfwpXR3M2M)mu<@~jmz7z*?Ek}7nDaL zSr!yXB-vr=$i*(eCYQy9?UdeF_1w2pE{hsD*Hz=x0Y1!|1-|4J#aZBLvN}Z1odv!$ zEMSw_dFqA4CS{5N*kpFBdSS3hnO{I!oSm?qZ+aC`{r^1lzcM}-I4HuihWwqjU!qVk zpAj6?oBW+AXFDT#!HCx0TJj8SUL&@LMKh8ZjKPPVVanOgpn1V2d77^Gn)7&G@Y75= zS7v-(a8R6Q9eIl5Lp~!os2_QX@A&h%z(L(~@+9x|?)5gM2lXRQ@?P&A&n@r;@dUl% zmjZXQGm`#`xFC)pPcYwm01G#klE>-RQ`D{pJpcD`rkw2zn*Uo${zlI{AeHPgc>eF- zm~sxB|Ied2f0nM%%^oKN&H2+crkw2zn)A1c{F`oGt4a?%=kMQ4IolaD=dXnPi&m9M z)uRla^Y<^Nob3#n^H)s%Nvp+{5NNXCIe-6T%Gu7KIe!*;o`xS&{T)2#?|G)2?F^dp zS4N(rwSQ4n51#Y)98=DA2F>{^BhS)?kowhl&fl|4IolbZ^B44~x0?Kec6eo`&-@GO zP5!}@v&{d)n(;BCOaB+_{cj=F!2jN&g;&7;x|2spJOZ*YWL~4!UOYH4c5wa7 z)Y$Os`hmfz$(i9A+9U7SK+ut`PqKBcpsO(7^ zesm<{vuN2%V~01Ehq{uw9~*g0RuUAFP@3pNCy5_8zg!QZZ8Ak6r)>6hZ<7`bIxw!68t%C}7{irT!Nn%dC|*Y>Q*TH*CMbkDKp#3q^X|OZ;fa zOZTv7B^+X*|8E&f=YOtn-@o|Li$~zaBk=#+5m3FqBHrhO-k#k{9&-ecc42IX)N14^CgE189ogb=~4Xyhdg}5d+qrmbp5aEwCZZP>vhV1L-G37 zgYlZy^4MJK{@C2Mq27+(@!pQD<-Ili;U2JKH9eIr-JOwGNB{o#NNp%KURx2X**n)b z5ju15VAIsVaAQ^DNLyD&c-u^0XK1>ue5|d#qphW_eWs$dt7~Fltai(e_QsjUiQb{z z4P9f6Ba_4Z-Cf=7WAVONecR4h$3SOacV$y&M?BV5AMYCM+%n!C-Zs@73WZy{dWU+$ zlcB+yp{};Ny8i8v{w(Q_a+*EHTU*EP~ov$vwByA}SdvAnmt^yk5bkY<<1p5< zzrB0Q#hqPU-oWIUcOAVa2j>dyM zk*4wZcT09jE%)2o%`W7#x#$`Q&t0 zXm6-}bmm}3c=+A}(cMQSRP&d+5TX|r2ZCzD&cU`Q%^T0%ZXl8o%@a#nc zUHdPN!*>ogG{rl++P3wDMi-s4_}su~Z_Uz4nn{)ygwef3lEw#d-z?(v~HxYqU$?yRfb zUA{Nc*%cq{9i1J9b2zlSHoPM~7T?}kR~zbzjBW2-e7?rxk+#v6?wFI!T4fkC<(ly>v(^($x=!D!#=(ZJeY@+n^~7pt+xu#U z_O(Z5+r<4g9v0YBP|_!;m_hj@b@d=9F)Uz1g^Qx;XSQqX7B&euFHAo9M0T3{TZ|)(!3NXqf8k3O7~^cEWYP zv$nFmb~fG-8LElbHMMlt4#nFVrds>PCd2LWepAmixIb!!hU4RHuwHmB&26oK=hXg| zj#wyGQw!^E-5;-y9qj3DgKMa6tfeMC)>2;^hHIum_>A*&(liX`3$8A>&fvP_|6Y8L zgok?hGi)?IIZ#_M-5ctf>8|hXYw51++P!o6@9M7Y-{0FXFx1(#ZAV{dpnv<^SW_!} z|L$>ED?B4Q;6G#GflycZKt*4D*G284b*-KCv2e%8-bMFF46YZrUn4EuaE@w5;ThI7 z6ocy=u6y`*Pk9W^Lob|1IIjm|HCgwumpDphi4NbRn)XnyfhRZwZXF@yA*<3z2 zQrAAP_&$N>+Gr?NAFJfYzISBXXso6`)HBx@g7*vD2k@TifM?t|{P!Fj%Mo~2jr1Rk zcWfJqchrry)b|dsn)-ZEbyBZOv^XV>`PiCL^t#v%R~!o5tW+ zO?B08nTm~%we&Si)xtYx(K+iKfp<=%H5~6~t?V7C8|fW~Yp}a*6yBHRa9l=v=Hesp zTlU8z{CC7c@wxsAco&CzM&KDX-U-k1(fuvm{Jp$wq-PwSfi+#@z3?tNR_6bkIsd;C z=6`+>v_IC8TgU-&s&M;zkxMowE3o0k$5e0c%Gz4|cm{es|wk-$ei5`aP4= z>t}}greWi6|IlFnDEt{bOQF96G}3Tyve^G3B$93{0e5Dn`zB`g3{FGvDrhf34`gT3 z@`G37e#K%yW^riV8tYD)erRNa=`c+65-`YXwI$uv1uyH)#7 ztvB~ep>qC24^IiY1H{lvwC`8WOC<%^#SzqRgI@CQ&8_pEh(c}9hpwR)AL6X4tfX~b zN-Gf978x|lIeg69^d0k_Mp|Ha?Xc^FO`Nbo^1Q-F zvx76UjpI|3)3g0E7tajN&cdr==7Rpo@u{)F*}+RT4E4=y92)E!7@VHj5Z%)^HZyqo zhJn7>!Hw{W8Sk6j5Z%x-KCl5k8yp)RAD$hYhEGn1{~H!DH}>`SPwtz5zZO1y!_3rR z|L~sSeh75QhPJ-lle_ySMr#IV`lpAdW``#y;4fWz`iB0=v3=tc8$Guz{wLFuV}sy& z?gkZe<1qNcH}&tEo*ta&KL{=ZgM1}?b;A>|6!>ZI-_!6-8wUn=56*7vhZo{7|EzCZ ztm^a)lT&>E#l&7OnE&4sedB(x-IJ4}Jn(|0p1$FN;k^@+)8g+e`h4-<%Jsiy2B-Qq z_Rnq{=Ev6ikI5+La$;~pG<5og*~!_yu?xh9;=c!{rziPU%fCooj&NOzixK{^{Gg)`Ruj6E z-~UHw2Q*OroFcc94PgGS{F6Avd!@zUkS)fpgC*;1Nokbc9&x@keh(tkip+WEU#0fPK?_{D!|^rtjiHuk(Z=vCh{Z z*7<*7l_wcumB%5@yLaUNSI_#OF5W1wIr@ymNkK1A?;-F%{5~o4{ZEWR4gUYde_lKS zFCKyai6fABrKKLtuM7MN*)u#bJTo*nugX*{buTqidSl_29(ZsePO zc^qLu9JNOXA4jbR;_$7$b#h-&4(owwi)uZgjj$fI6$rha&=xNaSg;NVIaJN#$elhptcRa^8K;W>F15V}zaBufRGdn_9=WL}hxJtWaVq$F z)b<|ydN}kbe^)tQkKE~#!+QMd8FpJoHrJ^=Klt_d*E8H{zms2-^0*5Nj=O4CBI8u~&kFYTePKL~5vG}w#BoS8n-N-b6GG^5_&@yYm=O95{kw5_8CF0FRBcx-TKvcN?=3YS}tz@_0ZT!4wpUK;9}kam#sI#Wz!9CS-&1GrV6-}8`i?Lg7+gBhws35 z;UR3p9RD5u5&j;yS^AYIuu=-_uP8%3CY!}tJ7Tqs*y=}Y7Q4+-GZL8aN5?~n5mIOe@_2O|AjN)*7fpcB}1Hi}fHbLjx2U5U6&unTGDSOW@lz%@gzk{#{UlPQG|a1tMRAs zL--B&W%zEK#9@2}pTs+{1()-`NnYZkyqAA1e+$2tui$>q{gnF-0GED$ zMG9=%fsb+i3;wXu^F~HzgX5uO;-+=m@gCDmJTw-HhoXT{V%;|Q8#*=l^pmmC#QLpx z4;P-+{x;sY1=nx^=yl_<_T(hmwALoe^(O-$|HF0=%jWXlyb-HXeft^P9?`hFP>5 zNzTuN5@lO3R4Ds)U^Y&^I8?w`$pZz_1u3tWuj8PO)8X;BU#W8i8UN+{76csl^KlQ} z1Z)4x`2o05`aQ!b;4C*FyS*A#?z$ruEBrHCYg`V8%UWOHR+}=Bm}y_T)kr{Lnb!l_ z6pDt%!hx_qKA(nf5(s>QKKQmM3_pLwJnE15VWLapa2W`WL;7&^lHop^fh}Wz^<10@ zMH3oqJtnXR^p)F~NQ9E<8rOlKY`5>z2YJGum`DSn;Ne-TIV zckzGX58~6X{=fMd-uX+-FHvBn6u2H&bGh?kc@?&E1?D^BR@}fXGmi-y@gXj6eln1I zdGk&g;dXN76TAV~jO=-WzaAel<&A%`{u_B0S^qENTQLS6e-8dE{Yn&g0tz%3O|V>{ zja%1q`j)IyP5n*bWFQufHkCIED)6yNoxpAG-nM8k77ZoB{-`-VI}`R#^v>{21{Kc% zy?9!hx|`y_Fu>~B4g7cT(Fb^EnTlhRUK~xmCwoKwNbg?WgH_NYdO=$Unpz6ubFjhI8+@;fs-n6ieqVQeeM6ai@-(&7? zY&z*N?=Jk(Zu7z2;oxpF%%LIB?wF&oq?ssS%`@@vG?6B!LLgDjCSkz=B`MKR9X9V) z)uq7^e{#1u2+FQxcsis&!L!*Ja;NaKU?>t=?pYuf4DB{w@W(;Bo2dpMjQXbwKS<0^ zkH#VeVS=Fm)IKsY6Y+!Au0ZR8RDe5RJ5YURnX&4&1Dv!c91Vsp=4g5p_0Y&{G<<$G zl=HwqX*ug{nh`xM8IkiSrTI6CX*0fOJH4n)@ftX-eM1gXEJR2FPk@ zFK4XUwTnw_RB&cgZkZgPgK2l>K~WUZzzcS(QGYTpv9vc;tK+{4p(^xo?p^$?IH(MN zX}4gk>Tz;Xi_)_S55N)9@o5(;{D!e$Un~4>Ha4}>6@HHpM;UR|Qq*XyI(Cd(T(e|? zQIwt8@XC#fT!JroUKHK`5;Mv&y;NFPZmeoK#!2=iUqxwO_PAZrxHV7sEtcR|`Wcj0 z2$_@`R;Tuuj8&U9ahIxn(wW>UinK8PW(!ZCGyeHV%ny@)GISAUeLC~Q zlndJtq3M}qfr%3y#zU}6b|F+?&B%JFEDhS40_zFEs%@{3bwhiYDK@(k*O2Dkf?as=RhAQK~44V=D z0=^I54w=%gM1c|oGzv772}H}Gb047#wgSS@@$_DV11rGW_320YiKzi>*V=gnYO6jd zcW5S-2q$Yf1!$K(prLSbA{h72`6ERBFToMM1c|ot_=#1?SQLQ$f? zHAsQd`hN|6v{IEz6j&t+sQUjOA@n2eZr;S-i=Vqnh@VPmq#bLFWP3+SC~E2%e35=#}0v8M&Zr7WNa!FEx7-Ztsu_qJ5mL5wS=xSR-J$f?o^b& zd1a0$1*&@Cyhj^yRm6EuvjJ7baNY}<)<6~3UabmB%L-PZ^6FK{0$jEVD%q-~`E|ys zJ$txCr}~*f@#%0PK|F6}GTH0%K2PRJQCkd!zl*B%zuJEAvRk{sa00e&=ST8hP%#EW zi9kF&Lrm|pdI>zMzOdd{)v$+?wyGT<(<%iK-DJxm!$wMm++EzP7qa0f;WJ1Hsgw#< zm-zpu(nM9NONjzcE(J>C|H-9ysk9OWo=Oxb$^TEKuTiQ?i2_e91xoAxlS}VXX(b9g zl_)^ge;yAY{2*LPzY+yX6j(V0yu1;N1~={0837>}nvS{QfAauX1C#&F;e^?5Cb!Kb z42X{-=&E+L=`-$&%|%1;J`&Zhi{uo9@!4p?oSXo>;V3AlH$(~8-%mEdDg#YtFE%qs24R;?;G zHWyUJE_qi~!)hc^6|kv^>y^EU%f>vV_Xv40qtI3UtjeWs-cv&7tXqYAqlo!8pdBgv z3>;EJ;qfSOoXW>kZO()eGG~>N*sVBWRTD2E)kn%4@FxQPAat;(S=FGY?;Iu`ubBeU z70E@dl7|Ipw!2q;wX@n=U449`P}OsYL&}$=jytM<7&uRjWKmA3nKkdmjZ*Io#;Wbx zxzwEUr4k`^Zz7%El>2Scx6_@!fO{Aj+qqvt-6aUmjQE2=@C^Vj1`UjENe~eKIQX)q zT_4o@%lbn^{E4Iz5$QrWIlt6(LDB#7_~!`!9RH85kzabLM1c|ou2BkHDeP3{)Hj{nd#+Kjwv2R9k3tlK%ssH%FYrWSOq)Q~!@oZV7Tai4yg zd0*Asr|(mhZnboAvl9Jk@tC3z4#c8k;qh5DvU<)R+j3RIInUDi;rysEPkI!yJcIR2 zY#VS_@TaxSX@`Y7tFi-+V*_JYLDc_kZGrFqU{LMys!(tM+`|8!|eAwAKg zCzQ*MTv99do>USq?;Mi$y~tAPXNXJ!)J>0qazEdNMAh$;KtZ7L&)W?cjl=>|g~A!Z zR*V|u`+(&DY=O}Cln7h7NR;?LCI8>N>7(>#i2~0M3Y7T&XUK;xHM2y40u&(pKj!-o zp5^}^B&A=80xP9}v<|NUd$leJ4wo!POKsMi1#TrLLgVq->6!UpIFR%wWAQ`DiFha^ zjuThwqk-7;UfcX$t7UI(Tl>kx`4i#uzQM6rq;|N)+A|A6Nrh_0r_LYqHMIr3#Q5k~PqL-X(VPf()wM4ycuq*=9I4QW6;WtORpv)0Lf)xlQ%9nGxP9)# z=+V*9=15o1`Q9E&N1b)z==lp>LnmjuXFJB5n>!-O?!o5xXdgM77vo=&Z{Dw-&AZai zUxF`DphSVEG6ni}LhX7t!lg$!|CdLX5W2*F{I6B#OEotshLz7ft|)V8aCkU%RXHIs zu`^etlfM0=YIUv_{(q?%XGX1)9q2yS)HCzWvWmA0;dpWujsl=3+wF#P3Z_Hxa3FnN zK^RV|%vI-ru7{j{c>#iEC&STs&?z3YR%_w0Gg=`l(F#Fq`}q6}9BS|cFB26~j^S>) z!MTRJs;X@{!`(a-`E22?zR z|L5=@NsG^_NrA?fZj~rdqQKLT0=KUr>pzc15E|hVe}M*Gs@$rCbh{E(ShTE@)U&d? za~ZVkC$QmLh+TTpH^+sAgnCsV#PM)T)4}s*+k}ZLp;VAIFL;_n7;7c zz038KY|tl_sn4osp^ge-7ULHV%*Ny3-nV?0$@vVq#piyB%(Exp|JS1bCFlQM!5w%5 z{|)}t{0P67`ziNsF2U8qqo?oBxwf1zb-Pe`0hM%tTWjH7%@K>^h{fu>jt%s>EYLM< zplh-~E7(9QvOrC2pr$O)ayC#x%`};eY@o(0P=O6p$O1L6ff}+v%h*86GN3L@K?|J1 zTZkve8PJOjJcZ@qW*zh|_-D4(xEv0bwGb?Mjgpx2x;O%C+J$riXJGk%QPzKc7UAP? zDg8M;Xe<#a%9TU)AyUh5Q`u(-`3W4#tSDWx8?#!+96?K<-fFjn#sZ=F_L*R~t+s92Y8`C# z%@6f^tOE;emcU?7{L~3&Q)f$Wo6q9x?e4dPPo8ME4G&$k`ugDgXlI|-J=il0bo!@z z>N&L?CfW|t7TxX8(~NpYy)9Vhbd1$F?bb0%O?@cnu-UBjbq;G??U>W9HDk&e?QHHH z8aUMy^G3aMKKr1xYq0nHXn$wNu*Ku-?rTf5MST;at%H%a$ym5Q(w-cJCVYpCe~yz8 z{44#gK?+`UTV1k@=oPd)s)7gEwY;)F=CzV$&dJ{!yiGOt2xFzJ& zPiU+1hH@}z#!xmRX-x-fvNlUH_s7DK(6U=FAdP?mFN03C#?1-it)4)tK7fNr$(haB zx2EBM=_~}9aueYNc#v=Q4FbdL3_05L3AbgIh-AfV%P980Jl>D+8{ty=l_*f6KmiI| z9%Xm;t=>QLbF zjXM-?mFKNj7U|J&B$7}r2XYr?<=!>1Brm&QPOIn4YmO?J^-k4jjZBBZWe*^zhgg=L0jL+cRkS+a66nOekpw3`YmhkPmm+n|39Q4m8>dREXohF@Y zeoKrT0Un!8+OP__Q7_~Pf84>VFjabCn#f{O&#~4q9t(geStzlVpF|vb9HGY{^K|?z z3OkKeTeotT!LA%6$kEv#Y{(^OQk}Q{st>R26 z;_s10oKfxn@z3y&@xQ9KuI;v!YG0zjGo1pbiI(5j50~L)xSXno%h1hm8Qcb!fptXw zFZuryB9wk53Ow^DpzQy1n-RB}f7LUu^~<$5b=NM4`hSeOtR8O=2~UTU*(PR7ZXC`P zB$hmXQbh_?xEWC^q1P%cqf`q;gN3ziI&ISOi_f)6%YJ$lyR=kjx3Oy1F7EOsMQjUz zh2(gQ2&|dh(OjBm9z3bUE_PFMSq z3N$MAfXHOmsStC=!_ldPc`gh)VPi0cqrrq-u?s||DxD}=W1!uxQ!E0JX{SygO%Z2T z>;d(crs&ihiX|X2-K4X$K7Yg?pT9*h0z{^2olw1@@bu_xJOSaqz-Gg$m;oZwMxD^0 zFg&gp{~^;Mogi)qs4#CH*YtnzjX5kM_*eRsC{UunHAaD3RX@O8TlLvaf_YNXb8#jH zj^)v*D4YO7)_;sXf%wm$Pw-vWSmPJh?^TT7E`y5===Q_Yq+uhIvC%|U)|$&EW<7aQ z`NZ_@Y$1y;IB%=q_BAn08(G$%4IE37v)M;PWmx#UsF~UJXrYTqSb#&pku-SL!<8%f z6Z^omXAdV;DS=nBXgvaLq`_CS?%L?L8R%^58St3fI^CXA=G|E@WPz#iK(n5a&qpX% zopSC`y1^x<(hWjN|5my|R`ytKH^_SOlK{ATQczfh3NmUu^FIL z-4X>#6exxQ-xmPtdmM@XX~eHb_(%9#`1Sam_+$8s_*casEWwm0P@=%;JA2lu(96>j zpdL}+5(F>J*5yfMnI})GBwMT@W`2m5%r5$}VO~}-3g_xXak*4?vl3w7xnAu%DPJhz zT>5RmV&EwfR5dNb78KdFUbNh?34i_X2BAG;Gb1y^lrE6~9sFP69EHptVL?|y{(FwLJNVdG)+&V-_H=sZ8H2m>a&Q}_G;+erW$9LZu~n_{!$6G=0i zv}m4-#Unv*#fVLtRsR1pLZ|sdtJ3fcl11+!<#?vMZ`YX85j94uDm~D=YblGGe(g!+ zR26oq@FtORs8%exyeY2Ql#vVm$ZX+YkE<*(td#Riw{c9c+ZZ(pM zf+HCZkB`HqUd|u46!@a)7oReplvZf@Z<>DL>HSg3_{Vr1!XLz^Au0Vz6nF+xpaUzb z_MttxS8cM0!g8gYR5=n(xZ#Mdpt&hF7Y^ZJl?AA7)Qv1_wFX@Muu^-0XCIn~nfqd} zV3VbLh%DWFtI81UIiQ;%P;@PH#f9UQLq{gnIO)XxpPv79uCxdE4DR}+rauEIFoG5R zAK!@JU+Gt(06PU1Z#iPD+O&zg`?MnECH>>zt~Q=Ho+|^8TTd$MU-WK}AJq6u9qo2Q z1ISq;1)pU{OL>9=0wR9cwwNV&v=GQDL?jj+FZ^U77Mng9x)6%wZZ9ZcE4E&&ChPwZ zUOD9JTeeLRJ3fLB;9U@Id=p-eO&F`_*}GN=qC|neGzGq~7=t4IVKE5TuP%~PeZI2T z3D++!o`CBY7D>Fk&n+H->!XXi;VLgy!Sxf1*TMCpi^OT>!>J2!{Xl98uJ2EsgX<%y z0l2<9)ehHprf!4l+fyW<&RbH|aD5;JyY1+Wsf}=beX1O;ua%w+*ZZY7Twf`T!}Vp- z8MwY!>VfMEB@(#r`BDR1?~yEUy&~;{>m_LmTvO6|xIRZJgX{b}T;`&1Ne18&Ckp); zFI=XZ;4)PQm+)b@jO~I;a5G#2%K4uhvLf7qsFS58t#P+>U)l zT_f3#)xt+(v8e*L=?->*C;HTuur`h;0`SsM`(U1~H=dOeG(YX77R+7jXzR4St^=!L zHj(GEko{@WpEg%Pn*YY~%F;a4>E4iwj@&JP0yYBZF|awydv@X3Jtr4ufwv%N+HS?- zqnh|C(5=6KA0nv*L(2I-rTCw^X1>x(B?>(4C_v*@?V7 zr|>M4PZ^nputC}R##|mH`_Yrin`F=7=}IiKG0Enlw|l~$m?$j5hKO%?aKsO%loP%o z38&UQ(IS*pwdL0!`qEWapwt}}V^u=~cX@4EfiO~l0EEXVaQj5=CtK#NqP`x%wG7n@ zRAiY~b60Q`V;-@HW@NeLIggE4rrbP)SsdyUQ=g3g()ypCC`xIG0?#Z85dB{nRu298 z4Cw!Uga1qYZ@o7SNFSQqs{#?^vTuKbANgMpRE4|{6jMC|1rJ(tA9Qn zrh-x%N)#wj0HMEKBzuwXNG0I<(iA!Q>kei8Ur^S66Zb;|yZl#R2Y)^Pb^ew7X?`c% zD*c`w6mXf!!4-u=yEuKmWf)$og%jwHSeyr4%26L|ur)`lwj-AMgD&Na4`x^>x9*@z zIqE|{EDv>taxF(JwK11++6ODF#d*Y16LTqtelWw8qa%=@Qe_Lg zXh*+sDF=VB!opXwq2Dwp0SuweI?GjQWvPKrjDAD-e~!Ni!N1b4M1c$i7K3)>2(-J^ zJ(C&m^O+wR^+%^hz!nXTs#$WQC6_;6^1L{vJRXAGh=ThPyJu&}PDSBoS)~`t&Z{O9 z*+vu@8w;H$q2<;hm;28=`)kN%X&VE@{!iquknjH8Jo~?I;jiH@;m_hv;*a3><9Fe= z;Wy#e;FseU;d}5Rem0)N=kXK{;SoHH`>+?c;6~hlYw$5#jrZW~xC&R|HQ2!ang2ci zYyLm^AM=m%|IB}b|0@3&|7rdc{D=5Q_;>Pe;ordD&)>(tK&?ck1D0~nEDHSdl?7<< zx33WUwQpWI2iLD(>4xjqu845``zyrq?eDIb;rit(8{qmSrPDrk_YCAefA=}Ke)ewi z{-^I2;rgk&$@`zY+YHx_-%Z~C*xhhJAGu84|Ip=gaQ)zA^8Vjk7UBB7%jEs{UN*z^ zJ(tP*4_}50dgu~)|81Af!S$_|$omgo65;yhOXU4GT{6S<4VTFKue$^n^qNKT{;L@j1dmF;5(P>WcuG*fSWyO?I>x^; zn$*lMjpZc6<6jt!YUaNLp$wUL{Byyer2lCwQy=`yh}F!02)y#--vv%df2!*LVXBb$ zH~v+cd1;$NvCX-1hid+jv0liwBTlsCa+g`!{HKmziRK0h)~mz{7aZf2$ZOH1roUOX zGlgEwaswIU2A7o?x2hRz+(I&Z8E?Ev&2YvW)eJIL zDH;4{VY9N1{-3Z(N&nY)19^b)pNt#T%pZ-{tFQjSSgB@yFKkfW{hhF0N&m;VPJQ*a z# zsmWT!hw755Thq3Pv!?7q_fF<6Y=vGaYH8CD6<)b^XH`p&nQJz>+$D)VDRRfV$Q3TN zwN8nRdTEowU`Atb_?2?$%q6FCuQ>V@sieG`k20wZIn#C-g74OzFMB*}etcBQgu?l+ z;9CV(#k{-Z|8womBBh#_D6nD*7ytu}|MPe${?CfZSwc{vz%@jHAFNgNf1gL(PtfO| z#ENso66q>GuJ0-j3p`5ga4NAW7ga4H)vrXt)8S+y=Z`}BLoMeg%8h6D7baY=NuybL-UI+vaWT9X5A(hM`&2)t5HaE8F>3h7~T@ zf59J)DEy^bwZxJeT{(o>z2x~*hEu-UDFWG&OSaE^A)V`0^s1Rppn6AUYjx8fVabym zh2L@&$(4@^yu~0Zr3sOXliXcip{Vfx26Pbdk8__!uR{UI7W_F4YLIg7rgEe=qH$SE zEH)kq)t6DwDhANTU~Dv$+5_>)$SEjXL;2!yy~X0b34c5kG@qCV1*YIw26Lkto-uPw zYmK*aI(c%?Gk9Ti$k*!kBt88t6X$)+gOkAm$F%nK58DFG?Jd3*Uthm_(BF5e>7sAS z8tb?El0zqwL;aSE_JL?*rvH@B*XMEid-~fuJ5MC+1O1lp$rJ6#;URChE8OPr4)xS@ z4h>kmlRXRG{weG5Lg%CxBDQzABN6YA$L@n8UOW5S?8B226T|(@u#It zoF1@yC);fBeBRq1SQuWIs_{(*tZh-t@zhqHQt1#Al^|3loEyD_I+1bvpnY&5G#Tml zwRX-8+lHRybUCz&F)1;0yOJ z^hSr8+a0G)I4uM2frZfl-vw_=b9c|+K+XZTowm+Nn0sx$$?+QBQ0KIFdNAUhoU#uO zbxr}*YC0DPg$Cxmww{Z_L%kEe=^necWuV48G&t#X_t-iY1{Qqo-l^fq=BeTSDM#m! z_hM&XyEC-SiIM3&C5dj9V^)Qv#nzkpr*@A!}M z=lEUR54Z=pGu&qMUGzeD$nd*mBOUBJ83wyMJ_}LfAS&s}>nZeY4A6ZfpjtF(J{f~x zsa^4Le>A)RVb)^N`pP0Ep1z4tB;wvcA&;@tBL+V6v1lY54VjhJG_R*v>)3`r!{5pzY7Xt$S zw0|KMJqpo$POYKfLk!^k{)-`UenrsSON?O};ZMXcxkP&@B}dGfPiv z@=w90Y*TDhG*RdS4E1W7omXn5gy+AtoMPC+fT3e9Jaxo##0xClt;QnY`(|fCae$r* zCEWsrXX&FPrtlE3jB)dc*z}A)I#2lj>(G}FeiUE8$M|3H55f4~4defA&dUk#;M(~) zcT$2duqXzVMe*#QpjQ3g($LY}Y3`ng!D#P}#~|!;C|=s45P%L zYm|svDQ1R#ZfK8`(@S~(E>GuBz~Y4K#;Crv?dsAM3HS&2`wC7lN&k zcE7u6AYhAF1O3f410LVNXfwROkS#p6gJ=Z2eShz?7kGCNdo13C@dY9{4o`MY`P}1{ zE)bDGP;@|Q^DcNnZ1YWcL0szUZvvR91@E+PYItaPez@N^?IZWx?GYk8lJUO|-HhOE@} zEWBPU7=0lPoP*{%HZ(l`Ng*`uH3W@GcWnw+#$8byWx&&oQpS43ZiOyN-1KzUMWaj} z8r5S@HyVvHxoF&q^By~8@~|jwd<vQ-Qrfg{d9jL^m!GQxa?`Ux)Yqz zd39H1Rt0rvNJvf{|NDsipTLLtf8*cC5A)Y^Uk3jFHuO96J`_RwkY^iP*VOtHZ8p@q z_6Rf^~b7e^Vo+yZ6l2xUM_FN!jGXv}PAG|J?naf66m3}y1L>?n>hxyztuJ6n^q(Uglu z8~;~b|LfVxQpraO(s&Jp0|OlpGx?TcsV$0pBqjx6o|Nbm(6MURnv^DAJ{kubnsmp? zL*s@W0%ppnaW=BrI9-6~!Xf(>?qH!N8jxxDewDEt{^}o3Y^1;BROqh~`j^$x% zw@SXHXxwaQw8<#9HtrKekZ(z4+)c$%hB9G*Phl(@*_xzLCJ&9shDM`IE*kf3#Ze{? z%dN#xhECH!8QT$?({r4yNg8GH(A>g?Mx#tF8aJrxnI|KqC26^xw8`x%jxuyg1AJsL z^*Gs@q){dpjW+(Ty8idGm8Fr7vV>4&MUT^09QpFF^cF`x%7j5(6vncLtw|dB^3Zg% zq0uOli^knm9A)ybbQVV$(xX(}5Yf_jylhR54@-M-lmRJ( zQby4kwXroxqf8!}RyH&mWpdHDTZ*GhE*5S4Uv>Q-EsA_;j}#Ze{?i?1lkq*ebg zeifb3Fk6$-l*vbPiVaPgGI?m+V4hLT`cHL6v`y~8;wY2540;CGnxs)C7Y!Nz<=mH- z=>OF{ztZ(uqJVp{h>k^A2z8ZHa==)Ii|JTP(M+(RQHR_TH12VNhGEx^Y!@%ZGFAj- zU~<;jf5-u4LTpV+54n6aK{hn$$tVwvJ5U5=mb8hC|23!);lIHXcpESCqx@#>!(jVc zhTZ|UpZGThM~D(Di(zgT;>yc2)HhpeTn>lJ>MU@JZ=Own71K;Cp7fjj@xZJ(5l))@ za3GgCbkUp)M+4#DY&2;DP+(Vx;J#+KLM`Vw&r(h}YL3MNVe?ox8Xg6U+JqlYSx$x% z>5Y7Xx?<3zqOM~?-9{t)jK{%{KIk{Y3C-c;(H~^0Z1R<>cyD9E8=gtbP6M`RC<<20 zi3~OH_cOT5&lpwQCzx>e%_e5RVI`bMgv=10C}|GHz(`NY@~1M`jqQSpeJ2z4&d{70 z&H)WfaAF3RaK=zfO#`uL0!-XO$=SHs9}Lfg69MANL1@hVcLuw`SEgcbWa@q=!Jcs1 ztWsJfLAw9#$lxw(SG`ZTVyTc-Pu#BhXcU2KhM1UUe;vME^=ROB!PhbA9&*apfgu+6 zr)^}yylBZ1c${et>IOd(aAyc5&d-L-!Ehi6vp$*L z?#RqQYtLP$V(ws?htqQzpbHU?2|%n)a>>v%EV6&VfkhsT1`VeHLeT=V0et?^I{kuyxAe8=Oe^rfS+lEfX~X zOHFb(G8`Z9G*9^!nwu4GuW9SV@I2s|1ou66s6P-F8BMSKCz zQcu4in6dgMdk2FpHIx0G-tImRIJLFT_3Ce11MI|377Zn=_%WpqIi6 zOh4C#3WX!$kWr^bpt4+Sv;N2B9Uty>u2*19`eDmu3hG+NT&z)Sky_q;!@}dvwXDS& zWeV!Lj=5Mt^nCo_ZytTo8Rr_-Vg;Fkx+++UU2hQk)_u6=KQ8Zdnplf9$Q0C7&RXoI zGI8L?gvPB-BWtl`G6i)Bti@Jg(QDX2n(H*M7K>#H>MCO`R^Y|mcID^9ti|#&1-0{P zu_nap96(#J*T$*k=?D7L`_=K^iXi6yJ-8EJ&wq=54R`?W<$lV&i;Hu$=#S`==n`su zqT%l{v(l+zgJ`{<{9f(c$x5dUG6i+*U@g|PUUa;jjJ#^+cGhCo%M{eLjkVbFbze|9utZ}V)Y!(`O@VCyJSc_dNQ&889ti@JbCpx~Pe%mV6Vy}}a zsB1H8v8FYmLsWXIa}#T^Yh()Qx`DOW@(R)ZW%B!qb0cf96*2{NUC&yq(InRW9A?#) zcRMRti#5pe-Q z(tC3(J`U$AC1YlymQIjR8sqU;AQXoHNPvMDEdaMNP@&IGe+uWeS>;5ceSf z-sjN%9MsmjlY)kN7aQuP*zEX3l2kWcQA^z!1I5OfZezp7E5S|(k7^!;IzxmmbGtt} z>yOW;v0E+uWm;XWY}mVFa}ZEKL7a!xQs09$tcTdJ_74_DS=-NRC>blN2InYPQAaOF zv6@VbJ*$_*O@SB$`6RX2)bT%n@VjvjE`u|;C=sOvCmvBGB2{CZU+Kg3$>W|@LIZcZ1gTUy+N zz4q#KfqJ1f{ptOVgRDjIH=uj%2Uv@`;m=9u{xpzYCDKOm)mLU7_*TVn*S>Tu^@48Q zD5qifvcj&tUKArL|G$T|%Gb*j)U}(r*htas3#-lfe;t2^o0j#z8D%^LDmnDhXn_3Ta!*-|&WRl~WOVhVh+x|3Tp)GHKIpz(FL z-$b!-CPogbfS8icQ(7$pH)@q-{r(v_B_FM&zPCz4%lOICg31z{wX3H86IrLP3bR=E z#p7hUcHYK1eVJtn>bjM+Skq4N==w86_wGE-TI^1lg1T;DEw+4zSo3{#oo`?*c85$s zT`tyQjoU@rz3S5BWG!~POhH}sti@Jt6FYn=4_C)p>^7N#x@wt=-72=cT-A2guok;j zrl2kdbFo{*mQ$*3*Unn(7MX&&Y^=quze()&Uly;9JfP)++CpDX6QRx!8T8O;o;}vyHXbeKG}gwXzmlu~$4whCghpwy+kv zSEit@X4Ya&d&FA0_2ywMc8^R!U2fK5%Xf=)isX2M^8{X{BpW*}jb=}}Z`Kep@>W?Y%Uqm}?rMtDWMULtG<{_*M%$T#Dn?LNGRj#? zm6iFtpsjx3fQFq>Yn`R7T!PkdX1_*SMrQko)RiD})DG^`u&F~Lu67Qv z7JEphpss$_VuhPU^LN#6+s9h$%`ydb^rnl|%L;F)27PW%xrm<0|?DTQqi}t#oL}85jLR zTQq>tde*LCTyrE2Xw#ae8w zOhN6F>0&vZHloIw>uuq5d3u3re(st`7o_)#R5--icdCrlc)C2junw7my2eJDdswOE@>L0zM)#Tu>RQQCL?ti@Vo3aZNg!$kf+ z&Hpdh{&#YJ;@-{;aTwPB;T0_ZC!1Dl3IsRhI$z@|PJxXx@o+@dB{(1ePp_!elyPmz z(of{z1pUNq8qOm16S)X$dvDbcGFCE6Kaq>m(S4laEM7m6i_tcGi;5BS6HYd@%u@YC z&O&9Y?`_afGg|3oDJODqI?lK>qOP)L0{DyCL8pd&)ie{pT-ZD6HM}cjtZa1uZzQw+ z7q-2!_%7t%m_ zS%(JkRpZLmu4^`3OTD058{{->k`;EXORTw~`Zpz5tL%~~s4LD~tW$K*4fOM@#X4mQ zs`CH!2!94&!5w%V{}24j;RJwfa01{1+&FhL`Z;<8&7%7J@$ZtDUHmFqMF-&t!5>Rv z_VH_#DXdFkcJec|h&IZfNz7h;Ei#35Nz873<;|jl?$Aoietyj|g>^~Hj($duSd#*; zpWl4iDKUHcd1MOflFC{8WTjhd`GGo}NzA@}ZkfWmBxYy7$`hj7Ksy(+x8Dhw!n&j~ z<`y@Jp8aFw=aiWJ{hDM7>yoI$pMI~`^^Ia5-RzX81e!{> zhKx~AE3B!64q7*SLc__Z0KBUE7ksihb^V98S& z3MI|&S1(vMaD|fU^2-n(pi6x9c4dZkNvg{)I5+9$3%g_unAz!fZKrrE7#b4qTZ!50 zw^OFDE{WOgcdb{nQAL=lvE~n|)=^^i|80{gtfv2aGw$Sn%|F2R z@I3ckZV>E$-nAnAU+*bx&RUguu!hWoLmJN2oCo0`ow}2Q8tN79*nq2Y-KhbJjWgA; zt=k26Y^Vz?)YH=4uho-r(aus}HeW|>iCYG@noR`IQdk}^mC2};T?4R!HK%3Pd|Go2biS6N9(cwcS(|02E* z58*BR_xU&RA^sruGwwZH0^)!C0eu2VS6}~K60?(E;B;DXdF6nC9{H!fxsp`~IHl0yO_Wy}^})8z2MER@EnZsjao7Is!5c`FqO0y4ZiQAf2r9J7>wFMzlMF))D^I5+dIx_ zcvmK2l1g@rtR6CZ$Jtdw2I`92HDs%+t^gX=bf3|XGcp#1466X6_3UX4#73W0TTNOFnDKUHeos%i7OJaBV z8xiZe=+evV^EV<>SeL}^^mkUQkT_WQeCrm!xF z+3~O9j9B-6MJw%;m_7f_$Q0HkF}wbmPK(x`5M4{PQ)2f0J1tXKm&EM+SMC$lCBN4x zF?;{{WD4t&nBD)3!(!b>sr=9E|2HgCSe5_p&W!)B!T#R}@c+L8`@g%u{$D_kp%_flnNlDXdFk zb_Nv2!~6oLUt%-(=wGKFG@tS6dxoyUd{hc zFIbT1cvaUwhR+cY@1IfnkV{fs{}}qg)_|-5GdusS9Ti0n-FRg7{u`AktV?2c|6A)9 zkDa6Q9 z(5{RR0%W${h=y!c)RnbFT{)%UTupT)ME|clIjNyup}G_Ry#ngW_StAi zHLiq;T6!k5iZW`%vh zO=X#b&^8oQ5rWQ=Q7|oJSecQhY5ebE`2Tb1^+4}ZT`?m%6hE*Er^M{#HzQM6m&EMm zXNrl|G^fPu=NFSHtV?2c^ec~wHEEq6v!`EFrm!xF+11ZDEjrTsf6Tsq(=vs1NzBfE zl@YP|yHvf(?ClqkDXdExWbIs)Q(~*K3h#7E%>I5;GKFs#X%btE;Y|Ty})6a!m8^(`2V~S_Wyp#KgbX972Nx{eqf*8#v1iM z!v%EBufqHnq0Hy}e=mQTzmuQiXZSGh2MgmKzKw6>oxGK==6CTo@s)f9cpm==;tu?r`!QG_ ze~0@T_Zasnh(hoP_YUq&-2L23xO+H>TYz232p56~#Y0>-*Tyw+PR`0zbGx{kxJu3h z^VaXtFVVlFAENIXCd-hCiy6Wsi5ezIGA)dkA&%oC!kCgy2_Yq&6oMob!a{(g#tC6m z`FmXOD}Rp(=ae)gjF7Y}Ae<#>`KWNalKKs22>Nq|($NxgPcGe66%!HX{aUW>kTy| zsWUi8Qfsi2q{d()iNjzeiQQl!iOq0~Bv#=lf#)s45hXn)99Gh!!Xc6hM}(V6YCJ4d zD}Nsn4k~}&EF4hMYGFS~%MJ+pNLs#M*sG-b40{Ory@uT+*<;v6lHCR~Np=}_lEiG- zL6V(@?IhV@*hZ4=!d3#$ZxgmC=~m$;CEX(2NK)Y@p^Bu&8->lv-&MjU!1aAJHCFB2B_&@Of!{5W- zz+VAx;7{NW;CI3Je;vdHc>%tJpM{e+f`f2IP#)KN^EdEoz{=?N+%I6Hf1mpX z_azwTAA<8n9)MB)BJMJG2X`KP56*G}+)3^P=YsKlklV>^;?}_E{vY(8Ft)#gz6zC2 z|BMp!WRB<2-x($7%p5PEuNWoh&m3<=Up7jlOT)b{870!G;ocXG66w~Ed(0@2jtxZl zyip=un=eP7GfJd$L+-PJ1l`-jqt6Hu^l!*~T9BZFL*`LIf*uZ;PYDuqamdJmMEW?7 zJ}F3~m-Fav1&Q=?9(}?nK~E<&{J2qq&Q5Y4GfL3kN$#UY3A#L~;YW-T^mtAl6#v` zf>A+oZ#7CVFh~vGVw7NPka|66l*sUadk+{TGC<(on~f3~B9MENQ6hr`YW)VIM1~2} z_4P)H3>3({PLN=zK(VhCBp54@d5s{!V1dl51qntAWbPLv7%q@`l^~Jv!lPFT5*aZ( zdW9g7F~g&m8zmSuq=qjuN-%Ip?mnXgV~6BkYLsC3kQ%UB7+F-y~rq$ zVFdSHXq3o6g4_#?5*bQR>*pIKGMJ#Q&ofG7I6>|nL4qex>~jSPMigYO2oh21E+gCA}m(TS*s%XDMk)xI;-L;{w6Oqvr_okm2~dgo{dgr!YrS;n~6kk{X{S%qoB1 zAtaT*7lee8&I@spmdy$0Nm_nEm{HPMLktSwl7=Wr5{7A##0?RWoHtC7WJcBhuhjlm zbNAP)#fUwc8UBL%|| zM-sE2{{@-Cx+KlfU$1z#c2>MCZ2_Y>{Objtl_{)CnqU>fE0SUzjiN4%E5HojDJfG} zm&ES+pAc)w77|1pl$d@06EcN$N$k%5anY)5YJ>j|v-f{orm!xF-TnW(Xnz^qHe>ex zKQB{QGX5vgT>k%h;1J5t|DxZb|3W{3 zMv=6#?2+5ui;K0a^{fFA=!_m?a_?0H;Lc0>UP7*sb#a_=L*;<0h5==Nnc2pBGjB zpTAAz|M^>0{+~ZC0B;2Ja0$Q~LF!Z`fO=H|s8c0?T2%t5Q6&I}DgoFHzz7j}0JtC` z4-giJ!xokQ$H!FuA0JiufBuNd|MQ1c{+~aj^8ftJD*w+{tNef2egU{1sK-840NASv z0DDvcV7Dp&>{0~)vnl}WR0V(?2H( zLF)fD|5g5T{KxtC!6^c-2k!rQFy1dfEWuIW{=4}W{#L$*Kg{ppx9}Tar2h#l41NOK z|37kH6%-03N_Cw|$1j6-0 z=4+Jc$LQ~kgz4vajJ|3lTtCTyU;%7DVbVab0KT8(K(GMDpX5NWAiW!MAXt$84LJ}j zfcGbCBZw0U^It~931I%AFc2qz_lFFK6TtdI2E+;A{2>G41nKWQ5hqBGSHubE^N;~? z0=Rz2fHblN<;U!1j|I2ob>dlN<;U!1$9K z2ob>fle&WF0IWaBf#{&}{vbL4?+>Md=m4xgWI%KP&L1)$IuN;85gmZi0Y&aQ-B74`uzIOIiOb!h%xp-GuXp3S1_fKctrk=MU*3;rt;@5zZe{$w)YV z_{PssS$}+&%KGCwRo0(>w#xeR&k}(3hvn!F0eF8%7X)DbA)Ob1`-f@of&lD4q_Zmj z--Phh?EjyGn$)pY=-QA%DwoLXcB-pH&k@_+s33DmG{}d3X!IOe17;5MQF)ix`YFmM zGROJ2OQx_ci8;_m<(;DE`^4x6HmH~*ecUNiSeK-Q`q0bHuYb1K{|7amiljyS&SWdV!xMQ&^XDfmH~px9M!lAJ0W#sCq&Ae)41weA+X0-KOkjJZCouoCBR$TA*Ii4M5dMP ztdX72UI}SCLaSn_R7Wh8?$U6srlk^mwz`vdYN%IesRXUB8-6y$wmOzdnJs3pRC<#P{+y zaNh!5{26WsNYM|VF?6sYMdSVGU#Bb3b4B|n6f<;JiU$1A4~?EHQ{aXajrgM<*o3Z# zwf8EoMCfAj+z(Phz* zZZ!@2qaPYwmML&UipKrX4_tvRi8bod=Smq>#h-p?bV;Vb4JjJ=M?bI$Es8e90Otl* z%D`OoqD+AsQe`Tmp}**IloB1P*MTdAnTt-z6u2QpqyOkH+K434TBYzBt`rUbqaPYc zG6g2%e+`89$8QG7UVy#35%B+e7~+5bFWCPuo%)mE3%aGu>8y;4f7(K`advz*kyMRS z>R}D-lQL(tGM=QHHTC6Tb@WK-!&*%y#tBuk)6BzZJ0;OGTESFv6-<>fb2XSMN%VMi zK+Qi z%$e#d#RdjTQu<7F=0jheAP5xDu|{40#mxHuWmx}D@n+D$Kf)!!|Nr;sW9UvKW=3~z zt|3KZ|LC85HlY`b1FCnLD@8*9>4!!ymML&UN{s}>=?AVtFB02{U);=tu9O-J2(&7C zWm5DanF2SYXe=Q8zyf-qxSvSFkG{y2qQQXlL!%eU6u2fuqXF?qub8(5y#RY1DH;e! zKOB0&pWa`SqA`H2F2RB+LfZgfAmA6=gAbfAw{GA=m%bl?h$LwD*7Z>iiZEu4~_1T zDX_Z!!}-4t;gb;m`%U~w?*F*Aavt<6^fILT{ND~Kb3E(HX1w|%WX4M$&$`+(9$>HO zmNKWaGOnV9j5Glys3tNev#z>o0@xo=O>EXyyVX)n5VopA+N9xK>31Wli5oO*tD>5y zC#nf)qlR=fbrbO2>P|}6Yq(daoB(Rpot7#oI?i;QEmBbdKdM~D(rW3GHfWV)WP!63 z6uDSc``-;nkNi(#{OF%#D$uLMI`tF?SBeJt(GQJYB~##r6piwuAJ~LmDLRG~%NtjU zhWXJCjb15J;D!{9^P?ZQ9KAw3R7XIkA7(M3Yh{oq-dlc{lG@_a?zo3YOWLw z^`jpeyz z`(z5-klL#NGjI=;=%u3jJ|*0NE47EY=$FbAxFJO&{^)P@dh`;p_jk0L(ZE0Yq0viZ z3as+~jtu{wumAf2=>Ht8umj!9C{<+3Dt-nES&lq#8h&=394==CxMZb;FnANqmUqSuM_ z5L5tqg)2qFe&~lruahZoLyE@z&<|XJUMt$4O?ACA@P~eA^jet$H>7G=2cZeQMzp6} zO+$Y$6#W{R0ym^+><|6G<>=MoG1Ugom7>8v^h2Xp%M`dFMWcV{2R5Sn#k#bvmxlk) z4~_1ZDX_}_I}v^#*!HjCALa+aF8|G38~QbRHF9Q;`n>#EDShxNXF9QBW~VPSlMGFd zhT^Kq&k3I6gVJ7&-(gf8W$`;p5L@c|q|9lojEq!TJ-HMsXu2}TvNBdTOGCL7v7=ww zMd@HNF&@jSDJhqtwfQ8oiWZcKjC}P{m&#nO(gq~nsUc^S1hO=hd00VLxkKv_tE{U8 zNdvy%b`3q_7hK-662uJ92X504uT;t+y2`Dqht007|BvC9zz)D>@ZEb2e-`wAKjhxR zO>j4Zx6OM{Ja_$XNYNNS`X`)9^mei3&5ALID@B9+=!Zscmnm>Vibna-4_t}fCc3vN zS{hf1hWXJCjov0x;D!{9^P?ZQ61`P)H!9Xft`rUQqaPZ*Ri?lVDH`cVKk$0=7ICnS z*u%HFQZ(R?erWU-nF2SYXxtzDz?;y6V*gjErn7~aD}GR>zzr!i3J?S9R)roA+ZCJm zJ6tIbbI}jT6u2SfW@SSJ^k#9-dfMs}%tgOhroc5R8VN{0le`7JiRgD}5Fq_<=uK(+ zABg|?2p#|r-uLl7SpVP3wL;wAhgTl|vqQ?9+q$xu&q)GbN|_T|SBwp2<;)?iE5$~1 zmgz%US6gQZyb$Ot4{LM&D(EZ$CFm>T`3y!M?W-rPx^@6h7^tYqaU~&y<4=U)loF) zkA7(MZkYl%q-fM1{lG@_F43ks!MRd2?2mqE^e&kKH>7CXAN{};=wY$$-xNPMSBeJy z(GQItmML&Uibnp?4{Sp36ptCG)`N!r(GQK@DO2Ev6pj6(AGjPnBs$)!#94KvXz(BX z(C8tV0ym^+^dJ4eM)VG`ri-#0H2jZ#X!H)50;}@>Q4;^JmH#i;`*#87|01p)JnWx` zj^@eyXjr1Bu$q{K#;Z+iR*N#RWlm#V*~A9uEOlq3^zo~l>9!T?C%OX3SXw(lYHR72 zGN-UIO66JFi9ECxF!8C=W_L!5q_l1#A7^d9RIA}+tZJ5KA|I`zSE`|CO(w>bQ`1Z2 zBemTwIaH*em0%R{mue-JXgzH8gOXjt&&ZTyDJGU82Hk{BBQj&f3n>rG{&9aenpTDp z-Go&`ziPS(U?-^jzY_Z&>ie&M^4WwwEcR_srWRL;D(eM34lpH{E+BP zn;xmL09bYvJ|t7%h7^qjpugS%`k=T!Z4FI>0qBQDACxI@O^QYX&@bn0K_4LUAq@ne z9}azh*#FQN0Q$jr^fzR$hr0jkheLlu_Igy$f0nQEe(_af$~SFDg;;sit?2!-1|4LD zUW?u*+MYvua)4Rpf1gZ&8&cH!Uw?;Kiyjf3YTN-=in{;nhenUc6j;^&>EQnt5i4~n zkSx}wO_bQZ{A8H|H>9YSpZ@ll&?m*CWDo8)pLV6Fo1cDY^huclH>9YapMKzS^tWP7 zS`S1W{q#elzm+L)LyCI(=?6BVPl&Z?*8}S6rym-9LZ-kCDeCK|AGi{ITx>l@M*?;B z(+`b4E>qx!6!rGg4_t{pCN{ra3H|R%QFlN6(CA|_1#U=De?R@emFT0Q=MlyB(3PSN zfBK=(M`a4!kfI)c`hnM@kBEacRO+FQfBK=(M`Q}D^8Y;uzZ3lb3=sdbhx;{X{)add zod46Kd{DN_87Xr}>*_K>z;_cy$g}E<5Mk&+9q85&uSl(=RI@`mv3l5`ZcVF(ji@Rc zHEfKM(bWyg2^~RIDQehPuBs#`K~;I1hH|x4l~4dNue?<&fKe*w4hN<}LG$d4c|0Bq zgyP{)LgCs8UQksY*YK{iPZ3q+EgCjPfvcdZ5~^sU`aeedpGD0VP_GF(kG=pr;iBdR zs22`>0eHei)&Gx$C;Ytl%6}m?AwO$aRNem=goLf=^Rfob?EkkGeNL>QTF*sx|G&@4 z6u4oL+5c}X`m9)|ED1YZi|qb?pOqDR9GL87qfdjvf_l)Xa2|-T&`VnF2Q~7UBQ*DY2gFdKcOK|2`#CV0Hb6 z_#f}U9sF+~YG*4%<$N*Mh<=1#e6^?_8K$5^N}tcVLicv7RdH{pr?Y~FaaATctFj+M z&t+X@wWU>2TV~E@U7^|%+Fy6O`Ff%=FBpI{}=`OfvTwXC@>WMD41+u%6ZggV{5|Ns2YBl_r}lXLp(>gun~ zdC&L0Clu^X?(`la)JNUqp~303`-aEH)-H}4tm{nf@Zu7`Hy5~E6128E+2#c$E_NPr zxg>6NSF%;cEh{6Q0P~>BB~inBk}V=CVwc3@|4XsUMaXElE|zTeq7&CV54l_%R`UPx z`A<3jW3Q#|loiSk&8DA-0ZlfY^Y=qXIvX~b^H-tVV>bVjXwukp&fh(bbT(`<=dVJ! z+iZHTs1I#A=kIPuIvcjf`SZ(FT&dh;#(pC&xX1kS>#f}7NN2k#gD?f0gvtbJ^AR%>It$g2%Tq&>sTaW=MS>yK|>1^0!E}*8|X|9vUkZn2_ z@J>fM*VrBx&~K8wM)@A%ySB#y^y{a5Pn`eRt=ypmUI;uLcr@?;s{j9I;Ol`ep&rmD z0@tAOe+HGmrvpQQOFZSjji~u~YT(s@GCVKzqWTB*H|hiG50@za9a1N7j`&`+OKnrP zs&(r5>Y2+Z|9u1Qz&YbL!bjkJ@J_Jc@UqH(ufkj4CFL3AapfW9KII3f{C9_LamndY zc)f0+^c+et!v@Xb+H-KOW^wH~I7hQkd#+4{vo#B)=g>Gyvrgg0nVN;dbNI@7%|hKd zG%7U>LV6!>PK3 zx^wKcHM)hubIf|JZgI)!5?HNUTynY;PSGvYo@1+CrCD5j4o=oA6raQQKS{Gtdk&3P zYZgk+p;4h(s62wd5Xd{8mdQxbc{tjI)TU@*6$5TUd|k$zEYSq9=Qpr6~{V$!=!p%CGfgOuY6h zJ=rB*dq_`q3hO~V*}<$50D!bykNg| z-WugWRv&tdV84FKgLwYO_PD@)-BcVWhrJ$;1?<;P`6cW12>vhSTls}~)ia#vWY~iK zOSmCURes@k-emr7x$=NnuM3iuP3QkU;7Dh~Ci8#Gm7kk6EZ@5NntGeg|NXfmo!Rfd zSNXcA{~tm1|GNXDtwz-bock5VDSvC#)#?d2cVMyd|Ei;DplLGb#&gkca^|KoxXgTV414a6Y0?mWXFxb8f| zAh_;4AO;~yf5ad}>5mwMDE$$G5UTSGgW#(30fs?fg~-;57=%!rXBdP~oktA9t^ZH; z>i;!f{r_57|Cb>0AnN}TL>^fEk4rpRW92^@CuywuN8{BREB?`_&{*w{#)%p${n1$I zR{kf*%AX+*SoyPcA@YEwKeG^dz}lZ#h&(VUdc3ZPJTNhOv=Di~@}F6VJc#FP2I37^`=fz)1D5`1Al`tL zKN^TPh{7N71}yw>T?}u)%AZ+?H(=?{EW{hI_Q!QGya9`UW+C2y)jzWkZ@}`OS%^2t z`XBKIUjLUcl!4d(B?_VpSpVa@BFcc}KN^TKVD*m%q6}F4qw%E1YJW5kWx&!O4MZ8R z@<#(v1}yy1K$HRNezq<|8L;qY7NQJT`7`Sgot6Gw;*zmVyFb)x>)4QeC20N0@Y*Ne)v%7xzV>?p=1$pc zx;{a?x9Ob!#~tZx*ksOssq$Mhc)QSqvgw@v-#XISu*sZ%U3tt5yMQa5^Z%G5oei7J z`L9rZWA0e+NPT2=^ODIvaKqwVbR_9yJ@z<|tgf-AI1xM;+;G*bVfze#C6( z7jm*To%8>QBb^P~Fv&BKfdy{fd zdoh5^#hWSvF6Gqrf`H249 zT=~6O`;H437p~r>^Z$SENN2;&ga7}uSr?O^n$G`!+L6wNod^H_DYLpwerh`Z|0zd0 z8+IQ2|KFLLXJoEN=l}oCk53|Xf-wNeLv&B9CN9X*$=tyV7CUbr(lo!kvb`b9LJL+vZ=l2CiIvcjf`SrU4 zu2lYPc8alo^|r_S_3N$t*^$nM?eTy8I0D#)b3e7;hvXXNPprS=v4H*h zDSvYHzYzbsHPEfTpx&hJK>Y7!*oAX;Km5x0-=3s9kBxi);8=ZHp8B(NCpVI<&~!(H z#6E3qBwKL-OHr<1>Ytm@Mnr#>@6(F7nDid;QojOHXb}wo%fntYBJi~gV9A3r)vH5Z z(4qmBJT_Cky3C7R2w*8bKZ?T~l;MgY4zsAdWa2Qh-fD@Z@H}ytMPZpZ%#0T{PjQ&V zF=hY1cA@@vT&;x1;3gP>^Kc*doHDD_E-d+t6kQDf6mxTh?D~_{06=l1vtj3<2Jknt zHYl+Yx*EXW9O-P>d8h&Wj~U{*=>_#RT@B!W9O-P>d8h&Ww;6Ji54sw_e>>8-+V<1{ zpcHW#9K(sYOlWPWroOtmKD1?-msTkMwW`YYWB~lSDgX79VUzj)Wy)X8DxPC?*4uRc z|6d*HY}jP}f2s1axk+lh+jRc_%Z_w5Y%>2}S6(u!Zj<*ko&W!mBb^2Q7Zv*7r>I{= z?Oz=}iRb_Rs@$kV7yjyB`QN&&+*CH|(=XSkX&T%&odW?`8?e*Ty`7KvymA{gOM=#R zTbdV?xY&7s<&wD7UDk0jZvKs$B~inBtP&9wF~x$}Ws4pI$S$)2UiAFgWsAdB@3mC# z5*HJ&6d6ZtkCmIOTpYksq#IQ`tlW_0LbELfSmwqn7aR=%%iKieLZgMYi2A>P^M5H% z3&3}M2&HD1RLD$uiU5ASq12JiMoQ=c5TL!)pqr6zbDnyAN(ciG>O;^S>1?FPx&Rtz z=2`MFl_Ki`faXZ&nv|yt;J40O1IJ-~nDP_>{QALhc>XWtX#x0kQ}JxlyQ)*VTlxF- zgA&AgQ=04M`-TXZSAUUTSR*Cq|AZUjR0uepH<|xm4yt*fq#mTm{C`j#>1?D_x90cz zsmlS(nz#U2Qb4|D;7Dij{NJert_yS^|NlC*6VLx&4Nb}uIR9hkD?jn)|BW41Zd6-g zI}>)Ja1YK6XDc)sHfAh$He0cg3f`Epa>L# z?f~Ut9IaZMR)&J&`S0WWPm0X>UjZk25+x}z=O0dVB}$AGne$%;E6qCDgiDb*|FF`L z&PIyN`7ebN%+TvZV_6 zFdzR}eTvNahvOaTY^2DX{|b1O*(e1UQ)JFRyvmWzMvBb&uYeV1(~yuGNRc`Ju)>kf zM#|&-`+feegfcUBGf#W!Qy%l*uQ!xA(pmieiT(eS#|8GA+o|vxmJ@m`V84Fw8kQ3Z z{x2m{Uu|CfWxN+xTwtUG{g;#zu68_cGXJ+6PBAx0C9D*g{|l!$(%DFn`M>3`%Dl+^ z5t8}8u*#9nMvBh=g_F(j`$Ydgbq+ZlJ=u}YMvBh=g_F$SdT|IZMdts)Nse?jQgr?= zyxQD+mE85n{9kyrBb|*@B{l!op~4LPMgIQRkuP|KBb`P5-@@>}IP!Turfx_5uMgq; zzvq>!l$QC~{ju$xR&F+1foV?_racMEoy7*!>2%A;U`6_-`-jAl!YVvY-f89Lu@TY4 zdF`16L4$SeR&FfYIkb5LE=0L7Vr{3D8>&uR<~)3IVbJPbmOER$tc+Nxc+yu3Lx%TS za-e!K4pT19Q1LLOC5NjQbeM8UT*M}GGu4ZXP4dH(R&KO<(XdG@46!cGO<6CrE)L87 zA5s4+Mdtigz*@7FB}(VMP@f`meqpU6osATk^IHLDm<^9f%#Y0Zg)o`SqLOt%TFeSdF~k9`o0)H=O23XCvkDfBibIg4da`v*cTQ zEMULh@H$628!5pBCZukw;Z!q{5Hy68AOlmf##0^XY^2CsU=7xo>s)-7%ms!uj&!a` zd0b$>N%9(aEyH&`7O-DGc&&T>C#Im<7d!v|N!0)NGhBtw|Gz4qR$|9~<{#}(d#v1q zwxVr)_~6E^FE^~Mu&u9(31C>cDQyKu!?m65wYmz8#^B1cyl5BEvWZtXScTWIW|x&4 ztxQY;B9qgD3#6_Q_QH?|SFZOKTy(e+GeS%QD!n%-7F>yO@h({B#Vz!EV{oOMs$49% z5^q$5E9ErhBEyyVz4@R2m!k9k;9PSPo5ArP@2O9b`G0V(Bb|*Do&N{tnALsa9B7Kn z|ATWJ>1?Fv{69F`4EM^Kkj(#svmNPdr0D!VILoZQU8Wmk{vVv>NM|EO=KrmLGtH1& z|C9NDaHb=jjTD*xR|f0N@ICT2BlG`Yy(68C6q)~53YBJ^3q6wge^BX2XCt+ZUjMH% z!~GKV*h;Sd*E!Nz;D3em|KCJi`YH&TUGiXB%3}oi&JTl*bT(3g7evYrH<>$bkhOpy z22t|EO^$RnQe<9`2Ire+v+s)`Y{a_>O|9e~@ zziuj?8bz$vV*&Z~gYy{HEBHT@Z{-cS&SO5pmyPE_+@`u{J$_0XXFS-DziI9i6k z@bykh4s9$}(o@Zo9yzJ8$VpE%lfSTX!x{?&E2}wJ=?-Ts23WauYI=A~fR(sb1S@kR z)QKqIJg{;>PzF}!CaLEMRxXT)U}bKaI&qovz{-U|5v+8Fs^*qKR8* zh1F8Y$^}4Z7*DCnk?I(mcs_rgUgg4|2vg?9s23TgWJNIh{r4(gSx*1!BZ2n?-Wjk0 zM*>rU1M~F1&kIxrUK2PW;9vi%UJa?|s%NODsO6|9{a1Jvo`8qoCvYb?@Fi3mW%^(5 zh7?>5(=ZHg!s%mCXoNRH7|w;$;bd3=LjUU#<$mRE<=ePi{*PvH>1oJl7MGrev~F?z zDHT$>#r3CDuysr7Pg%Of^`|sQ>K50Z(&6p8#r3B&c$;o<{V5&Zs#{!tN{1_SOX^Qu zu3M--#gwn-bPENjm~}+6xB?X%)-0|-1&1_?D^S5@n#C2U;Gkx41uB@;EUrKWGn$18 zRH$S%tyx@p8s4m1Tz^W1DcutKQ!uGpLVpS-bc^dxX)vx^Tz^W3G2P<&QyPrw7T2HB zVMMpM{*(^Gy2bUUOW=TRasBC1*soh$f4UR~HH%A7!GLCQ2`cE`!tJ7P(hz& zaS1ATlV)-0X}DCgP=N|peTinF02LY+>z2@;g1x#W^rv8tZVCMui zhuylx^`|uG)h(_+r9+QyHL$CxL$_{iXI2Sx>DD%Jg*$a?tFSsW%j9pnOS3NGM!RNR z$c?CGUBHcqX1$RcJ2lJT#tzM@=SG`mZQ(|%Zq>1fDzxa`{uZRBtJM$I~p8-`}R zfgAOj^?Giga@_`QpmN>0+(6~JbGU)Zb!T$}mFv!85mlyKcc!pVxo*9%P`R#>SsGKW zTgNONP`Pfccny{7&JeGma^2~|Lgl*Cm{kIA5c$8K^FL(H?+U0hYriOQU^3?y>Ky58 zq{y7#GT3a^{#>4pBy)aYvm>326q)l|3bm#?6E;QW{6ekc&V)^oIlns8m^JPNCv$$G z#*xlOip=?~fNHa~La@71WX>;CJJQ)mkvYE=P-Ql+lD8R|^9xmubT(3C&Tj>T&E`7U zgeP-;A?!$JBjs^^{g%NiA!K%srm>WLx<1uMuKz=hbQbwP&j0*;Afdjb-YE6IFM;Eb z|C7)B&rVAYYb+=UT9CDcF$u~IXe@FPgtZ3pNOE%;i`KBf9fy-=}J1nC} zP&G!wwbd6b8f#c=@uC&CVS(2X8y0n5^ddAYFeElCHhUq9-mt)`5E~Y?-WwFFVS#b+ zE~xS17J5$wrT!oB{9jseF+ac9jtVBjggoA_Uq3JzCL|cY6inzM^Xi>k63$2o!Y>IE zy2$ao$&BA}xX`R|F(ESJ7cO+9vymb*e#_wkv)VlgM`rxO1&(w!QXb>i@8f6%ywR-V z;O2_@lt=va>kV&oq_dHt^MAoGgA+n{BSq%_g5gMKBSq)`LcLkzcDTs=U#NGavyr0n ze_@MReKxr-U@FKlt7v#9@3|NF(j9|DgBeu4A9z8m;z;C7q`dTrqSSpU!0|K1VU z7T6Nl6gUg#e^mr@)C+!2eNz3^0{!o6afZl0sQ@x`lt%E&LI#5QRU&6+(EL!4Fe?njV9)cpujh`Jv^ z3Q_kXNFnNe1Sv$_k06Dp`w^rN!qW_-;DvvHffT&(4=|8I7XAoQ2;pf4Qt-lGWgrDF z{8a{0@WNkZAO$b{OBggE3x9+ru<*w(5upjJ`_VvXLX`aoO^C7|p$Sp;BQzn(euO4O z*^khKDEkqbz_K4jvKcgibw9HZn&5?hfI$KLx8h2JyQ~IprNp#-Uhg8(=Y!$n~_<=8jqC;??f5wt4Z0 zOPtrYn7_p8sI^swEGs*Uw#0cytaEF`o`op_7Ncjeu(GZb_#g57Z<@^cuYeY_xss(y z->gs5IsYw=bT-mt&VL0on=Sj~ThlrJ&5m?7(qztm1vHtBzZFSZn$G!ea-_47_Bj83 zQ|gt_Xm0Fccv;h1T$!lOMp6^J5 z`=);VV5>O)d!ph4|4*-VYkt4qq{%^#Q&PJNd|6Kt)%}w%jQ<~2I z-RVeYBTeT2mcb6Q=C+UHo4>g}P3QmaaHO-5Ci8zwq0QVZ@&7cP|J&wBXCqDK|LV|c z26r+QoxAGObpCIvBb`P5A5pN{--)xlQU3$Gg15j5ww z``3JHsZV=~0Diq;w2GO@$ctdeWW>fL}j|VXr4G`2Un| zrOUi#E5DnJw4nbJZirK%%kjL){Qq+3G`HR&n(*oMZu;%}PVRK1vymqA|I4AnG`nS` zL+Af@IMP`>|FQpnYhVu|`nRZiRRy2_7i0hb6M6OjcUf=pcB~?7g0*}T%#CR)`X-23 z2DWPR9@%zOpc3QaqtG4MRwz)(a-mL3PFpV)P+7}?N;zY_$Ur52Z+Of)H)p*-pb|en z(%{^=>OeiZy9`h{KhF#WJFMJjb>h+8c}V5_n8CWJl^d{5!~tEXazVh_4l6ffow&$( zSmlD4)dK(XQ~#Uv`u|O4-FL-wNwMw-m|*WqHb_U$sUqI3Q)cBHeBCUgEPV6WLKoA7Bm=YOvw zosBe^^Irjb%;wJu{$HBT`QPJ6XCqDK{8vE2Y^szn6P@#)aHO-5_Bj83lZTZMH)Fb> zlchc8zh7^NJJMM^{}W2!V}V#eQ}0kO#pnO6(4qWIxkX9vdnwQViCVcaZFz5hW3s$P z(Rtkda+BH$+5YCmtZuj5d2NLt7V{#8cUy8s8{!JYcG^Fxrt|;zJJQ)mlllMUFlcVB7wR`@I{$yrkhYYFzb5I`TzZnbT-m?@c;Lj=ezj-c5?l{&ymhXIuHJTpBd&o)|2bg zbpC&zBb~k9KV|-3n#}pHfGM*@o{CM=Isa3RbT-mt&VL0=noVwgNay@dI?~xllR5tt zFk!abCqWuI=YPVH&PLkf{QJ%DR>HX1eY}t%PJ7IMzuqwJNM|GM@&ElguYxhNt6e7K zo(h0pZy0l=vym2B0EDf3HH?}&B`ZEH!~iHyjZsHB8)>o@fCeMxdbjSQYXOWn(zzz> zX#x0sNUnilhVObR0Dk>o*wz2S`QNt&cA@^)4eBmD|Mw}>|9V0BphBAey~E0lXe%s- z+PslvJ?@0IV(ax|bnMjR2DA|~>E${#7!o@*x#4U@2Pv`qL69;xU%gm-5>{62u-pOb zg@TkA4M9pdS-n^w+&&+j|9hDuosD!J{NICSkT*9ks87@RzXu)ZY^2Hj-xV-xhO!dtrSpGh9qDYO z$^73km@(^a{xfD-8|u?^{_l({h`MDOG+PfII5;ya zPN~%4Ve&3p@o*60h`M>Z-@>RtJO-4T%|^@w+$PPUkhNV_Za^Dxq4U}_i=tL{SnjCh zGOD-p6!3!qU zhi@}?s1hp@#9&H&_%=s68)-5xScA8k>s*+M&I^94Bb{r~9xvE$owo+AVD+KL2=?m- zSFrOx9v9fJn+k7XSg*$d_Ui|4VOX!=|5Cn{%gw8<=17>47W7}j4RI=5?s(p0{%<+V znWiLMr|JCPIY&AhX)^z}9FCY5oG(chbpG!VM>>n&zmNPsL+1RffGf?gE^@*Qo%3_0 zBb|*5ne$TySu=Q81<@koSzD?%?3GyD{~w<3AY{TY-Grsp9-+d zhDU`6Z-&nKu^j1aWK?SYzY>yW*R=v<&3Mco-w)BGBb`P5e~A+KT%a5Izn@nV@Dkhz zEy~Nvt;!{Nw*Ec09d>SdV_~^U9qZECxv`A}N2}Ul=LR(v8V&JE+nv)`Y`hX{pPF5E zZmv49N?8uC#E_v&?A%E8qT`iy46n5F9@JhT< zH^=ROSE?E`25{5x*x2B}+S#GOwf%k5YY$GX?K{vnJmHnWn|Io(7rQ{b5X>QMjV?7(S(lLISozj#4? zT75*lU%gxXmilE>54aH(gx{yWQ?*e4V@f@sUZTd-Hq}&X)$`O!^)>1V$S!&rCyYIg z`k(i~4^aQ(%kVk489oZ{gLlB&;W8M5eXtw#KO3MPLa6_F2AqQWA0;@CHN@6s7Q>W}^fJY9Rblvr&Np zjbCUs3Q(Z&fMyH*iJxn>(4V+pvxWY|&oo=;PyAG~h5p2Sx{dl1Oq1~^x{U%9%=)oz zqXGr9ex%!6f32=32=x~>Aa|wzP_`Ys)35rs< zQ@6PUMJaqwvr&Qq*Z7~BjS3WKd{?uD0L6DSTL@5mTeF1##kVwD2vB@evxNY~e`vN4 zp!kMvqW}e4p`+WVK*6l9>o%95P~mI3%_S%__^NJm2?`y)qT5`8LW6(TZ7xBf!32l){&En@doX!WT6gB`9#QU(jr!KykZf3k8bLYqn6J_?%{+ z%Ky%7n!Sb_pVjQwa^o|a{Tgn3TDMn=wce`RrwHp7-CiZEPwDo_%+lbKx_uI}bhuf! zUoBp{Nw+J+Yd7lliNd-;w^uT&1U|0YCoroNKBn8{!n$6wkLPcDoo2s^8`o;~3T}K< zv&*>g5zQ{;#)maq=f;OLTjRzxntdEMuGZ}m7Ewhh8ap7Yt8`lx)(3PO?`pQ{_v^O8 zES*WwyiL4@QZ#QBub~vp6~aO(nzt~k1m2@tmouxBiPFpo3q@&;@V7-#n#0^cQJO>C zKv9~@xPhWH2f2ZwG_%~uiu|8={wL#cf&JEYD*O}Hgc*+o?AH(e32VZP;Qvx;!uOaT zJiz5Yjf|lGl4`>DIG#6||6304Hn&R7e}>NgeYYc>jSQLpTMqwdUMO4889M*>A06pz zWb)wuzRTRqa21^OlA-f|-{nYWBa;XJ_nl^yY&~S?{NHyv(%H!5!T)`SS$lmmD{<>H zbpG!<9O-Oi^5FmegBh9-^?!!W|NRF?I*a`O^-AETK#TgU`hN97cnsbHwaR@;MtQyW zm_;LQ%Q=iiZo+P1O;|fOi?P6@W(y`Ym)P#?MJU@&%fFUsM05>K%?^(58=MwN7_K+i zW9Mct5~1RGy@(}&gLPf@NnUnG>>ar6h^0|$d+b+xQHjf)*O6EnxH@K6$iQV~A=-uZ zni5OHh7*49$~!1E4|>vNd@yxQ7?*Gz1Kd$yTnEAO|a6p$1eB6 z6TiWubHA|+l=weU|0hG{{8qpR&E~hVj{F(*89L|pgN}4IGGxwg1zcq|tQJg(44w0P zl_Q;v44Ly=0Ut0M0umphbACVINM|GCaen0FbkbU(Gj&?3&;KF+_m;rLf#cLqs2%Ve>T&i$ znQ~iR{NKHHrMFWRc_ZAyH^OyZ_@Zru_((@MaIF`(FgTDcbBCQ9*;sHiTw89qdZE!6 zRO!xEFE&((*Ab!0+;H_GK$RF0p~~E7^`b+Sm@gt!nVYm;tfQ3}7w-ah*m|K*B}PN2 zQqEZ~7F3Bhs{i?)nLPNv*O}GwI9i6z|GmzU&PFB={_nMB)sWa#|gYaQuqWb)wu ze$+glNkTl8uFuf3`o3_rQ1HEAaUx_Lx{ z^#2EqLH`JA|6XIrKN?SK4ERUmDUIR&X#7rNus<44O4L69Pe{~10FO)5KLEd#sJ{x2 zNz`A3-$>M7g-3OU`ZEL>yBl11T4mi0#QoU{v8RE+KeMo>A&`IUX$a&Wdl~}y$DW2j z{;{Vaq^Gf`A*83Vr-8UXE)jbg2>YXfJq;;5jXe#H`UhA~gQNZd*3;mqzsh6AefORrB>K|a8437E-SSN#{{wnKaaMWLAoeYlp ztE`j3QGbaSw|LZJRC+$2$d z6>gNMzX~@<)V~C~77X=gYrWou{jYOj|7%^?|D!JK{}C7V|FAnc??VFrFXa56#|8FV z;jMz3&Dh^0R_rl?{d&XAj&wFMf)`AH_E*DAW}D;!W&|;q(p9+0kMdesBYx|I2t>V83oEe1i3QJQlEDKllXe^)$Iv ztnVA*?0xzs{$^#~shRf$CijA2aJKWY$CH|9;Go&PHY%{jIMztK4H| zTglh#dPh17{C_*c|Cu4RZb|#s~kC?wMbPC z24x2_s{mqv=-L8V4r{UUVDSYA)xFc8l=8V@1nhbcGB%!u+;`*_PHL+ zK}785buJc#4c2wo=Xha>)HJtmu{dUJ%s$(TNnGl@uEpZ8)t&ZPGHh8H5&!pk7K?+1 zFSgGVL9t^&M7@@FES7Xc!gW3NdM`c^p_tdSSQ^;B{#S;~`CkFInW5K;A)FaH=l?cG zIvW`>=f4a-YgWBO9>S$_{y*zTXCp)A{FlOK%v#BM%+NXipK+wKks)*bb@;Sd>lWd3 z&i|(!>1<@koc{{A)oh!Qm_MEKf2$*%jSQLdUjet6O-+JknxS+4Z*iovks)*bE8tUR z^Yc9SsL#+j|DSTCvyt&Q|9%sfmGDWk^CkJ?@|gdAz2TFNbQaJ5i@KL7XFaxf$Adl%52R!jH2MZ0RaE@sOajToKS4WGB`Ee?yl+T3JD z;%2>EGj-kxO_pq6uQoTJu~;n%_Uy(sK(+TkET|2@m$}2PDqb{PTexV^Sc@X$MI$Cb z&C`?pgVV!t?g8}gVltd3V>ffxP#S%g8xsc)c@7I`bV-R6!d=rNN_6r ztK)f-`TyncCG(Ea)yIJeL^&q(tzTJ_|MrMHe)}`=yGyFxN@06kQ|3B|Y zXCspb|NnF5`HVx0^FK0u>n=|Ak86Q-MyL=XrzL2rs}j(5SqK^FJ7ZFhb;P^;f%FpKEqWx3;)AzW^-+s=(qr``pVhX$wD?n741+Qo5$b)9x@ zpgIu>a8ZjTL2JA1++20yV&}mYOX5~{+3tk(GHNpH;TB7x=H`D>^#8JC&VL0sW~+NJ zi_ZCX9O-Oi$ejNQ_`2ELEjMX8=l|=DbT%?%&VL1b&6|3dp>zJf=1o1!c$|N~72Znt zsu^>+z#jA8uQzb*-*u$3k1<^4;QxQy46YM&lq{Y9|7}M)8(A{{e+7KYtd@6wmd^kGmLr{wtVT`3%ix=4 zjXZFbrSt#4=}2cITS9&7Quq&Z^BY91m!^;IhGsUIgG0Lxu$Y zSHSt7tj7iRo8hg3ADXdBA;OjQ7{PwM;fIcNHnM^jOv(@MF?arwu~hfgX9Y2sk{{mV zNM|EU<^^kTx4FJorX+M;@ZFAduE~15V840Z8n_Gd!>q>$_Ui|C;rXAe#|8H5ros;p z>&<#BV84Fw1H^i>g8xhTR=#guGtHA8BP-~?gd5^i_`c(Llli~paHm=C-cEG>@12fx zHnL>?Z#jI=yht7=%hLJ3-*cq1c>WuV|I?*Dhy0%w&i{!hPb*g|M(+8(jJ3yh=dqC= zqOY*mh525Wo5)s#y$(ZSlO{Kit>|baCM)>d&kblJCP7DS(qLSC;<lR)#rRDaR=n8LhRpgSD;LLQ zXk~7&a-L}AlAyBx=R^N5OXmEpfcwoL(?)3iOnsKl`Muwf&PJBZ`7MK=nc?eYB~0i1 z{>+iiMwZO^Erp+&HGk<~heGSKbk6Tj9qDXj$(&yu?lZ$~IZEgJ-sebXBTMG|R=`ip z=5}$WJ4@&M{=|{aMwZO^t$-h!Ew{)boX+|Eu_K+0?3vX1zXE<_Hr*-Npd+1)Y##i-Uz%0!u1Dwp{nC-nMm7)r-!IJY>tszw=l}h}k^JOhS=lnh4 zNM|EU=KNK_<7UGFR)@6IXX%{3#~tZxWXYVr3iz$r!p`HdT7b^^`>i9LjjYG{^PAzV zgvZS8_lr{xS&#YW*Bc&lq_dIr_A)j_`vZ3ez7_a)oCfsCz;%JEQ2#d_cuQb9Fob{Q@BjS$3jF;FkgkAk z?-A_$=X5*4tWu^h5f>IJOzh_J0H`q0%MDbR=-~z`OmuSt6((ZbK!u4eZlJHnWZy@i5=p#M@9Z0Q3BWF`QI1R>(ws! zE1v&tR-RNoq(n#`R#A4?xjBsmq$WJ)XPw#HWX3|HvCgbJn6aqc3cgm>naz!6EVNr8 zLSnaK)GJSl+pUl_RkuCjy#W=c9_}ASm3maCKj6}Byjl&@-C-|oq2C+pRt$O3h^TjT zaBu)ud7uxuKvG0s+*-&7Ki~x=3XC3IpkI7%*sa*_#V({(#(s7caC&6vSj|R4o{lZyq-R}K1=8SKIuqj@%%r7 z;eT=UW%c98|9KML2_fZY%3G8(l!PruE+Q~Q+$aOx6N8rxPK+H{J2N#lJiB&wx^H4; zzpJ8MgN^9jwj8*K!8a4h!;MD|U4@~0Z8>rgLlco9bwTKX!KulaVL67BExOm1;}?q> znXP$`og1Kj5$$p;y3~h;XNLx+`wsPuamn%!cD-xjc5a|L@ta(5K^DgjZLA9JvE?{* zye0*+z!yED7z$c1v%nXJMQk!RPraDfq)0Iko6L<>C*Fa}k<8unieZzv3G0QX zS7F}&KS9}|1ip!teoJ75`Xkf;8c@%J2OtYI$|K6V@eh~#r!y;9xuCW}sk^#hXlPJkDVdZVsAcYSEX2_0F%)-CfN^p#>hyDRH*cpIT?nN=b=kg+ey`@MjukS*7U@!eeTO`D#D{9K9xvE0#H7OWtnceF zg8llz^M9MI&hB^5wflB^PCLIQD@efD@%8I=Dm?enkBlst|62~vnjzMMJNJe9ES>-R ztRtO`ESdjX4u3K?AC}NEo&WnMM>?~gc6Uv6=l0oPUsF#*q_=u*V7#rjJ2*Ad-xICw zYiyg?y?x7Q-|o>Pk*cOc9aTfU9o?Zr15Kk_T89q}@9S+EX&spyj?Oh6Zf}m%MCV$o zqvP>I(UF0X#K_Q4VkCYjG14}YXxKKC7;iijAMZFEZ5SJhkHn8eW7|d(4d^q{GuGZP zfUk8NiS{;)#lsjO)-xVI(za#rNZZuFaBFSr=;6`co~FzCnmaqX!*!tn{CflAhu=If zI@{LK-MD2}SL;mcL}F-POV1cv&yKmap{}vk=~(YjcYM65318pV9XuTF?QT1m2zBmE zG|x74jJCCRHTG0RniFsCiH%*>ySrn$y&*Wfb9>utyf-r3KHN5%=v4aH+ygZQ0QC7PRt+Z(nXii||Uk#V$p8xKdvqhrx<>)}L0YgIIM;7EKf zI);BXo*2i^Ifvh0b9WH0=_tO(aI|4-O?+eszrX%N3qIF)Vzzhp)J&|ot*vwSSQFku z;lb|CaIB$iqP=CPb61S7CmioR97=TI_d9;Ly0d%B@IJg365(jLx4G?Lf3T-Hx_eu7 zPfO2a&qPnpVE63EKzL}je`4!&|M*Za)_h>Pf9}A7`>?&YqbAybpCQ&7jx@B5wa4Q4 zokfC)@s3a=7O9Ff42?$``orzb@v2B{Yd8^$A8POI8IO!N9*NG4;rG@UijF5n<8#}F zqT}6%T+`e)vMsUjK4ibC z=m@T-ntfN%hEZHs+i;Y9AF-yfXmdv}(l9U<85zUxBT|i@y(-e%T7&mos6Ey>#(s|O;lK9>qrHit z$arE5@1vn;Z|k8*xUri3&hg$G#XTb&k9Dx$PB<~rAC5LRR>$XtaCew@&BqR1*4Gpr zjf{^S>FchUj*a4X6rP!jHnqhfV?z^5?xl&LaQE(Qv3P8BZg4cw*friW8|!Ur=^g2r z>>Y0#>TMpF9hhs{JvcEsH!vC<9SB9c2gbLI_jE7X(_(R43+_YVw&6&)I}~j=a3nes zIT9V`_e)KCL(_PCq#u7LZ6j>YN{n<@MesKj8A*&s=C+MRn(=duw;oBv+D7BC=ujk# zpTBF~=Nj$At>n;jZ_8Bw&d}7szN&%7u3%eB_wJ!1z2mdnmVBq~BM`E}u&Efvt+lv2>pA+}< z_GY{e{GQr+I1%d^N#Gui-|uL9tZ{gM#|624ZKSiWb8IM>XskZm9UMq>jWxBzn%a8y zjSPg^Tl$ym*IkWUYWo|u1_zp}5A6%jPDgu&j>LNLI~g5H3^cYicJ&T5b#+Ip@V9bV z|JZ0a-d#7{72DR@A@&iuU$Z?6pCPd(eD?I?x+8P#xHq@QT5A#w?3vQf_q)VM6Fzs? zci4e@I(|mno7;Q4ai8uEN9XX0vAw-vXr#S2ayS~s=gETodT6}2Wgr$Go~`YviUxZ} z@mVbeYdq54+!&0GjEyH6I>*^_7oRsH@!nRp zpW|=8XEZuDG>p&MUK3M|m6m!J>b5W(5_!Oufi&HVuDSkW9;pnrG7MBU!;s2mj$t zKSsO2RmhYgi6!Ssx``@4hJ{!0+uu}b+I-#Bh zcfuh!Q@K~ULfPQ`{5!7HrUGAj>hErWgY~}yfiyFGut{oH90-oKXcj4;Oy-1#DSSh`zObz#s+5xFRvWx zo7p%t*f%gZJyTh~zi(`2@Vv@_zS+Tz`zNQz`(`WaE92t>mH67=*zox9?BF!MavuI? zn8)1M*WW*Ra035Y=)B6Asloo?{lonj=<>?WzI~JX`X)vj250)Gho@$TCnxYPU2$Gz z|K!-g@rjMD+ZOMY>B+G{blp=aV{RNqe|)F@gVWQ46a7cfWnhr4gspCP0+)gx4gY-_ z-)ZB(;J(4xjs4Su1H)^C~B&SZ8D6fE&#F_ldr7FWA1x$x#+~-crx2yma`$ z#N;&pI}2W4__y-?x0%7IzK#8}8;9Aob^m2@%KevteU%q`7iM5`<9OfH6n+Z4I^Mc@4TwXadJ2*9S>A>W~U}b&qyvo_h*}kz$`HTGD2dAee*)Ez-!bgeC}7G`G4h~%6g5{cm?{VX9oxNTwKH}lsxM=zS4CkZ6&PK-A{506?eeF z=|1*@zjUVWvSP1ueFw_wPqMUZprsP?EZ%}s= z_+NgW6#MrNfk6fT`}cqTeg*!11^zd#K=L)Z+?wCu`w_B#cw%^FXmDWu14Lv2MSQx2 z`18M9*{uZb2uuaeRe!C1P~C;6lpQz>LFEb50Nb741!eujrr4-AC|e$M+E-ew9ZYxsPBRzK=*5Eb`~V-Gl#5Fb>~G zgd$cR>yi5i{vY2*q&60PJ#rtxID8)w^0#@cNA4pShwmd&2$#MdxsPBRzK;k^+&tDJ M_YsVP`^c961CEtcp#T5? diff --git a/tests/Feature/Controllers/AccountControllerTest.php b/tests/Feature/Controllers/AccountControllerTest.php index 2f7ca99259..45a7b70733 100644 --- a/tests/Feature/Controllers/AccountControllerTest.php +++ b/tests/Feature/Controllers/AccountControllerTest.php @@ -40,7 +40,8 @@ class AccountControllerTest extends TestCase public function testDelete() { $this->be($this->user()); - $response = $this->get(route('accounts.delete', [1])); + $account = $this->user()->accounts()->where('account_type_id', 3)->whereNull('deleted_at')->first(); + $response = $this->get(route('accounts.delete', [$account->id])); $response->assertStatus(200); // has bread crumb $response->assertSee('
+ {% if budgets.count == 0 and inactive.count == 0 %} + {% include 'empty.budgets' %} + {% endif %}
{% for budget in budgets %}
diff --git a/resources/views/empty/accounts.twig b/resources/views/partials/empty.twig similarity index 89% rename from resources/views/empty/accounts.twig rename to resources/views/partials/empty.twig index 1f027086ac..51155d22ec 100644 --- a/resources/views/empty/accounts.twig +++ b/resources/views/partials/empty.twig @@ -14,7 +14,7 @@

- {{ ('empty_accounts_create_'~what)|_ }} + {{ ('empty_accounts_create_'~what)|_ }}

From 563c668e3f2f799a5a8504f4dd0b3ceab991f024 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 23 Feb 2017 07:24:05 +0100 Subject: [PATCH 032/244] Code to catch empty lists and nudge user in the right direction. --- app/Http/Controllers/TagController.php | 13 +- app/Repositories/Tag/TagRepository.php | 18 ++ .../Tag/TagRepositoryInterface.php | 12 + resources/lang/en_US/firefly.php | 270 ++++++++++-------- resources/views/budgets/index.twig | 2 +- resources/views/categories/index.twig | 43 ++- resources/views/partials/empty.twig | 8 +- resources/views/tags/index.twig | 63 ++-- resources/views/transactions/index-all.twig | 22 +- resources/views/transactions/index-date.twig | 28 +- resources/views/transactions/index.twig | 58 ++-- 11 files changed, 309 insertions(+), 228 deletions(-) diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 93d8236629..6b1a7cfa66 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -183,20 +183,23 @@ class TagController extends Controller } /** + * @param TagRepositoryInterface $repository * + * @return View */ - public function index() + public function index(TagRepositoryInterface $repository) { $title = 'Tags'; $mainTitleIcon = 'fa-tags'; $types = ['nothing', 'balancingAct', 'advancePayment']; + $count = $repository->count(); // loop each types and get the tags, group them by year. $collection = []; foreach ($types as $type) { /** @var Collection $tags */ - $tags = auth()->user()->tags()->where('tagMode', $type)->orderBy('date', 'ASC')->get(); + $tags = $repository->getByType($type); $tags = $tags->sortBy( function (Tag $tag) { $date = !is_null($tag->date) ? $tag->date->format('Ymd') : '000000'; @@ -216,7 +219,7 @@ class TagController extends Controller } } - return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection')); + return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection','count')); } /** @@ -266,8 +269,8 @@ class TagController extends Controller $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTag($tag) - ->withOpposingAccount()->disableInternalFilter() - ->withBudgetInformation()->withCategoryInformation(); + ->withOpposingAccount()->disableInternalFilter() + ->withBudgetInformation()->withCategoryInformation(); $journals = $collector->getPaginatedJournals(); $journals->setPath('tags/show/' . $tag->id . '/all'); diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 57bade58b5..3e19b968a9 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -68,6 +68,14 @@ class TagRepository implements TagRepositoryInterface return false; } + /** + * @return int + */ + public function count(): int + { + return $this->user->tags()->count(); + } + /** * @param Tag $tag * @@ -163,6 +171,16 @@ class TagRepository implements TagRepositoryInterface return $tags; } + /** + * @param string $type + * + * @return Collection + */ + public function getByType(string $type): Collection + { + return $this->user->tags()->where('tagMode', $type)->orderBy('date', 'ASC')->get(); + } + /** * @param Tag $tag * diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php index b360e7d436..dc7126d108 100644 --- a/app/Repositories/Tag/TagRepositoryInterface.php +++ b/app/Repositories/Tag/TagRepositoryInterface.php @@ -37,6 +37,11 @@ interface TagRepositoryInterface */ public function connect(TransactionJournal $journal, Tag $tag): bool; + /** + * @return int + */ + public function count(): int; + /** * This method destroys a tag. * @@ -83,6 +88,13 @@ interface TagRepositoryInterface */ public function get(): Collection; + /** + * @param string $type + * + * @return Collection + */ + public function getByType(string $type): Collection; + /** * @param Tag $tag * diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 4cd34fb3eb..bedb1653be 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -849,137 +849,163 @@ return [ 'tags_group' => 'Tags group transactions together, which makes it possible to store reimbursements (in case you front money for others) and other "balancing acts" where expenses are summed up (the payments on your new TV) or where expenses and deposits are cancelling each other out (buying something with saved money). It\'s all up to you. Using tags the old-fashioned way is of course always possible.', 'tags_start' => 'Create a tag to get started or enter tags when creating new transactions.', - 'transaction_journal_information' => 'Transaction information', - 'transaction_journal_meta' => 'Meta information', - 'total_amount' => 'Total amount', - 'number_of_decimals' => 'Number of decimals', + 'transaction_journal_information' => 'Transaction information', + 'transaction_journal_meta' => 'Meta information', + 'total_amount' => 'Total amount', + 'number_of_decimals' => 'Number of decimals', // administration - 'administration' => 'Administration', - 'user_administration' => 'User administration', - 'list_all_users' => 'All users', - 'all_users' => 'All users', - 'instance_configuration' => 'Configuration', - 'firefly_instance_configuration' => 'Configuration options for Firefly III', - 'setting_single_user_mode' => 'Single user mode', - 'setting_single_user_mode_explain' => 'By default, Firefly III only accepts one (1) registration: you. This is a security measure, preventing others from using your instance unless you allow them to. Future registrations are blocked. When you uncheck this box, others can use your instance as wel, assuming they can reach it (when it is connected to the internet).', - 'store_configuration' => 'Store configuration', - 'single_user_administration' => 'User administration for :email', - 'edit_user' => 'Edit user :email', - 'hidden_fields_preferences' => 'Not all fields are visible right now. You must enable them in your settings.', - 'user_data_information' => 'User data', - 'user_information' => 'User information', - 'total_size' => 'total size', - 'budget_or_budgets' => 'budget(s)', - 'budgets_with_limits' => 'budget(s) with configured amount', - 'rule_or_rules' => 'rule(s)', - 'rulegroup_or_groups' => 'rule group(s)', - 'setting_must_confirm_account' => 'Account confirmation', - 'setting_must_confirm_account_explain' => 'When this setting is enabled, users must activate their account before it can be used.', - 'configuration_updated' => 'The configuration has been updated', - 'setting_is_demo_site' => 'Demo site', - 'setting_is_demo_site_explain' => 'If you check this box, this installation will behave as if it is the demo site, which can have weird side effects.', - 'setting_send_email_notifications' => 'Send email notifications', - 'setting_send_email_explain' => 'Firefly III can send you email notifications about certain events. They will be sent to :site_owner. This email address can be set in the .env file.', - 'block_code_bounced' => 'Email message(s) bounced', - 'block_code_expired' => 'Demo account expired', - 'no_block_code' => 'No reason for block or user not blocked', + 'administration' => 'Administration', + 'user_administration' => 'User administration', + 'list_all_users' => 'All users', + 'all_users' => 'All users', + 'instance_configuration' => 'Configuration', + 'firefly_instance_configuration' => 'Configuration options for Firefly III', + 'setting_single_user_mode' => 'Single user mode', + 'setting_single_user_mode_explain' => 'By default, Firefly III only accepts one (1) registration: you. This is a security measure, preventing others from using your instance unless you allow them to. Future registrations are blocked. When you uncheck this box, others can use your instance as wel, assuming they can reach it (when it is connected to the internet).', + 'store_configuration' => 'Store configuration', + 'single_user_administration' => 'User administration for :email', + 'edit_user' => 'Edit user :email', + 'hidden_fields_preferences' => 'Not all fields are visible right now. You must enable them in your settings.', + 'user_data_information' => 'User data', + 'user_information' => 'User information', + 'total_size' => 'total size', + 'budget_or_budgets' => 'budget(s)', + 'budgets_with_limits' => 'budget(s) with configured amount', + 'rule_or_rules' => 'rule(s)', + 'rulegroup_or_groups' => 'rule group(s)', + 'setting_must_confirm_account' => 'Account confirmation', + 'setting_must_confirm_account_explain' => 'When this setting is enabled, users must activate their account before it can be used.', + 'configuration_updated' => 'The configuration has been updated', + 'setting_is_demo_site' => 'Demo site', + 'setting_is_demo_site_explain' => 'If you check this box, this installation will behave as if it is the demo site, which can have weird side effects.', + 'setting_send_email_notifications' => 'Send email notifications', + 'setting_send_email_explain' => 'Firefly III can send you email notifications about certain events. They will be sent to :site_owner. This email address can be set in the .env file.', + 'block_code_bounced' => 'Email message(s) bounced', + 'block_code_expired' => 'Demo account expired', + 'no_block_code' => 'No reason for block or user not blocked', // split a transaction: - 'transaction_meta_data' => 'Transaction meta-data', - 'transaction_dates' => 'Transaction dates', - 'splits' => 'Splits', - 'split_title_withdrawal' => 'Split your new withdrawal', - 'split_intro_one_withdrawal' => 'Firefly supports the "splitting" of a withdrawal.', - 'split_intro_two_withdrawal' => 'It means that the amount of money you\'ve spent is divided between several destination expense accounts, budgets or categories.', - 'split_intro_three_withdrawal' => 'For example: you could split your :total groceries so you pay :split_one from your "daily groceries" budget and :split_two from your "cigarettes" budget.', - 'split_table_intro_withdrawal' => 'Split your withdrawal in as many things as you want. By default the transaction will not split, there is just one entry. Add as many splits as you want to, below. Remember that you should not deviate from your total amount. If you do, Firefly will warn you but not correct you.', - 'store_splitted_withdrawal' => 'Store splitted withdrawal', - 'update_splitted_withdrawal' => 'Update splitted withdrawal', - 'split_title_deposit' => 'Split your new deposit', - 'split_intro_one_deposit' => 'Firefly supports the "splitting" of a deposit.', - 'split_intro_two_deposit' => 'It means that the amount of money you\'ve earned is divided between several source revenue accounts or categories.', - 'split_intro_three_deposit' => 'For example: you could split your :total salary so you get :split_one as your base salary and :split_two as a reimbursment for expenses made.', - 'split_table_intro_deposit' => 'Split your deposit in as many things as you want. By default the transaction will not split, there is just one entry. Add as many splits as you want to, below. Remember that you should not deviate from your total amount. If you do, Firefly will warn you but not correct you.', - 'store_splitted_deposit' => 'Store splitted deposit', - 'split_title_transfer' => 'Split your new transfer', - 'split_intro_one_transfer' => 'Firefly supports the "splitting" of a transfer.', - 'split_intro_two_transfer' => 'It means that the amount of money you\'re moving is divided between several categories or piggy banks.', - 'split_intro_three_transfer' => 'For example: you could split your :total move so you get :split_one in one piggy bank and :split_two in another.', - 'split_table_intro_transfer' => 'Split your transfer in as many things as you want. By default the transaction will not split, there is just one entry. Add as many splits as you want to, below. Remember that you should not deviate from your total amount. If you do, Firefly will warn you but not correct you.', - 'store_splitted_transfer' => 'Store splitted transfer', - 'add_another_split' => 'Add another split', - 'split-transactions' => 'Split transactions', - 'split-new-transaction' => 'Split a new transaction', - 'do_split' => 'Do a split', - 'split_this_withdrawal' => 'Split this withdrawal', - 'split_this_deposit' => 'Split this deposit', - 'split_this_transfer' => 'Split this transfer', - 'cannot_edit_multiple_source' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple source accounts.', - 'cannot_edit_multiple_dest' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple destination accounts.', - 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', + 'transaction_meta_data' => 'Transaction meta-data', + 'transaction_dates' => 'Transaction dates', + 'splits' => 'Splits', + 'split_title_withdrawal' => 'Split your new withdrawal', + 'split_intro_one_withdrawal' => 'Firefly supports the "splitting" of a withdrawal.', + 'split_intro_two_withdrawal' => 'It means that the amount of money you\'ve spent is divided between several destination expense accounts, budgets or categories.', + 'split_intro_three_withdrawal' => 'For example: you could split your :total groceries so you pay :split_one from your "daily groceries" budget and :split_two from your "cigarettes" budget.', + 'split_table_intro_withdrawal' => 'Split your withdrawal in as many things as you want. By default the transaction will not split, there is just one entry. Add as many splits as you want to, below. Remember that you should not deviate from your total amount. If you do, Firefly will warn you but not correct you.', + 'store_splitted_withdrawal' => 'Store splitted withdrawal', + 'update_splitted_withdrawal' => 'Update splitted withdrawal', + 'split_title_deposit' => 'Split your new deposit', + 'split_intro_one_deposit' => 'Firefly supports the "splitting" of a deposit.', + 'split_intro_two_deposit' => 'It means that the amount of money you\'ve earned is divided between several source revenue accounts or categories.', + 'split_intro_three_deposit' => 'For example: you could split your :total salary so you get :split_one as your base salary and :split_two as a reimbursment for expenses made.', + 'split_table_intro_deposit' => 'Split your deposit in as many things as you want. By default the transaction will not split, there is just one entry. Add as many splits as you want to, below. Remember that you should not deviate from your total amount. If you do, Firefly will warn you but not correct you.', + 'store_splitted_deposit' => 'Store splitted deposit', + 'split_title_transfer' => 'Split your new transfer', + 'split_intro_one_transfer' => 'Firefly supports the "splitting" of a transfer.', + 'split_intro_two_transfer' => 'It means that the amount of money you\'re moving is divided between several categories or piggy banks.', + 'split_intro_three_transfer' => 'For example: you could split your :total move so you get :split_one in one piggy bank and :split_two in another.', + 'split_table_intro_transfer' => 'Split your transfer in as many things as you want. By default the transaction will not split, there is just one entry. Add as many splits as you want to, below. Remember that you should not deviate from your total amount. If you do, Firefly will warn you but not correct you.', + 'store_splitted_transfer' => 'Store splitted transfer', + 'add_another_split' => 'Add another split', + 'split-transactions' => 'Split transactions', + 'split-new-transaction' => 'Split a new transaction', + 'do_split' => 'Do a split', + 'split_this_withdrawal' => 'Split this withdrawal', + 'split_this_deposit' => 'Split this deposit', + 'split_this_transfer' => 'Split this transfer', + 'cannot_edit_multiple_source' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple source accounts.', + 'cannot_edit_multiple_dest' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple destination accounts.', + 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', // import - 'configuration_file_help' => 'If you have previously imported data into Firefly III, you may have a configuration file, which will pre-set configuration values for you. For some banks, other users have kindly provided their configuration file.', - 'import_data_index' => 'Index', - 'import_file_type_csv' => 'CSV (comma separated values)', - 'import_file_type_help' => 'Select the type of file you will upload', - 'import_start' => 'Start the import', - 'configure_import' => 'Further configure your import', - 'import_finish_configuration' => 'Finish configuration', - 'settings_for_import' => 'Settings', - 'import_status' => 'Import status', - 'import_status_text' => 'The import is currently running, or will start momentarily.', - 'import_complete' => 'Import configuration complete!', - 'import_complete_text' => 'The import is ready to start. All the configuration you needed to do has been done. Please download the configuration file. It will help you with the import should it not go as planned. To actually run the import, you can either execute the following command in your console, or run the web-based import. Depending on your configuration, the console import will give you more feedback.', - 'import_download_config' => 'Download configuration', - 'import_start_import' => 'Start import', - 'import_data' => 'Import data', - 'import_data_full' => 'Import data into Firefly III', - 'import' => 'Import', - 'import_file_help' => 'Select your file', - 'import_status_settings_complete' => 'The import is ready to start.', - 'import_status_import_complete' => 'The import has completed.', - 'import_status_import_running' => 'The import is currently running. Please be patient.', - 'import_status_header' => 'Import status and progress', - 'import_status_errors' => 'Import errors', - 'import_status_report' => 'Import report', - 'import_finished' => 'Import has finished', - 'import_error_single' => 'An error has occured during the import.', - 'import_error_multi' => 'Some errors occured during the import.', - 'import_error_fatal' => 'There was an error during the import routine. Please check the log files. The error seems to be:', - 'import_error_timeout' => 'The import seems to have timed out. If this error persists, please import your data using the console command.', - 'import_double' => 'Row #:row: This row has been imported before, and is stored in :description.', - 'import_finished_all' => 'The import has finished. Please check out the results below.', - 'import_with_key' => 'Import with key \':key\'', - 'import_share_configuration' => 'Please consider downloading your configuration and sharing it at the import configuration center. This will allow other users of Firefly III to import their files more easily.', - 'import_finished_report' => 'The import has finished. Please note any errors in the block above this line. All transactions imported during this particular session have been tagged, and you can check them out below. ', - 'import_finished_link' => 'The transactions imported can be found in tag :tag.', - 'need_at_least_one_account' => 'You need at least one asset account to be able to create piggy banks', - 'see_help_top_right' => 'For more information, please check out the help pages using the icon in the top right corner of the page.', - 'bread_crumb_import_complete' => 'Import ":key" complete', - 'bread_crumb_configure_import' => 'Configure import ":key"', - 'bread_crumb_import_finished' => 'Import ":key" finished', - 'import_finished_intro' => 'The import has finished! You can now see the new transactions in Firefly.', - 'import_finished_text_without_link' => 'It seems there is no tag that points to all your imported transactions. Please look for your imported data in the menu on the left, under "Transactions".', - 'import_finished_text_with_link' => 'You can find a list of your imported transactions on the page of the tag that was created for this import.', + 'configuration_file_help' => 'If you have previously imported data into Firefly III, you may have a configuration file, which will pre-set configuration values for you. For some banks, other users have kindly provided their configuration file.', + 'import_data_index' => 'Index', + 'import_file_type_csv' => 'CSV (comma separated values)', + 'import_file_type_help' => 'Select the type of file you will upload', + 'import_start' => 'Start the import', + 'configure_import' => 'Further configure your import', + 'import_finish_configuration' => 'Finish configuration', + 'settings_for_import' => 'Settings', + 'import_status' => 'Import status', + 'import_status_text' => 'The import is currently running, or will start momentarily.', + 'import_complete' => 'Import configuration complete!', + 'import_complete_text' => 'The import is ready to start. All the configuration you needed to do has been done. Please download the configuration file. It will help you with the import should it not go as planned. To actually run the import, you can either execute the following command in your console, or run the web-based import. Depending on your configuration, the console import will give you more feedback.', + 'import_download_config' => 'Download configuration', + 'import_start_import' => 'Start import', + 'import_data' => 'Import data', + 'import_data_full' => 'Import data into Firefly III', + 'import' => 'Import', + 'import_file_help' => 'Select your file', + 'import_status_settings_complete' => 'The import is ready to start.', + 'import_status_import_complete' => 'The import has completed.', + 'import_status_import_running' => 'The import is currently running. Please be patient.', + 'import_status_header' => 'Import status and progress', + 'import_status_errors' => 'Import errors', + 'import_status_report' => 'Import report', + 'import_finished' => 'Import has finished', + 'import_error_single' => 'An error has occured during the import.', + 'import_error_multi' => 'Some errors occured during the import.', + 'import_error_fatal' => 'There was an error during the import routine. Please check the log files. The error seems to be:', + 'import_error_timeout' => 'The import seems to have timed out. If this error persists, please import your data using the console command.', + 'import_double' => 'Row #:row: This row has been imported before, and is stored in :description.', + 'import_finished_all' => 'The import has finished. Please check out the results below.', + 'import_with_key' => 'Import with key \':key\'', + 'import_share_configuration' => 'Please consider downloading your configuration and sharing it at the import configuration center. This will allow other users of Firefly III to import their files more easily.', + 'import_finished_report' => 'The import has finished. Please note any errors in the block above this line. All transactions imported during this particular session have been tagged, and you can check them out below. ', + 'import_finished_link' => 'The transactions imported can be found in tag :tag.', + 'need_at_least_one_account' => 'You need at least one asset account to be able to create piggy banks', + 'see_help_top_right' => 'For more information, please check out the help pages using the icon in the top right corner of the page.', + 'bread_crumb_import_complete' => 'Import ":key" complete', + 'bread_crumb_configure_import' => 'Configure import ":key"', + 'bread_crumb_import_finished' => 'Import ":key" finished', + 'import_finished_intro' => 'The import has finished! You can now see the new transactions in Firefly.', + 'import_finished_text_without_link' => 'It seems there is no tag that points to all your imported transactions. Please look for your imported data in the menu on the left, under "Transactions".', + 'import_finished_text_with_link' => 'You can find a list of your imported transactions on the page of the tag that was created for this import.', // sandstorm.io errors and messages: - 'sandstorm_not_available' => 'This function is not available when you are using Firefly III within a Sandstorm.io environment.', + 'sandstorm_not_available' => 'This function is not available when you are using Firefly III within a Sandstorm.io environment.', + + // empty lists? no objects? instructions: + 'no_transactions_in_period' => 'There are no transactions in this period.', + 'no_accounts_title_asset' => 'Let\'s create an asset account!', + 'no_accounts_intro_asset' => 'You have no asset accounts yet. Asset accounts are your main accounts: your checking account, savings account, shared account or even your credit card.', + 'no_accounts_imperative_asset' => 'To start using Firefly III you must create at least one asset account. Let\'s do so now:', + 'no_accounts_create_asset' => 'Create an asset account', + 'no_accounts_title_expense' => 'Let\'s create an expense account!', + 'no_accounts_intro_expense' => 'You have no expense accounts yet. Expense accounts are the places where you spend money, such as shops and supermarkets.', + 'no_accounts_imperative_expense' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', + 'no_accounts_create_expense' => 'Create an expense account', + 'no_accounts_title_revenue' => 'Let\'s create a revenue account!', + 'no_accounts_intro_revenue' => 'You have no revenue accounts yet. Revenue accounts are the places where you receive money from, such as your employer.', + 'no_accounts_imperative_revenue' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', + 'no_accounts_create_revenue' => 'Create a revenue account', + 'no_budgets_title_default' => 'Let\'s create a budget', + 'no_budgets_intro_default' => 'You have no budgets yet. Budgets are used to organise your expenses into logical groups, which you can give a soft-cap to limit your expenses', + 'no_budgets_imperative_default' => 'Budgets are the basic tools of financial management. Let\'s create one now:', + 'no_budgets_create_default' => 'Create a new budget', + 'no_categories_title_default' => 'Let\'s create a category', + 'no_categories_intro_default' => 'You have no categories yet. Categories are used to fine tune your transactions and label them with their designated category.', + 'no_categories_imperative_default' => 'Categories are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', + 'no_categories_create_default' => 'Create a new category', + 'no_tags_title_default' => 'Let\'s create a tags', + 'no_tags_intro_default' => 'You have no tags yet. Tags are used to fine tune your transactions and label them with specific keywords.', + 'no_tags_imperative_default' => 'Tags are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', + 'no_tags_create_default' => 'Create a new tag', + 'no_transactions_title_withdrawal' => 'Let\'s create an expense', + 'no_transactions_intro_withdrawal' => 'You have no expenses yet. You should create expenses to start managing your finances.', + 'no_transactions_imperative_withdrawal' => 'Have you spent some money? Then you should write it down:', + 'no_transactions_create_withdrawal' => 'Create a new expense', + 'no_transactions_title_deposit' => 'Let\'s create some income', + 'no_transactions_intro_deposit' => 'You have no recorded income yet. You should create income entries to start managing your finances.', + 'no_transactions_imperative_deposit' => 'Have you received some money? Then you should write it down:', + 'no_transactions_create_deposit' => 'Create a new deposit', + 'no_transactions_title_transfers' => 'Let\'s create a transfer', + 'no_transactions_intro_transfers' => 'You have no transfers yet. When you move money between asset accounts, it is recorded as a transfer.', + 'no_transactions_imperative_transfers' => 'Have you moved some money around? Then you should write it down:', + 'no_transactions_create_transfers' => 'Create a new transfer', - // empty lists? instructions: - 'empty_accounts_title_asset' => 'Let\'s create an asset account!', - 'empty_accounts_intro_asset' => 'You have no asset accounts yet. Asset accounts are your main accounts: your checking account, savings account, shared account or even your credit card.', - 'empty_accounts_imperative_asset' => 'To start using Firefly III you must create at least one asset account. Let\'s do so now:', - 'empty_accounts_create_asset' => 'Create an asset account', - 'empty_accounts_title_expense' => 'Let\'s create an expense account!', - 'empty_accounts_intro_expense' => 'You have no expense accounts yet. Expense accounts are the places where you spend money, such as shops and supermarkets.', - 'empty_accounts_imperative_expense' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want.', - 'empty_accounts_create_expense' => 'Create an expense account', - 'empty_accounts_title_revenue' => 'Let\'s create a revenue account!', - 'empty_accounts_intro_revenue' => 'You have no revenue accounts yet. Revenue accounts are the places where you receive money from, such as your employer.', - 'empty_accounts_imperative_revenue' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want.', - 'empty_accounts_create_revenue' => 'Create a revenue account', ]; diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 9bdd4da8f4..3928cd2714 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -85,7 +85,7 @@
{% if budgets.count == 0 and inactive.count == 0 %} - {% include 'empty.budgets' %} + {% include 'partials.empty' with {what: 'default', type: 'budgets',route: route('budgets.create')} %} {% endif %}
{% for budget in budgets %} diff --git a/resources/views/categories/index.twig b/resources/views/categories/index.twig index d4cfc7c335..8316a444b9 100644 --- a/resources/views/categories/index.twig +++ b/resources/views/categories/index.twig @@ -5,38 +5,37 @@ {% endblock %} {% block content %} -
-
-
-
-

{{ 'categories'|_ }}

+ {% if categories.count > 0 %} +
+
+
+
+

{{ 'categories'|_ }}

- -
- -
-
- {% include 'list/categories' %} +
+
+ {% include 'list/categories' %} +
-
+ {% else %} + {% include 'partials.empty' with {what: 'default', type: 'categories',route: route('categories.create')} %} + {% endif %} {% endblock %} {% block styles %} - {% endblock %} {% block scripts %} - - - - {% endblock %} diff --git a/resources/views/partials/empty.twig b/resources/views/partials/empty.twig index 51155d22ec..4dead2ac1d 100644 --- a/resources/views/partials/empty.twig +++ b/resources/views/partials/empty.twig @@ -2,19 +2,19 @@
-

{{ ('empty_accounts_title_'~what)|_ }}

+

{{ ('no_'~type~'_title_'~what)|_ }}

- {{ ('empty_accounts_intro_'~what)|_ }} + {{ ('no_'~type~'_intro_'~what)|_ }}

- {{ ('empty_accounts_imperative_'~what)|_ }} + {{ ('no_'~type~'_imperative_'~what)|_ }}

- {{ ('empty_accounts_create_'~what)|_ }} + {{ ('no_'~type~'_create_'~what)|_ }}

diff --git a/resources/views/tags/index.twig b/resources/views/tags/index.twig index 1baaace293..b92d2e51e9 100644 --- a/resources/views/tags/index.twig +++ b/resources/views/tags/index.twig @@ -30,46 +30,49 @@
-
- {% for type in types %} -
-
-
-

{{ ('tag_title_'~type)|_ }}

-
-
- {% for year,months in collection[type] %} -

{{ year }}

+ {% if count == 0 %} + {% include 'partials.empty' with {what: 'default', type: 'tags',route: route('tags.create')} %} + {% else %} +
+ {% for type in types %} +
+
+
+

{{ ('tag_title_'~type)|_ }}

+
+
+ {% for year,months in collection[type] %} +

{{ year }}

- {% for month,tags in months %} -
{{ month }}
-

- {% for tag in tags %} - + {% for month,tags in months %} +

{{ month }}
+

+ {% for tag in tags %} + {% if tag.tagMode == 'nothing' %} {% endif %} - {% if tag.tagMode == 'balancingAct' %} - - {% endif %} - {% if tag.tagMode == 'advancePayment' %} - - {% endif %} - {{ tag.tag }} + {% if tag.tagMode == 'balancingAct' %} + + {% endif %} + {% if tag.tagMode == 'advancePayment' %} + + {% endif %} + {{ tag.tag }} - {% endfor %} -

+ {% endfor %} +

+ {% endfor %} {% endfor %} - {% endfor %} +
-
- {% endfor %} - -
+ {% endfor %} +
+ {% endif %} {% endblock %} {% block scripts %} diff --git a/resources/views/transactions/index-all.twig b/resources/views/transactions/index-all.twig index 3d63c4e0b1..794c255614 100644 --- a/resources/views/transactions/index-all.twig +++ b/resources/views/transactions/index-all.twig @@ -5,18 +5,22 @@ {% endblock %} {% block content %} -
-
-
-
-

{{ subTitle }}

-
-
- {% include 'list.journals-tasker' %} + {% if journals.count == 0 %} + {% include 'partials.empty' with {what: what, type: 'transactions',route: route('transactions.create', [what])} %} + {% else %} +
+
+
+
+

{{ subTitle }}

+
+
+ {% include 'list.journals-tasker' %} +
-
+ {% endif %} {% endblock %} {% block scripts %} diff --git a/resources/views/transactions/index-date.twig b/resources/views/transactions/index-date.twig index 149aeeeb27..78df4d16d8 100644 --- a/resources/views/transactions/index-date.twig +++ b/resources/views/transactions/index-date.twig @@ -5,18 +5,28 @@ {% endblock %} {% block content %} -
-
-
-
-

{{ subTitle }}

-
-
- {% include 'list.journals-tasker' %} + {% if journals.count == 0 %} +
+
+

+ {{ 'no_transactions_in_period'|_ }} +

+
+
+ {% else %} +
+
+
+
+

{{ subTitle }}

+
+
+ {% include 'list.journals-tasker' %} +
-
+ {% endif %} {% endblock %} {% block scripts %} diff --git a/resources/views/transactions/index.twig b/resources/views/transactions/index.twig index 9aa5eb2b73..1b86e5ac6b 100644 --- a/resources/views/transactions/index.twig +++ b/resources/views/transactions/index.twig @@ -5,40 +5,46 @@ {% endblock %} {% block content %} -
-
-
-
-

{{ subTitle }}

-
-
- {% include 'list.journals-tasker' %} -

- - - {{ 'show_all_no_filter'|_ }} - -

-
-
-
-
- {% for entry in entries %} + + {% if journals.count == 0 %} + {% include 'partials.empty' with {what: what, type: 'transactions',route: route('transactions.create', [what])} %} + {% else %} +
+
-

- {{ entry[1] }} -

+

{{ subTitle }}

-   + {% include 'list.journals-tasker' %} +

+ + + {{ 'show_all_no_filter'|_ }} + +

- {% endfor %} -
+
-
+
+ {% for entry in entries %} +
+
+

+ {{ entry[1] }} +

+
+
+   +
+
+ {% endfor %} +
+ +
+ {% endif %} {% endblock %} {% block scripts %} From fc36f9cd8c70db8325b2472a50b2c8dfcb2149c8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 23 Feb 2017 07:30:08 +0100 Subject: [PATCH 033/244] Added empty box for bills as well. --- resources/lang/en_US/firefly.php | 8 +++ resources/views/bills/index.twig | 45 +++++++------- resources/views/piggy-banks/index.twig | 82 ++++++++++++++------------ 3 files changed, 72 insertions(+), 63 deletions(-) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index bedb1653be..d24c1a1c65 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1006,6 +1006,14 @@ return [ 'no_transactions_intro_transfers' => 'You have no transfers yet. When you move money between asset accounts, it is recorded as a transfer.', 'no_transactions_imperative_transfers' => 'Have you moved some money around? Then you should write it down:', 'no_transactions_create_transfers' => 'Create a new transfer', + 'no_piggies_title_default' => 'Let\'s create a piggy bank', + 'no_piggies_intro_default' => 'You have no piggy banks yet. You can create piggy banks to divide your savings and keep track of what you\'re saving up for.', + 'no_piggies_imperative_default' => 'Do you have things you\'re saving money for? Create a piggy bank and keep track:', + 'no_piggies_create_default' => 'Create a new piggy bank', + 'no_bills_title_default' => 'Let\'s create a bill', + 'no_bills_intro_default' => 'You have no bills yet. You can create bills to keep track of regular expenses, like your rent of insurance.', + 'no_bills_imperative_default' => 'Do you have such regular bills? Create a bill and keep track of your payments:', + 'no_bills_create_default' => 'Create a new bill', ]; diff --git a/resources/views/bills/index.twig b/resources/views/bills/index.twig index 28bc6261fe..b56f32da2b 100644 --- a/resources/views/bills/index.twig +++ b/resources/views/bills/index.twig @@ -5,33 +5,30 @@ {% endblock %} {% block content %} -
-
-
-
-

{{ title }}

+ {% if bills.count == 0 %} + {% include 'partials.empty' with {what: 'default', type: 'bills',route: route('bills.create')} %} + {% else %} +
+
+
+
+

{{ title }}

- -
-
- - + +
+
+ + +
-
-
- {% include 'list/bills' %} +
+ {% include 'list/bills' %} +
-
-{% endblock %} -{% block styles %} - -{% endblock %} - -{% block scripts %} - -{% endblock %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/resources/views/piggy-banks/index.twig b/resources/views/piggy-banks/index.twig index 821b731c9a..981fa84fb5 100644 --- a/resources/views/piggy-banks/index.twig +++ b/resources/views/piggy-banks/index.twig @@ -5,54 +5,58 @@ {% endblock %} {% block content %} -
-
-
-
-

{{ 'piggyBanks'|_ }}

-
-
- {% include 'list/piggy-banks' %} + {% if piggyBanks.count == 0 %} + {% include 'partials.empty' with {what: 'default', type: 'piggies',route: route('piggy-banks.create')} %} + {% else %} +
+
+
+
+

{{ 'piggyBanks'|_ }}

+
+
+ {% include 'list/piggy-banks' %} +
-
-
-
-
-
-

{{ 'account_status'|_ }}

-
-
- - - - - - - - - - - - - {% for id,info in accounts %} +
+
+
+
+

{{ 'account_status'|_ }}

+
+
+
{{ 'account'|_ }}{{ 'left_for_piggy_banks'|_ }}
+ - - - - - - + + + + + + - {% endfor %} - -
{{ info.name }}{{ info.leftForPiggyBanks|formatAmount }}{{ 'account'|_ }}{{ 'left_for_piggy_banks'|_ }}
+ + + {% for id,info in accounts %} + + {{ info.name }} + {{ info.balance|formatAmount }} + {{ info.leftForPiggyBanks|formatAmount }} + {{ info.sumOfTargets|formatAmount }} + {{ info.sumOfSaved|formatAmount }} + {{ info.leftToSave|formatAmount }} + + {% endfor %} + + +
-
+ {% endif %} {% endblock %} {% block scripts %} From 063ca3121a496ead05defc3173e1520a96927d83 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 23 Feb 2017 17:43:29 +0100 Subject: [PATCH 034/244] Fixes #593 --- app/Http/Controllers/Chart/BudgetController.php | 7 ++++++- public/js/ff/charts.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index cf99421aee..285db761b9 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -386,9 +386,14 @@ class BudgetController extends Controller ] ); } + /* + * amount: amount of budget limit + * left: amount of budget limit min spent, or 0 when < 0. + * spent: spent, or amount of budget limit when > amount + */ $amount = $budgetLimit->amount; $left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses); - $spent = $expenses; + $spent = bccomp($expenses, $amount) === 1 ? $expenses : bcmul($amount, '-1'); $overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0'; $return[$name] = [ 'left' => $left, diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index d402ab67f8..52ce69aba2 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -207,6 +207,7 @@ function stackedColumnChart(URI, container) { var options = defaultChartOptions; options.stacked = true; options.scales.xAxes[0].stacked = true; + options.scales.yAxes[0].stacked = true; var chartType = 'bar'; From 27236d19cfa0b9da9019cb94ebec301e4e355f1f Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 23 Feb 2017 17:47:36 +0100 Subject: [PATCH 035/244] Clone, not copy [skip ci] --- public/js/ff/charts.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index 52ce69aba2..cfc787fae6 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -204,7 +204,8 @@ function stackedColumnChart(URI, container) { "use strict"; var colorData = true; - var options = defaultChartOptions; + var options = $.extend(true, {}, defaultChartOptions); + options.stacked = true; options.scales.xAxes[0].stacked = true; options.scales.yAxes[0].stacked = true; From 35aa61bb2374ebbe525ceecfd17093bfeae15c1d Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 23 Feb 2017 17:47:46 +0100 Subject: [PATCH 036/244] Different icon [skip ci] --- resources/views/list/journals-tasker.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/list/journals-tasker.twig b/resources/views/list/journals-tasker.twig index 7502719e27..db348d7acb 100644 --- a/resources/views/list/journals-tasker.twig +++ b/resources/views/list/journals-tasker.twig @@ -22,7 +22,7 @@ {% if not hideBills %} - + {% endif %} From e737683efb3b8e15bfbed3bea5611807eaec99ed Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 24 Feb 2017 16:00:24 +0100 Subject: [PATCH 037/244] Fine-tune some translations [skip ci] --- resources/lang/en_US/firefly.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index d24c1a1c65..d4e5fb0c73 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -793,7 +793,7 @@ return [ // piggy banks: 'add_money_to_piggy' => 'Add money to piggy bank ":name"', 'piggy_bank' => 'Piggy bank', - 'new_piggy_bank' => 'Create new piggy bank', + 'new_piggy_bank' => 'New piggy bank', 'store_piggy_bank' => 'Store new piggy bank', 'stored_piggy_bank' => 'Store new piggy bank ":name"', 'account_status' => 'Account status', @@ -983,37 +983,37 @@ return [ 'no_accounts_imperative_revenue' => 'Expense accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', 'no_accounts_create_revenue' => 'Create a revenue account', 'no_budgets_title_default' => 'Let\'s create a budget', - 'no_budgets_intro_default' => 'You have no budgets yet. Budgets are used to organise your expenses into logical groups, which you can give a soft-cap to limit your expenses', + 'no_budgets_intro_default' => 'You have no budgets yet. Budgets are used to organise your expenses into logical groups, which you can give a soft-cap to limit your expenses.', 'no_budgets_imperative_default' => 'Budgets are the basic tools of financial management. Let\'s create one now:', - 'no_budgets_create_default' => 'Create a new budget', - 'no_categories_title_default' => 'Let\'s create a category', + 'no_budgets_create_default' => 'Create a budget', + 'no_categories_title_default' => 'Let\'s create a category!', 'no_categories_intro_default' => 'You have no categories yet. Categories are used to fine tune your transactions and label them with their designated category.', 'no_categories_imperative_default' => 'Categories are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', - 'no_categories_create_default' => 'Create a new category', - 'no_tags_title_default' => 'Let\'s create a tags', + 'no_categories_create_default' => 'Create a category', + 'no_tags_title_default' => 'Let\'s create a tag!', 'no_tags_intro_default' => 'You have no tags yet. Tags are used to fine tune your transactions and label them with specific keywords.', 'no_tags_imperative_default' => 'Tags are created automatically when you create transactions, but you can create one manually too. Let\'s create one now:', - 'no_tags_create_default' => 'Create a new tag', - 'no_transactions_title_withdrawal' => 'Let\'s create an expense', + 'no_tags_create_default' => 'Create a tag', + 'no_transactions_title_withdrawal' => 'Let\'s create an expense!', 'no_transactions_intro_withdrawal' => 'You have no expenses yet. You should create expenses to start managing your finances.', 'no_transactions_imperative_withdrawal' => 'Have you spent some money? Then you should write it down:', - 'no_transactions_create_withdrawal' => 'Create a new expense', - 'no_transactions_title_deposit' => 'Let\'s create some income', + 'no_transactions_create_withdrawal' => 'Create an expense', + 'no_transactions_title_deposit' => 'Let\'s create some income!', 'no_transactions_intro_deposit' => 'You have no recorded income yet. You should create income entries to start managing your finances.', 'no_transactions_imperative_deposit' => 'Have you received some money? Then you should write it down:', - 'no_transactions_create_deposit' => 'Create a new deposit', - 'no_transactions_title_transfers' => 'Let\'s create a transfer', + 'no_transactions_create_deposit' => 'Create a deposit', + 'no_transactions_title_transfers' => 'Let\'s create a transfer!', 'no_transactions_intro_transfers' => 'You have no transfers yet. When you move money between asset accounts, it is recorded as a transfer.', 'no_transactions_imperative_transfers' => 'Have you moved some money around? Then you should write it down:', - 'no_transactions_create_transfers' => 'Create a new transfer', - 'no_piggies_title_default' => 'Let\'s create a piggy bank', + 'no_transactions_create_transfers' => 'Create a transfer', + 'no_piggies_title_default' => 'Let\'s create a piggy bank!', 'no_piggies_intro_default' => 'You have no piggy banks yet. You can create piggy banks to divide your savings and keep track of what you\'re saving up for.', 'no_piggies_imperative_default' => 'Do you have things you\'re saving money for? Create a piggy bank and keep track:', 'no_piggies_create_default' => 'Create a new piggy bank', - 'no_bills_title_default' => 'Let\'s create a bill', + 'no_bills_title_default' => 'Let\'s create a bill!', 'no_bills_intro_default' => 'You have no bills yet. You can create bills to keep track of regular expenses, like your rent of insurance.', 'no_bills_imperative_default' => 'Do you have such regular bills? Create a bill and keep track of your payments:', - 'no_bills_create_default' => 'Create a new bill', + 'no_bills_create_default' => 'Create a bill', ]; From f63c6875cd736ae13e08d1a0f881c4ba310c5504 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 24 Feb 2017 20:01:35 +0100 Subject: [PATCH 038/244] Initial code base for tag report. --- .../Report/Budget/MonthReportGenerator.php | 72 +-- .../Report/Category/MonthReportGenerator.php | 125 +---- .../Category/MultiYearReportGenerator.php | 2 +- .../Report/Category/YearReportGenerator.php | 2 +- app/Generator/Report/Support.php | 120 +++++ .../Report/Tag/MonthReportGenerator.php | 219 +++++++++ .../Report/Tag/MultiYearReportGenerator.php | 25 + .../Report/Tag/YearReportGenerator.php | 26 ++ app/Helpers/Collector/JournalCollector.php | 14 + .../Collector/JournalCollectorInterface.php | 7 + .../Chart/CategoryReportController.php | 18 - .../Controllers/Chart/TagReportController.php | 216 +++++++++ app/Http/breadcrumbs.php | 13 + public/js/ff/reports/tag/all.js | 10 + public/js/ff/reports/tag/month.js | 66 +++ resources/lang/en_US/firefly.php | 1 + resources/views/reports/tag/month.twig | 434 ++++++++++++++++++ routes/web.php | 41 ++ 18 files changed, 1197 insertions(+), 214 deletions(-) create mode 100644 app/Generator/Report/Tag/MonthReportGenerator.php create mode 100644 app/Generator/Report/Tag/MultiYearReportGenerator.php create mode 100644 app/Generator/Report/Tag/YearReportGenerator.php create mode 100644 app/Http/Controllers/Chart/TagReportController.php create mode 100644 public/js/ff/reports/tag/all.js create mode 100644 public/js/ff/reports/tag/month.js create mode 100644 resources/views/reports/tag/month.twig diff --git a/app/Generator/Report/Budget/MonthReportGenerator.php b/app/Generator/Report/Budget/MonthReportGenerator.php index 87557c8682..0934ef20c6 100644 --- a/app/Generator/Report/Budget/MonthReportGenerator.php +++ b/app/Generator/Report/Budget/MonthReportGenerator.php @@ -141,52 +141,10 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface return $this; } - /** - * @param Collection $collection - * @param int $sortFlag - * - * @return array - */ - private function getAverages(Collection $collection, int $sortFlag): array - { - $result = []; - /** @var Transaction $transaction */ - foreach ($collection as $transaction) { - // opposing name and ID: - $opposingId = $transaction->opposing_account_id; - - // is not set? - if (!isset($result[$opposingId])) { - $name = $transaction->opposing_account_name; - $result[$opposingId] = [ - 'name' => $name, - 'count' => 1, - 'id' => $opposingId, - 'average' => $transaction->transaction_amount, - 'sum' => $transaction->transaction_amount, - ]; - continue; - } - $result[$opposingId]['count']++; - $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $transaction->transaction_amount); - $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], strval($result[$opposingId]['count'])); - } - - // sort result by average: - $average = []; - foreach ($result as $key => $row) { - $average[$key] = floatval($row['average']); - } - - array_multisort($average, $sortFlag, $result); - - return $result; - } - /** * @return Collection */ - private function getExpenses(): Collection + protected function getExpenses(): Collection { if ($this->expenses->count() > 0) { Log::debug('Return previous set of expenses.'); @@ -208,34 +166,6 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface return $transactions; } - /** - * @return Collection - */ - private function getTopExpenses(): Collection - { - $transactions = $this->getExpenses()->sortBy('transaction_amount'); - - return $transactions; - } - - /** - * @param Collection $collection - * - * @return array - */ - private function summarizeByAccount(Collection $collection): array - { - $result = []; - /** @var Transaction $transaction */ - foreach ($collection as $transaction) { - $accountId = $transaction->account_id; - $result[$accountId] = $result[$accountId] ?? '0'; - $result[$accountId] = bcadd($transaction->transaction_amount, $result[$accountId]); - } - - return $result; - } - /** * @param Collection $collection * diff --git a/app/Generator/Report/Category/MonthReportGenerator.php b/app/Generator/Report/Category/MonthReportGenerator.php index 0a3c60be3a..654f67befc 100644 --- a/app/Generator/Report/Category/MonthReportGenerator.php +++ b/app/Generator/Report/Category/MonthReportGenerator.php @@ -151,52 +151,10 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface return $this; } - /** - * @param Collection $collection - * @param int $sortFlag - * - * @return array - */ - private function getAverages(Collection $collection, int $sortFlag): array - { - $result = []; - /** @var Transaction $transaction */ - foreach ($collection as $transaction) { - // opposing name and ID: - $opposingId = $transaction->opposing_account_id; - - // is not set? - if (!isset($result[$opposingId])) { - $name = $transaction->opposing_account_name; - $result[$opposingId] = [ - 'name' => $name, - 'count' => 1, - 'id' => $opposingId, - 'average' => $transaction->transaction_amount, - 'sum' => $transaction->transaction_amount, - ]; - continue; - } - $result[$opposingId]['count']++; - $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $transaction->transaction_amount); - $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], strval($result[$opposingId]['count'])); - } - - // sort result by average: - $average = []; - foreach ($result as $key => $row) { - $average[$key] = floatval($row['average']); - } - - array_multisort($average, $sortFlag, $result); - - return $result; - } - /** * @return Collection */ - private function getExpenses(): Collection + protected function getExpenses(): Collection { if ($this->expenses->count() > 0) { Log::debug('Return previous set of expenses.'); @@ -221,7 +179,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface /** * @return Collection */ - private function getIncome(): Collection + protected function getIncome(): Collection { if ($this->income->count() > 0) { return $this->income; @@ -240,85 +198,6 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface return $transactions; } - /** - * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. - * @param array $spent - * @param array $earned - * - * @return array - */ - private function getObjectSummary(array $spent, array $earned): array - { - $return = []; - - /** - * @var int $accountId - * @var string $entry - */ - foreach ($spent as $objectId => $entry) { - if (!isset($return[$objectId])) { - $return[$objectId] = ['spent' => 0, 'earned' => 0]; - } - - $return[$objectId]['spent'] = $entry; - } - unset($entry); - - /** - * @var int $accountId - * @var string $entry - */ - foreach ($earned as $objectId => $entry) { - if (!isset($return[$objectId])) { - $return[$objectId] = ['spent' => 0, 'earned' => 0]; - } - - $return[$objectId]['earned'] = $entry; - } - - - return $return; - } - - - /** - * @return Collection - */ - private function getTopExpenses(): Collection - { - $transactions = $this->getExpenses()->sortBy('transaction_amount'); - - return $transactions; - } - - /** - * @return Collection - */ - private function getTopIncome(): Collection - { - $transactions = $this->getIncome()->sortByDesc('transaction_amount'); - - return $transactions; - } - - /** - * @param Collection $collection - * - * @return array - */ - private function summarizeByAccount(Collection $collection): array - { - $result = []; - /** @var Transaction $transaction */ - foreach ($collection as $transaction) { - $accountId = $transaction->account_id; - $result[$accountId] = $result[$accountId] ?? '0'; - $result[$accountId] = bcadd($transaction->transaction_amount, $result[$accountId]); - } - - return $result; - } - /** * @param Collection $collection * diff --git a/app/Generator/Report/Category/MultiYearReportGenerator.php b/app/Generator/Report/Category/MultiYearReportGenerator.php index 62b0e32af9..0f57b6b888 100644 --- a/app/Generator/Report/Category/MultiYearReportGenerator.php +++ b/app/Generator/Report/Category/MultiYearReportGenerator.php @@ -17,7 +17,7 @@ namespace FireflyIII\Generator\Report\Category; /** * Class MultiYearReportGenerator * - * @package FireflyIII\Generator\Report\Audit + * @package FireflyIII\Generator\Report\Category */ class MultiYearReportGenerator extends MonthReportGenerator { diff --git a/app/Generator/Report/Category/YearReportGenerator.php b/app/Generator/Report/Category/YearReportGenerator.php index a118c4c5b0..e4f62dba0f 100644 --- a/app/Generator/Report/Category/YearReportGenerator.php +++ b/app/Generator/Report/Category/YearReportGenerator.php @@ -17,7 +17,7 @@ namespace FireflyIII\Generator\Report\Category; /** * Class YearReportGenerator * - * @package FireflyIII\Generator\Report\Audit + * @package FireflyIII\Generator\Report\Category */ class YearReportGenerator extends MonthReportGenerator { diff --git a/app/Generator/Report/Support.php b/app/Generator/Report/Support.php index 573129ef97..9125031579 100644 --- a/app/Generator/Report/Support.php +++ b/app/Generator/Report/Support.php @@ -80,4 +80,124 @@ class Support return $result; } + /** + * @param Collection $collection + * @param int $sortFlag + * + * @return array + */ + protected function getAverages(Collection $collection, int $sortFlag): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + // opposing name and ID: + $opposingId = $transaction->opposing_account_id; + + // is not set? + if (!isset($result[$opposingId])) { + $name = $transaction->opposing_account_name; + $result[$opposingId] = [ + 'name' => $name, + 'count' => 1, + 'id' => $opposingId, + 'average' => $transaction->transaction_amount, + 'sum' => $transaction->transaction_amount, + ]; + continue; + } + $result[$opposingId]['count']++; + $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $transaction->transaction_amount); + $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], strval($result[$opposingId]['count'])); + } + + // sort result by average: + $average = []; + foreach ($result as $key => $row) { + $average[$key] = floatval($row['average']); + } + + array_multisort($average, $sortFlag, $result); + + return $result; + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. + * @param array $spent + * @param array $earned + * + * @return array + */ + protected function getObjectSummary(array $spent, array $earned): array + { + $return = []; + + /** + * @var int $accountId + * @var string $entry + */ + foreach ($spent as $objectId => $entry) { + if (!isset($return[$objectId])) { + $return[$objectId] = ['spent' => 0, 'earned' => 0]; + } + + $return[$objectId]['spent'] = $entry; + } + unset($entry); + + /** + * @var int $accountId + * @var string $entry + */ + foreach ($earned as $objectId => $entry) { + if (!isset($return[$objectId])) { + $return[$objectId] = ['spent' => 0, 'earned' => 0]; + } + + $return[$objectId]['earned'] = $entry; + } + + + return $return; + } + + /** + * @return Collection + */ + public function getTopExpenses(): Collection + { + $transactions = $this->getExpenses()->sortBy('transaction_amount'); + + return $transactions; + } + + /** + * @return Collection + */ + public function getTopIncome(): Collection + { + $transactions = $this->getIncome()->sortByDesc('transaction_amount'); + + return $transactions; + } + + /** + * @param Collection $collection + * + * @return array + */ + protected function summarizeByAccount(Collection $collection): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + $accountId = $transaction->account_id; + $result[$accountId] = $result[$accountId] ?? '0'; + $result[$accountId] = bcadd($transaction->transaction_amount, $result[$accountId]); + } + + return $result; + } + } diff --git a/app/Generator/Report/Tag/MonthReportGenerator.php b/app/Generator/Report/Tag/MonthReportGenerator.php new file mode 100644 index 0000000000..d58598c8f9 --- /dev/null +++ b/app/Generator/Report/Tag/MonthReportGenerator.php @@ -0,0 +1,219 @@ +expenses = new Collection; + $this->income = new Collection; + } + + /** + * @return string + */ + public function generate(): string + { + $accountIds = join(',', $this->accounts->pluck('id')->toArray()); + $tagTags = join(',', $this->tags->pluck('tag')->toArray()); + $reportType = 'tag'; + $expenses = $this->getExpenses(); + $income = $this->getIncome(); + $accountSummary = $this->getObjectSummary($this->summarizeByAccount($expenses), $this->summarizeByAccount($income)); + $tagSummary = $this->getObjectSummary($this->summarizeByTag($expenses), $this->summarizeByTag($income)); + $averageExpenses = $this->getAverages($expenses, SORT_ASC); + $averageIncome = $this->getAverages($income, SORT_DESC); + $topExpenses = $this->getTopExpenses(); + $topIncome = $this->getTopIncome(); + + + // render! + return view( + 'reports.tag.month', compact( + 'accountIds', 'tagTags', 'reportType', 'accountSummary', 'tagSummary', 'averageExpenses', 'averageIncome', 'topIncome', + 'topExpenses' + ) + )->with('start', $this->start)->with('end', $this->end)->with('tags', $this->tags)->with('accounts', $this->accounts)->render(); + } + + /** + * @param Collection $accounts + * + * @return ReportGeneratorInterface + */ + public function setAccounts(Collection $accounts): ReportGeneratorInterface + { + $this->accounts = $accounts; + + return $this; + } + + /** + * @param Collection $budgets + * + * @return ReportGeneratorInterface + */ + public function setBudgets(Collection $budgets): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Collection $categories + * + * @return ReportGeneratorInterface + */ + public function setCategories(Collection $categories): ReportGeneratorInterface + { + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setEndDate(Carbon $date): ReportGeneratorInterface + { + $this->end = $date; + + return $this; + } + + /** + * @param Carbon $date + * + * @return ReportGeneratorInterface + */ + public function setStartDate(Carbon $date): ReportGeneratorInterface + { + $this->start = $date; + + return $this; + } + + /** + * @param Collection $tags + * + * @return ReportGeneratorInterface + */ + public function setTags(Collection $tags): ReportGeneratorInterface + { + $this->tags = $tags; + + return $this; + } + + /** + * @return Collection + */ + protected function getExpenses(): Collection + { + if ($this->expenses->count() > 0) { + Log::debug('Return previous set of expenses.'); + + return $this->expenses; + } + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) + ->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setTags($this->tags)->withOpposingAccount()->disableFilter(); + + $accountIds = $this->accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $transactions = self::filterExpenses($transactions, $accountIds); + $this->expenses = $transactions; + + return $transactions; + } + + /** + * @return Collection + */ + protected function getIncome(): Collection + { + if ($this->income->count() > 0) { + return $this->income; + } + + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) + ->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->setTags($this->tags)->withOpposingAccount(); + $accountIds = $this->accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $transactions = self::filterIncome($transactions, $accountIds); + $this->income = $transactions; + + return $transactions; + } + + /** + * @param Collection $collection + * + * @return array + */ + protected function summarizeByTag(Collection $collection): array + { + $result = []; + /** @var Transaction $transaction */ + foreach ($collection as $transaction) { + $journal = $transaction->transactionJournal; + $journalTags = $journal->tags; + /** @var Tag $journalTag */ + foreach ($journalTags as $journalTag) { + $journalTagId = $journalTag->id; + $result[$journalTagId] = $result[$journalTagId] ?? '0'; + $result[$journalTagId] = bcadd($transaction->transaction_amount, $result[$journalTagId]); + } + } + + return $result; + } +} diff --git a/app/Generator/Report/Tag/MultiYearReportGenerator.php b/app/Generator/Report/Tag/MultiYearReportGenerator.php new file mode 100644 index 0000000000..3f92a96e7a --- /dev/null +++ b/app/Generator/Report/Tag/MultiYearReportGenerator.php @@ -0,0 +1,25 @@ +query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); } } + + /** + * @param Collection $tags + * + * @return JournalCollectorInterface + */ + public function setTags(Collection $tags): JournalCollectorInterface + { + $this->joinTagTables(); + $tagIds = $tags->pluck('id')->toArray(); + $this->query->whereIn('tag_transaction_journal.tag_id', $tagIds); + + return $this; + } } diff --git a/app/Helpers/Collector/JournalCollectorInterface.php b/app/Helpers/Collector/JournalCollectorInterface.php index 80e41eb781..05306699a7 100644 --- a/app/Helpers/Collector/JournalCollectorInterface.php +++ b/app/Helpers/Collector/JournalCollectorInterface.php @@ -141,6 +141,13 @@ interface JournalCollectorInterface */ public function setTag(Tag $tag): JournalCollectorInterface; + /** + * @param Collection $tags + * + * @return JournalCollectorInterface + */ + public function setTags(Collection $tags): JournalCollectorInterface; + /** * @param array $types * diff --git a/app/Http/Controllers/Chart/CategoryReportController.php b/app/Http/Controllers/Chart/CategoryReportController.php index 94f6cfb370..faffd3124f 100644 --- a/app/Http/Controllers/Chart/CategoryReportController.php +++ b/app/Http/Controllers/Chart/CategoryReportController.php @@ -331,22 +331,4 @@ class CategoryReportController extends Controller return $grouped; } - - /** - * @param Collection $set - * - * @return array - */ - private function groupByOpposingAccount(Collection $set): array - { - $grouped = []; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $accountId = $transaction->opposing_account_id; - $grouped[$accountId] = $grouped[$accountId] ?? '0'; - $grouped[$accountId] = bcadd($transaction->transaction_amount, $grouped[$accountId]); - } - - return $grouped; - } } diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php new file mode 100644 index 0000000000..afa6fb8c31 --- /dev/null +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -0,0 +1,216 @@ +generator = app(GeneratorInterface::class); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function mainChart(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty('chart.category.report.main'); + $cache->addProperty($accounts); + $cache->addProperty($tags); + $cache->addProperty($start); + $cache->addProperty($end); + if ($cache->has()) { + return Response::json($cache->get()); + } + + $format = Navigation::preferredCarbonLocalizedFormat($start, $end); + $function = Navigation::preferredEndOfPeriod($start, $end); + $chartData = []; + $currentStart = clone $start; + + // prep chart data: + foreach ($tags as $tag) { + $chartData[$tag->id . '-in'] = [ + 'label' => $tag->tag . ' (' . strtolower(strval(trans('firefly.income'))) . ')', + 'type' => 'bar', + 'yAxisID' => 'y-axis-0', + 'entries' => [], + ]; + $chartData[$tag->id . '-out'] = [ + 'label' => $tag->tag . ' (' . strtolower(strval(trans('firefly.expenses'))) . ')', + 'type' => 'bar', + 'yAxisID' => 'y-axis-0', + 'entries' => [], + ]; + // total in, total out: + $chartData[$tag->id . '-total-in'] = [ + 'label' => $tag->tag . ' (' . strtolower(strval(trans('firefly.sum_of_income'))) . ')', + 'type' => 'line', + 'fill' => false, + 'yAxisID' => 'y-axis-1', + 'entries' => [], + ]; + $chartData[$tag->id . '-total-out'] = [ + 'label' => $tag->tag . ' (' . strtolower(strval(trans('firefly.sum_of_expenses'))) . ')', + 'type' => 'line', + 'fill' => false, + 'yAxisID' => 'y-axis-1', + 'entries' => [], + ]; + } + $sumOfIncome = []; + $sumOfExpense = []; + + while ($currentStart < $end) { + $currentEnd = clone $currentStart; + $currentEnd = $currentEnd->$function(); + $expenses = $this->groupByTag($this->getExpenses($accounts, $tags, $currentStart, $currentEnd)); + $income = $this->groupByTag($this->getIncome($accounts, $tags, $currentStart, $currentEnd)); + $label = $currentStart->formatLocalized($format); + + /** @var Tag $tag */ + foreach ($tags as $tag) { + $labelIn = $tag->id . '-in'; + $labelOut = $tag->id . '-out'; + $labelSumIn = $tag->id . '-total-in'; + $labelSumOut = $tag->id . '-total-out'; + $currentIncome = $income[$tag->id] ?? '0'; + $currentExpense = $expenses[$tag->id] ?? '0'; + + + // add to sum: + $sumOfIncome[$tag->id] = $sumOfIncome[$tag->id] ?? '0'; + $sumOfExpense[$tag->id] = $sumOfExpense[$tag->id] ?? '0'; + $sumOfIncome[$tag->id] = bcadd($sumOfIncome[$tag->id], $currentIncome); + $sumOfExpense[$tag->id] = bcadd($sumOfExpense[$tag->id], $currentExpense); + + // add to chart: + $chartData[$labelIn]['entries'][$label] = $currentIncome; + $chartData[$labelOut]['entries'][$label] = $currentExpense; + $chartData[$labelSumIn]['entries'][$label] = $sumOfIncome[$tag->id]; + $chartData[$labelSumOut]['entries'][$label] = $sumOfExpense[$tag->id]; + } + $currentStart = clone $currentEnd; + $currentStart->addDay(); + } + // remove all empty entries to prevent cluttering: + $newSet = []; + foreach ($chartData as $key => $entry) { + if (!array_sum($entry['entries']) == 0) { + $newSet[$key] = $chartData[$key]; + } + } + if (count($newSet) === 0) { + $newSet = $chartData; + } + $data = $this->generator->multiSet($newSet); + $cache->store($data); + + return Response::json($data); + } + + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getExpenses(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): Collection + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setTags($tags)->withOpposingAccount()->disableFilter(); + $accountIds = $accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $set = MonthReportGenerator::filterExpenses($transactions, $accountIds); + + return $set; + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + private function getIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): Collection + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->setTags($tags)->withOpposingAccount(); + $accountIds = $accounts->pluck('id')->toArray(); + $transactions = $collector->getJournals(); + $set = MonthReportGenerator::filterIncome($transactions, $accountIds); + + return $set; + } + + /** + * @param Collection $set + * + * @return array + */ + private function groupByTag(Collection $set): array + { + // group by category ID: + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $journal = $transaction->transactionJournal; + $journalTags = $journal->tags; + /** @var Tag $journalTag */ + foreach ($journalTags as $journalTag) { + $journalTagId = $journalTag->id; + $grouped[$journalTagId] = $grouped[$journalTagId] ?? '0'; + $grouped[$journalTagId] = bcadd($transaction->transaction_amount, $grouped[$journalTagId]); + } + } + + return $grouped; + } + +} \ No newline at end of file diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index 05bf0acb56..62d7311e0a 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -556,6 +556,19 @@ Breadcrumbs::register( } ); +Breadcrumbs::register( + 'reports.report.tag', function (BreadCrumbGenerator $breadcrumbs, string $accountIds, string $tagTags, Carbon $start, Carbon $end) { + $breadcrumbs->parent('reports.index'); + + $monthFormat = (string)trans('config.month_and_day'); + $startString = $start->formatLocalized($monthFormat); + $endString = $end->formatLocalized($monthFormat); + $title = (string)trans('firefly.report_tag', ['start' => $startString, 'end' => $endString]); + + $breadcrumbs->push($title, route('reports.report.tag', [$accountIds, $tagTags, $start->format('Ymd'), $end->format('Ymd')])); +} +); + Breadcrumbs::register( 'reports.report.category', function (BreadCrumbGenerator $breadcrumbs, string $accountIds, string $categoryIds, Carbon $start, Carbon $end) { $breadcrumbs->parent('reports.index'); diff --git a/public/js/ff/reports/tag/all.js b/public/js/ff/reports/tag/all.js new file mode 100644 index 0000000000..25a412d1c5 --- /dev/null +++ b/public/js/ff/reports/tag/all.js @@ -0,0 +1,10 @@ +/* + * all.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + diff --git a/public/js/ff/reports/tag/month.js b/public/js/ff/reports/tag/month.js new file mode 100644 index 0000000000..1f1584655c --- /dev/null +++ b/public/js/ff/reports/tag/month.js @@ -0,0 +1,66 @@ +/* + * month.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: tagIncomeUri, tagExpenseUri, accountIncomeUri, accountExpenseUri, tagBudgetUri, tagCategoryUri, mainUri */ + +$(function () { + "use strict"; + drawChart(); + + $('#tags-in-pie-chart-checked').on('change', function () { + redrawPieChart('tags-in-pie-chart', tagIncomeUri); + }); + + $('#tags-out-pie-chart-checked').on('change', function () { + redrawPieChart('tags-out-pie-chart', tagExpenseUri); + }); + + $('#accounts-in-pie-chart-checked').on('change', function () { + redrawPieChart('accounts-in-pie-chart', accountIncomeUri); + }); + + $('#accounts-out-pie-chart-checked').on('change', function () { + redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + }); + + // two extra charts: + pieChart(tagBudgetUri, 'budgets-out-pie-chart'); + pieChart(tagCategoryUri, 'categories-out-pie-chart'); + +}); + + +function drawChart() { + "use strict"; + + // month view: + doubleYChart(mainUri, 'in-out-chart'); + + // draw pie chart of income, depending on "show other transactions too": + redrawPieChart('tags-in-pie-chart', tagIncomeUri); + redrawPieChart('tags-out-pie-chart', tagExpenseUri); + redrawPieChart('accounts-in-pie-chart', accountIncomeUri); + redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + + +} + +function redrawPieChart(container, uri) { + "use strict"; + var checkbox = $('#' + container + '-checked'); + + var others = '0'; + // check if box is checked: + if (checkbox.prop('checked')) { + others = '1'; + } + uri = uri.replace('OTHERS', others); + + pieChart(uri, container); + +} diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index d4e5fb0c73..3fe45e6dd0 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -667,6 +667,7 @@ return [ 'report_audit' => 'Transaction history overview between :start and :end', 'report_category' => 'Category report between :start and :end', 'report_budget' => 'Budget report between :start and :end', + 'report_tag' => 'Tag report between :start and :end', 'quick_link_reports' => 'Quick links', 'quick_link_default_report' => 'Default financial report', 'quick_link_audit_report' => 'Transaction history overview', diff --git a/resources/views/reports/tag/month.twig b/resources/views/reports/tag/month.twig new file mode 100644 index 0000000000..4ba5515c30 --- /dev/null +++ b/resources/views/reports/tag/month.twig @@ -0,0 +1,434 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, accountIds, tagTags, start, end) }} +{% endblock %} + +{% block content %} + +
+
+
+
+

{{ 'accounts'|_ }}

+
+
+ + + + + + + + + + {% for account in accounts %} + + + {% if accountSummary[account.id] %} + + {% else %} + + {% endif %} + {% if accountSummary[account.id] %} + + {% else %} + + {% endif %} + + {% endfor %} + +
{{ 'name'|_ }}{{ 'earned'|_ }}{{ 'spent'|_ }}
+ {{ account.name }} + {{ accountSummary[account.id].earned|formatAmount }}{{ 0|formatAmount }}{{ accountSummary[account.id].spent|formatAmount }}{{ 0|formatAmount }}
+
+
+
+
+
+
+

{{ 'income_per_tag'|_ }}

+
+
+ + +
+
+
+
+
+
+

{{ 'expense_per_tag'|_ }}

+
+
+ + +
+
+
+
+
+
+

{{ 'expense_per_budget'|_ }}

+
+
+ + Uitgaven per budget voor de gevonden transacties +
+
+
+
+
+
+
+
+

{{ 'tags'|_ }}

+
+
+ + + + + + + + + + {% for tag in tags %} + + + {% if tagSummary[tag.id] %} + + {% else %} + + {% endif %} + {% if tagSummary[tag.id] %} + + {% else %} + + {% endif %} + + {% endfor %} + +
{{ 'name'|_ }}{{ 'earned'|_ }}{{ 'spent'|_ }}
+ {{ tag.tag }} + {{ tagSummary[tag.id].earned|formatAmount }}{{ 0|formatAmount }}{{ tagSummary[tag.id].spent|formatAmount }}{{ 0|formatAmount }}
+
+
+
+
+
+
+

{{ 'income_per_account'|_ }}

+
+
+ + +
+
+
+
+
+
+

{{ 'expense_per_account'|_ }}

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

{{ 'expense_per_category'|_ }}

+
+
+ + Uitgaven per category voor de gevonden transacties +
+
+
+ +
+ +
+
+
+
+

{{ 'income_and_expenses'|_ }}

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

{{ 'average_spending_per_account'|_ }}

+
+
+ + + + + + + + + + + {% for row in averageExpenses %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + + + {% if averageExpenses|length > listLength %} + + + + {% endif %} + +
{{ 'account'|_ }}{{ 'spent_average'|_ }}{{ 'total'|_ }}{{ 'transaction_count'|_ }}
+ {{ row.name }} + + {{ row.average|formatAmount }} + + {{ row.sum|formatAmount }} + + {{ row.count }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
+
+
+
+ {% endif %} + {% if topExpenses.count > 0 %} +
+ +
+
+

{{ 'expenses'|_ }} ({{ trans('firefly.topX', {number: listLength}) }})

+
+
+ + + + + + + + + + + {% for row in topExpenses %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + + + {% if topExpenses|length > listLength %} + + + + {% endif %} + +
{{ 'description'|_ }}{{ 'date'|_ }}{{ 'account'|_ }}{{ 'amount'|_ }}
+ + {% if row.transaction_description|length > 0 %} + {{ row.transaction_description }} ({{ row.description }}) + {% else %} + {{ row.description }} + {% endif %} + + + {{ row.date.formatLocalized(monthAndDayFormat) }} + + + {{ row.opposing_account_name }} + + + {{ row.transaction_amount|formatAmount }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
+
+
+
+ {% endif %} +
+
+ {% if averageIncome|length > 0 %} +
+
+
+

{{ 'average_income_per_account'|_ }}

+
+
+ + + + + + + + + + + {% for row in averageIncome %} + + + + + + + {% endfor %} + +
{{ 'account'|_ }}{{ 'income_average'|_ }}{{ 'total'|_ }}{{ 'transaction_count'|_ }}
+ {{ row.name }} + + {{ row.average|formatAmount }} + + {{ row.sum|formatAmount }} + + {{ row.count }} +
+
+
+
+ {% endif %} +
+ {% if topIncome.count > 0 %} +
+
+

{{ 'income'|_ }} ({{ trans('firefly.topX', {number: listLength}) }})

+
+
+ + + + + + + + + + + {% for row in topIncome %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + + + {% if topIncome.count > listLength %} + + + + {% endif %} + +
{{ 'description'|_ }}{{ 'date'|_ }}{{ 'account'|_ }}{{ 'amount'|_ }}
+ + {% if row.transaction_description|length > 0 %} + {{ row.transaction_description }} ({{ row.description }}) + {% else %} + {{ row.description }} + {% endif %} + + + {{ row.date.formatLocalized(monthAndDayFormat) }} + + + {{ row.opposing_account_name }} + + + {{ row.transaction_amount|formatAmount }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
+
+
+ {% endif %} +
+
+ +{% endblock %} + +{% block scripts %} + + + + + + + + + + + +{% endblock %} + +{% block styles %} + +{% endblock %} diff --git a/routes/web.php b/routes/web.php index 92a108e5fe..9317011312 100755 --- a/routes/web.php +++ b/routes/web.php @@ -310,6 +310,47 @@ Route::group( } ); +/** + * Chart\Tag Controller + */ +Route::group( + ['middleware' => 'user-full-auth', 'namespace' => 'Chart', 'prefix' => 'chart/tag', 'as' => 'chart.tag.'], function () { + + // these charts are used in reports (tag reports): + Route::get( + 'tag/income/{accountList}/{tagList}/{start_date}/{end_date}/{others}', + ['uses' => 'TagReportController@tagIncome', 'as' => 'tag-income'] + ); + Route::get( + 'tag/expense/{accountList}/{tagList}/{start_date}/{end_date}/{others}', + ['uses' => 'TagReportController@tagExpense', 'as' => 'tag-expense'] + ); + Route::get( + 'account/income/{accountList}/{tagList}/{start_date}/{end_date}/{others}', + ['uses' => 'TagReportController@accountIncome', 'as' => 'account-income'] + ); + Route::get( + 'account/expense/{accountList}/{tagList}/{start_date}/{end_date}/{others}', + ['uses' => 'TagReportController@accountExpense', 'as' => 'account-expense'] + ); + Route::get( + 'budget/expense/{accountList}/{tagList}/{start_date}/{end_date}', + ['uses' => 'TagReportController@budgetExpense', 'as' => 'budget-expense'] + ); + Route::get( + 'category/expense/{accountList}/{tagList}/{start_date}/{end_date}', + ['uses' => 'TagReportController@categoryExpense', 'as' => 'category-expense'] + ); + + + Route::get( + 'operations/{accountList}/{tagList}/{start_date}/{end_date}', + ['uses' => 'TagReportController@mainChart', 'as' => 'main'] + ); + +} +); + /** * Chart\PiggyBank Controller */ From 3d4feff7deb8e98bcbcf2ff092627c4ede6ed8c2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 24 Feb 2017 20:27:26 +0100 Subject: [PATCH 039/244] More code for the tag report. --- app/Helpers/Chart/MetaPieChart.php | 23 ++++- app/Helpers/Chart/MetaPieChartInterface.php | 7 ++ .../Controllers/Chart/TagReportController.php | 96 ++++++++++++++++++- resources/views/transactions/index.twig | 4 - routes/web.php | 6 +- 5 files changed, 125 insertions(+), 11 deletions(-) diff --git a/app/Helpers/Chart/MetaPieChart.php b/app/Helpers/Chart/MetaPieChart.php index 6c340b5389..ba50d55738 100644 --- a/app/Helpers/Chart/MetaPieChart.php +++ b/app/Helpers/Chart/MetaPieChart.php @@ -47,7 +47,6 @@ class MetaPieChart implements MetaPieChartInterface 'budget' => ['transaction_journal_budget_id', 'transaction_budget_id'], 'category' => ['transaction_journal_category_id', 'transaction_category_id'], ]; - /** @var array */ protected $repositories = [ @@ -55,10 +54,10 @@ class MetaPieChart implements MetaPieChartInterface 'budget' => BudgetRepositoryInterface::class, 'category' => CategoryRepositoryInterface::class, ]; - - /** @var Carbon */ protected $start; + /** @var Collection */ + protected $tags; /** @var string */ protected $total = '0'; /** @var User */ @@ -87,7 +86,6 @@ class MetaPieChart implements MetaPieChartInterface if ($this->collectOtherObjects && $direction === 'expense') { /** @var JournalCollectorInterface $collector */ $collector = app(JournalCollectorInterface::class); - $collector->setUser($this->user); $collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::WITHDRAWAL]); $journals = $collector->getJournals(); $sum = strval($journals->sum('transaction_amount')); @@ -182,6 +180,18 @@ class MetaPieChart implements MetaPieChartInterface return $this; } + /** + * @param Collection $tags + * + * @return MetaPieChartInterface + */ + public function setTags(Collection $tags): MetaPieChartInterface + { + $this->tags = $tags; + + return $this; + } + /** * @param User $user * @@ -219,6 +229,11 @@ class MetaPieChart implements MetaPieChartInterface if ($this->categories->count() > 0) { $collector->setCategories($this->categories); } + if ($this->tags->count() > 0) { + $collector->setTags($this->tags); + $collector->withCategoryInformation(); + $collector->withBudgetInformation(); + } $accountIds = $this->accounts->pluck('id')->toArray(); $transactions = $collector->getJournals(); diff --git a/app/Helpers/Chart/MetaPieChartInterface.php b/app/Helpers/Chart/MetaPieChartInterface.php index 006300d308..b4aa0cd736 100644 --- a/app/Helpers/Chart/MetaPieChartInterface.php +++ b/app/Helpers/Chart/MetaPieChartInterface.php @@ -72,6 +72,13 @@ interface MetaPieChartInterface */ public function setStart(Carbon $start): MetaPieChartInterface; + /** + * @param Collection $tags + * + * @return MetaPieChartInterface + */ + public function setTags(Collection $tags): MetaPieChartInterface; + /** * @param User $user * diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php index afa6fb8c31..c4a6eb3cba 100644 --- a/app/Http/Controllers/Chart/TagReportController.php +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -15,6 +15,7 @@ namespace FireflyIII\Http\Controllers\Chart; use Carbon\Carbon; use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Generator\Report\Tag\MonthReportGenerator; +use FireflyIII\Helpers\Chart\MetaPieChartInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Tag; @@ -40,6 +41,100 @@ class TagReportController extends Controller $this->generator = app(GeneratorInterface::class); } + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setTags($tags); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('expense', 'account'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setTags($tags); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('income', 'account'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function budgetExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setTags($tags); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(false); + $chartData = $helper->generate('expense', 'budget'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function categoryExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setTags($tags); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(false); + $chartData = $helper->generate('expense', 'category'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + /** * @param Collection $accounts * @param Collection $tags @@ -146,7 +241,6 @@ class TagReportController extends Controller return Response::json($data); } - /** * @param Collection $accounts * @param Collection $tags diff --git a/resources/views/transactions/index.twig b/resources/views/transactions/index.twig index 1b86e5ac6b..33f8253f0c 100644 --- a/resources/views/transactions/index.twig +++ b/resources/views/transactions/index.twig @@ -7,9 +7,6 @@ {% block content %} - {% if journals.count == 0 %} - {% include 'partials.empty' with {what: what, type: 'transactions',route: route('transactions.create', [what])} %} - {% else %}
@@ -44,7 +41,6 @@
- {% endif %} {% endblock %} {% block scripts %} diff --git a/routes/web.php b/routes/web.php index 9317011312..5a70819fd4 100755 --- a/routes/web.php +++ b/routes/web.php @@ -333,13 +333,15 @@ Route::group( 'account/expense/{accountList}/{tagList}/{start_date}/{end_date}/{others}', ['uses' => 'TagReportController@accountExpense', 'as' => 'account-expense'] ); + + // new routes Route::get( 'budget/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@budgetExpense', 'as' => 'budget-expense'] ); - Route::get( - 'category/expense/{accountList}/{tagList}/{start_date}/{end_date}', + Route::get('category/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@categoryExpense', 'as' => 'category-expense'] + ); From fc2cee7a54eec30950e7b365015218d03b6218a3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 24 Feb 2017 21:01:33 +0100 Subject: [PATCH 040/244] Fixes tests. --- app/Helpers/Chart/MetaPieChart.php | 1 + .../Controllers/Chart/AccountControllerTest.php | 3 +++ .../Controllers/TransactionControllerTest.php | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/app/Helpers/Chart/MetaPieChart.php b/app/Helpers/Chart/MetaPieChart.php index ba50d55738..7d72637184 100644 --- a/app/Helpers/Chart/MetaPieChart.php +++ b/app/Helpers/Chart/MetaPieChart.php @@ -68,6 +68,7 @@ class MetaPieChart implements MetaPieChartInterface $this->accounts = new Collection; $this->budgets = new Collection; $this->categories = new Collection; + $this->tags = new Collection; } /** diff --git a/tests/Feature/Controllers/Chart/AccountControllerTest.php b/tests/Feature/Controllers/Chart/AccountControllerTest.php index e94470dd4b..d79f1cf9d5 100644 --- a/tests/Feature/Controllers/Chart/AccountControllerTest.php +++ b/tests/Feature/Controllers/Chart/AccountControllerTest.php @@ -18,6 +18,7 @@ class AccountControllerTest extends TestCase { /** * @covers \FireflyIII\Http\Controllers\Chart\AccountController::expenseAccounts + * @covers \FireflyIII\Generator\Chart\Basic\GeneratorInterface::singleSet * @dataProvider dateRangeProvider * * @param string $range @@ -61,6 +62,8 @@ class AccountControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\Chart\AccountController::frontpage * @covers \FireflyIII\Http\Controllers\Chart\AccountController::__construct + * @covers \FireflyIII\Http\Controllers\Chart\AccountController::accountBalanceChart + * @covers \FireflyIII\Generator\Chart\Basic\GeneratorInterface::multiSet * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/TransactionControllerTest.php b/tests/Feature/Controllers/TransactionControllerTest.php index 25171bfd85..e89196f637 100644 --- a/tests/Feature/Controllers/TransactionControllerTest.php +++ b/tests/Feature/Controllers/TransactionControllerTest.php @@ -67,6 +67,18 @@ class TransactionControllerTest extends TestCase $response->assertStatus(200); } + /** + * @covers \FireflyIII\Http\Controllers\Controller::redirectToAccount + * @covers \FireflyIII\Http\Controllers\TransactionController::show + */ + public function testShowOpeningBalance() + { + $this->be($this->user()); + $journal = $this->user()->transactionJournals()->where('transaction_type_id',4)->first(); + $response = $this->get(route('transactions.show', [$journal->id])); + $response->assertStatus(302); + } + /** * @covers \FireflyIII\Http\Controllers\TransactionController::show */ From 40c38af76642679689be3a61466cf87f4f94c1be Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 24 Feb 2017 21:09:20 +0100 Subject: [PATCH 041/244] Final code for tag report. --- app/Helpers/Chart/MetaPieChart.php | 31 +++++++++++- .../Controllers/Chart/TagReportController.php | 49 +++++++++++++++++++ resources/lang/en_US/firefly.php | 2 + resources/views/reports/tag/month.twig | 2 - 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/app/Helpers/Chart/MetaPieChart.php b/app/Helpers/Chart/MetaPieChart.php index 7d72637184..b93035c179 100644 --- a/app/Helpers/Chart/MetaPieChart.php +++ b/app/Helpers/Chart/MetaPieChart.php @@ -14,11 +14,13 @@ namespace FireflyIII\Helpers\Chart; use Carbon\Carbon; use FireflyIII\Generator\Report\Support; use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Collection; use Steam; @@ -46,6 +48,7 @@ class MetaPieChart implements MetaPieChartInterface 'account' => ['opposing_account_id'], 'budget' => ['transaction_journal_budget_id', 'transaction_budget_id'], 'category' => ['transaction_journal_category_id', 'transaction_category_id'], + 'tag' => [], ]; /** @var array */ protected $repositories @@ -53,6 +56,7 @@ class MetaPieChart implements MetaPieChartInterface 'account' => AccountRepositoryInterface::class, 'budget' => BudgetRepositoryInterface::class, 'category' => CategoryRepositoryInterface::class, + 'tag' => TagRepositoryInterface::class, ]; /** @var Carbon */ protected $start; @@ -249,8 +253,13 @@ class MetaPieChart implements MetaPieChartInterface * * @return array */ - protected function groupByFields(Collection $set, array $fields) + protected function groupByFields(Collection $set, array $fields): array { + if (count($fields) === 0 && $this->tags->count() > 0) { + // do a special group on tags: + return $this->groupByTag($set); + } + $grouped = []; /** @var Transaction $transaction */ foreach ($set as $transaction) { @@ -280,7 +289,7 @@ class MetaPieChart implements MetaPieChartInterface foreach ($array as $objectId => $amount) { if (!isset($names[$objectId])) { $object = $repository->find(intval($objectId)); - $names[$objectId] = $object->name; + $names[$objectId] = $object->name ?? $object->tag; } $amount = Steam::positive($amount); $this->total = bcadd($this->total, $amount); @@ -290,4 +299,22 @@ class MetaPieChart implements MetaPieChartInterface return $chartData; } + + private function groupByTag(Collection $set): array + { + $grouped = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $journal = $transaction->transactionJournal; + $tags = $journal->tags; + /** @var Tag $tag */ + foreach ($tags as $tag) { + $tagId = $tag->id; + $grouped[$tagId] = $grouped[$tagId] ?? '0'; + $grouped[$tagId] = bcadd($transaction->transaction_amount, $grouped[$tagId]); + } + } + + return $grouped; + } } diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php index c4a6eb3cba..0fd6f9d159 100644 --- a/app/Http/Controllers/Chart/TagReportController.php +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -241,6 +241,55 @@ class TagReportController extends Controller return Response::json($data); } + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function tagExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end, string $others) + { + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setTags($tags); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('expense', 'tag'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * @param string $others + * + * @return \Illuminate\Http\JsonResponse + */ + public function tagIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end, string $others) + { + + /** @var MetaPieChartInterface $helper */ + $helper = app(MetaPieChartInterface::class); + $helper->setAccounts($accounts); + $helper->setTags($tags); + $helper->setStart($start); + $helper->setEnd($end); + $helper->setCollectOtherObjects(intval($others) === 1); + $chartData = $helper->generate('income', 'tag'); + $data = $this->generator->pieChart($chartData); + + return Response::json($data); + } + /** * @param Collection $accounts * @param Collection $tags diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 3fe45e6dd0..d92a349172 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -747,6 +747,8 @@ return [ 'include_expense_not_in_category' => 'Included expenses not in the selected category(ies)', 'include_income_not_in_category' => 'Included income not in the selected category(ies)', 'include_income_not_in_account' => 'Included income not in the selected account(s)', + 'include_income_not_in_tags' => 'Included income not in the selected tag(s)', + 'include_expense_not_in_tags' => 'Included expenses not in the selected tag(s)', 'everything_else' => 'Everything else', 'income_and_expenses' => 'Income and expenses', 'spent_average' => 'Spent (average)', diff --git a/resources/views/reports/tag/month.twig b/resources/views/reports/tag/month.twig index 4ba5515c30..9f27985c04 100644 --- a/resources/views/reports/tag/month.twig +++ b/resources/views/reports/tag/month.twig @@ -81,7 +81,6 @@
- Uitgaven per budget voor de gevonden transacties
@@ -160,7 +159,6 @@
- Uitgaven per category voor de gevonden transacties
From 96c780c8045fdad4bcf81229a732197be1aa76ff Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 24 Feb 2017 21:11:51 +0100 Subject: [PATCH 042/244] New text for translation [skip ci] --- resources/lang/en_US/firefly.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index d92a349172..38908771f3 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -742,6 +742,8 @@ return [ 'expense_per_budget' => 'Expense per budget', 'income_per_account' => 'Income per account', 'expense_per_account' => 'Expense per account', + 'expense_per_tag' => 'Expense per tag', + 'income_per_tag' => 'Income per tag', 'include_expense_not_in_budget' => 'Included expenses not in the selected budget(s)', 'include_expense_not_in_account' => 'Included expenses not in the selected account(s)', 'include_expense_not_in_category' => 'Included expenses not in the selected category(ies)', From a0e66b913bd9fed8139a9952675b45beadfa81f1 Mon Sep 17 00:00:00 2001 From: Joris de Vries Date: Fri, 24 Feb 2017 22:00:49 +0100 Subject: [PATCH 043/244] Show suggested monthly savings for a piggybank If a piggybank has both a target date and a target amount, show how much money needs to be added to the piggybank each month to achieve both targets. Strings are currently hard-coded because I want to gauge the reaction to this :) --- app/Models/PiggyBank.php | 11 +++++++++++ app/Support/Twig/PiggyBank.php | 6 ++++++ resources/views/piggy-banks/show.twig | 8 ++++++++ 3 files changed, 25 insertions(+) diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index d5b2b00187..997d93fada 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -129,6 +129,17 @@ class PiggyBank extends Model } + public function getSuggestedMonthlyAmount() + { + if ($this->targetdate && $this->currentRelevantRep()->currentamount < $this->targetamount) { + $thisMonth = Carbon::now()->month; + $targetMonth = $this->targetdate->month; + $remainingAmount = $this->targetamount - $this->currentRelevantRep()->currentamount; + return $thisMonth < $targetMonth ? $remainingAmount / ($targetMonth - $thisMonth) : $remainingAmount ; + } + return 0; + } + /** * Get all of the piggy bank's notes. */ diff --git a/app/Support/Twig/PiggyBank.php b/app/Support/Twig/PiggyBank.php index 8a11fb546e..badb07889e 100644 --- a/app/Support/Twig/PiggyBank.php +++ b/app/Support/Twig/PiggyBank.php @@ -39,6 +39,12 @@ class PiggyBank extends Twig_Extension } ); + $functions[] = new Twig_SimpleFunction( + 'suggestedMonthlyAmount', function (PB $piggyBank) { + return $piggyBank->getSuggestedMonthlyAmount(); + } + ); + return $functions; } diff --git a/resources/views/piggy-banks/show.twig b/resources/views/piggy-banks/show.twig index 031aefb4ba..12b8140a71 100644 --- a/resources/views/piggy-banks/show.twig +++ b/resources/views/piggy-banks/show.twig @@ -70,6 +70,14 @@ {% endif %} + {% if piggyBank.targetdate %} + + Suggested monthly amount to save + + {{ suggestedMonthlyAmount(piggyBank)|formatAmount }} + + + {% endif %}
From 8c6972d12d7b3f309eafa3f07c76f070ee02b280 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 05:57:01 +0100 Subject: [PATCH 044/244] Various code cleanup. --- .../Collector/JournalExportCollector.php | 1 - app/Export/Entry/Entry.php | 1 - app/Generator/Report/Support.php | 40 ++++++------- .../Attachments/AttachmentHelperInterface.php | 2 + app/Helpers/Collector/JournalCollector.php | 28 ++++----- .../Controllers/Auth/RegisterController.php | 5 +- .../Controllers/Auth/TwoFactorController.php | 4 +- app/Http/Controllers/BudgetController.php | 5 +- .../Chart/BudgetReportController.php | 18 +----- .../Controllers/Chart/CategoryController.php | 58 +++++++++---------- app/Http/Controllers/CurrencyController.php | 4 ++ app/Http/Controllers/ExportController.php | 3 +- app/Http/Controllers/JavascriptController.php | 2 + app/Http/Controllers/JsonController.php | 2 + app/Http/Controllers/SearchController.php | 2 +- app/Http/Controllers/TagController.php | 2 +- .../RedirectIfTwoFactorAuthenticated.php | 6 +- app/Http/Middleware/Sandstorm.php | 36 +++--------- app/Import/Mapper/TransactionCurrencies.php | 4 +- app/Models/LimitRepetition.php | 21 ------- app/Models/PiggyBank.php | 24 ++++---- .../Account/AccountRepository.php | 3 +- app/Repositories/Bill/BillRepository.php | 3 +- .../Currency/CurrencyRepository.php | 1 + .../PiggyBank/PiggyBankRepository.php | 4 +- app/Support/Twig/PiggyBank.php | 2 +- app/Support/Twig/Transaction.php | 1 - public/js/ff/export/index.js | 2 +- public/js/ff/piggy-banks/index.js | 2 +- public/js/ff/reports/index.js | 4 +- public/js/ff/rules/create-edit.js | 2 +- public/js/ff/rules/index.js | 2 +- public/js/ff/transactions/single/create.js | 3 +- tests/Feature/ExampleTest.php | 4 +- tests/Unit/ExampleTest.php | 3 +- 35 files changed, 133 insertions(+), 171 deletions(-) diff --git a/app/Export/Collector/JournalExportCollector.php b/app/Export/Collector/JournalExportCollector.php index 551880ce4f..234c548baa 100644 --- a/app/Export/Collector/JournalExportCollector.php +++ b/app/Export/Collector/JournalExportCollector.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Export\Collector; use Carbon\Carbon; -use Crypt; use DB; use FireflyIII\Models\Transaction; use Illuminate\Database\Query\JoinClause; diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php index 2fb30850d0..f5300ede71 100644 --- a/app/Export/Entry/Entry.php +++ b/app/Export/Entry/Entry.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Export\Entry; -use Crypt; use Steam; /** diff --git a/app/Generator/Report/Support.php b/app/Generator/Report/Support.php index 9125031579..9a8e0f7233 100644 --- a/app/Generator/Report/Support.php +++ b/app/Generator/Report/Support.php @@ -80,6 +80,26 @@ class Support return $result; } + /** + * @return Collection + */ + public function getTopExpenses(): Collection + { + $transactions = $this->getExpenses()->sortBy('transaction_amount'); + + return $transactions; + } + + /** + * @return Collection + */ + public function getTopIncome(): Collection + { + $transactions = $this->getIncome()->sortByDesc('transaction_amount'); + + return $transactions; + } + /** * @param Collection $collection * @param int $sortFlag @@ -162,26 +182,6 @@ class Support return $return; } - /** - * @return Collection - */ - public function getTopExpenses(): Collection - { - $transactions = $this->getExpenses()->sortBy('transaction_amount'); - - return $transactions; - } - - /** - * @return Collection - */ - public function getTopIncome(): Collection - { - $transactions = $this->getIncome()->sortByDesc('transaction_amount'); - - return $transactions; - } - /** * @param Collection $collection * diff --git a/app/Helpers/Attachments/AttachmentHelperInterface.php b/app/Helpers/Attachments/AttachmentHelperInterface.php index dc6f9420aa..d380cfe9c6 100644 --- a/app/Helpers/Attachments/AttachmentHelperInterface.php +++ b/app/Helpers/Attachments/AttachmentHelperInterface.php @@ -44,6 +44,8 @@ interface AttachmentHelperInterface /** * @param Model $model * + * @param array $files + * * @return bool */ public function saveAttachmentsForModel(Model $model, array $files = null): bool; diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index 39b19b42de..46024ad842 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -430,6 +430,20 @@ class JournalCollector implements JournalCollectorInterface return $this; } + /** + * @param Collection $tags + * + * @return JournalCollectorInterface + */ + public function setTags(Collection $tags): JournalCollectorInterface + { + $this->joinTagTables(); + $tagIds = $tags->pluck('id')->toArray(); + $this->query->whereIn('tag_transaction_journal.tag_id', $tagIds); + + return $this; + } + /** * @param array $types * @@ -712,18 +726,4 @@ class JournalCollector implements JournalCollectorInterface $this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'); } } - - /** - * @param Collection $tags - * - * @return JournalCollectorInterface - */ - public function setTags(Collection $tags): JournalCollectorInterface - { - $this->joinTagTables(); - $tagIds = $tags->pluck('id')->toArray(); - $this->query->whereIn('tag_transaction_journal.tag_id', $tagIds); - - return $this; - } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 2cf28042ba..cd392a925c 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -122,12 +122,15 @@ class RegisterController extends Controller */ protected function create(array $data) { - return User::create( + /** @var User $user */ + $user = User::create( [ 'email' => $data['email'], 'password' => bcrypt($data['password']), ] ); + + return $user; } /** diff --git a/app/Http/Controllers/Auth/TwoFactorController.php b/app/Http/Controllers/Auth/TwoFactorController.php index 9b74cad644..55f581c777 100644 --- a/app/Http/Controllers/Auth/TwoFactorController.php +++ b/app/Http/Controllers/Auth/TwoFactorController.php @@ -79,9 +79,11 @@ class TwoFactorController extends Controller /** * @param TokenFormRequest $request - * @SuppressWarnings(PHPMD.UnusedFormalParameter) // it's unused but the class does some validation. + * @param CookieJar $cookieJar * * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) // it's unused but the class does some validation. + * */ public function postIndex(TokenFormRequest $request, CookieJar $cookieJar) { diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index c7c73030f7..717823ddbb 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -105,7 +105,8 @@ class BudgetController extends Controller } /** - * @param Budget $budget + * @param Request $request + * @param Budget $budget * * @return View */ @@ -214,6 +215,8 @@ class BudgetController extends Controller } /** + * @param BudgetIncomeRequest $request + * * @return \Illuminate\Http\RedirectResponse */ public function postUpdateIncome(BudgetIncomeRequest $request) diff --git a/app/Http/Controllers/Chart/BudgetReportController.php b/app/Http/Controllers/Chart/BudgetReportController.php index d570eae6b8..7c7eba81ac 100644 --- a/app/Http/Controllers/Chart/BudgetReportController.php +++ b/app/Http/Controllers/Chart/BudgetReportController.php @@ -203,6 +203,7 @@ class BudgetReportController extends Controller * Returns the budget limits belonging to the given budget and valid on the given day. * * @param Collection $budgetLimits + * @param Budget $budget * @param Carbon $start * @param Carbon $end * @@ -268,21 +269,4 @@ class BudgetReportController extends Controller return $grouped; } - /** - * @param Collection $set - * - * @return array - */ - private function groupByOpposingAccount(Collection $set): array - { - $grouped = []; - /** @var Transaction $transaction */ - foreach ($set as $transaction) { - $accountId = $transaction->opposing_account_id; - $grouped[$accountId] = $grouped[$accountId] ?? '0'; - $grouped[$accountId] = bcadd($transaction->transaction_amount, $grouped[$accountId]); - } - - return $grouped; - } } diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index 217d122bc8..40645df6d0 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -20,7 +20,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface as CRI; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; use Navigation; @@ -50,13 +50,13 @@ class CategoryController extends Controller /** * Show an overview for a category for all time, per month/week/year. * - * @param CRI $repository - * @param AccountRepositoryInterface $accountRepository - * @param Category $category + * @param CategoryRepositoryInterface $repository + * @param AccountRepositoryInterface $accountRepository + * @param Category $category * * @return \Symfony\Component\HttpFoundation\Response */ - public function all(CRI $repository, AccountRepositoryInterface $accountRepository, Category $category) + public function all(CategoryRepositoryInterface $repository, AccountRepositoryInterface $accountRepository, Category $category) { $cache = new CacheProperties; $cache->addProperty('chart.category.all'); @@ -106,12 +106,12 @@ class CategoryController extends Controller } /** - * @param CRI $repository - * @param Category $category + * @param CategoryRepositoryInterface $repository + * @param Category $category * * @return \Symfony\Component\HttpFoundation\Response */ - public function currentPeriod(CRI $repository, Category $category) + public function currentPeriod(CategoryRepositoryInterface $repository, Category $category) { $start = clone session('start', Carbon::now()->startOfMonth()); $end = session('end', Carbon::now()->endOfMonth()); @@ -121,12 +121,12 @@ class CategoryController extends Controller } /** - * @param CRI $repository - * @param AccountRepositoryInterface $accountRepository + * @param CategoryRepositoryInterface $repository + * @param AccountRepositoryInterface $accountRepository * * @return \Illuminate\Http\JsonResponse */ - public function frontpage(CRI $repository, AccountRepositoryInterface $accountRepository) + public function frontpage(CategoryRepositoryInterface $repository, AccountRepositoryInterface $accountRepository) { $start = session('start', Carbon::now()->startOfMonth()); $end = session('end', Carbon::now()->endOfMonth()); @@ -161,15 +161,15 @@ class CategoryController extends Controller } /** - * @param CRI $repository - * @param Category $category - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param CategoryRepositoryInterface $repository + * @param Category $category + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end * * @return \Illuminate\Http\JsonResponse|mixed */ - public function reportPeriod(CRI $repository, Category $category, Collection $accounts, Carbon $start, Carbon $end) + public function reportPeriod(CategoryRepositoryInterface $repository, Category $category, Collection $accounts, Carbon $start, Carbon $end) { $cache = new CacheProperties; $cache->addProperty($start); @@ -210,14 +210,14 @@ class CategoryController extends Controller } /** - * @param CRI $repository - * @param Collection $accounts - * @param Carbon $start - * @param Carbon $end + * @param CategoryRepositoryInterface $repository + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end * * @return \Illuminate\Http\JsonResponse|mixed */ - public function reportPeriodNoCategory(CRI $repository, Collection $accounts, Carbon $start, Carbon $end) + public function reportPeriodNoCategory(CategoryRepositoryInterface $repository, Collection $accounts, Carbon $start, Carbon $end) { $cache = new CacheProperties; $cache->addProperty($start); @@ -257,14 +257,14 @@ class CategoryController extends Controller } /** - * @param CRI $repository + * @param CategoryRepositoryInterface $repository * @param Category $category * * @param $date * * @return \Symfony\Component\HttpFoundation\Response */ - public function specificPeriod(CRI $repository, Category $category, $date) + public function specificPeriod(CategoryRepositoryInterface $repository, Category $category, $date) { $carbon = new Carbon($date); $range = Preferences::get('viewRange', '1M')->data; @@ -277,14 +277,14 @@ class CategoryController extends Controller /** - * @param CRI $repository - * @param Category $category - * @param Carbon $start - * @param Carbon $end + * @param CategoryRepositoryInterface $repository + * @param Category $category + * @param Carbon $start + * @param Carbon $end * * @return array */ - private function makePeriodChart(CRI $repository, Category $category, Carbon $start, Carbon $end) + private function makePeriodChart(CategoryRepositoryInterface $repository, Category $category, Carbon $start, Carbon $end) { $cache = new CacheProperties; $cache->addProperty($start); diff --git a/app/Http/Controllers/CurrencyController.php b/app/Http/Controllers/CurrencyController.php index d38e964c62..606cac686f 100644 --- a/app/Http/Controllers/CurrencyController.php +++ b/app/Http/Controllers/CurrencyController.php @@ -50,6 +50,8 @@ class CurrencyController extends Controller } /** + * @param Request $request + * * @return View */ public function create(Request $request) @@ -90,6 +92,7 @@ class CurrencyController extends Controller /** + * @param Request $request * @param CurrencyRepositoryInterface $repository * @param TransactionCurrency $currency * @@ -115,6 +118,7 @@ class CurrencyController extends Controller } /** + * @param Request $request * @param CurrencyRepositoryInterface $repository * @param TransactionCurrency $currency * diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index 823c82e928..865f8133eb 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -55,9 +55,10 @@ class ExportController extends Controller } /** + * @param EJRI $repository * @param ExportJob $job * - * @return \Symfony\Component\HttpFoundation\Response|\Illuminate\Contracts\Routing\ResponseFactory + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response * @throws FireflyException */ public function download(ExportJobRepositoryInterface $repository, ExportJob $job) diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php index 6e63e57a43..c5f76133d9 100644 --- a/app/Http/Controllers/JavascriptController.php +++ b/app/Http/Controllers/JavascriptController.php @@ -27,7 +27,9 @@ class JavascriptController extends Controller { /** + * @param Request $request * + * @return $this */ public function variables(Request $request) { diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index 4d95c51fc9..a87dbfba38 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -329,7 +329,9 @@ class JsonController extends Controller } /** + * @param JournalRepositoryInterface $repository * + * @return \Illuminate\Http\JsonResponse */ public function transactionTypes(JournalRepositoryInterface $repository) { diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 17e868a2fb..bf4b933d7d 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -56,7 +56,7 @@ class SearchController extends Controller $limit = 20; // ui stuff: - $subTitle = ''; + $subTitle = ''; // query stuff $query = null; diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 6b1a7cfa66..b746961419 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -219,7 +219,7 @@ class TagController extends Controller } } - return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection','count')); + return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection', 'count')); } /** diff --git a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php index 53dcaa619e..5d58521a7d 100644 --- a/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php +++ b/app/Http/Middleware/RedirectIfTwoFactorAuthenticated.php @@ -14,11 +14,11 @@ declare(strict_types = 1); namespace FireflyIII\Http\Middleware; use Closure; +use Cookie; use Illuminate\Support\Facades\Auth; use Preferences; -use Session; -use Log; -use Cookie; + + /** * Class RedirectIfTwoFactorAuthenticated * diff --git a/app/Http/Middleware/Sandstorm.php b/app/Http/Middleware/Sandstorm.php index b995bb0e51..e07a0ca1b5 100644 --- a/app/Http/Middleware/Sandstorm.php +++ b/app/Http/Middleware/Sandstorm.php @@ -35,6 +35,7 @@ class Sandstorm * @param string|null $guard * * @return mixed + * @throws FireflyException */ public function handle(Request $request, Closure $next, $guard = null) { @@ -59,30 +60,34 @@ class Sandstorm // and any other differences there may be between these users. if ($count === 1 && strlen($userId) > 0) { // login as first user user. - $user = User::first(); + $user = User::first(); Auth::guard($guard)->login($user); View::share('SANDSTORM_ANON', false); + return $next($request); } if ($count === 1 && strlen($userId) === 0) { // login but indicate anonymous - $user = User::first(); + $user = User::first(); Auth::guard($guard)->login($user); View::share('SANDSTORM_ANON', true); + return $next($request); } if ($count === 0 && strlen($userId) > 0) { // create new user. $email = $userId . '@firefly'; - $user = User::create( + /** @var User $user */ + $user = User::create( [ 'email' => $email, 'password' => str_random(16), ] ); Auth::guard($guard)->login($user); + return $next($request); } @@ -91,30 +96,7 @@ class Sandstorm } if ($count > 1) { - die('Cannot happen.'); - } - exit; - - if (strlen($userId) > 0) { - // find user? - $email = $userId . '@firefly'; - $user = User::whereEmail($email)->first(); - if (is_null($user)) { - $user = User::create( - [ - 'email' => $email, - 'password' => str_random(16), - ] - ); - - } - - - // login user: - Auth::guard($guard)->login($user); - } else { - echo 'user id no length, guest?'; - exit; + throw new FireflyException('Your Firefly III installation has more than one user, which is weird.'); } } diff --git a/app/Import/Mapper/TransactionCurrencies.php b/app/Import/Mapper/TransactionCurrencies.php index f5e157c5a3..94baa13b91 100644 --- a/app/Import/Mapper/TransactionCurrencies.php +++ b/app/Import/Mapper/TransactionCurrencies.php @@ -13,7 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Import\Mapper; -use FireflyIII\Models\TransactionCurrency as TC; +use FireflyIII\Models\TransactionCurrency; /** * Class TransactionCurrencies @@ -28,7 +28,7 @@ class TransactionCurrencies implements MapperInterface */ public function getMap(): array { - $currencies = TC::get(); + $currencies = TransactionCurrency::get(); $list = []; foreach ($currencies as $currency) { $list[$currency->id] = $currency->name . ' (' . $currency->code . ')'; diff --git a/app/Models/LimitRepetition.php b/app/Models/LimitRepetition.php index d4f5b5f3dc..3c61c378f9 100644 --- a/app/Models/LimitRepetition.php +++ b/app/Models/LimitRepetition.php @@ -16,7 +16,6 @@ namespace FireflyIII\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class LimitRepetition @@ -42,26 +41,6 @@ class LimitRepetition extends Model protected $dates = ['created_at', 'updated_at', 'startdate', 'enddate']; protected $hidden = ['amount_encrypted']; - /** - * @param $value - * - * @return mixed - */ - public static function routeBinder($value) - { - if (auth()->check()) { - $object = self::where('limit_repetitions.id', $value) - ->leftJoin('budget_limits', 'budget_limits.id', '=', 'limit_repetitions.budget_limit_id') - ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->where('budgets.user_id', auth()->user()->id) - ->first(['limit_repetitions.*']); - if ($object) { - return $object; - } - } - throw new NotFoundHttpException; - } - /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 997d93fada..b9113a8dbb 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -108,6 +108,19 @@ class PiggyBank extends Model return $value; } + public function getSuggestedMonthlyAmount() + { + if ($this->targetdate && $this->currentRelevantRep()->currentamount < $this->targetamount) { + $thisMonth = Carbon::now()->month; + $targetMonth = $this->targetdate->month; + $remainingAmount = $this->targetamount - $this->currentRelevantRep()->currentamount; + + return $thisMonth < $targetMonth ? $remainingAmount / ($targetMonth - $thisMonth) : $remainingAmount; + } + + return 0; + } + /** * * @param Carbon $date @@ -129,17 +142,6 @@ class PiggyBank extends Model } - public function getSuggestedMonthlyAmount() - { - if ($this->targetdate && $this->currentRelevantRep()->currentamount < $this->targetamount) { - $thisMonth = Carbon::now()->month; - $targetMonth = $this->targetdate->month; - $remainingAmount = $this->targetamount - $this->currentRelevantRep()->currentamount; - return $thisMonth < $targetMonth ? $remainingAmount / ($targetMonth - $thisMonth) : $remainingAmount ; - } - return 0; - } - /** * Get all of the piggy bank's notes. */ diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 26ba26b6ea..23e53fe8da 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -457,7 +457,8 @@ class AccountRepository implements AccountRepositoryInterface $name = $data['name']; $opposing = $this->storeOpposingAccount($name); $transactionType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first(); - $journal = TransactionJournal::create( + /** @var TransactionJournal $journal */ + $journal = TransactionJournal::create( [ 'user_id' => $this->user->id, 'transaction_type_id' => $transactionType->id, diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 545a29fb96..43874982fa 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -525,8 +525,7 @@ class BillRepository implements BillRepositoryInterface */ public function store(array $data): Bill { - - + /** @var Bill $bill */ $bill = Bill::create( [ 'name' => $data['name'], diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index b053780cac..9af830efae 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -193,6 +193,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface */ public function store(array $data): TransactionCurrency { + /** @var TransactionCurrency $currency */ $currency = TransactionCurrency::create( [ 'name' => $data['name'], diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 1f14467a5b..96226b81e3 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -40,6 +40,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function createEvent(PiggyBank $piggyBank, string $amount): PiggyBankEvent { + /** @var PiggyBankEvent $event */ $event = PiggyBankEvent::create(['date' => Carbon::now(), 'amount' => $amount, 'piggy_bank_id' => $piggyBank->id]); return $event; @@ -173,7 +174,8 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function store(array $data): PiggyBank { $data['order'] = $this->getMaxOrder() + 1; - $piggyBank = PiggyBank::create($data); + /** @var PiggyBank $piggyBank */ + $piggyBank = PiggyBank::create($data); $this->updateNote($piggyBank, $data['note']); diff --git a/app/Support/Twig/PiggyBank.php b/app/Support/Twig/PiggyBank.php index badb07889e..4263985a82 100644 --- a/app/Support/Twig/PiggyBank.php +++ b/app/Support/Twig/PiggyBank.php @@ -41,7 +41,7 @@ class PiggyBank extends Twig_Extension $functions[] = new Twig_SimpleFunction( 'suggestedMonthlyAmount', function (PB $piggyBank) { - return $piggyBank->getSuggestedMonthlyAmount(); + return $piggyBank->getSuggestedMonthlyAmount(); } ); diff --git a/app/Support/Twig/Transaction.php b/app/Support/Twig/Transaction.php index f1a0b98dd5..70abfc30d4 100644 --- a/app/Support/Twig/Transaction.php +++ b/app/Support/Twig/Transaction.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Support\Twig; use Amount; -use Crypt; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction as TransactionModel; use FireflyIII\Models\TransactionCurrency; diff --git a/public/js/ff/export/index.js b/public/js/ff/export/index.js index e7af126f51..c7d0f3fd40 100644 --- a/public/js/ff/export/index.js +++ b/public/js/ff/export/index.js @@ -79,7 +79,7 @@ function showDownload() { function showError(text) { "use strict"; $('#export-error').show(); - $('#export-error>p').text(text); + $('#export-error').find('p').text(text); } function callExport() { diff --git a/public/js/ff/piggy-banks/index.js b/public/js/ff/piggy-banks/index.js index 70630ad6d5..f83320f24f 100644 --- a/public/js/ff/piggy-banks/index.js +++ b/public/js/ff/piggy-banks/index.js @@ -24,7 +24,7 @@ $(function () { $('.addMoney').on('click', addMoney); $('.removeMoney').on('click', removeMoney); - $('#sortable-piggy tbody').sortable( + $('#sortable-piggy').find('tbody').sortable( { helper: fixHelper, stop: stopSorting, diff --git a/public/js/ff/reports/index.js b/public/js/ff/reports/index.js index df65b9704f..2891c12fd1 100644 --- a/public/js/ff/reports/index.js +++ b/public/js/ff/reports/index.js @@ -29,10 +29,10 @@ $(function () { { locale: { format: 'YYYY-MM-DD', - firstDay: 1, + firstDay: 1 }, minDate: minDate, - drops: 'up', + drops: 'up' } ); diff --git a/public/js/ff/rules/create-edit.js b/public/js/ff/rules/create-edit.js index 9100050012..1652ae804b 100644 --- a/public/js/ff/rules/create-edit.js +++ b/public/js/ff/rules/create-edit.js @@ -300,7 +300,7 @@ function testRuleTriggers() { } // Show the modal dialog - $("#testTriggerModal").modal(); + modal.modal(); }).fail(function () { alert('Cannot get transactions for given triggers.'); }); diff --git a/public/js/ff/rules/index.js b/public/js/ff/rules/index.js index 0fc0ef7371..a64d6a6cc6 100644 --- a/public/js/ff/rules/index.js +++ b/public/js/ff/rules/index.js @@ -25,7 +25,7 @@ $(function () { { helper: fixHelper, stop: sortStop, - cursor: "move", + cursor: "move" } ); diff --git a/public/js/ff/transactions/single/create.js b/public/js/ff/transactions/single/create.js index 7a38946984..7cf2796417 100644 --- a/public/js/ff/transactions/single/create.js +++ b/public/js/ff/transactions/single/create.js @@ -35,8 +35,7 @@ $(document).ready(function () { function updateDescription() { $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="description"]').typeahead('destroy'); - $('input[name="description"]').typeahead({source: data}); + $('input[name="description"]').typeahead('destroy').typeahead({source: data}); }); } diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index a8a339029f..11307fa66a 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -10,9 +10,7 @@ namespace Tests\Feature; use Tests\TestCase; -use Illuminate\Foundation\Testing\WithoutMiddleware; -use Illuminate\Foundation\Testing\DatabaseMigrations; -use Illuminate\Foundation\Testing\DatabaseTransactions; + class ExampleTest extends TestCase { diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php index 5663bb49f8..9744e771e7 100644 --- a/tests/Unit/ExampleTest.php +++ b/tests/Unit/ExampleTest.php @@ -3,8 +3,7 @@ namespace Tests\Unit; use Tests\TestCase; -use Illuminate\Foundation\Testing\DatabaseMigrations; -use Illuminate\Foundation\Testing\DatabaseTransactions; + class ExampleTest extends TestCase { From 251206fb75b2ac6e9c1f1c710550a7978b402bbd Mon Sep 17 00:00:00 2001 From: Joris de Vries Date: Sat, 25 Feb 2017 12:26:46 +0100 Subject: [PATCH 045/244] Make suggested savings text translatable PR #594 introduced suggested savings, this commit makes the text translatable. --- resources/lang/en_US/firefly.php | 1 + resources/views/piggy-banks/show.twig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 38908771f3..f5240948f6 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -806,6 +806,7 @@ return [ 'sum_of_piggy_banks' => 'Sum of piggy banks', 'saved_so_far' => 'Saved so far', 'left_to_save' => 'Left to save', + 'suggested_amount' => 'Suggested monthly amount to save', 'add_money_to_piggy_title' => 'Add money to piggy bank ":name"', 'remove_money_from_piggy_title' => 'Remove money from piggy bank ":name"', 'add' => 'Add', diff --git a/resources/views/piggy-banks/show.twig b/resources/views/piggy-banks/show.twig index 12b8140a71..748462ac64 100644 --- a/resources/views/piggy-banks/show.twig +++ b/resources/views/piggy-banks/show.twig @@ -72,7 +72,7 @@ {% if piggyBank.targetdate %} - Suggested monthly amount to save + {{ 'suggested_amount'|_ }} {{ suggestedMonthlyAmount(piggyBank)|formatAmount }} From 9eea4749f0e2f143f0ff8ce49e6131d88c714994 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 12:55:56 +0100 Subject: [PATCH 046/244] Remove IDE and environment specific files from gitignore, as inspired by #598. --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index 6b366227eb..b1e27bfc74 100755 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,7 @@ /node_modules /public/storage /vendor -/.idea Homestead.json Homestead.yaml .env -_development -.env.local -result.html -test-import.sh -test-import-report.txt public/google*.html -.env.backup From 4f50689d0e96b85771f194277e5fba757c7c5981 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 13:05:33 +0100 Subject: [PATCH 047/244] - Will now return 0 when nothing to save or when target date is in the past. - Will calculate correctly when date difference with target date is more than a year. - Will always return a string - Will do calculations using bcmath module. --- app/Models/PiggyBank.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index b9113a8dbb..2bdbc32791 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -108,17 +108,29 @@ class PiggyBank extends Model return $value; } - public function getSuggestedMonthlyAmount() + /** + * @return string + */ + public function getSuggestedMonthlyAmount(): string { + $savePerMonth = '0'; if ($this->targetdate && $this->currentRelevantRep()->currentamount < $this->targetamount) { - $thisMonth = Carbon::now()->month; - $targetMonth = $this->targetdate->month; - $remainingAmount = $this->targetamount - $this->currentRelevantRep()->currentamount; + $now = Carbon::now(); + $diffInMonths = $now->diffInMonths($this->targetdate, false); + $remainingAmount = bcsub($this->targetamount, $this->currentRelevantRep()->currentamount); - return $thisMonth < $targetMonth ? $remainingAmount / ($targetMonth - $thisMonth) : $remainingAmount; + // more than 1 month to go and still need money to save: + if ($diffInMonths > 0 && bccomp($remainingAmount, '0') === 1) { + $savePerMonth = bcdiv($remainingAmount, strval($diffInMonths)); + } + + // less than 1 month to go but still need money to save: + if ($diffInMonths === 0 && bccomp($remainingAmount, '0') === 1) { + $savePerMonth = $remainingAmount; + } } - return 0; + return $savePerMonth; } /** From de9ef200146de6dbc566c80d881ab2996168f515 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 13:13:51 +0100 Subject: [PATCH 048/244] First code for #595. Charts are still broken. --- app/Http/Controllers/AccountController.php | 240 +++++++++++++-------- app/Http/breadcrumbs.php | 32 ++- resources/lang/en_US/firefly.php | 1 + resources/views/accounts/show.twig | 45 ++-- routes/web.php | 3 +- 5 files changed, 197 insertions(+), 124 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 2c12693bcd..0327b6bfb8 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -227,103 +227,171 @@ class AccountController extends Controller return view('accounts.index', compact('what', 'subTitleIcon', 'subTitle', 'accounts')); } - /** - * @param Request $request - * @param JournalCollectorInterface $collector - * @param Account $account - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View - */ - public function show(Request $request, JournalCollectorInterface $collector, Account $account) - { - if ($account->accountType->type === AccountType::INITIAL_BALANCE) { - return $this->redirectToOriginalAccount($account); - } - // show journals from current period only: - $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); - $subTitle = $account->name; - $range = Preferences::get('viewRange', '1M')->data; - $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); - $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); - $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); - $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - $chartUri = route('chart.account.single', [$account->id]); - $accountType = $account->accountType->type; - - // grab those journals: - $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); - $journals = $collector->getPaginatedJournals(); - $journals->setPath('accounts/show/' . $account->id); - - // generate entries for each period (and cache those) - $entries = $this->periodEntries($account); - - return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); - } - - /** - * @param Request $request - * @param AccountRepositoryInterface $repository - * @param Account $account - * - * @return View - */ - public function showAll(Request $request, AccountRepositoryInterface $repository, Account $account) - { - $subTitle = sprintf('%s (%s)', $account->name, strtolower(trans('firefly.everything'))); - $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); - $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - $chartUri = route('chart.account.all', [$account->id]); - - // replace with journal collector: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setUser(auth()->user()); - $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); - $journals = $collector->getPaginatedJournals(); - $journals->setPath('accounts/show/' . $account->id . '/all'); - - // get oldest and newest journal for account: - $start = $repository->oldestJournalDate($account); - $end = $repository->newestJournalDate($account); - - // same call, except "entries". - return view('accounts.show', compact('account', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); - } /** * @param Request $request * @param Account $account - * @param string $date - * - * @return View + * @param string $moment */ - public function showByDate(Request $request, Account $account, string $date) + public function show(Request $request, Account $account, string $moment = '') { - $carbon = new Carbon($date); - $range = Preferences::get('viewRange', '1M')->data; - $start = Navigation::startOfPeriod($carbon, $range); - $end = Navigation::endOfPeriod($carbon, $range); - $subTitle = $account->name . ' (' . Navigation::periodShow($start, $range) . ')'; - $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); - $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - $chartUri = route('chart.account.period', [$account->id, $carbon->format('Y-m-d')]); + if ($account->accountType->type === AccountType::INITIAL_BALANCE) { + return $this->redirectToOriginalAccount($account); + } + $subTitle = $account->name; + $range = Preferences::get('viewRange', '1M')->data; + $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); + $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + $chartUri = route('chart.account.single', [$account->id]); + $start = null; + $end = null; + $periods = new Collection; + + // prep for "all" view. + if ($moment === 'all') { + $subTitle = $account->name . ' (' . strtolower(strval(trans('firefly.everything'))) . ')'; + $chartUri = route('chart.account.all', [$account->id]); + } + + // prep for "specific date" view. + if (strlen($moment) > 0 && $moment !== 'all') { + $start = new Carbon($moment); + $end = Navigation::endOfPeriod($start, $range); + $subTitle = $account->name . ' (' . strval(trans('firefly.from_to_breadcrumb', ['start' => 'x', 'end' => 'x'])) . ')'; + $chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d')]); + $periods = $this->periodEntries($account); + } + + // prep for current period + if (strlen($moment) === 0) { + $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); + $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); + $periods = $this->periodEntries($account); + } + $accountType = $account->accountType->type; + $count = 0; + // grab journals, but be prepared to jump a period back to get the right ones: + while ($count === 0) { + $collector = app(JournalCollectorInterface::class); + Log::debug('Count is zero, search for journals.'); + $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); + if (!is_null($start)) { + $collector->setRange($start, $end); + } + $journals = $collector->getPaginatedJournals(); + $journals->setPath('accounts/show/' . $account->id); + $count = $journals->getCollection()->count(); + if ($count === 0) { + $start->subDay(); + $start = Navigation::startOfPeriod($start, $range); + $end = Navigation::endOfPeriod($start, $range); + Log::debug(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d'))); + } + } - // replace with journal collector: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); - $journals = $collector->getPaginatedJournals(); - $journals->setPath('accounts/show/' . $account->id . '/' . $date); - // generate entries for each period (and cache those) - $entries = $this->periodEntries($account); - - // same call, except "entries". - return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); + return view('accounts.show', compact('account', 'accountType', 'periods', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); } + // /** + // * @param Request $request + // * @param JournalCollectorInterface $collector + // * @param Account $account + // * + // * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View + // */ + // public function show(Request $request, JournalCollectorInterface $collector, Account $account) + // { + // if ($account->accountType->type === AccountType::INITIAL_BALANCE) { + // return $this->redirectToOriginalAccount($account); + // } + // // show journals from current period only: + // $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); + // $subTitle = $account->name; + // $range = Preferences::get('viewRange', '1M')->data; + // $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); + // $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); + // $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + // $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + // $chartUri = route('chart.account.single', [$account->id]); + // $accountType = $account->accountType->type; + // + // // grab those journals: + // $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); + // $journals = $collector->getPaginatedJournals(); + // $journals->setPath('accounts/show/' . $account->id); + // + // // generate entries for each period (and cache those) + // $entries = $this->periodEntries($account); + // + // return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); + // } + // + // /** + // * @param Request $request + // * @param AccountRepositoryInterface $repository + // * @param Account $account + // * + // * @return View + // */ + // public function showAll(Request $request, AccountRepositoryInterface $repository, Account $account) + // { + // $subTitle = sprintf('%s (%s)', $account->name, strtolower(trans('firefly.everything'))); + // $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + // $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + // $chartUri = route('chart.account.all', [$account->id]); + // + // // replace with journal collector: + // /** @var JournalCollectorInterface $collector */ + // $collector = app(JournalCollectorInterface::class); + // $collector->setUser(auth()->user()); + // $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); + // $journals = $collector->getPaginatedJournals(); + // $journals->setPath('accounts/show/' . $account->id . '/all'); + // + // // get oldest and newest journal for account: + // $start = $repository->oldestJournalDate($account); + // $end = $repository->newestJournalDate($account); + // + // // same call, except "entries". + // return view('accounts.show', compact('account', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); + // } + // + // /** + // * @param Request $request + // * @param Account $account + // * @param string $date + // * + // * @return View + // */ + // public function showByDate(Request $request, Account $account, string $date) + // { + // $carbon = new Carbon($date); + // $range = Preferences::get('viewRange', '1M')->data; + // $start = Navigation::startOfPeriod($carbon, $range); + // $end = Navigation::endOfPeriod($carbon, $range); + // $subTitle = $account->name . ' (' . Navigation::periodShow($start, $range) . ')'; + // $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); + // $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); + // $chartUri = route('chart.account.period', [$account->id, $carbon->format('Y-m-d')]); + // $accountType = $account->accountType->type; + // + // // replace with journal collector: + // /** @var JournalCollectorInterface $collector */ + // $collector = app(JournalCollectorInterface::class); + // $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); + // $journals = $collector->getPaginatedJournals(); + // $journals->setPath('accounts/show/' . $account->id . '/' . $date); + // + // // generate entries for each period (and cache those) + // $entries = $this->periodEntries($account); + // + // // same call, except "entries". + // return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); + // } + /** * @param AccountFormRequest $request * @param AccountRepositoryInterface $repository diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index 62d7311e0a..356188163f 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -76,30 +76,26 @@ Breadcrumbs::register( ); Breadcrumbs::register( - 'accounts.show.date', function (BreadCrumbGenerator $breadcrumbs, Account $account, Carbon $start, Carbon $end) { + 'accounts.show.date', function (BreadCrumbGenerator $breadcrumbs, Account $account, Carbon $start = null, Carbon $end = null) { - $startString = $start->formatLocalized(strval(trans('config.month_and_day'))); - $endString = $end->formatLocalized(strval(trans('config.month_and_day'))); - $title = sprintf('%s (%s)', $account->name, trans('firefly.from_to', ['start' => $startString, 'end' => $endString])); + $title = ''; + $route = ''; + if (!is_null($start) && !is_null($end)) { + $startString = $start->formatLocalized(strval(trans('config.month_and_day'))); + $endString = $end->formatLocalized(strval(trans('config.month_and_day'))); + $title = sprintf('%s (%s)', $account->name, trans('firefly.from_to_breadcrumb', ['start' => $startString, 'end' => $endString])); + $route = route('accounts.show.date', [$account->id, $start->format('Y-m-d')]); + } + if (is_null($start) && is_null($end)) { + $title = $title = $account->name . ' (' . strtolower(strval(trans('firefly.everything'))) . ')'; + $route = route('accounts.show.date', [$account->id, 'all']); + } $breadcrumbs->parent('accounts.show', $account); - $breadcrumbs->push($title, route('accounts.show.date', [$account->id, $start->format('Y-m-d')])); + $breadcrumbs->push($title, $route); } ); -Breadcrumbs::register( - 'accounts.show.all', function (BreadCrumbGenerator $breadcrumbs, Account $account, Carbon $start, Carbon $end) { - - $startString = $start->formatLocalized(strval(trans('config.month_and_day'))); - $endString = $end->formatLocalized(strval(trans('config.month_and_day'))); - $title = sprintf('%s (%s)', $account->name, trans('firefly.from_to', ['start' => $startString, 'end' => $endString])); - - $breadcrumbs->parent('accounts.show', $account); - $breadcrumbs->push($title, route('accounts.show.all', [$account->id, $start->format('Y-m-d')])); -} -); - - Breadcrumbs::register( 'accounts.delete', function (BreadCrumbGenerator $breadcrumbs, Account $account) { $breadcrumbs->parent('accounts.show', $account); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index f5240948f6..5406586eb2 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -558,6 +558,7 @@ return [ 'select_more_than_one_budget' => 'Please select more than one budget', 'select_more_than_one_tag' => 'Please select more than one tag', 'from_to' => 'From :start to :end', + 'from_to_breadcrumb' => 'from :start to :end', // categories: 'new_category' => 'New category', diff --git a/resources/views/accounts/show.twig b/resources/views/accounts/show.twig index 4d9d3444a8..395e71767b 100644 --- a/resources/views/accounts/show.twig +++ b/resources/views/accounts/show.twig @@ -10,8 +10,12 @@

{{ account.name }} - ({{ trans('firefly.from_to', {start: start.formatLocalized(monthAndDayFormat), end: end.formatLocalized(monthAndDayFormat)}) }})

- + {% if start and end %} + ({{ trans('firefly.from_to_breadcrumb', {start: start.formatLocalized(monthAndDayFormat), end: end.formatLocalized(monthAndDayFormat)}) }}) + {% else %} + ({{ trans('firefly.everything')|lower }}) + {% endif %} +
@@ -67,26 +71,26 @@
-{% if entries %} -
-
-

{{ 'showEverything'|_ }}

+ {% if periods.count > 0 %} + -
-{% endif %} + {% endif %}
-
+

{{ 'transactions'|_ }}

{% include 'list.journals-tasker' with {sorting:true, hideBills:true, hideBudgets: true, hideCategories: true} %} - {% if entries %} + {% if periods.count > 0 %}

- + {{ 'show_all_no_filter'|_ }}

@@ -101,10 +105,9 @@
- {% if entries %} + {% if periods.count > 0 %}
- - {% for entry in entries %} + {% for entry in periods %} {% if (entry[2] != 0 or entry[3] != 0) or (accountType == 'Asset account') %}
@@ -130,7 +133,7 @@
{% endif %} {% endfor %} -

{{ 'showEverything'|_ }}

+

{{ 'showEverything'|_ }}

{% endif %}
@@ -144,9 +147,15 @@ var accountID = {{ account.id }}; // uri's for charts: var chartUri = '{{ chartUri }}'; - var incomeCategoryUri = '{{ route('chart.account.income-category', [account.id, start.format('Ymd'), end.format('Ymd')]) }}'; - var expenseCategoryUri = '{{ route('chart.account.expense-category', [account.id, start.format('Ymd'), end.format('Ymd')]) }}'; - var expenseBudgetUri = '{{ route('chart.account.expense-budget', [account.id, start.format('Ymd'), end.format('Ymd')]) }}'; + {% if start and end %} + var incomeCategoryUri = '{{ route('chart.account.income-category', [account.id, start.format('Ymd'), end.format('Ymd')]) }}'; + var expenseCategoryUri = '{{ route('chart.account.expense-category', [account.id, start.format('Ymd'), end.format('Ymd')]) }}'; + var expenseBudgetUri = '{{ route('chart.account.expense-budget', [account.id, start.format('Ymd'), end.format('Ymd')]) }}'; + {% else %} + var incomeCategoryUri = '{{ route('chart.account.income-category', [account.id, 'all', 'all']) }}'; + var expenseCategoryUri = '{{ route('chart.account.expense-category', [account.id, 'all', 'all']) }}'; + var expenseBudgetUri = '{{ route('chart.account.expense-budget', [account.id, 'all', 'all']) }}'; + {% endif %} diff --git a/routes/web.php b/routes/web.php index 5a70819fd4..cf64bc32e9 100755 --- a/routes/web.php +++ b/routes/web.php @@ -88,8 +88,7 @@ Route::group( Route::get('delete/{account}', ['uses' => 'AccountController@delete', 'as' => 'delete']); Route::get('show/{account}', ['uses' => 'AccountController@show', 'as' => 'show']); - Route::get('show/{account}/all', ['uses' => 'AccountController@showAll', 'as' => 'show.all']); - Route::get('show/{account}/{date}', ['uses' => 'AccountController@showByDate', 'as' => 'show.date']); + Route::get('show/{account}/{date}', ['uses' => 'AccountController@show', 'as' => 'show.date']); Route::post('store', ['uses' => 'AccountController@store', 'as' => 'store']); Route::post('update/{account}', ['uses' => 'AccountController@update', 'as' => 'update']); From 2e637031acf8c9866d1c530dba2e8e96e9ad5ff4 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 13:19:42 +0100 Subject: [PATCH 049/244] Fix charts for #595, account overview. --- app/Http/Controllers/AccountController.php | 97 ------------------- .../Controllers/Chart/AccountController.php | 77 +++++++++++---- routes/web.php | 4 +- 3 files changed, 61 insertions(+), 117 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 0327b6bfb8..e18c44e918 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -295,103 +295,6 @@ class AccountController extends Controller return view('accounts.show', compact('account', 'accountType', 'periods', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); } - // /** - // * @param Request $request - // * @param JournalCollectorInterface $collector - // * @param Account $account - // * - // * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View - // */ - // public function show(Request $request, JournalCollectorInterface $collector, Account $account) - // { - // if ($account->accountType->type === AccountType::INITIAL_BALANCE) { - // return $this->redirectToOriginalAccount($account); - // } - // // show journals from current period only: - // $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); - // $subTitle = $account->name; - // $range = Preferences::get('viewRange', '1M')->data; - // $start = session('start', Navigation::startOfPeriod(new Carbon, $range)); - // $end = session('end', Navigation::endOfPeriod(new Carbon, $range)); - // $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); - // $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - // $chartUri = route('chart.account.single', [$account->id]); - // $accountType = $account->accountType->type; - // - // // grab those journals: - // $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); - // $journals = $collector->getPaginatedJournals(); - // $journals->setPath('accounts/show/' . $account->id); - // - // // generate entries for each period (and cache those) - // $entries = $this->periodEntries($account); - // - // return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); - // } - // - // /** - // * @param Request $request - // * @param AccountRepositoryInterface $repository - // * @param Account $account - // * - // * @return View - // */ - // public function showAll(Request $request, AccountRepositoryInterface $repository, Account $account) - // { - // $subTitle = sprintf('%s (%s)', $account->name, strtolower(trans('firefly.everything'))); - // $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); - // $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - // $chartUri = route('chart.account.all', [$account->id]); - // - // // replace with journal collector: - // /** @var JournalCollectorInterface $collector */ - // $collector = app(JournalCollectorInterface::class); - // $collector->setUser(auth()->user()); - // $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); - // $journals = $collector->getPaginatedJournals(); - // $journals->setPath('accounts/show/' . $account->id . '/all'); - // - // // get oldest and newest journal for account: - // $start = $repository->oldestJournalDate($account); - // $end = $repository->newestJournalDate($account); - // - // // same call, except "entries". - // return view('accounts.show', compact('account', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); - // } - // - // /** - // * @param Request $request - // * @param Account $account - // * @param string $date - // * - // * @return View - // */ - // public function showByDate(Request $request, Account $account, string $date) - // { - // $carbon = new Carbon($date); - // $range = Preferences::get('viewRange', '1M')->data; - // $start = Navigation::startOfPeriod($carbon, $range); - // $end = Navigation::endOfPeriod($carbon, $range); - // $subTitle = $account->name . ' (' . Navigation::periodShow($start, $range) . ')'; - // $page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page')); - // $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - // $chartUri = route('chart.account.period', [$account->id, $carbon->format('Y-m-d')]); - // $accountType = $account->accountType->type; - // - // // replace with journal collector: - // /** @var JournalCollectorInterface $collector */ - // $collector = app(JournalCollectorInterface::class); - // $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page); - // $journals = $collector->getPaginatedJournals(); - // $journals->setPath('accounts/show/' . $account->id . '/' . $date); - // - // // generate entries for each period (and cache those) - // $entries = $this->periodEntries($account); - // - // // same call, except "entries". - // return view('accounts.show', compact('account', 'accountType', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')); - // } - /** * @param AccountFormRequest $request * @param AccountRepositoryInterface $repository diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index 5391d06883..efd1c1aaa6 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -139,14 +139,13 @@ class AccountController extends Controller } /** - * @param JournalCollectorInterface $collector - * @param Account $account - * @param Carbon $start - * @param Carbon $end + * @param Account $account + * @param Carbon $start + * @param Carbon $end * * @return \Illuminate\Http\JsonResponse */ - public function expenseBudget(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end) + public function expenseBudget(Account $account, Carbon $start, Carbon $end) { $cache = new CacheProperties; $cache->addProperty($account->id); @@ -156,10 +155,8 @@ class AccountController extends Controller if ($cache->has()) { return Response::json($cache->get()); } - $collector->setAccounts(new Collection([$account])) - ->setRange($start, $end) - ->withBudgetInformation() - ->setTypes([TransactionType::WITHDRAWAL]); + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withBudgetInformation()->setTypes([TransactionType::WITHDRAWAL]); $transactions = $collector->getJournals(); $chartData = []; $result = []; @@ -185,14 +182,27 @@ class AccountController extends Controller } /** - * @param JournalCollectorInterface $collector - * @param Account $account - * @param Carbon $start - * @param Carbon $end + * @param AccountRepositoryInterface $repository + * @param Account $account * * @return \Illuminate\Http\JsonResponse */ - public function expenseCategory(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end) + public function expenseBudgetAll(AccountRepositoryInterface $repository, Account $account) + { + $start = $repository->oldestJournalDate($account); + $end = Carbon::now(); + + return $this->expenseBudget($account, $start, $end); + } + + /** + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function expenseCategory(Account $account, Carbon $start, Carbon $end) { $cache = new CacheProperties; $cache->addProperty($account->id); @@ -203,6 +213,7 @@ class AccountController extends Controller return Response::json($cache->get()); } + $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withCategoryInformation()->setTypes([TransactionType::WITHDRAWAL]); $transactions = $collector->getJournals(); $result = []; @@ -228,6 +239,20 @@ class AccountController extends Controller } + /** + * @param AccountRepositoryInterface $repository + * @param Account $account + * + * @return \Illuminate\Http\JsonResponse + */ + public function expenseCategoryAll(AccountRepositoryInterface $repository, Account $account) + { + $start = $repository->oldestJournalDate($account); + $end = Carbon::now(); + + return $this->expenseCategory($account, $start, $end); + } + /** * Shows the balances for all the user's frontpage accounts. * @@ -254,14 +279,13 @@ class AccountController extends Controller } /** - * @param JournalCollectorInterface $collector - * @param Account $account - * @param Carbon $start - * @param Carbon $end + * @param Account $account + * @param Carbon $start + * @param Carbon $end * * @return \Illuminate\Http\JsonResponse */ - public function incomeCategory(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end) + public function incomeCategory(Account $account, Carbon $start, Carbon $end) { $cache = new CacheProperties; $cache->addProperty($account->id); @@ -273,6 +297,7 @@ class AccountController extends Controller } // grab all journals: + $collector = app(JournalCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withCategoryInformation()->setTypes([TransactionType::DEPOSIT]); $transactions = $collector->getJournals(); $result = []; @@ -297,6 +322,20 @@ class AccountController extends Controller } + /** + * @param AccountRepositoryInterface $repository + * @param Account $account + * + * @return \Illuminate\Http\JsonResponse + */ + public function incomeCategoryAll(AccountRepositoryInterface $repository, Account $account) + { + $start = $repository->oldestJournalDate($account); + $end = Carbon::now(); + + return $this->incomeCategory($account, $start, $end); + } + /** * @param Account $account * @param string $date diff --git a/routes/web.php b/routes/web.php index cf64bc32e9..1abb383b16 100755 --- a/routes/web.php +++ b/routes/web.php @@ -221,11 +221,13 @@ Route::group( Route::get('single/{account}', ['uses' => 'AccountController@single', 'as' => 'single']); Route::get('period/{account}/{date}', ['uses' => 'AccountController@period', 'as' => 'period']); + Route::get('income-category/{account}/all/all', ['uses' => 'AccountController@incomeCategoryAll', 'as' => 'income-category-all']); + Route::get('expense-category/{account}/all/all', ['uses' => 'AccountController@expenseCategoryAll', 'as' => 'expense-category-all']); + Route::get('expense-budget/{account}/all/all', ['uses' => 'AccountController@expenseBudgetAll', 'as' => 'expense-budget-all']); Route::get('income-category/{account}/{start_date}/{end_date}', ['uses' => 'AccountController@incomeCategory', 'as' => 'income-category']); Route::get('expense-category/{account}/{start_date}/{end_date}', ['uses' => 'AccountController@expenseCategory', 'as' => 'expense-category']); Route::get('expense-budget/{account}/{start_date}/{end_date}', ['uses' => 'AccountController@expenseBudget', 'as' => 'expense-budget']); - } ); From 79b0c20adbca12264a467b008105d99f22cc60fb Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 13:22:06 +0100 Subject: [PATCH 050/244] Forgot about the date for account lists, #595 [skip ci] --- app/Http/Controllers/AccountController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index e18c44e918..9a82bcade7 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -258,7 +258,8 @@ class AccountController extends Controller if (strlen($moment) > 0 && $moment !== 'all') { $start = new Carbon($moment); $end = Navigation::endOfPeriod($start, $range); - $subTitle = $account->name . ' (' . strval(trans('firefly.from_to_breadcrumb', ['start' => 'x', 'end' => 'x'])) . ')'; + $subTitle = $account->name . ' (' . strval(trans('firefly.from_to_breadcrumb', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)])) . ')'; $chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d')]); $periods = $this->periodEntries($account); } From 8decf8ab9fe8af240227a66ceb7dee2acaaa2745 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 13:26:01 +0100 Subject: [PATCH 051/244] Make PHP modules mandatory in composer file. --- composer.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/composer.json b/composer.json index a61e815c44..4fb50c948c 100755 --- a/composer.json +++ b/composer.json @@ -48,6 +48,10 @@ "require": { "php": ">=7.0.0", "ext-intl": "*", + "ext-bcmath": "*", + "ext-mbstring": "*", + "ext-curl": "*", + "ext-zip": "*", "laravel/framework": "5.4.*", "davejamesmiller/laravel-breadcrumbs": "3.*", "watson/validating": "3.*", From eed8fe22c6a88c8805f2e9f97d3e234fc3353859 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 25 Feb 2017 13:34:44 +0100 Subject: [PATCH 052/244] Make sure the loop is broken. #595 --- app/Http/Controllers/AccountController.php | 17 ++++++++++++----- .../Controllers/AccountControllerTest.php | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 9a82bcade7..96bf953590 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -258,8 +258,12 @@ class AccountController extends Controller if (strlen($moment) > 0 && $moment !== 'all') { $start = new Carbon($moment); $end = Navigation::endOfPeriod($start, $range); - $subTitle = $account->name . ' (' . strval(trans('firefly.from_to_breadcrumb', - ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)])) . ')'; + $subTitle = $account->name . ' (' . strval( + trans( + 'firefly.from_to_breadcrumb', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ) + ) . ')'; $chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d')]); $periods = $this->periodEntries($account); } @@ -273,10 +277,13 @@ class AccountController extends Controller $accountType = $account->accountType->type; $count = 0; + $loop = 0; // grab journals, but be prepared to jump a period back to get the right ones: - while ($count === 0) { + Log::info('Now at loop start.'); + while ($count === 0 && $loop < 3) { + $loop++; $collector = app(JournalCollectorInterface::class); - Log::debug('Count is zero, search for journals.'); + Log::info('Count is zero, search for journals.'); $collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page); if (!is_null($start)) { $collector->setRange($start, $end); @@ -288,7 +295,7 @@ class AccountController extends Controller $start->subDay(); $start = Navigation::startOfPeriod($start, $range); $end = Navigation::endOfPeriod($start, $range); - Log::debug(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d'))); + Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d'))); } } diff --git a/tests/Feature/Controllers/AccountControllerTest.php b/tests/Feature/Controllers/AccountControllerTest.php index 45a7b70733..908effbe73 100644 --- a/tests/Feature/Controllers/AccountControllerTest.php +++ b/tests/Feature/Controllers/AccountControllerTest.php @@ -175,7 +175,7 @@ class AccountControllerTest extends TestCase { $this->be($this->user()); $this->changeDateRange($this->user(), $range); - $response = $this->get(route('accounts.show.all', [1])); + $response = $this->get(route('accounts.show', [1, 'all'])); $response->assertStatus(200); // has bread crumb $response->assertSee('
From 3bf50403242c6f3841fcdd470910d3132c8722bd Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 2 Mar 2017 16:42:33 +0100 Subject: [PATCH 061/244] Fixed null pointer in debug message [skip ci] --- app/Repositories/Account/AccountTasker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index e5c81eb211..e5c4ab1bfb 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -127,12 +127,12 @@ class AccountTasker implements AccountTaskerInterface // get first journal date: $first = $repository->oldestJournal($account); - Log::debug(sprintf('Date of first journal for %s is %s', $account->name, $first->date->format('Y-m-d'))); $entry['start_balance'] = $startSet[$account->id] ?? '0'; $entry['end_balance'] = $endSet[$account->id] ?? '0'; // first journal exists, and is on start, then this is the actual opening balance: if (!is_null($first->id) && $first->date->isSameDay($start)) { + Log::debug(sprintf('Date of first journal for %s is %s', $account->name, $first->date->format('Y-m-d'))); $entry['start_balance'] = $first->transactions()->where('account_id', $account->id)->first()->amount; Log::debug(sprintf('Account %s was opened on %s, so opening balance is %f', $account->name, $start->format('Y-m-d'), $entry['start_balance'])); } From 7ef8ff60a5e162e106190b165fdc79cc30edef08 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 2 Mar 2017 19:41:17 +0100 Subject: [PATCH 062/244] Update config files. --- CHANGELOG.md | 24 +++++++++++ config/firefly.php | 7 +-- resources/views/auth/confirmation/error.twig | 43 ------------------- .../views/auth/confirmation/no-resent.twig | 43 ------------------- resources/views/auth/confirmation/resent.twig | 43 ------------------- 5 files changed, 25 insertions(+), 135 deletions(-) delete mode 100644 resources/views/auth/confirmation/error.twig delete mode 100644 resources/views/auth/confirmation/no-resent.twig delete mode 100644 resources/views/auth/confirmation/resent.twig diff --git a/CHANGELOG.md b/CHANGELOG.md index 81697a14cb..7758126fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.3.7] - 2017-03-05 +### Added +- Nice user friendly views for empty lists. +- Extended contribution guidelines. +- First version of financial report filtered on tags. +- Suggested monthly savings for piggy banks, by [Zsub](https://github.com/Zsub) + +### Changed +- Slightly changed tag overview. +- Consistent icon for bill in list. +- Slightly changed account overview. + +### Removed +- Removed IDE specific views from .gitignore, issue #598 + +### Fixed +- Force key generation during installation. +- The `date` function takes the fieldname where a date is stored, not the literal date by [Zsub](https://github.com/Zsub) +- Improved budget frontpage chart, as suggested by [skibbipl](https://github.com/skibbipl) +- Issue #602 and #607, as reported by [skibbipl](https://github.com/skibbipl) and [dzaikos](https://github.com/dzaikos). +- Issue #605, as reported by [Zsub](https://github.com/Zsub). +- Issue #599, as reported by [leander091](https://github.com/leander091). +- Various other bug fixes. + ## [4.3.6] - 2017-02-20 ### Fixed - #578, reported by [xpfgsyb](https://github.com/xpfgsyb). diff --git a/config/firefly.php b/config/firefly.php index 043b94c7c0..48edf214cb 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -23,15 +23,10 @@ return [ 'is_demo_site' => false, ], 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), - 'chart' => 'chartjs', - 'version' => '4.3.6', - 'csv_import_enabled' => true, + 'version' => '4.3.7', 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], - 'resend_confirmation' => 3600, - 'confirmation_age' => 14400, // four hours 'list_length' => 10, - 'export_formats' => [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], diff --git a/resources/views/auth/confirmation/error.twig b/resources/views/auth/confirmation/error.twig deleted file mode 100644 index a417e4db7b..0000000000 --- a/resources/views/auth/confirmation/error.twig +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Firefly III - - - - - - - - - {# favicons #} - {% include('partials.favicons') %} - - - -
- - -
-
-

{{ 'confirm_account_header'|_ }}

-
-
- -
-
-

- {{ 'confirm_account_intro'|_ }} -

-

- {{ 'confirm_account_resend_email'|_ }} -

-
-
-
- - diff --git a/resources/views/auth/confirmation/no-resent.twig b/resources/views/auth/confirmation/no-resent.twig deleted file mode 100644 index 64c24e95e0..0000000000 --- a/resources/views/auth/confirmation/no-resent.twig +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Firefly III - - - - - - - - - {# favicons #} - {% include('partials.favicons') %} - - - -
- - -
-
-

{{ 'confirm_account_not_resent_header'|_ }}

-
-
- -
-
-

- {{ trans('firefly.confirm_account_not_resent_intro', {owner:owner})|raw }} -

-

- {{ 'confirm_account_not_resent_go_home'|_ }} -

-
-
-
- - diff --git a/resources/views/auth/confirmation/resent.twig b/resources/views/auth/confirmation/resent.twig deleted file mode 100644 index accae395df..0000000000 --- a/resources/views/auth/confirmation/resent.twig +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Firefly III - - - - - - - - - {# favicons #} - {% include('partials.favicons') %} - - - -
- - -
-
-

{{ 'confirm_account_is_resent_header'|_ }}

-
-
- -
-
-

- {{ trans('firefly.confirm_account_is_resent_text', {owner:owner})|raw }} -

-

- {{ 'confirm_account_is_resent_go_home'|_ }} -

-
-
-
- - From 015064e5afb73585ad1bac85402627a8864caa3d Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 2 Mar 2017 19:45:04 +0100 Subject: [PATCH 063/244] Update composer.lock --- composer.lock | 97 +++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/composer.lock b/composer.lock index ad04b62708..8547984c0b 100644 --- a/composer.lock +++ b/composer.lock @@ -665,16 +665,16 @@ }, { "name": "laravel/framework", - "version": "v5.4.13", + "version": "v5.4.15", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "3eebfaa759156e06144892b00bb95304aa5a71c1" + "reference": "ecc6468b8af30b77566a8519ce8898740ef691d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/3eebfaa759156e06144892b00bb95304aa5a71c1", - "reference": "3eebfaa759156e06144892b00bb95304aa5a71c1", + "url": "https://api.github.com/repos/laravel/framework/zipball/ecc6468b8af30b77566a8519ce8898740ef691d7", + "reference": "ecc6468b8af30b77566a8519ce8898740ef691d7", "shasum": "" }, "require": { @@ -790,7 +790,7 @@ "framework", "laravel" ], - "time": "2017-02-22T16:07:04+00:00" + "time": "2017-03-02T14:41:40+00:00" }, { "name": "laravelcollective/html", @@ -1232,16 +1232,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.4", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e" + "reference": "b5ea1ef3d8ff10c307ba8c5945c2f134e503278f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e", - "reference": "a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/b5ea1ef3d8ff10c307ba8c5945c2f134e503278f", + "reference": "b5ea1ef3d8ff10c307ba8c5945c2f134e503278f", "shasum": "" }, "require": { @@ -1276,7 +1276,7 @@ "pseudorandom", "random" ], - "time": "2016-11-07T23:38:38+00:00" + "time": "2017-02-27T17:11:23+00:00" }, { "name": "pragmarx/google2fa", @@ -3059,16 +3059,16 @@ }, { "name": "mockery/mockery", - "version": "0.9.8", + "version": "0.9.9", "source": { "type": "git", "url": "https://github.com/padraic/mockery.git", - "reference": "1e5e2ffdc4d71d7358ed58a6fdd30a4a0c506855" + "reference": "6fdb61243844dc924071d3404bb23994ea0b6856" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/padraic/mockery/zipball/1e5e2ffdc4d71d7358ed58a6fdd30a4a0c506855", - "reference": "1e5e2ffdc4d71d7358ed58a6fdd30a4a0c506855", + "url": "https://api.github.com/repos/padraic/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856", + "reference": "6fdb61243844dc924071d3404bb23994ea0b6856", "shasum": "" }, "require": { @@ -3120,7 +3120,7 @@ "test double", "testing" ], - "time": "2017-02-09T13:29:38+00:00" + "time": "2017-02-28T12:52:32+00:00" }, { "name": "myclabs/deep-copy", @@ -3375,35 +3375,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "4.0.6", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca060f645beeddebedb1885c97bf163e93264c35" + "reference": "09e2277d14ea467e5a984010f501343ef29ffc69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca060f645beeddebedb1885c97bf163e93264c35", - "reference": "ca060f645beeddebedb1885c97bf163e93264c35", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/09e2277d14ea467e5a984010f501343ef29ffc69", + "reference": "09e2277d14ea467e5a984010f501343ef29ffc69", "shasum": "" }, "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/code-unit-reverse-lookup": "^1.0", "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "~1.0|~2.0" + "sebastian/version": "^1.0 || ^2.0" }, "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "^5.4" + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" }, "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.4.0", - "ext-xmlwriter": "*" + "ext-xdebug": "^2.5.1" }, "type": "library", "extra": { @@ -3434,7 +3434,7 @@ "testing", "xunit" ], - "time": "2017-02-23T07:38:02+00:00" + "time": "2017-03-01T09:12:17+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3526,25 +3526,30 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.8", + "version": "1.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4|~5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -3566,20 +3571,20 @@ "keywords": [ "timer" ], - "time": "2016-05-12T18:03:57+00:00" + "time": "2017-02-26T11:10:40+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.10", + "version": "1.4.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "284fb0679dd25fb5ffb56dad92c72860c0a22f1b" + "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/284fb0679dd25fb5ffb56dad92c72860c0a22f1b", - "reference": "284fb0679dd25fb5ffb56dad92c72860c0a22f1b", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", + "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", "shasum": "" }, "require": { @@ -3615,20 +3620,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-02-23T06:14:45+00:00" + "time": "2017-02-27T10:12:30+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.14", + "version": "5.7.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4906b8faf23e42612182fd212eb6f4c0f2954b57" + "reference": "b99112aecc01f62acf3d81a3f59646700a1849e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4906b8faf23e42612182fd212eb6f4c0f2954b57", - "reference": "4906b8faf23e42612182fd212eb6f4c0f2954b57", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b99112aecc01f62acf3d81a3f59646700a1849e5", + "reference": "b99112aecc01f62acf3d81a3f59646700a1849e5", "shasum": "" }, "require": { @@ -3697,7 +3702,7 @@ "testing", "xunit" ], - "time": "2017-02-19T07:22:16+00:00" + "time": "2017-03-02T15:22:43+00:00" }, { "name": "phpunit/phpunit-mock-objects", From b3b8981b4bececdb6cc7bcafcc55571fbbc71ba8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 2 Mar 2017 19:57:46 +0100 Subject: [PATCH 064/244] Catch null pointer exception. --- app/Support/Preferences.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 999b8847ef..ef7c5b6431 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -118,9 +118,13 @@ class Preferences */ public function lastActivity(): string { - $preference = $this->get('lastActivity', microtime())->data; + $lastActivity = microtime(); + $preference = $this->get('lastActivity', microtime()); + if (!is_null($preference)) { + $lastActivity = $preference->data; + } - return md5($preference); + return md5($lastActivity); } /** From 978e3e615c820cccb711dc0921c04b2377210538 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 3 Mar 2017 12:55:28 +0100 Subject: [PATCH 065/244] This prevented FF from displaying cash account properly. --- app/Support/Twig/Transaction.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Support/Twig/Transaction.php b/app/Support/Twig/Transaction.php index 70abfc30d4..648e4caabe 100644 --- a/app/Support/Twig/Transaction.php +++ b/app/Support/Twig/Transaction.php @@ -204,7 +204,7 @@ class Transaction extends Twig_Extension $name = $transaction->opposing_account_name; $id = intval($transaction->opposing_account_id); - $type = intval($transaction->opposing_account_type); + $type = $transaction->opposing_account_type; } // Find the opposing account and use that one: @@ -278,7 +278,7 @@ class Transaction extends Twig_Extension $name = $transaction->opposing_account_name; $id = intval($transaction->opposing_account_id); - $type = intval($transaction->opposing_account_type); + $type = $transaction->opposing_account_type; } // Find the opposing account and use that one: if (bccomp($transaction->transaction_amount, '0') === 1 && is_null($transaction->opposing_account_id)) { From 8fb6c1a0c86c5d93059473730f0efe0ca836b36d Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 3 Mar 2017 18:19:25 +0100 Subject: [PATCH 066/244] Various small changes. --- app/Http/Controllers/JavascriptController.php | 31 +++++++++++++++++ .../Transaction/SingleController.php | 25 ++++++++------ .../Transaction/SplitController.php | 6 ++-- public/js/ff/firefly.js | 2 +- public/js/ff/transactions/single/create.js | 33 +++++++++++++------ resources/lang/en_US/firefly.php | 1 + resources/views/accounts/create.twig | 2 +- resources/views/index.twig | 4 +-- resources/views/javascript/accounts.twig | 4 +++ routes/web.php | 1 + 10 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 resources/views/javascript/accounts.twig diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php index 042fd32217..c86a3feb55 100644 --- a/app/Http/Controllers/JavascriptController.php +++ b/app/Http/Controllers/JavascriptController.php @@ -13,6 +13,10 @@ namespace FireflyIII\Http\Controllers; use Amount; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use Illuminate\Http\Request; use Navigation; use Preferences; @@ -26,6 +30,33 @@ use Session; class JavascriptController extends Controller { + /** + * + */ + public function accounts(AccountRepositoryInterface $repository, CurrencyRepositoryInterface $currencyRepository) + { + $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $preference = Preferences::get('currencyPreference', config('firefly.default_currency', 'EUR')); + $default = $currencyRepository->findByCode($preference->data); + + $data = ['accounts' => [],]; + + + /** @var Account $account */ + foreach ($accounts as $account) { + $accountId = $account->id; + $currency = intval($account->getMeta('currency_id')); + $currency = $currency === 0 ? $default->id : $currency; + $entry = ['preferredCurrency' => $currency]; + $data['accounts'][$accountId] = $entry; + } + + + return response() + ->view('javascript.accounts', $data, 200) + ->header('Content-Type', 'text/javascript'); + } + /** * @param Request $request * diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index 830c32eb78..d895f776f1 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -162,9 +162,12 @@ class SingleController extends Controller */ public function delete(TransactionJournal $journal) { + // Covered by another controller's tests + // @codeCoverageIgnoreStart if ($this->isOpeningBalance($journal)) { return $this->redirectToAccount($journal); } + // @codeCoverageIgnoreEnd $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); $subTitle = trans('firefly.delete_' . $what, ['description' => $journal->description]); @@ -187,9 +190,11 @@ class SingleController extends Controller */ public function destroy(JournalRepositoryInterface $repository, TransactionJournal $transactionJournal) { + // @codeCoverageIgnoreStart if ($this->isOpeningBalance($transactionJournal)) { return $this->redirectToAccount($transactionJournal); } + // @codeCoverageIgnoreEnd $type = TransactionJournal::transactionTypeStr($transactionJournal); Session::flash('success', strval(trans('firefly.deleted_' . strtolower($type), ['description' => e($transactionJournal->description)]))); @@ -207,9 +212,11 @@ class SingleController extends Controller */ public function edit(TransactionJournal $journal) { + // @codeCoverageIgnoreStart if ($this->isOpeningBalance($journal)) { return $this->redirectToAccount($journal); } + // @codeCoverageIgnoreEnd $count = $journal->transactions()->count(); @@ -313,22 +320,20 @@ class SingleController extends Controller Session::flash('success', strval(trans('firefly.stored_journal', ['description' => e($journal->description)]))); Preferences::mark(); + // @codeCoverageIgnoreStart if ($createAnother === true) { - // set value so create routine will not overwrite URL: Session::put('transactions.create.fromStore', true); return redirect(route('transactions.create', [$request->input('what')]))->withInput(); } if ($doSplit === true) { - // redirect to edit screen: return redirect(route('transactions.split.edit', [$journal->id])); } + // @codeCoverageIgnoreEnd - // redirect to previous URL. return redirect($this->getPreviousUri('transactions.create.uri')); - } /** @@ -340,9 +345,11 @@ class SingleController extends Controller */ public function update(JournalFormRequest $request, JournalRepositoryInterface $repository, TransactionJournal $journal) { + // @codeCoverageIgnoreStart if ($this->isOpeningBalance($journal)) { return $this->redirectToAccount($journal); } + // @codeCoverageIgnoreEnd $data = $request->getJournalData(); $journal = $repository->update($journal, $data); @@ -350,14 +357,14 @@ class SingleController extends Controller $files = $request->hasFile('attachments') ? $request->file('attachments') : null; $this->attachments->saveAttachmentsForModel($journal, $files); - // flash errors + // @codeCoverageIgnoreStart if (count($this->attachments->getErrors()->get('attachments')) > 0) { Session::flash('error', $this->attachments->getErrors()->get('attachments')); } - // flash messages if (count($this->attachments->getMessages()->get('attachments')) > 0) { Session::flash('info', $this->attachments->getMessages()->get('attachments')); } + // @codeCoverageIgnoreEnd event(new UpdatedTransactionJournal($journal)); // update, get events by date and sort DESC @@ -366,15 +373,13 @@ class SingleController extends Controller Session::flash('success', strval(trans('firefly.updated_' . $type, ['description' => e($data['description'])]))); Preferences::mark(); - // if wishes to split: - - + // @codeCoverageIgnoreStart if (intval($request->get('return_to_edit')) === 1) { - // set value so edit routine will not overwrite URL: Session::put('transactions.edit.fromUpdate', true); return redirect(route('transactions.edit', [$journal->id]))->withInput(['return_to_edit' => 1]); } + // @codeCoverageIgnoreEnd // redirect to previous URL. return redirect($this->getPreviousUri('transactions.edit.uri')); diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index e667cb5f1b..b1c75d150a 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -141,25 +141,27 @@ class SplitController extends Controller $files = $request->hasFile('attachments') ? $request->file('attachments') : null; // save attachments: $this->attachments->saveAttachmentsForModel($journal, $files); - event(new UpdatedTransactionJournal($journal)); - // update, get events by date and sort DESC // flash messages + // @codeCoverageIgnoreStart if (count($this->attachments->getMessages()->get('attachments')) > 0) { Session::flash('info', $this->attachments->getMessages()->get('attachments')); } + // @codeCoverageIgnoreEnd $type = strtolower(TransactionJournal::transactionTypeStr($journal)); Session::flash('success', strval(trans('firefly.updated_' . $type, ['description' => e($data['journal_description'])]))); Preferences::mark(); + // @codeCoverageIgnoreStart if (intval($request->get('return_to_edit')) === 1) { // set value so edit routine will not overwrite URL: Session::put('transactions.edit-split.fromUpdate', true); return redirect(route('transactions.split.edit', [$journal->id]))->withInput(['return_to_edit' => 1]); } + // @codeCoverageIgnoreEnd // redirect to previous URL. return redirect($this->getPreviousUri('transactions.edit-split.uri')); diff --git a/public/js/ff/firefly.js b/public/js/ff/firefly.js index 4f33376896..4c11b51065 100644 --- a/public/js/ff/firefly.js +++ b/public/js/ff/firefly.js @@ -102,7 +102,7 @@ function currencySelect(e) { $('#' + spanId).text(symbol); // close the menu (hack hack) - $('#' + menuID).click(); + $('#' + menuID).dropdown('toggle'); return false; diff --git a/public/js/ff/transactions/single/create.js b/public/js/ff/transactions/single/create.js index 7cf2796417..203962b24c 100644 --- a/public/js/ff/transactions/single/create.js +++ b/public/js/ff/transactions/single/create.js @@ -6,19 +6,16 @@ * See the LICENSE file for details. */ -/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, doSwitch, middleCrumbUrl */ +/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, middleCrumbUrl */ $(document).ready(function () { "use strict"; - // respond to switch buttons when - // creating stuff: - if (doSwitch == true) { - updateButtons(); - updateForm(); - updateLayout(); - updateDescription(); - } + // respond to switch buttons + updateButtons(); + updateForm(); + updateLayout(); + updateDescription(); if (!Modernizr.inputtypes.date) { $('input[type="date"]').datepicker( @@ -28,11 +25,27 @@ $(document).ready(function () { ); } + // update currency + $('select[name="source_account_id"]').on('change', updateCurrency) + // get JSON things: getJSONautocomplete(); - }); + +function updateCurrency() { + // get value: + var accountId = $('select[name="source_account_id"]').val(); + console.log('account id is ' + accountId); + var currencyPreference = accountInfo[accountId].preferredCurrency; + console.log('currency pref is ' + currencyPreference); + + $('.currency-option[data-id="' + currencyPreference + '"]').click(); + $('[data-toggle="dropdown"]').parent().removeClass('open'); + $('select[name="source_account_id"]').focus(); + +} + function updateDescription() { $.getJSON('json/transaction-journals/' + what).done(function (data) { $('input[name="description"]').typeahead('destroy').typeahead({source: data}); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 5406586eb2..70abb28daf 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -559,6 +559,7 @@ return [ 'select_more_than_one_tag' => 'Please select more than one tag', 'from_to' => 'From :start to :end', 'from_to_breadcrumb' => 'from :start to :end', + 'account_default_currency' => 'If you select another currency, new transactions from this account will have this currency pre-selected.', // categories: 'new_category' => 'New category', diff --git a/resources/views/accounts/create.twig b/resources/views/accounts/create.twig index 2063b86e79..28f2407b81 100644 --- a/resources/views/accounts/create.twig +++ b/resources/views/accounts/create.twig @@ -19,7 +19,7 @@ {{ ExpandedForm.text('name') }} {% if what == 'asset' %} {# Not really mandatory but OK #} - {{ ExpandedForm.select('currency_id', currencies) }} + {{ ExpandedForm.select('currency_id', currencies, null, {helpText:'account_default_currency'|_}) }} {% endif %}
diff --git a/resources/views/index.twig b/resources/views/index.twig index 07450afebb..2a62729913 100644 --- a/resources/views/index.twig +++ b/resources/views/index.twig @@ -12,7 +12,7 @@
- + {# ACCOUNTS #}

{{ 'yourAccounts'|_ }}

@@ -22,7 +22,7 @@
- + {# BUDGETS #}

{{ 'budgetsAndSpending'|_ }}

diff --git a/resources/views/javascript/accounts.twig b/resources/views/javascript/accounts.twig new file mode 100644 index 0000000000..61fb09f610 --- /dev/null +++ b/resources/views/javascript/accounts.twig @@ -0,0 +1,4 @@ +var accountInfo = []; +{% for id, account in accounts %} + accountInfo[{{ id }}] = {preferredCurrency: {{ account.preferredCurrency}}}; +{% endfor %} diff --git a/routes/web.php b/routes/web.php index 1abb383b16..201351db87 100755 --- a/routes/web.php +++ b/routes/web.php @@ -414,6 +414,7 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'javascript', 'as' => 'javascript.'], function () { Route::get('variables', ['uses' => 'JavascriptController@variables', 'as' => 'variables']); + Route::get('accounts', ['uses' => 'JavascriptController@accounts', 'as' => 'accounts']); } ); From 8101f910f0a9aee1934b9c799e30280774ae1700 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 4 Mar 2017 06:53:46 +0100 Subject: [PATCH 067/244] Update tests. --- .../Controllers/AccountControllerTest.php | 4 +- .../Transaction/SingleControllerTest.php | 188 +++++++++++++++++- .../Transaction/SplitControllerTest.php | 49 ++++- 3 files changed, 231 insertions(+), 10 deletions(-) diff --git a/tests/Feature/Controllers/AccountControllerTest.php b/tests/Feature/Controllers/AccountControllerTest.php index 908effbe73..f8a79adfed 100644 --- a/tests/Feature/Controllers/AccountControllerTest.php +++ b/tests/Feature/Controllers/AccountControllerTest.php @@ -166,7 +166,7 @@ class AccountControllerTest extends TestCase } /** - * @covers \FireflyIII\Http\Controllers\AccountController::showAll + * @covers \FireflyIII\Http\Controllers\AccountController::show * @dataProvider dateRangeProvider * * @param string $range @@ -182,7 +182,7 @@ class AccountControllerTest extends TestCase } /** - * @covers \FireflyIII\Http\Controllers\AccountController::showByDate + * @covers \FireflyIII\Http\Controllers\AccountController::show * @dataProvider dateRangeProvider * * @param string $range diff --git a/tests/Feature/Controllers/Transaction/SingleControllerTest.php b/tests/Feature/Controllers/Transaction/SingleControllerTest.php index 29d2b5681f..4aa6c17a06 100644 --- a/tests/Feature/Controllers/Transaction/SingleControllerTest.php +++ b/tests/Feature/Controllers/Transaction/SingleControllerTest.php @@ -12,8 +12,20 @@ declare(strict_types = 1); namespace Tests\Feature\Controllers\Transaction; +use DB; +use FireflyIII\Events\StoredTransactionJournal; +use FireflyIII\Events\UpdatedTransactionJournal; +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; +use Illuminate\Support\Collection; +use Illuminate\Support\MessageBag; use Tests\TestCase; /** @@ -24,12 +36,31 @@ use Tests\TestCase; class SingleControllerTest extends TestCase { + /** + * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::cloneTransaction + */ + public function testCloneTransaction() + { + $this->be($this->user()); + $withdrawal = TransactionJournal::where('transaction_type_id', 1)->whereNull('deleted_at')->where('user_id', $this->user()->id)->first(); + $response = $this->get(route('transactions.clone', [$withdrawal->id])); + $response->assertStatus(302); + } + /** * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::create * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::__construct */ public function testCreate() { + $repository = $this->mock(AccountRepositoryInterface::class); + $repository->shouldReceive('getActiveAccountsByType')->once()->withArgs([[AccountType::DEFAULT, AccountType::ASSET]])->andReturn(new Collection); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $budgetRepos->shouldReceive('getActiveBudgets')->andReturn(new Collection)->once(); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $piggyRepos->shouldReceive('getPiggyBanksWithAmount')->andReturn(new Collection)->once(); + + $this->be($this->user()); $response = $this->get(route('transactions.create', ['withdrawal'])); $response->assertStatus(200); @@ -43,7 +74,8 @@ class SingleControllerTest extends TestCase public function testDelete() { $this->be($this->user()); - $response = $this->get(route('transactions.delete', [12])); + $withdrawal = TransactionJournal::where('transaction_type_id', 1)->whereNull('deleted_at')->where('user_id', $this->user()->id)->first(); + $response = $this->get(route('transactions.delete', [$withdrawal->id])); $response->assertStatus(200); // has bread crumb $response->assertSee('
- + {# BUDGET ONLY WHEN CREATING A WITHDRAWAL #} {% if budgets|length > 1 %} {{ ExpandedForm.select('budget_id', budgets, null) }} {% else %} {{ ExpandedForm.select('budget_id', budgets, null, {helpText: trans('firefly.no_budget_pointer')}) }} {% endif %} - + {# CATEGORY ALWAYS #} {{ ExpandedForm.text('category') }} - + {# TAGS #} {{ ExpandedForm.text('tags') }} - + {# RELATE THIS TRANSFER TO A PIGGY BANK #} {{ ExpandedForm.select('piggy_bank_id', piggies, '0') }}
- + {# explain if necessary #} {% if not optionalFields.interest_date or not optionalFields.book_date or @@ -97,7 +97,7 @@ {{ trans('firefly.hidden_fields_preferences', {link: route('preferences.index')})|raw }}

{% endif %} - + {# box for dates #} {% if optionalFields.interest_date or optionalFields.book_date or optionalFields.process_date or optionalFields.due_date or optionalFields.payment_date @@ -141,7 +141,7 @@
{% endif %} - + {# box for business fields #} {% if optionalFields.internal_reference or optionalFields.notes %}
@@ -163,7 +163,7 @@
{% endif %} - + {# box for attachments #} {% if optionalFields.attachments %}
@@ -178,7 +178,7 @@
{% endif %} - + {# panel for options #}

{{ 'options'|_ }}

@@ -201,7 +201,6 @@ + {% endblock %} From 638aab4eea652b75ca93377370ad93253b2e77f6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 6 Mar 2017 10:16:52 +0100 Subject: [PATCH 086/244] Update TagRepository.php --- app/Repositories/Tag/TagRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index cf022037a7..924f0cf77a 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -387,8 +387,8 @@ class TagRepository implements TagRepositoryInterface Log::debug(sprintf('Now existingcomparing new journal #%d to existing journal #%d', $journal->id, $existing->id)); // $checkAccount is the source_account for a withdrawal // $checkAccount is the destination_account for a deposit - $existingSources = join(',', array_unique($journal->sourceAccountList()->pluck('id')->toArray())); - $existingDestinations = join(',', array_unique($journal->destinationAccountList()->pluck('id')->toArray())); + $existingSources = join(',', array_unique($existing->sourceAccountList()->pluck('id')->toArray())); + $existingDestinations = join(',', array_unique($existing->destinationAccountList()->pluck('id')->toArray())); if ($existing->isWithdrawal() && $existingSources !== $journalDestinations) { /* From 7a1c14b7668fccc629dbf787b64a0636e619d935 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 6 Mar 2017 21:04:14 +0100 Subject: [PATCH 087/244] Update for new release. --- CHANGELOG.md | 3 +- composer.lock | 40 +++++++++++++-------------- storage/database/databasecopy.sqlite | Bin 240640 -> 240640 bytes 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33fdadf051..ebcea8bf76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## [4.3.7] - 2017-03-05 +## [4.3.7] - 2017-03-06 ### Added - Nice user friendly views for empty lists. - Extended contribution guidelines. @@ -29,6 +29,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Issue #610, as reported by [skibbipl](https://github.com/skibbipl). - Issue #611, as reported by [ragnarkarlsson](https://github.com/ragnarkarlsson). - Issue #612, as reported by [ragnarkarlsson](https://github.com/ragnarkarlsson). +- Issue #614, as reported by [worldworm](https://github.com/worldworm). - Various other bug fixes. ## [4.3.6] - 2017-02-20 diff --git a/composer.lock b/composer.lock index 8547984c0b..48f198f1ff 100644 --- a/composer.lock +++ b/composer.lock @@ -1232,16 +1232,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.7", + "version": "v2.0.9", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "b5ea1ef3d8ff10c307ba8c5945c2f134e503278f" + "reference": "6968531206671f94377b01dc7888d5d1b858a01b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/b5ea1ef3d8ff10c307ba8c5945c2f134e503278f", - "reference": "b5ea1ef3d8ff10c307ba8c5945c2f134e503278f", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/6968531206671f94377b01dc7888d5d1b858a01b", + "reference": "6968531206671f94377b01dc7888d5d1b858a01b", "shasum": "" }, "require": { @@ -1276,7 +1276,7 @@ "pseudorandom", "random" ], - "time": "2017-02-27T17:11:23+00:00" + "time": "2017-03-03T20:43:42+00:00" }, { "name": "pragmarx/google2fa", @@ -3312,27 +3312,27 @@ }, { "name": "phpspec/prophecy", - "version": "v1.6.2", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" + "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", - "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", + "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1", - "sebastian/recursion-context": "^1.0|^2.0" + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { - "phpspec/phpspec": "^2.0", + "phpspec/phpspec": "^2.5|^3.2", "phpunit/phpunit": "^4.8 || ^5.6.5" }, "type": "library", @@ -3371,7 +3371,7 @@ "spy", "stub" ], - "time": "2016-11-21T14:58:47+00:00" + "time": "2017-03-02T20:05:34+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3765,23 +3765,23 @@ }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", "shasum": "" }, "require": { - "php": ">=5.6" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "^5.7 || ^6.0" }, "type": "library", "extra": { @@ -3806,7 +3806,7 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2016-02-13T06:45:14+00:00" + "time": "2017-03-04T06:30:41+00:00" }, { "name": "sebastian/comparator", diff --git a/storage/database/databasecopy.sqlite b/storage/database/databasecopy.sqlite index 50428f96233c25583b07fbeb3beaf8f3f4b9d68c..1feae77f2b92a5dfd32cdcfe5ec9716a53d8e507 100755 GIT binary patch literal 240640 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 literal 240640 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 Date: Wed, 8 Mar 2017 20:37:40 +0100 Subject: [PATCH 088/244] Remove space. [skip ci] --- resources/lang/en_US/firefly.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 024a0e22ac..b6aceb646d 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -854,7 +854,7 @@ return [ 'tag_title_nothing' => 'Default tags', 'tag_title_balancingAct' => 'Balancing act tags', 'tag_title_advancePayment' => 'Advance payment tags', - 'tags_introduction' => 'Usually tags are singular words, designed to quickly band items together using things like expensive, bill or for-party. In Firefly III, tags can have more properties such as a date, description and location. This allows you to join transactions together in a more meaningful way. For example, you could make a tag called Christmas dinner with friends and add information about the restaurant. Such tags are "singular", you would only use them for a single occasion, perhaps with multiple transactions.', + 'tags_introduction' => 'Usually tags are singular words, designed to quickly band items together using things like expensive, bill or for-party. In Firefly III, tags can have more properties such as a date, description and location. This allows you to join transactions together in a more meaningful way. For example, you could make a tag called Christmas dinner with friends and add information about the restaurant. Such tags are "singular", you would only use them for a single occasion, perhaps with multiple transactions.', 'tags_group' => 'Tags group transactions together, which makes it possible to store reimbursements (in case you front money for others) and other "balancing acts" where expenses are summed up (the payments on your new TV) or where expenses and deposits are cancelling each other out (buying something with saved money). It\'s all up to you. Using tags the old-fashioned way is of course always possible.', 'tags_start' => 'Create a tag to get started or enter tags when creating new transactions.', From 176c44e2b9944a8d9021860d50b0aa11587a2360 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 8 Mar 2017 20:38:08 +0100 Subject: [PATCH 089/244] Remove newline [skip ci] --- app/Http/Controllers/Transaction/ConvertController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php index 0a182cf2b3..efae285b27 100644 --- a/app/Http/Controllers/Transaction/ConvertController.php +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -173,7 +173,6 @@ class ConvertController extends Controller $joined = $sourceType->type . '-' . $destinationType->type; switch ($joined) { default: - throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one $destination = $sourceAccount; From 0e59f7433cd80189410d6ed11fa6746b125f1efb Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 9 Mar 2017 08:19:05 +0100 Subject: [PATCH 090/244] Code and tests for #615 --- .../Transaction/MassController.php | 19 ++-- resources/views/transactions/mass/edit.twig | 89 ++++++++++++------- .../Transaction/MassControllerTest.php | 5 ++ 3 files changed, 72 insertions(+), 41 deletions(-) diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index 0457d4d677..dfff2e24b7 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -14,13 +14,13 @@ declare(strict_types = 1); namespace FireflyIII\Http\Controllers\Transaction; use Carbon\Carbon; -use ExpandedForm; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\MassDeleteJournalRequest; use FireflyIII\Http\Requests\MassEditJournalRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use Illuminate\Support\Collection; use Preferences; @@ -118,8 +118,13 @@ class MassController extends Controller $subTitle = trans('firefly.mass_edit_journals'); /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $accountList = ExpandedForm::makeSelectList($repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); + $repository = app(AccountRepositoryInterface::class); + $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + + // get budgets + /** @var BudgetRepositoryInterface $budgetRepository */ + $budgetRepository = app(BudgetRepositoryInterface::class); + $budgets = $budgetRepository->getBudgets(); // skip transactions that have multiple destinations // or multiple sources: @@ -177,7 +182,7 @@ class MassController extends Controller $journals = $filtered; - return view('transactions.mass.edit', compact('journals', 'subTitle', 'accountList')); + return view('transactions.mass.edit', compact('journals', 'subTitle', 'accounts', 'budgets')); } /** @@ -200,7 +205,7 @@ class MassController extends Controller $sourceAccountName = $request->get('source_account_name')[$journal->id] ?? ''; $destAccountId = $request->get('destination_account_id')[$journal->id] ?? 0; $destAccountName = $request->get('destination_account_name')[$journal->id] ?? ''; - $budgetId = $journal->budgets->first() ? $journal->budgets->first()->id : 0; + $budgetId = $request->get('budget_id')[$journal->id] ?? 0; $category = $request->get('category')[$journal->id]; $tags = $journal->tags->pluck('tag')->toArray(); @@ -214,12 +219,12 @@ class MassController extends Controller 'destination_account_id' => intval($destAccountId), 'destination_account_name' => $destAccountName, 'amount' => round($request->get('amount')[$journal->id], 12), - 'currency_id' => intval($request->get('amount_currency_id_amount_' . $journal->id)), + 'currency_id' => $journal->transaction_currency_id, 'date' => new Carbon($request->get('date')[$journal->id]), 'interest_date' => $journal->interest_date, 'book_date' => $journal->book_date, 'process_date' => $journal->process_date, - 'budget_id' => $budgetId, + 'budget_id' => intval($budgetId), 'category' => $category, 'tags' => $tags, diff --git a/resources/views/transactions/mass/edit.twig b/resources/views/transactions/mass/edit.twig index f4dfb503a9..4f47734b4d 100644 --- a/resources/views/transactions/mass/edit.twig +++ b/resources/views/transactions/mass/edit.twig @@ -22,66 +22,87 @@   - {{ trans('list.description') }} + {{ trans('list.description') }} {{ trans('list.amount') }} {{ trans('list.date') }} {{ trans('list.from') }} {{ trans('list.to') }} {{ trans('list.category') }} + {{ trans('list.budget') }} {% for journal in journals %} {% if journal.transaction_count == 2 %} - - - - + {# LINK TO EDIT FORM #} + - - - - - - {{ ExpandedForm.amountSmall('amount_'~journal.id, journal.amount, {'name' : 'amount['~journal.id~']', 'currency' : journal.transactionCurrency}) }} - - - - + {# DESCRIPTION #} + +
+ {{ journal.transactionCurrency.symbol }} + +
- + + + {# DATE #} + + + + {# SOURCE ACCOUNT ID FOR TRANSFER OR WITHDRAWAL #} {% if journal.transaction_type_type == 'Transfer' or journal.transaction_type_type == 'Withdrawal' %} - {{ Form.select('source_account_id['~journal.id~']', accountList, journal.source_account_id, {'class': 'form-control'}) }} + {% else %} - - {{ Form.input('text', 'source_account_name['~journal.id~']', journal.source_account_name, {'class': 'form-control', 'placeholder': trans('form.revenue_account')}) }} + {# SOURCE ACCOUNT NAME FOR DEPOSIT #} + {% endif %} - - {% if journal.transaction_type_type == 'Transfer' or journal.transaction_type_type == 'Deposit' %} - - {{ Form.select('destination_account_id['~journal.id~']', accountList, journal.destination_account_id, {'class': 'form-control'}) }} + {# DESTINATION ACCOUNT NAME FOR TRANSFER AND DEPOSIT #} + {% else %} - - - {{ Form.input('text', 'destination_account_name['~journal.id~']', journal.destination_account_name, {'class': 'form-control', 'placeholder': trans('form.expense_account')}) }} + {# DESTINATION ACCOUNT NAME FOR EXPENSE #} + {% endif %} - + {# category #} - {{ Form.input('text', 'category['~journal.id~']', journal.categories[0].name, {'class': 'form-control', 'placeholder': trans('form.category')}) }} + + + {# budget #} + + {% if journal.transaction_type_type == 'Withdrawal' %} + + {% endif %} {% endif %} diff --git a/tests/Feature/Controllers/Transaction/MassControllerTest.php b/tests/Feature/Controllers/Transaction/MassControllerTest.php index 342380be36..c662c2c6b5 100644 --- a/tests/Feature/Controllers/Transaction/MassControllerTest.php +++ b/tests/Feature/Controllers/Transaction/MassControllerTest.php @@ -16,6 +16,7 @@ use DB; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use Illuminate\Support\Collection; use Log; @@ -77,6 +78,10 @@ class MassControllerTest extends TestCase $repository = $this->mock(AccountRepositoryInterface::class); $repository->shouldReceive('getAccountsByType')->once()->withArgs([[AccountType::DEFAULT, AccountType::ASSET]])->andReturn(new Collection); + // mock more stuff: + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $budgetRepos->shouldReceive('getBudgets')->andReturn(new Collection); + $transfers = TransactionJournal::where('transaction_type_id', 3)->where('user_id', $this->user()->id)->take(2)->get()->pluck('id')->toArray(); $this->be($this->user()); From 61007a95a601d4599c069afad26816d1225bc805 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 9 Mar 2017 20:54:18 +0100 Subject: [PATCH 091/244] Initial code for #595, transactions with no budget --- app/Http/Controllers/BudgetController.php | 118 ++++++++++++++++++---- resources/views/budgets/no-budget.twig | 58 ++++++++++- routes/web.php | 2 +- 3 files changed, 158 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index a9ac2a2749..6802b9b235 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -24,9 +24,12 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Log; +use Navigation; use Preferences; use Response; use View; @@ -191,27 +194,66 @@ class BudgetController extends Controller * * @return View */ - public function noBudget(Request $request) + public function noBudget(Request $request, string $moment = '') { - /** @var Carbon $start */ - $start = session('start', Carbon::now()->startOfMonth()); - /** @var Carbon $end */ - $end = session('end', Carbon::now()->endOfMonth()); + // default values: + $range = Preferences::get('viewRange', '1M')->data; + $start = null; + $end = null; + $periods = new Collection; + + // prep for "all" view. + if ($moment === 'all') { + $subTitle = trans('firefly.all_journals_without_budget'); + } + + // prep for "specific date" view. + if (strlen($moment) > 0 && $moment !== 'all') { + $start = new Carbon($moment); + $end = Navigation::endOfPeriod($start, $range); + $subTitle = trans( + 'firefly.without_budget_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + $periods = $this->noBudgetPeriodEntries(); + } + + // prep for current period + if (strlen($moment) === 0) { + $start = clone session('start', Navigation::startOfPeriod(new Carbon, $range)); + $end = clone session('end', Navigation::endOfPeriod(new Carbon, $range)); + $periods = $this->noBudgetPeriodEntries(); + $subTitle = trans( + 'firefly.without_budget_between', + ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] + ); + } + $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); - $subTitle = trans( - 'firefly.without_budget_between', - ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] - ); - // collector - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutBudget(); - $journals = $collector->getPaginatedJournals(); - $journals->setPath('/budgets/list/noBudget'); + $count = 0; + $loop = 0; + // grab journals, but be prepared to jump a period back to get the right ones: + Log::info('Now at no-budget loop start.'); + while ($count === 0 && $loop < 3) { + $loop++; + Log::info('Count is zero, search for journals.'); + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutBudget(); + $journals = $collector->getPaginatedJournals(); + $journals->setPath('/budgets/list/no-budget'); + $count = $journals->getCollection()->count(); + if ($count === 0) { + $start->subDay(); + $start = Navigation::startOfPeriod($start, $range); + $end = Navigation::endOfPeriod($start, $range); + Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d'))); + } + } - return view('budgets.no-budget', compact('journals', 'subTitle')); + return view('budgets.no-budget', compact('journals', 'subTitle', 'periods', 'start', 'end')); } /** @@ -405,7 +447,6 @@ class BudgetController extends Controller return $return; } - /** * @param Budget $budget * @param Carbon $start @@ -442,4 +483,47 @@ class BudgetController extends Controller return $set; } + /** + * @return Collection + */ + private function noBudgetPeriodEntries(): Collection + { + $repository = app(JournalRepositoryInterface::class); + $first = $repository->first(); + $start = $first->date ?? new Carbon; + $range = Preferences::get('viewRange', '1M')->data; + $start = Navigation::startOfPeriod($start, $range); + $end = Navigation::endOfX(new Carbon, $range); + $entries = new Collection; + + // properties for cache + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('no-budget-period-entries'); + + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + Log::debug('Going to get period expenses and incomes.'); + while ($end >= $start) { + $end = Navigation::startOfPeriod($end, $range); + $currentEnd = Navigation::endOfPeriod($end, $range); + + // count journals without budget in this period: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutBudget(); + $journals = $collector->getJournals()->count(); + $dateStr = $end->format('Y-m-d'); + $dateName = Navigation::periodShow($end, $range); + $entries->push([$dateStr, $dateName, $journals, clone $end]); + $end = Navigation::subtractPeriod($end, $range, 1); + } + $cache->store($entries); + + return $entries; + } + } diff --git a/resources/views/budgets/no-budget.twig b/resources/views/budgets/no-budget.twig index e8e1f0809b..83ef89391e 100644 --- a/resources/views/budgets/no-budget.twig +++ b/resources/views/budgets/no-budget.twig @@ -5,19 +5,73 @@ {% endblock %} {% block content %} + + {# upper show-all instruction #} + {% if periods.count > 0 %} + + {% endif %} +
-
+

{{ subTitle }}

-
+
{% include 'list.journals-tasker' with {'journals': journals} %} + {% if periods.count > 0 %} +

+ + {{ 'show_all_no_filter'|_ }} +

+ {% else %} +

+ + {{ 'show_the_current_period_and_overview'|_ }} +

+ {% endif %}
+ + {% if periods.count > 0 %} +
+ {% for entry in periods %} + {% if entry[2] > 0 %} +
+ +
+ + + + + +
{{ 'transactions'|_ }}{{ entry[2] }}
+
+
+ {% endif %} + {% endfor %} +
+ {% endif %} +
+ {# lower show-all instruction #} + {% if periods.count > 0 %} + + {% endif %} + {% endblock %} {% block scripts %} diff --git a/routes/web.php b/routes/web.php index 201351db87..36571253d9 100755 --- a/routes/web.php +++ b/routes/web.php @@ -144,7 +144,7 @@ Route::group( Route::get('delete/{budget}', ['uses' => 'BudgetController@delete', 'as' => 'delete']); Route::get('show/{budget}', ['uses' => 'BudgetController@show', 'as' => 'show']); Route::get('show/{budget}/{budgetlimit}', ['uses' => 'BudgetController@showByBudgetLimit', 'as' => 'show.limit']); - Route::get('list/no-budget', ['uses' => 'BudgetController@noBudget', 'as' => 'no-budget']); + Route::get('list/no-budget/{moment?}', ['uses' => 'BudgetController@noBudget', 'as' => 'no-budget']); Route::post('income', ['uses' => 'BudgetController@postUpdateIncome', 'as' => 'income.post']); Route::post('store', ['uses' => 'BudgetController@store', 'as' => 'store']); From db6e6dfe4a3417453670df1ae3fead13035f0464 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 9 Mar 2017 21:05:37 +0100 Subject: [PATCH 092/244] Fix tests for #595 --- app/Http/Controllers/BudgetController.php | 6 +- resources/lang/en_US/firefly.php | 1 + .../Controllers/BudgetControllerTest.php | 71 ++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 6802b9b235..6437e97a33 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -191,10 +191,11 @@ class BudgetController extends Controller /** * @param Request $request + * @param string $moment * * @return View */ - public function noBudget(Request $request, string $moment = '') + public function noBudget(Request $request, JournalRepositoryInterface $repository, string $moment = '') { // default values: $range = Preferences::get('viewRange', '1M')->data; @@ -205,6 +206,9 @@ class BudgetController extends Controller // prep for "all" view. if ($moment === 'all') { $subTitle = trans('firefly.all_journals_without_budget'); + $first = $repository->first(); + $start = $first->date ?? new Carbon; + $end = new Carbon; } // prep for "specific date" view. diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index b6aceb646d..c33e58cfd8 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -115,6 +115,7 @@ return [ 'multi_select_no_selection' => 'None selected', 'multi_select_all_selected' => 'All selected', 'multi_select_filter_placeholder' => 'Find..', + 'all_journals_without_budget' => 'All transactions without a budget', // repeat frequencies: diff --git a/tests/Feature/Controllers/BudgetControllerTest.php b/tests/Feature/Controllers/BudgetControllerTest.php index d6d0882781..fca3c8dc29 100644 --- a/tests/Feature/Controllers/BudgetControllerTest.php +++ b/tests/Feature/Controllers/BudgetControllerTest.php @@ -142,6 +142,7 @@ class BudgetControllerTest extends TestCase /** * @covers \FireflyIII\Http\Controllers\BudgetController::noBudget + * @covers \FireflyIII\Http\Controllers\BudgetController::noBudgetPeriodEntries * @dataProvider dateRangeProvider * * @param string $range @@ -151,10 +152,11 @@ class BudgetControllerTest extends TestCase // mock stuff $collector = $this->mock(JournalCollectorInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); - $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); + $journalRepos->shouldReceive('first')->andReturn(new TransactionJournal); $collector->shouldReceive('setAllAssetAccounts')->andReturnSelf(); $collector->shouldReceive('setRange')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection); $collector->shouldReceive('setLimit')->andReturnSelf(); $collector->shouldReceive('setPage')->andReturnSelf(); $collector->shouldReceive('withoutBudget')->andReturnSelf(); @@ -172,6 +174,73 @@ class BudgetControllerTest extends TestCase $response->assertSee('