diff --git a/app/Console/Commands/DeleteUsers.php b/app/Console/Commands/DeleteUsers.php new file mode 100644 index 000000000..8829d3992 --- /dev/null +++ b/app/Console/Commands/DeleteUsers.php @@ -0,0 +1,62 @@ +user = $user; + $this->userRepo = $userRepo; + parent::__construct(); + } + + public function handle() + { + $confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)'); + $numDeleted = 0; + if (strtolower(trim($confirm)) === 'yes') + { + $totalUsers = $this->user->count(); + $users = $this->user->where('system_name', '=', null)->with('roles')->get(); + foreach ($users as $user) + { + if ($user->hasSystemRole('admin')) + { + // don't delete users with "admin" role + continue; + } + $this->userRepo->destroy($user); + ++$numDeleted; + } + $this->info("Deleted $numDeleted of $totalUsers total users."); + } + else + { + $this->info('Exiting...'); + } + } + +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 12792e151..a979072e2 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -4,11 +4,14 @@ namespace BookStack\Exceptions; use Exception; use Illuminate\Auth\AuthenticationException; +use Illuminate\Http\Request; +use Illuminate\Pipeline\Pipeline; use Illuminate\Validation\ValidationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Auth\Access\AuthorizationException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class Handler extends ExceptionHandler { @@ -60,9 +63,32 @@ class Handler extends ExceptionHandler return response()->view('errors/' . $code, ['message' => $message], $code); } + // Handle 404 errors with a loaded session to enable showing user-specific information + if ($this->isExceptionType($e, NotFoundHttpException::class)) { + return $this->loadErrorMiddleware($request, function ($request) use ($e) { + $message = $e->getMessage() ?: trans('errors.404_page_not_found'); + return response()->view('errors/404', ['message' => $message], 404); + }); + } + return parent::render($request, $e); } + /** + * Load the middleware required to show state/session-enabled error pages. + * @param Request $request + * @param $callback + * @return mixed + */ + protected function loadErrorMiddleware(Request $request, $callback) + { + $middleware = (\Route::getMiddlewareGroups()['web_errors']); + return (new Pipeline($this->container)) + ->send($request) + ->through($middleware) + ->then($callback); + } + /** * Check the exception chain to compare against the original exception type. * @param Exception $e diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index e181aec89..f1645bb4b 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -46,7 +46,7 @@ class BookController extends Controller 'books' => $books, 'recents' => $recents, 'popular' => $popular, - 'new' => $new, + 'new' => $new, 'booksViewType' => $booksViewType ]); } @@ -155,7 +155,7 @@ class BookController extends Controller $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-update', $book); $bookChildren = $this->entityRepo->getBookChildren($book, true); - $books = $this->entityRepo->getAll('book', false); + $books = $this->entityRepo->getAll('book', false, 'update'); $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()])); return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); } @@ -190,42 +190,56 @@ class BookController extends Controller } // Sort pages and chapters - $sortedBooks = []; - $updatedModels = collect(); - $sortMap = json_decode($request->get('sort-tree')); - $defaultBookId = $book->id; + $sortMap = collect(json_decode($request->get('sort-tree'))); + $bookIdsInvolved = collect([$book->id]); - // Loop through contents of provided map and update entities accordingly - foreach ($sortMap as $bookChild) { - $priority = $bookChild->sort; - $id = intval($bookChild->id); - $isPage = $bookChild->type == 'page'; - $bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId; - $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter); - $model = $this->entityRepo->getById($isPage?'page':'chapter', $id); + // Load models into map + $sortMap->each(function($mapItem) use ($bookIdsInvolved) { + $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter'); + $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id); + // Store source and target books + $bookIdsInvolved->push(intval($mapItem->model->book_id)); + $bookIdsInvolved->push(intval($mapItem->book)); + }); - // Update models only if there's a change in parent chain or ordering. - if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) { - $this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model); - $model->priority = $priority; - if ($isPage) $model->chapter_id = $chapterId; + // Get the books involved in the sort + $bookIdsInvolved = $bookIdsInvolved->unique()->toArray(); + $booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get(); + // Throw permission error if invalid ids or inaccessible books given. + if (count($bookIdsInvolved) !== count($booksInvolved)) { + $this->showPermissionError(); + } + // Check permissions of involved books + $booksInvolved->each(function(Book $book) { + $this->checkOwnablePermission('book-update', $book); + }); + + // Perform the sort + $sortMap->each(function($mapItem) { + $model = $mapItem->model; + + $priorityChanged = intval($model->priority) !== intval($mapItem->sort); + $bookChanged = intval($model->book_id) !== intval($mapItem->book); + $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter; + + if ($bookChanged) { + $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model); + } + if ($chapterChanged) { + $model->chapter_id = intval($mapItem->parentChapter); $model->save(); - $updatedModels->push($model); } - - // Store involved books to be sorted later - if (!in_array($bookId, $sortedBooks)) { - $sortedBooks[] = $bookId; + if ($priorityChanged) { + $model->priority = intval($mapItem->sort); + $model->save(); } - } + }); - // Add activity for books - foreach ($sortedBooks as $bookId) { - /** @var Book $updatedBook */ - $updatedBook = $this->entityRepo->getById('book', $bookId); - $this->entityRepo->buildJointPermissionsForBook($updatedBook); - Activity::add($updatedBook, 'book_sort', $updatedBook->id); - } + // Rebuild permissions and add activity for involved books. + $booksInvolved->each(function(Book $book) { + $this->entityRepo->buildJointPermissionsForBook($book); + Activity::add($book, 'book_sort', $book->id); + }); return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 13e928465..9dc7d6401 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -145,6 +145,7 @@ class PageController extends Controller * @param string $bookSlug * @param string $pageSlug * @return Response + * @throws NotFoundException */ public function show($bookSlug, $pageSlug) { @@ -152,7 +153,7 @@ class PageController extends Controller $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); } catch (NotFoundException $e) { $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug); - if ($page === null) abort(404); + if ($page === null) throw $e; return redirect($page->getUrl()); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index fe5c7a243..2fe22f1e1 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -249,4 +249,27 @@ class UserController extends Controller 'assetCounts' => $assetCounts ]); } + + /** + * Update the user's preferred book-list display setting. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function switchBookView($id, Request $request) { + $this->checkPermissionOr('users-manage', function () use ($id) { + return $this->currentUser->id == $id; + }); + + $viewType = $request->get('book_view_type'); + if (!in_array($viewType, ['grid', 'list'])) { + $viewType = 'list'; + } + + $user = $this->user->findOrFail($id); + setting()->putUser($user, 'books_view_type', $viewType); + + return redirect()->back(302, [], "/settings/users/$id"); + } + } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index cd894de95..9d2871bbe 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -33,6 +33,14 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \BookStack\Http\Middleware\Localization::class ], + 'web_errors' => [ + \BookStack\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \BookStack\Http\Middleware\VerifyCsrfToken::class, + \BookStack\Http\Middleware\Localization::class + ], 'api' => [ 'throttle:60,1', 'bindings', diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index c31ddfefe..2c92e1907 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -113,9 +113,9 @@ class EntityRepo * @param bool $allowDrafts * @return \Illuminate\Database\Query\Builder */ - protected function entityQuery($type, $allowDrafts = false) + protected function entityQuery($type, $allowDrafts = false, $permission = 'view') { - $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view'); + $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission); if (strtolower($type) === 'page' && !$allowDrafts) { $q = $q->where('draft', '=', false); } @@ -196,14 +196,15 @@ class EntityRepo } /** - * Get all entities of a type limited by count unless count if false. + * Get all entities of a type with the given permission, limited by count unless count is false. * @param string $type * @param integer|bool $count + * @param string $permission * @return Collection */ - public function getAll($type, $count = 20) + public function getAll($type, $count = 20, $permission = 'view') { - $q = $this->entityQuery($type)->orderBy('name', 'asc'); + $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc'); if ($count !== false) $q = $q->take($count); return $q->get(); } @@ -690,6 +691,7 @@ class EntityRepo preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches); if (count($matches[0]) === 0) return $content; + $topLevelTags = ['table', 'ul', 'ol']; foreach ($matches[1] as $index => $includeId) { $splitInclude = explode('#', $includeId, 2); $pageId = intval($splitInclude[0]); @@ -714,8 +716,13 @@ class EntityRepo continue; } $innerContent = ''; - foreach ($matchingElem->childNodes as $childNode) { - $innerContent .= $doc->saveHTML($childNode); + $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags); + if ($isTopLevel) { + $innerContent .= $doc->saveHTML($matchingElem); + } else { + foreach ($matchingElem->childNodes as $childNode) { + $innerContent .= $doc->saveHTML($childNode); + } } $content = str_replace($matches[0][$index], trim($innerContent), $content); } diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index c3546a442..52ad2e47e 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -115,9 +115,9 @@ class UserRepo */ public function isOnlyAdmin(User $user) { - if (!$user->roles->pluck('name')->contains('admin')) return false; + if (!$user->hasSystemRole('admin')) return false; - $adminRole = $this->role->getRole('admin'); + $adminRole = $this->role->getSystemRole('admin'); if ($adminRole->users->count() > 1) return false; return true; } diff --git a/app/Services/SettingService.php b/app/Services/SettingService.php index 18a7c0d1b..ce87f5a4b 100644 --- a/app/Services/SettingService.php +++ b/app/Services/SettingService.php @@ -98,6 +98,9 @@ class SettingService { $cacheKey = $this->cachePrefix . $key; $this->cache->forget($cacheKey); + if (isset($this->localCache[$key])) { + unset($this->localCache[$key]); + } } /** diff --git a/app/User.php b/app/User.php index 8033557e4..fd6879ba0 100644 --- a/app/User.php +++ b/app/User.php @@ -81,7 +81,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function hasSystemRole($role) { - return $this->roles->pluck('system_name')->contains('admin'); + return $this->roles->pluck('system_name')->contains($role); } /** diff --git a/config/view.php b/config/view.php index e193ab61d..8dc2841e7 100644 --- a/config/view.php +++ b/config/view.php @@ -1,5 +1,10 @@ [ - realpath(base_path('resources/views')), - ], + 'paths' => $viewPaths, /* |-------------------------------------------------------------------------- diff --git a/readme.md b/readme.md index 1b3db4a56..77f1e8805 100644 --- a/readme.md +++ b/readme.md @@ -72,7 +72,13 @@ Some strings have colon-prefixed variables in such as `:userName`. Leave these v Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue. -Pull requests are very welcome. If the scope of your pull request is very large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge. +### Pull Request + +Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge. + +Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. + +If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly. ## Website, Docs & Blog diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 54e109067..051d1978a 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -549,7 +549,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .content { padding: $-s; font-size: 0.666em; - p, ul { + p, ul, ol { font-size: $fs-m; margin: .5em 0; } diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 7cdd7c23e..26f096327 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -49,6 +49,8 @@ return [ 'toggle_details' => 'Toggle Details', 'toggle_thumbnails' => 'Toggle Thumbnails', 'details' => 'Details', + 'grid_view' => 'Grid View', + 'list_view' => 'List View', /** * Header diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index f35c486ad..f3e26fb45 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -96,7 +96,6 @@ return [ 'users_external_auth_id' => 'External Authentication ID', 'users_password_warning' => 'Only fill the below if you would like to change your password:', 'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.', - 'users_books_view_type' => 'Preferred layout for books viewing', 'users_delete' => 'Delete User', 'users_delete_named' => 'Delete user :userName', 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index d392af045..2ab819327 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -1,8 +1,21 @@ @extends('sidebar-layout') @section('toolbar') -
-
+
+
+
id}/switch-book-view") }}" method="POST" class="inline"> + {!! csrf_field() !!} + {!! method_field('PATCH') !!} + + @if ($booksViewType === 'list') + + @else + + @endif +
+
+
+
@if($currentUser->can('book-create-all')) {{ trans('entities.books_create') }} @@ -52,7 +65,7 @@
@endforeach {!! $books->render() !!} - @else + @else
@foreach($books as $key => $book) @include('books/grid-item', ['book' => $book]) diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php index f6ef850af..7cc67a677 100644 --- a/resources/views/errors/404.blade.php +++ b/resources/views/errors/404.blade.php @@ -1,8 +1,6 @@ @extends('simple-layout') @section('content') - -

 

