Merge branch 'sort_changes'

Related to #3134
This commit is contained in:
Dan Brown 2022-01-06 12:03:15 +00:00
commit cb0d674a71
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 456 additions and 167 deletions

View File

@ -10,6 +10,7 @@ use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use Exception; use Exception;
@ -85,15 +86,19 @@ class ChapterRepo
* 'book:<id>' (book:5). * 'book:<id>' (book:5).
* *
* @throws MoveOperationException * @throws MoveOperationException
* @throws PermissionsException
*/ */
public function move(Chapter $chapter, string $parentIdentifier): Book public function move(Chapter $chapter, string $parentIdentifier): Book
{ {
/** @var Book $parent */
$parent = $this->findParentByIdentifier($parentIdentifier); $parent = $this->findParentByIdentifier($parentIdentifier);
if (is_null($parent)) { if (is_null($parent)) {
throw new MoveOperationException('Book to move chapter into not found'); throw new MoveOperationException('Book to move chapter into not found');
} }
if (!userCan('chapter-create', $parent)) {
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
}
$chapter->changeBook($parent->id); $chapter->changeBook($parent->id);
$chapter->rebuildPermissions(); $chapter->rebuildPermissions();
Activity::add(ActivityType::CHAPTER_MOVE, $chapter); Activity::add(ActivityType::CHAPTER_MOVE, $chapter);

View File

@ -328,7 +328,7 @@ class PageRepo
public function move(Page $page, string $parentIdentifier): Entity public function move(Page $page, string $parentIdentifier): Entity
{ {
$parent = $this->findParentByIdentifier($parentIdentifier); $parent = $this->findParentByIdentifier($parentIdentifier);
if ($parent === null) { if (is_null($parent)) {
throw new MoveOperationException('Book or chapter to move page into not found'); throw new MoveOperationException('Book or chapter to move page into not found');
} }

View File

@ -7,7 +7,6 @@ use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Exceptions\SortOperationException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class BookContents class BookContents
@ -107,111 +106,207 @@ class BookContents
} }
/** /**
* Sort the books content using the given map. * Sort the books content using the given sort map.
* The map is a single-dimension collection of objects in the following format:
* {
* +"id": "294" (ID of item)
* +"sort": 1 (Sort order index)
* +"parentChapter": false (ID of parent chapter, as string, or false)
* +"type": "page" (Entity type of item)
* +"book": "1" (Id of book to place item in)
* }.
*
* Returns a list of books that were involved in the operation. * Returns a list of books that were involved in the operation.
* *
* @throws SortOperationException * @returns Book[]
*/ */
public function sortUsingMap(Collection $sortMap): Collection public function sortUsingMap(BookSortMap $sortMap): array
{ {
// Load models into map // Load models into map
$this->loadModelsIntoSortMap($sortMap); $modelMap = $this->loadModelsFromSortMap($sortMap);
$booksInvolved = $this->getBooksInvolvedInSort($sortMap);
// Sort our changes from our map to be chapters first
// Since they need to be process to ensure book alignment for child page changes.
$sortMapItems = $sortMap->all();
usort($sortMapItems, function(BookSortMapItem $itemA, BookSortMapItem $itemB) {
$aScore = $itemA->type === 'page' ? 2 : 1;
$bScore = $itemB->type === 'page' ? 2 : 1;
return $aScore - $bScore;
});
// Perform the sort // Perform the sort
$sortMap->each(function ($mapItem) { foreach ($sortMapItems as $item) {
$this->applySortUpdates($mapItem); $this->applySortUpdates($item, $modelMap);
}); }
// Update permissions and activity. /** @var Book[] $booksInvolved */
$booksInvolved->each(function (Book $book) { $booksInvolved = array_values(array_filter($modelMap, function (string $key) {
return strpos($key, 'book:') === 0;
}, ARRAY_FILTER_USE_KEY));
// Update permissions of books involved
foreach ($booksInvolved as $book) {
$book->rebuildPermissions(); $book->rebuildPermissions();
}); }
return $booksInvolved; return $booksInvolved;
} }
/** /**
* Using the given sort map item, detect changes for the related model * Using the given sort map item, detect changes for the related model
* and update it if required. * and update it if required. Changes where permissions are lacking will
* be skipped and not throw an error.
*
* @param array<string, Entity> $modelMap
*/ */
protected function applySortUpdates(\stdClass $sortMapItem) protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
{ {
/** @var BookChild $model */ /** @var BookChild $model */
$model = $sortMapItem->model; $model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
if (!$model) {
return;
}
$priorityChanged = intval($model->priority) !== intval($sortMapItem->sort); $priorityChanged = $model->priority !== $sortMapItem->sort;
$bookChanged = intval($model->book_id) !== intval($sortMapItem->book); $bookChanged = $model->book_id !== $sortMapItem->parentBookId;
$chapterChanged = ($model instanceof Page) && intval($model->chapter_id) !== $sortMapItem->parentChapter; $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
// Stop if there's no change
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
return;
}
$currentParentKey = 'book:' . $model->book_id;
if ($model instanceof Page && $model->chapter_id) {
$currentParentKey = 'chapter:' . $model->chapter_id;
}
$currentParent = $modelMap[$currentParentKey] ?? null;
/** @var Book $newBook */
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
/** @var ?Chapter $newChapter */
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
return;
}
// Action the required changes
if ($bookChanged) { if ($bookChanged) {
$model->changeBook($sortMapItem->book); $model->changeBook($newBook->id);
} }
if ($chapterChanged) { if ($chapterChanged) {
$model->chapter_id = intval($sortMapItem->parentChapter); $model->chapter_id = $newChapter->id ?? 0;
$model->save();
} }
if ($priorityChanged) { if ($priorityChanged) {
$model->priority = intval($sortMapItem->sort); $model->priority = $sortMapItem->sort;
}
if ($chapterChanged || $priorityChanged) {
$model->save(); $model->save();
} }
} }
/**
* Check if the current user has permissions to apply the given sorting change.
* Is quite complex since items can gain a different parent change. Acts as a:
* - Update of old parent element (Change of content/order).
* - Update of sorted/moved element.
* - Deletion of element (Relative to parent upon move).
* - Creation of element within parent (Upon move to new parent).
*/
protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
{
// Stop if we can't see the current parent or new book.
if (!$currentParent || !$newBook) {
return false;
}
$hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));
if ($model instanceof Chapter) {
$hasPermission = userCan('book-update', $currentParent)
&& userCan('book-update', $newBook)
&& userCan('chapter-update', $model)
&& (!$hasNewParent || userCan('chapter-create', $newBook))
&& (!$hasNewParent || userCan('chapter-delete', $model));
if (!$hasPermission) {
return false;
}
}
if ($model instanceof Page) {
$parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasCurrentParentPermission = userCan($parentPermission, $currentParent);
// This needs to check if there was an intended chapter location in the original sort map
// rather than inferring from the $newChapter since that variable may be null
// due to other reasons (Visibility).
$newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;
if (!$newParent) {
return false;
}
$hasPageEditPermission = userCan('page-update', $model);
$newParentInRightLocation = ($newParent instanceof Book || $newParent->book_id === $newBook->id);
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasNewParentPermission = userCan($newParentPermission, $newParent);
$hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model));
$hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent));
$hasPermission = $hasCurrentParentPermission
&& $newParentInRightLocation
&& $hasNewParentPermission
&& $hasPageEditPermission
&& $hasDeletePermissionIfMoving
&& $hasCreatePermissionIfMoving;
if (!$hasPermission) {
return false;
}
}
return true;
}
/** /**
* Load models from the database into the given sort map. * Load models from the database into the given sort map.
* @return array<string, Entity>
*/ */
protected function loadModelsIntoSortMap(Collection $sortMap): void protected function loadModelsFromSortMap(BookSortMap $sortMap): array
{ {
$keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) { $modelMap = [];
return $sortMapItem->type . ':' . $sortMapItem->id; $ids = [
}); 'chapter' => [],
$pageIds = $sortMap->where('type', '=', 'page')->pluck('id'); 'page' => [],
$chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id'); 'book' => [],
];
$pages = Page::visible()->whereIn('id', $pageIds)->get(); foreach ($sortMap->all() as $sortMapItem) {
$chapters = Chapter::visible()->whereIn('id', $chapterIds)->get(); $ids[$sortMapItem->type][] = $sortMapItem->id;
$ids['book'][] = $sortMapItem->parentBookId;
if ($sortMapItem->parentChapterId) {
$ids['chapter'][] = $sortMapItem->parentChapterId;
}
}
$pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes);
/** @var Page $page */
foreach ($pages as $page) { foreach ($pages as $page) {
$sortItem = $keyMap->get('page:' . $page->id); $modelMap['page:' . $page->id] = $page;
$sortItem->model = $page; $ids['book'][] = $page->book_id;
if ($page->chapter_id) {
$ids['chapter'][] = $page->chapter_id;
}
} }
$chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get();
/** @var Chapter $chapter */
foreach ($chapters as $chapter) { foreach ($chapters as $chapter) {
$sortItem = $keyMap->get('chapter:' . $chapter->id); $modelMap['chapter:' . $chapter->id] = $chapter;
$sortItem->model = $chapter; $ids['book'][] = $chapter->book_id;
}
}
/**
* Get the books involved in a sort.
* The given sort map should have its models loaded first.
*
* @throws SortOperationException
*/
protected function getBooksInvolvedInSort(Collection $sortMap): Collection
{
$bookIdsInvolved = collect([$this->book->id]);
$bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
$bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
$books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
if (count($books) !== count($bookIdsInvolved)) {
throw new SortOperationException('Could not find all books requested in sort operation');
} }
return $books; $books = Book::visible()->whereIn('id', array_unique($ids['book']))->get();
/** @var Book $book */
foreach ($books as $book) {
$modelMap['book:' . $book->id] = $book;
}
return $modelMap;
} }
} }

