BookStack/app/Entities/Tools/BookContents.php

219 lines
6.7 KiB
PHP
Raw Normal View History

2021-06-26 23:23:15 +08:00
<?php
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\SortOperationException;
use Illuminate\Support\Collection;
class BookContents
{
/**
* @var Book
*/
protected $book;
/**
* BookContents constructor.
*/
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');
2021-06-26 23:23:15 +08:00
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, $renderPages);
$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('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
} else {
$lonePages = $lonePages->concat($pages);
}
});
$chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
$chapter->setAttribute('visible_pages', collect([]));
});
$all->each(function (Entity $entity) use ($renderPages) {
$entity->setRelation('book', $this->book);
if ($renderPages && $entity instanceof Page) {
$entity->html = (new PageContent($entity))->render();
}
});
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;
}
2021-06-26 23:23:15 +08:00
return $entity['priority'] ?? 0;
};
}
/**
* Get the visible pages within this book.
*/
protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
{
$query = Page::visible()
->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
->where('book_id', '=', $this->book->id);
if (!$showDrafts) {
$query->where('draft', '=', false);
}
return $query->get();
}
/**
* Sort the books content using the given sort map.
* Returns a list of books that were involved in the operation.
2021-06-26 23:23:15 +08:00
*
* @throws SortOperationException
*/
public function sortUsingMap(BookSortMap $sortMap): Collection
{
// Load models into map
$this->loadModelsIntoSortMap($sortMap);
$booksInvolved = $this->getBooksInvolvedInSort($sortMap);
// Perform the sort
foreach ($sortMap->all() as $item) {
$this->applySortUpdates($item);
}
// 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(BookSortMapItem $sortMapItem): void
{
$model = $sortMapItem->model;
if (!$model) {
return;
}
$priorityChanged = $model->priority !== $sortMapItem->sort;
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
if ($bookChanged) {
$model->changeBook($sortMapItem->parentBookId);
}
if ($chapterChanged) {
$model->chapter_id = intval($sortMapItem->parentChapterId);
$model->save();
}
if ($priorityChanged) {
$model->priority = $sortMapItem->sort;
$model->save();
}
}
/**
* Load models from the database into the given sort map.
*/
protected function loadModelsIntoSortMap(BookSortMap $sortMap): void
{
$collection = collect($sortMap->all());
$keyMap = $collection->keyBy(function (BookSortMapItem $sortMapItem) {
return $sortMapItem->type . ':' . $sortMapItem->id;
});
$pageIds = $collection->where('type', '=', 'page')->pluck('id');
$chapterIds = $collection->where('type', '=', 'chapter')->pluck('id');
$pages = Page::visible()->whereIn('id', $pageIds)->get();
$chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
foreach ($pages as $page) {
/** @var BookSortMapItem $sortItem */
$sortItem = $keyMap->get('page:' . $page->id);
$sortItem->model = $page;
}
foreach ($chapters as $chapter) {
/** @var BookSortMapItem $sortItem */
$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.
2021-06-26 23:23:15 +08:00
*
* @throws SortOperationException
*/
protected function getBooksInvolvedInSort(BookSortMap $sortMap): Collection
{
$collection = collect($sortMap->all());
$bookIdsInvolved = array_unique(array_merge(
[$this->book->id],
$collection->pluck('parentBookId')->values()->all(),
$collection->pluck('model.book_id')->values()->all(),
));
$books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
if (count($books) !== count($bookIdsInvolved)) {
2021-06-26 23:23:15 +08:00
throw new SortOperationException('Could not find all books requested in sort operation');
}
return $books;
}
}