Added book export and created export tests to cover

In reference to #177
This commit is contained in:
Dan Brown 2017-02-26 13:26:51 +00:00
parent 0abed1afe5
commit eded8abded
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 306 additions and 15 deletions

View File

@ -3,6 +3,7 @@
use Activity; use Activity;
use BookStack\Repos\EntityRepo; use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
@ -12,16 +13,19 @@ class BookController extends Controller
protected $entityRepo; protected $entityRepo;
protected $userRepo; protected $userRepo;
protected $exportService;
/** /**
* BookController constructor. * BookController constructor.
* @param EntityRepo $entityRepo * @param EntityRepo $entityRepo
* @param UserRepo $userRepo * @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->entityRepo = $entityRepo;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct(); parent::__construct();
} }
@ -258,4 +262,49 @@ class BookController extends Controller
session()->flash('success', trans('entities.books_permissions_updated')); session()->flash('success', trans('entities.books_permissions_updated'));
return redirect($book->getUrl()); 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'
]);
}
} }

View File

@ -439,7 +439,6 @@ class PageController extends Controller
{ {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$pdfContent = $this->exportService->pageToPdf($page); $pdfContent = $this->exportService->pageToPdf($page);
// return $pdfContent;
return response()->make($pdfContent, 200, [ return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'

View File

@ -313,11 +313,12 @@ class EntityRepo
* Loads the book slug onto child elements to prevent access database access for getting the slug. * Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param Book $book * @param Book $book
* @param bool $filterDrafts * @param bool $filterDrafts
* @param bool $renderPages
* @return mixed * @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 = []; $entities = [];
$parents = []; $parents = [];
$tree = []; $tree = [];
@ -325,6 +326,10 @@ class EntityRepo
foreach ($q as $index => $rawEntity) { foreach ($q as $index => $rawEntity) {
if ($rawEntity->entity_type === 'BookStack\\Page') { if ($rawEntity->entity_type === 'BookStack\\Page') {
$entities[$index] = $this->page->newFromBuilder($rawEntity); $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') { } else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
$entities[$index] = $this->chapter->newFromBuilder($rawEntity); $entities[$index] = $this->chapter->newFromBuilder($rawEntity);
$key = $entities[$index]->entity_type . ':' . $entities[$index]->id; $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;

View File

@ -1,5 +1,6 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Page; use BookStack\Page;
use BookStack\Repos\EntityRepo; use BookStack\Repos\EntityRepo;
@ -25,24 +26,69 @@ class ExportService
*/ */
public function pageToContainedHtml(Page $page) public function pageToContainedHtml(Page $page)
{ {
$cssContent = file_get_contents(public_path('/css/export-styles.css')); $pageHtml = view('pages/export', [
$pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); 'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->containHtml($pageHtml); 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 * @param Page $page
* @return mixed|string * @return mixed|string
*/ */
public function pageToPdf(Page $page) public function pageToPdf(Page $page)
{ {
$cssContent = file_get_contents(public_path('/css/export-styles.css')); $html = view('pages/pdf', [
$pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); '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; $useWKHTML = config('snappy.pdf.binary') !== false;
$containedHtml = $this->containHtml($pageHtml);
if ($useWKHTML) { if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml); $pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else { } else {
$pdf = \PDF::loadHTML($containedHtml); $pdf = \PDF::loadHTML($containedHtml);
} }
@ -122,6 +168,29 @@ class ExportService
return $text; 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;
}
} }

View File

@ -475,10 +475,12 @@ class PermissionService
* Get the children of a book in an efficient single query, Filtered by the permission system. * Get the children of a book in an efficient single query, Filtered by the permission system.
* @param integer $book_id * @param integer $book_id
* @param bool $filterDrafts * @param bool $filterDrafts
* @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder * @return \Illuminate\Database\Query\Builder
*/ */
public function bookChildrenQuery($book_id, $filterDrafts = false) { public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = 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) { $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); $query->where('draft', '=', 0);
if (!$filterDrafts) { if (!$filterDrafts) {
$query->orWhere(function($query) { $query->orWhere(function($query) {

View File

@ -143,7 +143,7 @@ return [
* the desired content might be different (e.g. screen or projection view of html file). * the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here. * Therefore allow specification of content here.
*/ */
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen", "DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
/** /**
* The default paper size. * The default paper size.

View 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>

View File

@ -10,6 +10,14 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="action-buttons faded"> <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)) @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> <a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
@endif @endif

View File

@ -5,7 +5,7 @@
<title>{{ $page->name }}</title> <title>{{ $page->name }}</title>
<style> <style>
{!! $css !!} {!! file_get_contents(public_path('/css/export-styles.css')) !!}
</style> </style>
@yield('head') @yield('head')
</head> </head>

View File

@ -26,6 +26,9 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{slug}/delete', 'BookController@showDelete'); Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort'); Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort'); 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 // Pages
Route::get('/{bookSlug}/page/create', 'PageController@create'); Route::get('/{bookSlug}/page/create', 'PageController@create');

View 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');
}
}