]> BookStack Code Mirror - bookstack/commitdiff
Added book export and created export tests to cover
authorDan Brown <redacted>
Sun, 26 Feb 2017 13:26:51 +0000 (13:26 +0000)
committerDan Brown <redacted>
Sun, 26 Feb 2017 13:26:51 +0000 (13:26 +0000)
In reference to #177

app/Http/Controllers/BookController.php
app/Http/Controllers/PageController.php
app/Repos/EntityRepo.php
app/Services/ExportService.php
app/Services/PermissionService.php
config/dompdf.php
resources/views/books/export.blade.php [new file with mode: 0644]
resources/views/books/show.blade.php
resources/views/pages/export.blade.php
routes/web.php
tests/Entity/ExportTest.php [new file with mode: 0644]

index 408192ff9085dadb68363c1c1eb821704572301c..fe9ece5b252e53b30addf58a4d08a1689c873b65 100644 (file)
@@ -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'
+        ]);
+    }
 }
index 623cb9c4d9c4f4d7f9ca2ddb0a49e376ec9acdf5..4a29c20d62f7dc6db65c49ba1541254755560c31 100644 (file)
@@ -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'
index 7b262c3de70892c637d88d0241fadf7d745303c2..4db69137f0e7956bb6f6a8da925703d66afa27c9 100644 (file)
@@ -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;
index e51577a22b5be4ee457942fcba0b2b75d12fa2fa..3ac69871885853f166d5fa2cd67034d0d0225c0d 100644 (file)
@@ -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;
+    }
+
 }
 
 
index 72a810b6b4ebf657f6c29cd681141b930652a34c..8b47e1246683c4f30e33976cb4cc84fdfbe58dde 100644 (file)
@@ -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) {
index 1eb1d97828e94dfdac155910f32e0d93c90252be..036e1bb3c7b601a04f367035807d1834fbff69e5 100644 (file)
@@ -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.
diff --git a/resources/views/books/export.blade.php b/resources/views/books/export.blade.php
new file mode 100644 (file)
index 0000000..e5fbada
--- /dev/null
@@ -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>
index 6b4e7f88a96efd61abb5465e9caeb3f086ca9e00..99ffe80e12026f8fb8d352d666c4e1027a957260 100644 (file)
                 </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
index 19a6355637c74db5c132a9b21a14529ed2d1c158..e0813e46849429cee36a032b161be03a0d7362d9 100644 (file)
@@ -5,7 +5,7 @@
     <title>{{ $page->name }}</title>
 
     <style>
-        {!! $css !!}
+        {!! file_get_contents(public_path('/css/export-styles.css')) !!}
     </style>
     @yield('head')
 </head>
index 8d166f1d6090245f4f07adfbfc4b0f70b174c3a9..670439a6659065a8ffe3068828536dc72f3bb05a 100644 (file)
@@ -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');
diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php
new file mode 100644 (file)
index 0000000..b1dab09
--- /dev/null
@@ -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');
+    }
+
+}
\ No newline at end of file