From ad818cf886b7990ec2f98714760d0251f50c726c Mon Sep 17 00:00:00 2001 From: akemidx Date: Thu, 11 Jul 2024 17:13:47 -0400 Subject: [PATCH 01/38] missing translation --- resources/views/partials/bootstrap-table.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index e2e65cf912..08c751754a 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -432,7 +432,7 @@ if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && ((!row.asset_id) && (!row.assigned_to))) { return '{{ trans('general.checkout') }}'; } else { - return '{{ trans('general.checkin') }}'; + return '{{ trans('general.checkin') }}'; } } From eed253bd2fcaee8ebc8fb4c60f74366e3017f688 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 13 Jul 2024 15:15:18 +0100 Subject: [PATCH 02/38] Use app helped instead of facade Signed-off-by: snipe --- app/Http/Middleware/CheckLocale.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Middleware/CheckLocale.php b/app/Http/Middleware/CheckLocale.php index 412a423f22..68a9c48c34 100644 --- a/app/Http/Middleware/CheckLocale.php +++ b/app/Http/Middleware/CheckLocale.php @@ -45,7 +45,7 @@ class CheckLocale } - \App::setLocale(Helper::mapLegacyLocale($language)); + app()->setLocale(Helper::mapLegacyLocale($language)); return $next($request); } } From f91ad6b2db19a3fea75c823680c00fecb9a6ac40 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 13 Jul 2024 15:15:44 +0100 Subject: [PATCH 03/38] =?UTF-8?q?Use=20app()->getLocale()=20to=20determine?= =?UTF-8?q?=20imported=20user=E2=80=99s=20language?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: snipe --- app/Console/Commands/LdapSync.php | 1 + app/Models/Ldap.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index 1856bf10fc..c90c3c2a92 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -232,6 +232,7 @@ class LdapSync extends Command $item['department'] = $results[$i][$ldap_result_dept][0] ?? ''; $item['manager'] = $results[$i][$ldap_result_manager][0] ?? ''; $item['location'] = $results[$i][$ldap_result_location][0] ?? ''; + $item['locale'] = app()->getLocale(); // ONLY if you are using the "ldap_location" option *AND* you have an actual result if ($ldap_result_location && $item['location']) { diff --git a/app/Models/Ldap.php b/app/Models/Ldap.php index dd3920b56a..ecce46d82a 100644 --- a/app/Models/Ldap.php +++ b/app/Models/Ldap.php @@ -229,6 +229,7 @@ class Ldap extends Model $item['department'] = $ldapattributes[$ldap_result_dept][0] ?? ''; $item['manager'] = $ldapattributes[$ldap_result_manager][0] ?? ''; $item['location'] = $ldapattributes[$ldap_result_location][0] ?? ''; + $item['locale'] = app()->getLocale(); return $item; } @@ -239,7 +240,7 @@ class Ldap extends Model * @author [A. Gianotto] [] * @since [v3.0] * @param $ldapatttibutes - * @return array|bool + * @return User | bool */ public static function createUserFromLdap($ldapatttibutes, $password) { @@ -252,6 +253,7 @@ class Ldap extends Model $user->last_name = $item['lastname']; $user->username = $item['username']; $user->email = $item['email']; + $user->locale = $item['locale']; $user->password = $user->noPassword(); if (Setting::getSettings()->ldap_pw_sync == '1') { From 24e58d14558b32e98e160dc004696e1d3418fb83 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 13 Jul 2024 15:20:04 +0100 Subject: [PATCH 04/38] Do not update users who already exist since that locale may have been overirrden manually Signed-off-by: snipe --- app/Console/Commands/LdapSync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index c90c3c2a92..676d207c22 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -232,7 +232,6 @@ class LdapSync extends Command $item['department'] = $results[$i][$ldap_result_dept][0] ?? ''; $item['manager'] = $results[$i][$ldap_result_manager][0] ?? ''; $item['location'] = $results[$i][$ldap_result_location][0] ?? ''; - $item['locale'] = app()->getLocale(); // ONLY if you are using the "ldap_location" option *AND* you have an actual result if ($ldap_result_location && $item['location']) { @@ -252,6 +251,7 @@ class LdapSync extends Command // Creating a new user. $user = new User; $user->password = $user->noPassword(); + $item['locale'] = app()->getLocale(); $user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below) $item['createorupdate'] = 'created'; } From ab6b8f520e203b86385109d870292e14b1c48339 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 13 Jul 2024 15:25:37 +0100 Subject: [PATCH 05/38] Fixed field name Signed-off-by: snipe --- app/Console/Commands/LdapSync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index 676d207c22..845db27ef9 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -251,7 +251,7 @@ class LdapSync extends Command // Creating a new user. $user = new User; $user->password = $user->noPassword(); - $item['locale'] = app()->getLocale(); + $user->locale = app()->getLocale(); $user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below) $item['createorupdate'] = 'created'; } From e06e4db4b71939b35d88e5e4f326d9f5bb9b70d0 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 13 Jul 2024 18:16:03 +0100 Subject: [PATCH 06/38] Small layout tweaks to oauth page Signed-off-by: snipe --- resources/lang/en-US/account/general.php | 1 + .../views/livewire/oauth-clients.blade.php | 69 +++++++++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/resources/lang/en-US/account/general.php b/resources/lang/en-US/account/general.php index d99e36df82..4fe86164ef 100644 --- a/resources/lang/en-US/account/general.php +++ b/resources/lang/en-US/account/general.php @@ -12,4 +12,5 @@ return array( 'api_reference' => 'Please check the API reference to find specific API endpoints and additional API documentation.', 'profile_updated' => 'Account successfully updated', 'no_tokens' => 'You have not created any personal access tokens.', + 'expires' => 'Expires', ); diff --git a/resources/views/livewire/oauth-clients.blade.php b/resources/views/livewire/oauth-clients.blade.php index 27a76b623f..eeba0e6867 100644 --- a/resources/views/livewire/oauth-clients.blade.php +++ b/resources/views/livewire/oauth-clients.blade.php @@ -32,14 +32,23 @@ @endif @if ($clients->count() > 0) - +
- - - - + + + + + + @@ -57,7 +66,7 @@ @@ -65,6 +74,16 @@ {{ $client->secret }} + + + + @@ -96,18 +115,29 @@
-

+

{{ trans('admin/settings/general.oauth_authorized_apps') }}

-
{{ trans('general.id') }}{{ trans('general.name') }}{{ trans('admin/settings/general.oauth_redirect_url') }}{{ trans('admin/settings/general.oauth_secret') }}{{ trans('general.actions') }}{{ trans('general.name') }}{{ trans('admin/settings/general.oauth_redirect_url') }}{{ trans('admin/settings/general.oauth_secret') }}{{ trans('general.created_at') }}{{ trans('general.updated_at') }}{{ trans('general.actions') }}
- {{ $client->redirect }} + {{ $client->redirect }} + {{ $client->created_at ? Helper::getFormattedDateObject($client->created_at, 'datetime', false) : '' }} + + @if ($client->created_at != $client->updated_at) + {{ $client->updated_at ? Helper::getFormattedDateObject($client->updated_at, 'datetime', false) : '' }} + @endif + @@ -77,8 +96,8 @@ - {{ trans('general.delete') }} - + {{ trans('general.delete') }} +
+
- - + + + + + @@ -120,6 +150,10 @@ {{ $token->client->name }} + + + + + - + @@ -90,7 +94,10 @@ - {{ trans('general.update') }} + + + {{ trans('general.update') }} + @@ -115,21 +122,21 @@
-

+

{{ trans('admin/settings/general.oauth_authorized_apps') }}

