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

Add Form Request and Tests for Update Asset API Method #14458

Open
wants to merge 40 commits into
base: snipeit_v7_laravel10
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c025e25
just the basics and notes, pushing to keep track
spencerrlongg Feb 26, 2024
eac0186
not all working, but pushing to work on something else
spencerrlongg Mar 5, 2024
b239b3a
some good progress, lots of testing needs to be done on the new inclu…
spencerrlongg Mar 9, 2024
c8341d9
aha, got it working.
spencerrlongg Mar 9, 2024
eb8f1dd
some cleanup
spencerrlongg Mar 9, 2024
6732b66
some cool progress, but something with unique not working
spencerrlongg Mar 12, 2024
04d7884
some more testing stuff
spencerrlongg Mar 13, 2024
8962ced
push to switch branches
spencerrlongg Mar 13, 2024
f01b205
some changes
spencerrlongg Mar 13, 2024
f6ab0f8
lots of cleanup to do, but this DOES work
spencerrlongg Mar 13, 2024
86ab880
buncha progress
spencerrlongg Mar 19, 2024
c0110e7
some more tests and refinement
spencerrlongg Mar 20, 2024
1e810d2
most tests now passing, still one broken
spencerrlongg Mar 20, 2024
0ffceb9
some notes and a little progress
spencerrlongg Mar 20, 2024
b4b4927
Merge branch 'develop' into bug/sc-24884
spencerrlongg Mar 20, 2024
e1addc5
oops, typo from conflict resolve
spencerrlongg Mar 20, 2024
fdf0be0
all tests passing
spencerrlongg Mar 20, 2024
c155e4a
new test for not found assets
spencerrlongg Mar 20, 2024
d18aa1d
some more cleanup + tests
spencerrlongg Mar 20, 2024
e3e01e0
final cleanup
spencerrlongg Mar 20, 2024
e7b9903
delete some extra lines
spencerrlongg Mar 20, 2024
8cc1397
rm a couple unnecessary
spencerrlongg Mar 20, 2024
be282dd
resolve a couple issues
spencerrlongg Mar 21, 2024
60ca634
remove interactswithsettings
spencerrlongg Mar 21, 2024
39c15b2
reformat array
spencerrlongg Mar 27, 2024
013463a
Merge branch 'develop' into bug/sc-24884
spencerrlongg Mar 27, 2024
701411c
get rid of a couple unnecessary changes
spencerrlongg Mar 27, 2024
1d4a7a7
added audit dates
spencerrlongg Mar 27, 2024
cec84b8
fixed last audit date + test
spencerrlongg Mar 27, 2024
4ab75c1
Merge branch 'develop' into bug/sc-24884
spencerrlongg Apr 3, 2024
0a90df2
alright conflicts resolved
spencerrlongg Apr 3, 2024
53ccd19
Merge branch 'develop' into bug/sc-24884
spencerrlongg Apr 23, 2024
b11c900
fix bracket, + overwrite required rulesets
spencerrlongg Apr 23, 2024
107f8db
another option
spencerrlongg Apr 23, 2024
8696a42
another option
spencerrlongg Apr 23, 2024
03f091a
ammended note, got real rule in there
spencerrlongg Apr 25, 2024
97a6152
update comment per @uberbrady
spencerrlongg Apr 25, 2024
8b3ff5a
Handle potentially deleted admin users in license export
snipe May 23, 2024
cdb1140
Merge branch 'develop' into bug/sc-24884
spencerrlongg May 23, 2024
8ce577d
adds @snipe's rules for undeleted assigned targets
spencerrlongg May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
98 changes: 46 additions & 52 deletions app/Http/Controllers/Api/AssetsController.php
Expand Up @@ -4,6 +4,7 @@

