From 6affacc5cb73aff30381286ecbe375df6aa98643 Mon Sep 17 00:00:00 2001 From: Maciej Rymarz <59456825+mako321@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:42:14 +0200 Subject: [PATCH] Add integration with Pencil Spaces (#12) * Add integration with pencil space * Add test --- README.md | 3 + composer.json | 12 +++- ...to_consultation_access_enquiries_table.php | 22 +++++++ src/Enum/MeetingLinkTypeEnum.php | 9 +++ ...laLmsConsultationAccessServiceProvider.php | 2 + ...ConsultationAccessEnquiryApiController.php | 6 ++ .../ConsultationAccessEnquiryApiSwagger.php | 42 ++++++++++++ ...pproveConsultationAccessEnquiryRequest.php | 3 +- .../ConsultationAccessEnquiryResource.php | 1 + .../JoinConsultationAccessResource.php | 44 +++++++++++++ src/Jobs/CreatePencilSpaceJob.php | 60 +++++++++++++++++ src/Models/ConsultationAccessEnquiry.php | 2 + .../ConsultationAccessEnquiryService.php | 9 ++- src/routes.php | 1 + ...tationAccessEnquiryAdminApproveApiTest.php | 24 +++++++ .../ConsultationAccessEnquiryJoinApiTest.php | 64 +++++++++++++++++++ tests/Feature/CreatePencilSpaceJobTest.php | 64 +++++++++++++++++++ 17 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 database/migrations/2023_08_04_150519_add_meeting_link_type_field_to_consultation_access_enquiries_table.php create mode 100644 src/Enum/MeetingLinkTypeEnum.php create mode 100644 src/Http/Resources/JoinConsultationAccessResource.php create mode 100644 src/Jobs/CreatePencilSpaceJob.php create mode 100644 tests/Api/ConsultationAccessEnquiryJoinApiTest.php create mode 100644 tests/Feature/CreatePencilSpaceJobTest.php diff --git a/README.md b/README.md index 73d3dd7..c794620 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ sequenceDiagram end ``` + +If you don't send the `meeting_url` during the approval, an automatic space will be created in [Pencil Spaces](https://www.pencilspaces.com/), and that link will be saved. You can find more information [here](https://github.com/EscolaLMS/Pencil-Spaces). + ## Listeners This package does not listen for any events. diff --git a/composer.json b/composer.json index f1f440d..ae52c4c 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "escolalms/core": "^1", "escolalms/settings": "^0", "escolalms/consultations": "^0", - "laravel/framework": "^8|^9" + "laravel/framework": "^8|^9", + "escolalms/pencil-spaces": "dev-main" }, "require-dev": { "phpunit/phpunit": "^9.0", @@ -43,5 +44,12 @@ "sort-packages": true }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/EscolaLMS/Pencil-Spaces.git" + } + ] + } diff --git a/database/migrations/2023_08_04_150519_add_meeting_link_type_field_to_consultation_access_enquiries_table.php b/database/migrations/2023_08_04_150519_add_meeting_link_type_field_to_consultation_access_enquiries_table.php new file mode 100644 index 0000000..b5f75cc --- /dev/null +++ b/database/migrations/2023_08_04_150519_add_meeting_link_type_field_to_consultation_access_enquiries_table.php @@ -0,0 +1,22 @@ +string('meeting_link_type')->nullable(); + }); + } + + public function down(): void + { + Schema::table('consultation_access_enquiries', function (Blueprint $table) { + $table->dropColumn('meeting_link_type'); + }); + } +} diff --git a/src/Enum/MeetingLinkTypeEnum.php b/src/Enum/MeetingLinkTypeEnum.php new file mode 100644 index 0000000..f6cbeb6 --- /dev/null +++ b/src/Enum/MeetingLinkTypeEnum.php @@ -0,0 +1,9 @@ +app->register(AuthServiceProvider::class); $this->app->register(EscolaLmsConsultationsServiceProvider::class); + $this->app->register(EscolaLmsPencilSpacesServiceProvider::class); } } diff --git a/src/Http/Controllers/ConsultationAccessEnquiryApiController.php b/src/Http/Controllers/ConsultationAccessEnquiryApiController.php index c85019c..d581264 100644 --- a/src/Http/Controllers/ConsultationAccessEnquiryApiController.php +++ b/src/Http/Controllers/ConsultationAccessEnquiryApiController.php @@ -9,6 +9,7 @@ use EscolaLms\ConsultationAccess\Http\Requests\UpdateConsultationAccessEnquiryRequest; use EscolaLms\ConsultationAccess\Http\Resources\ConsultationAccessEnquiryResource; use EscolaLms\ConsultationAccess\Http\Requests\CreateConsultationAccessEnquiryRequest; +use EscolaLms\ConsultationAccess\Http\Resources\JoinConsultationAccessResource; use EscolaLms\ConsultationAccess\Services\Contracts\ConsultationAccessEnquiryServiceContract; use EscolaLms\Core\Http\Controllers\EscolaLmsBaseController; use Illuminate\Http\JsonResponse; @@ -54,4 +55,9 @@ public function read(ReadConsultationAccessEnquiryRequest $request): JsonRespons { return $this->sendResponseForResource(ConsultationAccessEnquiryResource::make($request->getEnquiry())); } + + public function join(ReadConsultationAccessEnquiryRequest $request): JsonResponse + { + return $this->sendResponseForResource(JoinConsultationAccessResource::make($request->getEnquiry())); + } } diff --git a/src/Http/Controllers/Swagger/ConsultationAccessEnquiryApiSwagger.php b/src/Http/Controllers/Swagger/ConsultationAccessEnquiryApiSwagger.php index a2aacf5..99c2d73 100644 --- a/src/Http/Controllers/Swagger/ConsultationAccessEnquiryApiSwagger.php +++ b/src/Http/Controllers/Swagger/ConsultationAccessEnquiryApiSwagger.php @@ -151,6 +151,48 @@ public function index(ListConsultationAccessEnquiryRequest $request): JsonRespon */ public function read(ReadConsultationAccessEnquiryRequest $request): JsonResponse; + /** + * @OA\Get( + * path="/api/consultation-access-enquiries/{id}/join", + * summary="Get url to consultation by enquiry id", + * tags={"Consultation Access"}, + * security={ + * {"passport": {}}, + * }, + * @OA\Parameter( + * name="id", + * required=true, + * in="path", + * @OA\Schema( + * type="integer", + * ), + * ), + * @OA\Response( + * response=200, + * description="Successfull operation", + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="object", + * @OA\Property( + * property="success", + * type="boolean" + * ), + * @OA\Property( + * property="data", + * ref="#/components/schemas/JoinConsultationAccessResource" + * ), + * @OA\Property( + * property="message", + * type="string" + * ) + * ) + * ) + * ) + * ) + */ + public function join(ReadConsultationAccessEnquiryRequest $request): JsonResponse; + /** * @OA\Post( * path="/api/consultation-access-enquiries", diff --git a/src/Http/Requests/Admin/AdminApproveConsultationAccessEnquiryRequest.php b/src/Http/Requests/Admin/AdminApproveConsultationAccessEnquiryRequest.php index f1839a2..008cbfb 100644 --- a/src/Http/Requests/Admin/AdminApproveConsultationAccessEnquiryRequest.php +++ b/src/Http/Requests/Admin/AdminApproveConsultationAccessEnquiryRequest.php @@ -10,7 +10,6 @@ /** * @OA\Schema( * schema="AdminApproveConsultationAccessEnquiryRequest", - * required={"meeting_link"}, * @OA\Property( * property="meeting_link", * description="meeting_link", @@ -30,7 +29,7 @@ public function rules(): array { return [ 'proposed_term_id' => ['required', 'integer', 'exists:consultation_access_enquiry_proposed_terms,id'], - 'meeting_link' => ['sometimes', 'string', 'url'], + 'meeting_link' => ['nullable', 'string', 'url'], ]; } diff --git a/src/Http/Resources/ConsultationAccessEnquiryResource.php b/src/Http/Resources/ConsultationAccessEnquiryResource.php index e8b2b19..b9c8bb1 100644 --- a/src/Http/Resources/ConsultationAccessEnquiryResource.php +++ b/src/Http/Resources/ConsultationAccessEnquiryResource.php @@ -87,6 +87,7 @@ public function toArray($request): array 'description' => $this->description, 'consultation_term' => $this->consultationUser ? ConsultationTermsResource::make($this->consultationUser) : null, 'meeting_link' => $this->meeting_link, + 'meeting_link_type' => $this->meeting_link_type, 'related_type' => $this->related_type, 'related_id' => $this->related_id, 'title' => $this->title, diff --git a/src/Http/Resources/JoinConsultationAccessResource.php b/src/Http/Resources/JoinConsultationAccessResource.php new file mode 100644 index 0000000..c76751d --- /dev/null +++ b/src/Http/Resources/JoinConsultationAccessResource.php @@ -0,0 +1,44 @@ + $this->id, + 'meeting_link_type' => $this->meeting_link_type, + 'meeting_link' => $this->meeting_link_type === MeetingLinkTypeEnum::PENCIL_SPACES + ? PencilSpace::getDirectLoginUrl(Auth::id(), $this->meeting_link) + : $this->meeting_link, + ]; + } +} diff --git a/src/Jobs/CreatePencilSpaceJob.php b/src/Jobs/CreatePencilSpaceJob.php new file mode 100644 index 0000000..5d5edb0 --- /dev/null +++ b/src/Jobs/CreatePencilSpaceJob.php @@ -0,0 +1,60 @@ +consultationAccessEnquiryId = $consultationAccessEnquiryId; + } + + public function handle(ConsultationAccessEnquiryRepositoryContract $consultationAccessEnquiryRepository): void + { + try { + /** @var ?ConsultationAccessEnquiry $enquiry */ + $enquiry = $consultationAccessEnquiryRepository->find($this->consultationAccessEnquiryId); + + if (!$enquiry) { + return; + } + + $resource = new CreatePencilSpaceResource( + $enquiry->title ?? 'Consultation X ' . $enquiry->user->name, + collect($enquiry->consultation->author_id), + collect($enquiry->user_id) + ); + + $space = PencilSpace::createSpace($resource); + + $consultationAccessEnquiryRepository->update([ + 'meeting_link' => Arr::get($space, 'link'), + 'meeting_link_type' => MeetingLinkTypeEnum::PENCIL_SPACES, + ], $this->consultationAccessEnquiryId); + + event(new ConsultationAccessEnquiryApprovedEvent($enquiry->user, $enquiry)); + } catch (Exception $e) { + Log::error('[ConsultationAccess][CreatePencilSpaceJob] Fails', ['error' => $e->getMessage()]); + $this->fail(); + } + } +} diff --git a/src/Models/ConsultationAccessEnquiry.php b/src/Models/ConsultationAccessEnquiry.php index a613f40..347296f 100644 --- a/src/Models/ConsultationAccessEnquiry.php +++ b/src/Models/ConsultationAccessEnquiry.php @@ -22,6 +22,7 @@ * @property string $description * @property int $consultation_user_id * @property string $meeting_link + * @property ?string $meeting_link_type * @property ?string $title * @property ?string $related_type * @property ?int $related_id @@ -42,6 +43,7 @@ class ConsultationAccessEnquiry extends Model 'description', 'consultation_user_id', 'meeting_link', + 'meeting_link_type', 'title', 'related_type', 'related_id', diff --git a/src/Services/ConsultationAccessEnquiryService.php b/src/Services/ConsultationAccessEnquiryService.php index afd6057..2443a79 100644 --- a/src/Services/ConsultationAccessEnquiryService.php +++ b/src/Services/ConsultationAccessEnquiryService.php @@ -8,6 +8,7 @@ use EscolaLms\ConsultationAccess\Dtos\PageDto; use EscolaLms\ConsultationAccess\Dtos\UpdateConsultationAccessEnquiryDto; use EscolaLms\ConsultationAccess\Enum\EnquiryStatusEnum; +use EscolaLms\ConsultationAccess\Enum\MeetingLinkTypeEnum; use EscolaLms\ConsultationAccess\Events\ConsultationAccessEnquiryAdminCreatedEvent; use EscolaLms\ConsultationAccess\Events\ConsultationAccessEnquiryAdminUpdatedEvent; use EscolaLms\ConsultationAccess\Events\ConsultationAccessEnquiryApprovedEvent; @@ -15,6 +16,7 @@ use EscolaLms\ConsultationAccess\Exceptions\ConsultationAccessException; use EscolaLms\ConsultationAccess\Exceptions\EnquiryAlreadyApprovedException; use EscolaLms\ConsultationAccess\Exceptions\TermIsBusyException; +use EscolaLms\ConsultationAccess\Jobs\CreatePencilSpaceJob; use EscolaLms\ConsultationAccess\Models\ConsultationAccessEnquiry; use EscolaLms\ConsultationAccess\Models\ConsultationAccessEnquiryProposedTerm; use EscolaLms\ConsultationAccess\Repositories\Contracts\ConsultationAccessEnquiryProposedTermRepositoryContract; @@ -104,9 +106,14 @@ public function approveByProposedTerm(ApproveConsultationAccessEnquiryDto $dto): 'consultation_user_id' => $consultationUser->getKey(), 'status' => EnquiryStatusEnum::APPROVED, 'meeting_link' => $dto->getMeetingLink(), + 'meeting_link_type' => $dto->getMeetingLink() ? MeetingLinkTypeEnum::CUSTOM : null, ], $enquiry->getKey()); - event(new ConsultationAccessEnquiryApprovedEvent($enquiry->user, $enquiry)); + if (!$dto->getMeetingLink()) { + CreatePencilSpaceJob::dispatch($enquiry->getKey()); + } else { + event(new ConsultationAccessEnquiryApprovedEvent($enquiry->user, $enquiry)); + } }); } diff --git a/src/routes.php b/src/routes.php index 72d77e2..88d68ec 100644 --- a/src/routes.php +++ b/src/routes.php @@ -17,6 +17,7 @@ Route::get(null, [ConsultationAccessEnquiryApiController::class, 'index']); Route::post(null, [ConsultationAccessEnquiryApiController::class, 'create']); Route::get('{id}', [ConsultationAccessEnquiryApiController::class, 'read'])->whereNumber('id'); + Route::get('{id}/join', [ConsultationAccessEnquiryApiController::class, 'join'])->whereNumber('id'); Route::patch('{id}', [ConsultationAccessEnquiryApiController::class, 'update'])->whereNumber('id'); Route::delete('{id}', [ConsultationAccessEnquiryApiController::class, 'delete'])->whereNumber('id'); }); diff --git a/tests/Api/Admin/ConsultationAccessEnquiryAdminApproveApiTest.php b/tests/Api/Admin/ConsultationAccessEnquiryAdminApproveApiTest.php index 051bce8..4ce5abc 100644 --- a/tests/Api/Admin/ConsultationAccessEnquiryAdminApproveApiTest.php +++ b/tests/Api/Admin/ConsultationAccessEnquiryAdminApproveApiTest.php @@ -4,12 +4,15 @@ use EscolaLms\ConsultationAccess\Database\Seeders\ConsultationAccessPermissionSeeder; use EscolaLms\ConsultationAccess\Enum\EnquiryStatusEnum; +use EscolaLms\ConsultationAccess\Enum\MeetingLinkTypeEnum; +use EscolaLms\ConsultationAccess\Jobs\CreatePencilSpaceJob; use EscolaLms\Consultations\Enum\ConsultationTermStatusEnum; use EscolaLms\ConsultationAccess\Models\ConsultationAccessEnquiry; use EscolaLms\ConsultationAccess\Models\ConsultationAccessEnquiryProposedTerm; use EscolaLms\ConsultationAccess\Tests\TestCase; use EscolaLms\Consultations\Models\ConsultationUserPivot; use EscolaLms\Core\Tests\CreatesUsers; +use Illuminate\Support\Facades\Bus; class ConsultationAccessEnquiryAdminApproveApiTest extends TestCase { @@ -44,6 +47,7 @@ public function testConsultationAccessEnquiryAdminApprove(): void 'status' => EnquiryStatusEnum::APPROVED, 'consultation_id' => $proposedTerm->consultationAccessEnquiry->consultation_id, 'meeting_link' => $meetingLink, + 'meeting_link_type' => MeetingLinkTypeEnum::CUSTOM, ]); $this->assertDatabaseHas('consultation_user', [ @@ -93,4 +97,24 @@ public function testConsultationAccessEnquiryAdminApproveProposedTermIsBusyExcep 'message' => __('Term is busy'), ]); } + + public function testConsultationAccessEnquiryAdminApproveWithoutMeetingLink(): void + { + Bus::fake([CreatePencilSpaceJob::class]); + + /** @var ConsultationAccessEnquiryProposedTerm $proposedTerm */ + $proposedTerm = ConsultationAccessEnquiryProposedTerm::factory()->create(); + $this->actingAs($this->makeAdmin(), 'api') + ->postJson('api/admin/consultation-access-enquiries/approve/' . $proposedTerm->getKey()) + ->assertOk(); + + $proposedTerm->refresh(); + + $this->assertDatabaseHas('consultation_access_enquiries', [ + 'status' => EnquiryStatusEnum::APPROVED, + 'consultation_id' => $proposedTerm->consultationAccessEnquiry->consultation_id, + ]); + + Bus::assertDispatched(CreatePencilSpaceJob::class); + } } diff --git a/tests/Api/ConsultationAccessEnquiryJoinApiTest.php b/tests/Api/ConsultationAccessEnquiryJoinApiTest.php new file mode 100644 index 0000000..3be6328 --- /dev/null +++ b/tests/Api/ConsultationAccessEnquiryJoinApiTest.php @@ -0,0 +1,64 @@ +seed(ConsultationAccessPermissionSeeder::class); + $this->student = $this->makeStudent(); + $this->enquiry = ConsultationAccessEnquiry::factory() + ->state(['user_id' => $this->student->getKey()]) + ->has(ConsultationAccessEnquiryProposedTerm::factory()->count(4)) + ->create(); + } + + public function testConsultationAccessEnquiryJoinUnauthorized(): void + { + $this->getJson('api/consultation-access-enquiries/' . $this->enquiry->getKey() . '/join') + ->assertUnauthorized(); + } + + public function testConsultationAccessEnquiryJoinForbidden(): void + { + $this->actingAs($this->makeStudent(), 'api') + ->getJson('api/consultation-access-enquiries/' . $this->enquiry->getKey() . '/join') + ->assertForbidden(); + } + + public function testConsultationAccessEnquiryJoinToPencilSpace(): void + { + PencilSpace::fake(); + + $this->enquiry->update([ + 'meeting_link' => $this->faker->url, + 'meeting_link_type' => MeetingLinkTypeEnum::PENCIL_SPACES, + ]); + + $this->actingAs($this->student, 'api') + ->getJson('api/consultation-access-enquiries/' . $this->enquiry->getKey() . '/join') + ->assertOk() + ->assertJsonStructure([ + 'data' => [ + 'id', + 'meeting_link', + 'meeting_link_type', + ], + ]); + } +} diff --git a/tests/Feature/CreatePencilSpaceJobTest.php b/tests/Feature/CreatePencilSpaceJobTest.php new file mode 100644 index 0000000..6195cf1 --- /dev/null +++ b/tests/Feature/CreatePencilSpaceJobTest.php @@ -0,0 +1,64 @@ +faker->numberBetween(1)); + Event::assertNothingDispatched(); + } + + public function testCreatePencilSpace(): void + { + PencilSpace::fake(); + Event::fake([ConsultationAccessEnquiryApprovedEvent::class]); + + /** @var ConsultationAccessEnquiry $enquiry */ + $enquiry = ConsultationAccessEnquiry::factory() + ->state([ + 'status' => EnquiryStatusEnum::APPROVED, + 'meeting_link' => null, + 'meeting_link_type' => null, + ]) + ->create(); + + CreatePencilSpaceJob::dispatch($enquiry->getKey()); + $enquiry->refresh(); + $this->assertNotNull($enquiry->meeting_link); + $this->assertEquals(MeetingLinkTypeEnum::PENCIL_SPACES, $enquiry->meeting_link_type); + + Event::assertDispatched(ConsultationAccessEnquiryApprovedEvent::class); + } + + public function testCreatePencilSpaceWithNonExistentUser(): void + { + Event::fake([ConsultationAccessEnquiryApprovedEvent::class]); + + $user = $this->makeStudent(); + + $enquiry = ConsultationAccessEnquiry::factory() + ->state([ + 'user_id' => $user->getKey(), + ]) + ->create(); + + $user->delete(); + CreatePencilSpaceJob::dispatch($enquiry->getKey()); + Event::assertNothingDispatched(); + } +}