Added importer details endpoint

This commit is contained in:
Constantin Graf
2024-04-16 16:52:13 +02:00
committed by Gregor Vostrak
parent fe61a54c35
commit 1f79d5e50a
15 changed files with 180 additions and 9 deletions

View File

@@ -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<array{ key: string, name: string, description: string }>}
*/
public function index(Organization $organization, ImporterProvider $importerProvider): JsonResponse
{
$this->checkPermission($organization, 'import');
$importers = $importerProvider->getImporters();
/** @var array<array{ key: string, name: string, description: string }> $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
*

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -13,4 +13,8 @@ interface ImporterContract
public function importData(string $data): void;
public function getReport(): ReportDto;
public function getName(): string;
public function getDescription(): string;
}

View File

@@ -32,6 +32,14 @@ class ImporterProvider
return array_keys($this->importers);
}
/**
* @return array<string, class-string<ImporterContract>>
*/
public function getImporters(): array
{
return $this->importers;
}
public function getImporter(string $type): ImporterContract
{
if (! array_key_exists($type, $this->importers)) {

View File

@@ -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');
}
}

View File

@@ -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');
}
}

32
lang/en/importer.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
return [
'clockify_time_entries' => [
'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.',
],
];

View File

@@ -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');
});
});

View File

@@ -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());
}

View File

@@ -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'),
]);

View File

@@ -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(),
]);
}

View File

@@ -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

View File

@@ -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',
]);
}

View File

@@ -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);
}