use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
Expand Down Expand Up @@ -659,37 +660,35 @@ public function store(StoreAssetRequest $request): JsonResponse
* Accepts a POST request to update an asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param \App\Http\Requests\ImageUploadRequest $request
* @since [v4.0]
* @return \Illuminate\Http\JsonResponse
*/
public function update(ImageUploadRequest $request, $id)
public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse
spencerrlongg marked this conversation as resolved.
Show resolved Hide resolved
{
$this->authorize('update', Asset::class);

if ($asset = Asset::find($id)) {
$asset->fill($request->all());
$asset->fill($request->validated());

($request->filled('model_id')) ?
$asset->model()->associate(AssetModel::find($request->get('model_id'))) : null;
($request->filled('rtd_location_id')) ?
$asset->location_id = $request->get('rtd_location_id') : '';
($request->filled('company_id')) ?
$asset->company_id = Company::getIdForCurrentUser($request->get('company_id')) : '';
if ($request->has('model_id')) {
$asset->model()->associate(AssetModel::find($request->validated()['model_id']));
}
if ($request->has('company_id')) {
$asset->company_id = Company::getIdForCurrentUser($request->validated()['company_id']);
}
if ($request->has('rtd_location_id') && !$request->has('location_id')) {
$asset->location_id = $request->validated()['rtd_location_id'];
}
if ($request->input('last_audit_date')) {
$asset->last_audit_date = Carbon::parse($request->input('last_audit_date'))->startOfDay()->format('Y-m-d H:i:s');
}

($request->filled('rtd_location_id')) ?
$asset->location_id = $request->get('rtd_location_id') : null;
/**
* this is here just legacy reasons. Api\AssetController
* used image_source once to allow encoded image uploads.
*/
if ($request->has('image_source')) {
$request->offsetSet('image', $request->offsetGet('image_source'));
}

/**
* this is here just legacy reasons. Api\AssetController
* used image_source once to allow encoded image uploads.
*/
if ($request->has('image_source')) {
$request->offsetSet('image', $request->offsetGet('image_source'));
}

$asset = $request->handleImages($asset);
$model = AssetModel::find($asset->model_id);
$asset = $request->handleImages($asset);
$model = $asset->model;

// Update custom fields
$problems_updating_encrypted_custom_fields = false;
Expand All @@ -716,38 +715,33 @@ public function update(ImageUploadRequest $request, $id)
}
}

if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
$location = $target->location_id;
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) {
$location = $target->location_id;
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
->update(['location_id' => $target->location_id]);
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) {
$location = $target->id;
}

if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
$location = $target->location_id;
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) {
$location = $target->location_id;

Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $id)
->update(['location_id' => $target->location_id]);
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) {
$location = $target->id;
}

if (isset($target)) {
$asset->checkOut($target, Auth::user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', e($request->get('name')), $location);
}

if ($asset->image) {
$asset->image = $asset->getImageUrl();
}
if (isset($target)) {
$asset->checkOut($target, Auth::user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update',
e($request->get('name')), $location);
}

if ($problems_updating_encrypted_custom_fields) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
} else {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
}
if ($asset->image) {
$asset->image = $asset->getImageUrl();
}

return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
if ($problems_updating_encrypted_custom_fields) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
} else {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
}
}

return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
}


Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Licenses/LicensesController.php
Expand Up @@ -360,7 +360,7 @@ public function getExportLicensesCsv()
$license->order_number,
$license->free_seat_count,
$license->seats,
$license->adminuser->present()->fullName(),
($license->adminuser ? $license->adminuser->present()->fullName() : trans('admin/reports/general.deleted_user')),
$license->depreciation ? $license->depreciation->name: '',
$license->updated_at,
$license->deleted_at,
Expand Down
1 change: 1 addition & 0 deletions app/Http/Kernel.php
Expand Up @@ -48,6 +48,7 @@ class Kernel extends HttpKernel

'api' => [
'auth:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
spencerrlongg marked this conversation as resolved.
Show resolved Hide resolved
],
];

Expand Down
59 changes: 59 additions & 0 deletions app/Http/Requests/UpdateAssetRequest.php
@@ -0,0 +1,59 @@
<?php

