1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-06-13 00:41:59 +03:00

Added entity meta link to reference page

Not totally happy with implementation as is requires extra service to be
injected to core controllers, but does the job.
Included test to cover.
Updated some controller properties to be typed while there.
This commit is contained in:
Dan Brown
2022-08-20 12:07:38 +01:00
parent d198332d3c
commit f634b4ea57
12 changed files with 143 additions and 63 deletions

View File

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\References\ReferenceService; use BookStack\References\ReferenceStore;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -22,14 +22,14 @@ class RegenerateReferences extends Command
*/ */
protected $description = 'Regenerate all the cross-item model reference index'; protected $description = 'Regenerate all the cross-item model reference index';
protected ReferenceService $references; protected ReferenceStore $references;
/** /**
* Create a new command instance. * Create a new command instance.
* *
* @return void * @return void
*/ */
public function __construct(ReferenceService $references) public function __construct(ReferenceStore $references)
{ {
$this->references = $references; $this->references = $references;
parent::__construct(); parent::__construct();

View File

@ -16,7 +16,7 @@ use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\References\ReferenceService; use BookStack\References\ReferenceStore;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
@ -24,12 +24,12 @@ use Illuminate\Pagination\LengthAwarePaginator;
class PageRepo class PageRepo
{ {
protected BaseRepo $baseRepo; protected BaseRepo $baseRepo;
protected ReferenceService $references; protected ReferenceStore $references;
/** /**
* PageRepo constructor. * PageRepo constructor.
*/ */
public function __construct(BaseRepo $baseRepo, ReferenceService $references) public function __construct(BaseRepo $baseRepo, ReferenceStore $references)
{ {
$this->baseRepo = $baseRepo; $this->baseRepo = $baseRepo;
$this->references = $references; $this->references = $references;

View File

@ -15,19 +15,22 @@ use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\References\ReferenceFetcher;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
class BookController extends Controller class BookController extends Controller
{ {
protected $bookRepo; protected BookRepo $bookRepo;
protected $entityContextManager; protected ShelfContext $shelfContext;
protected ReferenceFetcher $referenceFetcher;
public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo) public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo, ReferenceFetcher $referenceFetcher)
{ {
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
$this->entityContextManager = $entityContextManager; $this->shelfContext = $entityContextManager;
$this->referenceFetcher = $referenceFetcher;
} }
/** /**
@ -44,7 +47,7 @@ class BookController extends Controller
$popular = $this->bookRepo->getPopular(4); $popular = $this->bookRepo->getPopular(4);
$new = $this->bookRepo->getRecentlyCreated(4); $new = $this->bookRepo->getRecentlyCreated(4);
$this->entityContextManager->clearShelfContext(); $this->shelfContext->clearShelfContext();
$this->setPageTitle(trans('entities.books')); $this->setPageTitle(trans('entities.books'));
@ -122,7 +125,7 @@ class BookController extends Controller
View::incrementFor($book); View::incrementFor($book);
if ($request->has('shelf')) { if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf'))); $this->shelfContext->setShelfContext(intval($request->get('shelf')));
} }
$this->setPageTitle($book->getShortName()); $this->setPageTitle($book->getShortName());
@ -133,6 +136,7 @@ class BookController extends Controller
'bookChildren' => $bookChildren, 'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves, 'bookParentShelves' => $bookParentShelves,
'activity' => $activities->entityActivity($book, 20, 1), 'activity' => $activities->entityActivity($book, 20, 1),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($book),
]); ]);
} }

View File

@ -10,6 +10,7 @@ use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext; use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\References\ReferenceFetcher;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -18,11 +19,13 @@ class BookshelfController extends Controller
{ {
protected BookshelfRepo $shelfRepo; protected BookshelfRepo $shelfRepo;
protected ShelfContext $shelfContext; protected ShelfContext $shelfContext;
protected ReferenceFetcher $referenceFetcher;
public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext) public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext, ReferenceFetcher $referenceFetcher)
{ {
$this->shelfRepo = $shelfRepo; $this->shelfRepo = $shelfRepo;
$this->shelfContext = $shelfContext; $this->shelfContext = $shelfContext;
$this->referenceFetcher = $referenceFetcher;
} }
/** /**
@ -124,6 +127,7 @@ class BookshelfController extends Controller
'activity' => $activities->entityActivity($shelf, 20, 1), 'activity' => $activities->entityActivity($shelf, 20, 1),
'order' => $order, 'order' => $order,
'sort' => $sort, 'sort' => $sort,
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
]); ]);
} }

View File

@ -13,20 +13,21 @@ use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\References\ReferenceFetcher;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
class ChapterController extends Controller class ChapterController extends Controller
{ {
protected $chapterRepo; protected ChapterRepo $chapterRepo;
protected ReferenceFetcher $referenceFetcher;
/**
* ChapterController constructor. public function __construct(ChapterRepo $chapterRepo, ReferenceFetcher $referenceFetcher)
*/
public function __construct(ChapterRepo $chapterRepo)
{ {
$this->chapterRepo = $chapterRepo; $this->chapterRepo = $chapterRepo;
$this->referenceFetcher = $referenceFetcher;
} }
/** /**
@ -77,13 +78,14 @@ class ChapterController extends Controller
$this->setPageTitle($chapter->getShortName()); $this->setPageTitle($chapter->getShortName());
return view('chapters.show', [ return view('chapters.show', [
'book' => $chapter->book, 'book' => $chapter->book,
'chapter' => $chapter, 'chapter' => $chapter,
'current' => $chapter, 'current' => $chapter,
'sidebarTree' => $sidebarTree, 'sidebarTree' => $sidebarTree,
'pages' => $pages, 'pages' => $pages,
'next' => $nextPreviousLocator->getNext(), 'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(), 'previous' => $nextPreviousLocator->getPrevious(),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($chapter),
]); ]);
} }

View File

@ -14,6 +14,7 @@ use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\References\ReferenceFetcher;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -23,13 +24,15 @@ use Throwable;
class PageController extends Controller class PageController extends Controller
{ {
protected PageRepo $pageRepo; protected PageRepo $pageRepo;
protected ReferenceFetcher $referenceFetcher;
/** /**
* PageController constructor. * PageController constructor.
*/ */
public function __construct(PageRepo $pageRepo) public function __construct(PageRepo $pageRepo, ReferenceFetcher $referenceFetcher)
{ {
$this->pageRepo = $pageRepo; $this->pageRepo = $pageRepo;
$this->referenceFetcher = $referenceFetcher;
} }
/** /**
@ -160,6 +163,7 @@ class PageController extends Controller
'pageNav' => $pageNav, 'pageNav' => $pageNav,
'next' => $nextPreviousLocator->getNext(), 'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(), 'previous' => $nextPreviousLocator->getPrevious(),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($page),
]); ]);
} }

View File

@ -2,23 +2,19 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Collection; use BookStack\References\ReferenceFetcher;
use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceController extends Controller class ReferenceController extends Controller
{ {
protected ReferenceFetcher $referenceFetcher;
protected PermissionApplicator $permissions; public function __construct(ReferenceFetcher $referenceFetcher)
public function __construct(PermissionApplicator $permissions)
{ {
$this->permissions = $permissions; $this->referenceFetcher = $referenceFetcher;
} }
/** /**
@ -28,7 +24,7 @@ class ReferenceController extends Controller
{ {
/** @var Page $page */ /** @var Page $page */
$page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail(); $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
$references = $this->getEntityReferences($page); $references = $this->referenceFetcher->getPageReferencesToEntity($page);
return view('pages.references', [ return view('pages.references', [
'page' => $page, 'page' => $page,
@ -43,7 +39,7 @@ class ReferenceController extends Controller
{ {
/** @var Chapter $chapter */ /** @var Chapter $chapter */
$chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail(); $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
$references = $this->getEntityReferences($chapter); $references = $this->referenceFetcher->getPageReferencesToEntity($chapter);
return view('chapters.references', [ return view('chapters.references', [
'chapter' => $chapter, 'chapter' => $chapter,
@ -57,7 +53,7 @@ class ReferenceController extends Controller
public function book(string $slug) public function book(string $slug)
{ {
$book = Book::visible()->where('slug', '=', $slug)->firstOrFail(); $book = Book::visible()->where('slug', '=', $slug)->firstOrFail();
$references = $this->getEntityReferences($book); $references = $this->referenceFetcher->getPageReferencesToEntity($book);
return view('books.references', [ return view('books.references', [
'book' => $book, 'book' => $book,
@ -71,35 +67,11 @@ class ReferenceController extends Controller
public function shelf(string $slug) public function shelf(string $slug)
{ {
$shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail(); $shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail();
$references = $this->getEntityReferences($shelf); $references = $this->referenceFetcher->getPageReferencesToEntity($shelf);
return view('shelves.references', [ return view('shelves.references', [
'shelf' => $shelf, 'shelf' => $shelf,
'references' => $references, 'references' => $references,
]); ]);
} }
/**
* Query the references for the given entities.
* Loads the commonly required relations while taking permissions into account.
*/
protected function getEntityReferences(Entity $entity): Collection
{
$baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass())
->with([
'from' => fn(Relation $query) => $query->select(Page::$listAttributes),
'from.book' => fn(Relation $query) => $query->scopes('visible'),
'from.chapter' => fn(Relation $query) => $query->scopes('visible')
]);
$references = $this->permissions->restrictEntityRelationQuery(
$baseQuery,
'references',
'from_id',
'from_type'
)->get();
return $references;
}
} }

View File

@ -0,0 +1,62 @@
<?php
namespace BookStack\References;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceFetcher
{
protected PermissionApplicator $permissions;
public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
}
/**
* Query and return the page references pointing to the given entity.
* Loads the commonly required relations while taking permissions into account.
*/
public function getPageReferencesToEntity(Entity $entity): Collection
{
$baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass())
->with([
'from' => fn(Relation $query) => $query->select(Page::$listAttributes),
'from.book' => fn(Relation $query) => $query->scopes('visible'),
'from.chapter' => fn(Relation $query) => $query->scopes('visible')
]);
$references = $this->permissions->restrictEntityRelationQuery(
$baseQuery,
'references',
'from_id',
'from_type'
)->get();
return $references;
}
/**
* Returns the count of page references pointing to the given entity.
* Takes permissions into account.
*/
public function getPageReferenceCountToEntity(Entity $entity): int
{
$baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass());
$count = $this->permissions->restrictEntityRelationQuery(
$baseQuery,
'references',
'from_id',
'from_type'
)->count();
return $count;
}
}

View File

@ -5,7 +5,7 @@ namespace BookStack\References;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class ReferenceService class ReferenceStore
{ {
/** /**

View File

@ -23,6 +23,7 @@ return [
'meta_updated' => 'Updated :timeLength', 'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user', 'meta_updated_name' => 'Updated :timeLength by :user',
'meta_owned_name' => 'Owned by :user', 'meta_owned_name' => 'Owned by :user',
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
'entity_select' => 'Entity Select', 'entity_select' => 'Entity Select',
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item', 'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
'images' => 'Images', 'images' => 'Images',

View File

@ -59,4 +59,13 @@
<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span> <span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
</div> </div>
@endif @endif
@if($referenceCount ?? 0)
<a href="{{ $entity->getUrl('/references') }}" class="entity-meta-item">
@icon('reference')
<div>
{!! trans_choice('entities.meta_reference_page_count', $referenceCount, ['count' => $referenceCount]) !!}
</div>
</a>
@endif
</div> </div>

View File

@ -54,6 +54,28 @@ class ReferencesTest extends TestCase
$this->assertDatabaseMissing('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]); $this->assertDatabaseMissing('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]);
} }
public function test_references_to_count_visible_on_entity_show_view()
{
$entities = $this->getEachEntityType();
/** @var Page $otherPage */
$otherPage = Page::query()->where('id', '!=', $entities['page']->id)->first();
$this->asEditor();
foreach ($entities as $entity) {
$this->createReference($entities['page'], $entity);
}
foreach ($entities as $entity) {
$resp = $this->get($entity->getUrl());
$resp->assertSee('Referenced on 1 page');
$resp->assertDontSee('Referenced on 1 pages');
}
$this->createReference($otherPage, $entities['page']);
$resp = $this->get($entities['page']->getUrl());
$resp->assertSee('Referenced on 2 pages');
}
public function test_references_to_visible_on_references_page() public function test_references_to_visible_on_references_page()
{ {
$entities = $this->getEachEntityType(); $entities = $this->getEachEntityType();