Skip to content

Commit c908169

Browse files
committed
Slugs: Rolled out history lookup to other types
Added testing to cover. Also added batch recording of child slug pairs on book slug changes.
1 parent dd39369 commit c908169

File tree

8 files changed

+131
-12
lines changed

8 files changed

+131
-12
lines changed

app/Entities/Controllers/BookController.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use BookStack\Activity\Tools\UserEntityWatchOptions;
99
use BookStack\Entities\Queries\BookQueries;
1010
use BookStack\Entities\Queries\BookshelfQueries;
11+
use BookStack\Entities\Queries\EntityQueries;
1112
use BookStack\Entities\Repos\BookRepo;
1213
use BookStack\Entities\Tools\BookContents;
1314
use BookStack\Entities\Tools\Cloner;
@@ -31,6 +32,7 @@ public function __construct(
3132
protected ShelfContext $shelfContext,
3233
protected BookRepo $bookRepo,
3334
protected BookQueries $queries,
35+
protected EntityQueries $entityQueries,
3436
protected BookshelfQueries $shelfQueries,
3537
protected ReferenceFetcher $referenceFetcher,
3638
) {
@@ -127,7 +129,16 @@ public function store(Request $request, ?string $shelfSlug = null)
127129
*/
128130
public function show(Request $request, ActivityQueries $activities, string $slug)
129131
{
130-
$book = $this->queries->findVisibleBySlugOrFail($slug);
132+
try {
133+
$book = $this->queries->findVisibleBySlugOrFail($slug);
134+
} catch (NotFoundException $exception) {
135+
$book = $this->entityQueries->findVisibleByOldSlugs('book', $slug);
136+
if (is_null($book)) {
137+
throw $exception;
138+
}
139+
return redirect($book->getUrl());
140+
}
141+
131142
$bookChildren = (new BookContents($book))->getTree(true);
132143
$bookParentShelves = $book->shelves()->scopes('visible')->get();
133144

app/Entities/Controllers/BookshelfController.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use BookStack\Activity\Models\View;
77
use BookStack\Entities\Queries\BookQueries;
88
use BookStack\Entities\Queries\BookshelfQueries;
9+
use BookStack\Entities\Queries\EntityQueries;
910
use BookStack\Entities\Repos\BookshelfRepo;
1011
use BookStack\Entities\Tools\ShelfContext;
1112
use BookStack\Exceptions\ImageUploadException;
@@ -23,6 +24,7 @@ class BookshelfController extends Controller
2324
public function __construct(
2425
protected BookshelfRepo $shelfRepo,
2526
protected BookshelfQueries $queries,
27+
protected EntityQueries $entityQueries,
2628
protected BookQueries $bookQueries,
2729
protected ShelfContext $shelfContext,
2830
protected ReferenceFetcher $referenceFetcher,
@@ -105,7 +107,16 @@ public function store(Request $request)
105107
*/
106108
public function show(Request $request, ActivityQueries $activities, string $slug)
107109
{
108-
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
110+
try {
111+
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
112+
} catch (NotFoundException $exception) {
113+
$shelf = $this->entityQueries->findVisibleByOldSlugs('bookshelf', $slug);
114+
if (is_null($shelf)) {
115+
throw $exception;
116+
}
117+
return redirect($shelf->getUrl());
118+
}
119+
109120
$this->checkOwnablePermission(Permission::BookshelfView, $shelf);
110121

111122
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([

app/Entities/Controllers/ChapterController.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,15 @@ public function store(Request $request, string $bookSlug)
7777
*/
7878
public function show(string $bookSlug, string $chapterSlug)
7979
{
80-
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
80+
try {
81+
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
82+
} catch (NotFoundException $exception) {
83+
$chapter = $this->entityQueries->findVisibleByOldSlugs('chapter', $chapterSlug, $bookSlug);
84+
if (is_null($chapter)) {
85+
throw $exception;
86+
}
87+
return redirect($chapter->getUrl());
88+
}
8189

8290
$sidebarTree = (new BookContents($chapter->book))->getTree();
8391
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();

app/Entities/Models/BookChild.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ abstract class BookChild extends Entity
1616
{
1717
/**
1818
* Get the book this page sits in.
19+
* @return BelongsTo<Book, $this>
1920
*/
2021
public function book(): BelongsTo
2122
{

app/Entities/Models/Entity.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,14 @@ public function watches(): MorphMany
430430
return $this->morphMany(Watch::class, 'watchable');
431431
}
432432

433+
/**
434+
* Get the related slug history for this entity.
435+
*/
436+
public function slugHistory(): MorphMany
437+
{
438+
return $this->morphMany(SlugHistory::class, 'sluggable');
439+
}
440+
433441
/**
434442
* {@inheritdoc}
435443
*/

app/Entities/Tools/SlugHistory.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace BookStack\Entities\Tools;
44

5+
use BookStack\Entities\Models\Book;
56
use BookStack\Entities\Models\BookChild;
67
use BookStack\Entities\Models\Entity;
8+
use BookStack\Entities\Models\EntityTable;
79
use BookStack\Entities\Models\SlugHistory as SlugHistoryModel;
810
use BookStack\Permissions\PermissionApplicator;
11+
use Illuminate\Support\Facades\DB;
912

1013
class SlugHistory
1114
{
@@ -43,6 +46,23 @@ public function recordForEntity(Entity $entity): void
4346
$entry = new SlugHistoryModel();
4447
$entry->forceFill($info);
4548
$entry->save();
49+
50+
if ($entity instanceof Book) {
51+
$this->recordForBookChildren($entity);
52+
}
53+
}
54+
55+
protected function recordForBookChildren(Book $book): void
56+
{
57+
$query = EntityTable::query()
58+
->select(['type', 'id', 'slug', DB::raw("'{$book->slug}' as parent_slug"), DB::raw('now() as created_at'), DB::raw('now() as updated_at')])
59+
->where('book_id', '=', $book->id)
60+
->whereNotNull('book_id');
61+
62+
SlugHistoryModel::query()->insertUsing(
63+
['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'],
64+
$query
65+
);
4666
}
4767

4868
/**

app/Entities/Tools/TrashCan.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ public function destroyEntity(Entity $entity): int
388388
/**
389389
* Update entity relations to remove or update outstanding connections.
390390
*/
391-
protected function destroyCommonRelations(Entity $entity)
391+
protected function destroyCommonRelations(Entity $entity): void
392392
{
393393
Activity::removeEntity($entity);
394394
$entity->views()->delete();
@@ -402,6 +402,7 @@ protected function destroyCommonRelations(Entity $entity)
402402
$entity->watches()->delete();
403403
$entity->referencesTo()->delete();
404404
$entity->referencesFrom()->delete();
405+
$entity->slugHistory()->delete();
405406

406407
if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) {
407408
$imageService = app()->make(ImageService::class);

tests/Entity/SlugTest.php

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,82 @@ public function test_slug_format()
3333
public function test_old_page_slugs_redirect_to_new_pages()
3434
{
3535
$page = $this->entities->page();
36+
$pageUrl = $page->getUrl();
3637

37-
// Need to save twice since revisions are not generated in seeder.
38-
$this->asAdmin()->put($page->getUrl(), [
39-
'name' => 'super test',
38+
$this->asAdmin()->put($pageUrl, [
39+
'name' => 'super test page',
4040
'html' => '<p></p>',
4141
]);
4242

43-
$page->refresh();
43+
$this->get($pageUrl)
44+
->assertRedirect("/books/{$page->book->slug}/page/super-test-page");
45+
}
46+
47+
public function test_old_shelf_slugs_redirect_to_new_shelf()
48+
{
49+
$shelf = $this->entities->shelf();
50+
$shelfUrl = $shelf->getUrl();
51+
52+
$this->asAdmin()->put($shelf->getUrl(), [
53+
'name' => 'super test shelf',
54+
]);
55+
56+
$this->get($shelfUrl)
57+
->assertRedirect("/shelves/super-test-shelf");
58+
}
59+
60+
public function test_old_book_slugs_redirect_to_new_book()
61+
{
62+
$book = $this->entities->book();
63+
$bookUrl = $book->getUrl();
64+
65+
$this->asAdmin()->put($book->getUrl(), [
66+
'name' => 'super test book',
67+
]);
68+
69+
$this->get($bookUrl)
70+
->assertRedirect("/books/super-test-book");
71+
}
72+
73+
public function test_old_chapter_slugs_redirect_to_new_chapter()
74+
{
75+
$chapter = $this->entities->chapter();
76+
$chapterUrl = $chapter->getUrl();
77+
78+
$this->asAdmin()->put($chapter->getUrl(), [
79+
'name' => 'super test chapter',
80+
]);
81+
82+
$this->get($chapterUrl)
83+
->assertRedirect("/books/{$chapter->book->slug}/chapter/super-test-chapter");
84+
}
85+
86+
public function test_old_book_slugs_in_page_urls_redirect_to_current_page_url()
87+
{
88+
$page = $this->entities->page();
89+
$book = $page->book;
4490
$pageUrl = $page->getUrl();
4591

46-
$this->put($pageUrl, [
47-
'name' => 'super test page',
48-
'html' => '<p></p>',
92+
$this->asAdmin()->put($book->getUrl(), [
93+
'name' => 'super test book',
4994
]);
5095

5196
$this->get($pageUrl)
52-
->assertRedirect("/books/{$page->book->slug}/page/super-test-page");
97+
->assertRedirect("/books/super-test-book/page/{$page->slug}");
98+
}
99+
100+
public function test_old_book_slugs_in_chapter_urls_redirect_to_current_chapter_url()
101+
{
102+
$chapter = $this->entities->chapter();
103+
$book = $chapter->book;
104+
$chapterUrl = $chapter->getUrl();
105+
106+
$this->asAdmin()->put($book->getUrl(), [
107+
'name' => 'super test book',
108+
]);
109+
110+
$this->get($chapterUrl)
111+
->assertRedirect("/books/super-test-book/chapter/{$chapter->slug}");
53112
}
54113

55114
public function test_slugs_recorded_in_history_on_page_update()

0 commit comments

Comments
 (0)