From d60dc75f996e641c4161c78d7279dd7b8cd71ecf Mon Sep 17 00:00:00 2001 From: Adrian Hopek Date: Thu, 21 Apr 2022 08:16:31 +0200 Subject: [PATCH] #118 - keys (#128) * #118 - wip * #118 - keys * #118 - fix * #118 - fix menu * #118 - fix to policies and added translations * #118 - wip * #118 - tests * #118 - fix * #118 - fix * #118 - fix * Update resources/lang/pl.json Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com> * #118 - cr fix Co-authored-by: EwelinaLasowy Co-authored-by: Ewelina Lasowy <56546832+EwelinaLasowy@users.noreply.github.com> --- .../Providers/AuthServiceProvider.php | 3 + app/Domain/Policies/KeyPolicy.php | 26 ++ app/Eloquent/Models/Key.php | 31 ++ app/Eloquent/Models/User.php | 5 + .../Http/Controllers/KeysController.php | 104 ++++++ .../Http/Requests/GiveKeyRequest.php | 23 ++ .../Http/Resources/KeyResource.php | 25 ++ database/factories/KeyFactory.php | 21 ++ .../2022_04_12_152528_create_keys_table.php | 26 ++ database/seeders/DatabaseSeeder.php | 7 + database/seeders/DemoSeeder.php | 7 + resources/js/Pages/Keys.vue | 351 ++++++++++++++++++ resources/js/Shared/MainMenu.vue | 10 +- resources/lang/pl.json | 6 +- routes/web.php | 9 +- tests/Feature/KeyTest.php | 118 ++++++ 16 files changed, 769 insertions(+), 3 deletions(-) create mode 100644 app/Domain/Policies/KeyPolicy.php create mode 100644 app/Eloquent/Models/Key.php create mode 100644 app/Infrastructure/Http/Controllers/KeysController.php create mode 100644 app/Infrastructure/Http/Requests/GiveKeyRequest.php create mode 100644 app/Infrastructure/Http/Resources/KeyResource.php create mode 100644 database/factories/KeyFactory.php create mode 100644 database/migrations/2022_04_12_152528_create_keys_table.php create mode 100644 resources/js/Pages/Keys.vue create mode 100644 tests/Feature/KeyTest.php diff --git a/app/Architecture/Providers/AuthServiceProvider.php b/app/Architecture/Providers/AuthServiceProvider.php index afa2b3b..cb5b112 100644 --- a/app/Architecture/Providers/AuthServiceProvider.php +++ b/app/Architecture/Providers/AuthServiceProvider.php @@ -7,7 +7,9 @@ namespace Toby\Architecture\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; use Toby\Domain\Enums\Role; +use Toby\Domain\Policies\KeyPolicy; use Toby\Domain\Policies\VacationRequestPolicy; +use Toby\Eloquent\Models\Key; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationRequest; @@ -15,6 +17,7 @@ class AuthServiceProvider extends ServiceProvider { protected $policies = [ VacationRequest::class => VacationRequestPolicy::class, + Key::class => KeyPolicy::class, ]; public function boot(): void diff --git a/app/Domain/Policies/KeyPolicy.php b/app/Domain/Policies/KeyPolicy.php new file mode 100644 index 0000000..cb62986 --- /dev/null +++ b/app/Domain/Policies/KeyPolicy.php @@ -0,0 +1,26 @@ +role === Role::AdministrativeApprover; + } + + public function give(User $user, Key $key): bool + { + if ($key->user()->is($user)) { + return true; + } + + return $user->role === Role::AdministrativeApprover; + } +} diff --git a/app/Eloquent/Models/Key.php b/app/Eloquent/Models/Key.php new file mode 100644 index 0000000..cb7ee23 --- /dev/null +++ b/app/Eloquent/Models/Key.php @@ -0,0 +1,31 @@ +belongsTo(User::class); + } + + protected static function newFactory(): KeyFactory + { + return KeyFactory::new(); + } +} diff --git a/app/Eloquent/Models/User.php b/app/Eloquent/Models/User.php index d167dd5..4bf2891 100644 --- a/app/Eloquent/Models/User.php +++ b/app/Eloquent/Models/User.php @@ -74,6 +74,11 @@ class User extends Authenticatable return $this->hasMany(Vacation::class); } + public function keys(): HasMany + { + return $this->hasMany(Key::class); + } + public function hasRole(Role $role): bool { return $this->role === $role; diff --git a/app/Infrastructure/Http/Controllers/KeysController.php b/app/Infrastructure/Http/Controllers/KeysController.php new file mode 100644 index 0000000..096e540 --- /dev/null +++ b/app/Infrastructure/Http/Controllers/KeysController.php @@ -0,0 +1,104 @@ +oldest() + ->get(); + + $users = User::query() + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(); + + return inertia("Keys", [ + "keys" => KeyResource::collection($keys), + "users" => SimpleUserResource::collection($users), + "can" => [ + "manageKeys" => $request->user()->can("manage", Key::class), + ], + ]); + } + + /** + * @throws AuthorizationException + */ + public function store(Request $request): RedirectResponse + { + $this->authorize("manage", Key::class); + + $key = $request->user()->keys()->create(); + + return redirect() + ->back() + ->with("success", __("Key no :number has been created.", [ + "number" => $key->id, + ])); + } + + public function take(Key $key, Request $request): RedirectResponse + { + $previousUser = $key->user; + + $key->user()->associate($request->user()); + + $key->save(); + + return redirect() + ->back() + ->with("success", __("Key no :number has been taken from :user.", [ + "number" => $key->id, + "user" => $previousUser->profile->full_name, + ])); + } + + /** + * @throws AuthorizationException + */ + public function give(Key $key, GiveKeyRequest $request): RedirectResponse + { + $this->authorize("give", $key); + + $recipient = $request->recipient(); + + $key->user()->associate($recipient); + + $key->save(); + + return redirect() + ->back() + ->with("success", __("Key no :number has been given to :user.", [ + "number" => $key->id, + "user" => $recipient->profile->full_name, + ])); + } + + public function destroy(Key $key): RedirectResponse + { + $this->authorize("manage", Key::class); + + $key->delete(); + + return redirect() + ->back() + ->with("success", __("Key no :number has been deleted.", [ + "number" => $key->id, + ])); + } +} diff --git a/app/Infrastructure/Http/Requests/GiveKeyRequest.php b/app/Infrastructure/Http/Requests/GiveKeyRequest.php new file mode 100644 index 0000000..7505821 --- /dev/null +++ b/app/Infrastructure/Http/Requests/GiveKeyRequest.php @@ -0,0 +1,23 @@ + ["required", "exists:users,id"], + ]; + } + + public function recipient(): User + { + return User::find($this->get("user")); + } +} diff --git a/app/Infrastructure/Http/Resources/KeyResource.php b/app/Infrastructure/Http/Resources/KeyResource.php new file mode 100644 index 0000000..7eedbf7 --- /dev/null +++ b/app/Infrastructure/Http/Resources/KeyResource.php @@ -0,0 +1,25 @@ + $this->id, + "user" => new SimpleUserResource($this->user), + "updatedAt" => $this->updated_at->toDisplayString(), + "can" => [ + "give" => $request->user()->can("give", $this->resource), + "take" => !$this->user()->is($request->user()), + ], + ]; + } +} diff --git a/database/factories/KeyFactory.php b/database/factories/KeyFactory.php new file mode 100644 index 0000000..52e1694 --- /dev/null +++ b/database/factories/KeyFactory.php @@ -0,0 +1,21 @@ + User::factory(), + ]; + } +} diff --git a/database/migrations/2022_04_12_152528_create_keys_table.php b/database/migrations/2022_04_12_152528_create_keys_table.php new file mode 100644 index 0000000..1d3ee72 --- /dev/null +++ b/database/migrations/2022_04_12_152528_create_keys_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignIdFor(User::class) + ->constrained() + ->cascadeOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("keys"); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 581ecfd..f5e0e51 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -8,6 +8,7 @@ use Illuminate\Database\Seeder; use Illuminate\Support\Carbon; use Toby\Domain\PolishHolidaysRetriever; use Toby\Domain\VacationDaysCalculator; +use Toby\Eloquent\Models\Key; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationLimit; use Toby\Eloquent\Models\VacationRequest; @@ -85,5 +86,11 @@ class DatabaseSeeder extends Seeder }) ->create(); } + + foreach ($users as $user) { + Key::factory() + ->for($user) + ->create(); + } } } diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 53454e2..41470ba 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -19,6 +19,7 @@ use Toby\Domain\States\VacationRequest\Rejected; use Toby\Domain\States\VacationRequest\WaitingForAdministrative; use Toby\Domain\States\VacationRequest\WaitingForTechnical; use Toby\Domain\VacationDaysCalculator; +use Toby\Eloquent\Models\Key; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationLimit; use Toby\Eloquent\Models\VacationRequest; @@ -328,5 +329,11 @@ class DemoSeeder extends Seeder $vacationRequestRejected->state = new Rejected($vacationRequestRejected); $vacationRequestRejected->save(); + + foreach ($users as $user) { + Key::factory() + ->for($user) + ->create(); + } } } diff --git a/resources/js/Pages/Keys.vue b/resources/js/Pages/Keys.vue new file mode 100644 index 0000000..5c158fa --- /dev/null +++ b/resources/js/Pages/Keys.vue @@ -0,0 +1,351 @@ + + + diff --git a/resources/js/Shared/MainMenu.vue b/resources/js/Shared/MainMenu.vue index 7d059a2..dc7305e 100644 --- a/resources/js/Shared/MainMenu.vue +++ b/resources/js/Shared/MainMenu.vue @@ -294,6 +294,7 @@ import { CalendarIcon, DocumentTextIcon, AdjustmentsIcon, + KeyIcon, } from '@heroicons/vue/outline' import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/solid' @@ -351,7 +352,6 @@ const navigation = computed(() => can: props.auth.can.manageVacationLimits, }, { - name: 'Podsumowanie roczne', href: '/vacation/annual-summary', section: 'AnnualSummary', @@ -365,5 +365,13 @@ const navigation = computed(() => icon: UserGroupIcon, can: props.auth.can.manageUsers, }, + { + name: 'Klucze', + href: '/keys', + section: 'Keys', + icon: KeyIcon, + can: true, + }, + ].filter(item => item.can)) diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 955ae7f..d792d41 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -67,5 +67,9 @@ "Vacation request :title has been :status": "Wniosek :title został :status", "The vacation request :title from user :requester has been :status.": "Wniosek urlopowy :title użytkownika :requester został :status.", "Vacation request :title has been created on your behalf": "Wniosek urlopowy :title został utworzony w Twoim imieniu", - "The vacation request :title has been created correctly by user :creator on your behalf in the :appName.": "W systemie :appName został poprawnie utworzony wniosek urlopowy :title w Twoim imieniu przez użytkownika :creator." + "The vacation request :title has been created correctly by user :creator on your behalf in the :appName.": "W systemie :appName został poprawnie utworzony wniosek urlopowy :title w Twoim imieniu przez użytkownika :creator.", + "Key no :number has been created.": "Klucz nr :number został utworzony.", + "Key no :number has been deleted.": "Klucz nr :number został usunięty.", + "Key no :number has been taken from :user.": "Klucz nr :number został zabrany użytkownikowi :user.", + "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user." } diff --git a/routes/web.php b/routes/web.php index b67244b..f35e8f6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use Toby\Infrastructure\Http\Controllers\AnnualSummaryController; use Toby\Infrastructure\Http\Controllers\DashboardController; use Toby\Infrastructure\Http\Controllers\GoogleController; use Toby\Infrastructure\Http\Controllers\HolidayController; +use Toby\Infrastructure\Http\Controllers\KeysController; use Toby\Infrastructure\Http\Controllers\LogoutController; use Toby\Infrastructure\Http\Controllers\MonthlyUsageController; use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController; @@ -33,7 +34,13 @@ Route::middleware(["auth", TrackUserLastActivity::class])->group(function (): vo ->except("show") ->whereNumber("holiday"); - Route::post("year-periods/{yearPeriod}/select", SelectYearPeriodController::class) + Route::get("/keys", [KeysController::class, "index"]); + Route::post("/keys", [KeysController::class, "store"]); + Route::delete("/keys/{key}", [KeysController::class, "destroy"]); + Route::post("/keys/{key}/take", [KeysController::class, "take"]); + Route::post("/keys/{key}/give", [KeysController::class, "give"]); + + Route::post("/year-periods/{yearPeriod}/select", SelectYearPeriodController::class) ->whereNumber("yearPeriod") ->name("year-periods.select"); diff --git a/tests/Feature/KeyTest.php b/tests/Feature/KeyTest.php new file mode 100644 index 0000000..e9c3ddc --- /dev/null +++ b/tests/Feature/KeyTest.php @@ -0,0 +1,118 @@ +count(10)->create(); + $user = User::factory()->create(); + + $this->assertDatabaseCount("keys", 10); + + $this->actingAs($user) + ->get("/keys") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("Keys") + ->has("keys.data", 10), + ); + } + + public function testAdminCanCreateKey(): void + { + $admin = User::factory()->admin()->create(); + + $this->assertDatabaseMissing("keys", [ + "user_id" => $admin->id, + ]); + + $this->actingAs($admin) + ->post("/keys") + ->assertSessionHasNoErrors() + ->assertRedirect(); + + $this->assertDatabaseHas("keys", [ + "user_id" => $admin->id, + ]); + } + + public function testUserCannotCreateKey(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->post("/keys") + ->assertForbidden(); + } + + public function testAdminCanDeleteKey(): void + { + $admin = User::factory()->admin()->create(); + + $key = Key::factory()->create(); + + $this->actingAs($admin) + ->delete("/keys/{$key->id}") + ->assertRedirect(); + } + + public function testUserCanTakeKeyFromAnotherUser(): void + { + $user = User::factory()->create(); + $userWithKey = User::factory()->create(); + + $key = Key::factory()->for($userWithKey)->create(); + + $this->assertDatabaseHas("keys", [ + "id" => $key->id, + "user_id" => $userWithKey->id, + ]); + + $this->actingAs($user) + ->post("/keys/{$key->id}/take") + ->assertRedirect(); + + $this->assertDatabaseHas("keys", [ + "id" => $key->id, + "user_id" => $user->id, + ]); + } + + public function testUserCanGiveTheirKeyToAnotherUser(): void + { + $userWithKey = User::factory()->create(); + $user = User::factory()->create(); + + $key = Key::factory()->for($userWithKey)->create(); + + $this->assertDatabaseHas("keys", [ + "id" => $key->id, + "user_id" => $userWithKey->id, + ]); + + $this->actingAs($userWithKey) + ->post("/keys/{$key->id}/give", [ + "user" => $user->id, + ]) + ->assertSessionhasNoErrors() + ->assertRedirect(); + + $this->assertDatabaseHas("keys", [ + "id" => $key->id, + "user_id" => $user->id, + ]); + } +}