namespace App\Http\Requests;

use App\Models\Asset;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;

class UpdateAssetRequest extends ImageUploadRequest
spencerrlongg marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Gate::allows('update', new Asset);
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = array_merge(
parent::rules(),
spencerrlongg marked this conversation as resolved.
Show resolved Hide resolved
(new Asset)->getRules(),
// this is to overwrite rulesets that include required, and rewrite unique_undeleted
[
'model_id' => ['integer', 'exists:models,id,deleted_at,NULL', 'not_array'],
'status_id' => ['integer', 'exists:status_labels,id'],
'asset_tag' => [
'min:1', 'max:255', 'not_array',
Rule::unique('assets', 'asset_tag')->ignore($this->asset)->withoutTrashed()
],
],
);

// OR

$rules2 = array_merge(
parent::rules(),
// collects rules, 'rejects' required rules not a fan of this approach, feels inflexible
// what if we decide something _is_ required, etc, it could get complicated and harder to read than the above
collect((new Asset)->getRules())->map(function ($rules) {
return collect($rules)->reject(function ($rule) {
return $rule === 'required';
})->reject(function ($rule) {
return $rule === 'unique_undeleted:assets,asset_tag';
})->values()->all();
})->all(),
);

return $rules2;
}
}
55 changes: 29 additions & 26 deletions app/Models/Asset.php
Expand Up @@ -89,35 +89,38 @@ public function declinedCheckout(User $declinedBy, $signature)
];

protected $rules = [
'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array',
'status_id' => 'required|integer|exists:status_labels,id',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array',
'name' => 'nullable|max:255',
'company_id' => 'nullable|integer|exists:companies,id',
'warranty_months' => 'nullable|numeric|digits_between:0,240',
'last_checkout' => 'nullable|date_format:Y-m-d H:i:s',
'last_checkin' => 'nullable|date_format:Y-m-d H:i:s',
'expected_checkin' => 'nullable|date',
'last_audit_date' => 'nullable|date_format:Y-m-d H:i:s',
// 'next_audit_date' => 'nullable|date|after:last_audit_date',
'next_audit_date' => 'nullable|date',
'location_id' => 'nullable|exists:locations,id',
'rtd_location_id' => 'nullable|exists:locations,id',
'purchase_date' => 'nullable|date|date_format:Y-m-d',
'serial' => 'nullable|unique_undeleted:assets,serial',
'purchase_cost' => 'nullable|numeric|gte:0',
'supplier_id' => 'nullable|exists:suppliers,id',
'asset_eol_date' => 'nullable|date',
'eol_explicit' => 'nullable|boolean',
'byod' => 'nullable|boolean',
'order_number' => 'nullable|string|max:191',
'notes' => 'nullable|string|max:65535',
'assigned_to' => 'nullable|integer',
'requestable' => 'nullable|boolean',
'model_id' => ['required', 'integer', 'exists:models,id,deleted_at,NULL', 'not_array'],
'status_id' => ['required', 'integer', 'exists:status_labels,id'],
'asset_tag' => ['required', 'min:1', 'max:255', 'unique_undeleted:assets,asset_tag', 'not_array'],
'name' => ['nullable', 'max:255'],
'company_id' => ['nullable', 'integer', 'exists:companies,id'],
'warranty_months' => ['nullable', 'numeric', 'digits_between:0,240'],
'last_checkout' => ['nullable', 'date_format:Y-m-d H:i:s'],
'last_checkin' => ['nullable', 'date_format:Y-m-d H:i:s'],
'expected_checkin' => ['nullable', 'date'],
'last_audit_date' => ['nullable', 'date_format:Y-m-d H:i:s'],
'next_audit_date' => ['nullable', 'date'],
//'after:last_audit_date'],
'location_id' => ['nullable', 'exists:locations,id'],
'rtd_location_id' => ['nullable', 'exists:locations,id'],
'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'],
'serial' => ['nullable', 'unique_undeleted:assets,serial'],
'purchase_cost' => ['nullable', 'numeric', 'gte:0'],
'supplier_id' => ['nullable', 'exists:suppliers,id'],
'asset_eol_date' => ['nullable', 'date'],
'eol_explicit' => ['nullable', 'boolean'],
'byod' => ['nullable', 'boolean'],
'order_number' => ['nullable', 'string', 'max:191'],
'notes' => ['nullable', 'string', 'max:65535'],
'assigned_to' => ['nullable', 'integer'],
'requestable' => ['nullable', 'boolean'],
'assigned_user' => ['nullable', 'exists:users,id,deleted_at,NULL'],
'assigned_location' => ['nullable', 'exists:locations,id,deleted_at,NULL'],
'assigned_asset' => ['nullable', 'exists:assets,id,deleted_at,NULL']
];