-
{{ trans('general.name') }}{{ trans('admin/settings/general.oauth_scopes') }}{{ trans('general.name') }} {{ trans('account/general.personal_access_token') }}{{ trans('admin/settings/general.oauth_scopes') }}{{ trans('general.created_at') }}{{ trans('account/general.expires') }}
+ {{ $token->name }} + @if(!$token->scopes) @@ -129,6 +163,13 @@ @endif + {{ $token->created_at ? Helper::getFormattedDateObject($token->created_at, 'datetime', false) : '' }} + + {{ $token->expires_at ? Helper::getFormattedDateObject($token->expires_at, 'datetime', false) : '' }} + +@section('moar_scripts') + @include ('partials.bootstrap-table') +@endsection + From 2d1aeca9494b9320e8412e8219b546e3d9e84bc5 Mon Sep 17 00:00:00 2001 From: snipe Date: Sat, 13 Jul 2024 18:24:56 +0100 Subject: [PATCH 07/38] Re-use existing string Signed-off-by: snipe --- resources/lang/en-US/account/general.php | 1 - .../views/livewire/oauth-clients.blade.php | 27 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/resources/lang/en-US/account/general.php b/resources/lang/en-US/account/general.php index 4fe86164ef..d99e36df82 100644 --- a/resources/lang/en-US/account/general.php +++ b/resources/lang/en-US/account/general.php @@ -12,5 +12,4 @@ return array( 'api_reference' => 'Please check the API reference to find specific API endpoints and additional API documentation.', 'profile_updated' => 'Account successfully updated', 'no_tokens' => 'You have not created any personal access tokens.', - 'expires' => 'Expires', ); diff --git a/resources/views/livewire/oauth-clients.blade.php b/resources/views/livewire/oauth-clients.blade.php index eeba0e6867..7caf5f6a2f 100644 --- a/resources/views/livewire/oauth-clients.blade.php +++ b/resources/views/livewire/oauth-clients.blade.php @@ -48,7 +48,11 @@ {{ trans('admin/settings/general.oauth_secret') }} {{ trans('general.created_at') }} {{ trans('general.updated_at') }}{{ trans('general.actions') }} + + {{ trans('general.actions') }} + +
@@ -137,8 +144,12 @@ - - + + From e49c96a35e29967845880edb2090b794beee039e Mon Sep 17 00:00:00 2001 From: MrM <11910225+mauro-miatello@users.noreply.github.com> Date: Sun, 14 Jul 2024 19:10:33 +0200 Subject: [PATCH 08/38] Added EULAs in print user's assets Added EULAs when we want to print all assets assigned to a users and ask him/her to sign. --- resources/views/users/print.blade.php | 65 +++++++++++++++++---------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/resources/views/users/print.blade.php b/resources/views/users/print.blade.php index acadf16a03..a9f977a3ac 100644 --- a/resources/views/users/print.blade.php +++ b/resources/views/users/print.blade.php @@ -33,7 +33,6 @@ size: A4; } - .print-logo { max-height: 40px; } @@ -42,8 +41,6 @@ margin-top: 20px; margin-bottom: 10px; } - - - - - - - - - From 0ddb447c4cdd5da9256856236a58c99dbc9f564b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:39:24 +0000 Subject: [PATCH 09/38] Bump codacy/codacy-analysis-cli-action from 4.4.1 to 4.4.5 Bumps [codacy/codacy-analysis-cli-action](https://github.com/codacy/codacy-analysis-cli-action) from 4.4.1 to 4.4.5. - [Release notes](https://github.com/codacy/codacy-analysis-cli-action/releases) - [Commits](https://github.com/codacy/codacy-analysis-cli-action/compare/v4.4.1...v4.4.5) --- updated-dependencies: - dependency-name: codacy/codacy-analysis-cli-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codacy-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codacy-analysis.yml b/.github/workflows/codacy-analysis.yml index f8368dff83..e3e9356425 100644 --- a/.github/workflows/codacy-analysis.yml +++ b/.github/workflows/codacy-analysis.yml @@ -36,7 +36,7 @@ jobs: # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@v4.4.1 + uses: codacy/codacy-analysis-cli-action@v4.4.5 with: # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository # You can also omit the token and run the tools that support default configurations From ba13b9924b8526afaaf455c6095082cd741e7e7e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 17 Jul 2024 11:04:35 -0700 Subject: [PATCH 10/38] Fix test namespaces --- tests/Feature/Departments/Api/CreateDepartmentsTest.php | 2 +- tests/Feature/Locations/Ui/IndexLocationsTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Departments/Api/CreateDepartmentsTest.php b/tests/Feature/Departments/Api/CreateDepartmentsTest.php index be2bbbc18d..a8725c5ff2 100644 --- a/tests/Feature/Departments/Api/CreateDepartmentsTest.php +++ b/tests/Feature/Departments/Api/CreateDepartmentsTest.php @@ -1,6 +1,6 @@ Date: Wed, 17 Jul 2024 12:21:46 -0700 Subject: [PATCH 11/38] Clear the config and route caching done by optimize command after test --- tests/Feature/Console/OptimizeTest.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Console/OptimizeTest.php b/tests/Feature/Console/OptimizeTest.php index 93db05627a..8dd6f270f9 100644 --- a/tests/Feature/Console/OptimizeTest.php +++ b/tests/Feature/Console/OptimizeTest.php @@ -2,13 +2,17 @@ namespace Tests\Feature\Console; -use Illuminate\Support\Facades\Artisan; use Tests\TestCase; class OptimizeTest extends TestCase { public function testOptimizeSucceeds() { + $this->beforeApplicationDestroyed(function () { + $this->artisan('config:clear'); + $this->artisan('route:clear'); + }); + $this->artisan('optimize')->assertSuccessful(); } } From c21821a8642a99773be618c9a92bb1fa84ac54f9 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 17 Jul 2024 20:58:12 +0100 Subject: [PATCH 12/38] Added new form fields Signed-off-by: snipe --- .../views/accessories/checkout.blade.php | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/resources/views/accessories/checkout.blade.php b/resources/views/accessories/checkout.blade.php index fc69d4655b..71035a87c1 100755 --- a/resources/views/accessories/checkout.blade.php +++ b/resources/views/accessories/checkout.blade.php @@ -49,9 +49,35 @@ @endif + +
+ +
+

{{ $accessory->qty }}

+
+
+ + +
+ +
+

{{ $accessory->numRemaining() }}

