From d676e1e824e0377cfcb1736dd1ff622e383d8d02 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 13 Jun 2022 17:20:21 +0100 Subject: [PATCH] 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 --- app/Entities/Repos/BookRepo.php | 6 ++ app/Entities/Repos/BookshelfRepo.php | 19 ++--- app/Entities/Tools/Cloner.php | 37 ++++++---- app/Entities/Tools/HierarchyTransformer.php | 73 +++++++++++++++++++ app/Entities/Tools/TrashCan.php | 2 +- .../Controllers/Api/BookApiController.php | 30 ++++---- .../Api/BookshelfApiController.php | 35 +++++---- app/Http/Controllers/BookController.php | 14 ++-- app/Http/Controllers/BookshelfController.php | 17 +++-- tests/OpenGraphTest.php | 5 +- 10 files changed, 166 insertions(+), 72 deletions(-) create mode 100644 app/Entities/Tools/HierarchyTransformer.php diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 7c4b280a8..0c62a13fc 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -91,6 +91,7 @@ class BookRepo { $book = new Book(); $this->baseRepo->create($book, $input); + $this->baseRepo->updateCoverImage($book, $input['image']); 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 (isset($input['image'])) { + $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null); + } + Activity::add(ActivityType::BOOK_UPDATE, $book); return $book; diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index ceabba59a..03e7804d5 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -89,6 +89,7 @@ class BookshelfRepo { $shelf = new Bookshelf(); $this->baseRepo->create($shelf, $input); + $this->baseRepo->updateCoverImage($shelf, $input['image']); $this->updateBooks($shelf, $bookIds); Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf); @@ -106,14 +107,17 @@ class BookshelfRepo $this->updateBooks($shelf, $bookIds); } + if (isset($input['image'])) { + $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 +136,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. */ diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index b4923b90a..3553a9db3 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -50,11 +50,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 +62,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 +83,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,16 +99,26 @@ 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 + */ + 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; } /** diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php new file mode 100644 index 000000000..17e153e05 --- /dev/null +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -0,0 +1,73 @@ +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; + } +} \ No newline at end of file diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 1e130c9e1..abec2e2d5 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -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); diff --git a/app/Http/Controllers/Api/BookApiController.php b/app/Http/Controllers/Api/BookApiController.php index 2b6e7a2e1..73cac6318 100644 --- a/app/Http/Controllers/Api/BookApiController.php +++ b/app/Http/Controllers/Api/BookApiController.php @@ -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; @@ -97,4 +84,21 @@ 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()), + ], + ]; + } } diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index 2720d1db2..400dff977 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -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. */ @@ -117,4 +102,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()), + ], + ]; + } } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index bc403c6d0..b9dd0e799 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -100,7 +100,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 +157,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; + } else if (is_null($validated['image'])) { + unset($validated['image']); + } + + $book = $this->bookRepo->update($book, $validated); return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index 9a7f78a85..ce2e508c8 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -83,15 +83,14 @@ 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()), ]); $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 +159,20 @@ 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()), ]); + if ($request->has('image_reset')) { + $validated['image'] = null; + } else if (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()); } diff --git a/tests/OpenGraphTest.php b/tests/OpenGraphTest.php index 17a5aa2c5..dd99b7bef 100644 --- a/tests/OpenGraphTest.php +++ b/tests/OpenGraphTest.php @@ -6,6 +6,7 @@ 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; @@ -69,8 +70,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);