Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed accessories and consumables not being logged correctly during bulk check-in #15221

Merged
merged 33 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7161b64
Add failing test for accessories and consumables checkin
marcusmoore Jul 24, 2024
44dbbeb
Add accessory and consumable specific checkin methods
marcusmoore Jul 24, 2024
78a0417
Add another user into the mix
marcusmoore Aug 5, 2024
480e4f3
Improve readability
marcusmoore Aug 5, 2024
bfebcdc
Improve variable name
marcusmoore Aug 5, 2024
364775d
Improve readability
marcusmoore Aug 5, 2024
4ed3347
Add permission check
marcusmoore Aug 5, 2024
5ecdb7e
Add validation test
marcusmoore Aug 5, 2024
e3049ff
Remove comment
marcusmoore Aug 5, 2024
b06501d
Add assertions
marcusmoore Aug 5, 2024
67b3ab8
Add todo comments
marcusmoore Aug 5, 2024
8a206a6
Add assertion
marcusmoore Aug 5, 2024
5786ff5
Add assertion for success message
marcusmoore Aug 5, 2024
fc405d9
Assert redirected to correct place
marcusmoore Aug 5, 2024
5cd9dd4
Add assertion to ensure user cannot perform bulk actions on self
marcusmoore Aug 5, 2024
d1bb3ef
Scaffold two tests
marcusmoore Aug 5, 2024
16aa475
Implement test for assets
marcusmoore Aug 5, 2024
59b7d5d
Remove comment
marcusmoore Aug 5, 2024
1acd24f
Re-order helpers
marcusmoore Aug 5, 2024
a556932
Move test class
marcusmoore Aug 5, 2024
96fafa6
Improve readability
marcusmoore Aug 5, 2024
35e7a31
Implement test case
marcusmoore Aug 5, 2024
392d344
Remove code handled by ConsumableAssignment:: call above
marcusmoore Aug 5, 2024
1c664af
Remove todo outside of scope
marcusmoore Aug 5, 2024
01e4382
Formatting
marcusmoore Aug 5, 2024
17eccfc
Formatting
marcusmoore Aug 5, 2024
f2b78d1
Merge branch 'develop' into fixes/bulk-checkin-logging
marcusmoore Aug 6, 2024
94e00b8
Use new accessory checkout relationship
marcusmoore Aug 6, 2024
2a8ac81
Fix namespace
marcusmoore Aug 6, 2024
a220986
Switch to snake case
marcusmoore Aug 6, 2024
41437e4
Merge branch 'develop' into fixes/bulk-checkin-logging
marcusmoore Aug 7, 2024
d03baa4
Make diff cleaner
marcusmoore Aug 7, 2024
4a2d2f7
Improve variable name
marcusmoore Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions app/Http/Controllers/Users/BulkUsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Password;
Expand Down Expand Up @@ -219,20 +220,18 @@ public function destroy(Request $request)

$users = User::whereIn('id', $user_raw_array)->get();
$assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', User::class)->get();
$accessories = DB::table('accessories_checkout')->where('assigned_type', User::class)->whereIn('assigned_to', $user_raw_array)->get();
$accessoryUserRows = DB::table('accessories_checkout')->where('assigned_type', User::class)->whereIn('assigned_to', $user_raw_array)->get();
$licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get();
$consumables = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();
$consumableUserRows = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();

if ((($assets->count() > 0) && ((!$request->filled('status_id')) || ($request->input('status_id') == '')))) {
return redirect()->route('users.index')->with('error', 'No status selected');
}


$this->logItemCheckinAndDelete($assets, Asset::class);
$this->logItemCheckinAndDelete($accessories, Accessory::class);
$this->logAccessoriesCheckin($accessoryUserRows);
$this->logItemCheckinAndDelete($licenses, License::class);
$this->logItemCheckinAndDelete($consumables, Consumable::class);

$this->logConsumablesCheckin($consumableUserRows);

Asset::whereIn('id', $assets->pluck('id'))->update([
'status_id' => e(request('status_id')),
Expand All @@ -241,19 +240,14 @@ public function destroy(Request $request)
'expected_checkin' => null,
]);


LicenseSeat::whereIn('id', $licenses->pluck('id'))->update(['assigned_to' => null]);
ConsumableAssignment::whereIn('id', $consumables->pluck('id'))->delete();

ConsumableAssignment::whereIn('id', $consumableUserRows->pluck('id'))->delete();

foreach ($users as $user) {

$user->consumables()->sync([]);
$user->accessories()->sync([]);
if ($request->input('delete_user')=='1') {
$user->delete();
}

}

$msg = trans('general.bulk_checkin_success');
Expand All @@ -279,7 +273,7 @@ protected function logItemCheckinAndDelete($items, $itemType)
if ($itemType == License::class){
$item_id = $item->license_id;
}

