]> BookStack Code Mirror - bookstack/commitdiff
ZIP Imports: Fleshed out continue page, Added testing
authorDan Brown <redacted>
Sun, 3 Nov 2024 17:28:18 +0000 (17:28 +0000)
committerDan Brown <redacted>
Sun, 3 Nov 2024 17:28:18 +0000 (17:28 +0000)
app/Exports/Controllers/ImportController.php
app/Exports/Import.php
lang/en/entities.php
resources/views/exports/import-show.blade.php
routes/web.php
tests/Exports/ZipImportTest.php

index 582fff975d2880a4928e3f884fd320d18071e74d..787fd1b27e08227f5556d21dd9bcc10e061d8ad0 100644 (file)
@@ -23,9 +23,8 @@ class ImportController extends Controller
      * Show the view to start a new import, and also list out the existing
      * in progress imports that are visible to the user.
      */
-    public function start(Request $request)
+    public function start()
     {
-        // TODO - Test visibility access for listed items
         $imports = $this->imports->getVisibleImports();
 
         $this->setPageTitle(trans('entities.import'));
@@ -64,7 +63,6 @@ class ImportController extends Controller
      */
     public function show(int $id)
     {
-        // TODO - Test visibility access
         $import = $this->imports->findVisible($id);
 
         $this->setPageTitle(trans('entities.import_continue'));
@@ -74,12 +72,23 @@ class ImportController extends Controller
         ]);
     }
 
