]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'chapter-templates' into development
authorDan Brown <redacted>
Thu, 1 Feb 2024 12:55:38 +0000 (12:55 +0000)
committerDan Brown <redacted>
Thu, 1 Feb 2024 12:55:38 +0000 (12:55 +0000)
23 files changed:
app/Api/ListingResponseBuilder.php
app/Entities/Controllers/ChapterApiController.php
app/Entities/Controllers/ChapterController.php
app/Entities/Controllers/PageController.php
app/Entities/Models/Chapter.php
app/Entities/Repos/BaseRepo.php
app/Entities/Repos/BookRepo.php
app/Entities/Repos/ChapterRepo.php
app/Entities/Repos/PageRepo.php
app/Entities/Tools/TrashCan.php
database/migrations/2024_01_01_104542_add_default_template_to_chapters.php [new file with mode: 0644]
dev/api/requests/chapters-create.json
dev/api/requests/chapters-update.json
dev/api/responses/chapters-create.json
dev/api/responses/chapters-read.json
dev/api/responses/chapters-update.json
lang/en/entities.php
resources/views/books/parts/form.blade.php
resources/views/chapters/parts/form.blade.php
resources/views/entities/template-selector.blade.php [new file with mode: 0644]
tests/Api/ChaptersApiTest.php
tests/Entity/BookDefaultTemplateTest.php [deleted file]
tests/Entity/DefaultTemplateTest.php [new file with mode: 0644]

index 44117bad9759e5108502485003c66fd3a599e96f..329f5ce1c58fc54e8f11d279e691f673e89aea89 100644 (file)
@@ -61,6 +61,8 @@ class ListingResponseBuilder
             }
         });
 
