1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2024-10-30 07:32:39 +01:00

Started work on hierachy conversion actions

- Updates book/shelf cover image handling for easier cloning/handling.
- Adds core logic for promoting books/chapters up a level.
- Enables usage of book/shelf cover image via API.

Related to #1087
This commit is contained in:
Dan Brown 2022-06-13 17:20:21 +01:00
parent 0a05119aa5
commit d676e1e824
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 166 additions and 72 deletions

View File

@ -91,6 +91,7 @@ class BookRepo
{ {
$book = new Book(); $book = new Book();
$this->baseRepo->create($book, $input); $this->baseRepo->create($book, $input);
$this->baseRepo->updateCoverImage($book, $input['image']);
Activity::add(ActivityType::BOOK_CREATE, $book); Activity::add(ActivityType::BOOK_CREATE, $book);
return $book; return $book;
@ -102,6 +103,11 @@ class BookRepo
public function update(Book $book, array $input): Book public function update(Book $book, array $input): Book
{ {
$this->baseRepo->update($book, $input); $this->baseRepo->update($book, $input);
if (isset($input['image'])) {
$this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null);
}
Activity::add(ActivityType::BOOK_UPDATE, $book); Activity::add(ActivityType::BOOK_UPDATE, $book);
return $book; return $book;

View File

@ -89,6 +89,7 @@ class BookshelfRepo
{ {
$shelf = new Bookshelf(); $shelf = new Bookshelf();
$this->baseRepo->create($shelf, $input); $this->baseRepo->create($shelf, $input);
$this->baseRepo->updateCoverImage($shelf, $input['image']);
$this->updateBooks($shelf, $bookIds); $this->updateBooks($shelf, $bookIds);
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf); Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
@ -106,14 +107,17 @@ class BookshelfRepo
$this->updateBooks($shelf, $bookIds); $this->updateBooks($shelf, $bookIds);
} }
if (isset($input['image'])) {
$this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null);
}
Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf); Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
return $shelf; return $shelf;
} }
/** /**
* Update which books are assigned to this shelf by * Update which books are assigned to this shelf by syncing the given book ids.
* syncing the given book ids.
* Function ensures the books are visible to the current user and existing. * Function ensures the books are visible to the current user and existing.
*/ */
protected function updateBooks(Bookshelf $shelf, array $bookIds) protected function updateBooks(Bookshelf $shelf, array $bookIds)
@ -132,17 +136,6 @@ class BookshelfRepo
$shelf->books()->sync($syncData); $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. * Copy down the permissions of the given shelf to all child books.
*/ */

View File

@ -50,11 +50,8 @@ class Cloner
public function clonePage(Page $original, Entity $parent, string $newName): Page public function clonePage(Page $original, Entity $parent, string $newName): Page
{ {
$copyPage = $this->pageRepo->getNewDraftPage($parent); $copyPage = $this->pageRepo->getNewDraftPage($parent);
$pageData = $original->getAttributes(); $pageData = $this->entityToInputData($original);
// Update name & tags
$pageData['name'] = $newName; $pageData['name'] = $newName;
$pageData['tags'] = $this->entityTagsToInputArray($original);
return $this->pageRepo->publishDraft($copyPage, $pageData); return $this->pageRepo->publishDraft($copyPage, $pageData);
} }
@ -65,9 +62,8 @@ class Cloner
*/ */
public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
{ {
$chapterDetails = $original->getAttributes(); $chapterDetails = $this->entityToInputData($original);
$chapterDetails['name'] = $newName; $chapterDetails['name'] = $newName;
$chapterDetails['tags'] = $this->entityTagsToInputArray($original);
$copyChapter = $this->chapterRepo->create($chapterDetails, $parent); $copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
@ -87,9 +83,8 @@ class Cloner
*/ */
public function cloneBook(Book $original, string $newName): Book public function cloneBook(Book $original, string $newName): Book
{ {
$bookDetails = $original->getAttributes(); $bookDetails = $this->entityToInputData($original);
$bookDetails['name'] = $newName; $bookDetails['name'] = $newName;
$bookDetails['tags'] = $this->entityTagsToInputArray($original);
$copyBook = $this->bookRepo->create($bookDetails); $copyBook = $this->bookRepo->create($bookDetails);
@ -104,16 +99,26 @@ class Cloner
} }
} }
if ($original->cover) { return $copyBook;
try { }
$tmpImgFile = tmpfile();
$uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile); /**
$this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false); * Convert an entity to a raw data array of input data.
} catch (\Exception $exception) { * @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) {
$tmpImgFile = tmpfile();
$uploadedFile = $this->imageToUploadedFile($entity->cover, $tmpImgFile);
$inputData['image'] = $uploadedFile;
} }
return $copyBook; return $inputData;
} }
/** /**

View File

@ -0,0 +1,73 @@
<?php
namespace BookStack\Entities\Tools;
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;
class HierarchyTransformer
{
protected BookRepo $bookRepo;
protected BookshelfRepo $shelfRepo;
protected Cloner $cloner;
protected TrashCan $trashCan;
// TODO - Test setting book cover image from API
// Ensure we can update without resetting image accidentally
// Ensure api docs correct.
// TODO - As above but for shelves.
public function transformChapterToBook(Chapter $chapter): Book
{
// TODO - Check permissions before call
// Permissions: edit-chapter, delete-chapter, create-book
$inputData = $this->cloner->entityToInputData($chapter);
$book = $this->bookRepo->create($inputData);
// TODO - Copy permissions
/** @var Page $page */
foreach ($chapter->pages as $page) {
$page->chapter_id = 0;
$page->changeBook($book->id);
}
$this->trashCan->destroyEntity($chapter);
// TODO - Log activity for change
return $book;
}
public function transformBookToShelf(Book $book): Bookshelf
{
// TODO - Check permissions before call
// Permissions: edit-book, delete-book, create-shelf
$inputData = $this->cloner->entityToInputData($book);
$shelf = $this->shelfRepo->create($inputData, []);
// TODO - Copy permissions?
$shelfBookSyncData = [];
/** @var Chapter $chapter */
foreach ($book->chapters as $index => $chapter) {
$newBook = $this->transformChapterToBook($chapter);
$shelfBookSyncData[$newBook->id] = ['order' => $index];
}
$shelf->books()->sync($shelfBookSyncData);
if ($book->directPages->count() > 0) {
$book->name .= ' ' . trans('entities.pages');
} else {
$this->trashCan->destroyEntity($book);
}
// TODO - Log activity for change
return $shelf;
}
}

