]> BookStack Code Mirror - bookstack/commitdiff
Sorting: Covered sort set management with tests
authorDan Brown <redacted>
Mon, 10 Feb 2025 17:14:06 +0000 (17:14 +0000)
committerDan Brown <redacted>
Mon, 10 Feb 2025 17:19:49 +0000 (17:19 +0000)
app/Sorting/SortSetController.php
database/factories/Entities/Models/BookFactory.php
lang/en/settings.php
resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php
tests/Sorting/SortSetTest.php [new file with mode: 0644]

index b0ad2a7d766f10f2fccc86b5406e2c39726a1a58..7b1c0bc4160fd10b131334715ad4be2d852c1325 100644 (file)
@@ -4,14 +4,13 @@ namespace BookStack\Sorting;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Http\Controller;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Http\Controller;
-use BookStack\Http\Request;
+use Illuminate\Http\Request;
 
 class SortSetController extends Controller
 {
     public function __construct()
     {
         $this->middleware('can:settings-manage');
 
 class SortSetController extends Controller
 {
     public function __construct()
     {
         $this->middleware('can:settings-manage');
-        // TODO - Test
     }
 
     public function create()
     }
 
     public function create()
index 9cb8e971c6e14e16542ea4d203da45a4d3511ff5..29403a2949c5d4b0bcdb6e3c4944e1324f6e04de 100644 (file)
@@ -26,7 +26,9 @@ class BookFactory extends Factory
             'name'        => $this->faker->sentence(),
             'slug'        => Str::random(10),
             'description' => $description,
             'name'        => $this->faker->sentence(),
             'slug'        => Str::random(10),
             'description' => $description,
-            'description_html' => '<p>' . e($description) . '</p>'
+            'description_html' => '<p>' . e($description) . '</p>',
+            'sort_set_id' => null,
+            'default_template_id' => null,
         ];
     }
 }
         ];
     }
 }