+        dd($data->first());
+
         return response()->json([
             'data'  => $data,
             'total' => $total,
index 85c81c2485c92e129c56f2a32ed3f09b5e00e62b..3fbe852220579d2b0ad36b8b7f3237d5338af658 100644 (file)
@@ -15,20 +15,22 @@ class ChapterApiController extends ApiController
 {
     protected $rules = [
         'create' => [
-            'book_id'          => ['required', 'integer'],
-            'name'             => ['required', 'string', 'max:255'],
-            'description'      => ['string', 'max:1900'],
-            'description_html' => ['string', 'max:2000'],
-            'tags'             => ['array'],
-            'priority'         => ['integer'],
+            'book_id'             => ['required', 'integer'],
+            'name'                => ['required', 'string', 'max:255'],
+            'description'         => ['string', 'max:1900'],
+            'description_html'    => ['string', 'max:2000'],
+            'tags'                => ['array'],
+            'priority'            => ['integer'],
+            'default_template_id' => ['nullable', 'integer'],
         ],
         'update' => [
-            'book_id'          => ['integer'],
-            'name'             => ['string', 'min:1', 'max:255'],
-            'description'      => ['string', 'max:1900'],
-            'description_html' => ['string', 'max:2000'],
-            'tags'             => ['array'],
-            'priority'         => ['integer'],
+            'book_id'             => ['integer'],
+            'name'                => ['string', 'min:1', 'max:255'],
+            'description'         => ['string', 'max:1900'],
+            'description_html'    => ['string', 'max:2000'],
+            'tags'                => ['array'],
+            'priority'            => ['integer'],
+            'default_template_id' => ['nullable', 'integer'],
         ],
     ];
 
index 28ad35fa4b37e1b949ee942cd0f05475427baada..00616888a701c7aea91cc8f9f8517d9a3e1426c7 100644 (file)
@@ -49,9 +49,10 @@ class ChapterController extends Controller
     public function store(Request $request, string $bookSlug)
     {
         $validated = $this->validate($request, [
-            'name'             => ['required', 'string', 'max:255'],
-            'description_html' => ['string', 'max:2000'],
-            'tags'             => ['array'],
+            'name'                => ['required', 'string', 'max:255'],
+            'description_html'    => ['string', 'max:2000'],
+            'tags'                => ['array'],
+            'default_template_id' => ['nullable', 'integer'],
         ]);
 
         $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
@@ -111,9 +112,10 @@ class ChapterController extends Controller
     public function update(Request $request, string $bookSlug, string $chapterSlug)
     {
         $validated = $this->validate($request, [
-            'name'             => ['required', 'string', 'max:255'],
-            'description_html' => ['string', 'max:2000'],
-            'tags'             => ['array'],
+            'name'                => ['required', 'string', 'max:255'],
+            'description_html'    => ['string', 'max:2000'],
+            'tags'                => ['array'],
+            'default_template_id' => ['nullable', 'integer'],
         ]);
 
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
index adafcdc7bd919a26ca5a57c735f0f3440e1fcdbe..eaad3c0b79de3303d1544b45d018cbef42fd75b1 100644 (file)
@@ -6,6 +6,7 @@ use BookStack\Activity\Models\View;
 use BookStack\Activity\Tools\CommentTree;
 use BookStack\Activity\Tools\UserEntityWatchOptions;
 use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -259,7 +260,9 @@ class PageController extends Controller
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
-        $usedAsTemplate = Book::query()->where('default_template_id', '=', $page->id)->count() > 0;
+        $usedAsTemplate =
+            Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
+            Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
 
         return view('pages.delete', [
             'book'    => $page->book,
@@ -279,7 +282,9 @@ class PageController extends Controller
         $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
-        $usedAsTemplate = Book::query()->where('default_template_id', '=', $page->id)->count() > 0;
+        $usedAsTemplate =
+            Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
+            Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
 
         return view('pages.delete', [
             'book'    => $page->book,
index f30d77b5c5c33d86d38554e431c4dbe64028d859..d3a7101116ba5e36341773b2190e08a899deba86 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Entities\Models;
 
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Support\Collection;
@@ -11,6 +12,8 @@ use Illuminate\Support\Collection;
  *
  * @property Collection<Page> $pages
  * @property string           $description
+ * @property ?int             $default_template_id
+ * @property ?Page            $defaultTemplate
  */
 class Chapter extends BookChild
 {
@@ -48,6 +51,14 @@ class Chapter extends BookChild
         return url('/' . implode('/', $parts));
     }
 
+    /**
+     * Get the Page that is used as default template for newly created pages within this Chapter.
+     */
+    public function defaultTemplate(): BelongsTo
+    {
+        return $this->belongsTo(Page::class, 'default_template_id');
+    }
+
     /**
      * Get the visible pages in this chapter.
      */
index 27bf00161867b1c7d530005a49a6b3d5281045b5..17208ae032a196b6780da187b675c7e24967c854 100644 (file)
@@ -3,9 +3,12 @@
 namespace BookStack\Entities\Repos;
 
 use BookStack\Activity\TagRepo;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\HasCoverImage;
 use BookStack\Entities\Models\HasHtmlDescription;
+use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\References\ReferenceStore;
 use BookStack\References\ReferenceUpdater;
@@ -104,6 +107,33 @@ class BaseRepo
         }
     }
 
+    /**
+     * Update the default page template used for this item.
+     * Checks that, if changing, the provided value is a valid template and the user
+     * has visibility of the provided page template id.
+     */
+    public function updateDefaultTemplate(Book|Chapter $entity, int $templateId): void
+    {
+        $changing = $templateId !== intval($entity->default_template_id);
+        if (!$changing) {
+            return;
+        }
+
+        if ($templateId === 0) {
+            $entity->default_template_id = null;
+            $entity->save();
+            return;
+        }
+
+        $templateExists = Page::query()->visible()
+            ->where('template', '=', true)
+            ->where('id', '=', $templateId)
+            ->exists();
+
+        $entity->default_template_id = $templateExists ? $templateId : null;
+        $entity->save();
+    }
+
     protected function updateDescription(Entity $entity, array $input): void
     {
         if (!in_array(HasHtmlDescription::class, class_uses($entity))) {
index 03e1118b12280c9c67ef93de3a96d8d74fa98632..bf765b22d158f43524c4b51f077d5f5a8f109430 100644 (file)
@@ -86,7 +86,7 @@ class BookRepo
         $book = new Book();
         $this->baseRepo->create($book, $input);
         $this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
-        $this->updateBookDefaultTemplate($book, intval($input['default_template_id'] ?? null));
+        $this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
         Activity::add(ActivityType::BOOK_CREATE, $book);
 
         return $book;
@@ -100,7 +100,7 @@ class BookRepo
         $this->baseRepo->update($book, $input);
 
         if (array_key_exists('default_template_id', $input)) {
-            $this->updateBookDefaultTemplate($book, intval($input['default_template_id']));
+            $this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id']));
         }
 
         if (array_key_exists('image', $input)) {
@@ -112,33 +112,6 @@ class BookRepo
         return $book;
     }
 
-    /**
-     * Update the default page template used for this book.
-     * Checks that, if changing, the provided value is a valid template and the user
-     * has visibility of the provided page template id.
-     */
-    protected function updateBookDefaultTemplate(Book $book, int $templateId): void
-    {
-        $changing = $templateId !== intval($book->default_template_id);
-        if (!$changing) {
-            return;
-        }
-
-        if ($templateId === 0) {
-            $book->default_template_id = null;
-            $book->save();
-            return;
-        }
-
-        $templateExists = Page::query()->visible()
-            ->where('template', '=', true)
-            ->where('id', '=', $templateId)
-            ->exists();
-
-        $book->default_template_id = $templateExists ? $templateId : null;
-        $book->save();
-    }
-
     /**
      * Update the given book's cover image, or clear it.
      *
index 977193d85bbd0229a2d1a1e745dbf5fd01dd4a83..50b554d68dc585b4d1f3b42b16c8021936721c8e 100644 (file)
@@ -4,8 +4,8 @@ namespace BookStack\Entities\Repos;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\MoveOperationException;
@@ -46,6 +46,7 @@ class ChapterRepo
         $chapter->book_id = $parentBook->id;
         $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
         $this->baseRepo->create($chapter, $input);
+        $this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
         Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
 
         return $chapter;
@@ -57,6 +58,11 @@ class ChapterRepo
     public function update(Chapter $chapter, array $input): Chapter
     {
         $this->baseRepo->update($chapter, $input);
+
+        if (array_key_exists('default_template_id', $input)) {
+            $this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id']));
+        }
+
         Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);
 
         return $chapter;
index 7b14ea7d278c93b45bb0599251e8bbab6abfa337..85237a75219f62784160f388fa91601bcaa338db 100644 (file)
@@ -136,7 +136,7 @@ class PageRepo
             $page->book_id = $parent->id;
         }
 
-        $defaultTemplate = $page->book->defaultTemplate;
+        $defaultTemplate = $page->chapter->defaultTemplate ?? $page->book->defaultTemplate;
         if ($defaultTemplate && userCan('view', $defaultTemplate)) {
             $page->forceFill([
                 'html'  => $defaultTemplate->html,
index b25103985418f637604fb751a8d8b6868aacc409..8e9f010df0ff6fb5d5ab37e937e0481811d3f3df 100644 (file)
@@ -206,6 +206,10 @@ class TrashCan
         Book::query()->where('default_template_id', '=', $page->id)
             ->update(['default_template_id' => null]);
 
+        // Remove chapter template usages
+        Chapter::query()->where('default_template_id', '=', $page->id)
+            ->update(['default_template_id' => null]);
+
         $page->forceDelete();
 
         return 1;
diff --git a/database/migrations/2024_01_01_104542_add_default_template_to_chapters.php b/database/migrations/2024_01_01_104542_add_default_template_to_chapters.php
new file mode 100644 (file)
index 0000000..b3a103a
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddDefaultTemplateToChapters extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('chapters', function (Blueprint $table) {
+            $table->integer('default_template_id')->nullable()->default(null);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('chapters', function (Blueprint $table) {
+            $table->dropColumn('default_template_id');
+        });
+    }
+}
index e9d9033874e91ae33feac193fad1ed8025d128bb..02aee9eeabf3687b6f16ce8c1506c8184b32cbc5 100644 (file)
@@ -3,6 +3,7 @@
   "name": "My fantastic new chapter",
   "description_html": "<p>This is a <strong>great new chapter</strong> that I've created via the API</p>",
   "priority": 15,
+  "default_template_id": 25,
   "tags": [
     {"name": "Category", "value": "Top Content"},
     {"name": "Rating", "value": "Highest"}
index be675772bc6d26ac5a1dfc055cd10d98d414b358..cf9c89eacdcf2da6c698192c0a54fcc06b8a55a6 100644 (file)
@@ -3,6 +3,7 @@
   "name": "My fantastic updated chapter",
   "description_html": "<p>This is an <strong>updated chapter</strong> that I've altered via the API</p>",
   "priority": 16,
+  "default_template_id": 2428,
   "tags": [
     {"name": "Category", "value": "Kinda Good Content"},
     {"name": "Rating", "value": "Medium"}
index 2d40444058751c6affd3dd819b0a81a8359e1a32..3711d1cc9d6cea5248242e152dda3c9980ffb835 100644 (file)
@@ -11,6 +11,7 @@
   "updated_by": 1,
   "owned_by": 1,
   "description_html": "<p>This is a <strong>great new chapter<\/strong> that I've created via the API<\/p>",
+  "default_template_id": 25,
   "book_slug": "example-book",
   "tags": [
     {
@@ -24,4 +25,4 @@
       "order": 0
     }
   ]
-}
\ No newline at end of file
+}
index 192ffce7cd25cbe16f1a0488d7554431551065fe..01a2f4b9f9811825b0f2e808cc8ec97cb154ace2 100644 (file)
@@ -5,6 +5,7 @@
   "name": "Content Creation",
   "description": "How to create documentation on whatever subject you need to write about.",
   "description_html": "<p>How to create <strong>documentation</strong> on whatever subject you need to write about.</p>",
+  "default_template_id": 25,
   "priority": 3,
   "created_at": "2019-05-05T21:49:56.000000Z",
   "updated_at": "2019-09-28T11:24:23.000000Z",
index 3dad6aa0c12245c4ae4cdd870a96eb637a384ad5..96784dcedf7da605d962c2ddd39c6adeffaeb5f1 100644 (file)
@@ -11,6 +11,7 @@
   "updated_by": 1,
   "owned_by": 1,
   "description_html": "<p>This is an <strong>updated chapter<\/strong> that I've altered via the API<\/p>",
+  "default_template_id": 2428,
   "book_slug": "example-book",
   "tags": [
     {
@@ -24,4 +25,4 @@
       "order": 0
     }
   ]
-}
\ No newline at end of file
+}
index f1f915544d18db285e63110cb743a2004089e86c..9e620b24ed1dfcde5d5283b659d78b3fc63e5c10 100644 (file)
@@ -39,6 +39,9 @@ return [
     'export_pdf' => 'PDF File',
     'export_text' => 'Plain Text File',
     'export_md' => 'Markdown File',
+    'default_template' => 'Default Page Template',
+    'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',
+    'default_template_select' => 'Select a template page',
 
     // Permissions and restrictions
     'permissions' => 'Permissions',
@@ -132,9 +135,6 @@ return [
     'books_edit_named' => 'Edit Book :bookName',
     'books_form_book_name' => 'Book Name',
     'books_save' => 'Save Book',
-    'books_default_template' => 'Default Page Template',
-    'books_default_template_explain' => 'Assign a page template that will be used as the default content for all new pages in this book. Keep in mind this will only be used if the page creator has view access to those chosen template page.',
-    'books_default_template_select' => 'Select a template page',
     'books_permissions' => 'Book Permissions',
     'books_permissions_updated' => 'Book Permissions Updated',
     'books_empty_contents' => 'No pages or chapters have been created for this book.',
@@ -207,7 +207,7 @@ return [
     'pages_delete_draft' => 'Delete Draft Page',
     'pages_delete_success' => 'Page deleted',
     'pages_delete_draft_success' => 'Draft page deleted',
-    'pages_delete_warning_template' => 'This page is in active use as a book default page template. These books will no longer have a default page template assigned after this page is deleted.',
+    'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',
     'pages_delete_confirm' => 'Are you sure you want to delete this page?',
     'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
     'pages_editing_named' => 'Editing Page :pageName',
index fa8f16e52f4a5d5d17273025900d9c08bd6a5682..ee261e72d4a55b5449f84891cafcec85502711c2 100644 (file)
 
 <div class="form-group collapsible" component="collapsible" id="template-control">
     <button refs="collapsible@trigger" type="button" class="collapse-title text-link" aria-expanded="false">
-        <label for="template-manager">{{ trans('entities.books_default_template') }}</label>
+        <label for="template-manager">{{ trans('entities.default_template') }}</label>
     </button>
     <div refs="collapsible@content" class="collapse-content">
-        <div class="flex-container-row gap-l justify-space-between pb-xs wrap">
-            <p class="text-muted small my-none min-width-xs flex">
-                {{ trans('entities.books_default_template_explain') }}
-            </p>
-
-            <div class="min-width-m">
-                @include('form.page-picker', [
-                    'name' => 'default_template_id',
-                    'placeholder' => trans('entities.books_default_template_select'),
-                    'value' => $book->default_template_id ?? null,
-                    'selectorEndpoint' => '/search/entity-selector-templates',
-                ])
-            </div>
-        </div>
-
+        @include('entities.template-selector', ['entity' => $book ?? null])
     </div>
 </div>
 
index c6052c93af415b82d523f3163d3ceeaf0a45e108..602693916ea04ef2b00c9779b7a93ea0c0a28e61 100644 (file)
     </div>
 </div>
 
+<div class="form-group collapsible" component="collapsible" id="template-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-link" aria-expanded="false">
+        <label for="template-manager">{{ trans('entities.default_template') }}</label>
+    </button>
+    <div refs="collapsible@content" class="collapse-content">
+        @include('entities.template-selector', ['entity' => $chapter ?? null])
+    </div>
+</div>
+
 <div class="form-group text-right">
     <a href="{{ isset($chapter) ? $chapter->getUrl() : $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
     <button type="submit" class="button">{{ trans('entities.chapters_save') }}</button>
diff --git a/resources/views/entities/template-selector.blade.php b/resources/views/entities/template-selector.blade.php
new file mode 100644 (file)
index 0000000..80b2f49
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="flex-container-row gap-l justify-space-between pb-xs wrap">\r
+    <p class="text-muted small my-none min-width-xs flex">\r
+        {{ trans('entities.default_template_explain') }}\r
+    </p>\r
+\r
+    <div class="min-width-m">\r
+        @include('form.page-picker', [\r
+            'name' => 'default_template_id',\r
+            'placeholder' => trans('entities.default_template_select'),\r
+            'value' => $entity->default_template_id ?? null,\r
+            'selectorEndpoint' => '/search/entity-selector-templates',\r
+        ])\r
+    </div>\r
+</div>
\ No newline at end of file
index 002046c3a3b2388ab56fc89f5bfeff03e64fed5d..e2d6cfc81f0a8ce5e12172c7f4c793787feff2e4 100644 (file)
@@ -36,6 +36,7 @@ class ChaptersApiTest extends TestCase
     {
         $this->actingAsApiEditor();
         $book = $this->entities->book();
+        $templatePage = $this->entities->templatePage();
         $details = [
             'name'        => 'My API chapter',
             'description' => 'A chapter created via the API',
@@ -47,6 +48,7 @@ class ChaptersApiTest extends TestCase
                 ],
             ],
             'priority' => 15,
+            'default_template_id' => $templatePage->id,
         ];
 
         $resp = $this->postJson($this->baseEndpoint, $details);
@@ -149,6 +151,7 @@ class ChaptersApiTest extends TestCase
                     'name' => $page->name,
                 ],
             ],
+            'default_template_id' => null,
         ]);
         $resp->assertJsonMissingPath('book');
         $resp->assertJsonCount($chapter->pages()->count(), 'pages');
@@ -158,6 +161,7 @@ class ChaptersApiTest extends TestCase
     {
         $this->actingAsApiEditor();
         $chapter = $this->entities->chapter();
+        $templatePage = $this->entities->templatePage();
         $details = [
             'name'        => 'My updated API chapter',
             'description' => 'A chapter updated via the API',
@@ -168,6 +172,7 @@ class ChaptersApiTest extends TestCase
                 ],
             ],
             'priority'    => 15,
+            'default_template_id' => $templatePage->id,
         ];
 
         $resp = $this->putJson($this->baseEndpoint . "/{$chapter->id}", $details);
diff --git a/tests/Entity/BookDefaultTemplateTest.php b/tests/Entity/BookDefaultTemplateTest.php
deleted file mode 100644 (file)
index d4cd5b2..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-<?php
-
-namespace Tests\Entity;
-
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Page;
-use Tests\TestCase;
-
-class BookDefaultTemplateTest extends TestCase
-{
-    public function test_creating_book_with_default_template()
-    {
-        $templatePage = $this->entities->templatePage();
-        $details = [
-            'name' => 'My book with default template',
-            'default_template_id' => $templatePage->id,
-        ];
-
-        $this->asEditor()->post('/books', $details);
-        $this->assertDatabaseHas('books', $details);
-    }
-
-    public function test_updating_book_with_default_template()
-    {
-        $book = $this->entities->book();
-        $templatePage = $this->entities->templatePage();
-
-        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => strval($templatePage->id)]);
-        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => $templatePage->id]);
-
-        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => '']);
-        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]);
-    }
-
-    public function test_default_template_cannot_be_set_if_not_a_template()
-    {
-        $book = $this->entities->book();
-        $page = $this->entities->page();
-        $this->assertFalse($page->template);
-
-        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $page->id]);
-        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]);
-    }
-
-    public function test_default_template_cannot_be_set_if_not_have_access()
-    {
-        $book = $this->entities->book();
-        $templatePage = $this->entities->templatePage();
-        $this->permissions->disableEntityInheritedPermissions($templatePage);
-
-        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $templatePage->id]);
-        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]);
-    }
-
-    public function test_inaccessible_default_template_can_be_set_if_unchanged()
-    {
-        $templatePage = $this->entities->templatePage();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-        $this->permissions->disableEntityInheritedPermissions($templatePage);
-
-        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $templatePage->id]);
-        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => $templatePage->id]);
-    }
-
-    public function test_default_page_template_option_shows_on_book_form()
-    {
-        $templatePage = $this->entities->templatePage();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-
-        $resp = $this->asEditor()->get($book->getUrl('/edit'));
-        $this->withHtml($resp)->assertElementExists('input[name="default_template_id"][value="' . $templatePage->id . '"]');
-    }
-
-    public function test_default_page_template_option_only_shows_template_name_if_visible()
-    {
-        $templatePage = $this->entities->templatePage();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-
-        $resp = $this->asEditor()->get($book->getUrl('/edit'));
-        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}");
-
-        $this->permissions->disableEntityInheritedPermissions($templatePage);
-
-        $resp = $this->asEditor()->get($book->getUrl('/edit'));
-        $this->withHtml($resp)->assertElementNotContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}");
-        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}");
-    }
-
-    public function test_creating_book_page_uses_default_template()
-    {
-        $templatePage = $this->entities->templatePage();
-        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-
-        $this->asEditor()->get($book->getUrl('/create-page'));
-        $latestPage = $book->pages()
-            ->where('draft', '=', true)
-            ->where('template', '=', false)
-            ->latest()->first();
-
-        $this->assertEquals('<p>My template page</p>', $latestPage->html);
-        $this->assertEquals('# My template page', $latestPage->markdown);
-    }
-
-    public function test_creating_chapter_page_uses_default_template()
-    {
-        $templatePage = $this->entities->templatePage();
-        $templatePage->forceFill(['html' => '<p>My template page in chapter</p>', 'markdown' => '# My template page in chapter'])->save();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-        $chapter = $book->chapters()->first();
-
-        $this->asEditor()->get($chapter->getUrl('/create-page'));
-        $latestPage = $chapter->pages()
-            ->where('draft', '=', true)
-            ->where('template', '=', false)
-            ->latest()->first();
-
-        $this->assertEquals('<p>My template page in chapter</p>', $latestPage->html);
-        $this->assertEquals('# My template page in chapter', $latestPage->markdown);
-    }
-
-    public function test_creating_book_page_as_guest_uses_default_template()
-    {
-        $templatePage = $this->entities->templatePage();
-        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-        $guest = $this->users->guest();
-
-        $this->permissions->makeAppPublic();
-        $this->permissions->grantUserRolePermissions($guest, ['page-create-all', 'page-update-all']);
-
-        $resp = $this->post($book->getUrl('/create-guest-page'), [
-            'name' => 'My guest page with template'
-        ]);
-        $latestPage = $book->pages()
-            ->where('draft', '=', false)
-            ->where('template', '=', false)
-            ->where('created_by', '=', $guest->id)
-            ->latest()->first();
-
-        $this->assertEquals('<p>My template page</p>', $latestPage->html);
-        $this->assertEquals('# My template page', $latestPage->markdown);
-    }
-
-    public function test_creating_book_page_does_not_use_template_if_not_visible()
-    {
-        $templatePage = $this->entities->templatePage();
-        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-        $this->permissions->disableEntityInheritedPermissions($templatePage);
-
-        $this->asEditor()->get($book->getUrl('/create-page'));
-        $latestPage = $book->pages()
-            ->where('draft', '=', true)
-            ->where('template', '=', false)
-            ->latest()->first();
-
-        $this->assertEquals('', $latestPage->html);
-        $this->assertEquals('', $latestPage->markdown);
-    }
-
-    public function test_template_page_delete_removes_book_template_usage()
-    {
-        $templatePage = $this->entities->templatePage();
-        $book = $this->bookUsingDefaultTemplate($templatePage);
-
-        $book->refresh();
-        $this->assertEquals($templatePage->id, $book->default_template_id);
-
-        $this->asEditor()->delete($templatePage->getUrl());
-        $this->asAdmin()->post('/settings/recycle-bin/empty');
-
-        $book->refresh();
-        $this->assertEquals(null, $book->default_template_id);
-    }
-
-    protected function bookUsingDefaultTemplate(Page $page): Book
-    {
-        $book = $this->entities->book();
-        $book->default_template_id = $page->id;
-        $book->save();
-
-        return $book;
-    }
-}
diff --git a/tests/Entity/DefaultTemplateTest.php b/tests/Entity/DefaultTemplateTest.php
new file mode 100644 (file)
index 0000000..5369a54
--- /dev/null
@@ -0,0 +1,341 @@
+<?php
+
+namespace Tests\Entity;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class DefaultTemplateTest extends TestCase
+{
+    public function test_creating_book_with_default_template()
+    {
+        $templatePage = $this->entities->templatePage();
+        $details = [
+            'name' => 'My book with default template',
+            'default_template_id' => $templatePage->id,
+        ];
+
+        $this->asEditor()->post('/books', $details);
+        $this->assertDatabaseHas('books', $details);
+    }
+
+    public function test_creating_chapter_with_default_template()
+    {
+        $templatePage = $this->entities->templatePage();
+        $book = $this->entities->book();
+        $details = [
+            'name' => 'My chapter with default template',
+            'default_template_id' => $templatePage->id,
+        ];
+
+        $this->asEditor()->post($book->getUrl('/create-chapter'), $details);
+        $this->assertDatabaseHas('chapters', $details);
+    }
+
+    public function test_updating_book_with_default_template()
+    {
+        $book = $this->entities->book();
+        $templatePage = $this->entities->templatePage();
+
+        $this->asEditor()->put($book->getUrl(), ['name' => $book->name, 'default_template_id' => strval($templatePage->id)]);
+        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => $templatePage->id]);
+
+        $this->asEditor()->put($book->getUrl(), ['name' => $book->name, 'default_template_id' => '']);
+        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]);
+    }
+
+    public function test_updating_chapter_with_default_template()
+    {
+        $chapter = $this->entities->chapter();
+        $templatePage = $this->entities->templatePage();
+
+        $this->asEditor()->put($chapter->getUrl(), ['name' => $chapter->name, 'default_template_id' => strval($templatePage->id)]);
+        $this->assertDatabaseHas('chapters', ['id' => $chapter->id, 'default_template_id' => $templatePage->id]);
+
+        $this->asEditor()->put($chapter->getUrl(), ['name' => $chapter->name, 'default_template_id' => '']);
+        $this->assertDatabaseHas('chapters', ['id' => $chapter->id, 'default_template_id' => null]);
+    }
+
+    public function test_default_book_template_cannot_be_set_if_not_a_template()
+    {
+        $book = $this->entities->book();
+        $page = $this->entities->page();
+        $this->assertFalse($page->template);
+
+        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $page->id]);
+        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]);
+    }
+
+    public function test_default_chapter_template_cannot_be_set_if_not_a_template()
+    {
+        $chapter = $this->entities->chapter();
+        $page = $this->entities->page();
+        $this->assertFalse($page->template);
+
+        $this->asEditor()->put("/chapters/{$chapter->slug}", ['name' => $chapter->name, 'default_template_id' => $page->id]);
+        $this->assertDatabaseHas('chapters', ['id' => $chapter->id, 'default_template_id' => null]);
+    }
+
+
+    public function test_default_book_template_cannot_be_set_if_not_have_access()
+    {
+        $book = $this->entities->book();
+        $templatePage = $this->entities->templatePage();
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $templatePage->id]);
+        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => null]);
+    }
+
+    public function test_default_chapter_template_cannot_be_set_if_not_have_access()
+    {
+        $chapter = $this->entities->chapter();
+        $templatePage = $this->entities->templatePage();
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $this->asEditor()->put("/chapters/{$chapter->slug}", ['name' => $chapter->name, 'default_template_id' => $templatePage->id]);
+        $this->assertDatabaseHas('chapters', ['id' => $chapter->id, 'default_template_id' => null]);
+    }
+
+    public function test_inaccessible_book_default_template_can_be_set_if_unchanged()
+    {
+        $templatePage = $this->entities->templatePage();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $this->asEditor()->put("/books/{$book->slug}", ['name' => $book->name, 'default_template_id' => $templatePage->id]);
+        $this->assertDatabaseHas('books', ['id' => $book->id, 'default_template_id' => $templatePage->id]);
+    }
+
+    public function test_inaccessible_chapter_default_template_can_be_set_if_unchanged()
+    {
+        $templatePage = $this->entities->templatePage();
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $this->asEditor()->put("/chapters/{$chapter->slug}", ['name' => $chapter->name, 'default_template_id' => $templatePage->id]);
+        $this->assertDatabaseHas('chapters', ['id' => $chapter->id, 'default_template_id' => $templatePage->id]);
+    }
+
+    public function test_default_page_template_option_shows_on_book_form()
+    {
+        $templatePage = $this->entities->templatePage();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+
+        $resp = $this->asEditor()->get($book->getUrl('/edit'));
+        $this->withHtml($resp)->assertElementExists('input[name="default_template_id"][value="' . $templatePage->id . '"]');
+    }
+
+    public function test_default_page_template_option_shows_on_chapter_form()
+    {
+        $templatePage = $this->entities->templatePage();
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+
+        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));
+        $this->withHtml($resp)->assertElementExists('input[name="default_template_id"][value="' . $templatePage->id . '"]');
+    }
+
+    public function test_book_default_page_template_option_only_shows_template_name_if_visible()
+    {
+        $templatePage = $this->entities->templatePage();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+
+        $resp = $this->asEditor()->get($book->getUrl('/edit'));
+        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}");
+
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $resp = $this->asEditor()->get($book->getUrl('/edit'));
+        $this->withHtml($resp)->assertElementNotContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}");
+        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}");
+    }
+
+    public function test_chapter_default_page_template_option_only_shows_template_name_if_visible()
+    {
+        $templatePage = $this->entities->templatePage();
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+
+        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));
+        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}");
+
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $resp = $this->asEditor()->get($chapter->getUrl('/edit'));
+        $this->withHtml($resp)->assertElementNotContains('#template-control a.text-page', "#{$templatePage->id}, {$templatePage->name}");
+        $this->withHtml($resp)->assertElementContains('#template-control a.text-page', "#{$templatePage->id}");
+    }
+
+    public function test_creating_book_page_uses_book_default_template()
+    {
+        $templatePage = $this->entities->templatePage();
+        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+
+        $this->asEditor()->get($book->getUrl('/create-page'));
+        $latestPage = $book->pages()
+            ->where('draft', '=', true)
+            ->where('template', '=', false)
+            ->latest()->first();
+
+        $this->assertEquals('<p>My template page</p>', $latestPage->html);
+        $this->assertEquals('# My template page', $latestPage->markdown);
+    }
+
+    public function test_creating_chapter_page_uses_chapter_default_template()
+    {
+        $templatePage = $this->entities->templatePage();
+        $templatePage->forceFill(['html' => '<p>My chapter template page</p>', 'markdown' => '# My chapter template page'])->save();
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+
+        $this->asEditor()->get($chapter->getUrl('/create-page'));
+        $latestPage = $chapter->pages()
+            ->where('draft', '=', true)
+            ->where('template', '=', false)
+            ->latest()->first();
+
+        $this->assertEquals('<p>My chapter template page</p>', $latestPage->html);
+        $this->assertEquals('# My chapter template page', $latestPage->markdown);
+    }
+
+    public function test_creating_chapter_page_uses_book_default_template_if_no_chapter_template_set()
+    {
+        $templatePage = $this->entities->templatePage();
+        $templatePage->forceFill(['html' => '<p>My template page in chapter</p>', 'markdown' => '# My template page in chapter'])->save();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+        $chapter = $book->chapters()->first();
+
+        $this->asEditor()->get($chapter->getUrl('/create-page'));
+        $latestPage = $chapter->pages()
+            ->where('draft', '=', true)
+            ->where('template', '=', false)
+            ->latest()->first();
+
+        $this->assertEquals('<p>My template page in chapter</p>', $latestPage->html);
+        $this->assertEquals('# My template page in chapter', $latestPage->markdown);
+    }
+
+    public function test_creating_chapter_page_uses_chapter_template_instead_of_book_template()
+    {
+        $bookTemplatePage = $this->entities->templatePage();
+        $bookTemplatePage->forceFill(['html' => '<p>My book template</p>', 'markdown' => '# My book template'])->save();
+        $book = $this->bookUsingDefaultTemplate($bookTemplatePage);
+
+        $chapterTemplatePage = $this->entities->templatePage();
+        $chapterTemplatePage->forceFill(['html' => '<p>My chapter template</p>', 'markdown' => '# My chapter template'])->save();
+        $chapter = $book->chapters()->first();
+        $chapter->default_template_id = $chapterTemplatePage->id;
+        $chapter->save();
+
+        $this->asEditor()->get($chapter->getUrl('/create-page'));
+        $latestPage = $chapter->pages()
+            ->where('draft', '=', true)
+            ->where('template', '=', false)
+            ->latest()->first();
+
+        $this->assertEquals('<p>My chapter template</p>', $latestPage->html);
+        $this->assertEquals('# My chapter template', $latestPage->markdown);
+    }
+
+    public function test_creating_page_as_guest_uses_default_template()
+    {
+        $templatePage = $this->entities->templatePage();
+        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+        $guest = $this->users->guest();
+
+        $this->permissions->makeAppPublic();
+        $this->permissions->grantUserRolePermissions($guest, ['page-create-all', 'page-update-all']);
+
+        $this->post($book->getUrl('/create-guest-page'), [
+            'name' => 'My guest page with template'
+        ]);
+        $latestBookPage = $book->pages()
+            ->where('draft', '=', false)
+            ->where('template', '=', false)
+            ->where('created_by', '=', $guest->id)
+            ->latest()->first();
+
+        $this->assertEquals('<p>My template page</p>', $latestBookPage->html);
+        $this->assertEquals('# My template page', $latestBookPage->markdown);
+
+        $this->post($chapter->getUrl('/create-guest-page'), [
+            'name' => 'My guest page with template'
+        ]);
+        $latestChapterPage = $chapter->pages()
+            ->where('draft', '=', false)
+            ->where('template', '=', false)
+            ->where('created_by', '=', $guest->id)
+            ->latest()->first();
+
+        $this->assertEquals('<p>My template page</p>', $latestChapterPage->html);
+        $this->assertEquals('# My template page', $latestChapterPage->markdown);
+    }
+
+    public function test_templates_not_used_if_not_visible()
+    {
+        $templatePage = $this->entities->templatePage();
+        $templatePage->forceFill(['html' => '<p>My template page</p>', 'markdown' => '# My template page'])->save();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+
+        $this->permissions->disableEntityInheritedPermissions($templatePage);
+
+        $this->asEditor()->get($book->getUrl('/create-page'));
+        $latestBookPage = $book->pages()
+            ->where('draft', '=', true)
+            ->where('template', '=', false)
+            ->latest()->first();
+
+        $this->assertEquals('', $latestBookPage->html);
+        $this->assertEquals('', $latestBookPage->markdown);
+
+        $this->asEditor()->get($chapter->getUrl('/create-page'));
+        $latestChapterPage = $chapter->pages()
+            ->where('draft', '=', true)
+            ->where('template', '=', false)
+            ->latest()->first();
+
+        $this->assertEquals('', $latestChapterPage->html);
+        $this->assertEquals('', $latestChapterPage->markdown);
+    }
+
+    public function test_template_page_delete_removes_template_usage()
+    {
+        $templatePage = $this->entities->templatePage();
+        $book = $this->bookUsingDefaultTemplate($templatePage);
+        $chapter = $this->chapterUsingDefaultTemplate($templatePage);
+
+        $book->refresh();
+        $this->assertEquals($templatePage->id, $book->default_template_id);
+        $this->assertEquals($templatePage->id, $chapter->default_template_id);
+
+        $this->asEditor()->delete($templatePage->getUrl());
+        $this->asAdmin()->post('/settings/recycle-bin/empty');
+
+        $book->refresh();
+        $chapter->refresh();
+        $this->assertEquals(null, $book->default_template_id);
+        $this->assertEquals(null, $chapter->default_template_id);
+    }
+
+    protected function bookUsingDefaultTemplate(Page $page): Book
+    {
+        $book = $this->entities->book();
+        $book->default_template_id = $page->id;
+        $book->save();
+
+        return $book;
+    }
+
+    protected function chapterUsingDefaultTemplate(Page $page): Chapter
+    {
+        $chapter = $this->entities->chapter();
+        $chapter->default_template_id = $page->id;
+        $chapter->save();
+
+        return $chapter;
+    }
+}