From 1f79d5e50a4b0b4e9ff4b83deac93b6029d5552a Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Tue, 16 Apr 2024 16:52:13 +0200 Subject: [PATCH] Added importer details endpoint --- .../Controllers/Api/V1/ImportController.php | 35 +++++++++++++ .../Importers/ClockifyProjectsImporter.php | 12 +++++ .../Importers/ClockifyTimeEntriesImporter.php | 12 +++++ .../Import/Importers/ImporterContract.php | 4 ++ .../Import/Importers/ImporterProvider.php | 8 +++ .../Import/Importers/TogglDataImporter.php | 12 +++++ .../Importers/TogglTimeEntriesImporter.php | 12 +++++ lang/en/importer.php | 32 ++++++++++++ routes/api.php | 1 + tests/Feature/DeleteTeamTest.php | 4 +- .../Endpoint/Api/V1/ImportEndpointTest.php | 49 +++++++++++++++++-- .../Unit/Endpoint/Api/V1/TaskEndpointTest.php | 2 +- .../Filament/OrganizationResourceTest.php | 2 +- tests/Unit/Model/TimeEntryModelTest.php | 2 +- tests/Unit/Service/UserServiceTest.php | 2 +- 15 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 lang/en/importer.php diff --git a/app/Http/Controllers/Api/V1/ImportController.php b/app/Http/Controllers/Api/V1/ImportController.php index 3b9cbf2d..898a85c7 100644 --- a/app/Http/Controllers/Api/V1/ImportController.php +++ b/app/Http/Controllers/Api/V1/ImportController.php @@ -6,6 +6,8 @@ namespace App\Http\Controllers\Api\V1; use App\Http\Requests\V1\Import\ImportRequest; use App\Models\Organization; +use App\Service\Import\Importers\ImporterContract; +use App\Service\Import\Importers\ImporterProvider; use App\Service\Import\Importers\ImportException; use App\Service\Import\ImportService; use Illuminate\Auth\Access\AuthorizationException; @@ -13,6 +15,39 @@ use Illuminate\Http\JsonResponse; class ImportController extends Controller { + /** + * Get information about available importers + * + * @operationId getImporters + * + * @throws AuthorizationException + * + * @response array{data: array} + */ + public function index(Organization $organization, ImporterProvider $importerProvider): JsonResponse + { + $this->checkPermission($organization, 'import'); + + $importers = $importerProvider->getImporters(); + + /** @var array $importersResponse */ + $importersResponse = []; + + foreach ($importers as $key => $importerClass) { + /** @var ImporterContract $importer */ + $importer = new $importerClass(); + $importersResponse[] = [ + 'key' => $key, + 'name' => $importer->getName(), + 'description' => $importer->getDescription(), + ]; + } + + return new JsonResponse([ + 'data' => $importersResponse, + ], 200); + } + /** * Import data into the organization * diff --git a/app/Service/Import/Importers/ClockifyProjectsImporter.php b/app/Service/Import/Importers/ClockifyProjectsImporter.php index d71c6cd9..daa0f003 100644 --- a/app/Service/Import/Importers/ClockifyProjectsImporter.php +++ b/app/Service/Import/Importers/ClockifyProjectsImporter.php @@ -84,4 +84,16 @@ class ClockifyProjectsImporter extends DefaultImporter } } } + + #[\Override] + public function getName(): string + { + return __('importer.clockify_projects.name'); + } + + #[\Override] + public function getDescription(): string + { + return __('importer.clockify_projects.description'); + } } diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php index f766f002..f5ad09dd 100644 --- a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -158,4 +158,16 @@ class ClockifyTimeEntriesImporter extends DefaultImporter } } } + + #[\Override] + public function getName(): string + { + return __('importer.toggl_data_importer.name'); + } + + #[\Override] + public function getDescription(): string + { + return __('importer.toggl_data_importer.description'); + } } diff --git a/app/Service/Import/Importers/ImporterContract.php b/app/Service/Import/Importers/ImporterContract.php index a4846167..3a1b819a 100644 --- a/app/Service/Import/Importers/ImporterContract.php +++ b/app/Service/Import/Importers/ImporterContract.php @@ -13,4 +13,8 @@ interface ImporterContract public function importData(string $data): void; public function getReport(): ReportDto; + + public function getName(): string; + + public function getDescription(): string; } diff --git a/app/Service/Import/Importers/ImporterProvider.php b/app/Service/Import/Importers/ImporterProvider.php index 56eacf08..6edf4a98 100644 --- a/app/Service/Import/Importers/ImporterProvider.php +++ b/app/Service/Import/Importers/ImporterProvider.php @@ -32,6 +32,14 @@ class ImporterProvider return array_keys($this->importers); } + /** + * @return array> + */ + public function getImporters(): array + { + return $this->importers; + } + public function getImporter(string $type): ImporterContract { if (! array_key_exists($type, $this->importers)) { diff --git a/app/Service/Import/Importers/TogglDataImporter.php b/app/Service/Import/Importers/TogglDataImporter.php index 1adf5e1f..eacb14a0 100644 --- a/app/Service/Import/Importers/TogglDataImporter.php +++ b/app/Service/Import/Importers/TogglDataImporter.php @@ -114,4 +114,16 @@ class TogglDataImporter extends DefaultImporter throw new ImportException('Unknown error'); } } + + #[\Override] + public function getName(): string + { + return __('importer.toggl_data_importer.name'); + } + + #[\Override] + public function getDescription(): string + { + return __('importer.toggl_data_importer.description'); + } } diff --git a/app/Service/Import/Importers/TogglTimeEntriesImporter.php b/app/Service/Import/Importers/TogglTimeEntriesImporter.php index 01d4cfd1..6f082d0f 100644 --- a/app/Service/Import/Importers/TogglTimeEntriesImporter.php +++ b/app/Service/Import/Importers/TogglTimeEntriesImporter.php @@ -142,4 +142,16 @@ class TogglTimeEntriesImporter extends DefaultImporter } } } + + #[\Override] + public function getName(): string + { + return __('importer.toggl_time_entries.name'); + } + + #[\Override] + public function getDescription(): string + { + return __('importer.toggl_time_entries.description'); + } } diff --git a/lang/en/importer.php b/lang/en/importer.php new file mode 100644 index 00000000..b546ad46 --- /dev/null +++ b/lang/en/importer.php @@ -0,0 +1,32 @@ + [ + 'name' => 'Clockify Time Entries', + 'description' => 'Go to REPORTS -> TIME -> Detailed in the navigation on the left. '. + 'Now select the date range that you want to export in the right top. '. + 'It is currently not possible to select more than one year. You can export each year seperatly and import them one after another.'. + 'Now click Export -> Save as CSV. The Export dropdown is in the header of the export table left of the printer symbol.', + ], + 'clockify_projects' => [ + 'name' => 'Clockify Projects', + 'description' => 'Go to PROJECTS in the navigation on the left. '. + 'Now click on the three dots on the right of the project that you want to export and select Export. '. + 'Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.', + ], + 'toggl_data_importer' => [ + 'name' => 'Toggl Data Importer', + 'description' => 'Go to Admin -> Settings -> Data export. '. + 'Under "Data Export" select all items for export and click on "Export to email". '. + 'You will receive an email with a download link. Download the ZIP and upload it here. '. + 'The "Data Export" exports everything except time entries. '. + 'If you want to also import time entries use the "Toggl Time Entries" importer afterwards.', + ], + 'toggl_time_entries' => [ + 'name' => 'Toggl Time Entries', + 'description' => 'Important: If you want to import a Toggl organization use the "Toggl Data Importer" before using this importer, since this export contains more details. '. + 'Go to Admin -> Settings -> Data export. Under "Time entries" select the year you want to export and click on "Export time entries". You can export all years one after another and import them one after another.', + ], +]; diff --git a/routes/api.php b/routes/api.php index 48ff142b..6fbc3fef 100644 --- a/routes/api.php +++ b/routes/api.php @@ -106,6 +106,7 @@ Route::middleware([ // Import routes Route::name('import.')->group(static function () { + Route::get('/organizations/{organization}/importers', [ImportController::class, 'index'])->name('index'); Route::post('/organizations/{organization}/import', [ImportController::class, 'import'])->name('import'); }); }); diff --git a/tests/Feature/DeleteTeamTest.php b/tests/Feature/DeleteTeamTest.php index 741cac38..dff844ef 100644 --- a/tests/Feature/DeleteTeamTest.php +++ b/tests/Feature/DeleteTeamTest.php @@ -25,7 +25,7 @@ class DeleteTeamTest extends TestCase $otherUser = User::factory()->create(), ['role' => 'test-role'] ); - $response = $this->delete('/teams/'.$team->id); + $response = $this->delete('/teams/'.$team->getKey()); $this->assertNull($team->fresh()); $this->assertCount(0, $otherUser->fresh()->teams); @@ -35,7 +35,7 @@ class DeleteTeamTest extends TestCase { $this->actingAs($user = User::factory()->withPersonalOrganization()->create()); - $response = $this->delete('/teams/'.$user->currentTeam->id); + $response = $this->delete('/teams/'.$user->currentTeam->getKey()); $this->assertNotNull($user->currentTeam->fresh()); } diff --git a/tests/Unit/Endpoint/Api/V1/ImportEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ImportEndpointTest.php index 9c826cf5..21bb6d51 100644 --- a/tests/Unit/Endpoint/Api/V1/ImportEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ImportEndpointTest.php @@ -13,6 +13,49 @@ use Mockery\MockInterface; class ImportEndpointTest extends ApiEndpointTestAbstract { + public function test_index_fails_if_user_does_not_have_permission() + { + // Arrange + $data = $this->createUserWithPermission([ + ]); + + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.import.index', ['organization' => $data->organization->getKey()])); + + // Assert + $response->assertForbidden(); + } + + public function test_index_returns_importers_if_user_has_permission() + { + // Arrange + $data = $this->createUserWithPermission([ + 'import', + ]); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.import.index', ['organization' => $data->organization->getKey()])); + + // Assert + $response->assertOk(); + $response->assertJsonStructure([ + 'data' => [ + [ + 'key', + 'name', + 'description', + ], + ], + ]); + $toggleTimeEntries = collect($response->json('data'))->where('key', 'toggl_time_entries')->first(); + $this->assertSame('toggl_time_entries', $toggleTimeEntries['key']); + $this->assertSame('Toggl Time Entries', $toggleTimeEntries['name']); + $this->assertSame(__('importer.toggl_time_entries.description'), $toggleTimeEntries['description']); + } + public function test_import_fails_if_user_does_not_have_permission() { // Arrange @@ -22,7 +65,7 @@ class ImportEndpointTest extends ApiEndpointTestAbstract Passport::actingAs($data->user); // Act - $response = $this->postJson(route('api.v1.import.import', ['organization' => $data->organization->id]), [ + $response = $this->postJson(route('api.v1.import.import', ['organization' => $data->organization->getKey()]), [ 'type' => 'toggl_time_entries', 'data' => base64_encode('some data'), 'options' => [], @@ -48,7 +91,7 @@ class ImportEndpointTest extends ApiEndpointTestAbstract Passport::actingAs($user->user); // Act - $response = $this->postJson(route('api.v1.import.import', ['organization' => $user->organization->id]), [ + $response = $this->postJson(route('api.v1.import.import', ['organization' => $user->organization->getKey()]), [ 'type' => 'toggl_time_entries', 'data' => base64_encode('some data'), ]); @@ -84,7 +127,7 @@ class ImportEndpointTest extends ApiEndpointTestAbstract Passport::actingAs($user->user); // Act - $response = $this->postJson(route('api.v1.import.import', ['organization' => $user->organization->id]), [ + $response = $this->postJson(route('api.v1.import.import', ['organization' => $user->organization->getKey()]), [ 'type' => 'toggl_time_entries', 'data' => base64_encode('some data'), ]); diff --git a/tests/Unit/Endpoint/Api/V1/TaskEndpointTest.php b/tests/Unit/Endpoint/Api/V1/TaskEndpointTest.php index 47247eea..6faff64b 100644 --- a/tests/Unit/Endpoint/Api/V1/TaskEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/TaskEndpointTest.php @@ -233,7 +233,7 @@ class TaskEndpointTest extends ApiEndpointTestAbstract $this->assertDatabaseHas(Task::class, [ 'name' => 'Task 1', 'project_id' => $project->getKey(), - 'organization_id' => $data->organization->id, + 'organization_id' => $data->organization->getKey(), ]); } diff --git a/tests/Unit/Filament/OrganizationResourceTest.php b/tests/Unit/Filament/OrganizationResourceTest.php index 7bc172c4..86c6be60 100644 --- a/tests/Unit/Filament/OrganizationResourceTest.php +++ b/tests/Unit/Filament/OrganizationResourceTest.php @@ -28,7 +28,7 @@ class OrganizationResourceTest extends FilamentTestCase // Arrange $user = User::factory()->create(); $organizations = Organization::factory()->state([ - 'user_id' => $user->id, + 'user_id' => $user->getKey(), ])->createMany(5); // Act diff --git a/tests/Unit/Model/TimeEntryModelTest.php b/tests/Unit/Model/TimeEntryModelTest.php index 14f7af7c..a7126c63 100644 --- a/tests/Unit/Model/TimeEntryModelTest.php +++ b/tests/Unit/Model/TimeEntryModelTest.php @@ -113,7 +113,7 @@ class TimeEntryModelTest extends ModelTestAbstract $this->assertSame('UTC', $timeEntry->start->getTimezone()->toRegionName()); $this->assertSame('2021-01-01 13:00:00', $timeEntry->start->toDateTimeString()); $this->assertDatabaseHas(TimeEntry::class, [ - 'id' => $timeEntry->id, + 'id' => $timeEntry->getKey(), 'start' => '2021-01-01 13:00:00', ]); } diff --git a/tests/Unit/Service/UserServiceTest.php b/tests/Unit/Service/UserServiceTest.php index b8d28acd..e9d522c0 100644 --- a/tests/Unit/Service/UserServiceTest.php +++ b/tests/Unit/Service/UserServiceTest.php @@ -65,7 +65,7 @@ class UserServiceTest extends TestCase $userService->changeOwnership($organization, $newOwner); // Assert - $this->assertSame($newOwner->id, $organization->refresh()->user_id); + $this->assertSame($newOwner->getKey(), $organization->refresh()->user_id); $this->assertSame(Role::Owner->value, Membership::whereBelongsTo($newOwner)->whereBelongsTo($organization)->firstOrFail()->role); $this->assertSame(Role::Admin->value, Membership::whereBelongsTo($oldOwner)->whereBelongsTo($organization)->firstOrFail()->role); }