@@ -16,7 +14,6 @@
@if (setting('app-public') || !user()->isDefault()) -
diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php index 71fb78a35..a01234d81 100644 --- a/resources/views/errors/500.blade.php +++ b/resources/views/errors/500.blade.php @@ -6,7 +6,7 @@

{{ trans('errors.error_occurred') }}

-
{{ $message }}
+
{{ $message or 'An unknown error occurred' }}

{{ trans('errors.return_home') }}

diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 14ab19e0e..fc75593b8 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -43,13 +43,6 @@ @endforeach
-
- - -
diff --git a/routes/web.php b/routes/web.php index 266e297f3..08c919e26 100644 --- a/routes/web.php +++ b/routes/web.php @@ -149,6 +149,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/users', 'UserController@index'); Route::get('/users/create', 'UserController@create'); Route::get('/users/{id}/delete', 'UserController@delete'); + Route::patch('/users/{id}/switch-book-view', 'UserController@switchBookView'); Route::post('/users/create', 'UserController@store'); Route::get('/users/{id}', 'UserController@edit'); Route::put('/users/{id}', 'UserController@update'); diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php index 1eabc7417..d5c9911f8 100644 --- a/tests/BrowserKitTest.php +++ b/tests/BrowserKitTest.php @@ -3,7 +3,6 @@ use BookStack\Entity; use BookStack\Role; use BookStack\Services\PermissionService; -use BookStack\User; use Illuminate\Contracts\Console\Kernel; use Illuminate\Foundation\Testing\DatabaseTransactions; use Laravel\BrowserKitTesting\TestCase; diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php index a43f65b5e..4d4e0e6cd 100644 --- a/tests/Entity/EntityTest.php +++ b/tests/Entity/EntityTest.php @@ -82,6 +82,27 @@ class EntityTest extends BrowserKitTest ->see($firstChapter->name); } + public function test_toggle_book_view() + { + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'grid'); + + $this->actingAs($editor) + ->visit('/books') + ->pageHasElement('.featured-image-container') + ->submitForm('List View') + // Check redirection. + ->seePageIs('/books') + ->pageNotHasElement('.featured-image-container'); + + $this->actingAs($editor) + ->visit('/books') + ->submitForm('Grid View') + ->seePageIs('/books') + ->pageHasElement('.featured-image-container'); + + } + public function pageCreation($chapter) { $page = factory(Page::class)->make([ @@ -155,7 +176,7 @@ class EntityTest extends BrowserKitTest ->type($book->name, '#name') ->type($book->description, '#description') ->press('Save Book'); - + $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/'; $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index cd6526aec..370514788 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -9,7 +9,7 @@ class PageContentTest extends TestCase public function test_page_includes() { $page = Page::first(); - $secondPage = Page::all()->get(2); + $secondPage = Page::where('id', '!=', $page->id)->first(); $secondPage->html = "

