Added chapters to the API
This commit is contained in:
		
							parent
							
								
									24bad5034a
								
							
						
					
					
						commit
						8a6cf0cdec
					
				| 
						 | 
				
			
			@ -9,6 +9,7 @@ use BookStack\Model;
 | 
			
		|||
class Tag extends Model
 | 
			
		||||
{
 | 
			
		||||
    protected $fillable = ['name', 'value', 'order'];
 | 
			
		||||
    protected $hidden = ['id', 'entity_id', 'entity_type'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the entity that this tag belongs to
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,14 +106,13 @@ class TagRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save an array of tags to an entity
 | 
			
		||||
     * @param \BookStack\Entities\Entity $entity
 | 
			
		||||
     * @param array $tags
 | 
			
		||||
     * @return array|\Illuminate\Database\Eloquent\Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function saveTagsToEntity(Entity $entity, $tags = [])
 | 
			
		||||
    public function saveTagsToEntity(Entity $entity, array $tags = [])
 | 
			
		||||
    {
 | 
			
		||||
        $entity->tags()->delete();
 | 
			
		||||
        $newTags = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($tags as $tag) {
 | 
			
		||||
            if (trim($tag['name']) === '') {
 | 
			
		||||
                continue;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 | 
			
		|||
     */
 | 
			
		||||
    protected $hidden = [
 | 
			
		||||
        'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
 | 
			
		||||
        'created_at', 'updated_at',
 | 
			
		||||
        'created_at', 'updated_at', 'image_id',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ class Book extends Entity implements HasCoverImage
 | 
			
		|||
    public $searchFactor = 2;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['name', 'description'];
 | 
			
		||||
    protected $hidden = ['restricted', 'pivot'];
 | 
			
		||||
    protected $hidden = ['restricted', 'pivot', 'image_id'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the url for this book.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ class Bookshelf extends Entity implements HasCoverImage
 | 
			
		|||
 | 
			
		||||
    protected $fillable = ['name', 'description', 'image_id'];
 | 
			
		||||
 | 
			
		||||
    protected $hidden = ['restricted'];
 | 
			
		||||
    protected $hidden = ['restricted', 'image_id'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the books in this shelf.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ class Chapter extends BookChild
 | 
			
		|||
    public $searchFactor = 1.3;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['name', 'description', 'priority', 'book_id'];
 | 
			
		||||
    protected $hidden = ['restricted', 'pivot'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the pages that this chapter contains.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -288,7 +288,7 @@ class Entity extends Ownable
 | 
			
		|||
    public function rebuildPermissions()
 | 
			
		||||
    {
 | 
			
		||||
        /** @noinspection PhpUnhandledExceptionInspection */
 | 
			
		||||
        Permissions::buildJointPermissionsForEntity($this);
 | 
			
		||||
        Permissions::buildJointPermissionsForEntity(clone $this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -297,7 +297,7 @@ class Entity extends Ownable
 | 
			
		|||
    public function indexForSearch()
 | 
			
		||||
    {
 | 
			
		||||
        $searchService = app()->make(SearchService::class);
 | 
			
		||||
        $searchService->indexEntity($this);
 | 
			
		||||
        $searchService->indexEntity(clone $this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,8 @@ class Page extends BookChild
 | 
			
		|||
 | 
			
		||||
    public $textField = 'text';
 | 
			
		||||
 | 
			
		||||
    protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the entities that are visible to the current user.
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -211,7 +211,7 @@ class PageRepo
 | 
			
		|||
     */
 | 
			
		||||
    protected function savePageRevision(Page $page, string $summary = null)
 | 
			
		||||
    {
 | 
			
		||||
        $revision = new PageRevision($page->toArray());
 | 
			
		||||
        $revision = new PageRevision($page->getAttributes());
 | 
			
		||||
 | 
			
		||||
        if (setting('app-editor') !== 'markdown') {
 | 
			
		||||
            $revision->markdown = '';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ use Illuminate\Contracts\Container\BindingResolutionException;
 | 
			
		|||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
 | 
			
		||||
class BooksApiController extends ApiController
 | 
			
		||||
class BookApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $bookRepo;
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +17,12 @@ class BooksApiController extends ApiController
 | 
			
		|||
        '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',
 | 
			
		||||
        ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -5,9 +5,8 @@ use BookStack\Entities\ExportService;
 | 
			
		|||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class BooksExportApiController extends ApiController
 | 
			
		||||
class BookExportApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $bookRepo;
 | 
			
		||||
    protected $exportService;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,104 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers\Api;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Repos\ChapterRepo;
 | 
			
		||||
use BookStack\Facades\Activity;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
 | 
			
		||||
class ChapterApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
    protected $chapterRepo;
 | 
			
		||||
 | 
			
		||||
    protected $rules = [
 | 
			
		||||
        'create' => [
 | 
			
		||||
            'book_id' => 'required|integer',
 | 
			
		||||
            'name' => 'required|string|max:255',
 | 
			
		||||
            'description' => 'string|max:1000',
 | 
			
		||||
            'tags' => 'array',
 | 
			
		||||
        ],
 | 
			
		||||
        'update' => [
 | 
			
		||||
            'book_id' => 'integer',
 | 
			
		||||
            'name' => 'string|min:1|max:255',
 | 
			
		||||
            'description' => 'string|max:1000',
 | 
			
		||||
            'tags' => 'array',
 | 
			
		||||
        ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ChapterController constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(ChapterRepo $chapterRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->chapterRepo = $chapterRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a listing of chapters visible to the user.
 | 
			
		||||
     */
 | 
			
		||||
    public function list()
 | 
			
		||||
    {
 | 
			
		||||
        $chapters = Chapter::visible();
 | 
			
		||||
        return $this->apiListingResponse($chapters, [
 | 
			
		||||
            'id', 'book_id', 'name', 'slug', 'description', 'priority',
 | 
			
		||||
            'created_at', 'updated_at', 'created_by', 'updated_by',
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new chapter in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function create(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, $this->rules['create']);
 | 
			
		||||
 | 
			
		||||
        $bookId = $request->get('book_id');
 | 
			
		||||
        $book = Book::visible()->findOrFail($bookId);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-create', $book);
 | 
			
		||||
 | 
			
		||||
        $chapter = $this->chapterRepo->create($request->all(), $book);
 | 
			
		||||
        Activity::add($chapter, 'chapter_create', $book->id);
 | 
			
		||||
 | 
			
		||||
        return response()->json($chapter->load(['tags']));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View the details of a single chapter.
 | 
			
		||||
     */
 | 
			
		||||
    public function read(string $id)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'pages' => function (HasMany $query) {
 | 
			
		||||
            $query->visible()->get(['id', 'name', 'slug']);
 | 
			
		||||
        }])->findOrFail($id);
 | 
			
		||||
        return response()->json($chapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the details of a single chapter.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, string $id)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->findOrFail($id);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
 | 
			
		||||
        $updatedChapter = $this->chapterRepo->update($chapter, $request->all());
 | 
			
		||||
        Activity::add($chapter, 'chapter_update', $chapter->book->id);
 | 
			
		||||
 | 
			
		||||
        return response()->json($updatedChapter->load(['tags']));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a chapter from the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function delete(string $id)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->findOrFail($id);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-delete', $chapter);
 | 
			
		||||
 | 
			
		||||
        $this->chapterRepo->destroy($chapter);
 | 
			
		||||
        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
 | 
			
		||||
 | 
			
		||||
        return response('', 204);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers\Api;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\ExportService;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class ChapterExportApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
    protected $chapterRepo;
 | 
			
		||||
    protected $exportService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ChapterExportController constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(BookRepo $chapterRepo, ExportService $exportService)
 | 
			
		||||
    {
 | 
			
		||||
        $this->chapterRepo = $chapterRepo;
 | 
			
		||||
        $this->exportService = $exportService;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a chapter as a PDF file.
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function exportPdf(int $id)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->findOrFail($id);
 | 
			
		||||
        $pdfContent = $this->exportService->chapterToPdf($chapter);
 | 
			
		||||
        return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a chapter as a contained HTML file.
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function exportHtml(int $id)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->findOrFail($id);
 | 
			
		||||
        $htmlContent = $this->exportService->chapterToContainedHtml($chapter);
 | 
			
		||||
        return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a chapter as a plain text file.
 | 
			
		||||
     */
 | 
			
		||||
    public function exportPlainText(int $id)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->findOrFail($id);
 | 
			
		||||
        $textContent = $this->exportService->chapterToPlainText($chapter);
 | 
			
		||||
        return $this->downloadResponse($textContent, $chapter->slug . '.txt');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
{
 | 
			
		||||
  "book_id": 1,
 | 
			
		||||
  "name": "My fantastic new chapter",
 | 
			
		||||
  "description": "This is a great new chapter that I've created via the API",
 | 
			
		||||
  "tags": [
 | 
			
		||||
    {"name": "Category", "value": "Top Content"},
 | 
			
		||||
    {"name": "Rating", "value": "Highest"}
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
{
 | 
			
		||||
  "book_id": 1,
 | 
			
		||||
  "name": "My fantastic updated chapter",
 | 
			
		||||
  "description": "This is an updated chapter that I've altered via the API",
 | 
			
		||||
  "tags": [
 | 
			
		||||
    {"name": "Category", "value": "Kinda Good Content"},
 | 
			
		||||
    {"name": "Rating", "value": "Medium"}
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,15 +7,12 @@
 | 
			
		|||
  "updated_at": "2020-01-12 14:11:51",
 | 
			
		||||
  "created_by": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "Admin",
 | 
			
		||||
    "image_id": 48
 | 
			
		||||
    "name": "Admin"
 | 
			
		||||
  },
 | 
			
		||||
  "updated_by": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "Admin",
 | 
			
		||||
    "image_id": 48
 | 
			
		||||
    "name": "Admin"
 | 
			
		||||
  },
 | 
			
		||||
  "image_id": 452,
 | 
			
		||||
  "tags": [
 | 
			
		||||
    {
 | 
			
		||||
      "id": 13,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
{
 | 
			
		||||
  "book_id": 1,
 | 
			
		||||
  "priority": 6,
 | 
			
		||||
  "name": "My fantastic new chapter",
 | 
			
		||||
  "description": "This is a great new chapter that I've created via the API",
 | 
			
		||||
  "created_by": 1,
 | 
			
		||||
  "updated_by": 1,
 | 
			
		||||
  "slug": "my-fantastic-new-chapter",
 | 
			
		||||
  "updated_at": "2020-05-22 22:59:55",
 | 
			
		||||
  "created_at": "2020-05-22 22:59:55",
 | 
			
		||||
  "id": 74,
 | 
			
		||||
  "book": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "BookStack User Guide",
 | 
			
		||||
    "slug": "bookstack-user-guide",
 | 
			
		||||
    "description": "This is a general guide on using BookStack on a day-to-day basis.",
 | 
			
		||||
    "created_at": "2019-05-05 21:48:46",
 | 
			
		||||
    "updated_at": "2019-12-11 20:57:31",
 | 
			
		||||
    "created_by": 1,
 | 
			
		||||
    "updated_by": 1
 | 
			
		||||
  },
 | 
			
		||||
  "tags": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Category",
 | 
			
		||||
      "value": "Top Content",
 | 
			
		||||
      "order": 0,
 | 
			
		||||
      "created_at": "2020-05-22 22:59:55",
 | 
			
		||||
      "updated_at": "2020-05-22 22:59:55"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Rating",
 | 
			
		||||
      "value": "Highest",
 | 
			
		||||
      "order": 0,
 | 
			
		||||
      "created_at": "2020-05-22 22:59:55",
 | 
			
		||||
      "updated_at": "2020-05-22 22:59:55"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
{
 | 
			
		||||
  "data": [
 | 
			
		||||
    {
 | 
			
		||||
      "id": 1,
 | 
			
		||||
      "book_id": 1,
 | 
			
		||||
      "name": "Content Creation",
 | 
			
		||||
      "slug": "content-creation",
 | 
			
		||||
      "description": "How to create documentation on whatever subject you need to write about.",
 | 
			
		||||
      "priority": 3,
 | 
			
		||||
      "created_at": "2019-05-05 21:49:56",
 | 
			
		||||
      "updated_at": "2019-09-28 11:24:23",
 | 
			
		||||
      "created_by": 1,
 | 
			
		||||
      "updated_by": 1
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "id": 2,
 | 
			
		||||
      "book_id": 1,
 | 
			
		||||
      "name": "Managing Content",
 | 
			
		||||
      "slug": "managing-content",
 | 
			
		||||
      "description": "How to keep things organised and orderly in the system for easier navigation and better user experience.",
 | 
			
		||||
      "priority": 5,
 | 
			
		||||
      "created_at": "2019-05-05 21:58:07",
 | 
			
		||||
      "updated_at": "2019-10-17 15:05:34",
 | 
			
		||||
      "created_by": 3,
 | 
			
		||||
      "updated_by": 3
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "total": 40
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
{
 | 
			
		||||
  "id": 1,
 | 
			
		||||
  "book_id": 1,
 | 
			
		||||
  "slug": "content-creation",
 | 
			
		||||
  "name": "Content Creation",
 | 
			
		||||
  "description": "How to create documentation on whatever subject you need to write about.",
 | 
			
		||||
  "priority": 3,
 | 
			
		||||
  "created_at": "2019-05-05 21:49:56",
 | 
			
		||||
  "updated_at": "2019-09-28 11:24:23",
 | 
			
		||||
  "created_by": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "Admin"
 | 
			
		||||
  },
 | 
			
		||||
  "updated_by": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "Admin"
 | 
			
		||||
  },
 | 
			
		||||
  "tags": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Category",
 | 
			
		||||
      "value": "Guide",
 | 
			
		||||
      "order": 0,
 | 
			
		||||
      "created_at": "2020-05-22 22:51:51",
 | 
			
		||||
      "updated_at": "2020-05-22 22:51:51"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "pages": [
 | 
			
		||||
    {
 | 
			
		||||
      "id": 1,
 | 
			
		||||
      "book_id": 1,
 | 
			
		||||
      "chapter_id": 1,
 | 
			
		||||
      "name": "How to create page content",
 | 
			
		||||
      "slug": "how-to-create-page-content",
 | 
			
		||||
      "priority": 0,
 | 
			
		||||
      "created_at": "2019-05-05 21:49:58",
 | 
			
		||||
      "updated_at": "2019-08-26 14:32:59",
 | 
			
		||||
      "created_by": 1,
 | 
			
		||||
      "updated_by": 1,
 | 
			
		||||
      "draft": 0,
 | 
			
		||||
      "revision_count": 2,
 | 
			
		||||
      "template": 0
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "id": 7,
 | 
			
		||||
      "book_id": 1,
 | 
			
		||||
      "chapter_id": 1,
 | 
			
		||||
      "name": "Good book structure",
 | 
			
		||||
      "slug": "good-book-structure",
 | 
			
		||||
      "priority": 1,
 | 
			
		||||
      "created_at": "2019-05-05 22:01:55",
 | 
			
		||||
      "updated_at": "2019-06-06 12:03:04",
 | 
			
		||||
      "created_by": 3,
 | 
			
		||||
      "updated_by": 3,
 | 
			
		||||
      "draft": 0,
 | 
			
		||||
      "revision_count": 1,
 | 
			
		||||
      "template": 0
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
{
 | 
			
		||||
  "id": 75,
 | 
			
		||||
  "book_id": 1,
 | 
			
		||||
  "slug": "my-fantastic-updated-chapter",
 | 
			
		||||
  "name": "My fantastic updated chapter",
 | 
			
		||||
  "description": "This is an updated chapter that I've altered via the API",
 | 
			
		||||
  "priority": 7,
 | 
			
		||||
  "created_at": "2020-05-22 23:03:35",
 | 
			
		||||
  "updated_at": "2020-05-22 23:07:20",
 | 
			
		||||
  "created_by": 1,
 | 
			
		||||
  "updated_by": 1,
 | 
			
		||||
  "book": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "BookStack User Guide",
 | 
			
		||||
    "slug": "bookstack-user-guide",
 | 
			
		||||
    "description": "This is a general guide on using BookStack on a day-to-day basis.",
 | 
			
		||||
    "created_at": "2019-05-05 21:48:46",
 | 
			
		||||
    "updated_at": "2019-12-11 20:57:31",
 | 
			
		||||
    "created_by": 1,
 | 
			
		||||
    "updated_by": 1
 | 
			
		||||
  },
 | 
			
		||||
  "tags": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Category",
 | 
			
		||||
      "value": "Kinda Good Content",
 | 
			
		||||
      "order": 0,
 | 
			
		||||
      "created_at": "2020-05-22 23:07:20",
 | 
			
		||||
      "updated_at": "2020-05-22 23:07:20"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Rating",
 | 
			
		||||
      "value": "Medium",
 | 
			
		||||
      "order": 0,
 | 
			
		||||
      "created_at": "2020-05-22 23:07:20",
 | 
			
		||||
      "updated_at": "2020-05-22 23:07:20"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,15 +5,12 @@
 | 
			
		|||
  "description": "This is my shelf with some books",
 | 
			
		||||
  "created_by": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "Admin",
 | 
			
		||||
    "image_id": 48
 | 
			
		||||
    "name": "Admin"
 | 
			
		||||
  },
 | 
			
		||||
  "updated_by": {
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "name": "Admin",
 | 
			
		||||
    "image_id": 48
 | 
			
		||||
    "name": "Admin"
 | 
			
		||||
  },
 | 
			
		||||
  "image_id": 501,
 | 
			
		||||
  "created_at": "2020-04-10 13:24:09",
 | 
			
		||||
  "updated_at": "2020-04-10 13:31:04",
 | 
			
		||||
  "tags": [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,15 +9,25 @@
 | 
			
		|||
Route::get('docs', 'ApiDocsController@display');
 | 
			
		||||
Route::get('docs.json', 'ApiDocsController@json');
 | 
			
		||||
 | 
			
		||||
Route::get('books', 'BooksApiController@list');
 | 
			
		||||
Route::post('books', 'BooksApiController@create');
 | 
			
		||||
Route::get('books/{id}', 'BooksApiController@read');
 | 
			
		||||
Route::put('books/{id}', 'BooksApiController@update');
 | 
			
		||||
Route::delete('books/{id}', 'BooksApiController@delete');
 | 
			
		||||
Route::get('books', 'BookApiController@list');
 | 
			
		||||
Route::post('books', 'BookApiController@create');
 | 
			
		||||
Route::get('books/{id}', 'BookApiController@read');
 | 
			
		||||
Route::put('books/{id}', 'BookApiController@update');
 | 
			
		||||
Route::delete('books/{id}', 'BookApiController@delete');
 | 
			
		||||
 | 
			
		||||
Route::get('books/{id}/export/html', 'BooksExportApiController@exportHtml');
 | 
			
		||||
Route::get('books/{id}/export/pdf', 'BooksExportApiController@exportPdf');
 | 
			
		||||
Route::get('books/{id}/export/plaintext', 'BooksExportApiController@exportPlainText');
 | 
			
		||||
Route::get('books/{id}/export/html', 'BookExportApiController@exportHtml');
 | 
			
		||||
Route::get('books/{id}/export/pdf', 'BookExportApiController@exportPdf');
 | 
			
		||||
Route::get('books/{id}/export/plaintext', 'BookExportApiController@exportPlainText');
 | 
			
		||||
 | 
			
		||||
Route::get('chapters', 'ChapterApiController@list');
 | 
			
		||||
Route::post('chapters', 'ChapterApiController@create');
 | 
			
		||||
Route::get('chapters/{id}', 'ChapterApiController@read');
 | 
			
		||||
Route::put('chapters/{id}', 'ChapterApiController@update');
 | 
			
		||||
Route::delete('chapters/{id}', 'ChapterApiController@delete');
 | 
			
		||||
 | 
			
		||||
Route::get('chapters/{id}/export/html', 'ChapterExportApiController@exportHtml');
 | 
			
		||||
Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf');
 | 
			
		||||
Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText');
 | 
			
		||||
 | 
			
		||||
Route::get('shelves', 'BookshelfApiController@list');
 | 
			
		||||
Route::post('shelves', 'BookshelfApiController@create');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,186 @@
 | 
			
		|||
<?php namespace Tests\Api;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use Tests\TestCase;
 | 
			
		||||
 | 
			
		||||
class ChaptersApiTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use TestsApi;
 | 
			
		||||
 | 
			
		||||
    protected $baseEndpoint = '/api/chapters';
 | 
			
		||||
 | 
			
		||||
    public function test_index_endpoint_returns_expected_chapter()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $firstChapter = Chapter::query()->orderBy('id', 'asc')->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
 | 
			
		||||
        $resp->assertJson(['data' => [
 | 
			
		||||
            [
 | 
			
		||||
                'id' => $firstChapter->id,
 | 
			
		||||
                'name' => $firstChapter->name,
 | 
			
		||||
                'slug' => $firstChapter->slug,
 | 
			
		||||
                'book_id' => $firstChapter->book->id,
 | 
			
		||||
                'priority' => $firstChapter->priority,
 | 
			
		||||
            ]
 | 
			
		||||
        ]]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_create_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $book = Book::query()->first();
 | 
			
		||||
        $details = [
 | 
			
		||||
            'name' => 'My API chapter',
 | 
			
		||||
            'description' => 'A chapter created via the API',
 | 
			
		||||
            'book_id' => $book->id,
 | 
			
		||||
            'tags' => [
 | 
			
		||||
                [
 | 
			
		||||
                    'name' => 'tagname',
 | 
			
		||||
                    'value' => 'tagvalue',
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $resp = $this->postJson($this->baseEndpoint, $details);
 | 
			
		||||
        $resp->assertStatus(200);
 | 
			
		||||
        $newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
 | 
			
		||||
        $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
 | 
			
		||||
        $this->assertDatabaseHas('tags', [
 | 
			
		||||
            'entity_id' => $newItem->id,
 | 
			
		||||
            'entity_type' => $newItem->getMorphClass(),
 | 
			
		||||
            'name' => 'tagname',
 | 
			
		||||
            'value' => 'tagvalue',
 | 
			
		||||
        ]);
 | 
			
		||||
        $resp->assertJsonMissing(['pages' => []]);
 | 
			
		||||
        $this->assertActivityExists('chapter_create', $newItem);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_chapter_name_needed_to_create()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $book = Book::query()->first();
 | 
			
		||||
        $details = [
 | 
			
		||||
            'book_id' => $book->id,
 | 
			
		||||
            'description' => 'A chapter created via the API',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $resp = $this->postJson($this->baseEndpoint, $details);
 | 
			
		||||
        $resp->assertStatus(422);
 | 
			
		||||
        $resp->assertJson($this->validationResponse([
 | 
			
		||||
            "name" => ["The name field is required."]
 | 
			
		||||
        ]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_chapter_book_id_needed_to_create()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $details = [
 | 
			
		||||
            'name' => 'My api chapter',
 | 
			
		||||
            'description' => 'A chapter created via the API',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $resp = $this->postJson($this->baseEndpoint, $details);
 | 
			
		||||
        $resp->assertStatus(422);
 | 
			
		||||
        $resp->assertJson($this->validationResponse([
 | 
			
		||||
            "book_id" => ["The book id field is required."]
 | 
			
		||||
        ]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_read_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $chapter = Chapter::visible()->first();
 | 
			
		||||
        $page = $chapter->pages()->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->getJson($this->baseEndpoint . "/{$chapter->id}");
 | 
			
		||||
        $resp->assertStatus(200);
 | 
			
		||||
        $resp->assertJson([
 | 
			
		||||
            'id' => $chapter->id,
 | 
			
		||||
            'slug' => $chapter->slug,
 | 
			
		||||
            'created_by' => [
 | 
			
		||||
                'name' => $chapter->createdBy->name,
 | 
			
		||||
            ],
 | 
			
		||||
            'book_id' => $chapter->book_id,
 | 
			
		||||
            'updated_by' => [
 | 
			
		||||
                'name' => $chapter->createdBy->name,
 | 
			
		||||
            ],
 | 
			
		||||
            'pages' => [
 | 
			
		||||
                [
 | 
			
		||||
                    'id' => $page->id,
 | 
			
		||||
                    'slug' => $page->slug,
 | 
			
		||||
                    'name' => $page->name,
 | 
			
		||||
                ]
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
        $resp->assertJsonCount($chapter->pages()->count(), 'pages');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_update_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $chapter = Chapter::visible()->first();
 | 
			
		||||
        $details = [
 | 
			
		||||
            'name' => 'My updated API chapter',
 | 
			
		||||
            'description' => 'A chapter created via the API',
 | 
			
		||||
            'tags' => [
 | 
			
		||||
                [
 | 
			
		||||
                    'name' => 'freshtag',
 | 
			
		||||
                    'value' => 'freshtagval',
 | 
			
		||||
                ]
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $resp = $this->putJson($this->baseEndpoint . "/{$chapter->id}", $details);
 | 
			
		||||
        $chapter->refresh();
 | 
			
		||||
 | 
			
		||||
        $resp->assertStatus(200);
 | 
			
		||||
        $resp->assertJson(array_merge($details, [
 | 
			
		||||
            'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id
 | 
			
		||||
        ]));
 | 
			
		||||
        $this->assertActivityExists('chapter_update', $chapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_delete_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $chapter = Chapter::visible()->first();
 | 
			
		||||
        $resp = $this->deleteJson($this->baseEndpoint . "/{$chapter->id}");
 | 
			
		||||
 | 
			
		||||
        $resp->assertStatus(204);
 | 
			
		||||
        $this->assertActivityExists('chapter_delete');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_export_html_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $chapter = Chapter::visible()->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/html");
 | 
			
		||||
        $resp->assertStatus(200);
 | 
			
		||||
        $resp->assertSee($chapter->name);
 | 
			
		||||
        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_export_plain_text_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $chapter = Chapter::visible()->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/plaintext");
 | 
			
		||||
        $resp->assertStatus(200);
 | 
			
		||||
        $resp->assertSee($chapter->name);
 | 
			
		||||
        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_export_pdf_endpoint()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAsApiEditor();
 | 
			
		||||
        $chapter = Chapter::visible()->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/pdf");
 | 
			
		||||
        $resp->assertStatus(200);
 | 
			
		||||
        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +23,16 @@ trait TestsApi
 | 
			
		|||
        return ["error" => ["code" => $code, "message" => $message]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format the given (field_name => ["messages"]) array
 | 
			
		||||
     * into a standard validation response format.
 | 
			
		||||
     */
 | 
			
		||||
    protected function validationResponse(array $messages): array
 | 
			
		||||
    {
 | 
			
		||||
        $err = $this->errorResponse("The given data was invalid.", 422);
 | 
			
		||||
        $err['error']['validation'] = $messages;
 | 
			
		||||
        return $err;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an approved API auth header.
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue