Rolled out reference pages to all entities, added testing

Including testing to check permissions applied to listed references.
This commit is contained in:
Dan Brown 2022-08-19 22:40:44 +01:00
parent d5465726e2
commit d198332d3c
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 182 additions and 19 deletions

View File

@ -3,7 +3,12 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
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 Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceController extends Controller class ReferenceController extends Controller
@ -23,8 +28,64 @@ 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);
$baseQuery = $page->referencesTo() return view('pages.references', [
'page' => $page,
'references' => $references,
]);
}
/**
* Display the references to a given chapter.
*/
public function chapter(string $bookSlug, string $chapterSlug)
{
/** @var Chapter $chapter */
$chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
$references = $this->getEntityReferences($chapter);
return view('chapters.references', [
'chapter' => $chapter,
'references' => $references,
]);
}
/**
* Display the references to a given book.
*/
public function book(string $slug)
{
$book = Book::visible()->where('slug', '=', $slug)->firstOrFail();
$references = $this->getEntityReferences($book);
return view('books.references', [
'book' => $book,
'references' => $references,
]);
}
/**
* Display the references to a given shelf.
*/
public function shelf(string $slug)
{
$shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail();
$references = $this->getEntityReferences($shelf);
return view('shelves.references', [
'shelf' => $shelf,
'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()) ->where('from_type', '=', (new Page())->getMorphClass())
->with([ ->with([
'from' => fn(Relation $query) => $query->select(Page::$listAttributes), 'from' => fn(Relation $query) => $query->select(Page::$listAttributes),
@ -39,9 +100,6 @@ class ReferenceController extends Controller
'from_type' 'from_type'
)->get(); )->get();
return view('pages.references', [ return $references;
'page' => $page,
'references' => $references,
]);
} }
} }

View File

@ -0,0 +1,20 @@
@extends('layouts.simple')
@section('body')
<div class="container small">
<div class="my-s">
@include('entities.breadcrumbs', ['crumbs' => [
$book,
$book->getUrl('/references') => [
'text' => trans('entities.references'),
'icon' => 'reference',
]
]])
</div>
@include('entities.references', ['references' => $references])
</div>
@stop

View File

@ -0,0 +1,21 @@
@extends('layouts.simple')
@section('body')
<div class="container small">
<div class="my-s">
@include('entities.breadcrumbs', ['crumbs' => [
$chapter->book,
$chapter,
$chapter->getUrl('/references') => [
'text' => trans('entities.references'),
'icon' => 'reference',
]
]])
</div>
@include('entities.references', ['references' => $references])
</div>
@stop

View File

@ -0,0 +1,13 @@
<main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.references') }}</h1>
<p>{{ trans('entities.references_to_desc') }}</p>
@if(count($references) > 0)
<div class="book-contents">
@include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])
</div>
@else
<p class="text-muted italic">{{ trans('entities.references_none') }}</p>
@endif
</main>

View File

@ -16,19 +16,7 @@
]]) ]])
</div> </div>
<main class="card content-wrap"> @include('entities.references', ['references' => $references])
<h1 class="list-heading">{{ trans('entities.references') }}</h1>
<p>{{ trans('entities.references_to_desc') }}</p>
@if(count($references) > 0)
<div class="book-contents">
@include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])
</div>
@else
<p class="text-muted italic">{{ trans('entities.references_none') }}</p>
@endif
</main>
</div> </div>
@stop @stop

View File

@ -0,0 +1,20 @@
@extends('layouts.simple')
@section('body')
<div class="container small">
<div class="my-s">
@include('entities.breadcrumbs', ['crumbs' => [
$shelf,
$shelf->getUrl('/references') => [
'text' => trans('entities.references'),
'icon' => 'reference',
]
]])
</div>
@include('entities.references', ['references' => $references])
</div>
@stop

View File

@ -64,6 +64,7 @@ Route::middleware('auth')->group(function () {
Route::get('/shelves/{slug}/permissions', [BookshelfController::class, 'showPermissions']); Route::get('/shelves/{slug}/permissions', [BookshelfController::class, 'showPermissions']);
Route::put('/shelves/{slug}/permissions', [BookshelfController::class, 'permissions']); Route::put('/shelves/{slug}/permissions', [BookshelfController::class, 'permissions']);
Route::post('/shelves/{slug}/copy-permissions', [BookshelfController::class, 'copyPermissions']); Route::post('/shelves/{slug}/copy-permissions', [BookshelfController::class, 'copyPermissions']);
Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
// Book Creation // Book Creation
Route::get('/shelves/{shelfSlug}/create-book', [BookController::class, 'create']); Route::get('/shelves/{shelfSlug}/create-book', [BookController::class, 'create']);
@ -86,6 +87,7 @@ Route::middleware('auth')->group(function () {
Route::post('/books/{bookSlug}/convert-to-shelf', [BookController::class, 'convertToShelf']); Route::post('/books/{bookSlug}/convert-to-shelf', [BookController::class, 'convertToShelf']);
Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']); Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']); Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
Route::get('/books/{slug}/references', [ReferenceController::class, 'book']);
Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']); Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
Route::get('/books/{bookSlug}/export/pdf', [BookExportController::class, 'pdf']); Route::get('/books/{bookSlug}/export/pdf', [BookExportController::class, 'pdf']);
Route::get('/books/{bookSlug}/export/markdown', [BookExportController::class, 'markdown']); Route::get('/books/{bookSlug}/export/markdown', [BookExportController::class, 'markdown']);
@ -142,6 +144,7 @@ Route::middleware('auth')->group(function () {
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ChapterExportController::class, 'markdown']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ChapterExportController::class, 'markdown']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ChapterExportController::class, 'plainText']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ChapterExportController::class, 'plainText']);
Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'permissions']); Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'permissions']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [ChapterController::class, 'showDelete']); Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [ChapterController::class, 'showDelete']);
Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'destroy']); Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'destroy']);

View File

@ -54,6 +54,46 @@ 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_visible_on_references_page()
{
$entities = $this->getEachEntityType();
$this->asEditor();
foreach ($entities as $entity) {
$this->createReference($entities['page'], $entity);
}
foreach ($entities as $entity) {
$resp = $this->get($entity->getUrl('/references'));
$resp->assertSee('References');
$resp->assertSee($entities['page']->name);
$resp->assertDontSee('There are no tracked references');
}
}
public function test_reference_not_visible_if_view_permission_does_not_permit()
{
/** @var Page $page */
/** @var Page $pageB */
$page = Page::query()->first();
$pageB = Page::query()->where('id', '!=', $page->id)->first();
$this->createReference($pageB, $page);
$this->setEntityRestrictions($pageB);
$this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name);
$this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name);
}
public function test_reference_page_shows_empty_state_with_no_references()
{
/** @var Page $page */
$page = Page::query()->first();
$this->asEditor()
->get($page->getUrl('/references'))
->assertSee('There are no tracked references');
}
protected function createReference(Model $from, Model $to) protected function createReference(Model $from, Model $to)
{ {
(new Reference())->forceFill([ (new Reference())->forceFill([

View File

@ -430,7 +430,7 @@ abstract class TestCase extends BaseTestCase
} }
/** /**
* @return Entity[] * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf}
*/ */
protected function getEachEntityType(): array protected function getEachEntityType(): array
{ {