View File

@ -0,0 +1,45 @@
<?php
namespace BookStack\Entities\Tools;
class BookSortMap
{
/**
* @var BookSortMapItem[]
*/
protected $mapData = [];
public function addItem(BookSortMapItem $mapItem): void
{
$this->mapData[] = $mapItem;
}
/**
* @return BookSortMapItem[]
*/
public function all(): array
{
return $this->mapData;
}
public static function fromJson(string $json): self
{
$map = new static();
$mapData = json_decode($json);
foreach ($mapData as $mapDataItem) {
$item = new BookSortMapItem(
intval($mapDataItem->id),
intval($mapDataItem->sort),
$mapDataItem->parentChapter ? intval($mapDataItem->parentChapter) : null,
$mapDataItem->type,
intval($mapDataItem->book)
);
$map->addItem($item);
}
return $map;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace BookStack\Entities\Tools;
class BookSortMapItem
{
/**
* @var int
*/
public $id;
/**
* @var int
*/
public $sort;
/**
* @var ?int
*/
public $parentChapterId;
/**
* @var string
*/
public $type;
/**
* @var int
*/
public $parentBookId;
public function __construct(int $id, int $sort, ?int $parentChapterId, string $type, int $parentBookId)
{
$this->id = $id;
$this->sort = $sort;
$this->parentChapterId = $parentChapterId;
$this->type = $type;
$this->parentBookId = $parentBookId;
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace BookStack\Exceptions;
use Exception;
class SortOperationException extends Exception
{
}

View File

@ -3,10 +3,9 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType; use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Exceptions\SortOperationException; use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -59,20 +58,14 @@ class BookSortController extends Controller
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
$sortMap = collect(json_decode($request->get('sort-tree'))); $sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$bookContents = new BookContents($book); $bookContents = new BookContents($book);
$booksInvolved = collect(); $booksInvolved = $bookContents->sortUsingMap($sortMap);
try {
$booksInvolved = $bookContents->sortUsingMap($sortMap);
} catch (SortOperationException $exception) {
$this->showPermissionError();
}
// Rebuild permissions and add activity for involved books. // Rebuild permissions and add activity for involved books.
$booksInvolved->each(function (Book $book) { foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $book); Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
}); }
return redirect($book->getUrl()); return redirect($book->getUrl());
} }

