2019-10-05 07:55:01 -04:00
|
|
|
<?php namespace BookStack\Entities\Managers;
|
|
|
|
|
|
|
|
use BookStack\Entities\Book;
|
|
|
|
use BookStack\Entities\BookChild;
|
|
|
|
use BookStack\Entities\Chapter;
|
|
|
|
use BookStack\Entities\Entity;
|
|
|
|
use BookStack\Entities\Page;
|
|
|
|
use BookStack\Exceptions\SortOperationException;
|
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
|
|
|
|
class BookContents
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var Book
|
|
|
|
*/
|
|
|
|
protected $book;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* BookContents constructor.
|
|
|
|
* @param $book
|
|
|
|
*/
|
|
|
|
public function __construct(Book $book)
|
|
|
|
{
|
|
|
|
$this->book = $book;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current priority of the last item
|
|
|
|
* at the top-level of the book.
|
|
|
|
*/
|
|
|
|
public function getLastPriority(): int
|
|
|
|
{
|
|
|
|
$maxPage = Page::visible()->where('book_id', '=', $this->book->id)
|
|
|
|
->where('draft', '=', false)
|
|
|
|
->where('chapter_id', '=', 0)->max('priority');
|
|
|
|
$maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
|
|
|
|
->max('priority');
|
|
|
|
return max($maxChapter, $maxPage, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the contents as a sorted collection tree.
|
|
|
|
*/
|
|
|
|
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
|
|
|
{
|
|
|
|
$pages = $this->getPages($showDrafts);
|
|
|
|
$chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
|
|
|
|
$all = collect()->concat($pages)->concat($chapters);
|
|
|
|
$chapterMap = $chapters->keyBy('id');
|
|
|
|
$lonePages = collect();
|
|
|
|
|
|
|
|
$pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
|
|
|
|
$chapter = $chapterMap->get($chapter_id);
|
|
|
|
if ($chapter) {
|
|
|
|
$chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
|
|
|
|
} else {
|
|
|
|
$lonePages = $lonePages->concat($pages);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-08-14 17:13:52 -04:00
|
|
|
$all->each(function (Entity $entity) use ($renderPages) {
|
2019-10-05 07:55:01 -04:00
|
|
|
$entity->setRelation('book', $this->book);
|
2020-08-14 17:13:52 -04:00
|
|
|
|
2020-09-26 12:00:17 -04:00
|
|
|
if ($renderPages && $entity->isA('page')) {
|
2020-08-14 17:13:52 -04:00
|
|
|
$entity->html = (new PageContent($entity))->render();
|
|
|
|
}
|
2019-10-05 07:55:01 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function for providing a sorting score for an entity in relation to the
|
|
|
|
* other items within the book.
|
|
|
|
*/
|
|
|
|
protected function bookChildSortFunc(): callable
|
|
|
|
{
|
|
|
|
return function (Entity $entity) {
|
|
|
|
if (isset($entity['draft']) && $entity['draft']) {
|
|
|
|
return -100;
|
|
|
|
}
|
|
|
|
return $entity['priority'] ?? 0;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the visible pages within this book.
|
|
|
|
*/
|
|
|
|
protected function getPages(bool $showDrafts = false): Collection
|
|
|
|
{
|
|
|
|
$query = Page::visible()->where('book_id', '=', $this->book->id);
|
|
|
|
|
|
|
|
if (!$showDrafts) {
|
|
|
|
$query->where('draft', '=', false);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $query->get();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sort the books content using the given 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.
|
|
|
|
* @throws SortOperationException
|
|
|
|
*/
|
|
|
|
public function sortUsingMap(Collection $sortMap): Collection
|
|
|
|
{
|
|
|
|
// Load models into map
|
|
|
|
$this->loadModelsIntoSortMap($sortMap);
|
|
|
|
$booksInvolved = $this->getBooksInvolvedInSort($sortMap);
|
|
|
|
|
|
|
|
// Perform the sort
|
|
|
|
$sortMap->each(function ($mapItem) {
|
|
|
|
$this->applySortUpdates($mapItem);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Update permissions and activity.
|
|
|
|
$booksInvolved->each(function (Book $book) {
|
|
|
|
$book->rebuildPermissions();
|
|
|
|
});
|
|
|
|
|
|
|
|
return $booksInvolved;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Using the given sort map item, detect changes for the related model
|
|
|
|
* and update it if required.
|
|
|
|
*/
|
|
|
|
protected function applySortUpdates(\stdClass $sortMapItem)
|
|
|
|
{
|
|
|
|
/** @var BookChild $model */
|
|
|
|
$model = $sortMapItem->model;
|
|
|
|
|
|
|
|
$priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
|
|
|
|
$bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
|
|
|
|
$chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
|
|
|
|
|
|
|
|
if ($bookChanged) {
|
|
|
|
$model->changeBook($sortMapItem->book);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($chapterChanged) {
|
|
|
|
$model->chapter_id = intval($sortMapItem->parentChapter);
|
|
|
|
$model->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($priorityChanged) {
|
|
|
|
$model->priority = intval($sortMapItem->sort);
|
|
|
|
$model->save();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load models from the database into the given sort map.
|
|
|
|
*/
|
|
|
|
protected function loadModelsIntoSortMap(Collection $sortMap): void
|
|
|
|
{
|
|
|
|
$keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
|
|
|
|
return $sortMapItem->type . ':' . $sortMapItem->id;
|
|
|
|
});
|
|
|
|
$pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
|
|
|
|
$chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
|
|
|
|
|
|
|
|
$pages = Page::visible()->whereIn('id', $pageIds)->get();
|
|
|
|
$chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
|
|
|
|
|
|
|
|
foreach ($pages as $page) {
|
|
|
|
$sortItem = $keyMap->get('page:' . $page->id);
|
|
|
|
$sortItem->model = $page;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($chapters as $chapter) {
|
|
|
|
$sortItem = $keyMap->get('chapter:' . $chapter->id);
|
|
|
|
$sortItem->model = $chapter;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|