$logAction->item_id = $item_id;
// We can't rely on get_class here because the licenses/accessories fetched above are not eloquent models, but simply arrays.
$logAction->item_type = $itemType;
Expand All @@ -291,6 +285,34 @@ protected function logItemCheckinAndDelete($items, $itemType)
}
}

private function logAccessoriesCheckin(Collection $accessoryUserRows): void
{
foreach ($accessoryUserRows as $accessoryUserRow) {
$logAction = new Actionlog();
$logAction->item_id = $accessoryUserRow->accessory_id;
$logAction->item_type = Accessory::class;
$logAction->target_id = $accessoryUserRow->assigned_to;
$logAction->target_type = User::class;
$logAction->user_id = Auth::id();
$logAction->note = 'Bulk checkin items';
$logAction->logaction('checkin from');
}
}

private function logConsumablesCheckin(Collection $consumableUserRows): void
{
foreach ($consumableUserRows as $consumableUserRow) {
$logAction = new Actionlog();
$logAction->item_id = $consumableUserRow->consumable_id;
$logAction->item_type = Consumable::class;
$logAction->target_id = $consumableUserRow->assigned_to;
$logAction->target_type = User::class;
$logAction->user_id = Auth::id();
$logAction->note = 'Bulk checkin items';
$logAction->logaction('checkin from');
}
}
Comment on lines +288 to +314
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not loving the duplication but there is enough difference that using explicit method names instead of passing a handful of parameters is cleaner I think