View File

@ -11,6 +11,7 @@ use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PermissionsUpdater; 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 Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
@ -180,6 +181,8 @@ class ChapterController extends Controller
try { try {
$newBook = $this->chapterRepo->move($chapter, $entitySelection); $newBook = $this->chapterRepo->move($chapter, $entitySelection);
} catch (PermissionsException $exception) {
$this->showPermissionError();
} catch (MoveOperationException $exception) { } catch (MoveOperationException $exception) {
$this->showErrorNotification(trans('errors.selected_book_not_found')); $this->showErrorNotification(trans('errors.selected_book_not_found'));

View File

@ -412,11 +412,9 @@ class PageController extends Controller
try { try {
$parent = $this->pageRepo->move($page, $entitySelection); $parent = $this->pageRepo->move($page, $entitySelection);
} catch (PermissionsException $exception) {
$this->showPermissionError();
} catch (Exception $exception) { } catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found')); $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect()->back(); return redirect()->back();

View File

@ -33,9 +33,9 @@ class SortTest extends TestCase
public function test_page_move_into_book() public function test_page_move_into_book()
{ {
$page = Page::first(); $page = Page::query()->first();
$currentBook = $page->book; $currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$resp = $this->asEditor()->get($page->getUrl('/move')); $resp = $this->asEditor()->get($page->getUrl('/move'));
$resp->assertSee('Move Page'); $resp->assertSee('Move Page');
@ -43,7 +43,7 @@ class SortTest extends TestCase
$movePageResp = $this->put($page->getUrl('/move'), [ $movePageResp = $this->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$page = Page::find($page->id); $page = Page::query()->find($page->id);
$movePageResp->assertRedirect($page->getUrl()); $movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
@ -55,15 +55,15 @@ class SortTest extends TestCase
public function test_page_move_into_chapter() public function test_page_move_into_chapter()
{ {
$page = Page::first(); $page = Page::query()->first();
$currentBook = $page->book; $currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$newChapter = $newBook->chapters()->first(); $newChapter = $newBook->chapters()->first();
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [
'entity_selection' => 'chapter:' . $newChapter->id, 'entity_selection' => 'chapter:' . $newChapter->id,
]); ]);
$page = Page::find($page->id); $page = Page::query()->find($page->id);
$movePageResp->assertRedirect($page->getUrl()); $movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new chapter'); $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new chapter');
@ -74,9 +74,9 @@ class SortTest extends TestCase
public function test_page_move_from_chapter_to_book() public function test_page_move_from_chapter_to_book()
{ {
$oldChapter = Chapter::first(); $oldChapter = Chapter::query()->first();
$page = $oldChapter->pages()->first(); $page = $oldChapter->pages()->first();
$newBook = Book::where('id', '!=', $oldChapter->book_id)->first(); $newBook = Book::query()->where('id', '!=', $oldChapter->book_id)->first();
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
@ -110,7 +110,7 @@ class SortTest extends TestCase
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$page = Page::find($page->id); $page = Page::query()->find($page->id);
$movePageResp->assertRedirect($page->getUrl()); $movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
@ -118,9 +118,9 @@ class SortTest extends TestCase
public function test_page_move_requires_delete_permissions() public function test_page_move_requires_delete_permissions()
{ {
$page = Page::first(); $page = Page::query()->first();
$currentBook = $page->book; $currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all());
@ -138,17 +138,17 @@ class SortTest extends TestCase
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$page = Page::find($page->id); $page = Page::query()->find($page->id);
$movePageResp->assertRedirect($page->getUrl()); $movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
} }
public function test_chapter_move() public function test_chapter_move()
{ {
$chapter = Chapter::first(); $chapter = Chapter::query()->first();
$currentBook = $chapter->book; $currentBook = $chapter->book;
$pageToCheck = $chapter->pages->first(); $pageToCheck = $chapter->pages->first();
$newBook = Book::where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$chapterMoveResp = $this->asEditor()->get($chapter->getUrl('/move')); $chapterMoveResp = $this->asEditor()->get($chapter->getUrl('/move'));
$chapterMoveResp->assertSee('Move Chapter'); $chapterMoveResp->assertSee('Move Chapter');
@ -157,7 +157,7 @@ class SortTest extends TestCase
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$chapter = Chapter::find($chapter->id); $chapter = Chapter::query()->find($chapter->id);
$moveChapterResp->assertRedirect($chapter->getUrl()); $moveChapterResp->assertRedirect($chapter->getUrl());
$this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book'); $this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');
@ -165,7 +165,7 @@ class SortTest extends TestCase
$newBookResp->assertSee('moved chapter'); $newBookResp->assertSee('moved chapter');
$newBookResp->assertSee($chapter->name); $newBookResp->assertSee($chapter->name);
$pageToCheck = Page::find($pageToCheck->id); $pageToCheck = Page::query()->find($pageToCheck->id);
$this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book'); $this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book');
$pageCheckResp = $this->get($pageToCheck->getUrl()); $pageCheckResp = $this->get($pageToCheck->getUrl());
$pageCheckResp->assertSee($newBook->name); $pageCheckResp->assertSee($newBook->name);
@ -173,9 +173,9 @@ class SortTest extends TestCase
public function test_chapter_move_requires_delete_permissions() public function test_chapter_move_requires_delete_permissions()
{ {
$chapter = Chapter::first(); $chapter = Chapter::query()->first();
$currentBook = $chapter->book; $currentBook = $chapter->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all());
@ -193,7 +193,32 @@ class SortTest extends TestCase
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$chapter = Chapter::find($chapter->id); $chapter = Chapter::query()->find($chapter->id);
$moveChapterResp->assertRedirect($chapter->getUrl());
$this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');
}
public function test_chapter_move_requires_create_permissions_in_new_book()
{
$chapter = Chapter::query()->first();
$currentBook = $chapter->book;
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]);
$this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]);
$moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id,
]);
$this->assertPermissionError($moveChapterResp);
$this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]);
$moveChapterResp = $this->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id,
]);
$chapter = Chapter::query()->find($chapter->id);
$moveChapterResp->assertRedirect($chapter->getUrl()); $moveChapterResp->assertRedirect($chapter->getUrl());
$this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book'); $this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');
} }
@ -239,20 +264,20 @@ class SortTest extends TestCase
// Create request data // Create request data
$reqData = [ $reqData = [
[ [
'id' => $chapterToMove->id, 'id' => $chapterToMove->id,
'sort' => 0, 'sort' => 0,
'parentChapter' => false, 'parentChapter' => false,
'type' => 'chapter', 'type' => 'chapter',
'book' => $newBook->id, 'book' => $newBook->id,
], ],
]; ];
foreach ($pagesToMove as $index => $page) { foreach ($pagesToMove as $index => $page) {
$reqData[] = [ $reqData[] = [
'id' => $page->id, 'id' => $page->id,
'sort' => $index, 'sort' => $index,
'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false, 'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false,
'type' => 'page', 'type' => 'page',
'book' => $newBook->id, 'book' => $newBook->id,
]; ];
} }
@ -260,18 +285,153 @@ class SortTest extends TestCase
$sortResp->assertRedirect($newBook->getUrl()); $sortResp->assertRedirect($newBook->getUrl());
$sortResp->assertStatus(302); $sortResp->assertStatus(302);
$this->assertDatabaseHas('chapters', [ $this->assertDatabaseHas('chapters', [
'id' => $chapterToMove->id, 'id' => $chapterToMove->id,
'book_id' => $newBook->id, 'book_id' => $newBook->id,
'priority' => 0, 'priority' => 0,
]); ]);
$this->assertTrue($newBook->chapters()->count() === 1); $this->assertTrue($newBook->chapters()->count() === 1);
$this->assertTrue($newBook->chapters()->first()->pages()->count() === 1); $this->assertTrue($newBook->chapters()->first()->pages()->count() === 1);
$checkPage = $pagesToMove[1]; $checkPage = $pagesToMove[1];
$checkResp = $this->get(Page::find($checkPage->id)->getUrl()); $checkResp = $this->get($checkPage->refresh()->getUrl());
$checkResp->assertSee($newBook->name); $checkResp->assertSee($newBook->name);
} }
public function test_book_sort_makes_no_changes_if_new_chapter_does_not_align_with_new_book()
{
/** @var Page $page */
$page = Page::query()->where('chapter_id', '!=', 0)->first();
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$sortData = [
'id' => $page->id,
'sort' => 0,
'parentChapter' => $otherChapter->id,
'type' => 'page',
'book' => $page->book_id,
];
$this->asEditor()->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();
$this->assertDatabaseHas('pages', [
'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,
]);
}
public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_chapter()
{
/** @var Page $page */
$page = Page::query()->where('chapter_id', '!=', 0)->first();
/** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$this->setEntityRestrictions($otherChapter);
$sortData = [
'id' => $page->id,
'sort' => 0,
'parentChapter' => $otherChapter->id,
'type' => 'page',
'book' => $otherChapter->book_id,
];
$this->asEditor()->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();
$this->assertDatabaseHas('pages', [
'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,
]);
}
public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_book()
{
/** @var Page $page */
$page = Page::query()->where('chapter_id', '!=', 0)->first();
/** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]);
$sortData = [
'id' => $page->id,
'sort' => 0,
'parentChapter' => $otherChapter->id,
'type' => 'page',
'book' => $otherChapter->book_id,
];
$this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();
$this->assertDatabaseHas('pages', [
'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,
]);
}
public function test_book_sort_makes_no_changes_if_no_update_or_create_permissions_on_new_chapter()
{
/** @var Page $page */
$page = Page::query()->where('chapter_id', '!=', 0)->first();
/** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]);
$sortData = [
'id' => $page->id,
'sort' => 0,
'parentChapter' => $otherChapter->id,
'type' => 'page',
'book' => $otherChapter->book_id,
];
$this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();
$this->assertDatabaseHas('pages', [
'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,
]);
}
public function test_book_sort_makes_no_changes_if_no_update_permissions_on_moved_item()
{
/** @var Page $page */
$page = Page::query()->where('chapter_id', '!=', 0)->first();
/** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($page, ['view', 'delete'], [$editor->roles()->first()]);
$sortData = [
'id' => $page->id,
'sort' => 0,
'parentChapter' => $otherChapter->id,
'type' => 'page',
'book' => $otherChapter->book_id,
];
$this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();
$this->assertDatabaseHas('pages', [
'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,
]);
}
public function test_book_sort_makes_no_changes_if_no_delete_permissions_on_moved_item()
{
/** @var Page $page */
$page = Page::query()->where('chapter_id', '!=', 0)->first();
/** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($page, ['view', 'update'], [$editor->roles()->first()]);
$sortData = [
'id' => $page->id,
'sort' => 0,
'parentChapter' => $otherChapter->id,
'type' => 'page',
'book' => $otherChapter->book_id,
];
$this->actingAs($editor)->put($page->book->getUrl('/sort'), ['sort-tree' => json_encode([$sortData])])->assertRedirect();
$this->assertDatabaseHas('pages', [
'id' => $page->id, 'chapter_id' => $page->chapter_id, 'book_id' => $page->book_id,
]);
}
public function test_book_sort_item_returns_book_content() public function test_book_sort_item_returns_book_content()
{ {
$books = Book::all(); $books = Book::all();

View File

@ -670,51 +670,6 @@ class EntityPermissionsTest extends TestCase
$this->actingAs($this->user)->get($firstBook->getUrl('/sort')); $this->actingAs($this->user)->get($firstBook->getUrl('/sort'));
} }
public function test_book_sort_permission()
{
/** @var Book $firstBook */
$firstBook = Book::query()->first();
/** @var Book $secondBook */
$secondBook = Book::query()->find(2);
$this->setRestrictionsForTestRoles($firstBook, ['view', 'update']);
$this->setRestrictionsForTestRoles($secondBook, ['view']);
$firstBookChapter = $this->newChapter(['name' => 'first book chapter'], $firstBook);
$secondBookChapter = $this->newChapter(['name' => 'second book chapter'], $secondBook);
// Create request data
$reqData = [
[
'id' => $firstBookChapter->id,
'sort' => 0,
'parentChapter' => false,
'type' => 'chapter',
'book' => $secondBook->id,
],
];
// Move chapter from first book to a second book
$this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)])
->assertRedirect('/');
$this->get('/')->assertSee('You do not have permission');
$reqData = [
[
'id' => $secondBookChapter->id,
'sort' => 0,
'parentChapter' => false,
'type' => 'chapter',
'book' => $firstBook->id,
],
];
// Move chapter from second book to first book
$this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)])
->assertRedirect('/');
$this->get('/')->assertSee('You do not have permission');
}
public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible() public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible()
{ {
/** @var Book $book */ /** @var Book $book */