1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-12-13 07:42:23 +03:00

Slugs: Rolled out history lookup to other types

Added testing to cover.
Also added batch recording of child slug pairs on book slug changes.
This commit is contained in:
Dan Brown
2025-11-24 19:49:34 +00:00
parent dd393691b1
commit c90816987c
8 changed files with 132 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Queries\BookshelfQueries; use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner; use BookStack\Entities\Tools\Cloner;
@@ -31,6 +32,7 @@ class BookController extends Controller
protected ShelfContext $shelfContext, protected ShelfContext $shelfContext,
protected BookRepo $bookRepo, protected BookRepo $bookRepo,
protected BookQueries $queries, protected BookQueries $queries,
protected EntityQueries $entityQueries,
protected BookshelfQueries $shelfQueries, protected BookshelfQueries $shelfQueries,
protected ReferenceFetcher $referenceFetcher, protected ReferenceFetcher $referenceFetcher,
) { ) {
@@ -127,7 +129,16 @@ class BookController extends Controller
*/ */
public function show(Request $request, ActivityQueries $activities, string $slug) public function show(Request $request, ActivityQueries $activities, string $slug)
{ {
try {
$book = $this->queries->findVisibleBySlugOrFail($slug); $book = $this->queries->findVisibleBySlugOrFail($slug);
} catch (NotFoundException $exception) {
$book = $this->entityQueries->findVisibleByOldSlugs('book', $slug);
if (is_null($book)) {
throw $exception;
}
return redirect($book->getUrl());
}
$bookChildren = (new BookContents($book))->getTree(true); $bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->scopes('visible')->get(); $bookParentShelves = $book->shelves()->scopes('visible')->get();

View File

@@ -6,6 +6,7 @@ use BookStack\Activity\ActivityQueries;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Queries\BookshelfQueries; use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\ShelfContext; use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
@@ -23,6 +24,7 @@ class BookshelfController extends Controller
public function __construct( public function __construct(
protected BookshelfRepo $shelfRepo, protected BookshelfRepo $shelfRepo,
protected BookshelfQueries $queries, protected BookshelfQueries $queries,
protected EntityQueries $entityQueries,
protected BookQueries $bookQueries, protected BookQueries $bookQueries,
protected ShelfContext $shelfContext, protected ShelfContext $shelfContext,
protected ReferenceFetcher $referenceFetcher, protected ReferenceFetcher $referenceFetcher,
@@ -105,7 +107,16 @@ class BookshelfController extends Controller
*/ */
public function show(Request $request, ActivityQueries $activities, string $slug) public function show(Request $request, ActivityQueries $activities, string $slug)
{ {
try {
$shelf = $this->queries->findVisibleBySlugOrFail($slug); $shelf = $this->queries->findVisibleBySlugOrFail($slug);
} catch (NotFoundException $exception) {
$shelf = $this->entityQueries->findVisibleByOldSlugs('bookshelf', $slug);
if (is_null($shelf)) {
throw $exception;
}
return redirect($shelf->getUrl());
}
$this->checkOwnablePermission(Permission::BookshelfView, $shelf); $this->checkOwnablePermission(Permission::BookshelfView, $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([ $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([

View File

@@ -77,7 +77,15 @@ class ChapterController extends Controller
*/ */
public function show(string $bookSlug, string $chapterSlug) public function show(string $bookSlug, string $chapterSlug)
{ {
try {
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
} catch (NotFoundException $exception) {
$chapter = $this->entityQueries->findVisibleByOldSlugs('chapter', $chapterSlug, $bookSlug);
if (is_null($chapter)) {
throw $exception;
}
return redirect($chapter->getUrl());
}
$sidebarTree = (new BookContents($chapter->book))->getTree(); $sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get(); $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();

View File

@@ -16,6 +16,7 @@ abstract class BookChild extends Entity
{ {
/** /**
* Get the book this page sits in. * Get the book this page sits in.
* @return BelongsTo<Book, $this>
*/ */
public function book(): BelongsTo public function book(): BelongsTo
{ {

View File

@@ -430,6 +430,14 @@ abstract class Entity extends Model implements
return $this->morphMany(Watch::class, 'watchable'); return $this->morphMany(Watch::class, 'watchable');
} }
/**
* Get the related slug history for this entity.
*/
public function slugHistory(): MorphMany
{
return $this->morphMany(SlugHistory::class, 'sluggable');
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@@ -2,10 +2,13 @@
namespace BookStack\Entities\Tools; namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\EntityTable;
use BookStack\Entities\Models\SlugHistory as SlugHistoryModel; use BookStack\Entities\Models\SlugHistory as SlugHistoryModel;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Support\Facades\DB;
class SlugHistory class SlugHistory
{ {
@@ -43,6 +46,23 @@ class SlugHistory
$entry = new SlugHistoryModel(); $entry = new SlugHistoryModel();
$entry->forceFill($info); $entry->forceFill($info);
$entry->save(); $entry->save();
if ($entity instanceof Book) {
$this->recordForBookChildren($entity);
}
}
protected function recordForBookChildren(Book $book): void
{
$query = EntityTable::query()
->select(['type', 'id', 'slug', DB::raw("'{$book->slug}' as parent_slug"), DB::raw('now() as created_at'), DB::raw('now() as updated_at')])
->where('book_id', '=', $book->id)
->whereNotNull('book_id');
SlugHistoryModel::query()->insertUsing(
['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'],
$query
);
} }
/** /**

View File

@@ -388,7 +388,7 @@ class TrashCan
/** /**
* Update entity relations to remove or update outstanding connections. * Update entity relations to remove or update outstanding connections.
*/ */
protected function destroyCommonRelations(Entity $entity) protected function destroyCommonRelations(Entity $entity): void
{ {
Activity::removeEntity($entity); Activity::removeEntity($entity);
$entity->views()->delete(); $entity->views()->delete();
@@ -402,6 +402,7 @@ class TrashCan
$entity->watches()->delete(); $entity->watches()->delete();
$entity->referencesTo()->delete(); $entity->referencesTo()->delete();
$entity->referencesFrom()->delete(); $entity->referencesFrom()->delete();
$entity->slugHistory()->delete();
if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) { if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) {
$imageService = app()->make(ImageService::class); $imageService = app()->make(ImageService::class);

View File

@@ -33,17 +33,9 @@ class SlugTest extends TestCase
public function test_old_page_slugs_redirect_to_new_pages() public function test_old_page_slugs_redirect_to_new_pages()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
// Need to save twice since revisions are not generated in seeder.
$this->asAdmin()->put($page->getUrl(), [
'name' => 'super test',
'html' => '<p></p>',
]);
$page->refresh();
$pageUrl = $page->getUrl(); $pageUrl = $page->getUrl();
$this->put($pageUrl, [ $this->asAdmin()->put($pageUrl, [
'name' => 'super test page', 'name' => 'super test page',
'html' => '<p></p>', 'html' => '<p></p>',
]); ]);
@@ -52,6 +44,73 @@ class SlugTest extends TestCase
->assertRedirect("/books/{$page->book->slug}/page/super-test-page"); ->assertRedirect("/books/{$page->book->slug}/page/super-test-page");
} }
public function test_old_shelf_slugs_redirect_to_new_shelf()
{
$shelf = $this->entities->shelf();
$shelfUrl = $shelf->getUrl();
$this->asAdmin()->put($shelf->getUrl(), [
'name' => 'super test shelf',
]);
$this->get($shelfUrl)
->assertRedirect("/shelves/super-test-shelf");
}
public function test_old_book_slugs_redirect_to_new_book()
{
$book = $this->entities->book();
$bookUrl = $book->getUrl();
$this->asAdmin()->put($book->getUrl(), [
'name' => 'super test book',
]);
$this->get($bookUrl)
->assertRedirect("/books/super-test-book");
}
public function test_old_chapter_slugs_redirect_to_new_chapter()
{
$chapter = $this->entities->chapter();
$chapterUrl = $chapter->getUrl();
$this->asAdmin()->put($chapter->getUrl(), [
'name' => 'super test chapter',
]);
$this->get($chapterUrl)
->assertRedirect("/books/{$chapter->book->slug}/chapter/super-test-chapter");
}
public function test_old_book_slugs_in_page_urls_redirect_to_current_page_url()
{
$page = $this->entities->page();
$book = $page->book;
$pageUrl = $page->getUrl();
$this->asAdmin()->put($book->getUrl(), [
'name' => 'super test book',
]);
$this->get($pageUrl)
->assertRedirect("/books/super-test-book/page/{$page->slug}");
}
public function test_old_book_slugs_in_chapter_urls_redirect_to_current_chapter_url()
{
$chapter = $this->entities->chapter();
$book = $chapter->book;
$chapterUrl = $chapter->getUrl();
$this->asAdmin()->put($book->getUrl(), [
'name' => 'super test book',
]);
$this->get($chapterUrl)
->assertRedirect("/books/super-test-book/chapter/{$chapter->slug}");
}
public function test_slugs_recorded_in_history_on_page_update() public function test_slugs_recorded_in_history_on_page_update()
{ {
$page = $this->entities->page(); $page = $this->entities->page();