Hello, This is a test

This is a second block of content

"; $secondPage->save(); @@ -38,7 +38,7 @@ class PageContentTest extends TestCase public function test_saving_page_with_includes() { $page = Page::first(); - $secondPage = Page::all()->get(2); + $secondPage = Page::where('id', '!=', $page->id)->first(); $this->asEditor(); $page->html = "

{{@$secondPage->id}}

"; @@ -50,6 +50,23 @@ class PageContentTest extends TestCase $this->assertContains("{{@$secondPage->id}}", $page->html); } + public function test_page_includes_do_not_break_tables() + { + $page = Page::first(); + $secondPage = Page::where('id', '!=', $page->id)->first(); + + $content = '
test
'; + $secondPage->html = $content; + $secondPage->save(); + + $page->html = "{{@{$secondPage->id}#table}}"; + $page->save(); + + $this->asEditor(); + $pageResp = $this->get($page->getUrl()); + $pageResp->assertSee($content); + } + public function test_page_revision_views_viewable() { $this->asEditor(); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php new file mode 100644 index 000000000..c9b5a0109 --- /dev/null +++ b/tests/ErrorTest.php @@ -0,0 +1,18 @@ +getEditor(); + $this->actingAs($editor); + $notFound = $this->get('/fgfdngldfnotfound'); + $notFound->assertStatus(404); + $notFound->assertDontSeeText('Log in'); + $notFound->assertSeeText($editor->getShortName(9)); + } +} \ No newline at end of file diff --git a/tests/Permissions/RestrictionsTest.php b/tests/Permissions/RestrictionsTest.php index 218b7a0d8..8f37b2517 100644 --- a/tests/Permissions/RestrictionsTest.php +++ b/tests/Permissions/RestrictionsTest.php @@ -3,6 +3,7 @@ use BookStack\Book; use BookStack\Services\PermissionService; use BookStack\User; +use BookStack\Repos\EntityRepo; class RestrictionsTest extends BrowserKitTest { @@ -554,4 +555,70 @@ class RestrictionsTest extends BrowserKitTest $this->dontSee(substr($bookChapter->name, 0, 15)); } + public function test_book_sort_view_permission() + { + $firstBook = Book::first(); + $secondBook = Book::find(2); + $thirdBook = Book::find(3); + + $this->setEntityRestrictions($firstBook, ['view', 'update']); + $this->setEntityRestrictions($secondBook, ['view']); + $this->setEntityRestrictions($thirdBook, ['view', 'update']); + + // Test sort page visibility + $this->actingAs($this->user)->visit($secondBook->getUrl() . '/sort') + ->see('You do not have permission') + ->seePageIs('/'); + + // Check sort page on first book + $this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort') + ->see($thirdBook->name) + ->dontSee($secondBook->name); + } + + public function test_book_sort_permission() { + $firstBook = Book::first(); + $secondBook = Book::find(2); + + $this->setEntityRestrictions($firstBook, ['view', 'update']); + $this->setEntityRestrictions($secondBook, ['view']); + + $firstBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter', + ['name' => 'first book chapter'], $firstBook); + $secondBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter', + ['name' => 'second book chapter'], $secondBook); + + // Create request data + $reqData = [ + [ + 'id' => $firstBookChapter->id, + 'sort' => 0, + 'parentChapter' => false, + 'type' => 'chapter', + 'book' => $secondBook->id + ] + ]; + + // Move chapter from first book to a second book + $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) + ->followRedirects() + ->see('You do not have permission') + ->seePageIs('/'); + + $reqData = [ + [ + 'id' => $secondBookChapter->id, + 'sort' => 0, + 'parentChapter' => false, + 'type' => 'chapter', + 'book' => $firstBook->id + ] + ]; + + // Move chapter from second book to first book + $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) + ->followRedirects() + ->see('You do not have permission') + ->seePageIs('/'); + } } diff --git a/tests/UserProfileTest.php b/tests/UserProfileTest.php index 0c66363f0..3262117d5 100644 --- a/tests/UserProfileTest.php +++ b/tests/UserProfileTest.php @@ -103,7 +103,7 @@ class UserProfileTest extends BrowserKitTest $this->actingAs($editor) ->visit('/books') ->pageNotHasElement('.featured-image-container') - ->pageHasElement('.entity-list-item'); + ->pageHasElement('.content .entity-list-item'); } public function test_books_view_is_grid() diff --git a/themes/.gitignore b/themes/.gitignore new file mode 100755 index 000000000..d6b7ef32c --- /dev/null +++ b/themes/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/version b/version index de0ad7791..0507cd08e 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.18-dev +v0.20-dev