diff --git a/app/Http/Controllers/Api/LicenseSeatsController.php b/app/Http/Controllers/Api/LicenseSeatsController.php new file mode 100644 index 000000000..05cdd9895 --- /dev/null +++ b/app/Http/Controllers/Api/LicenseSeatsController.php @@ -0,0 +1,138 @@ +authorize('view', $license); + + $seats = LicenseSeat::with('license', 'user', 'asset', 'user.department') + ->where('license_seats.license_id', $licenseId); + + $order = $request->input('order') === 'asc' ? 'asc' : 'desc'; + + if ($request->input('sort')=='department') { + $seats->OrderDepartments($order); + } else { + $seats->orderBy('id', $order); + } + + $total = $seats->count(); + $offset = (($seats) && (request('offset') > $total)) ? 0 : request('offset', 0); + $limit = request('limit', 50); + + $seats = $seats->skip($offset)->take($limit)->get(); + + if ($seats) { + return (new LicenseSeatsTransformer)->transformLicenseSeats($seats, $total); + } + } + + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.does_not_exist')), 200); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function show($licenseId, $seatId) + { + // + $this->authorize('view', License::class); + // sanity checks: + // 1. does the license seat exist? + if (!$licenseSeat = LicenseSeat::find($seatId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found')); + } + // 2. does the seat belong to the specified license? + if (!$license = $licenseSeat->license()->first() || $license->id != intval($licenseId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license')); + } + return (new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $licenseId + * @param int $seatId + * @return \Illuminate\Http\Response + */ + public function update(Request $request, $licenseId, $seatId) + { + $this->authorize('checkout', License::class); + + // sanity checks: + // 1. does the license seat exist? + if (!$licenseSeat = LicenseSeat::find($seatId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found')); + } + // 2. does the seat belong to the specified license? + if (!$license = $licenseSeat->license()->first() || $license->id != intval($licenseId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license')); + } + + $oldUser = $licenseSeat->user()->first(); + $oldAsset = $licenseSeat->asset()->first(); + + // attempt to update the license seat + $licenseSeat->fill($request->all()); + $licenseSeat->user_id = Auth::user()->id; + + // check if this update is a checkin operation + // 1. are relevant fields touched at all? + $touched = $licenseSeat->isDirty('assigned_to') || $licenseSeat->isDirty('asset_id'); + // 2. are they cleared? if yes then this is a checkin operation + $is_checkin = ($touched && $licenseSeat->assigned_to === null && $licenseSeat->asset_id === null); + + if (!$touched) { + // nothing to update + return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); + } + + if ($licenseSeat->save()) { + // the logging functions expect only one "target". if both asset and user are present in the request, + // we simply let assets take precedence over users... + $changes = $licenseSeat->getChanges(); + if (array_key_exists('assigned_to', $changes)) { + $target = $is_checkin ? $oldUser : User::find($changes['assigned_to']); + } + if (array_key_exists('asset_id', $changes)) { + $target = $is_checkin ? $oldAsset : Asset::find($changes['asset_id']); + } + + if ($is_checkin) { + $licenseSeat->logCheckin($target, $request->input('note')); + return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); + } + + // in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation. + $licenseSeat->logCheckout($request->input('note'), $target); + return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); + } + + return Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()); + } +} diff --git a/app/Http/Controllers/Api/LicensesController.php b/app/Http/Controllers/Api/LicensesController.php index 07bacdc4d..268248ab7 100644 --- a/app/Http/Controllers/Api/LicensesController.php +++ b/app/Http/Controllers/Api/LicensesController.php @@ -237,50 +237,6 @@ class LicensesController extends Controller } return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.assoc_users'))); } - - /** - * Get license seat listing - * - * @author [A. Gianotto] [] - * @since [v1.0] - * @param int $licenseId - * @return \Illuminate\Contracts\View\View - */ - public function seats(Request $request, $licenseId) - { - - if ($license = License::find($licenseId)) { - - $this->authorize('view', $license); - - $seats = LicenseSeat::with('license', 'user', 'asset', 'user.department') - ->where('license_seats.license_id', $licenseId); - - $order = $request->input('order') === 'asc' ? 'asc' : 'desc'; - - if ($request->input('sort')=='department') { - $seats->OrderDepartments($order); - } else { - $seats->orderBy('id', $order); - } - - $offset = (($seats) && (request('offset') > $seats->count())) ? 0 : request('offset', 0); - $limit = request('limit', 50); - - $total = $seats->count(); - - $seats = $seats->skip($offset)->take($limit)->get(); - - if ($seats) { - return (new LicenseSeatsTransformer)->transformLicenseSeats($seats, $total); - } - - } - - return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.does_not_exist')), 200); - - } - /** * Gets a paginated collection for the select2 menus diff --git a/app/Http/Transformers/LicenseSeatsTransformer.php b/app/Http/Transformers/LicenseSeatsTransformer.php index f7685db16..c8454d06e 100644 --- a/app/Http/Transformers/LicenseSeatsTransformer.php +++ b/app/Http/Transformers/LicenseSeatsTransformer.php @@ -20,12 +20,11 @@ class LicenseSeatsTransformer return (new DatatablesTransformer)->transformDatatables($array, $total); } - public function transformLicenseSeat (LicenseSeat $seat, $seat_count) + public function transformLicenseSeat (LicenseSeat $seat, $seat_count=0) { $array = [ 'id' => (int) $seat->id, 'license_id' => (int) $seat->license->id, - 'name' => 'Seat '.$seat_count, 'assigned_user' => ($seat->user) ? [ 'id' => (int) $seat->user->id, 'name'=> e($seat->user->present()->fullName), @@ -49,6 +48,10 @@ class LicenseSeatsTransformer 'user_can_checkout' => (($seat->assigned_to=='') && ($seat->asset_id=='')), ]; + if($seat_count != 0) { + $array['name'] = 'Seat '.$seat_count; + } + $permissions_array['available_actions'] = [ 'checkout' => Gate::allows('checkout', License::class), 'checkin' => Gate::allows('checkin', License::class), diff --git a/app/Models/LicenseSeat.php b/app/Models/LicenseSeat.php index 590409f77..40a53adf8 100755 --- a/app/Models/LicenseSeat.php +++ b/app/Models/LicenseSeat.php @@ -20,6 +20,16 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild protected $guarded = 'id'; protected $table = 'license_seats'; + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'assigned_to', + 'asset_id' + ]; + use Acceptable; public function getCompanyableParents() diff --git a/resources/views/licenses/view.blade.php b/resources/views/licenses/view.blade.php index a578efc0a..80de54d95 100755 --- a/resources/views/licenses/view.blade.php +++ b/resources/views/licenses/view.blade.php @@ -350,7 +350,7 @@ data-sort-order="asc" data-sort-name="name" class="table table-striped snipe-table" - data-url="{{ route('api.license.seats', $license->id) }}" + data-url="{{ route('api.licenses.seats.index', $license->id) }}" data-export-options='{ "fileName": "export-seats-{{ str_slug($license->name) }}-{{ date('Y-m-d') }}", "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"] diff --git a/routes/api.php b/routes/api.php index 2c6a6ab26..c8f05d2f1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -166,7 +166,6 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api'] /*--- Departments API ---*/ - /*--- Suppliers API ---*/ Route::group(['prefix' => 'departments'], function () { @@ -496,11 +495,6 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api'] /*--- Licenses API ---*/ Route::group(['prefix' => 'licenses'], function () { - Route::get('{licenseId}/seats', [ - 'as' => 'api.license.seats', - 'uses' => 'LicensesController@seats' - ]); - Route::get('selectlist', [ 'as' => 'api.licenses.selectlist', @@ -525,7 +519,18 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api'] ] ); // Licenses resource - + Route::resource('licenses.seats', 'LicenseSeatsController', + [ + 'names' => + [ + 'index' => 'api.licenses.seats.index', + 'show' => 'api.licenses.seats.show', + 'update' => 'api.licenses.seats.update' + ], + 'except' => ['create', 'edit', 'destroy', 'store'], + 'parameters' => ['licenseseat' => 'licenseseat_id'] + ] + ); // Licenseseats resource /*--- Locations API ---*/ diff --git a/tests/api/ApiLicenseSeatsCest.php b/tests/api/ApiLicenseSeatsCest.php new file mode 100644 index 000000000..30c9ccaa9 --- /dev/null +++ b/tests/api/ApiLicenseSeatsCest.php @@ -0,0 +1,185 @@ +user = \App\Models\User::find(1); + $I->haveHttpHeader('Accept', 'application/json'); + $I->amBearerAuthenticated($I->getToken($this->user)); + } + + /** @test */ + public function indexLicenseSeats(ApiTester $I) + { + $I->wantTo('Get a list of license seats for a specific license'); + + // call + $I->sendGET('/licenses/1/seats?limit=10&order=desc'); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + + // sample verify + $licenseSeats = App\Models\LicenseSeat::where('license_id', 1) + ->orderBy('id','desc')->take(10)->get(); + // pick a random seat + $licenseSeat = $licenseSeats->random(); + // need the index in the original list so that the "name" field is determined correctly + $licenseSeatNumber = 0; + foreach($licenseSeats as $index=>$seat) { + if ($licenseSeat === $seat) { + $licenseSeatNumber = $index+1; + } + } + $I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat, $licenseSeatNumber))); + } + + /** @test */ + public function showLicenseSeat(ApiTester $I) + { + $I->wantTo('Get a license seat'); + + // call + $I->sendGET('/licenses/1/seats/10'); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + + // sample verify + $licenseSeat = App\Models\LicenseSeat::findOrFail(10); + $I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat))); + } + + /** @test */ + public function checkoutLicenseSeatToUser(ApiTester $I) + { + $I->wantTo('Checkout a license seat to a user'); + + $user = App\Models\User::all()->random(); + $licenseSeat = App\Models\LicenseSeat::all()->random(); + $endpoint = '/licenses/'.$licenseSeat->license_id.'/seats/'.$licenseSeat->id; + + $data = [ + 'assigned_to' => $user->id, + 'note' => 'Test Checkout to User via API' + ]; + + // update + $I->sendPATCH($endpoint, $data); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + + $response = json_decode($I->grabResponse()); + $I->assertEquals('success', $response->status); + $I->assertEquals(trans('admin/licenses/message.update.success'), $response->messages); + $I->assertEquals($licenseSeat->license_id, $response->payload->license_id); // license id does not change + $I->assertEquals($licenseSeat->id, $response->payload->id); // license seat id does not change + + // verify + $licenseSeat = $licenseSeat->fresh(); + $I->sendGET($endpoint); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + $I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat))); + + // verify that the last logged action is a checkout + $I->sendGET('/reports/activity?item_type=license&limit=1&item_id='.$licenseSeat->license_id); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + $I->seeResponseContainsJson([ + "action_type" => "checkout" + ]); + } + + /** @test */ + public function checkoutLicenseSeatToAsset(ApiTester $I) + { + $I->wantTo('Checkout a license seat to an asset'); + + $asset = App\Models\Asset::all()->random(); + $licenseSeat = App\Models\LicenseSeat::all()->random(); + $endpoint = '/licenses/'.$licenseSeat->license_id.'/seats/'.$licenseSeat->id; + + $data = [ + 'asset_id' => $asset->id, + 'note' => 'Test Checkout to Asset via API' + ]; + + // update + $I->sendPATCH($endpoint, $data); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + + $response = json_decode($I->grabResponse()); + $I->assertEquals('success', $response->status); + $I->assertEquals(trans('admin/licenses/message.update.success'), $response->messages); + $I->assertEquals($licenseSeat->license_id, $response->payload->license_id); // license id does not change + $I->assertEquals($licenseSeat->id, $response->payload->id); // license seat id does not change + + // verify + $licenseSeat = $licenseSeat->fresh(); + $I->sendGET($endpoint); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + $I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat))); + + // verify that the last logged action is a checkout + $I->sendGET('/reports/activity?item_type=license&limit=1&item_id='.$licenseSeat->license_id); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + $I->seeResponseContainsJson([ + "action_type" => "checkout" + ]); + } + + /** @test */ + public function checkoutLicenseSeatToUserAndAsset(ApiTester $I) + { + $I->wantTo('Checkout a license seat to a user AND an asset'); + + $asset = App\Models\Asset::all()->random(); + $user = App\Models\User::all()->random(); + $licenseSeat = App\Models\LicenseSeat::all()->random(); + $endpoint = '/licenses/'.$licenseSeat->license_id.'/seats/'.$licenseSeat->id; + + $data = [ + 'asset_id' => $asset->id, + 'assigned_to' => $user->id, + 'note' => 'Test Checkout to User and Asset via API' + ]; + + // update + $I->sendPATCH($endpoint, $data); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + + $response = json_decode($I->grabResponse()); + $I->assertEquals('success', $response->status); + $I->assertEquals(trans('admin/licenses/message.update.success'), $response->messages); + $I->assertEquals($licenseSeat->license_id, $response->payload->license_id); // license id does not change + $I->assertEquals($licenseSeat->id, $response->payload->id); // license seat id does not change + + // verify + $licenseSeat = $licenseSeat->fresh(); + $I->sendGET($endpoint); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + $I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat))); + + // verify that the last logged action is a checkout + $I->sendGET('/reports/activity?item_type=license&limit=1&item_id='.$licenseSeat->license_id); + $I->seeResponseIsJson(); + $I->seeResponseCodeIs(200); + $I->seeResponseContainsJson([ + "action_type" => "checkout" + ]); + } +}