View File

@ -344,7 +344,7 @@ class TrashCan
* *
* @throws Exception * @throws Exception
*/ */
protected function destroyEntity(Entity $entity): int public function destroyEntity(Entity $entity): int
{ {
if ($entity instanceof Page) { if ($entity instanceof Page) {
return $this->destroyPage($entity); return $this->destroyPage($entity);

View File

@ -11,19 +11,6 @@ class BookApiController extends ApiController
{ {
protected $bookRepo; 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) public function __construct(BookRepo $bookRepo)
{ {
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
@ -97,4 +84,21 @@ class BookApiController extends ApiController
return response('', 204); 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()),
],
];
}
} }

View File

@ -13,21 +13,6 @@ class BookshelfApiController extends ApiController
{ {
protected BookshelfRepo $bookshelfRepo; 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. * BookshelfApiController constructor.
*/ */
@ -117,4 +102,24 @@ class BookshelfApiController extends ApiController
return response('', 204); 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()),
],
];
}
} }

View File

@ -100,7 +100,6 @@ class BookController extends Controller
} }
$book = $this->bookRepo->create($request->all()); $book = $this->bookRepo->create($request->all());
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
if ($bookshelf) { if ($bookshelf) {
$bookshelf->appendBook($book); $bookshelf->appendBook($book);
@ -158,15 +157,20 @@ class BookController extends Controller
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'], 'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'image' => array_merge(['nullable'], $this->getImageValidationRules()),
]); ]);
$book = $this->bookRepo->update($book, $request->all()); if ($request->has('image_reset')) {
$resetCover = $request->has('image_reset'); $validated['image'] = null;
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover); } else if (is_null($validated['image'])) {
unset($validated['image']);
}
$book = $this->bookRepo->update($book, $validated);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }

View File

@ -83,15 +83,14 @@ class BookshelfController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$this->checkPermission('bookshelf-create-all'); $this->checkPermission('bookshelf-create-all');
$this->validate($request, [ $validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'], 'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'image' => array_merge(['nullable'], $this->getImageValidationRules()),
]); ]);
$bookIds = explode(',', $request->get('books', '')); $bookIds = explode(',', $request->get('books', ''));
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds); $shelf = $this->bookshelfRepo->create($validated, $bookIds);
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
return redirect($shelf->getUrl()); return redirect($shelf->getUrl());
} }
@ -160,16 +159,20 @@ class BookshelfController extends Controller
{ {
$shelf = $this->bookshelfRepo->getBySlug($slug); $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf); $this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [ $validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'], 'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()), 'image' => array_merge(['nullable'], $this->getImageValidationRules()),
]); ]);
if ($request->has('image_reset')) {
$validated['image'] = null;
} else if (is_null($validated['image'])) {
unset($validated['image']);
}
$bookIds = explode(',', $request->get('books', '')); $bookIds = explode(',', $request->get('books', ''));
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds); $shelf = $this->bookshelfRepo->update($shelf, $validated, $bookIds);
$resetCover = $request->has('image_reset');
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
return redirect($shelf->getUrl()); return redirect($shelf->getUrl());
} }

View File

@ -6,6 +6,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BaseRepo;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -69,8 +70,8 @@ class OpenGraphTest extends TestCase
$this->assertArrayNotHasKey('image', $tags); $this->assertArrayNotHasKey('image', $tags);
// Test image set if image has cover image // Test image set if image has cover image
$shelfRepo = app(BookshelfRepo::class); $baseRepo = app(BaseRepo::class);
$shelfRepo->updateCoverImage($shelf, $this->getTestImage('image.png')); $baseRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
$resp = $this->asEditor()->get($shelf->getUrl()); $resp = $this->asEditor()->get($shelf->getUrl());
$tags = $this->getOpenGraphTags($resp); $tags = $this->getOpenGraphTags($resp);