Added book export and created export tests to cover
In reference to #177
This commit is contained in:
parent
0abed1afe5
commit
eded8abded
11 changed files with 306 additions and 15 deletions
|
@ -3,6 +3,7 @@
|
|||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
@ -12,16 +13,19 @@ class BookController extends Controller
|
|||
|
||||
protected $entityRepo;
|
||||
protected $userRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
@ -258,4 +262,49 @@ class BookController extends Controller
|
|||
session()->flash('success', trans('entities.books_permissions_updated'));
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a PDF file.
|
||||
* @param string $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportPdf($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$pdfContent = $this->exportService->bookToPdf($book);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a contained HTML file.
|
||||
* @param string $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportHtml($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a book as a plain text file.
|
||||
* @param $bookSlug
|
||||
* @return mixed
|
||||
*/
|
||||
public function exportPlainText($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToPlainText($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -439,7 +439,6 @@ class PageController extends Controller
|
|||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
// return $pdfContent;
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
|
||||
|
|
|
@ -313,11 +313,12 @@ class EntityRepo
|
|||
* Loads the book slug onto child elements to prevent access database access for getting the slug.
|
||||
* @param Book $book
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $renderPages
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBookChildren(Book $book, $filterDrafts = false)
|
||||
public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
|
||||
{
|
||||
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts)->get();
|
||||
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
|
||||
$entities = [];
|
||||
$parents = [];
|
||||
$tree = [];
|
||||
|
@ -325,6 +326,10 @@ class EntityRepo
|
|||
foreach ($q as $index => $rawEntity) {
|
||||
if ($rawEntity->entity_type === 'BookStack\\Page') {
|
||||
$entities[$index] = $this->page->newFromBuilder($rawEntity);
|
||||
if ($renderPages) {
|
||||
$entities[$index]->html = $rawEntity->description;
|
||||
$entities[$index]->html = $this->renderPage($entities[$index]);
|
||||
};
|
||||
} else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
|
||||
$entities[$index] = $this->chapter->newFromBuilder($rawEntity);
|
||||
$key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Page;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
|
||||
|
@ -25,24 +26,69 @@ class ExportService
|
|||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
|
||||
$pageHtml = view('pages/export', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a pdf file.
|
||||
* Convert a book to a self-contained HTML file.
|
||||
* @param Book $book
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function bookToContainedHtml(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$html = view('books/export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree
|
||||
])->render();
|
||||
return $this->containHtml($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a PDF file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
|
||||
$html = view('pages/pdf', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book to a PDF file
|
||||
* @param Book $book
|
||||
* @return string
|
||||
*/
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$html = view('books/export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normal webpage HTML to a PDF.
|
||||
* @param $html
|
||||
* @return string
|
||||
*/
|
||||
protected function htmlToPdf($html)
|
||||
{
|
||||
$containedHtml = $this->containHtml($html);
|
||||
$useWKHTML = config('snappy.pdf.binary') !== false;
|
||||
$containedHtml = $this->containHtml($pageHtml);
|
||||
if ($useWKHTML) {
|
||||
$pdf = \SnappyPDF::loadHTML($containedHtml);
|
||||
$pdf->setOption('print-media-type', true);
|
||||
} else {
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
}
|
||||
|
@ -122,6 +168,29 @@ class ExportService
|
|||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book into a plain text string.
|
||||
* @param Book $book
|
||||
* @return string
|
||||
*/
|
||||
public function bookToPlainText(Book $book)
|
||||
{
|
||||
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
|
||||
$text = $book->name . "\n\n";
|
||||
foreach ($bookTree as $bookChild) {
|
||||
if ($bookChild->isA('chapter')) {
|
||||
$text .= $bookChild->name . "\n\n";
|
||||
$text .= $bookChild->description . "\n\n";
|
||||
foreach ($bookChild->pages as $page) {
|
||||
$text .= $this->pageToPlainText($page);
|
||||
}
|
||||
} else {
|
||||
$text .= $this->pageToPlainText($bookChild);
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -474,11 +474,13 @@ class PermissionService
|
|||
/**
|
||||
* Get the children of a book in an efficient single query, Filtered by the permission system.
|
||||
* @param integer $book_id
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $fetchPageContent
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false) {
|
||||
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
|
||||
$pageContentSelect = $fetchPageContent ? 'html' : "''";
|
||||
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function($query) {
|
||||
|
|
|
@ -143,7 +143,7 @@ return [
|
|||
* the desired content might be different (e.g. screen or projection view of html file).
|
||||
* Therefore allow specification of content here.
|
||||
*/
|
||||
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen",
|
||||
"DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
|
||||
|
||||
/**
|
||||
* The default paper size.
|
||||
|
|
78
resources/views/books/export.blade.php
Normal file
78
resources/views/books/export.blade.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>{{ $book->name }}</title>
|
||||
|
||||
<style>
|
||||
{!! file_get_contents(public_path('/css/export-styles.css')) !!}
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
.chapter-hint {
|
||||
color: #888;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.chapter-hint + h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul.contents ul li {
|
||||
list-style: circle;
|
||||
}
|
||||
@media screen {
|
||||
.page-break {
|
||||
border-top: 1px solid #DDD;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@yield('head')
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="page-content">
|
||||
|
||||
<h1 style="font-size: 4.8em">{{$book->name}}</h1>
|
||||
|
||||
<p>{{ $book->description }}</p>
|
||||
|
||||
@if(count($bookChildren) > 0)
|
||||
<ul class="contents">
|
||||
@foreach($bookChildren as $bookChild)
|
||||
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
|
||||
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
|
||||
<ul>
|
||||
@foreach($bookChild->pages as $page)
|
||||
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@foreach($bookChildren as $bookChild)
|
||||
<div class="page-break"></div>
|
||||
<h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
|
||||
@if($bookChild->isA('chapter'))
|
||||
<p>{{ $bookChild->description }}</p>
|
||||
@if(count($bookChild->pages) > 0)
|
||||
@foreach($bookChild->pages as $page)
|
||||
<div class="page-break"></div>
|
||||
<div class="chapter-hint">{{$bookChild->name}}</div>
|
||||
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
|
||||
{!! $page->html !!}
|
||||
@endforeach
|
||||
@endif
|
||||
@else
|
||||
{!! $bookChild->html !!}
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -10,6 +10,14 @@
|
|||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="action-buttons faded">
|
||||
<span dropdown class="dropdown-container">
|
||||
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.pages_export') }}</div>
|
||||
<ul class="wide">
|
||||
<li><a href="{{ $book->getUrl('/export/html') }}" target="_blank">{{ trans('entities.pages_export_html') }} <span class="text-muted float right">.html</span></a></li>
|
||||
<li><a href="{{ $book->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.pages_export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
|
||||
<li><a href="{{ $book->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.pages_export_text') }} <span class="text-muted float right">.txt</span></a></li>
|
||||
</ul>
|
||||
</span>
|
||||
@if(userCan('page-create', $book))
|
||||
<a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
|
||||
@endif
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<title>{{ $page->name }}</title>
|
||||
|
||||
<style>
|
||||
{!! $css !!}
|
||||
{!! file_get_contents(public_path('/css/export-styles.css')) !!}
|
||||
</style>
|
||||
@yield('head')
|
||||
</head>
|
||||
|
|
|
@ -26,6 +26,9 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
Route::get('/{slug}/delete', 'BookController@showDelete');
|
||||
Route::get('/{bookSlug}/sort', 'BookController@sort');
|
||||
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
|
||||
Route::get('/{bookSlug}/export/html', 'BookController@exportHtml');
|
||||
Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf');
|
||||
Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText');
|
||||
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
|
|
78
tests/Entity/ExportTest.php
Normal file
78
tests/Entity/ExportTest.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
|
||||
use BookStack\Page;
|
||||
|
||||
class ExportTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_page_text_export()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asEditor();
|
||||
|
||||
$resp = $this->get($page->getUrl('/export/plaintext'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_page_pdf_export()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asEditor();
|
||||
|
||||
$resp = $this->get($page->getUrl('/export/pdf'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_page_html_export()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asEditor();
|
||||
|
||||
$resp = $this->get($page->getUrl('/export/html'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_book_text_export()
|
||||
{
|
||||
$page = Page::first();
|
||||
$book = $page->book;
|
||||
$this->asEditor();
|
||||
|
||||
$resp = $this->get($book->getUrl('/export/plaintext'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_book_pdf_export()
|
||||
{
|
||||
$page = Page::first();
|
||||
$book = $page->book;
|
||||
$this->asEditor();
|
||||
|
||||
$resp = $this->get($book->getUrl('/export/pdf'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_book_html_export()
|
||||
{
|
||||
$page = Page::first();
|
||||
$book = $page->book;
|
||||
$this->asEditor();
|
||||
|
||||
$resp = $this->get($book->getUrl('/export/html'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html');
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue