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:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user