Commit 4641d0b2 authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

Paginate attempts

parent cca55e29
......@@ -5,9 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Source\Authorization\Permissions;
use Source\UseCases\Attempts\GetAttempts\GetAttemptsUseCase;
use Source\UseCases\Attempts\GetAttemptsForDoor\GetAttemptsForDoorUseCase;
use Source\UseCases\Attempts\GetAttempts\APIPresenter as GetAttemptsAPIPresenter;
use Source\UseCases\Attempts\GetAttemptsForDoor\APIPresenter as GetAttemptsForDoorAPIPresenter;
use Source\UseCases\Attempts\APIPresenter as GetAttemptsAPIPresenter;
class AttemptsController extends ApiController
{
......@@ -24,44 +22,20 @@ class AttemptsController extends ApiController
$this->authorizer->protect(Permissions::LOGS_READ);
$this->validate($this->request, [
'start' => 'required|date|before:end',
'end' => 'required|date|after:start',
'start' => 'date',
'end' => 'date',
'door_id' => 'integer',
]);
$presenter = new GetAttemptsAPIPresenter();
$attempts->getBetweenDates($this->request->input('start'), $this->request->input('end'), $presenter);
return $this->respondWithData($presenter->getViewModel());
}
/**
* @param \Source\UseCases\Attempts\GetAttemptsForDoor\GetAttemptsForDoorUseCase $doorAttempts
* @param string $doorId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
* @throws \Source\Exceptions\AuthorizationException
* @throws \Source\Exceptions\EntityNotFoundException
* @throws \Exception
*/
public function doorAttempts(GetAttemptsForDoorUseCase $doorAttempts, string $doorId): JsonResponse
{
$this->authorizer->protect(Permissions::LOGS_READ);
$this->validate($this->request, [
'start' => 'required|date|before:end',
'end' => 'required|date|after:start',
]);
$presenter = new GetAttemptsForDoorAPIPresenter();
$doorAttempts->getAttemptsForDoorBetween(
$doorId,
$attempts->getBetweenDates(
$this->request->input('start'),
$this->request->input('end'),
$this->request->input('door_id'),
$presenter
);
return $this->respondWithData($presenter->getViewModel());
return $this->respondWithData($presenter->getViewModel(50));
}
}
......@@ -63,7 +63,6 @@ use Source\UseCases\Doors\GenerateDoorToken\GenerateDoorTokenUseCaseServiceProvi
use Source\UseCases\Entries\GetEntriesForDoor\GetEntriesForDoorUseCaseServiceProvider;
use Source\UseCases\Entries\GetEntriesForUser\GetEntriesForUserUseCaseServiceProvider;
use Source\UseCases\Overrides\OverridesForDoor\OverridesForDoorUseCaseServiceProvider;
use Source\UseCases\Attempts\GetAttemptsForDoor\GetAttemptsForDoorUseCaseServiceProvider;
use Source\UseCases\DoorGroup\RemoveDoorFromGroup\RemoveDoorFromGroupUseCaseServiceProvider;
use Source\UseCases\GroupSchedule\SchedulesForGroup\SchedulesForGroupUseCaseServiceProvider;
use Source\UseCases\GroupUser\RemoveUserFromGroup\RemoveUserFromGroupUseCaseServiceProvider;
......@@ -160,7 +159,6 @@ class AppServiceProvider extends ServiceProvider
// Attempts
GetAttemptsUseCaseServiceProvider::class,
GetAttemptsForDoorUseCaseServiceProvider::class,
// Entries
GetEntriesForDoorUseCaseServiceProvider::class,
......
......@@ -72,7 +72,6 @@ Route::group(['middleware' => 'auth:api'], static function () {
Route::delete('{doorId}', [DoorsController::class, 'delete']);
Route::get('{doorId}/entries', [EntriesController::class, 'doorEntries']);
Route::get('{doorId}/attempts', [AttemptsController::class, 'doorAttempts']);
Route::get('{doorId}/groups', [DoorsController::class, 'getGroupsForDoor']);
Route::post('{doorId}/group/{groupId}', [DoorsController::class, 'addDoorToGroup']);
......
......@@ -76,9 +76,21 @@ class Attempt
* @param \Carbon\Carbon $end
* @return bool
*/
public function isBetween(Carbon $begin, Carbon $end): bool
public function isBetween(?Carbon $begin, ?Carbon $end): bool
{
return $this->createdAt->isBetween($begin, $end);
if ($begin && $end) {
return $this->createdAt->isBetween($begin, $end);
}
if ($begin) {
return $this->createdAt->greaterThan($begin);
}
if ($end) {
return $this->createdAt->lessThan($end);
}
return true;
}
/**
......
......@@ -15,17 +15,10 @@ interface AttemptsRepository
public function add(Attempt $attempt): ?Attempt;
/**
* @param \Carbon\Carbon $begin
* @param \Carbon\Carbon $end
* @param \Carbon\Carbon|null $begin
* @param \Carbon\Carbon|null $end
* @param string|null $doorId
* @return \Source\Entities\Attempt[]
*/
public function getBetween(Carbon $begin, Carbon $end): array;
/**
* @param string $doorId
* @param \Carbon\Carbon $begin
* @param \Carbon\Carbon $end
* @return \Source\Entities\Attempt[]
*/
public function getForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array;
public function getBetween(?Carbon $begin = null, ?Carbon $end = null, ?string $doorId = null): array;
}
......@@ -28,28 +28,25 @@ class DatabaseAttemptsRepository implements AttemptsRepository
/**
* @inheritDoc
*/
public function getBetween(Carbon $begin, Carbon $end): array
public function getBetween(?Carbon $begin = null, ?Carbon $end = null, ?string $doorId = null): array
{
$attempts = \App\Attempt::query()->whereBetween('created_at', [$begin, $end])
->get()->values()->all();
$query = \App\Attempt::query()->orderByDesc('created_at');
return array_map(static function (\App\Attempt $attempt) {
return self::toAttempt($attempt);
}, $attempts);
}
if ($doorId) {
$query->where('door_id', $this->castToInt($doorId));
}
/**
* @inheritDoc
*/
public function getForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array
{
$attempts = \App\Attempt::query()->where('door_id', $this->castToInt($doorId))
->whereBetween('created_at', [$begin, $end])
->get()->values()->all();
if ($begin && $end) {
$query->whereBetween('created_at', [$begin, $end]);
} elseif ($begin) {
$query->where('created_at', '>', $begin);
} elseif ($end) {
$query->where('created_at', '<', $end);
}
return array_map(static function (\App\Attempt $attempt) {
return self::toAttempt($attempt);
}, $attempts);
}, $query->get()->values()->all());
}
/**
......
......@@ -34,20 +34,13 @@ class InMemoryAttemptsRepository implements AttemptsRepository
/**
* @inheritDoc
*/
public function getBetween(Carbon $begin, Carbon $end): array
public function getBetween(?Carbon $begin = null, ?Carbon $end = null, ?string $doorId = null): array
{
return array_values(array_filter($this->attempts, static function (Attempt $attempt) use ($begin, $end) {
return array_values(array_filter($this->attempts, static function (Attempt $attempt) use ($begin, $end, $doorId) {
if ($doorId) {
return $attempt->isBetween($begin, $end) && $attempt->hasDoorIdOf($doorId);
}
return $attempt->isBetween($begin, $end);
}));
}
/**
* @inheritDoc
*/
public function getForDoorBetween(string $doorId, Carbon $begin, Carbon $end): array
{
return array_values(array_filter($this->attempts, static function (Attempt $attempt) use ($doorId, $begin, $end) {
return $attempt->isBetween($begin, $end) && $attempt->hasDoorIdOf($doorId);
}));
}
}
......@@ -7,11 +7,11 @@ use Carbon\Carbon;
trait CastsTo
{
/**
* @param $date
* @param string $format
* @param string|null $date
* @param string $format
* @return \Carbon\Carbon|null
*/
protected function castToCarbon($date, $format = 'Y-m-d H:i:s'): ?Carbon
protected function castToCarbon(?string $date, $format = 'Y-m-d H:i:s'): ?Carbon
{
if (empty($date)) {
return null;
......@@ -20,6 +20,20 @@ trait CastsTo
return Carbon::createFromFormat($format, $date);
}
/**
* @param string|null $date
* @return \Carbon\Carbon|null
* @throws \Exception
*/
protected function liberalCastToCarbon(?string $date): ?Carbon
{
if (empty($date)) {
return null;
}
return new Carbon($date);
}
/**
* @param string|null $int
* @return int|null
......
......@@ -6,60 +6,21 @@ use Illuminate\Pagination\LengthAwarePaginator;
trait Paginates
{
protected int $perPage = 50;
protected int $page = 1;
protected array $appends = [];
protected ?LengthAwarePaginator $paginator;
/**
* @param int $perPage
*/
public function setPerPage(int $perPage): void
{
$this->perPage = $perPage;
}
/**
* @param int $page
*/
public function setPage(int $page): void
{
$this->page = $page;
}
/**
* @return array
*/
public function getItems(): array
{
if (!$this->paginator) {
return [];
}
return $this->paginator->toArray();
}
/**
* @param array $items
* @param int $perPage
* @param array $appends
* @return array
*/
public function setItems(array $items): void
protected function paginate(array $items, int $perPage = 50, array $appends = []): array
{
$currentPage = LengthAwarePaginator::resolveCurrentPage();
$currentItems = array_slice($items, $this->perPage * ($currentPage - 1), $this->perPage);
$currentItems = array_slice($items, $perPage * ($currentPage - 1), $perPage);
$this->paginator = new LengthAwarePaginator($currentItems, count($items), $this->perPage, $currentPage);
$this->paginator->appends($this->appends);
}
$paginator = new LengthAwarePaginator($currentItems, count($items), $perPage, $currentPage);
$paginator->appends($appends);
/**
* @param array $appends
*/
public function setAppends(array $appends): void
{
$this->appends = $appends;
return $paginator->toArray();
}
}
<?php
namespace Source\UseCases\Attempts\GetAttempts;
namespace Source\UseCases\Attempts;
use Source\Entities\Attempt;
use Source\Sanitize\Paginates;
use Source\UseCases\BasePresenter;
class APIPresenter extends BasePresenter implements Presenter
{
protected array $viewModel = [];
use Paginates;
protected array $attempts = [];
/** @inheritDoc */
public function present(ResponseModel $responseModel): void
{
$this->viewModel['attempts'] = array_map(function (Attempt $attempt) {
$this->attempts = array_map(function (Attempt $attempt) {
return $this->formatAttempt($attempt);
}, $responseModel->getAttempts());
}
/** @inheritDoc */
public function getViewModel(): array
public function getViewModel(int $perPage, array $appends = []): array
{
return $this->viewModel;
return $this->paginate($this->attempts, $perPage, $appends);
}
}
......@@ -2,11 +2,15 @@
namespace Source\UseCases\Attempts\GetAttempts;
use Carbon\Carbon;
use Source\Sanitize\CastsTo;
use Source\UseCases\Attempts\Presenter;
use Source\UseCases\Attempts\ResponseModel;
use Source\Gateways\Attempts\AttemptsRepository;
class GetAttempts implements GetAttemptsUseCase
{
use CastsTo;
/**
* @var \Source\Gateways\Attempts\AttemptsRepository
*/
......@@ -20,11 +24,19 @@ class GetAttempts implements GetAttemptsUseCase
/**
* @inheritDoc
*/
public function getBetweenDates(string $start, string $end, Presenter $presenter): void
public function getBetweenDates(?string $start, ?string $end, ?string $doorId, Presenter $presenter): void
{
$attempts = $this->attempts->getBetween(new Carbon($start), new Carbon($end));
$attempts = $this->attempts->getBetween(
$this->liberalCastToCarbon($start),
$this->liberalCastToCarbon($end),
$doorId
);
$response = new ResponseModel();
$response = new ResponseModel($attempts);
foreach ($attempts as $attempt) {
$response->addAttempt($attempt);
}
$presenter->present($response);
}
......
......@@ -3,15 +3,18 @@
namespace Source\UseCases\Attempts\GetAttempts;
use Source\UseCases\Attempts\Presenter;
interface GetAttemptsUseCase
{
/**
* the passed start and end dates must be parsable by datetime
*
* @param string $start
* @param string $end
* @param \Source\UseCases\Attempts\GetAttempts\Presenter $presenter
* @param string $start
* @param string $end
* @param string|null $doorId
* @param \Source\UseCases\Attempts\Presenter $presenter
* @throws \Exception
*/
public function getBetweenDates(string $start, string $end, Presenter $presenter): void;
public function getBetweenDates(?string $start, ?string $end, ?string $doorId, Presenter $presenter): void;
}
<?php
namespace Source\UseCases\Attempts\GetAttempts;
interface Presenter
{
/**
* @param ResponseModel $responseModel
* @return void
*/
public function present(ResponseModel $responseModel): void;
/**
* @return array
*/
public function getViewModel(): array;
}
<?php
namespace Source\UseCases\Attempts\GetAttempts;
class ResponseModel
{
/**
* @var \Source\Entities\Attempt[]
*/
protected array $attempts;
/**
* @var \Source\Entities\Attempt[]
*/
public function __construct(array $attempts)
{
$this->attempts = $attempts;
}
/**
* @return \Source\Entities\Attempt[]
*/
public function getAttempts(): array
{
return $this->attempts;
}
}
<?php
namespace Source\UseCases\Attempts\GetAttemptsForDoor;
use Source\Entities\Attempt;
use Source\UseCases\BasePresenter;
class APIPresenter extends BasePresenter implements Presenter
{
protected array $viewModel = [];
/** @inheritDoc */
public function present(ResponseModel $responseModel): void
{
$this->viewModel['attempts'] = array_map(function (Attempt $attempt) {
return $this->formatAttempt($attempt);
}, $responseModel->getAttempts());
}
/** @inheritDoc */
public function getViewModel(): array
{
return $this->viewModel;
}
}
<?php
namespace Source\UseCases\Attempts\GetAttemptsForDoor;
use Carbon\Carbon;
use Source\Gateways\Doors\DoorsRepository;
use Source\Exceptions\EntityNotFoundException;
use Source\Gateways\Attempts\AttemptsRepository;
class GetAttemptsForDoor implements GetAttemptsForDoorUseCase
{
/**
* @var \Source\Gateways\Attempts\AttemptsRepository
*/
protected AttemptsRepository $attempts;
/**
* @var \Source\Gateways\Doors\DoorsRepository
*/
protected DoorsRepository $doors;
public function __construct(AttemptsRepository $attempts, DoorsRepository $doors)
{
$this->attempts = $attempts;
$this->doors = $doors;
}
/**
* @inheritDoc
*/
public function getAttemptsForDoorBetween(string $doorId, string $begin, string $end, Presenter $presenter): void
{
if (!$this->doors->exists($doorId)) {
throw new EntityNotFoundException();
}
$attempts = $this->attempts->getForDoorBetween($doorId, new Carbon($begin), new Carbon($end));
$response = new ResponseModel($attempts);
$presenter->present($response);
}
}
<?php
namespace Source\UseCases\Attempts\GetAttemptsForDoor;
interface GetAttemptsForDoorUseCase
{
/**
* Begin and end must be parsable by datetime
*
* @param string $doorId
* @param string $begin
* @param string $end
* @param \Source\UseCases\Attempts\GetAttemptsForDoor\Presenter $presenter
* @throws \Source\Exceptions\EntityNotFoundException
* @throws \Exception
*/
public function getAttemptsForDoorBetween(string $doorId, string $begin, string $end, Presenter $presenter): void;
}
<?php
namespace Source\UseCases\Attempts\GetAttemptsForDoor;
use Illuminate\Support\ServiceProvider;
use Source\Gateways\Doors\DoorsRepository;
use Illuminate\Contracts\Foundation\Application;
use Source\Gateways\Attempts\AttemptsRepository;
use Illuminate\Contracts\Support\DeferrableProvider;
/**
* Service provider must be registered in AppServiceProvider
*/
class GetAttemptsForDoorUseCaseServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind(GetAttemptsForDoorUseCase::class, static function (Application $app) {
return new GetAttemptsForDoor($app->make(AttemptsRepository::class), $app->make(DoorsRepository::class));
});
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
}
/**
* @return array
*/
public function provides