From b2381f465756a2bb50b1b26e76864cc0a89eb728 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 5 Dec 2018 19:12:38 +0100 Subject: [PATCH] Add tag endpoint. --- app/Api/V1/Controllers/TagController.php | 234 +++++++++++++++++++++++ app/Api/V1/Requests/TagRequest.php | 115 +++++++++++ app/Support/Binder/TagOrId.php | 64 +++++++ app/Transformers/TagTransformer.php | 1 - config/firefly.php | 1 + resources/lang/en_US/demo.php | 3 +- routes/api.php | 16 ++ 7 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 app/Api/V1/Controllers/TagController.php create mode 100644 app/Api/V1/Requests/TagRequest.php create mode 100644 app/Support/Binder/TagOrId.php diff --git a/app/Api/V1/Controllers/TagController.php b/app/Api/V1/Controllers/TagController.php new file mode 100644 index 0000000000..1c7e99c594 --- /dev/null +++ b/app/Api/V1/Controllers/TagController.php @@ -0,0 +1,234 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Controllers; + +use FireflyIII\Api\V1\Requests\TagRequest; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Filter\InternalTransferFilter; +use FireflyIII\Models\Tag; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Transformers\TagTransformer; +use FireflyIII\Transformers\TransactionTransformer; +use FireflyIII\User; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; +use League\Fractal\Manager; +use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use League\Fractal\Resource\Item; +use League\Fractal\Serializer\JsonApiSerializer; + +use League\Fractal\Resource\Collection as FractalCollection; +/** + * Class TagController + */ +class TagController extends Controller +{ + /** @var TagRepositoryInterface The tag repository */ + private $repository; + + /** + * RuleController constructor. + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + /** @var User $user */ + $user = auth()->user(); + + $this->repository = app(TagRepositoryInterface::class); + $this->repository->setUser($user); + + return $next($request); + } + ); + } + + /** + * Delete the resource. + * + * @param Tag $tag + * + * @return JsonResponse + */ + public function delete(Tag $tag): JsonResponse + { + $this->repository->destroy($tag); + + return response()->json([], 204); + } + + /** + * List all of them. + * + * @param Request $request + * + * @return JsonResponse] + */ + public function index(Request $request): JsonResponse + { + // create some objects: + $manager = new Manager; + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + + // types to get, page size: + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + + // get list of budgets. Count it and split it. + $collection = $this->repository->get(); + $count = $collection->count(); + $rules = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + + // make paginator: + $paginator = new LengthAwarePaginator($rules, $count, $pageSize, $this->parameters->get('page')); + $paginator->setPath(route('api.v1.tags.index') . $this->buildParams()); + + // present to user. + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + $resource = new FractalCollection($rules, new TagTransformer($this->parameters), 'tags'); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + + } + + /** + * List single resource. + * + * @param Request $request + * @param Tag $tag + * + * @return JsonResponse + */ + public function show(Request $request, Tag $tag): JsonResponse + { + $manager = new Manager(); + // add include parameter: + $include = $request->get('include') ?? ''; + $manager->parseIncludes($include); + + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $resource = new Item($tag, new TagTransformer($this->parameters), 'tags'); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + + } + + + /** + * Show all transactions. + * + * @param Request $request + * @param Tag $tag + * + * @return JsonResponse + */ + public function transactions(Request $request, Tag $tag): JsonResponse + { + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $type = $request->get('type') ?? 'default'; + $this->parameters->set('type', $type); + + $types = $this->mapTypes($this->parameters->get('type')); + $manager = new Manager(); + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + /** @var User $admin */ + $admin = auth()->user(); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setUser($admin); + $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); + $collector->setAllAssetAccounts(); + $collector->setTag($tag); + + if (\in_array(TransactionType::TRANSFER, $types, true)) { + $collector->removeFilter(InternalTransferFilter::class); + } + + if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) { + $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); + } + $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); + $collector->setTypes($types); + $paginator = $collector->getPaginatedTransactions(); + $paginator->setPath(route('api.v1.transactions.index') . $this->buildParams()); + $transactions = $paginator->getCollection(); + + $repository = app(JournalRepositoryInterface::class); + + $resource = new FractalCollection($transactions, new TransactionTransformer($this->parameters, $repository), 'transactions'); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + + + /** + * Store new object. + * + * @param TagRequest $request + * + * @return JsonResponse + */ + public function store(TagRequest $request): JsonResponse + { + $rule = $this->repository->store($request->getAll()); + $manager = new Manager(); + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $resource = new Item($rule, new TagTransformer($this->parameters), 'tags'); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + + /** + * Update a rule. + * + * @param TagRequest $request + * @param Tag $tag + * + * @return JsonResponse + */ + public function update(TagRequest $request, Tag $tag): JsonResponse + { + $rule = $this->repository->update($tag, $request->getAll()); + $manager = new Manager(); + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $resource = new Item($rule, new TagTransformer($this->parameters), 'tags'); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + + } +} \ No newline at end of file diff --git a/app/Api/V1/Requests/TagRequest.php b/app/Api/V1/Requests/TagRequest.php new file mode 100644 index 0000000000..21c2f722cb --- /dev/null +++ b/app/Api/V1/Requests/TagRequest.php @@ -0,0 +1,115 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Requests; + +use FireflyIII\Models\Tag; +use Illuminate\Validation\Validator; + +/** + * Class TagRequest + */ +class TagRequest extends Request +{ + + /** + * Authorize logged in users. + * + * @return bool + */ + public function authorize(): bool + { + // Only allow authenticated users + return auth()->check(); + } + + /** + * Get all data from the request. + * + * @return array + */ + public function getAll(): array + { + $data = [ + 'tag' => $this->string('tag'), + 'date' => $this->date('date'), + 'description' => $this->string('description'), + 'latitude' => '' === $this->string('latitude') ? null : $this->string('latitude'), + 'longitude' => '' === $this->string('longitude') ? null : $this->string('longitude'), + 'zoomLevel' => $this->integer('zoom_level'), + ]; + + return $data; + } + + /** + * The rules that the incoming request must be matched against. + * + * @return array + */ + public function rules(): array + { + $rules = [ + 'tag' => 'required|min:1|uniqueObjectForUser:tags,tag', + 'description' => 'min:1|nullable', + 'date' => 'date|nullable', + 'latitude' => 'numeric|min:-90|max:90|nullable|required_with:longitude', + 'longitude' => 'numeric|min:-90|max:90|nullable|required_with:latitude', + 'zoomLevel' => 'numeric|min:0|max:80|nullable', + ]; + switch ($this->method()) { + default: + break; + case 'PUT': + case 'PATCH': + /** @var Tag $tag */ + $tag = $this->route()->parameter('tagOrId'); + $rules['tag'] = 'required|min:1|uniqueObjectForUser:tags,tag,' . $tag->id; + break; + } + + return $rules; + } + + /** + * Configure the validator instance. + * + * @param Validator $validator + * + * @return void + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator) { + $data = $validator->getData(); + $min = (float)($data['amount_min'] ?? 0); + $max = (float)($data['amount_max'] ?? 0); + if ($min > $max) { + $validator->errors()->add('amount_min', (string)trans('validation.amount_min_over_max')); + } + } + ); + } +} diff --git a/app/Support/Binder/TagOrId.php b/app/Support/Binder/TagOrId.php new file mode 100644 index 0000000000..596de5135b --- /dev/null +++ b/app/Support/Binder/TagOrId.php @@ -0,0 +1,64 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Binder; + +use FireflyIII\Models\Tag; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use Illuminate\Routing\Route; +use Illuminate\Support\Collection; +use Log; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Class TagOrId. + */ +class TagOrId implements BinderInterface +{ + /** + * @param string $value + * @param Route $route + * + * @return Collection + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public static function routeBinder(string $value, Route $route): Tag + { + if (auth()->check()) { + /** @var TagRepositoryInterface $repository */ + $repository = app(TagRepositoryInterface::class); + $repository->setUser(auth()->user()); + + $result = $repository->findByTag($value); + if (null === $result) { + $result = $repository->findNull((int)$value); + } + if (null !== $result) { + return $result; + } + Log::error('TagOrId: tag not found.'); + throw new NotFoundHttpException; + } + Log::error('TagOrId: user is not logged in.'); + throw new NotFoundHttpException; + } +} diff --git a/app/Transformers/TagTransformer.php b/app/Transformers/TagTransformer.php index 09f1ac8264..f9f304f330 100644 --- a/app/Transformers/TagTransformer.php +++ b/app/Transformers/TagTransformer.php @@ -119,7 +119,6 @@ class TagTransformer extends TransformerAbstract 'updated_at' => $tag->updated_at->toAtomString(), 'created_at' => $tag->created_at->toAtomString(), 'tag' => $tag->tag, - 'tag_mode' => $tag->tagMode, 'date' => $date, 'description' => '' === $tag->description ? null : $tag->description, 'latitude' => (float)$tag->latitude, diff --git a/config/firefly.php b/config/firefly.php index 7cca068529..abb028bd36 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -324,6 +324,7 @@ return [ 'toCurrencyCode' => \FireflyIII\Support\Binder\CurrencyCode::class, 'unfinishedJournal' => \FireflyIII\Support\Binder\UnfinishedJournal::class, 'cliToken' => \FireflyIII\Support\Binder\CLIToken::class, + 'tagOrId' => \FireflyIII\Support\Binder\TagOrId::class, ], diff --git a/resources/lang/en_US/demo.php b/resources/lang/en_US/demo.php index 2b01c140f5..24aa708375 100644 --- a/resources/lang/en_US/demo.php +++ b/resources/lang/en_US/demo.php @@ -34,6 +34,5 @@ return [ 'transactions-index' => 'These expenses, deposits and transfers are not particularly imaginative. They have been generated automatically.', 'piggy-banks-index' => 'As you can see, there are three piggy banks. Use the plus and minus buttons to influence the amount of money in each piggy bank. Click the name of the piggy bank to see the administration for each piggy bank.', 'import-index' => 'Any CSV file can be imported into Firefly III. It also supports importing data from bunq and Spectre. Other banks and financial aggregators will be implemented in the future. As a demo-user however, you can only see the "fake"-provider in action. It will generate some random transactions to show you how the process works.', - 'recurring-index' => 'Please note that this feature is under active development and may not work as expected.', - 'recurring-create' => 'Please note that this feature is under active development and may not work as expected.', + 'profile-index' => 'Keep in mind that the demo site resets every four hours. Your access may be revoked at any time. This happens automatically and is not a bug.', ]; diff --git a/routes/api.php b/routes/api.php index 41f3073cc3..f709e76e5e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -245,6 +245,22 @@ Route::group( } ); +Route::group( + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'tags', 'as' => 'api.v1.tags.'], + function () { + + // Transaction currency API routes: + Route::get('', ['uses' => 'TagController@index', 'as' => 'index']); + Route::post('', ['uses' => 'TagController@store', 'as' => 'store']); + Route::get('{tagOrId}', ['uses' => 'TagController@show', 'as' => 'show']); + Route::put('{tagOrId}', ['uses' => 'TagController@update', 'as' => 'update']); + Route::delete('{tagOrId}', ['uses' => 'TagController@delete', 'as' => 'delete']); + Route::get('{tagOrId}/transactions', ['uses' => 'TagController@transactions', 'as' => 'delete']); + } +); + + + Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'transactions', 'as' => 'api.v1.transactions.'], function () {