index 19ffd92404926df0f1dfd6d5281465f208f677d7..344c186cb2f08c01ac59f827ffe6583902064faf 100644 (file)
@@ -80,6 +80,7 @@ return [
     'sorting_book_default_desc' => 'Select the default sort set to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
     'sorting_sets' => 'Sort Sets',
     'sorting_sets_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
     'sorting_book_default_desc' => 'Select the default sort set to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
     'sorting_sets' => 'Sort Sets',
     'sorting_sets_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
+    'sort_set_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
     'sort_set_create' => 'Create Sort Set',
     'sort_set_edit' => 'Edit Sort Set',
     'sort_set_delete' => 'Delete Sort Set',
     'sort_set_create' => 'Create Sort Set',
     'sort_set_edit' => 'Edit Sort Set',
     'sort_set_delete' => 'Delete Sort Set',
index e977c286e11e8497d2ce8931b53f02456baa1c21..6c0b84047c09233e4b16907190da178f514e4a84 100644 (file)
@@ -6,7 +6,7 @@
         {{ implode(', ', array_map(fn ($op) => $op->getLabel(), $set->getOperations())) }}
     </div>
     <div>
         {{ implode(', ', array_map(fn ($op) => $op->getLabel(), $set->getOperations())) }}
     </div>
     <div>
-        <span title="{{ trans('entities.tags_assigned_books') }}"
+        <span title="{{ trans_choice('settings.sort_set_assigned_to_x_books', $set->books_count ?? 0) }}"
               class="flex fill-area min-width-xxs bold text-right text-book"><span class="opacity-60">@icon('book')</span>{{ $set->books_count ?? 0 }}</span>
     </div>
 </div>
\ No newline at end of file
               class="flex fill-area min-width-xxs bold text-right text-book"><span class="opacity-60">@icon('book')</span>{{ $set->books_count ?? 0 }}</span>
     </div>
 </div>
\ No newline at end of file
diff --git a/tests/Sorting/SortSetTest.php b/tests/Sorting/SortSetTest.php
new file mode 100644 (file)
index 0000000..5f30034
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+
+namespace Sorting;
+
+use BookStack\Activity\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Sorting\SortSet;
+use Tests\Api\TestsApi;
+use Tests\TestCase;
+
+class SortSetTest extends TestCase
+{
+    use TestsApi;
+
+    public function test_manage_settings_permission_required()
+    {
+        $set = SortSet::factory()->create();
+        $user = $this->users->viewer();
+        $this->actingAs($user);
+
+        $actions = [
+            ['GET', '/settings/sorting'],
+            ['POST', '/settings/sorting/sets'],
+            ['GET', "/settings/sorting/sets/{$set->id}"],
+            ['PUT', "/settings/sorting/sets/{$set->id}"],
+            ['DELETE', "/settings/sorting/sets/{$set->id}"],
+        ];
+
+        foreach ($actions as [$method, $path]) {
+            $resp = $this->call($method, $path);
+            $this->assertPermissionError($resp);
+        }
+
+        $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
+
+        foreach ($actions as [$method, $path]) {
+            $resp = $this->call($method, $path);
+            $this->assertNotPermissionError($resp);
+        }
+    }
+
+    public function test_create_flow()
+    {
+        $resp = $this->asAdmin()->get('/settings/sorting');
+        $this->withHtml($resp)->assertLinkExists(url('/settings/sorting/sets/new'));
+
+        $resp = $this->get('/settings/sorting/sets/new');
+        $this->withHtml($resp)->assertElementExists('form[action$="/settings/sorting/sets"] input[name="name"]');
+        $resp->assertSeeText('Name - Alphabetical (Asc)');
+
+        $details = ['name' => 'My new sort', 'sequence' => 'name_asc'];
+        $resp = $this->post('/settings/sorting/sets', $details);
+        $resp->assertRedirect('/settings/sorting');
+
+        $this->assertActivityExists(ActivityType::SORT_SET_CREATE);
+        $this->assertDatabaseHas('sort_sets', $details);
+    }
+
+    public function test_listing_in_settings()
+    {
+        $set = SortSet::factory()->create(['name' => 'My super sort set', 'sequence' => 'name_asc']);
+        $books = Book::query()->limit(5)->get();
+        foreach ($books as $book) {
+            $book->sort_set_id = $set->id;
+            $book->save();
+        }
+
+        $resp = $this->asAdmin()->get('/settings/sorting');
+        $resp->assertSeeText('My super sort set');
+        $resp->assertSeeText('Name - Alphabetical (Asc)');
+        $this->withHtml($resp)->assertElementContains('.item-list-row [title="Assigned to 5 Books"]', '5');
+    }
+
+    public function test_update_flow()
+    {
+        $set = SortSet::factory()->create(['name' => 'My sort set to update', 'sequence' => 'name_asc']);
+
+        $resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}");
+        $respHtml = $this->withHtml($resp);
+        $respHtml->assertElementContains('.configured-option-list', 'Name - Alphabetical (Asc)');
+        $respHtml->assertElementNotContains('.available-option-list', 'Name - Alphabetical (Asc)');
+
+        $updateData = ['name' => 'My updated sort', 'sequence' => 'name_desc,chapters_last'];
+        $resp = $this->put("/settings/sorting/sets/{$set->id}", $updateData);
+
+        $resp->assertRedirect('/settings/sorting');
+        $this->assertActivityExists(ActivityType::SORT_SET_UPDATE);
+        $this->assertDatabaseHas('sort_sets', $updateData);
+    }
+
+    public function test_update_triggers_resort_on_assigned_books()
+    {
+        $book = $this->entities->bookHasChaptersAndPages();
+        $chapter = $book->chapters()->first();
+        $set = SortSet::factory()->create(['name' => 'My sort set to update', 'sequence' => 'name_asc']);
+        $book->sort_set_id = $set->id;
+        $book->save();
+        $chapter->priority = 10000;
+        $chapter->save();
+
+        $resp = $this->asAdmin()->put("/settings/sorting/sets/{$set->id}", ['name' => $set->name, 'sequence' => 'chapters_last']);
+        $resp->assertRedirect('/settings/sorting');
+
+        $chapter->refresh();
+        $this->assertNotEquals(10000, $chapter->priority);
+    }
+
+    public function test_delete_flow()
+    {
+        $set = SortSet::factory()->create();
+
+        $resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}");
+        $resp->assertSeeText('Delete Sort Set');
+
+        $resp = $this->delete("settings/sorting/sets/{$set->id}");
+        $resp->assertRedirect('/settings/sorting');
+
+        $this->assertActivityExists(ActivityType::SORT_SET_DELETE);
+        $this->assertDatabaseMissing('sort_sets', ['id' => $set->id]);
+    }
+
+    public function test_delete_requires_confirmation_if_books_assigned()
+    {
+        $set = SortSet::factory()->create();
+        $books = Book::query()->limit(5)->get();
+        foreach ($books as $book) {
+            $book->sort_set_id = $set->id;
+            $book->save();
+        }
+
+        $resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}");
+        $resp->assertSeeText('Delete Sort Set');
+
+        $resp = $this->delete("settings/sorting/sets/{$set->id}");
+        $resp->assertRedirect("/settings/sorting/sets/{$set->id}#delete");
+        $resp = $this->followRedirects($resp);
+
+        $resp->assertSeeText('This sort set is currently used on 5 book(s). Are you sure you want to delete this?');
+        $this->assertDatabaseHas('sort_sets', ['id' => $set->id]);
+
+        $resp = $this->delete("settings/sorting/sets/{$set->id}", ['confirm' => 'true']);
+        $resp->assertRedirect('/settings/sorting');
+        $this->assertDatabaseMissing('sort_sets', ['id' => $set->id]);
+        $this->assertDatabaseMissing('books', ['sort_set_id' => $set->id]);
+    }
+
+    public function test_page_create_triggers_book_sort()
+    {
+        $book = $this->entities->bookHasChaptersAndPages();
+        $set = SortSet::factory()->create(['sequence' => 'name_asc,chapters_first']);
+        $book->sort_set_id = $set->id;
+        $book->save();
+
+        $resp = $this->actingAsApiEditor()->post("/api/pages", [
+            'book_id' => $book->id,
+            'name' => '1111 page',
+            'markdown' => 'Hi'
+        ]);
+        $resp->assertOk();
+
+        $this->assertDatabaseHas('pages', [
+            'book_id' => $book->id,
+            'name' => '1111 page',
+            'priority' => $book->chapters()->count() + 1,
+        ]);
+    }
+
+    public function test_name_numeric_ordering()
+    {
+        $book = Book::factory()->create();
+        $set = SortSet::factory()->create(['sequence' => 'name_numeric_asc']);
+        $book->sort_set_id = $set->id;
+        $book->save();
+        $this->permissions->regenerateForEntity($book);
+
+        $namesToAdd = [
+            "1 - Pizza",
+            "2.0 - Tomato",
+            "2.5 - Beans",
+            "10 - Bread",
+            "20 - Milk",
+        ];
+
+        foreach ($namesToAdd as $name) {
+            $this->actingAsApiEditor()->post("/api/pages", [
+                'book_id' => $book->id,
+                'name' => $name,
+                'markdown' => 'Hello'
+            ]);
+        }
+
+        foreach ($namesToAdd as $index => $name) {
+            $this->assertDatabaseHas('pages', [
+                'book_id' => $book->id,
+                'name' => $name,
+                'priority' => $index + 1,
+            ]);
+        }
+    }
+}