/**
/**
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can not figure out how to get rid of this, sorry 🙄

* The attributes that are mass assignable.
*
* @var array
Expand Down
1 change: 0 additions & 1 deletion app/Providers/ValidationServiceProvider.php
Expand Up @@ -66,7 +66,6 @@ public function boot()
* `unique_undeleted:table,fieldname` in your rules out of the box
*/
Validator::extend('unique_undeleted', function ($attribute, $value, $parameters, $validator) {

if (count($parameters)) {

// This is a bit of a shim, but serial doesn't have any other rules around it other than that it's nullable
Expand Down
13 changes: 12 additions & 1 deletion database/factories/AssetFactory.php
Expand Up @@ -48,6 +48,7 @@ public function definition()
'assigned_type' => null,
'next_audit_date' => null,
'last_checkout' => null,
'asset_eol_date' => null
];
}

Expand Down Expand Up @@ -354,6 +355,17 @@ public function nonrequestable()
return $this->state(['requestable' => false]);
}

public function noPurchaseOrEolDate()
{
return $this->afterCreating(function (Asset $asset) {
$asset->update([
'purchase_date' => null,
'asset_eol_date' => null
]);
});
}


public function hasEncryptedCustomField(CustomField $field = null)
{
return $this->state(function () use ($field) {
Expand All @@ -372,7 +384,6 @@ public function hasMultipleCustomFields(array $fields = null): self
});
}


/**
* This allows bypassing model level validation if you want to purposefully
* create an asset in an invalid state. Validation is turned back on
Expand Down
5 changes: 5 additions & 0 deletions database/factories/LocationFactory.php
Expand Up @@ -25,4 +25,9 @@ public function definition()
'image' => rand(1, 9).'.jpg',
];
}

public function deleted(): self
{
return $this->state(['deleted_at' => $this->faker->dateTime()]);
}
}
5 changes: 5 additions & 0 deletions database/factories/UserFactory.php
Expand Up @@ -299,4 +299,9 @@ private function appendPermission(array $permission)
];
});
}

public function deleted(): self
{
return $this->state(['deleted_at' => $this->faker->dateTime()]);
}
}
11 changes: 5 additions & 6 deletions routes/api.php
@@ -1,7 +1,6 @@
<?php

use App\Http\Controllers\Api;
// use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;


Expand Down Expand Up @@ -547,20 +546,20 @@

});




// pulling this out of resource route group to begin normalizing for route-model binding.
// this would probably keep working with the resource route group, but the general practice is for
// the model name to be the parameter - and i think it's a good differentiation in the code while we convert the others.
Route::patch('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.update');
spencerrlongg marked this conversation as resolved.
Show resolved Hide resolved

Route::resource('hardware',
Api\AssetsController::class,
['names' => [
'index' => 'api.assets.index',
'show' => 'api.assets.show',
'update' => 'api.assets.update',
'store' => 'api.assets.store',
'destroy' => 'api.assets.destroy',
],
'except' => ['create', 'edit'],
'except' => ['create', 'edit', 'update'],
'parameters' => ['asset' => 'asset_id'],
]
); // end assets API routes
Expand Down