Commit 9725d71b authored by Jacob Priddy's avatar Jacob Priddy 👌

Add door administrative notes column as well as door publicity

parent ff223f98
......@@ -7,6 +7,9 @@ use ReflectionClass;
use RuntimeException;
use Illuminate\Routing\Route;
use ReflectionFunctionAbstract;
use League\Flysystem\Adapter\Local;
use Source\Gateways\Doors\DoorsRepository;
use Source\Gateways\Doors\LocalDoorsRepository;
use Knuckles\Scribe\Extracting\Strategies\Strategy;
class GoodResponseValidationStrategy extends Strategy
......
......@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Door extends Authenticatable
{
protected $fillable = ['id', 'name', 'location', 'created_at', 'updated_at', 'last_seen_at'];
protected $fillable = ['id', 'name', 'location', 'created_at', 'updated_at', 'last_seen_at', 'public', 'notes'];
protected $dates = ['last_seen_at'];
......
......@@ -72,8 +72,8 @@ class DoorController extends ApiController
* Also processes commands that are separated by a '*' from the doorcode. If a command is accepted a 200 is
* returned. If a command is rejected, a 403 is given.
*
* @bodyParam doorcode string required The doorcode to query. Example: 123456*00110
* @bodyParam foresight int Number of minutes ahead of now to get the open mode times for. Example: 720
* @bodyParam doorcode string required The doorcode to query. Example: 123456*00110
* @bodyParam foresight int Number of minutes ahead of now to get the open mode times for. Example: 720
*
* @response 422
* {"message":"The given data was invalid.","errors":{"foresight":["The foresight must be an integer."]}}
......
......@@ -59,7 +59,7 @@ class DoorsController extends ApiController
$presenter = new GetAllDoorsAPIPresenter();
$getDoors->query($this->request->all(), $presenter);
$getDoors->query($this->request->all(), false, $presenter);
return $this->respondWithData($presenter->getViewModel($this->request->all()));
}
......@@ -95,6 +95,8 @@ class DoorsController extends ApiController
*
* @bodyParam location string required Door Number or location describing where it is such as `CSP165`. Example: CSP165
* @bodyParam name string required The unique name for the door. Must be unique. Example: Engineering Lecture Hall
* @bodyParam public boolean Marks the door as visible to general users or ony to admins. Example: 0
* @bodyParam notes string|null Administrative notes that can only be seen from the administrative panel. Example: Replaced 10/10/11
*
* @param \Source\UseCases\Doors\CreateDoor\CreateDoorUseCase $createDoor
* @return \Illuminate\Http\JsonResponse
......@@ -110,6 +112,8 @@ class DoorsController extends ApiController
$this->validate($this->request, [
'location' => 'required|string|max:255',
'name' => 'required|string|max:255',
'public' => 'required|boolean',
'notes' => 'nullable|string|max:255',
]);
$presenter = new CreateDoorAPIPresenter();
......@@ -127,6 +131,8 @@ class DoorsController extends ApiController
* @urlParam doorId required The application ID of the door to update. Example: 2
* @bodyParam location string Door Number or location describing where it is such as `CSP165`. Example: CSP166
* @bodyParam name string The unique name for the door. Must be unique. Example: Not the Engineering Lecture Hall
* @bodyParam public boolean Marks the door as visible to general users or ony to admins. Example: 1
* @bodyParam notes string|null Administrative notes that can only be seen from the administrative panel. Example: Replaced 10/10/10
*
* @param \Source\UseCases\Doors\UpdateDoor\UpdateDoorUseCase $updateDoor
* @param string $doorId
......@@ -143,6 +149,8 @@ class DoorsController extends ApiController
$this->validate($this->request, [
'location' => 'string|max:255',
'name' => 'string|max:255',
'public' => 'boolean',
'notes' => 'nullable|string|max:255',
]);
$presenter = new UpdateDoorAPIPresenter();
......
......@@ -3,7 +3,7 @@
namespace App\Http\Controllers\Web\Admin;
use Illuminate\View\View;
use Illuminate\Contracts\View\View;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Source\Exceptions\EntityExistsException;
......@@ -33,9 +33,22 @@ use Source\UseCases\DoorGroup\RemoveDoorFromGroup\APIPresenter as RemoveDoorGrou
class DoorsController extends Controller
{
/**
* Since a checkbox input sends "on" when selected and null when not selected.
* So here if it is set to "on" we turn it to false, and if it is not present, we set it to false.
*/
protected function processPublicField(): void
{
if (!$this->request->has('public')) {
$this->request['public'] = false;
} else {
$this->request['public'] = true;
}
}
/**
* @param \Source\UseCases\Doors\GetDoors\GetDoorsUseCase $doors
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Validation\ValidationException
*/
public function index(GetDoorsUseCase $doors): View
......@@ -47,7 +60,7 @@ class DoorsController extends Controller
]);
$presenter = new GetDoorsPresenter();
$doors->query($this->request->all(), $presenter);
$doors->query($this->request->all(), false, $presenter);
return view('admin.doors', $presenter->getViewModel($this->request->all()));
}
......@@ -55,7 +68,7 @@ class DoorsController extends Controller
/**
* @param string $doorId
* @param \Source\UseCases\Doors\GetDoor\GetDoorUseCase $getDoor
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\View\View
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function edit(string $doorId, GetDoorUseCase $getDoor): View
......@@ -80,9 +93,13 @@ class DoorsController extends Controller
*/
public function update(string $doorId, UpdateDoorUseCase $updateDoor): RedirectResponse
{
$this->processPublicField();
$this->validate($this->request, [
'location' => 'required|string|max:255',
'name' => 'required|string|max:255',
'public' => 'boolean',
'notes' => 'string|max:255',
]);
$presenter = new UpdateDoorPresenter();
......@@ -102,9 +119,13 @@ class DoorsController extends Controller
*/
public function store(CreateDoorUseCase $createDoor): RedirectResponse
{
$this->processPublicField();
$this->validate($this->request, [
'location' => 'required|string|max:255',
'name' => 'required|string|max:255',
'public' => 'required|boolean',
'notes' => 'nullable|string|max:255',
]);
$presenter = new CreateDoorPresenter();
......@@ -166,7 +187,7 @@ class DoorsController extends Controller
* @param string $doorId
* @param \Source\UseCases\DoorGroup\GetDoorGroups\GetDoorGroupsUseCase $doorGroups
* @param \Source\UseCases\Groups\GetGroups\GetGroupsUseCase $groups
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\View\View
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function groups(string $doorId, GetDoorGroupsUseCase $doorGroups, GetGroupsUseCase $groups): View
......@@ -231,7 +252,7 @@ class DoorsController extends Controller
/**
* @param string $doorId
* @param \Source\UseCases\DoorUser\UserDoorAccess\UserDoorAccessUseCase $doorUsers
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\View\View
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function users(string $doorId, UserDoorAccessUseCase $doorUsers): View
......
......@@ -3,8 +3,8 @@
namespace App\Http\Controllers\Web\Admin;
use Illuminate\View\View;
use Source\Entities\Override;
use Illuminate\Contracts\View\View;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\ValidationException;
......@@ -57,7 +57,7 @@ class OverridesController extends Controller
{
$presenter = new GetDoorsPresenter();
$doors->query([], $presenter);
$doors->query([], false, $presenter);
return view('admin.entities.override', ['doors' => $presenter->getViewModel()]);
}
......@@ -109,7 +109,7 @@ class OverridesController extends Controller
{
$doorsPresenter = new GetDoorsPresenter();
$doors->query([], $doorsPresenter);
$doors->query([], false, $doorsPresenter);
$presenter = new OverrideGetPresenter();
......
......@@ -2,7 +2,7 @@
namespace App\Http\Controllers\Web\Admin;
use Illuminate\View\View;
use Illuminate\Contracts\View\View;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Source\Exceptions\DeleteFailedException;
......@@ -165,7 +165,7 @@ class UsersController extends Controller
$presenter = new DoorsPresenter();
$doors->query([], $presenter);
$doors->query([], false, $presenter);
return view('admin.entities.userAccess')
->with('doorId', $this->request->input('door_id'))
......
......@@ -2,8 +2,8 @@
namespace App\Http\Controllers\Web;
use Illuminate\View\View;
use Source\Sanitize\CastsTo;
use Illuminate\Contracts\View\View;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Source\UseCases\Requests\WebPresenter;
......@@ -28,7 +28,7 @@ class MeController extends Controller
/**
* @param \Source\UseCases\Users\UpdateUser\UpdateCurrentUser $userUpdate
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Validation\ValidationException
* @throws \Source\Exceptions\AuthorizationException
*/
......@@ -85,7 +85,7 @@ class MeController extends Controller
$doorsPresenter = new MePresenter();
$doors->query([], $doorsPresenter);
$doors->query([], true, $doorsPresenter);
return view('access')
->with('doorId', $this->request->input('door_id'))
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddDoorsAdministrativeColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('doors', static function(Blueprint $table) {
$table->boolean('public')->default(true);
$table->string('notes')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('doors', static function(Blueprint $table) {
$table->dropColumn('public');
$table->dropColumn('notes');
});
}
}
......@@ -19,4 +19,29 @@
value="{{ old('location') }}"
placeholder="CSP316">
</div>
<div class="form-check">
@if (old('public'))
<input type="checkbox" name="public" id="public" aria-describedby="publicHelp"
checked="{{ old('public') }}">
@else
<input type="checkbox" name="public" id="public" aria-describedby="publicHelp">
@endif
<label for="public">
Public
</label>
<small id="publicHelp" class="form-text text-muted">
If public, it will be visible to all users.
</small>
</div>
<br>
<div class="form-group">
<label for="notes">
Administrative Notes
</label>
<span class="text-muted float-right">required</span>
<textarea name="notes" class="form-control" id="notes" placeholder="Anything useful can go here.">{{ old('notes') }}</textarea>
<small id="notesHelp" class="form-text text-muted">
This is a place only for administrative notes that only administrators can see.
</small>
</div>
@endsection
......@@ -49,11 +49,17 @@ class Door
protected ?string $version;
protected bool $public;
protected ?string $notes;
/**
* @param int $id
* @param string $location
* @param string $name
* @param \Source\Entities\HashedSearchable $token
* @param bool $public
* @param string|null $notes
* @param string|null $version
* @param Carbon|null $createdAt
* @param Carbon|null $updatedAt
......@@ -64,6 +70,8 @@ class Door
string $location,
string $name,
HashedSearchable $token,
bool $public = false,
?string $notes = null,
?string $version = null,
?Carbon $createdAt = null,
?Carbon $updatedAt = null,
......@@ -77,6 +85,8 @@ class Door
$this->updatedAt = $updatedAt;
$this->lastSeenAt = $lastSeenAt;
$this->version = $version;
$this->public = $public;
$this->notes = $notes;
}
/**
......@@ -205,4 +215,20 @@ class Door
{
$this->version = $version;
}
/**
* @return bool
*/
public function isPublic(): bool
{
return $this->public;
}
/**
* @return string|null
*/
public function getNotes(): ?string
{
return $this->notes;
}
}
......@@ -4,7 +4,6 @@
namespace Source\Entities;
use Carbon\Carbon;
use JetBrains\PhpStorm\Pure;
class SplittableDate
{
......@@ -74,7 +73,7 @@ class SplittableDate
* @param \Source\Entities\SplittableDate $s
* @return bool
*/
#[Pure] public function engulfs(SplittableDate $s): bool
public function engulfs(SplittableDate $s): bool
{
return $this->getBegin() <= $s->getBegin() && $this->getEnd() >= $s->getEnd();
}
......@@ -83,7 +82,7 @@ class SplittableDate
* @param \Source\Entities\SplittableDate $s
* @return bool
*/
#[Pure] public function hasNoOverlapWith(SplittableDate $s): bool
public function hasNoOverlapWith(SplittableDate $s): bool
{
return $this->getBegin() >= $s->getEnd() || $this->getEnd() <= $s->getBegin();
}
......
......@@ -30,6 +30,8 @@ class DatabaseDoorsRepository implements DoorsRepository
$dbDoor->setAttribute('api_token', $door->getToken()->getHash());
$dbDoor->setAttribute('version', $door->getVersion());
$dbDoor->setAttribute('last_seen_at', $door->getLastSeenAt());
$dbDoor->setAttribute('public', $door->isPublic());
$dbDoor->setAttribute('notes', $door->getNotes());
try {
$dbDoor->save();
......@@ -63,6 +65,8 @@ class DatabaseDoorsRepository implements DoorsRepository
$door->getAttribute('location'),
$door->getAttribute('name'),
new HashedSearchable($door->getAttribute('api_token')),
$door->getAttribute('public'),
$door->getAttribute('notes'),
$door->getAttribute('version'),
$door->getAttribute('created_at'),
$door->getAttribute('updated_at'),
......
......@@ -27,6 +27,8 @@ class LocalDoorsRepository extends InMemoryDoorsRepository
'The Amazon',
'chicken izta door',
HashedSearchable::hash($salt, 'door_1_api_token'),
true,
'Administrative note for this door',
'door version here',
null,
null,
......@@ -46,6 +48,8 @@ class LocalDoorsRepository extends InMemoryDoorsRepository
'Bat Cave',
'Bruce\' lair',
HashedSearchable::hash($salt, 'door_2_api_token'),
false,
'Mac address: 00:1A:C2:7B:00:47. Hidden door.',
null,
null,
null,
......
......@@ -44,6 +44,7 @@ QUERY;
/**
* @inheritDoc
* @throws \Exception
*/
public function getEntryFailurePercentages(Carbon $start, Carbon $end, int $limit): array
{
......@@ -52,6 +53,8 @@ select S.id,
S.location,
S.name,
S.api_token,
S.public,
S.notes,
S.version,
S.last_seen_at,
S.created_at,
......@@ -64,6 +67,8 @@ FROM (
D.location,
D.name,
D.api_token,
D.public,
D.notes,
D.version,
D.last_seen_at,
D.created_at,
......@@ -75,6 +80,8 @@ FROM (
SELECT D.id,
D.location,
D.name,
D.public,
D.notes,
D.api_token,
D.version,
D.last_seen_at,
......@@ -113,6 +120,8 @@ QUERY;
$percentage->location,
$percentage->name,
new HashedSearchable($percentage->api_token),
$percentage->public,
$percentage->notes,
$percentage->version,
self::liberalCastToCarbon($percentage->created_at),
self::liberalCastToCarbon($percentage->updated_at),
......@@ -125,6 +134,7 @@ QUERY;
/**
* @inheritDoc
* @throws \Exception
*/
public function mostUsedDoors(Carbon $start, Carbon $end, int $limit): array
{
......@@ -133,6 +143,8 @@ select S.id,
S.location,
S.name,
S.api_token,
S.public,
S.notes,
S.version,
S.last_seen_at,
S.created_at,
......@@ -143,6 +155,8 @@ FROM (
D.location,
D.name,
D.api_token,
D.public,
D.notes,
D.version,
D.last_seen_at,
D.created_at,
......@@ -176,6 +190,8 @@ QUERY;
$usage->location,
$usage->name,
new HashedSearchable($usage->api_token),
$usage->public,
$usage->notes,
$usage->version,
self::liberalCastToCarbon($usage->created_at),
self::liberalCastToCarbon($usage->updated_at),
......
......@@ -32,7 +32,6 @@ trait Paginates
$currentPage,
['path' => config('app.url') . '/' . $request->path()],
);
url();
$paginator->appends($appends);
......
......@@ -104,6 +104,8 @@ abstract class BasePresenter
'name' => $door->getName(),
'location' => $door->getLocation(),
'version' => $door->getVersion() ?? 'Unknown',
'public' => $door->isPublic(),
'notes' => $door->getNotes(),
'created_at' => self::formatDateTime($door->getCreatedAt()),
'updated_at' => self::formatDateTime($door->getUpdatedAt()),
'last_seen_at' => self::formatDateTime($door->getLastSeenAt()),
......
......@@ -19,12 +19,15 @@ class TranslationPresenter extends BasePresenter implements Presenter
}
$this->viewModel = new Door();
$this->viewModel->id = $door->getId();
$this->viewModel->name = $door->getName();
$this->viewModel->location = $door->getLocation();
$this->viewModel->created_at = $door->getCreatedAt();
$this->viewModel->updated_at = $door->getUpdatedAt();
$this->viewModel->api_token = $door->getToken()->getHash();
$this->viewModel->setAttribute('id', $door->getId());
$this->viewModel->setAttribute('name', $door->getName());
$this->viewModel->setAttribute('location', $door->getLocation());
$this->viewModel->setAttribute('created_at', $door->getCreatedAt());
$this->viewModel->setAttribute('updated_at', $door->getUpdatedAt());
$this->viewModel->setAttribute('public', $door->isPublic());
$this->viewModel->setAttribute('notes', $door->getNotes());
$this->viewModel->setAttribute('api_token', $door->getToken()->getHash());
$this->viewModel->setAttribute('last_seen_at', $door->getLastSeenAt());
}
/** @inheritDoc */
......
......@@ -3,17 +3,23 @@
namespace Source\UseCases\Doors\CreateDoor;
use Source\Entities\Door;
use Source\Sanitize\CastsTo;
use Source\Entities\HashedSearchable;
use Source\Gateways\Doors\DoorsRepository;
use Source\Gateways\Tokens\TokensRepository;
class CreateDoor implements CreateDoorUseCase
{
use CastsTo;
/**
* @var \Source\Gateways\Doors\DoorsRepository
*/
protected DoorsRepository $doors;
/**
* @var \Source\Gateways\Tokens\TokensRepository
*
/**
* @var \Source\Gateways\Tokens\TokensRepository
*/
......@@ -44,7 +50,9 @@ class CreateDoor implements CreateDoorUseCase
0,
$attributes['location'],
$attributes['name'],
HashedSearchable::hash($this->salt, $token)
HashedSearchable::hash($this->salt, $token),
$attributes['public'] ?? false,
$attributes['notes'] ?? null
);
$door = $this->doors->create($door);
......
......@@ -11,6 +11,9 @@ interface CreateDoorUseCase
* Required attributes:
* location
* name
* public
* Optional Attributes
* notes
*
* @param array $attributes
* @param Presenter $presenter
......
......@@ -47,6 +47,8 @@ class GenerateDoorToken implements GenerateDoorTokenUseCase
$door->getLocation(),
$door->getName(),
HashedSearchable::hash($this->salt, $token),