/**
* Save bulk-edited users
*
Expand Down
260 changes: 260 additions & 0 deletions tests/Feature/Users/Ui/BulkActions/BulkDeleteUsersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
<?php

namespace Tests\Feature\Users\Ui\BulkActions;

use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Consumable;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\Statuslabel;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Tests\TestCase;

class BulkDeleteUsersTest extends TestCase
{
public function test_requires_correct_permission()
{
$this->actingAs(User::factory()->create())
->post(route('users/bulksave'), [
'ids' => [
User::factory()->create()->id,
],
'status_id' => Statuslabel::factory()->create()->id,
])
->assertForbidden();
}

public function test_validation()
{
$user = User::factory()->create();
Asset::factory()->assignedToUser($user)->create();

$actor = $this->actingAs(User::factory()->editUsers()->create());

// "ids" required
$actor->post(route('users/bulksave'), [
// 'ids' => [
// $user->id,
// ],
'status_id' => Statuslabel::factory()->create()->id,
])->assertSessionHas('error')->assertRedirect();

// "status_id" needed when provided users have assets associated
$actor->post(route('users/bulksave'), [
'ids' => [
$user->id,
],
// 'status_id' => Statuslabel::factory()->create()->id,
])->assertSessionHas('error')->assertRedirect();
}

public function test_cannot_perform_bulk_actions_on_self()
{
$actor = User::factory()->editUsers()->create();

$this->actingAs($actor)
->post(route('users/bulksave'), [
'ids' => [
$actor->id,
],
'delete_user' => '1',
])
->assertRedirect(route('users.index'))
->assertSessionHas('success', trans('general.bulk_checkin_delete_success'));

$this->assertNotSoftDeleted($actor);
}

public function test_accessories_can_be_bulk_checked_in()
{
[$accessoryA, $accessoryB] = Accessory::factory()->count(2)->create();
[$userA, $userB, $userC] = User::factory()->count(3)->create();

// Add checkouts for multiple accessories to multiple users to get different ids in the mix
$this->attachAccessoryToUsers($accessoryA, [$userA, $userB, $userC]);
$this->attachAccessoryToUsers($accessoryB, [$userA, $userB]);

$this->actingAs(User::factory()->editUsers()->create())
->post(route('users/bulksave'), [
'ids' => [
$userA->id,
$userC->id,
],
'status_id' => Statuslabel::factory()->create()->id,
])
->assertRedirect(route('users.index'));

$this->assertTrue($userA->fresh()->accessories->isEmpty());
$this->assertTrue($userB->fresh()->accessories->isNotEmpty());
$this->assertTrue($userC->fresh()->accessories->isEmpty());

// These assertions check against a bug where the wrong value from
// accessories_users was being populated in action_logs.item_id.
$this->assertActionLogCheckInEntryFor($userA, $accessoryA);
$this->assertActionLogCheckInEntryFor($userA, $accessoryB);
$this->assertActionLogCheckInEntryFor($userC, $accessoryA);
}

public function test_assets_can_be_bulk_checked_in()
{
[$userA, $userB, $userC] = User::factory()->count(3)->create();

$assetForUserA = $this->assignAssetToUser($userA);
$lonelyAsset = $this->assignAssetToUser($userB);
$assetForUserC = $this->assignAssetToUser($userC);

$this->actingAs(User::factory()->editUsers()->create())
->post(route('users/bulksave'), [
'ids' => [
$userA->id,
$userC->id,
],
'status_id' => Statuslabel::factory()->create()->id,
])
->assertRedirect(route('users.index'));

$this->assertTrue($userA->fresh()->assets->isEmpty());
$this->assertTrue($userB->fresh()->assets->isNotEmpty());
$this->assertTrue($userC->fresh()->assets->isEmpty());

$this->assertActionLogCheckInEntryFor($userA, $assetForUserA);
$this->assertActionLogCheckInEntryFor($userC, $assetForUserC);
}

public function test_consumables_can_be_bulk_checked_in()
{
[$consumableA, $consumableB] = Consumable::factory()->count(2)->create();
[$userA, $userB, $userC] = User::factory()->count(3)->create();

// Add checkouts for multiple consumables to multiple users to get different ids in the mix
$this->attachConsumableToUsers($consumableA, [$userA, $userB, $userC]);
$this->attachConsumableToUsers($consumableB, [$userA, $userB]);

$this->actingAs(User::factory()->editUsers()->create())
->post(route('users/bulksave'), [
'ids' => [
$userA->id,
$userC->id,
],
'status_id' => Statuslabel::factory()->create()->id,
])
->assertRedirect(route('users.index'));

$this->assertTrue($userA->fresh()->consumables->isEmpty());
$this->assertTrue($userB->fresh()->consumables->isNotEmpty());
$this->assertTrue($userC->fresh()->consumables->isEmpty());

// These assertions check against a bug where the wrong value from
// consumables_users was being populated in action_logs.item_id.
$this->assertActionLogCheckInEntryFor($userA, $consumableA);
$this->assertActionLogCheckInEntryFor($userA, $consumableB);
$this->assertActionLogCheckInEntryFor($userC, $consumableA);
}

public function test_license_seats_can_be_bulk_checked_in()
{
[$userA, $userB, $userC] = User::factory()->count(3)->create();

$licenseSeatForUserA = LicenseSeat::factory()->assignedToUser($userA)->create();
$lonelyLicenseSeat = LicenseSeat::factory()->assignedToUser($userB)->create();
$licenseSeatForUserC = LicenseSeat::factory()->assignedToUser($userC)->create();

$this->actingAs(User::factory()->editUsers()->create())
->post(route('users/bulksave'), [
'ids' => [
$userA->id,
$userC->id,
],
])
->assertRedirect(route('users.index'))
->assertSessionHas('success', trans('general.bulk_checkin_success'));

$this->assertDatabaseMissing('license_seats', [
'license_id' => $licenseSeatForUserA->license->id,
'assigned_to' => $userA->id,
]);

$this->assertDatabaseMissing('license_seats', [
'license_id' => $licenseSeatForUserC->license->id,
'assigned_to' => $userC->id,
]);

// Slightly different from the other assertions since we use
// the license and not the license seat in this case.
$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkin from',
'target_id' => $userA->id,
'target_type' => User::class,
'note' => 'Bulk checkin items',
'item_type' => License::class,
'item_id' => $licenseSeatForUserA->license->id,
]);

$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkin from',
'target_id' => $userC->id,
'target_type' => User::class,
'note' => 'Bulk checkin items',
'item_type' => License::class,
'item_id' => $licenseSeatForUserC->license->id,
]);
}

public function test_users_can_be_deleted_in_bulk()
{
[$userA, $userB, $userC] = User::factory()->count(3)->create();

$this->actingAs(User::factory()->editUsers()->create())
->post(route('users/bulksave'), [
'ids' => [
$userA->id,
$userC->id,
],
'delete_user' => '1',
])
->assertRedirect(route('users.index'))
->assertSessionHas('success', trans('general.bulk_checkin_delete_success'));

$this->assertSoftDeleted($userA);
$this->assertNotSoftDeleted($userB);
$this->assertSoftDeleted($userC);
}

private function assignAssetToUser(User $user): Asset
{
return Asset::factory()->assignedToUser($user)->create();
}

private function attachAccessoryToUsers(Accessory $accessory, array $users): void
{
foreach ($users as $user) {
$accessoryCheckout = $accessory->checkouts()->make();
$accessoryCheckout->assignedTo()->associate($user);
$accessoryCheckout->save();
}
}

private function attachConsumableToUsers(Consumable $consumable, array $users): void
{
foreach ($users as $user) {
$consumable->users()->attach($consumable->id, [
'consumable_id' => $consumable->id,
'assigned_to' => $user->id,
]);
}
}

private function assertActionLogCheckInEntryFor(User $user, Model $model): void
{
$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkin from',
'target_id' => $user->id,
'target_type' => User::class,
'note' => 'Bulk checkin items',
'item_type' => get_class($model),
'item_id' => $model->id,
]);
}
}
Loading