+
+
- @include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to']) + @include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to', 'required'=> 'true']) + + +
+ +
+
+ +
+
+ {!! $errors->first('checkout_qty', '
') !!} +
@if ($accessory->requireAcceptance() || $accessory->getEula() || ($snipeSettings->webhook_endpoint!='')) From 05e278d08bf48b35f5d361ee521949dd12a58825 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 17 Jul 2024 20:58:21 +0100 Subject: [PATCH 13/38] Added qty to email Signed-off-by: snipe --- .../views/notifications/markdown/checkout-accessory.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/notifications/markdown/checkout-accessory.blade.php b/resources/views/notifications/markdown/checkout-accessory.blade.php index a3126f7aae..836f574133 100644 --- a/resources/views/notifications/markdown/checkout-accessory.blade.php +++ b/resources/views/notifications/markdown/checkout-accessory.blade.php @@ -20,6 +20,9 @@ @if (isset($item->model_no)) | **{{ trans('general.model_no') }}** | {{ $item->model_no }} | @endif +@if (isset($checkout_qty)) +| **{{ trans('general.qty') }}** | {{ $checkout_qty }} | +@endif @if ($note) | **{{ trans('mail.additional_notes') }}** | {{ $note }} | @endif From beb0836d69097f0ad7e4109b8bcc614ac1205069 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 17 Jul 2024 23:01:44 +0100 Subject: [PATCH 14/38] Updated route Signed-off-by: snipe --- routes/web/accessories.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/web/accessories.php b/routes/web/accessories.php index 35ec73e7df..1f28892a0e 100644 --- a/routes/web/accessories.php +++ b/routes/web/accessories.php @@ -13,7 +13,7 @@ Route::group(['prefix' => 'accessories', 'middleware' => ['auth']], function () )->name('accessories.checkout.show'); Route::post( - '{accessoryID}/checkout', + '{accessory}/checkout', [Accessories\AccessoryCheckoutController::class, 'store'] )->name('accessories.checkout.store'); From 345a4306e876e706dac6154b4233e295349fcc4f Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 17 Jul 2024 23:02:10 +0100 Subject: [PATCH 15/38] Added SubstituteBindings Signed-off-by: snipe --- app/Http/Kernel.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e3419f247c..73358454df 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -43,10 +43,12 @@ class Kernel extends HttpKernel \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, \App\Http\Middleware\AssetCountForSidebar::class, \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ 'auth:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; From 2b0627c1f61d2603eac53a34fa41ebc0ad9b56c3 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:24:28 +0100 Subject: [PATCH 16/38] Added accessory checkout request Signed-off-by: snipe --- .../Requests/AccessoryCheckoutRequest.php | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/Http/Requests/AccessoryCheckoutRequest.php diff --git a/app/Http/Requests/AccessoryCheckoutRequest.php b/app/Http/Requests/AccessoryCheckoutRequest.php new file mode 100644 index 0000000000..981606eeae --- /dev/null +++ b/app/Http/Requests/AccessoryCheckoutRequest.php @@ -0,0 +1,74 @@ +accessory) { + + $this->merge([ + 'checkout_qty' => (int) $this->checkout_qty ?? 1, + 'number_remaining_after_checkout' => (int) ($this->accessory->numRemaining() - $this->checkout_qty) ?? 0, + ]); + } + + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + + return array_merge( + [ + 'assigned_to' => [ + 'required', + 'integer', + 'exists:users,id,deleted_at,NULL', + 'not_array' + ], + 'number_remaining_after_checkout' => [ + //'gte:checkout_qty', + 'required', + 'integer', + 'min:0', + ], + 'checkout_qty' => [ + 'lte:number_remaining_after_checkout', + ], + ], + ); + } + + public function messages(): array + { + $messages = ['checkout_qty.lte' => 'There are only '.$this->accessory->qty.'/'.$this->number_remaining_after_checkout.' accessories remaining, trying to check out '.$this->checkout_qty]; + return $messages; + } + + + public function response(array $errors) + { + return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag); + } +} From 4f3064bdb1deacc1fafe3aff6be40b4ebe8e4a03 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:24:51 +0100 Subject: [PATCH 17/38] Added store accesstory form request Signed-off-by: snipe --- app/Http/Requests/StoreAccessoryRequest.php | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/Http/Requests/StoreAccessoryRequest.php diff --git a/app/Http/Requests/StoreAccessoryRequest.php b/app/Http/Requests/StoreAccessoryRequest.php new file mode 100644 index 0000000000..c41bae7b40 --- /dev/null +++ b/app/Http/Requests/StoreAccessoryRequest.php @@ -0,0 +1,56 @@ +category_id) { + if ($category = Category::find($this->category_id)) { + $this->merge([ + 'category_type' => $category->category_type ?? null, + ]); + } + } + + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return array_merge( + ['category_type' => 'in:accessory'], + parent::rules(), + ); + } + + public function messages(): array + { + $messages = ['category_type.in' => trans('admin/accessories/message.invalid_category_type')]; + return $messages; + } + + public function response(array $errors) + { + return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag); + } +} From 16ae23dbeb7a1f2f9fbc9c3883da9e4c2730158e Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:25:00 +0100 Subject: [PATCH 18/38] Updated validation strings Signed-off-by: snipe --- resources/lang/en-US/validation.php | 262 +++++++++++++++------------- 1 file changed, 142 insertions(+), 120 deletions(-) diff --git a/resources/lang/en-US/validation.php b/resources/lang/en-US/validation.php index 05374e23af..fcdc3ee08c 100644 --- a/resources/lang/en-US/validation.php +++ b/resources/lang/en-US/validation.php @@ -13,132 +13,154 @@ return [ | */ - 'accepted' => 'The :attribute must be accepted.', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'before' => 'The :attribute must be a date before :date.', - 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', - 'between' => [ - 'numeric' => 'The :attribute must be between :min - :max.', - 'file' => 'The :attribute must be between :min - :max kilobytes.', - 'string' => 'The :attribute must be between :min - :max characters.', - 'array' => 'The :attribute must have between :min and :max items.', + 'accepted' => 'The :attribute field must be accepted.', + 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', + 'active_url' => 'The :attribute field must be a valid URL.', + 'after' => 'The :attribute field must be a date after :date.', + 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', + 'alpha' => 'The :attribute field must only contain letters.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', + 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'array' => 'The :attribute field must be an array.', + 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', + 'before' => 'The :attribute field must be a date before :date.', + 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute field must have between :min and :max items.', + 'file' => 'The :attribute field must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute field must be between :min and :max.', + 'string' => 'The :attribute field must be between :min and :max characters.', ], - 'boolean' => 'The :attribute must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'email' => 'The :attribute format is invalid.', - 'exists' => 'The selected :attribute is invalid.', - 'file' => 'The :attribute must be a file.', - 'filled' => 'The :attribute field must have a value.', - 'image' => 'The :attribute must be an image.', + 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', + 'confirmed' => 'The :attribute field confirmation does not match.', + 'contains' => 'The :attribute field is missing a required value.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute field must be a valid date.', + 'date_equals' => 'The :attribute field must be a date equal to :date.', + 'date_format' => 'The :attribute field must match the format :format.', + 'decimal' => 'The :attribute field must have :decimal decimal places.', + 'declined' => 'The :attribute field must be declined.', + 'declined_if' => 'The :attribute field must be declined when :other is :value.', + 'different' => 'The :attribute field and :other must be different.', + 'digits' => 'The :attribute field must be :digits digits.', + 'digits_between' => 'The :attribute field must be between :min and :max digits.', + 'dimensions' => 'The :attribute field has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', + 'email' => 'The :attribute field must be a valid email address.', + 'ends_with' => 'The :attribute field must end with one of the following: :values.', + 'enum' => 'The selected :attribute is invalid.', + 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', + 'file' => 'The :attribute field must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute field must have more than :value items.', + 'file' => 'The :attribute field must be greater than :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than :value.', + 'string' => 'The :attribute field must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute field must have :value items or more.', + 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than or equal to :value.', + 'string' => 'The :attribute field must be greater than or equal to :value characters.', + ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', + 'image' => 'The :attribute field must be an image.', 'import_field_empty' => 'The value for :fieldname cannot be null.', - 'in' => 'The selected :attribute is invalid.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'ipv4' => 'The :attribute must be a valid IPv4 address.', - 'ipv6' => 'The :attribute must be a valid IPv6 address.', - 'is_unique_department' => 'The :attribute must be unique to this Company Location', - 'json' => 'The :attribute must be a valid JSON string.', - 'max' => [ - 'numeric' => 'The :attribute may not be greater than :max.', - 'file' => 'The :attribute may not be greater than :max kilobytes.', - 'string' => 'The :attribute may not be greater than :max characters.', - 'array' => 'The :attribute may not have more than :max items.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field must exist in :other.', + 'integer' => 'The :attribute field must be an integer.', + 'ip' => 'The :attribute field must be a valid IP address.', + 'ipv4' => 'The :attribute field must be a valid IPv4 address.', + 'ipv6' => 'The :attribute field must be a valid IPv6 address.', + 'json' => 'The :attribute field must be a valid JSON string.', + 'list' => 'The :attribute field must be a list.', + 'lowercase' => 'The :attribute field must be lowercase.', + 'lt' => [ + 'array' => 'The :attribute field must have less than :value items.', + 'file' => 'The :attribute field must be less than :value kilobytes.', + 'numeric' => 'The :attribute field must be less than :value.', + 'string' => 'The :attribute field must be less than :value characters.', ], - 'mimes' => 'The :attribute must be a file of type: :values.', - 'mimetypes' => 'The :attribute must be a file of type: :values.', - 'min' => [ - 'numeric' => 'The :attribute must be at least :min.', - 'file' => 'The :attribute must be at least :min kilobytes.', - 'string' => 'The :attribute must be at least :min characters.', - 'array' => 'The :attribute must have at least :min items.', + 'lte' => [ + 'array' => 'The :attribute field must not have more than :value items.', + 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be less than or equal to :value.', + 'string' => 'The :attribute field must be less than or equal to :value characters.', ], - 'starts_with' => 'The :attribute must start with one of the following: :values.', - 'ends_with' => 'The :attribute must end with one of the following: :values.', - - 'not_in' => 'The selected :attribute is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'present' => 'The :attribute field must be present.', - 'valid_regex' => 'That is not a valid regex. ', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_unless' => 'The :attribute field is required unless :other is in :values.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values is present.', - 'required_without' => 'The :attribute field is required when :values is not present.', + 'mac_address' => 'The :attribute field must be a valid MAC address.', + 'max' => [ + 'array' => 'The :attribute field must not have more than :max items.', + 'file' => 'The :attribute field must not be greater than :max kilobytes.', + 'numeric' => 'The :attribute field must not be greater than :max.', + 'string' => 'The :attribute field must not be greater than :max characters.', + ], + 'max_digits' => 'The :attribute field must not have more than :max digits.', + 'mimes' => 'The :attribute field must be a file of type: :values.', + 'mimetypes' => 'The :attribute field must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute field must have at least :min items.', + 'file' => 'The :attribute field must be at least :min kilobytes.', + 'numeric' => 'The :attribute field must be at least :min.', + 'string' => 'The :attribute field must be at least :min characters.', + ], + 'min_digits' => 'The :attribute field must have at least :min digits.', + 'missing' => 'The :attribute field must be missing.', + 'missing_if' => 'The :attribute field must be missing when :other is :value.', + 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', + 'missing_with' => 'The :attribute field must be missing when :values is present.', + 'missing_with_all' => 'The :attribute field must be missing when :values are present.', + 'multiple_of' => 'The :attribute field must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute field format is invalid.', + 'numeric' => 'The :attribute field must be a number.', + 'password' => [ + 'letters' => 'The :attribute field must contain at least one letter.', + 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', + 'numbers' => 'The :attribute field must contain at least one number.', + 'symbols' => 'The :attribute field must contain at least one symbol.', + 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', + ], + 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', + 'regex' => 'The :attribute field format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', + 'required_if_declined' => 'The :attribute field is required when :other is declined.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'size' => [ - 'numeric' => 'The :attribute must be :size.', - 'file' => 'The :attribute must be :size kilobytes.', - 'string' => 'The :attribute must be :size characters.', - 'array' => 'The :attribute must contain :size items.', - ], - 'string' => 'The :attribute must be a string.', - 'timezone' => 'The :attribute must be a valid zone.', - 'two_column_unique_undeleted' => 'The :attribute must be unique across :table1 and :table2. ', - 'unique' => 'The :attribute has already been taken.', - 'uploaded' => 'The :attribute failed to upload.', - 'url' => 'The :attribute format is invalid.', - 'unique_undeleted' => 'The :attribute must be unique.', - 'non_circular' => 'The :attribute must not create a circular reference.', - 'not_array' => ':attribute cannot be an array.', - 'disallow_same_pwd_as_user_fields' => 'Password cannot be the same as the username.', - 'letters' => 'Password must contain at least one letter.', - 'numbers' => 'Password must contain at least one number.', - 'case_diff' => 'Password must use mixed case.', - 'symbols' => 'Password must contain symbols.', - 'gte' => [ - 'numeric' => 'Value cannot be negative' - ], - 'checkboxes' => ':attribute contains invalid options.', - 'radio_buttons' => ':attribute is invalid.', - - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => [ - 'alpha_space' => 'The :attribute field contains a character that is not allowed.', - 'email_array' => 'One or more email addresses is invalid.', - 'hashed_pass' => 'Your current password is incorrect', - 'dumbpwd' => 'That password is too common.', - 'statuslabel_type' => 'You must select a valid status label type', - - // date_format validation with slightly less stupid messages. It duplicates a lot, but it gets the job done :( - // We use this because the default error message for date_format is reflects php Y-m-d, which non-PHP - // people won't know how to format. - 'purchase_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', - 'last_audit_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD hh:mm:ss format', - 'expiration_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', - 'termination_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', - 'expected_checkin.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', - 'start_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', - 'end_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', - + 'same' => 'The :attribute field must match :other.', + 'size' => [ + 'array' => 'The :attribute field must contain :size items.', + 'file' => 'The :attribute field must be :size kilobytes.', + 'numeric' => 'The :attribute field must be :size.', + 'string' => 'The :attribute field must be :size characters.', ], + 'starts_with' => 'The :attribute field must start with one of the following: :values.', + 'string' => 'The :attribute field must be a string.', + 'timezone' => 'The :attribute field must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'uppercase' => 'The :attribute field must be uppercase.', + 'url' => 'The :attribute field must be a valid URL.', + 'ulid' => 'The :attribute field must be a valid ULID.', + 'uuid' => 'The :attribute field must be a valid UUID.', /* |-------------------------------------------------------------------------- From 6c3cafa72f903923de339e6594ba1e4a434d564c Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:25:07 +0100 Subject: [PATCH 19/38] Added tests Signed-off-by: snipe --- .../Checkouts/Api/AccessoryCheckoutTest.php | 97 ++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php index e6128aa78c..9b69395e35 100644 --- a/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php @@ -33,18 +33,111 @@ class AccessoryCheckoutTest extends TestCase ->postJson(route('api.accessories.checkout', Accessory::factory()->withoutItemsRemaining()->create()), [ 'assigned_to' => User::factory()->create()->id, ]) - ->assertStatusMessageIs('error'); + ->assertOk() + ->assertStatusMessageIs('error') + ->assertJson([ + 'messages' => [ + 'assigned_to' => ['The assigned to field must be an integer.'], + ], + ]) + ->assertStatus(200) + ->json(); } public function testAccessoryCanBeCheckedOut() { $accessory = Accessory::factory()->create(); $user = User::factory()->create(); + $admin = User::factory()->checkoutAccessories()->create(); $this->actingAsForApi(User::factory()->checkoutAccessories()->create()) ->postJson(route('api.accessories.checkout', $accessory), [ 'assigned_to' => $user->id, - ]); + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->assertStatus(200) + ->assertJson([ + 'messages' => [ + 'assigned_to' => ['Accessory checked out successfully.'], + ], + ]) + ->json(); + + $this->assertTrue($accessory->users->contains($user)); + + $this->assertEquals( + 1, + Actionlog::where([ + 'action_type' => 'checkout', + 'target_id' => $user->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'user_id' => $admin->id, + 'note' => 'oh hi there', + ])->count(), + 'Log entry either does not exist or there are more than expected' + ); + } + + public function testAccessoryCanBeCheckedOutWithQty() + { + $accessory = Accessory::factory()->create(['qty' => 20]); + $user = User::factory()->create(); + $admin = User::factory()->checkoutAccessories()->create(); + + $this->actingAsForApi(User::factory()->checkoutAccessories()->create()) + ->postJson(route('api.accessories.checkout', $accessory), [ + 'assigned_to' => $user->id, + 'checkout_qty' => 2, + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->assertStatus(200) + ->assertJson([ + 'messages' => [ + 'assigned_to' => ['The assigned to field must be an integer.'], + ], + ]) + ->json(); + + $this->assertTrue($accessory->users->contains($user)); + + $this->assertEquals( + 1, + Actionlog::where([ + 'action_type' => 'checkout', + 'target_id' => $user->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'user_id' => $admin->id, + 'note' => 'oh hi there', + ])->count(), + 'Log entry either does not exist or there are more than expected' + ); + } + + public function testAccessoryCannotBeCheckedOutToInvalidUser() + { + $accessory = Accessory::factory()->create(); + $user = User::factory()->create(); + + $this->actingAsForApi(User::factory()->checkoutAccessories()->create()) + ->postJson(route('api.accessories.checkout', $accessory), [ + 'assigned_to' => 'invalid-user-id', + 'note' => 'oh hi there', + ]) + ->assertOk() + ->assertStatusMessageIs('error') + ->assertJson([ + 'messages' => [ + 'assigned_to' => ['The assigned to field must be an integer.'], + ], + ]) + ->assertStatus(200) + ->json(); $this->assertTrue($accessory->users->contains($user)); } From 4c4b0f722a40ab68c78e0592888fbea9618b51ba Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:25:29 +0100 Subject: [PATCH 20/38] Added qty to email notification Signed-off-by: snipe --- app/Notifications/CheckoutAccessoryNotification.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Notifications/CheckoutAccessoryNotification.php b/app/Notifications/CheckoutAccessoryNotification.php index 803b697e86..721ba7f6a4 100644 --- a/app/Notifications/CheckoutAccessoryNotification.php +++ b/app/Notifications/CheckoutAccessoryNotification.php @@ -30,6 +30,7 @@ class CheckoutAccessoryNotification extends Notification $this->item = $accessory; $this->admin = $checkedOutBy; $this->note = $note; + $this->checkout_qty = $accessory->checkout_qty; $this->target = $checkedOutTo; $this->acceptance = $acceptance; $this->settings = Setting::getSettings(); @@ -107,7 +108,7 @@ class CheckoutAccessoryNotification extends Notification ->from($botname) ->to($channel) ->attachment(function ($attachment) use ($item, $note, $admin, $fields) { - $attachment->title(htmlspecialchars_decode($item->present()->name), $item->present()->viewUrl()) + $attachment->title(htmlspecialchars_decode($this->checkout_qty.' x '.$item->present()->name), $item->present()->viewUrl()) ->fields($fields) ->content($note); }); @@ -127,6 +128,7 @@ class CheckoutAccessoryNotification extends Notification ->addStartGroupToSection('activityText') ->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle') ->fact(trans('mail.assigned_to'), $target->present()->name) + ->fact(trans('general.qty'), $this->checkout_qty) ->fact(trans('mail.checkedout_from'), $item->location->name ? $item->location->name : '') ->fact(trans('mail.Accessory_Checkout_Notification') . " by ", $admin->present()->fullName()) ->fact(trans('admin/consumables/general.remaining'), $item->numRemaining()) @@ -184,6 +186,7 @@ class CheckoutAccessoryNotification extends Notification 'eula' => $eula, 'req_accept' => $req_accept, 'accept_url' => $accept_url, + 'checkout_qty' => $this->checkout_qty, ]) ->subject(trans('mail.Confirm_accessory_delivery')); } From 0c933bcc5d58debd8e0b06c2114167ffa31afe69 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:25:42 +0100 Subject: [PATCH 21/38] Cleaned up controllers, use form requests Signed-off-by: snipe --- .../Controllers/Api/AccessoriesController.php | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php index 383b95e6ea..1ffdcaf193 100644 --- a/app/Http/Controllers/Api/AccessoriesController.php +++ b/app/Http/Controllers/Api/AccessoriesController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api; use App\Events\CheckoutableCheckedOut; use App\Helpers\Helper; use App\Http\Controllers\Controller; +use App\Http\Requests\AccessoryCheckoutRequest; +use App\Http\Requests\StoreAccessoryRequest; use App\Http\Transformers\AccessoriesTransformer; use App\Http\Transformers\SelectlistTransformer; use App\Models\Accessory; @@ -121,12 +123,12 @@ class AccessoriesController extends Controller /** * Store a newly created resource in storage. * + * @param \App\Http\Requests\ImageUploadRequest $request + * @return \Illuminate\Http\JsonResponse * @author [A. Gianotto] [] * @since [v4.0] - * @param \App\Http\Requests\ImageUploadRequest $request - * @return \Illuminate\Http\Response */ - public function store(ImageUploadRequest $request) + public function store(StoreAccessoryRequest $request) { $this->authorize('create', Accessory::class); $accessory = new Accessory; @@ -144,10 +146,10 @@ class AccessoriesController extends Controller /** * Display the specified resource. * + * @param int $id + * @return array * @author [A. Gianotto] [] * @since [v4.0] - * @param int $id - * @return \Illuminate\Http\Response */ public function show($id) { @@ -161,10 +163,10 @@ class AccessoriesController extends Controller /** * Display the specified resource. * + * @param int $id + * @return array * @author [A. Gianotto] [] * @since [v4.0] - * @param int $id - * @return \Illuminate\Http\Response */ public function accessory_detail($id) { @@ -273,43 +275,31 @@ class AccessoriesController extends Controller * If Slack is enabled and/or asset acceptance is enabled, it will also * trigger a Slack message and send an email. * - * @author [A. Gianotto] [] * @param int $accessoryId - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\JsonResponse + * @author [A. Gianotto] [] */ - public function checkout(Request $request, $accessoryId) + public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory) { - // Check if the accessory exists - if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) { - return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist'))); - } - $this->authorize('checkout', $accessory); + $accessory->assigned_to = $request->input('assigned_to'); + $user = User::find($request->input('assigned_to')); + $accessory->checkout_qty = $request->input('checkout_qty', 1); - - if ($accessory->numRemaining() > 0) { - - if (! $user = User::find($request->input('assigned_to'))) { - return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkout.user_does_not_exist'))); - } - - // Update the accessory data - $accessory->assigned_to = $request->input('assigned_to'); - + for ($i = 0; $i < $accessory->checkout_qty; $i++) { $accessory->users()->attach($accessory->id, [ 'accessory_id' => $accessory->id, 'created_at' => Carbon::now(), 'user_id' => Auth::id(), - 'assigned_to' => $request->get('assigned_to'), - 'note' => $request->get('note'), + 'assigned_to' => $request->input('assigned_to'), + 'note' => $request->input('note'), ]); - - event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note'))); - - return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success'))); } - return response()->json(Helper::formatStandardApiResponse('error', null, 'No accessories remaining')); + // Set this value to be able to pass the qty through to the event + event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note'))); + + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success'))); } From d9b7df5b85f48d90689d62b74023d83339cbc621 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:26:42 +0100 Subject: [PATCH 22/38] Added form requests Signed-off-by: snipe --- .../AccessoryCheckoutController.php | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php index 103cbd09b0..19c8c6c7c5 100644 --- a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php +++ b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php @@ -4,12 +4,12 @@ namespace App\Http\Controllers\Accessories; use App\Events\CheckoutableCheckedOut; use App\Http\Controllers\Controller; +use App\Http\Requests\AccessoryCheckoutRequest; use App\Models\Accessory; use App\Models\User; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; use \Illuminate\Contracts\View\View; use \Illuminate\Http\RedirectResponse; @@ -57,44 +57,29 @@ class AccessoryCheckoutController extends Controller * * @author [A. Gianotto] [] * @param Request $request - * @param int $accessoryId + * @param int $accessory */ - public function store(Request $request, $accessoryId) : RedirectResponse + public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse { - // Check if the accessory exists - if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) { - // Redirect to the accessory management page with error - return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.user_not_found')); - } $this->authorize('checkout', $accessory); + $accessory->assigned_to = $request->input('assigned_to'); + $user = User::find($request->input('assigned_to')); + $accessory->checkout_qty = $request->input('checkout_qty', 1); - if (!$user = User::find($request->input('assigned_to'))) { - return redirect()->route('accessories.checkout.show', $accessory->id)->with('error', trans('admin/accessories/message.checkout.user_does_not_exist')); + for ($i = 0; $i < $accessory->checkout_qty; $i++) { + $accessory->users()->attach($accessory->id, [ + 'accessory_id' => $accessory->id, + 'created_at' => Carbon::now(), + 'user_id' => Auth::id(), + 'assigned_to' => $request->input('assigned_to'), + 'note' => $request->input('note'), + ]); } - - // Make sure there is at least one available to checkout - if ($accessory->numRemaining() <= 0){ - return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable')); - } - - - // Update the accessory data - $accessory->assigned_to = e($request->input('assigned_to')); - - $accessory->users()->attach($accessory->id, [ - 'accessory_id' => $accessory->id, - 'created_at' => Carbon::now(), - 'user_id' => Auth::id(), - 'assigned_to' => $request->get('assigned_to'), - 'note' => $request->input('note'), - ]); - - DB::table('accessories_users')->where('assigned_to', '=', $accessory->assigned_to)->where('accessory_id', '=', $accessory->id)->first(); - event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note'))); // Redirect to the new accessory page - return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.checkout.success')); + return redirect()->route('accessories.index') + ->with('success', trans('admin/accessories/message.checkout.success')); } } From 79a13e3618b2652381ad31d7a213415baef51b57 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 00:26:58 +0100 Subject: [PATCH 23/38] Added numCheckedOut method Signed-off-by: snipe --- app/Models/Accessory.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index a234b1e570..7b19d90316 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -329,6 +329,20 @@ class Accessory extends SnipeModel } + /** + * Check how many items within an accessory are checked out + * + * @author [A. Gianotto] [] + * @since [v5.0] + * @return int + */ + public function numCheckedOut() + { + \Log::debug('numCheckedOut: '.$this->users_count ?? $this->users->count()); + return $this->users_count ?? $this->users->count(); + } + + /** * Check how many items of an accessory remain. * @@ -342,10 +356,14 @@ class Accessory extends SnipeModel */ public function numRemaining() { - $checkedout = $this->users_count; + $checkedout = $this->numCheckedOut(); $total = $this->qty; $remaining = $total - $checkedout; + \Log::debug('checked out: '.$checkedout); + \Log::debug('total: '.$total); + \Log::debug('remaining: '.$remaining); + return (int) $remaining; } From f56006fb6b3f008ec851db11be0e07cc9bfc6a26 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 01:46:53 +0100 Subject: [PATCH 24/38] More refactoring Signed-off-by: snipe --- .../Requests/AccessoryCheckoutRequest.php | 29 ++++++++++++++----- app/Models/Accessory.php | 16 +++++----- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/Http/Requests/AccessoryCheckoutRequest.php b/app/Http/Requests/AccessoryCheckoutRequest.php index 981606eeae..83cd28dc20 100644 --- a/app/Http/Requests/AccessoryCheckoutRequest.php +++ b/app/Http/Requests/AccessoryCheckoutRequest.php @@ -3,8 +3,6 @@ namespace App\Http\Requests; use App\Models\Accessory; -use App\Models\Category; -use App\Models\User; use Illuminate\Support\Facades\Gate; class AccessoryCheckoutRequest extends ImageUploadRequest @@ -23,10 +21,21 @@ class AccessoryCheckoutRequest extends ImageUploadRequest if ($this->accessory) { + $this->diff = ($this->accessory->numRemaining() - $this->checkout_qty); + + \Log::debug('num remaining in form request: '.$this->accessory->numRemaining()); + \Log::debug('accessory qty in form request: '.$this->accessory->qty); + \Log::debug('checkout qty in form request: '.$this->checkout_qty); + \Log::debug('diff in form request: '.$this->diff); + $this->merge([ - 'checkout_qty' => (int) $this->checkout_qty ?? 1, - 'number_remaining_after_checkout' => (int) ($this->accessory->numRemaining() - $this->checkout_qty) ?? 0, + 'checkout_qty' => $this->checkout_qty, + 'number_remaining_after_checkout' => ($this->accessory->numRemaining() - $this->checkout_qty), + 'number_currently_remaining' => $this->accessory->numRemaining(), + 'checkout_difference' => $this->diff, ]); + + \Log::debug('---------------------------------------------'); } } @@ -47,14 +56,18 @@ class AccessoryCheckoutRequest extends ImageUploadRequest 'exists:users,id,deleted_at,NULL', 'not_array' ], + 'number_remaining_after_checkout' => [ - //'gte:checkout_qty', + 'min:0', 'required', 'integer', - 'min:0', ], + 'checkout_qty' => [ - 'lte:number_remaining_after_checkout', + 'integer', + 'lte:qty', + 'lte:number_currently_remaining', + 'min:1', ], ], ); @@ -62,7 +75,7 @@ class AccessoryCheckoutRequest extends ImageUploadRequest public function messages(): array { - $messages = ['checkout_qty.lte' => 'There are only '.$this->accessory->qty.'/'.$this->number_remaining_after_checkout.' accessories remaining, trying to check out '.$this->checkout_qty]; + $messages = ['checkout_qty.lte' => 'There are only '.$this->accessory->qty.' available accessories, and you are trying to check out '.$this->checkout_qty.', leaving '.$this->number_remaining_after_checkout.' ('.$this->number_currently_remaining.') accessories remaining ('.$this->diff.').']; return $messages; } diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index 7b19d90316..a3282b325d 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -63,7 +63,7 @@ class Accessory extends SnipeModel 'company_id' => 'integer|nullable', 'min_amt' => 'integer|min:0|nullable', 'purchase_cost' => 'numeric|nullable|gte:0', - 'purchase_date' => 'date_format:Y-m-d|nullable', + 'purchase_date' => 'date_format:Y-m-d|nullable', ]; @@ -338,8 +338,7 @@ class Accessory extends SnipeModel */ public function numCheckedOut() { - \Log::debug('numCheckedOut: '.$this->users_count ?? $this->users->count()); - return $this->users_count ?? $this->users->count(); + return (int) $this->users_count ?? $this->users->count(); } @@ -347,7 +346,7 @@ class Accessory extends SnipeModel * Check how many items of an accessory remain. * * In order to use this model method, you MUST call withCount('users as users_count') - * on the eloquent query in the controller, otherwise $this->>users_count will be null and + * on the eloquent query in the controller, otherwise $this->users_count will be null and * bad things happen. * * @author [A. Gianotto] [] @@ -357,14 +356,15 @@ class Accessory extends SnipeModel public function numRemaining() { $checkedout = $this->numCheckedOut(); - $total = $this->qty; - $remaining = $total - $checkedout; - \Log::debug('checked out: '.$checkedout); + + $total = $this->qty; \Log::debug('total: '.$total); + + $remaining = $total - $checkedout; \Log::debug('remaining: '.$remaining); - return (int) $remaining; + return $remaining; } /** From 2f0c74aef06de72543e08f40b60328c02d471b1f Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 03:48:06 +0100 Subject: [PATCH 25/38] Updated tests Signed-off-by: snipe --- .../Checkouts/Api/AccessoryCheckoutTest.php | 53 +++++++++--------- .../Checkouts/Ui/AccessoryCheckoutTest.php | 56 +++++++++++++++++-- 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php index 9b69395e35..3f99f67ebf 100644 --- a/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Api/AccessoryCheckoutTest.php @@ -35,33 +35,42 @@ class AccessoryCheckoutTest extends TestCase ]) ->assertOk() ->assertStatusMessageIs('error') - ->assertJson([ - 'messages' => [ - 'assigned_to' => ['The assigned to field must be an integer.'], - ], - ]) + ->assertJson( + [ + 'status' => 'error', + 'messages' => + [ + 'checkout_qty' => + [ + trans_choice('admin/accessories/message.checkout.checkout_qty.lte', 0, + [ + 'number_currently_remaining' => 0, + 'checkout_qty' => 1, + 'number_remaining_after_checkout' => 0 + ]) + ], + + ], + 'payload' => null, + ]) ->assertStatus(200) ->json(); } - public function testAccessoryCanBeCheckedOut() + public function testAccessoryCanBeCheckedOutWithoutQty() { $accessory = Accessory::factory()->create(); $user = User::factory()->create(); $admin = User::factory()->checkoutAccessories()->create(); - $this->actingAsForApi(User::factory()->checkoutAccessories()->create()) + $this->actingAsForApi($admin) ->postJson(route('api.accessories.checkout', $accessory), [ 'assigned_to' => $user->id, ]) ->assertOk() ->assertStatusMessageIs('success') ->assertStatus(200) - ->assertJson([ - 'messages' => [ - 'assigned_to' => ['Accessory checked out successfully.'], - ], - ]) + ->assertJson(['messages' => trans('admin/accessories/message.checkout.success')]) ->json(); $this->assertTrue($accessory->users->contains($user)); @@ -75,9 +84,7 @@ class AccessoryCheckoutTest extends TestCase 'item_id' => $accessory->id, 'item_type' => Accessory::class, 'user_id' => $admin->id, - 'note' => 'oh hi there', - ])->count(), - 'Log entry either does not exist or there are more than expected' + ])->count(),'Log entry either does not exist or there are more than expected' ); } @@ -87,7 +94,7 @@ class AccessoryCheckoutTest extends TestCase $user = User::factory()->create(); $admin = User::factory()->checkoutAccessories()->create(); - $this->actingAsForApi(User::factory()->checkoutAccessories()->create()) + $this->actingAsForApi($admin) ->postJson(route('api.accessories.checkout', $accessory), [ 'assigned_to' => $user->id, 'checkout_qty' => 2, @@ -95,11 +102,7 @@ class AccessoryCheckoutTest extends TestCase ->assertOk() ->assertStatusMessageIs('success') ->assertStatus(200) - ->assertJson([ - 'messages' => [ - 'assigned_to' => ['The assigned to field must be an integer.'], - ], - ]) + ->assertJson(['messages' => trans('admin/accessories/message.checkout.success')]) ->json(); $this->assertTrue($accessory->users->contains($user)); @@ -113,7 +116,6 @@ class AccessoryCheckoutTest extends TestCase 'item_id' => $accessory->id, 'item_type' => Accessory::class, 'user_id' => $admin->id, - 'note' => 'oh hi there', ])->count(), 'Log entry either does not exist or there are more than expected' ); @@ -131,15 +133,10 @@ class AccessoryCheckoutTest extends TestCase ]) ->assertOk() ->assertStatusMessageIs('error') - ->assertJson([ - 'messages' => [ - 'assigned_to' => ['The assigned to field must be an integer.'], - ], - ]) ->assertStatus(200) ->json(); - $this->assertTrue($accessory->users->contains($user)); + $this->assertFalse($accessory->users->contains($user)); } public function testUserSentNotificationUponCheckout() diff --git a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php index 8f97a48315..6822aa96ae 100644 --- a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php @@ -20,23 +20,33 @@ class AccessoryCheckoutTest extends TestCase public function testValidationWhenCheckingOutAccessory() { - $this->actingAs(User::factory()->checkoutAccessories()->create()) - ->post(route('accessories.checkout.store', Accessory::factory()->create()), [ + $accessory = Accessory::factory()->create(); + $response = $this->actingAs(User::factory()->superuser()->create()) + ->post(route('accessories.checkout.store', $accessory), [ // missing assigned_to ]) - ->assertSessionHas('error'); + ->assertStatus(302) + ->assertSessionHas('errors') + ->assertRedirect(route('accessories.checkout.store', $accessory)); + + $this->followRedirects($response)->assertSee(trans('general.error')); } public function testAccessoryMustBeAvailableWhenCheckingOut() { - $this->actingAs(User::factory()->checkoutAccessories()->create()) + + $response = $this->actingAs(User::factory()->viewAccessories()->checkoutAccessories()->create()) ->post(route('accessories.checkout.store', Accessory::factory()->withoutItemsRemaining()->create()), [ 'assigned_to' => User::factory()->create()->id, ]) - ->assertSessionHas('error'); + ->assertStatus(302) + ->assertSessionHas('error') + ->assertRedirect(route('accessories.index')); + + $this->followRedirects($response)->assertSee(trans('general.error')); } - public function testAccessoryCanBeCheckedOut() + public function testAccessoryCanBeCheckedOutWithoutQuantity() { $accessory = Accessory::factory()->create(); $user = User::factory()->create(); @@ -44,9 +54,43 @@ class AccessoryCheckoutTest extends TestCase $this->actingAs(User::factory()->checkoutAccessories()->create()) ->post(route('accessories.checkout.store', $accessory), [ 'assigned_to' => $user->id, + 'note' => 'oh hi there', ]); $this->assertTrue($accessory->users->contains($user)); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'checkout', + 'target_id' => $user->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'note' => 'oh hi there', + ]); + } + + public function testAccessoryCanBeCheckedOutWithQuantity() + { + $accessory = Accessory::factory()->count(5)->create(); + $user = User::factory()->create(); + + $this->actingAs(User::factory()->checkoutAccessories()->create()) + ->post(route('accessories.checkout.store', $accessory), [ + 'assigned_to' => $user->id, + 'checkout_qty' => 3, + 'note' => 'oh hi there', + ]); + + $this->assertTrue($accessory->users->contains($user)); + + $this->assertDatabaseHas('action_logs', [ + 'action_type' => 'checkout', + 'target_id' => $user->id, + 'target_type' => User::class, + 'item_id' => $accessory->id, + 'item_type' => Accessory::class, + 'note' => 'oh hi there', + ]); } public function testUserSentNotificationUponCheckout() From b5b60f22d57e988ac1fd22dab2dae0abef6555d5 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 03:48:21 +0100 Subject: [PATCH 26/38] Removed int Signed-off-by: snipe --- app/Models/Accessory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index a3282b325d..51f07fba18 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -338,7 +338,7 @@ class Accessory extends SnipeModel */ public function numCheckedOut() { - return (int) $this->users_count ?? $this->users->count(); + return $this->users_count ?? $this->users->count(); } From 0c4e498df3488a901721f0e136b744af09808e07 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 03:48:35 +0100 Subject: [PATCH 27/38] Removed debugging Signed-off-by: snipe --- .../Requests/AccessoryCheckoutRequest.php | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/app/Http/Requests/AccessoryCheckoutRequest.php b/app/Http/Requests/AccessoryCheckoutRequest.php index 83cd28dc20..e35248b8ba 100644 --- a/app/Http/Requests/AccessoryCheckoutRequest.php +++ b/app/Http/Requests/AccessoryCheckoutRequest.php @@ -22,17 +22,11 @@ class AccessoryCheckoutRequest extends ImageUploadRequest if ($this->accessory) { $this->diff = ($this->accessory->numRemaining() - $this->checkout_qty); - - \Log::debug('num remaining in form request: '.$this->accessory->numRemaining()); - \Log::debug('accessory qty in form request: '.$this->accessory->qty); - \Log::debug('checkout qty in form request: '.$this->checkout_qty); - \Log::debug('diff in form request: '.$this->diff); - $this->merge([ - 'checkout_qty' => $this->checkout_qty, - 'number_remaining_after_checkout' => ($this->accessory->numRemaining() - $this->checkout_qty), - 'number_currently_remaining' => $this->accessory->numRemaining(), - 'checkout_difference' => $this->diff, + 'checkout_qty' => $this->checkout_qty ?? 1, + 'number_remaining_after_checkout' => (int) ($this->accessory->numRemaining() - $this->checkout_qty), + 'number_currently_remaining' => (int) $this->accessory->numRemaining(), + 'checkout_difference' => (int) $this->diff, ]); \Log::debug('---------------------------------------------'); @@ -65,7 +59,6 @@ class AccessoryCheckoutRequest extends ImageUploadRequest 'checkout_qty' => [ 'integer', - 'lte:qty', 'lte:number_currently_remaining', 'min:1', ], @@ -75,7 +68,12 @@ class AccessoryCheckoutRequest extends ImageUploadRequest public function messages(): array { - $messages = ['checkout_qty.lte' => 'There are only '.$this->accessory->qty.' available accessories, and you are trying to check out '.$this->checkout_qty.', leaving '.$this->number_remaining_after_checkout.' ('.$this->number_currently_remaining.') accessories remaining ('.$this->diff.').']; + $messages = [ + 'checkout_qty.lte' => trans_choice('admin/accessories/message.checkout.checkout_qty.lte', $this->number_currently_remaining, [ + 'number_currently_remaining' => $this->number_currently_remaining, + 'checkout_qty' => $this->checkout_qty, + ]), + ]; return $messages; } From 0ef58a9aefd15a896b52dfe007771a067d50cb00 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 03:48:42 +0100 Subject: [PATCH 28/38] Added translation Signed-off-by: snipe --- resources/lang/en-US/admin/accessories/message.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/lang/en-US/admin/accessories/message.php b/resources/lang/en-US/admin/accessories/message.php index c688d5e03d..f60d41957b 100644 --- a/resources/lang/en-US/admin/accessories/message.php +++ b/resources/lang/en-US/admin/accessories/message.php @@ -26,7 +26,11 @@ return array( 'error' => 'Accessory was not checked out, please try again', 'success' => 'Accessory checked out successfully.', 'unavailable' => 'Accessory is not available for checkout. Check quantity available', - 'user_does_not_exist' => 'That user is invalid. Please try again.' + 'user_does_not_exist' => 'That user is invalid. Please try again.', + 'checkout_qty' => array( + 'lte' => 'There is currently only one available accessory of this type, and you are trying to check out :checkout_qty. Please adjust the checkout quantity or the total stock of this accessory and try again.|There are :number_currently_remaining total available accessories, and you are trying to check out :checkout_qty. Please adjust the checkout quantity or the total stock of this accessory and try again.', + ), + ), 'checkin' => array( From fa5b59cf21308595b5b639422662cc2240878c7b Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 04:29:49 +0100 Subject: [PATCH 29/38] Added qty Signed-off-by: snipe --- tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php index 6822aa96ae..c7d86de085 100644 --- a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php @@ -71,7 +71,7 @@ class AccessoryCheckoutTest extends TestCase public function testAccessoryCanBeCheckedOutWithQuantity() { - $accessory = Accessory::factory()->count(5)->create(); + $accessory = Accessory::factory()->create(['qty'=>5]); $user = User::factory()->create(); $this->actingAs(User::factory()->checkoutAccessories()->create()) From 9858cc5baf4a8efe9014683207af860841866dbd Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 04:43:30 +0100 Subject: [PATCH 30/38] Removed debugging Signed-off-by: snipe --- app/Models/Accessory.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index 51f07fba18..e8fd85d8a6 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -356,13 +356,8 @@ class Accessory extends SnipeModel public function numRemaining() { $checkedout = $this->numCheckedOut(); - \Log::debug('checked out: '.$checkedout); - $total = $this->qty; - \Log::debug('total: '.$total); - $remaining = $total - $checkedout; - \Log::debug('remaining: '.$remaining); return $remaining; } From 670021a4827fb219b6f0fff79c4d542b2db1354e Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 04:43:46 +0100 Subject: [PATCH 31/38] Apply the optimize fix Signed-off-by: snipe --- tests/Feature/Console/OptimizeTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Feature/Console/OptimizeTest.php b/tests/Feature/Console/OptimizeTest.php index 93db05627a..0b4dbc19ac 100644 --- a/tests/Feature/Console/OptimizeTest.php +++ b/tests/Feature/Console/OptimizeTest.php @@ -9,6 +9,12 @@ class OptimizeTest extends TestCase { public function testOptimizeSucceeds() { + + $this->beforeApplicationDestroyed(function () { + $this->artisan('config:clear'); + $this->artisan('route:clear'); + }); + $this->artisan('optimize')->assertSuccessful(); } } From 97ead7120e36a1fd3a6588d2f34a2f31eb7fe876 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 04:54:07 +0100 Subject: [PATCH 32/38] Use from routes for posting Signed-off-by: snipe --- tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php index c7d86de085..9845baf36a 100644 --- a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php @@ -71,10 +71,12 @@ class AccessoryCheckoutTest extends TestCase public function testAccessoryCanBeCheckedOutWithQuantity() { + //$this->withoutExceptionHandling(); $accessory = Accessory::factory()->create(['qty'=>5]); $user = User::factory()->create(); $this->actingAs(User::factory()->checkoutAccessories()->create()) + ->from(route('accessories.checkout.show', $accessory)) ->post(route('accessories.checkout.store', $accessory), [ 'assigned_to' => $user->id, 'checkout_qty' => 3, @@ -101,6 +103,7 @@ class AccessoryCheckoutTest extends TestCase $user = User::factory()->create(); $this->actingAs(User::factory()->checkoutAccessories()->create()) + ->from(route('accessories.checkout.show', $accessory)) ->post(route('accessories.checkout.store', $accessory), [ 'assigned_to' => $user->id, ]); @@ -115,6 +118,7 @@ class AccessoryCheckoutTest extends TestCase $user = User::factory()->create(); $this->actingAs($actor) + ->from(route('accessories.checkout.show', $accessory)) ->post(route('accessories.checkout.store', $accessory), [ 'assigned_to' => $user->id, 'note' => 'oh hi there', From 985714d50487c14e900eb2d290a628160588fc4b Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 05:04:17 +0100 Subject: [PATCH 33/38] Test passing now - I hope Signed-off-by: snipe --- .../Feature/Checkouts/Ui/AccessoryCheckoutTest.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php index 9845baf36a..e0af379db0 100644 --- a/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php +++ b/tests/Feature/Checkouts/Ui/AccessoryCheckoutTest.php @@ -22,6 +22,7 @@ class AccessoryCheckoutTest extends TestCase { $accessory = Accessory::factory()->create(); $response = $this->actingAs(User::factory()->superuser()->create()) + ->from(route('accessories.checkout.show', $accessory)) ->post(route('accessories.checkout.store', $accessory), [ // missing assigned_to ]) @@ -32,17 +33,19 @@ class AccessoryCheckoutTest extends TestCase $this->followRedirects($response)->assertSee(trans('general.error')); } - public function testAccessoryMustBeAvailableWhenCheckingOut() + public function testAccessoryMustHaveAvailableItemsForCheckoutWhenCheckingOut() { + $accessory = Accessory::factory()->withoutItemsRemaining()->create(); $response = $this->actingAs(User::factory()->viewAccessories()->checkoutAccessories()->create()) - ->post(route('accessories.checkout.store', Accessory::factory()->withoutItemsRemaining()->create()), [ + ->from(route('accessories.checkout.show', $accessory)) + ->post(route('accessories.checkout.store', $accessory), [ 'assigned_to' => User::factory()->create()->id, ]) ->assertStatus(302) - ->assertSessionHas('error') - ->assertRedirect(route('accessories.index')); - + ->assertSessionHas('errors') + ->assertRedirect(route('accessories.checkout.store', $accessory)); + $response->assertInvalid(['checkout_qty']); $this->followRedirects($response)->assertSee(trans('general.error')); } @@ -71,7 +74,6 @@ class AccessoryCheckoutTest extends TestCase public function testAccessoryCanBeCheckedOutWithQuantity() { - //$this->withoutExceptionHandling(); $accessory = Accessory::factory()->create(['qty'=>5]); $user = User::factory()->create(); From 44b950cb8e491fb2a7fd85ceb2ed5c41fb6ea3e6 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 05:07:50 +0100 Subject: [PATCH 34/38] Added back in missing validation Signed-off-by: snipe --- resources/lang/en-US/validation.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/lang/en-US/validation.php b/resources/lang/en-US/validation.php index fcdc3ee08c..74d72941d5 100644 --- a/resources/lang/en-US/validation.php +++ b/resources/lang/en-US/validation.php @@ -153,7 +153,16 @@ return [ 'string' => 'The :attribute field must be :size characters.', ], 'starts_with' => 'The :attribute field must start with one of the following: :values.', - 'string' => 'The :attribute field must be a string.', + 'string' => 'The :attribute must be a string.', + 'two_column_unique_undeleted' => 'The :attribute must be unique across :table1 and :table2. ', + 'unique_undeleted' => 'The :attribute must be unique.', + 'non_circular' => 'The :attribute must not create a circular reference.', + 'not_array' => ':attribute cannot be an array.', + 'disallow_same_pwd_as_user_fields' => 'Password cannot be the same as the username.', + 'letters' => 'Password must contain at least one letter.', + 'numbers' => 'Password must contain at least one number.', + 'case_diff' => 'Password must use mixed case.', + 'symbols' => 'Password must contain symbols.', 'timezone' => 'The :attribute field must be a valid timezone.', 'unique' => 'The :attribute has already been taken.', 'uploaded' => 'The :attribute failed to upload.', From d56252c6b31d6406988c08fd58c88bca8b185630 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 05:10:45 +0100 Subject: [PATCH 35/38] Added more back :( Signed-off-by: snipe --- resources/lang/en-US/validation.php | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/resources/lang/en-US/validation.php b/resources/lang/en-US/validation.php index 74d72941d5..f1a5b9ed0f 100644 --- a/resources/lang/en-US/validation.php +++ b/resources/lang/en-US/validation.php @@ -171,6 +171,38 @@ return [ 'ulid' => 'The :attribute field must be a valid ULID.', 'uuid' => 'The :attribute field must be a valid UUID.', + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'alpha_space' => 'The :attribute field contains a character that is not allowed.', + 'email_array' => 'One or more email addresses is invalid.', + 'hashed_pass' => 'Your current password is incorrect', + 'dumbpwd' => 'That password is too common.', + 'statuslabel_type' => 'You must select a valid status label type', + + // date_format validation with slightly less stupid messages. It duplicates a lot, but it gets the job done :( + // We use this because the default error message for date_format is reflects php Y-m-d, which non-PHP + // people won't know how to format. + 'purchase_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', + 'last_audit_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD hh:mm:ss format', + 'expiration_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', + 'termination_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', + 'expected_checkin.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', + 'start_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', + 'end_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format', + 'checkboxes' => ':attribute contains invalid options.', + 'radio_buttons' => ':attribute is invalid.', + 'invalid_value_in_field' => 'Invalid value included in this field', + ], /* |-------------------------------------------------------------------------- | Custom Validation Attributes @@ -189,5 +221,5 @@ return [ | Generic Validation Messages |-------------------------------------------------------------------------- */ - 'invalid_value_in_field' => 'Invalid value included in this field', + ]; From 5c17fefb0830c9b0b518728dc7a38b293da690a6 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:54:58 +0200 Subject: [PATCH 36/38] Implement `PHP_UPLOAD_LIMIT` in Alpine images. --- docker/entrypoint_alpine.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker/entrypoint_alpine.sh b/docker/entrypoint_alpine.sh index 783b50b69c..c1a75b0cbd 100644 --- a/docker/entrypoint_alpine.sh +++ b/docker/entrypoint_alpine.sh @@ -39,6 +39,14 @@ chown -R apache:root /var/lib/snipeit/data/* chown -R apache:root /var/lib/snipeit/dumps chown -R apache:root /var/lib/snipeit/keys +# Fix php settings +if [ ! -z "${PHP_UPLOAD_LIMIT}" ] +then + echo "Changing upload limit to ${PHP_UPLOAD_LIMIT}" + sed -i "s/^upload_max_filesize.*/upload_max_filesize = ${PHP_UPLOAD_LIMIT}M/" /etc/php*/php.ini + sed -i "s/^post_max_size.*/post_max_size = ${PHP_UPLOAD_LIMIT}M/" /etc/php*/php.ini +fi + # If the Oauth DB files are not present copy the vendor files over to the db migrations if [ ! -f "/var/www/html/database/migrations/*create_oauth*" ] then From 19bd99d159a78d8d7eefad38d276491ae0cd35b7 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 17:33:24 +0100 Subject: [PATCH 37/38] Updated count Signed-off-by: snipe --- app/Models/Accessory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php index e8fd85d8a6..20d2584c31 100755 --- a/app/Models/Accessory.php +++ b/app/Models/Accessory.php @@ -338,7 +338,7 @@ class Accessory extends SnipeModel */ public function numCheckedOut() { - return $this->users_count ?? $this->users->count(); + return $this->users_count ?? $this->users()->count(); } From 822bc6f08542d183f5922ef98e9d9df74346c8e2 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 18 Jul 2024 17:37:45 +0100 Subject: [PATCH 38/38] Removed response in form request Signed-off-by: snipe --- app/Http/Requests/AccessoryCheckoutRequest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/Http/Requests/AccessoryCheckoutRequest.php b/app/Http/Requests/AccessoryCheckoutRequest.php index e35248b8ba..0e17b390c2 100644 --- a/app/Http/Requests/AccessoryCheckoutRequest.php +++ b/app/Http/Requests/AccessoryCheckoutRequest.php @@ -76,10 +76,4 @@ class AccessoryCheckoutRequest extends ImageUploadRequest ]; return $messages; } - - - public function response(array $errors) - { - return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag); - } }
{{ trans('account/general.personal_access_token') }} {{ trans('admin/settings/general.oauth_scopes') }} {{ trans('general.created_at') }}{{ trans('account/general.expires') }}{{ trans('general.expires') }} + + {{ trans('general.actions') }} + +