+    public function run(int $id)
+    {
+        // TODO - Test access/visibility
+
+        $import = $this->imports->findVisible($id);
+
+        // TODO  - Run import
+           // Validate again before
+        // TODO - Redirect to result
+        // TOOD - Or redirect back with errors
+    }
+
     /**
      * Delete an active pending import from the filesystem and database.
      */
     public function delete(int $id)
     {
-        // TODO - Test visibility access
         $import = $this->imports->findVisible($id);
         $this->imports->deleteImport($import);
 
index 520d8ea6cc8445635460b1a2709dcf307ca6e40a..8400382fd0d757a0974064e1b36e2cee9272fa0e 100644 (file)
@@ -3,11 +3,14 @@
 namespace BookStack\Exports;
 
 use BookStack\Activity\Models\Loggable;
+use BookStack\Users\Models\User;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 /**
+ * @property int $id
  * @property string $path
  * @property string $name
  * @property int $size - ZIP size in bytes
@@ -17,6 +20,7 @@ use Illuminate\Database\Eloquent\Model;
  * @property int $created_by
  * @property Carbon $created_at
  * @property Carbon $updated_at
+ * @property User $createdBy
  */
 class Import extends Model implements Loggable
 {
@@ -59,4 +63,9 @@ class Import extends Model implements Loggable
     {
         return "({$this->id}) {$this->name}";
     }
+
+    public function createdBy(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
 }
index e2d8e47c5929f4eed985ef37d62e5c7f9006186c..4f5a530049bc9cf45be6f4694d4f6d602e686994 100644 (file)
@@ -51,7 +51,11 @@ return [
     'import_pending' => 'Pending Imports',
     'import_pending_none' => 'No imports have been started.',
     'import_continue' => 'Continue Import',
+    'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',
     'import_run' => 'Run Import',
+    'import_size' => 'Import ZIP Size:',
+    'import_uploaded_at' => 'Uploaded:',
+    'import_uploaded_by' => 'Uploaded by:',
     'import_delete_confirm' => 'Are you sure you want to delete this import?',
     'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',
 
index 843a052468cb396332a3b5654a8440d9e9779f1c..ac1b8a45d062d893b9a2bf93855ef67961c08dde 100644 (file)
@@ -6,7 +6,44 @@
 
         <main class="card content-wrap auto-height mt-xxl">
             <h1 class="list-heading">{{ trans('entities.import_continue') }}</h1>
-            <form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
+            <p class="text-muted">{{ trans('entities.import_continue_desc') }}</p>
+
+            <div class="mb-m">
+                @php
+                    $type = $import->getType();
+                @endphp
+                <div class="flex-container-row items-center justify-space-between wrap">
+                    <div class="py-s">
+                        <p class="text-{{ $type }} mb-xs bold">@icon($type) {{ $import->name }}</p>
+                        @if($type === 'book')
+                            <p class="text-chapter mb-xs ml-l">@icon('chapter') {{ trans_choice('entities.x_chapters', $import->chapter_count) }}</p>
+                        @endif
+                        @if($type === 'book' || $type === 'chapter')
+                            <p class="text-page mb-xs ml-l">@icon('page') {{ trans_choice('entities.x_pages', $import->page_count) }}</p>
+                        @endif
+                    </div>
+                    <div class="py-s">
+                        <div class="opacity-80">
+                            <strong>{{ trans('entities.import_size') }}</strong>
+                            <span>{{ $import->getSizeString() }}</span>
+                        </div>
+                        <div class="opacity-80">
+                            <strong>{{ trans('entities.import_uploaded_at') }}</strong>
+                            <span title="{{ $import->created_at->toISOString() }}">{{ $import->created_at->diffForHumans() }}</span>
+                        </div>
+                        @if($import->createdBy)
+                            <div class="opacity-80">
+                                <strong>{{ trans('entities.import_uploaded_by') }}</strong>
+                                <a href="{{ $import->createdBy->getProfileUrl() }}">{{ $import->createdBy->name }}</a>
+                            </div>
+                        @endif
+                    </div>
+                </div>
+            </div>
+
+            <form id="import-run-form"
+                  action="{{ $import->getUrl() }}"
+                  method="POST">
                 {{ csrf_field() }}
             </form>
 
@@ -23,7 +60,7 @@
                         <button type="submit" form="import-delete-form" class="text-link small text-item">{{ trans('common.confirm') }}</button>
                     </div>
                 </div>
-                <button type="submit" class="button">{{ trans('entities.import_run') }}</button>
+                <button type="submit" form="import-run-form" class="button">{{ trans('entities.import_run') }}</button>
             </div>
         </main>
     </div>
index c490bb3b34ea3bc0b8762aeaa35af381361d96d4..85f83352859a8d49b155f93202f786f155e01c47 100644 (file)
@@ -210,6 +210,7 @@ Route::middleware('auth')->group(function () {
     Route::get('/import', [ExportControllers\ImportController::class, 'start']);
     Route::post('/import', [ExportControllers\ImportController::class, 'upload']);
     Route::get('/import/{id}', [ExportControllers\ImportController::class, 'show']);
+    Route::post('/import/{id}', [ExportControllers\ImportController::class, 'run']);
     Route::delete('/import/{id}', [ExportControllers\ImportController::class, 'delete']);
 
     // Other Pages
index c9d255b1e9738147074c0462c18a817caa96b39c..b9a8598fabe6c00fd2f7db4efdb44c70f7ea858c 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Tests\Exports;
 
+use BookStack\Activity\ActivityType;
+use BookStack\Exports\Import;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Testing\TestResponse;
 use Tests\TestCase;
@@ -35,6 +37,25 @@ class ZipImportTest extends TestCase
         $resp->assertSeeText('Select ZIP file to upload');
     }
 
+    public function test_import_page_pending_import_visibility_limited()
+    {
+        $user = $this->users->viewer();
+        $admin = $this->users->admin();
+        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
+        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
+        $this->permissions->grantUserRolePermissions($user, ['content-import']);
+
+        $resp = $this->actingAs($user)->get('/import');
+        $resp->assertSeeText('MySuperUserImport');
+        $resp->assertDontSeeText('MySuperAdminImport');
+
+        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
+
+        $resp = $this->actingAs($user)->get('/import');
+        $resp->assertSeeText('MySuperUserImport');
+        $resp->assertSeeText('MySuperAdminImport');
+    }
+
     public function test_zip_read_errors_are_shown_on_validation()
     {
         $invalidUpload = $this->files->uploadedImage('image.zip');
@@ -105,6 +126,125 @@ class ZipImportTest extends TestCase
         $resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');
     }
 
+    public function test_import_upload_success()
+    {
+        $admin = $this->users->admin();
+        $this->actingAs($admin);
+        $resp = $this->runImportFromFile($this->zipUploadFromData([
+            'book' => [
+                'name' => 'My great book name',
+                'chapters' => [
+                    [
+                        'name' => 'my chapter',
+                        'pages' => [
+                            [
+                                'name' => 'my chapter page',
+                            ]
+                        ]
+                    ]
+                ],
+                'pages' => [
+                    [
+                        'name' => 'My page',
+                    ]
+                ],
+            ],
+        ]));
+
+        $this->assertDatabaseHas('imports', [
+            'name' => 'My great book name',
+            'book_count' => 1,
+            'chapter_count' => 1,
+            'page_count' => 2,
+            'created_by' => $admin->id,
+        ]);
+
+        /** @var Import $import */
+        $import = Import::query()->latest()->first();
+        $resp->assertRedirect("/import/{$import->id}");
+        $this->assertFileExists(storage_path($import->path));
+        $this->assertActivityExists(ActivityType::IMPORT_CREATE);
+    }
+
+    public function test_import_show_page()
+    {
+        $import = Import::factory()->create(['name' => 'MySuperAdminImport']);
+
+        $resp = $this->asAdmin()->get("/import/{$import->id}");
+        $resp->assertOk();
+        $resp->assertSee('MySuperAdminImport');
+    }
+
+    public function test_import_show_page_access_limited()
+    {
+        $user = $this->users->viewer();
+        $admin = $this->users->admin();
+        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
+        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
+        $this->actingAs($user);
+
+        $this->get("/import/{$userImport->id}")->assertRedirect('/');
+        $this->get("/import/{$adminImport->id}")->assertRedirect('/');
+
+        $this->permissions->grantUserRolePermissions($user, ['content-import']);
+
+        $this->get("/import/{$userImport->id}")->assertOk();
+        $this->get("/import/{$adminImport->id}")->assertStatus(404);
+
+        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
+
+        $this->get("/import/{$userImport->id}")->assertOk();
+        $this->get("/import/{$adminImport->id}")->assertOk();
+    }
+
+    public function test_import_delete()
+    {
+        $this->asAdmin();
+        $this->runImportFromFile($this->zipUploadFromData([
+            'book' => [
+                'name' => 'My great book name'
+            ],
+        ]));
+
+        /** @var Import $import */
+        $import = Import::query()->latest()->first();
+        $this->assertDatabaseHas('imports', [
+            'id' => $import->id,
+            'name' => 'My great book name'
+        ]);
+        $this->assertFileExists(storage_path($import->path));
+
+        $resp = $this->delete("/import/{$import->id}");
+
+        $resp->assertRedirect('/import');
+        $this->assertActivityExists(ActivityType::IMPORT_DELETE);
+        $this->assertDatabaseMissing('imports', [
+            'id' => $import->id,
+        ]);
+        $this->assertFileDoesNotExist(storage_path($import->path));
+    }
+
+    public function test_import_delete_access_limited()
+    {
+        $user = $this->users->viewer();
+        $admin = $this->users->admin();
+        $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
+        $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
+        $this->actingAs($user);
+
+        $this->delete("/import/{$userImport->id}")->assertRedirect('/');
+        $this->delete("/import/{$adminImport->id}")->assertRedirect('/');
+
+        $this->permissions->grantUserRolePermissions($user, ['content-import']);
+
+        $this->delete("/import/{$userImport->id}")->assertRedirect('/import');
+        $this->delete("/import/{$adminImport->id}")->assertStatus(404);
+
+        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
+
+        $this->delete("/import/{$adminImport->id}")->assertRedirect('/import');
+    }
+
     protected function runImportFromFile(UploadedFile $file): TestResponse
     {
         return $this->call('POST', '/import', [], [], ['file' => $file]);
Morty Proxy This is a proxified and sanitized view of the page, visit original site.