mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 05:36:00 +00:00
Merge pull request #3499 from BookStackApp/convert_hierachy
Chapter and Book Conversion Actions
This commit is contained in:
commit
be1d691529
@ -16,11 +16,13 @@ class ActivityType
|
||||
const CHAPTER_MOVE = 'chapter_move';
|
||||
|
||||
const BOOK_CREATE = 'book_create';
|
||||
const BOOK_CREATE_FROM_CHAPTER = 'book_create_from_chapter';
|
||||
const BOOK_UPDATE = 'book_update';
|
||||
const BOOK_DELETE = 'book_delete';
|
||||
const BOOK_SORT = 'book_sort';
|
||||
|
||||
const BOOKSHELF_CREATE = 'bookshelf_create';
|
||||
const BOOKSHELF_CREATE_FROM_BOOK = 'bookshelf_create_from_book';
|
||||
const BOOKSHELF_UPDATE = 'bookshelf_update';
|
||||
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
||||
|
||||
|
@ -91,6 +91,7 @@ class BookRepo
|
||||
{
|
||||
$book = new Book();
|
||||
$this->baseRepo->create($book, $input);
|
||||
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
|
||||
Activity::add(ActivityType::BOOK_CREATE, $book);
|
||||
|
||||
return $book;
|
||||
@ -102,6 +103,11 @@ class BookRepo
|
||||
public function update(Book $book, array $input): Book
|
||||
{
|
||||
$this->baseRepo->update($book, $input);
|
||||
|
||||
if (array_key_exists('image', $input)) {
|
||||
$this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::BOOK_UPDATE, $book);
|
||||
|
||||
return $book;
|
||||
|
@ -6,12 +6,10 @@ use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BookshelfRepo
|
||||
@ -89,6 +87,7 @@ class BookshelfRepo
|
||||
{
|
||||
$shelf = new Bookshelf();
|
||||
$this->baseRepo->create($shelf, $input);
|
||||
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
|
||||
$this->updateBooks($shelf, $bookIds);
|
||||
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
|
||||
|
||||
@ -106,14 +105,17 @@ class BookshelfRepo
|
||||
$this->updateBooks($shelf, $bookIds);
|
||||
}
|
||||
|
||||
if (array_key_exists('image', $input)) {
|
||||
$this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
|
||||
|
||||
return $shelf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update which books are assigned to this shelf by
|
||||
* syncing the given book ids.
|
||||
* Update which books are assigned to this shelf by syncing the given book ids.
|
||||
* Function ensures the books are visible to the current user and existing.
|
||||
*/
|
||||
protected function updateBooks(Bookshelf $shelf, array $bookIds)
|
||||
@ -132,17 +134,6 @@ class BookshelfRepo
|
||||
$shelf->books()->sync($syncData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given shelf cover image, or clear it.
|
||||
*
|
||||
* @throws ImageUploadException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateCoverImage(Bookshelf $shelf, ?UploadedFile $coverImage, bool $removeImage = false)
|
||||
{
|
||||
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy down the permissions of the given shelf to all child books.
|
||||
*/
|
||||
|
@ -392,23 +392,6 @@ class PageRepo
|
||||
return $parentClass::visible()->where('id', '=', $entityId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
*/
|
||||
protected function changeParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = ($parent instanceof Chapter) ? $parent->book : $parent;
|
||||
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
|
||||
$page->save();
|
||||
|
||||
if ($page->book->id !== $book->id) {
|
||||
$page->changeBook($book->id);
|
||||
}
|
||||
|
||||
$page->load('book');
|
||||
$book->rebuildPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page revision to update for the given page.
|
||||
* Checks for an existing revisions before providing a fresh one.
|
||||
|
@ -16,25 +16,10 @@ use Illuminate\Http\UploadedFile;
|
||||
|
||||
class Cloner
|
||||
{
|
||||
/**
|
||||
* @var PageRepo
|
||||
*/
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* @var ChapterRepo
|
||||
*/
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* @var BookRepo
|
||||
*/
|
||||
protected $bookRepo;
|
||||
|
||||
/**
|
||||
* @var ImageService
|
||||
*/
|
||||
protected $imageService;
|
||||
protected PageRepo $pageRepo;
|
||||
protected ChapterRepo $chapterRepo;
|
||||
protected BookRepo $bookRepo;
|
||||
protected ImageService $imageService;
|
||||
|
||||
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
|
||||
{
|
||||
@ -50,11 +35,8 @@ class Cloner
|
||||
public function clonePage(Page $original, Entity $parent, string $newName): Page
|
||||
{
|
||||
$copyPage = $this->pageRepo->getNewDraftPage($parent);
|
||||
$pageData = $original->getAttributes();
|
||||
|
||||
// Update name & tags
|
||||
$pageData = $this->entityToInputData($original);
|
||||
$pageData['name'] = $newName;
|
||||
$pageData['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
return $this->pageRepo->publishDraft($copyPage, $pageData);
|
||||
}
|
||||
@ -65,9 +47,8 @@ class Cloner
|
||||
*/
|
||||
public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
|
||||
{
|
||||
$chapterDetails = $original->getAttributes();
|
||||
$chapterDetails = $this->entityToInputData($original);
|
||||
$chapterDetails['name'] = $newName;
|
||||
$chapterDetails['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
$copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
|
||||
|
||||
@ -87,9 +68,8 @@ class Cloner
|
||||
*/
|
||||
public function cloneBook(Book $original, string $newName): Book
|
||||
{
|
||||
$bookDetails = $original->getAttributes();
|
||||
$bookDetails = $this->entityToInputData($original);
|
||||
$bookDetails['name'] = $newName;
|
||||
$bookDetails['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
$copyBook = $this->bookRepo->create($bookDetails);
|
||||
|
||||
@ -104,26 +84,48 @@ class Cloner
|
||||
}
|
||||
}
|
||||
|
||||
if ($original->cover) {
|
||||
try {
|
||||
$tmpImgFile = tmpfile();
|
||||
$uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile);
|
||||
$this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false);
|
||||
} catch (\Exception $exception) {
|
||||
}
|
||||
return $copyBook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an entity to a raw data array of input data.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function entityToInputData(Entity $entity): array
|
||||
{
|
||||
$inputData = $entity->getAttributes();
|
||||
$inputData['tags'] = $this->entityTagsToInputArray($entity);
|
||||
|
||||
// Add a cover to the data if existing on the original entity
|
||||
if ($entity->cover instanceof Image) {
|
||||
$uploadedFile = $this->imageToUploadedFile($entity->cover);
|
||||
$inputData['image'] = $uploadedFile;
|
||||
}
|
||||
|
||||
return $copyBook;
|
||||
return $inputData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permission settings from the source entity to the target entity.
|
||||
*/
|
||||
public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void
|
||||
{
|
||||
$targetEntity->restricted = $sourceEntity->restricted;
|
||||
$permissions = $sourceEntity->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$targetEntity->permissions()->delete();
|
||||
$targetEntity->permissions()->createMany($permissions);
|
||||
$targetEntity->rebuildPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an image instance to an UploadedFile instance to mimic
|
||||
* a file being uploaded.
|
||||
*/
|
||||
protected function imageToUploadedFile(Image $image, &$tmpFile): ?UploadedFile
|
||||
protected function imageToUploadedFile(Image $image): ?UploadedFile
|
||||
{
|
||||
$imgData = $this->imageService->getImageData($image);
|
||||
$tmpImgFilePath = stream_get_meta_data($tmpFile)['uri'];
|
||||
$tmpImgFilePath = tempnam(sys_get_temp_dir(), 'bs_cover_clone_');
|
||||
file_put_contents($tmpImgFilePath, $imgData);
|
||||
|
||||
return new UploadedFile($tmpImgFilePath, basename($image->path));
|
||||
|
87
app/Entities/Tools/HierarchyTransformer.php
Normal file
87
app/Entities/Tools/HierarchyTransformer.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use BookStack\Facades\Activity;
|
||||
|
||||
class HierarchyTransformer
|
||||
{
|
||||
protected BookRepo $bookRepo;
|
||||
protected BookshelfRepo $shelfRepo;
|
||||
protected Cloner $cloner;
|
||||
protected TrashCan $trashCan;
|
||||
|
||||
public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->shelfRepo = $shelfRepo;
|
||||
$this->cloner = $cloner;
|
||||
$this->trashCan = $trashCan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a chapter into a book.
|
||||
* Does not check permissions, check before calling.
|
||||
*/
|
||||
public function transformChapterToBook(Chapter $chapter): Book
|
||||
{
|
||||
$inputData = $this->cloner->entityToInputData($chapter);
|
||||
$book = $this->bookRepo->create($inputData);
|
||||
$this->cloner->copyEntityPermissions($chapter, $book);
|
||||
|
||||
/** @var Page $page */
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->changeBook($book->id);
|
||||
}
|
||||
|
||||
$this->trashCan->destroyEntity($chapter);
|
||||
|
||||
Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER, $book);
|
||||
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a book into a shelf.
|
||||
* Does not check permissions, check before calling.
|
||||
*/
|
||||
public function transformBookToShelf(Book $book): Bookshelf
|
||||
{
|
||||
$inputData = $this->cloner->entityToInputData($book);
|
||||
$shelf = $this->shelfRepo->create($inputData, []);
|
||||
$this->cloner->copyEntityPermissions($book, $shelf);
|
||||
|
||||
$shelfBookSyncData = [];
|
||||
|
||||
/** @var Chapter $chapter */
|
||||
foreach ($book->chapters as $index => $chapter) {
|
||||
$newBook = $this->transformChapterToBook($chapter);
|
||||
$shelfBookSyncData[$newBook->id] = ['order' => $index];
|
||||
if (!$newBook->restricted) {
|
||||
$this->cloner->copyEntityPermissions($shelf, $newBook);
|
||||
}
|
||||
}
|
||||
|
||||
if ($book->directPages->count() > 0) {
|
||||
$book->name .= ' ' . trans('entities.pages');
|
||||
$shelfBookSyncData[$book->id] = ['order' => count($shelfBookSyncData) + 1];
|
||||
$book->save();
|
||||
} else {
|
||||
$this->trashCan->destroyEntity($book);
|
||||
}
|
||||
|
||||
$shelf->books()->sync($shelfBookSyncData);
|
||||
|
||||
Activity::add(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $shelf);
|
||||
|
||||
return $shelf;
|
||||
}
|
||||
}
|
@ -344,7 +344,7 @@ class TrashCan
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function destroyEntity(Entity $entity): int
|
||||
public function destroyEntity(Entity $entity): int
|
||||
{
|
||||
if ($entity instanceof Page) {
|
||||
return $this->destroyPage($entity);
|
||||
|
@ -11,19 +11,6 @@ class BookApiController extends ApiController
|
||||
{
|
||||
protected $bookRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
@ -43,13 +30,15 @@ class BookApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Create a new book in the system.
|
||||
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the book cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
|
||||
$book = $this->bookRepo->create($requestData);
|
||||
|
||||
@ -68,6 +57,8 @@ class BookApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Update the details of a single book.
|
||||
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the book cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@ -76,7 +67,7 @@ class BookApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$book = $this->bookRepo->update($book, $requestData);
|
||||
|
||||
return response()->json($book);
|
||||
@ -97,4 +88,22 @@ class BookApiController extends ApiController
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -13,21 +13,6 @@ class BookshelfApiController extends ApiController
|
||||
{
|
||||
protected BookshelfRepo $bookshelfRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* BookshelfApiController constructor.
|
||||
*/
|
||||
@ -52,13 +37,15 @@ class BookshelfApiController extends ApiController
|
||||
* Create a new shelf in the system.
|
||||
* An array of books IDs can be provided in the request. These
|
||||
* will be added to the shelf in the same order as provided.
|
||||
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the shelf cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
|
||||
$bookIds = $request->get('books', []);
|
||||
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
||||
@ -86,6 +73,8 @@ class BookshelfApiController extends ApiController
|
||||
* An array of books IDs can be provided in the request. These
|
||||
* will be added to the shelf in the same order as provided and overwrite
|
||||
* any existing book assignments.
|
||||
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the shelf cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@ -94,7 +83,7 @@ class BookshelfApiController extends ApiController
|
||||
$shelf = Bookshelf::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$bookIds = $request->get('books', null);
|
||||
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
||||
@ -117,4 +106,24 @@ class BookshelfApiController extends ApiController
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
@ -100,7 +101,6 @@ class BookController extends Controller
|
||||
}
|
||||
|
||||
$book = $this->bookRepo->create($request->all());
|
||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
|
||||
|
||||
if ($bookshelf) {
|
||||
$bookshelf->appendBook($book);
|
||||
@ -158,15 +158,20 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
]);
|
||||
|
||||
$book = $this->bookRepo->update($book, $request->all());
|
||||
$resetCover = $request->has('image_reset');
|
||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
|
||||
if ($request->has('image_reset')) {
|
||||
$validated['image'] = null;
|
||||
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$book = $this->bookRepo->update($book, $validated);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@ -262,4 +267,20 @@ class BookController extends Controller
|
||||
|
||||
return redirect($bookCopy->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chapter to a book.
|
||||
*/
|
||||
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->checkPermission('book-create-all');
|
||||
|
||||
$shelf = $transformer->transformBookToShelf($book);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
}
|
||||
|
@ -83,15 +83,15 @@ class BookshelfController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->validate($request, [
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
|
||||
$shelf = $this->bookshelfRepo->create($validated, $bookIds);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
@ -160,16 +160,21 @@ class BookshelfController extends Controller
|
||||
{
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
$this->validate($request, [
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
if ($request->has('image_reset')) {
|
||||
$validated['image'] = null;
|
||||
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
|
||||
$resetCover = $request->has('image_reset');
|
||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $validated, $bookIds);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Repos\ChapterRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
@ -272,4 +273,19 @@ class ChapterController extends Controller
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chapter to a book.
|
||||
*/
|
||||
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
|
||||
{
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->checkPermission('book-create-all');
|
||||
|
||||
$book = $transformer->transformChapterToBook($chapter);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ return [
|
||||
// Books
|
||||
'book_create' => 'created book',
|
||||
'book_create_notification' => 'Book successfully created',
|
||||
'book_create_from_chapter' => 'converted chapter to book',
|
||||
'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',
|
||||
'book_update' => 'updated book',
|
||||
'book_update_notification' => 'Book successfully updated',
|
||||
'book_delete' => 'deleted book',
|
||||
@ -38,6 +40,8 @@ return [
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'updated bookshelf',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted bookshelf',
|
||||
|
@ -355,4 +355,16 @@ return [
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
|
||||
// Conversions
|
||||
'convert_to_shelf' => 'Convert to Shelf',
|
||||
'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',
|
||||
'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',
|
||||
'convert_book' => 'Convert Book',
|
||||
'convert_book_confirm' => 'Are you sure you want to convert this book?',
|
||||
'convert_undo_warning' => 'This cannot be as easily undone.',
|
||||
'convert_to_book' => 'Convert to Book',
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
];
|
||||
|
@ -14,12 +14,17 @@
|
||||
]])
|
||||
</div>
|
||||
|
||||
<main class="content-wrap card">
|
||||
<main class="content-wrap card auto-height">
|
||||
<h1 class="list-heading">{{ trans('entities.books_edit') }}</h1>
|
||||
<form action="{{ $book->getUrl() }}" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
@include('books.parts.form', ['model' => $book, 'returnLocation' => $book->getUrl()])
|
||||
</form>
|
||||
</main>
|
||||
|
||||
|
||||
@if(userCan('book-delete', $book) && userCan('book-create-all') && userCan('bookshelf-create-all'))
|
||||
@include('books.parts.convert-to-shelf', ['book' => $book])
|
||||
@endif
|
||||
</div>
|
||||
@stop
|
26
resources/views/books/parts/convert-to-shelf.blade.php
Normal file
26
resources/views/books/parts/convert-to-shelf.blade.php
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="content-wrap card auto-height">
|
||||
<h2 class="list-heading">{{ trans('entities.convert_to_shelf') }}</h2>
|
||||
<p>
|
||||
{{ trans('entities.convert_to_shelf_contents_desc') }}
|
||||
<br><br>
|
||||
{{ trans('entities.convert_to_shelf_permissions_desc') }}
|
||||
</p>
|
||||
<div class="text-right">
|
||||
<div component="dropdown" class="dropdown-container">
|
||||
<button refs="dropdown@toggle" class="button outline" aria-haspopup="true" aria-expanded="false">{{ trans('entities.convert_book') }}</button>
|
||||
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
|
||||
<li class="px-m py-s text-small text-muted">
|
||||
{{ trans('entities.convert_book_confirm') }}
|
||||
<br>
|
||||
{{ trans('entities.convert_undo_warning') }}
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ $book->getUrl('/convert-to-shelf') }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<button type="submit" class="text-primary text-item">{{ trans('common.confirm') }}</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -15,7 +15,7 @@
|
||||
]])
|
||||
</div>
|
||||
|
||||
<main class="content-wrap card">
|
||||
<main class="content-wrap card auto-height">
|
||||
<h1 class="list-heading">{{ trans('entities.chapters_edit') }}</h1>
|
||||
<form action="{{ $chapter->getUrl() }}" method="POST">
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
@ -23,6 +23,10 @@
|
||||
</form>
|
||||
</main>
|
||||
|
||||
@if(userCan('chapter-delete', $chapter) && userCan('book-create-all'))
|
||||
@include('chapters.parts.convert-to-book')
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@stop
|
28
resources/views/chapters/parts/convert-to-book.blade.php
Normal file
28
resources/views/chapters/parts/convert-to-book.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
<div class="content-wrap card auto-height">
|
||||
<h2 class="list-heading">{{ trans('entities.convert_to_book') }}</h2>
|
||||
<div class="grid half left-focus no-row-gap">
|
||||
<p>
|
||||
{{ trans('entities.convert_to_book_desc') }}
|
||||
</p>
|
||||
<div class="text-m-right">
|
||||
<div component="dropdown" class="dropdown-container">
|
||||
<button refs="dropdown@toggle" class="button outline" aria-haspopup="true" aria-expanded="false">
|
||||
{{ trans('entities.convert_chapter') }}
|
||||
</button>
|
||||
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
|
||||
<li class="px-m py-s text-small text-muted">
|
||||
{{ trans('entities.convert_chapter_confirm') }}
|
||||
<br>
|
||||
{{ trans('entities.convert_undo_warning') }}
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ $chapter->getUrl('/convert-to-book') }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<button type="submit" class="text-primary text-item">{{ trans('common.confirm') }}</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -82,6 +82,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
|
||||
Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']);
|
||||
Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']);
|
||||
Route::post('/books/{bookSlug}/convert-to-shelf', [BookController::class, 'convertToShelf']);
|
||||
Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
|
||||
Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
|
||||
Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
|
||||
@ -132,6 +133,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'showCopy']);
|
||||
Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']);
|
||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']);
|
||||
Route::post('/books/{bookSlug}/chapter/{chapterSlug}/convert-to-book', [ChapterController::class, 'convertToBook']);
|
||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']);
|
||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']);
|
||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ChapterExportController::class, 'html']);
|
||||
|
@ -6,10 +6,12 @@ use BookStack\Entities\Models\Book;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
use Tests\Uploads\UsesImages;
|
||||
|
||||
class BooksApiTest extends TestCase
|
||||
{
|
||||
use TestsApi;
|
||||
use UsesImages;
|
||||
|
||||
protected string $baseEndpoint = '/api/books';
|
||||
|
||||
@ -118,6 +120,42 @@ class BooksApiTest extends TestCase
|
||||
$this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
|
||||
}
|
||||
|
||||
public function test_update_cover_image_control()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
/** @var Book $book */
|
||||
$book = Book::visible()->first();
|
||||
$this->assertNull($book->cover);
|
||||
$file = $this->getTestImage('image.png');
|
||||
|
||||
// Ensure cover image can be set via API
|
||||
$resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [
|
||||
'name' => 'My updated API book with image',
|
||||
], [], ['image' => $file]);
|
||||
$book->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertNotNull($book->cover);
|
||||
|
||||
// Ensure further updates without image do not clear cover image
|
||||
$resp = $this->put($this->baseEndpoint . "/{$book->id}", [
|
||||
'name' => 'My updated book again',
|
||||
]);
|
||||
$book->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertNotNull($book->cover);
|
||||
|
||||
// Ensure update with null image property clears image
|
||||
$resp = $this->put($this->baseEndpoint . "/{$book->id}", [
|
||||
'image' => null,
|
||||
]);
|
||||
$book->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertNull($book->cover);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
|
@ -7,10 +7,12 @@ use BookStack\Entities\Models\Bookshelf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
use Tests\Uploads\UsesImages;
|
||||
|
||||
class ShelvesApiTest extends TestCase
|
||||
{
|
||||
use TestsApi;
|
||||
use UsesImages;
|
||||
|
||||
protected string $baseEndpoint = '/api/shelves';
|
||||
|
||||
@ -146,6 +148,42 @@ class ShelvesApiTest extends TestCase
|
||||
$this->assertTrue($shelf->books()->count() === 0);
|
||||
}
|
||||
|
||||
public function test_update_cover_image_control()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
/** @var Book $shelf */
|
||||
$shelf = Bookshelf::visible()->first();
|
||||
$this->assertNull($shelf->cover);
|
||||
$file = $this->getTestImage('image.png');
|
||||
|
||||
// Ensure cover image can be set via API
|
||||
$resp = $this->call('PUT', $this->baseEndpoint . "/{$shelf->id}", [
|
||||
'name' => 'My updated API shelf with image',
|
||||
], [], ['image' => $file]);
|
||||
$shelf->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertNotNull($shelf->cover);
|
||||
|
||||
// Ensure further updates without image do not clear cover image
|
||||
$resp = $this->put($this->baseEndpoint . "/{$shelf->id}", [
|
||||
'name' => 'My updated shelf again',
|
||||
]);
|
||||
$shelf->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertNotNull($shelf->cover);
|
||||
|
||||
// Ensure update with null image property clears image
|
||||
$resp = $this->put($this->baseEndpoint . "/{$shelf->id}", [
|
||||
'image' => null,
|
||||
]);
|
||||
$shelf->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertNull($shelf->cover);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
|
@ -290,6 +290,7 @@ class BookTest extends TestCase
|
||||
|
||||
/** @var Book $copy */
|
||||
$copy = Book::query()->where('name', '=', 'My copy book')->first();
|
||||
|
||||
$this->assertNotNull($copy->cover);
|
||||
$this->assertNotEquals($book->cover->id, $copy->cover->id);
|
||||
}
|
||||
|
145
tests/Entity/ConvertTest.php
Normal file
145
tests/Entity/ConvertTest.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Entity;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConvertTest extends TestCase
|
||||
{
|
||||
public function test_chapter_edit_view_shows_convert_option()
|
||||
{
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = Chapter::query()->first();
|
||||
|
||||
$resp = $this->asEditor()->get($chapter->getUrl('/edit'));
|
||||
$resp->assertSee('Convert to Book');
|
||||
$resp->assertSee('Convert Chapter');
|
||||
$resp->assertElementExists('form[action$="/convert-to-book"] button');
|
||||
}
|
||||
|
||||
public function test_convert_chapter_to_book()
|
||||
{
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = Chapter::query()->whereHas('pages')->first();
|
||||
$chapter->tags()->save(new Tag(['name' => 'Category', 'value' => 'Penguins']));
|
||||
/** @var Page $childPage */
|
||||
$childPage = $chapter->pages()->first();
|
||||
|
||||
$resp = $this->asEditor()->post($chapter->getUrl('/convert-to-book'));
|
||||
$resp->assertRedirectContains('/books/');
|
||||
|
||||
/** @var Book $newBook */
|
||||
$newBook = Book::query()->orderBy('id', 'desc')->first();
|
||||
|
||||
$this->assertDatabaseMissing('chapters', ['id' => $chapter->id]);
|
||||
$this->assertDatabaseHas('pages', ['id' => $childPage->id, 'book_id' => $newBook->id, 'chapter_id' => 0]);
|
||||
$this->assertCount(1, $newBook->tags);
|
||||
$this->assertEquals('Category', $newBook->tags->first()->name);
|
||||
$this->assertEquals('Penguins', $newBook->tags->first()->value);
|
||||
$this->assertEquals($chapter->name, $newBook->name);
|
||||
$this->assertEquals($chapter->description, $newBook->description);
|
||||
|
||||
$this->assertActivityExists(ActivityType::BOOK_CREATE_FROM_CHAPTER, $newBook);
|
||||
}
|
||||
|
||||
public function test_convert_chapter_to_book_requires_permissions()
|
||||
{
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = Chapter::query()->first();
|
||||
$user = $this->getViewer();
|
||||
|
||||
$permissions = ['chapter-delete-all', 'book-create-all', 'chapter-update-all'];
|
||||
$this->giveUserPermissions($user, $permissions);
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
$this->removePermissionFromUser($user, $permission);
|
||||
$resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book'));
|
||||
$this->assertPermissionError($resp);
|
||||
$this->giveUserPermissions($user, [$permission]);
|
||||
}
|
||||
|
||||
$resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book'));
|
||||
$this->assertNotPermissionError($resp);
|
||||
$resp->assertRedirect();
|
||||
}
|
||||
|
||||
public function test_book_edit_view_shows_convert_option()
|
||||
{
|
||||
$book = Book::query()->first();
|
||||
|
||||
$resp = $this->asEditor()->get($book->getUrl('/edit'));
|
||||
$resp->assertSee('Convert to Shelf');
|
||||
$resp->assertSee('Convert Book');
|
||||
$resp->assertSee('Note that permissions on shelves do not auto-cascade to content');
|
||||
$resp->assertElementExists('form[action$="/convert-to-shelf"] button');
|
||||
}
|
||||
|
||||
public function test_book_convert_to_shelf()
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = Book::query()->whereHas('directPages')->whereHas('chapters')->firstOrFail();
|
||||
$book->tags()->save(new Tag(['name' => 'Category', 'value' => 'Ducks']));
|
||||
/** @var Page $childPage */
|
||||
$childPage = $book->directPages()->first();
|
||||
/** @var Chapter $childChapter */
|
||||
$childChapter = $book->chapters()->whereHas('pages')->firstOrFail();
|
||||
/** @var Page $chapterChildPage */
|
||||
$chapterChildPage = $childChapter->pages()->firstOrFail();
|
||||
$bookChapterCount = $book->chapters()->count();
|
||||
$systemBookCount = Book::query()->count();
|
||||
|
||||
// Run conversion
|
||||
$resp = $this->asEditor()->post($book->getUrl('/convert-to-shelf'));
|
||||
|
||||
/** @var Bookshelf $newShelf */
|
||||
$newShelf = Bookshelf::query()->orderBy('id', 'desc')->first();
|
||||
|
||||
// Checks for new shelf
|
||||
$resp->assertRedirectContains('/shelves/');
|
||||
$this->assertDatabaseMissing('chapters', ['id' => $childChapter->id]);
|
||||
$this->assertCount(1, $newShelf->tags);
|
||||
$this->assertEquals('Category', $newShelf->tags->first()->name);
|
||||
$this->assertEquals('Ducks', $newShelf->tags->first()->value);
|
||||
$this->assertEquals($book->name, $newShelf->name);
|
||||
$this->assertEquals($book->description, $newShelf->description);
|
||||
$this->assertEquals($newShelf->books()->count(), $bookChapterCount + 1);
|
||||
$this->assertEquals($systemBookCount + $bookChapterCount, Book::query()->count());
|
||||
$this->assertActivityExists(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $newShelf);
|
||||
|
||||
// Checks for old book to contain child pages
|
||||
$this->assertDatabaseHas('books', ['id' => $book->id, 'name' => $book->name . ' Pages']);
|
||||
$this->assertDatabaseHas('pages', ['id' => $childPage->id, 'book_id' => $book->id, 'chapter_id' => 0]);
|
||||
|
||||
// Checks for nested page
|
||||
$chapterChildPage->refresh();
|
||||
$this->assertEquals(0, $chapterChildPage->chapter_id);
|
||||
$this->assertEquals($childChapter->name, $chapterChildPage->book->name);
|
||||
}
|
||||
|
||||
public function test_book_convert_to_shelf_requires_permissions()
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = Book::query()->first();
|
||||
$user = $this->getViewer();
|
||||
|
||||
$permissions = ['book-delete-all', 'bookshelf-create-all', 'book-update-all', 'book-create-all'];
|
||||
$this->giveUserPermissions($user, $permissions);
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
$this->removePermissionFromUser($user, $permission);
|
||||
$resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf'));
|
||||
$this->assertPermissionError($resp);
|
||||
$this->giveUserPermissions($user, [$permission]);
|
||||
}
|
||||
|
||||
$resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf'));
|
||||
$this->assertNotPermissionError($resp);
|
||||
$resp->assertRedirect();
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\BaseRepo;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\Uploads\UsesImages;
|
||||
|
||||
@ -69,8 +69,8 @@ class OpenGraphTest extends TestCase
|
||||
$this->assertArrayNotHasKey('image', $tags);
|
||||
|
||||
// Test image set if image has cover image
|
||||
$shelfRepo = app(BookshelfRepo::class);
|
||||
$shelfRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
|
||||
$baseRepo = app(BaseRepo::class);
|
||||
$baseRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
|
||||
$resp = $this->asEditor()->get($shelf->getUrl());
|
||||
$tags = $this->getOpenGraphTags($resp);
|
||||
|
||||
|
@ -194,13 +194,23 @@ trait SharedTestHelpers
|
||||
/**
|
||||
* Completely remove the given permission name from the given user.
|
||||
*/
|
||||
protected function removePermissionFromUser(User $user, string $permission)
|
||||
protected function removePermissionFromUser(User $user, string $permissionName)
|
||||
{
|
||||
$permission = RolePermission::query()->where('name', '=', $permission)->first();
|
||||
$permissionService = app()->make(PermissionService::class);
|
||||
|
||||
/** @var RolePermission $permission */
|
||||
$permission = RolePermission::query()->where('name', '=', $permissionName)->firstOrFail();
|
||||
|
||||
$roles = $user->roles()->whereHas('permissions', function ($query) use ($permission) {
|
||||
$query->where('id', '=', $permission->id);
|
||||
})->get();
|
||||
|
||||
/** @var Role $role */
|
||||
foreach ($user->roles as $role) {
|
||||
foreach ($roles as $role) {
|
||||
$role->detachPermission($permission);
|
||||
$permissionService->buildJointPermissionForRole($role);
|
||||
}
|
||||
|
||||
$user->clearPermissionCache();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user