mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-25 09:42:10 +08:00
commit
131fcae4c7
17
app/Book.php
17
app/Book.php
|
@ -48,14 +48,6 @@ class Book extends Entity
|
|||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
|
@ -75,6 +67,15 @@ class Book extends Entity
|
|||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shelves this book is contained within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function shelves()
|
||||
{
|
||||
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
|
|
84
app/Bookshelf.php
Normal file
84
app/Bookshelf.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Bookshelf extends Entity
|
||||
{
|
||||
protected $table = 'bookshelves';
|
||||
|
||||
public $searchFactor = 3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
/**
|
||||
* Get the books in this shelf.
|
||||
* Should not be used directly since does not take into account permissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function books()
|
||||
{
|
||||
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this bookshelf.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
if ($path !== false) {
|
||||
return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/shelves/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns BookShelf cover image, if cover does not exists return default cover image.
|
||||
* @param int $width - Width of the image
|
||||
* @param int $height - Height of the image
|
||||
* @return string
|
||||
*/
|
||||
public function getBookCover($width = 440, $height = 250)
|
||||
{
|
||||
$default = baseUrl('/book_default_cover.png');
|
||||
if (!$this->image_id) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$cover = $default;
|
||||
}
|
||||
return $cover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image of the book
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function cover()
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
}
|
|
@ -152,7 +152,7 @@ class Entity extends Ownable
|
|||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
|
|
244
app/Http/Controllers/BookshelfController.php
Normal file
244
app/Http/Controllers/BookshelfController.php
Normal file
|
@ -0,0 +1,244 @@
|
|||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Bookshelf;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class BookshelfController 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, ExportService $exportService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
|
||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
|
||||
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
|
||||
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
|
||||
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves'));
|
||||
return view('shelves/index', [
|
||||
'shelves' => $shelves,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new,
|
||||
'shelvesViewType' => $shelvesViewType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new bookshelf.
|
||||
* @return Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||
$this->setPageTitle(trans('entities.shelves_create'));
|
||||
return view('shelves/create', ['books' => $books]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created bookshelf in storage.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
]);
|
||||
|
||||
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
||||
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
|
||||
Activity::add($bookshelf, 'bookshelf_create');
|
||||
|
||||
return redirect($bookshelf->getUrl());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the specified bookshelf.
|
||||
* @param String $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function show(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('book-view', $bookshelf);
|
||||
|
||||
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
|
||||
Views::add($bookshelf);
|
||||
|
||||
$this->setPageTitle($bookshelf->getShortName());
|
||||
return view('shelves/show', [
|
||||
'shelf' => $bookshelf,
|
||||
'books' => $books,
|
||||
'activity' => Activity::entityActivity($bookshelf, 20, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified bookshelf.
|
||||
* @param $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function edit(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||
|
||||
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
|
||||
$shelfBookIds = $shelfBooks->pluck('id');
|
||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||
$books = $books->filter(function ($book) use ($shelfBookIds) {
|
||||
return !$shelfBookIds->contains($book->id);
|
||||
});
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
|
||||
return view('shelves/edit', [
|
||||
'shelf' => $bookshelf,
|
||||
'books' => $books,
|
||||
'shelfBooks' => $shelfBooks,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified bookshelf in storage.
|
||||
* @param Request $request
|
||||
* @param string $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function update(Request $request, string $slug)
|
||||
{
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
]);
|
||||
|
||||
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
|
||||
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
||||
Activity::add($shelf, 'bookshelf_update');
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows the page to confirm deletion
|
||||
* @param $slug
|
||||
* @return \Illuminate\View\View
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showDelete(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
|
||||
return view('shelves/delete', ['shelf' => $bookshelf]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified bookshelf from storage.
|
||||
* @param string $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroy(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
||||
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
|
||||
$this->entityRepo->destroyBookshelf($bookshelf);
|
||||
return redirect('/shelves');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $slug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showRestrict(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('shelves.restrictions', [
|
||||
'shelf' => $bookshelf,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this bookshelf.
|
||||
* @param $slug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function restrict(string $slug, Request $request)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
|
||||
session()->flash('success', trans('entities.shelves_permissions_updated'));
|
||||
return redirect($bookshelf->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permissions of a bookshelf to the child books.
|
||||
* @param string $slug
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function copyPermissions(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
|
||||
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
|
||||
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
||||
return redirect($bookshelf->getUrl());
|
||||
}
|
||||
|
||||
}
|
|
@ -33,42 +33,42 @@ class HomeController extends Controller
|
|||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
||||
|
||||
|
||||
$customHomepage = false;
|
||||
$books = false;
|
||||
$booksViewType = false;
|
||||
|
||||
// Check book homepage
|
||||
$bookHomepageSetting = setting('app-book-homepage');
|
||||
if ($bookHomepageSetting) {
|
||||
$books = $this->entityRepo->getAllPaginated('book', 18);
|
||||
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
|
||||
} else {
|
||||
// Check custom homepage
|
||||
$homepageSetting = setting('app-homepage');
|
||||
if ($homepageSetting) {
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
}
|
||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
||||
$homepageOption = setting('app-homepage-type', 'default');
|
||||
if (!in_array($homepageOption, $homepageOptions)) {
|
||||
$homepageOption = 'default';
|
||||
}
|
||||
|
||||
$view = 'home';
|
||||
if ($bookHomepageSetting) {
|
||||
$view = 'home-book';
|
||||
} else if ($customHomepage) {
|
||||
$view = 'home-custom';
|
||||
}
|
||||
|
||||
return view('common/' . $view, [
|
||||
$commonData = [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages,
|
||||
'customHomepage' => $customHomepage,
|
||||
'books' => $books,
|
||||
'booksViewType' => $booksViewType
|
||||
]);
|
||||
];
|
||||
|
||||
if ($homepageOption === 'bookshelves') {
|
||||
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
|
||||
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
|
||||
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
|
||||
return view('common.home-shelves', $data);
|
||||
}
|
||||
|
||||
if ($homepageOption === 'books') {
|
||||
$books = $this->entityRepo->getAllPaginated('book', 18);
|
||||
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
|
||||
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
|
||||
return view('common.home-book', $data);
|
||||
}
|
||||
|
||||
if ($homepageOption === 'page') {
|
||||
$homepageSetting = setting('app-homepage', '0:');
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
|
||||
}
|
||||
|
||||
return view('common.home', $commonData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -252,7 +252,7 @@ class UserController extends Controller
|
|||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$viewType = $request->get('book_view_type');
|
||||
$viewType = $request->get('view_type');
|
||||
if (!in_array($viewType, ['grid', 'list'])) {
|
||||
$viewType = 'list';
|
||||
}
|
||||
|
@ -262,4 +262,27 @@ class UserController extends Controller
|
|||
|
||||
return redirect()->back(302, [], "/settings/users/$id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred shelf-list display setting.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function switchShelfView($id, Request $request)
|
||||
{
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$viewType = $request->get('view_type');
|
||||
if (!in_array($viewType, ['grid', 'list'])) {
|
||||
$viewType = 'list';
|
||||
}
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
setting()->putUser($user, 'bookshelves_view_type', $viewType);
|
||||
|
||||
return redirect()->back(302, [], "/settings/users/$id");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Bookshelf;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
|
@ -18,6 +19,10 @@ use Illuminate\Support\Collection;
|
|||
|
||||
class EntityRepo
|
||||
{
|
||||
/**
|
||||
* @var Bookshelf
|
||||
*/
|
||||
public $bookshelf;
|
||||
|
||||
/**
|
||||
* @var Book $book
|
||||
|
@ -67,6 +72,7 @@ class EntityRepo
|
|||
|
||||
/**
|
||||
* EntityRepo constructor.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
|
@ -77,6 +83,7 @@ class EntityRepo
|
|||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(
|
||||
Bookshelf $bookshelf,
|
||||
Book $book,
|
||||
Chapter $chapter,
|
||||
Page $page,
|
||||
|
@ -86,11 +93,13 @@ class EntityRepo
|
|||
TagRepo $tagRepo,
|
||||
SearchService $searchService
|
||||
) {
|
||||
$this->bookshelf = $bookshelf;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
$this->entities = [
|
||||
'bookshelf' => $this->bookshelf,
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
|
@ -331,6 +340,17 @@ class EntityRepo
|
|||
->skip($count * $page)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getBookshelfChildren(Bookshelf $bookshelf)
|
||||
{
|
||||
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child objects of a book.
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
|
@ -533,6 +553,28 @@ class EntityRepo
|
|||
return $entityModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the books assigned to a shelf from a comma-separated list
|
||||
* of book IDs.
|
||||
* @param Bookshelf $shelf
|
||||
* @param string $books
|
||||
*/
|
||||
public function updateShelfBooks(Bookshelf $shelf, string $books)
|
||||
{
|
||||
$ids = explode(',', $books);
|
||||
|
||||
// Check books exist and match ordering
|
||||
$bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
|
||||
$syncData = [];
|
||||
foreach ($ids as $index => $id) {
|
||||
if ($bookIds->contains($id)) {
|
||||
$syncData[$id] = ['order' => $index];
|
||||
}
|
||||
}
|
||||
|
||||
$shelf->books()->sync($syncData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the book that an entity belongs to.
|
||||
* @param string $type
|
||||
|
@ -1154,9 +1196,22 @@ class EntityRepo
|
|||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a bookshelf instance
|
||||
* @param Bookshelf $shelf
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyBookshelf(Bookshelf $shelf)
|
||||
{
|
||||
$this->destroyEntityCommonRelations($shelf);
|
||||
$shelf->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the provided book and all its child entities.
|
||||
* @param Book $book
|
||||
* @throws NotifyException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyBook(Book $book)
|
||||
{
|
||||
|
@ -1166,17 +1221,14 @@ class EntityRepo
|
|||
foreach ($book->chapters as $chapter) {
|
||||
$this->destroyChapter($chapter);
|
||||
}
|
||||
\Activity::removeEntity($book);
|
||||
$book->views()->delete();
|
||||
$book->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($book);
|
||||
$this->searchService->deleteEntityTerms($book);
|
||||
$this->destroyEntityCommonRelations($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a chapter and its relations.
|
||||
* @param Chapter $chapter
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyChapter(Chapter $chapter)
|
||||
{
|
||||
|
@ -1186,11 +1238,7 @@ class EntityRepo
|
|||
$page->save();
|
||||
}
|
||||
}
|
||||
\Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($chapter);
|
||||
$this->searchService->deleteEntityTerms($chapter);
|
||||
$this->destroyEntityCommonRelations($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
|
@ -1198,23 +1246,18 @@ class EntityRepo
|
|||
* Destroy a given page along with its dependencies.
|
||||
* @param Page $page
|
||||
* @throws NotifyException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyPage(Page $page)
|
||||
{
|
||||
\Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->tags()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
$this->searchService->deleteEntityTerms($page);
|
||||
|
||||
// Check if set as custom homepage
|
||||
$customHome = setting('app-homepage', '0:');
|
||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||
}
|
||||
|
||||
$this->destroyEntityCommonRelations($page);
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
foreach ($page->attachments as $attachment) {
|
||||
|
@ -1223,4 +1266,46 @@ class EntityRepo
|
|||
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy or handle the common relations connected to an entity.
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function destroyEntityCommonRelations(Entity $entity)
|
||||
{
|
||||
\Activity::removeEntity($entity);
|
||||
$entity->views()->delete();
|
||||
$entity->permissions()->delete();
|
||||
$entity->tags()->delete();
|
||||
$entity->comments()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($entity);
|
||||
$this->searchService->deleteEntityTerms($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permissions of a bookshelf to all child books.
|
||||
* Returns the number of books that had permissions updated.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @return int
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function copyBookshelfPermissions(Bookshelf $bookshelf)
|
||||
{
|
||||
$shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$shelfBooks = $bookshelf->books()->get();
|
||||
$updatedBookCount = 0;
|
||||
|
||||
foreach ($shelfBooks as $book) {
|
||||
if (!userCan('restrictions-manage', $book)) continue;
|
||||
$book->permissions()->delete();
|
||||
$book->restricted = $bookshelf->restricted;
|
||||
$book->permissions()->createMany($shelfPermissions);
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
$updatedBookCount++;
|
||||
}
|
||||
|
||||
return $updatedBookCount;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ class PermissionsRepo
|
|||
|
||||
/**
|
||||
* Updates an existing role.
|
||||
* Ensure Admin role always has all permissions.
|
||||
* Ensure Admin role always have core permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
|
@ -90,13 +90,18 @@ class PermissionsRepo
|
|||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
$permissions = array_merge($permissions, [
|
||||
'users-manage',
|
||||
'user-roles-manage',
|
||||
'restrictions-manage-all',
|
||||
'restrictions-manage-own',
|
||||
'settings-manage',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Bookshelf;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\EntityPermission;
|
||||
|
@ -25,6 +26,7 @@ class PermissionService
|
|||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
public $bookshelf;
|
||||
|
||||
protected $db;
|
||||
|
||||
|
@ -38,22 +40,26 @@ class PermissionService
|
|||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Role $role
|
||||
* @param Connection $db
|
||||
* @param Bookshelf $bookshelf
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
public function __construct(
|
||||
JointPermission $jointPermission, EntityPermission $entityPermission, Role $role, Connection $db,
|
||||
Bookshelf $bookshelf, Book $book, Chapter $chapter, Page $page
|
||||
)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->bookshelf = $bookshelf;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
// TODO - Update so admin still goes through filters
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,6 +165,12 @@ class PermissionService
|
|||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -174,6 +186,20 @@ class PermissionService
|
|||
}]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $shelves
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
|
@ -204,6 +230,7 @@ class PermissionService
|
|||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
|
@ -214,7 +241,9 @@ class PermissionService
|
|||
return;
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
|
@ -226,13 +255,13 @@ class PermissionService
|
|||
}
|
||||
}
|
||||
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
|
@ -254,6 +283,12 @@ class PermissionService
|
|||
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -412,7 +447,7 @@ class PermissionService
|
|||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity->isA('book') || $entity->isA('bookshelf')) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
|
@ -484,11 +519,6 @@ class PermissionService
|
|||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return true;
|
||||
}
|
||||
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
|
@ -581,17 +611,16 @@ class PermissionService
|
|||
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
|
||||
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
|
||||
|
||||
if (!$this->isAdmin()) {
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
// Add joint permission filter
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
}
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
|
||||
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
|
||||
$this->clean();
|
||||
|
@ -619,11 +648,6 @@ class PermissionService
|
|||
});
|
||||
}
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
}
|
||||
|
@ -639,10 +663,6 @@ class PermissionService
|
|||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
@ -675,11 +695,6 @@ class PermissionService
|
|||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
|
@ -704,19 +719,6 @@ class PermissionService
|
|||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin.
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdmin()
|
||||
{
|
||||
if ($this->isAdminUser === null) {
|
||||
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
|
||||
}
|
||||
|
||||
return $this->isAdminUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return User
|
||||
|
|
|
@ -21,6 +21,14 @@ $factory->define(BookStack\User::class, function ($faker) {
|
|||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Bookshelf::class, function ($faker) {
|
||||
return [
|
||||
'name' => $faker->sentence,
|
||||
'slug' => str_random(10),
|
||||
'description' => $faker->paragraph
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Book::class, function ($faker) {
|
||||
return [
|
||||
'name' => $faker->sentence,
|
||||
|
|
|
@ -74,10 +74,6 @@ class CreateJointPermissionsTable extends Migration
|
|||
|
||||
// Update admin role with system name
|
||||
DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
|
||||
|
||||
// Generate the new entity jointPermissions
|
||||
$restrictionService = app(\BookStack\Services\PermissionService::class);
|
||||
$restrictionService->buildJointPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateBookshelvesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('bookshelves', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name', 200);
|
||||
$table->string('slug', 200);
|
||||
$table->text('description');
|
||||
$table->integer('created_by')->nullable()->default(null);
|
||||
$table->integer('updated_by')->nullable()->default(null);
|
||||
$table->boolean('restricted')->default(false);
|
||||
$table->integer('image_id')->nullable()->default(null);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('slug');
|
||||
$table->index('created_by');
|
||||
$table->index('updated_by');
|
||||
$table->index('restricted');
|
||||
});
|
||||
|
||||
Schema::create('bookshelves_books', function (Blueprint $table) {
|
||||
$table->integer('bookshelf_id')->unsigned();
|
||||
$table->integer('book_id')->unsigned();
|
||||
$table->integer('order')->unsigned();
|
||||
|
||||
$table->foreign('bookshelf_id')->references('id')->on('bookshelves')
|
||||
->onUpdate('cascade')->onDelete('cascade');
|
||||
$table->foreign('book_id')->references('id')->on('books')
|
||||
->onUpdate('cascade')->onDelete('cascade');
|
||||
|
||||
$table->primary(['bookshelf_id', 'book_id']);
|
||||
});
|
||||
|
||||
// Copy existing role permissions from Books
|
||||
$ops = ['View All', 'View Own', 'Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
|
||||
foreach ($ops as $op) {
|
||||
$dbOpName = strtolower(str_replace(' ', '-', $op));
|
||||
$roleIdsWithBookPermission = DB::table('role_permissions')
|
||||
->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')
|
||||
->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')
|
||||
->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');
|
||||
|
||||
$permId = DB::table('role_permissions')->insertGetId([
|
||||
'name' => 'bookshelf-' . $dbOpName,
|
||||
'display_name' => $op . ' ' . 'BookShelves',
|
||||
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
|
||||
]);
|
||||
|
||||
$rowsToInsert = $roleIdsWithBookPermission->map(function($roleId) use ($permId) {
|
||||
return [
|
||||
'role_id' => $roleId,
|
||||
'permission_id' => $permId
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
// Assign view permission to all current roles
|
||||
DB::table('permission_role')->insert($rowsToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Drop created permissions
|
||||
$ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own'];
|
||||
|
||||
$permissionIds = DB::table('role_permissions')->whereIn('name', $ops)
|
||||
->get(['id'])->pluck('id')->toArray();
|
||||
DB::table('permission_role')->whereIn('permission_id', $permissionIds)->delete();
|
||||
DB::table('role_permissions')->whereIn('id', $permissionIds)->delete();
|
||||
|
||||
// Drop shelves table
|
||||
Schema::dropIfExists('bookshelves_books');
|
||||
Schema::dropIfExists('bookshelves');
|
||||
|
||||
// Drop related polymorphic items
|
||||
DB::table('activities')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
|
||||
DB::table('views')->where('viewable_type', '=', 'BookStack\Bookshelf')->delete();
|
||||
DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Bookshelf')->delete();
|
||||
DB::table('tags')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
|
||||
DB::table('search_terms')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
|
||||
DB::table('comments')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
|
||||
}
|
||||
}
|
|
@ -21,23 +21,29 @@ class DummyContentSeeder extends Seeder
|
|||
$role = \BookStack\Role::getRole('viewer');
|
||||
$viewerUser->attachRole($role);
|
||||
|
||||
factory(\BookStack\Book::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
|
||||
->each(function($book) use ($editorUser) {
|
||||
$chapters = factory(\BookStack\Chapter::class, 3)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
|
||||
->each(function($chapter) use ($editorUser, $book){
|
||||
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
|
||||
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
|
||||
|
||||
factory(\BookStack\Book::class, 5)->create($byData)
|
||||
->each(function($book) use ($editorUser, $byData) {
|
||||
$chapters = factory(\BookStack\Chapter::class, 3)->create($byData)
|
||||
->each(function($chapter) use ($editorUser, $book, $byData){
|
||||
$pages = factory(\BookStack\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
|
||||
$chapter->pages()->saveMany($pages);
|
||||
});
|
||||
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
|
||||
$pages = factory(\BookStack\Page::class, 3)->make($byData);
|
||||
$book->chapters()->saveMany($chapters);
|
||||
$book->pages()->saveMany($pages);
|
||||
});
|
||||
|
||||
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
|
||||
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
|
||||
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
|
||||
$largeBook = factory(\BookStack\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
|
||||
$pages = factory(\BookStack\Page::class, 200)->make($byData);
|
||||
$chapters = factory(\BookStack\Chapter::class, 50)->make($byData);
|
||||
$largeBook->pages()->saveMany($pages);
|
||||
$largeBook->chapters()->saveMany($chapters);
|
||||
|
||||
$shelves = factory(\BookStack\Bookshelf::class, 10)->create($byData);
|
||||
$largeBook->shelves()->attach($shelves->pluck('id'));
|
||||
|
||||
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
|
||||
app(\BookStack\Services\SearchService::class)->indexAllEntities();
|
||||
}
|
||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -5857,6 +5857,21 @@
|
|||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
|
||||
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
|
||||
},
|
||||
"jquery-sortable": {
|
||||
"version": "0.9.13",
|
||||
"resolved": "https://registry.npmjs.org/jquery-sortable/-/jquery-sortable-0.9.13.tgz",
|
||||
"integrity": "sha1-HL+2VQE6B0c3BXHwbiL1JKAP+6I=",
|
||||
"requires": {
|
||||
"jquery": "^2.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"jquery": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
|
||||
"integrity": "sha1-LInWiJterFIqfuoywUUhVZxsvwI="
|
||||
}
|
||||
}
|
||||
},
|
||||
"js-base64": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"codemirror": "^5.26.0",
|
||||
"dropzone": "^5.4.0",
|
||||
"jquery": "^3.3.1",
|
||||
"jquery-sortable": "^0.9.13",
|
||||
"markdown-it": "^8.3.1",
|
||||
"markdown-it-task-lists": "^2.0.0",
|
||||
"vue": "^2.2.6",
|
||||
|
|
2
resources/assets/icons/bookshelf.svg
Normal file
2
resources/assets/icons/bookshelf.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 375 B |
22
resources/assets/js/components/homepage-control.js
Normal file
22
resources/assets/js/components/homepage-control.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
|
||||
class HomepageControl {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.typeControl = elem.querySelector('[name="setting-app-homepage-type"]');
|
||||
this.pagePickerContainer = elem.querySelector('[page-picker-container]');
|
||||
|
||||
this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this));
|
||||
this.controlPagePickerVisibility();
|
||||
}
|
||||
|
||||
controlPagePickerVisibility() {
|
||||
const showPagePicker = this.typeControl.value === 'page';
|
||||
this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = HomepageControl;
|
|
@ -18,6 +18,8 @@ let componentMapping = {
|
|||
'collapsible': require('./collapsible'),
|
||||
'toggle-switch': require('./toggle-switch'),
|
||||
'page-display': require('./page-display'),
|
||||
'shelf-sort': require('./shelf-sort'),
|
||||
'homepage-control': require('./homepage-control'),
|
||||
};
|
||||
|
||||
window.components = {};
|
||||
|
|
|
@ -15,18 +15,20 @@ class PagePicker {
|
|||
}
|
||||
|
||||
setupListeners() {
|
||||
// Select click
|
||||
this.selectButton.addEventListener('click', event => {
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
this.setValue(entity.id, entity.name);
|
||||
});
|
||||
});
|
||||
this.selectButton.addEventListener('click', this.showPopup.bind(this));
|
||||
this.display.parentElement.addEventListener('click', this.showPopup.bind(this));
|
||||
|
||||
this.resetButton.addEventListener('click', event => {
|
||||
this.setValue('', '');
|
||||
});
|
||||
}
|
||||
|
||||
showPopup() {
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
this.setValue(entity.id, entity.name);
|
||||
});
|
||||
}
|
||||
|
||||
setValue(value, name) {
|
||||
this.value = value;
|
||||
this.input.value = value;
|
||||
|
|
71
resources/assets/js/components/shelf-sort.js
Normal file
71
resources/assets/js/components/shelf-sort.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
|
||||
class ShelfSort {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.sortGroup = this.initSortable();
|
||||
this.input = document.getElementById('books-input');
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
initSortable() {
|
||||
const sortable = require('jquery-sortable');
|
||||
const placeHolderContent = this.getPlaceholderHTML();
|
||||
|
||||
return $('.scroll-box').sortable({
|
||||
group: 'shelf-books',
|
||||
exclude: '.instruction,.scroll-box-placeholder',
|
||||
containerSelector: 'div.scroll-box',
|
||||
itemSelector: '.scroll-box-item',
|
||||
placeholder: placeHolderContent,
|
||||
onDrop: this.onDrop.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
this.elem.addEventListener('click', event => {
|
||||
const sortItem = event.target.closest('.scroll-box-item:not(.instruction)');
|
||||
if (sortItem) {
|
||||
event.preventDefault();
|
||||
this.sortItemClick(sortItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a sort item is clicked.
|
||||
* @param {Element} sortItem
|
||||
*/
|
||||
sortItemClick(sortItem) {
|
||||
const lists = this.elem.querySelectorAll('.scroll-box');
|
||||
const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
|
||||
if (newList.length > 0) {
|
||||
newList[0].appendChild(sortItem);
|
||||
}
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
onDrop($item, container, _super) {
|
||||
this.onChange();
|
||||
_super($item, container);
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const data = this.sortGroup.sortable('serialize').get();
|
||||
this.input.value = data[0].map(item => item.id).join(',');
|
||||
const instruction = this.elem.querySelector('.scroll-box-item.instruction');
|
||||
instruction.parentNode.insertBefore(instruction, instruction.parentNode.children[0]);
|
||||
}
|
||||
|
||||
getPlaceholderHTML() {
|
||||
const placeHolder = document.querySelector('.scroll-box-placeholder');
|
||||
placeHolder.style.display = 'block';
|
||||
const placeHolderContent = placeHolder.outerHTML;
|
||||
placeHolder.style.display = 'none';
|
||||
return placeHolderContent;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = ShelfSort;
|
|
@ -192,8 +192,26 @@ div[class^="col-"] img {
|
|||
flex-direction: column;
|
||||
border: 1px solid #ddd;
|
||||
min-width: 100px;
|
||||
h2 {
|
||||
width: 100%;
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
h2 a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
}
|
||||
p {
|
||||
font-size: .85em;
|
||||
margin: 0;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
.grid-card-content {
|
||||
flex: 1;
|
||||
border-top: 0;
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
.grid-card-content, .grid-card-footer {
|
||||
padding: $-l;
|
||||
|
@ -203,6 +221,23 @@ div[class^="col-"] img {
|
|||
}
|
||||
}
|
||||
|
||||
.book-grid-item .grid-card-content h2 a {
|
||||
color: $color-book;
|
||||
fill: $color-book;
|
||||
}
|
||||
|
||||
.bookshelf-grid-item .grid-card-content h2 a {
|
||||
color: $color-bookshelf;
|
||||
fill: $color-bookshelf;
|
||||
}
|
||||
|
||||
.book-grid-item .grid-card-footer {
|
||||
p.small {
|
||||
font-size: .8em;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include smaller-than($m) {
|
||||
.grid.third {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
|
@ -412,32 +412,3 @@ ul.pagination {
|
|||
}
|
||||
}
|
||||
|
||||
.book-grid-item .grid-card-content {
|
||||
border-top: 0;
|
||||
border-bottom-width: 2px;
|
||||
h2 {
|
||||
width: 100%;
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
h2 a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 1.2;
|
||||
color: #009688;;
|
||||
fill: #009688;;
|
||||
text-decoration: none;
|
||||
}
|
||||
p {
|
||||
font-size: .85em;
|
||||
margin: 0;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.book-grid-item .grid-card-footer {
|
||||
p.small {
|
||||
font-size: .8em;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
|
@ -281,6 +281,14 @@ p.secondary, p .secondary, span.secondary, .text-secondary {
|
|||
}
|
||||
}
|
||||
|
||||
.text-bookshelf {
|
||||
color: $color-bookshelf;
|
||||
fill: $color-bookshelf;
|
||||
&:hover {
|
||||
color: $color-bookshelf;
|
||||
fill: $color-bookshelf;
|
||||
}
|
||||
}
|
||||
.text-book {
|
||||
color: $color-book;
|
||||
fill: $color-book;
|
||||
|
|
|
@ -47,6 +47,7 @@ $warning: $secondary;
|
|||
$primary-faded: rgba(21, 101, 192, 0.15);
|
||||
|
||||
// Item Colors
|
||||
$color-bookshelf: #af5a5a;
|
||||
$color-book: #009688;
|
||||
$color-chapter: #ef7c3c;
|
||||
$color-page: $primary;
|
||||
|
|
|
@ -206,6 +206,12 @@ $btt-size: 40px;
|
|||
transition: all ease-in-out 120ms;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.compact {
|
||||
font-size: 10px;
|
||||
.entity-item-snippet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-list-item.selected {
|
||||
|
@ -214,6 +220,20 @@ $btt-size: 40px;
|
|||
}
|
||||
}
|
||||
|
||||
.scroll-box {
|
||||
max-height: 250px;
|
||||
overflow-y: scroll;
|
||||
border: 1px solid #DDD;
|
||||
border-radius: 3px;
|
||||
.scroll-box-item {
|
||||
padding: $-xs $-m;
|
||||
border-bottom: 1px solid #DDD;
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center-box {
|
||||
margin: $-xxl auto 0 auto;
|
||||
width: 420px;
|
||||
|
|
|
@ -37,6 +37,14 @@ return [
|
|||
'book_sort' => 'sorted book',
|
||||
'book_sort_notification' => 'Book Successfully Re-sorted',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created Bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf Successfully Created',
|
||||
'bookshelf_update' => 'updated bookshelf',
|
||||
'bookshelf_update_notification' => 'Bookshelf Successfully Updated',
|
||||
'bookshelf_delete' => 'deleted bookshelf',
|
||||
'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'commented on',
|
||||
];
|
||||
|
|
|
@ -52,6 +52,7 @@ return [
|
|||
'details' => 'Details',
|
||||
'grid_view' => 'Grid View',
|
||||
'list_view' => 'List View',
|
||||
'default' => 'Default',
|
||||
|
||||
/**
|
||||
* Header
|
||||
|
|
|
@ -64,6 +64,37 @@ return [
|
|||
'search_set_date' => 'Set Date',
|
||||
'search_update' => 'Update Search',
|
||||
|
||||
/**
|
||||
* Shelves
|
||||
*/
|
||||
'shelves' => 'Shelves',
|
||||
'shelves_long' => 'Bookshelves',
|
||||
'shelves_empty' => 'No shelves have been created',
|
||||
'shelves_create' => 'Create New Shelf',
|
||||
'shelves_popular' => 'Popular Shelves',
|
||||
'shelves_new' => 'New Shelves',
|
||||
'shelves_popular_empty' => 'The most popular shelves will appear here.',
|
||||
'shelves_new_empty' => 'The most recently created shelves will appear here.',
|
||||
'shelves_save' => 'Save Shelf',
|
||||
'shelves_books' => 'Books on this shelf',
|
||||
'shelves_add_books' => 'Add books to this shelf',
|
||||
'shelves_drag_books' => 'Drag books here to add them to this shelf',
|
||||
'shelves_empty_contents' => 'This shelf has no books assigned to it',
|
||||
'shelves_edit_and_assign' => 'Edit shelf to assign books',
|
||||
'shelves_edit_named' => 'Edit Bookshelf :name',
|
||||
'shelves_edit' => 'Edit Bookshelf',
|
||||
'shelves_delete' => 'Delete Bookshelf',
|
||||
'shelves_delete_named' => 'Delete Bookshelf :name',
|
||||
'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',
|
||||
'shelves_permissions' => 'Bookshelf Permissions',
|
||||
'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Bookshelf Permissions Active',
|
||||
'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
|
||||
'shelves_copy_permissions' => 'Copy Permissions',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
|
||||
|
||||
/**
|
||||
* Books
|
||||
*/
|
||||
|
@ -199,6 +230,7 @@ return [
|
|||
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
|
||||
'pages_specific' => 'Specific Page',
|
||||
|
||||
/**
|
||||
* Editor sidebar
|
||||
|
@ -206,6 +238,7 @@ return [
|
|||
'page_tags' => 'Page Tags',
|
||||
'chapter_tags' => 'Chapter Tags',
|
||||
'book_tags' => 'Book Tags',
|
||||
'shelf_tags' => 'Shelf Tags',
|
||||
'tag' => 'Tag',
|
||||
'tags' => 'Tags',
|
||||
'tag_value' => 'Tag Value (Optional)',
|
||||
|
|
|
@ -49,6 +49,7 @@ return [
|
|||
|
||||
// Entities
|
||||
'entity_not_found' => 'Entity not found',
|
||||
'bookshelf_not_found' => 'Bookshelf not found',
|
||||
'book_not_found' => 'Book not found',
|
||||
'page_not_found' => 'Page not found',
|
||||
'chapter_not_found' => 'Chapter not found',
|
||||
|
|
|
@ -32,9 +32,8 @@ return [
|
|||
'app_primary_color' => 'Application primary color',
|
||||
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
|
||||
'app_homepage' => 'Application Homepage',
|
||||
'app_homepage_desc' => 'Select a page to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
|
||||
'app_homepage_default' => 'Default homepage view chosen',
|
||||
'app_homepage_books' => 'Or select the books page as your homepage. This will override any page selected as your homepage.',
|
||||
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
|
||||
'app_homepage_select' => 'Select a page',
|
||||
'app_disable_comments' => 'Disable comments',
|
||||
'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.',
|
||||
|
||||
|
@ -91,6 +90,7 @@ return [
|
|||
'role_manage_settings' => 'Manage app settings',
|
||||
'role_asset' => 'Asset Permissions',
|
||||
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
|
||||
'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
|
||||
'role_all' => 'All',
|
||||
'role_own' => 'Own',
|
||||
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<header id="header">
|
||||
<div class="container fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<a href="{{ baseUrl('/') }}" class="logo">
|
||||
@if(setting('app-logo', '') !== 'none')
|
||||
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
|
||||
|
@ -43,7 +43,7 @@
|
|||
@endif
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<div class="float right">
|
||||
<div class="header-search">
|
||||
<form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
|
||||
|
@ -52,6 +52,9 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="links text-center">
|
||||
@if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
|
||||
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
|
||||
@endif
|
||||
<a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
|
||||
@if(signedInUser() && userCan('settings-manage'))
|
||||
<a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
|
||||
@if($booksViewType === 'list')
|
||||
<div class="container small">
|
||||
@else
|
||||
<div class="container">
|
||||
@endif
|
||||
<div class="container{{ $booksViewType === 'list' ? ' small' : '' }}">
|
||||
<h1>{{ trans('entities.books') }}</h1>
|
||||
@if(count($books) > 0)
|
||||
@if($booksViewType === 'list')
|
||||
|
@ -25,7 +21,7 @@
|
|||
@else
|
||||
<p class="text-muted">{{ trans('entities.books_empty') }}</p>
|
||||
@if(userCan('books-create-all'))
|
||||
<a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_one_now') }}</a>
|
||||
<a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_now') }}</a>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
|
@ -25,7 +25,7 @@
|
|||
<a dropdown-toggle class="text-primary text-button">@icon('more'){{ trans('common.more') }}</a>
|
||||
<ul>
|
||||
@if(userCan('book-update', $book))
|
||||
<li><a href="{{$book->getEditUrl()}}" class="text-primary">@icon('edit'){{ trans('common.edit') }}</a></li>
|
||||
<li><a href="{{ $book->getUrl('/edit') }}" class="text-primary">@icon('edit'){{ trans('common.edit') }}</a></li>
|
||||
<li><a href="{{ $book->getUrl('/sort') }}" class="text-primary">@icon('sort'){{ trans('common.sort') }}</a></li>
|
||||
@endif
|
||||
@if(userCan('restrictions-manage', $book))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-book-view") }}" method="POST" class="inline">
|
||||
{!! csrf_field() !!}
|
||||
{!! method_field('PATCH') !!}
|
||||
<input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="book_view_type">
|
||||
<input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="view_type">
|
||||
@if ($booksViewType === 'list')
|
||||
<button type="submit" class="text-pos text-button">@icon('grid'){{ trans('common.grid_view') }}</button>
|
||||
@else
|
||||
|
|
18
resources/views/common/home-shelves.blade.php
Normal file
18
resources/views/common/home-shelves.blade.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
@extends('sidebar-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-sm-6 faded">
|
||||
<div class="action-buttons text-left">
|
||||
<a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button">@icon('expand-text'){{ trans('common.toggle_details') }}</a>
|
||||
@include('shelves/view-toggle', ['shelvesViewType' => $shelvesViewType])
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('sidebar')
|
||||
@include('common/home-sidebar')
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
@include('shelves/list', ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType])
|
||||
@stop
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
@section('body')
|
||||
|
||||
<div class="container">
|
||||
<div class="container" id="home-default">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-4">
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
@include('books/list-item', ['book' => $entity])
|
||||
@elseif($entity->isA('chapter'))
|
||||
@include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true])
|
||||
@elseif($entity->isA('bookshelf'))
|
||||
@include('shelves/list-item', ['bookshelf' => $entity])
|
||||
@endif
|
||||
|
||||
@if($index !== count($entities) - 1)
|
||||
|
|
|
@ -76,12 +76,22 @@
|
|||
<input type="text" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
|
||||
<input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
|
||||
</div>
|
||||
<div class="form-group" id="homepage-control">
|
||||
<div homepage-control class="form-group" id="homepage-control">
|
||||
<label for="setting-app-homepage">{{ trans('settings.app_homepage') }}</label>
|
||||
<p class="small">{{ trans('settings.app_homepage_desc') }}</p>
|
||||
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_default'), 'value' => setting('app-homepage')])
|
||||
<p class="small">{{ trans('settings.app_homepage_books') }}</p>
|
||||
@include('components.toggle-switch', ['name' => 'setting-app-book-homepage', 'value' => setting('app-book-homepage')])
|
||||
|
||||
<select name="setting-app-homepage-type" id="setting-app-homepage-type">
|
||||
<option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
|
||||
<option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
|
||||
<option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value="bookshelves">{{ trans('entities.shelves') }}</option>
|
||||
<option @if(setting('app-homepage-type') === 'page') selected @endif value="page">{{ trans('entities.pages_specific') }}</option>
|
||||
</select>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div page-picker-container style="display: none;">
|
||||
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
<h5>{{ trans('settings.role_asset') }}</h5>
|
||||
<p>{{ trans('settings.role_asset_desc') }}</p>
|
||||
|
||||
@if (isset($role) && $role->system_name === 'admin')
|
||||
<p>{{ trans('settings.role_asset_admins') }}</p>
|
||||
@endif
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th width="20%"></th>
|
||||
|
@ -44,6 +48,24 @@
|
|||
<th width="20%">{{ trans('common.edit') }}</th>
|
||||
<th width="20%">{{ trans('common.delete') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ trans('entities.shelves_long') }}</td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-create-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-view-own']) {{ trans('settings.role_own') }}</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-view-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-update-own']) {{ trans('settings.role_own') }}</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-update-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-delete-own']) {{ trans('settings.role_own') }}</label>
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-delete-all']) {{ trans('settings.role_all') }}</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ trans('entities.books') }}</td>
|
||||
<td>
|
||||
|
|
3
resources/views/shelves/_breadcrumbs.blade.php
Normal file
3
resources/views/shelves/_breadcrumbs.blade.php
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="breadcrumbs">
|
||||
<a href="{{$shelf->getUrl()}}" class="text-bookshelf text-button">@icon('bookshelf'){{ $shelf->getShortName() }}</a>
|
||||
</div>
|
31
resources/views/shelves/create.blade.php
Normal file
31
resources/views/shelves/create.blade.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-sm-8 faded">
|
||||
<div class="breadcrumbs">
|
||||
<a href="{{ baseUrl('/shelves') }}" class="text-button">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
|
||||
<span class="sep">»</span>
|
||||
<a href="{{ baseUrl('/create-shelf') }}" class="text-button">@icon('add'){{ trans('entities.shelves_create') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small">
|
||||
<p> </p>
|
||||
<div class="card">
|
||||
<h3>@icon('add') {{ trans('entities.shelves_create') }}</h3>
|
||||
<div class="body">
|
||||
<form action="{{ baseUrl("/shelves") }}" method="POST" enctype="multipart/form-data">
|
||||
@include('shelves/form', ['shelf' => null, 'books' => $books])
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="margin-top large"><br></p>
|
||||
|
||||
@include('components.image-manager', ['imageType' => 'cover'])
|
||||
|
||||
@stop
|
30
resources/views/shelves/delete.blade.php
Normal file
30
resources/views/shelves/delete.blade.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-sm-12 faded">
|
||||
@include('shelves._breadcrumbs', ['shelf' => $shelf])
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small">
|
||||
<p> </p>
|
||||
<div class="card">
|
||||
<h3>@icon('delete') {{ trans('entities.shelves_delete') }}</h3>
|
||||
<div class="body">
|
||||
<p>{{ trans('entities.shelves_delete_explain', ['name' => $shelf->name]) }}</p>
|
||||
<p class="text-neg">{{ trans('entities.shelves_delete_confirmation') }}</p>
|
||||
|
||||
<form action="{{ $shelf->getUrl() }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
|
||||
<a href="{{ $shelf->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" class="button">{{ trans('common.confirm') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stop
|
24
resources/views/shelves/edit.blade.php
Normal file
24
resources/views/shelves/edit.blade.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-sm-12 faded">
|
||||
@include('shelves._breadcrumbs', ['shelf' => $shelf])
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small">
|
||||
<p> </p>
|
||||
<div class="card">
|
||||
<h3>@icon('edit') {{ trans('entities.shelves_edit') }}</h3>
|
||||
<div class="body">
|
||||
<form action="{{ $shelf->getUrl() }}" method="POST">
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
@include('shelves/form', ['model' => $shelf])
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include('components.image-manager', ['imageType' => 'cover'])
|
||||
@stop
|
80
resources/views/shelves/export.blade.php
Normal file
80
resources/views/shelves/export.blade.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>{{ $book->name }}</title>
|
||||
|
||||
<style>
|
||||
@if (!app()->environment('testing'))
|
||||
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
|
||||
@endif
|
||||
.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>
|
84
resources/views/shelves/form.blade.php
Normal file
84
resources/views/shelves/form.blade.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
|
||||
{{ csrf_field() }}
|
||||
<div class="form-group title-input">
|
||||
<label for="name">{{ trans('common.name') }}</label>
|
||||
@include('form/text', ['name' => 'name'])
|
||||
</div>
|
||||
|
||||
<div class="form-group description-input">
|
||||
<label for="description">{{ trans('common.description') }}</label>
|
||||
@include('form/textarea', ['name' => 'description'])
|
||||
</div>
|
||||
|
||||
<div shelf-sort class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="books">{{ trans('entities.shelves_books') }}</label>
|
||||
<input type="hidden" id="books-input" name="books"
|
||||
value="{{ isset($shelf) ? $shelf->books->implode('id', ',') : '' }}">
|
||||
<div class="scroll-box">
|
||||
<div class="scroll-box-item text-small text-muted instruction">
|
||||
{{ trans('entities.shelves_drag_books') }}
|
||||
</div>
|
||||
<div class="scroll-box-item scroll-box-placeholder" style="display: none;">
|
||||
<a href="#" class="text-muted">@icon('book') ...</a>
|
||||
</div>
|
||||
@if (isset($shelfBooks) && count($shelfBooks) > 0)
|
||||
@foreach ($shelfBooks as $book)
|
||||
<div data-id="{{ $book->id }}" class="scroll-box-item">
|
||||
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="books">{{ trans('entities.shelves_add_books') }}</label>
|
||||
<div class="scroll-box">
|
||||
@foreach ($books as $book)
|
||||
<div data-id="{{ $book->id }}" class="scroll-box-item">
|
||||
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-group" collapsible id="logo-control">
|
||||
<div class="collapse-title text-primary" collapsible-trigger>
|
||||
<label for="user-avatar">{{ trans('common.cover_image') }}</label>
|
||||
</div>
|
||||
<div class="collapse-content" collapsible-content>
|
||||
<p class="small">{{ trans('common.cover_image_description') }}</p>
|
||||
|
||||
@include('components.image-picker', [
|
||||
'resizeHeight' => '512',
|
||||
'resizeWidth' => '512',
|
||||
'showRemove' => false,
|
||||
'defaultImage' => baseUrl('/book_default_cover.png'),
|
||||
'currentImage' => isset($shelf) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') ,
|
||||
'currentId' => isset($shelf) && $shelf->image_id ? $shelf->image_id : 0,
|
||||
'name' => 'image_id',
|
||||
'imageClass' => 'cover'
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" collapsible id="tags-control">
|
||||
<div class="collapse-title text-primary" collapsible-trigger>
|
||||
<label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
|
||||
</div>
|
||||
<div class="collapse-content" collapsible-content>
|
||||
@include('components.tag-manager', ['entity' => $shelf ?? null, 'entityType' => 'bookshelf'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ isset($shelf) ? $shelf->getUrl() : baseUrl('/shelves') }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" class="button pos">{{ trans('entities.shelves_save') }}</button>
|
||||
</div>
|
18
resources/views/shelves/grid-item.blade.php
Normal file
18
resources/views/shelves/grid-item.blade.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="bookshelf-grid-item grid-card" data-entity-type="bookshelf" data-entity-id="{{$bookshelf->id}}">
|
||||
<div class="featured-image-container">
|
||||
<a href="{{$bookshelf->getUrl()}}" title="{{$bookshelf->name}}">
|
||||
<img src="{{$bookshelf->getBookCover()}}" alt="{{$bookshelf->name}}">
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid-card-content">
|
||||
<h2><a class="break-text" href="{{$bookshelf->getUrl()}}" title="{{$bookshelf->name}}">{{$bookshelf->getShortName(35)}}</a></h2>
|
||||
@if(isset($bookshelf->searchSnippet))
|
||||
<p >{!! $bookshelf->searchSnippet !!}</p>
|
||||
@else
|
||||
<p >{{ $bookshelf->getExcerpt(130) }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="grid-card-footer text-muted text-small">
|
||||
<span>@include('partials.entity-meta', ['entity' => $bookshelf])</span>
|
||||
</div>
|
||||
</div>
|
48
resources/views/shelves/index.blade.php
Normal file
48
resources/views/shelves/index.blade.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
@extends('sidebar-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-xs-6 faded">
|
||||
<div class="action-buttons text-left">
|
||||
@include('shelves/view-toggle', ['shelvesViewType' => $shelvesViewType])
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 faded">
|
||||
<div class="action-buttons">
|
||||
@if($currentUser->can('bookshelf-create-all'))
|
||||
<a href="{{ baseUrl("/create-shelf") }}" class="text-pos text-button">@icon('add'){{ trans('entities.shelves_create') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('sidebar')
|
||||
@if($recents)
|
||||
<div id="recents" class="card">
|
||||
<h3>@icon('view') {{ trans('entities.recently_viewed') }}</h3>
|
||||
@include('partials/entity-list', ['entities' => $recents, 'style' => 'compact'])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div id="popular" class="card">
|
||||
<h3>@icon('popular') {{ trans('entities.shelves_popular') }}</h3>
|
||||
@if(count($popular) > 0)
|
||||
@include('partials/entity-list', ['entities' => $popular, 'style' => 'compact'])
|
||||
@else
|
||||
<div class="body text-muted">{{ trans('entities.shelves_popular_empty') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div id="new" class="card">
|
||||
<h3>@icon('star-circle') {{ trans('entities.shelves_new') }}</h3>
|
||||
@if(count($new) > 0)
|
||||
@include('partials/entity-list', ['entities' => $new, 'style' => 'compact'])
|
||||
@else
|
||||
<div class="body text-muted">{{ trans('entities.shelves_new_empty') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
@include('shelves/list', ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType])
|
||||
<p><br></p>
|
||||
@stop
|
10
resources/views/shelves/list-item.blade.php
Normal file
10
resources/views/shelves/list-item.blade.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="shelf entity-list-item" data-entity-type="bookshelf" data-entity-id="{{$bookshelf->id}}">
|
||||
<h4 class="text-shelf"><a class="text-bookshelf entity-list-item-link" href="{{$bookshelf->getUrl()}}">@icon('bookshelf')<span class="entity-list-item-name break-text">{{$bookshelf->name}}</span></a></h4>
|
||||
<div class="entity-item-snippet">
|
||||
@if(isset($bookshelf->searchSnippet))
|
||||
<p class="text-muted break-text">{!! $bookshelf->searchSnippet !!}</p>
|
||||
@else
|
||||
<p class="text-muted break-text">{{ $bookshelf->getExcerpt() }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
26
resources/views/shelves/list.blade.php
Normal file
26
resources/views/shelves/list.blade.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
|
||||
<div class="container{{ $shelvesViewType === 'list' ? ' small' : '' }}">
|
||||
<h1>{{ trans('entities.shelves') }}</h1>
|
||||
@if(count($shelves) > 0)
|
||||
@if($shelvesViewType === 'grid')
|
||||
<div class="grid third">
|
||||
@foreach($shelves as $key => $shelf)
|
||||
@include('shelves/grid-item', ['bookshelf' => $shelf])
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
@foreach($shelves as $shelf)
|
||||
@include('shelves/list-item', ['bookshelf' => $shelf])
|
||||
<hr>
|
||||
@endforeach
|
||||
@endif
|
||||
<div>
|
||||
{!! $shelves->render() !!}
|
||||
</div>
|
||||
@else
|
||||
<p class="text-muted">{{ trans('entities.shelves_empty') }}</p>
|
||||
@if(userCan('bookshelf-create-all'))
|
||||
<a href="{{ baseUrl("/create-shelf") }}" class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
34
resources/views/shelves/restrictions.blade.php
Normal file
34
resources/views/shelves/restrictions.blade.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-sm-12 faded">
|
||||
@include('shelves._breadcrumbs', ['shelf' => $shelf])
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small">
|
||||
<p> </p>
|
||||
<div class="card">
|
||||
<h3>@icon('lock') {{ trans('entities.shelves_permissions') }}</h3>
|
||||
<div class="body">
|
||||
@include('form/restriction-form', ['model' => $shelf])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<div class="card">
|
||||
<h3>@icon('copy') {{ trans('entities.shelves_copy_permissions_to_books') }}</h3>
|
||||
<div class="body">
|
||||
<p>{{ trans('entities.shelves_copy_permissions_explain') }}</p>
|
||||
<form action="{{ $shelf->getUrl('/copy-permissions') }}" method="post" class="text-right">
|
||||
{{ csrf_field() }}
|
||||
<button class="button">{{ trans('entities.shelves_copy_permissions') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stop
|
88
resources/views/shelves/show.blade.php
Normal file
88
resources/views/shelves/show.blade.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
@extends('sidebar-layout')
|
||||
|
||||
@section('toolbar')
|
||||
<div class="col-sm-6 col-xs-1 faded">
|
||||
@include('shelves._breadcrumbs', ['shelf' => $shelf])
|
||||
</div>
|
||||
<div class="col-sm-6 col-xs-11">
|
||||
<div class="action-buttons faded">
|
||||
@if(userCan('bookshelf-update', $shelf))
|
||||
<a href="{{ $shelf->getUrl('/edit') }}" class="text-button text-primary">@icon('edit'){{ trans('common.edit') }}</a>
|
||||
@endif
|
||||
@if(userCan('restrictions-manage', $shelf) || userCan('bookshelf-delete', $shelf))
|
||||
<div dropdown class="dropdown-container">
|
||||
<a dropdown-toggle class="text-primary text-button">@icon('more'){{ trans('common.more') }}</a>
|
||||
<ul>
|
||||
@if(userCan('restrictions-manage', $shelf))
|
||||
<li><a href="{{ $shelf->getUrl('/permissions') }}" class="text-primary">@icon('lock'){{ trans('entities.permissions') }}</a></li>
|
||||
@endif
|
||||
@if(userCan('bookshelf-delete', $shelf))
|
||||
<li><a href="{{ $shelf->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('common.delete') }}</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
||||
@section('sidebar')
|
||||
|
||||
@if($shelf->tags->count() > 0)
|
||||
<section>
|
||||
@include('components.tag-list', ['entity' => $shelf])
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<div class="card entity-details">
|
||||
<h3>@icon('info') {{ trans('common.details') }}</h3>
|
||||
<div class="body text-small text-muted blended-links">
|
||||
@include('partials.entity-meta', ['entity' => $shelf])
|
||||
@if($shelf->restricted)
|
||||
<div class="active-restriction">
|
||||
@if(userCan('restrictions-manage', $shelf))
|
||||
<a href="{{ $shelf->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.shelves_permissions_active') }}</a>
|
||||
@else
|
||||
@icon('lock'){{ trans('entities.shelves_permissions_active') }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(count($activity) > 0)
|
||||
<div class="activity card">
|
||||
<h3>@icon('time') {{ trans('entities.recent_activity') }}</h3>
|
||||
@include('partials/activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
@endif
|
||||
@stop
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small nopad">
|
||||
<h1 class="break-text">{{$shelf->name}}</h1>
|
||||
<div class="book-content">
|
||||
<p class="text-muted">{!! nl2br(e($shelf->description)) !!}</p>
|
||||
@if(count($books) > 0)
|
||||
<div class="page-list">
|
||||
<hr>
|
||||
@foreach($books as $book)
|
||||
@include('books/list-item', ['book' => $book])
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p>
|
||||
<hr>
|
||||
<span class="text-muted italic">{{ trans('entities.shelves_empty_contents') }}</span>
|
||||
@if(userCan('bookshelf-create', $shelf))
|
||||
<br>
|
||||
<a href="{{ $shelf->getUrl('/edit') }}" class="button outline bookshelf">{{ trans('entities.shelves_edit_and_assign') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@stop
|
10
resources/views/shelves/view-toggle.blade.php
Normal file
10
resources/views/shelves/view-toggle.blade.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-shelf-view") }}" method="POST" class="inline">
|
||||
{!! csrf_field() !!}
|
||||
{!! method_field('PATCH') !!}
|
||||
<input type="hidden" value="{{ $shelvesViewType === 'list'? 'grid' : 'list' }}" name="view_type">
|
||||
@if ($shelvesViewType === 'list')
|
||||
<button type="submit" class="text-pos text-button">@icon('grid'){{ trans('common.grid_view') }}</button>
|
||||
@else
|
||||
<button type="submit" class="text-pos text-button">@icon('list'){{ trans('common.list_view') }}</button>
|
||||
@endif
|
||||
</form>
|
|
@ -14,6 +14,21 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
|
||||
});
|
||||
|
||||
// Shelves
|
||||
Route::get('/create-shelf', 'BookshelfController@create');
|
||||
Route::group(['prefix' => 'shelves'], function() {
|
||||
Route::get('/', 'BookshelfController@index');
|
||||
Route::post('/', 'BookshelfController@store');
|
||||
Route::get('/{slug}/edit', 'BookshelfController@edit');
|
||||
Route::get('/{slug}/delete', 'BookshelfController@showDelete');
|
||||
Route::get('/{slug}', 'BookshelfController@show');
|
||||
Route::put('/{slug}', 'BookshelfController@update');
|
||||
Route::delete('/{slug}', 'BookshelfController@destroy');
|
||||
Route::get('/{slug}/permissions', 'BookshelfController@showRestrict');
|
||||
Route::put('/{slug}/permissions', 'BookshelfController@restrict');
|
||||
Route::post('/{slug}/copy-permissions', 'BookshelfController@copyPermissions');
|
||||
});
|
||||
|
||||
Route::get('/create-book', 'BookController@create');
|
||||
Route::group(['prefix' => 'books'], function () {
|
||||
|
||||
|
@ -160,6 +175,7 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
Route::get('/users/create', 'UserController@create');
|
||||
Route::get('/users/{id}/delete', 'UserController@delete');
|
||||
Route::patch('/users/{id}/switch-book-view', 'UserController@switchBookView');
|
||||
Route::patch('/users/{id}/switch-shelf-view', 'UserController@switchShelfView');
|
||||
Route::post('/users/create', 'UserController@store');
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
|
|
170
tests/Entity/BookShelfTest.php
Normal file
170
tests/Entity/BookShelfTest.php
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Bookshelf;
|
||||
|
||||
class BookShelfTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_shelves_shows_in_header_if_have_view_permissions()
|
||||
{
|
||||
$viewer = $this->getViewer();
|
||||
$resp = $this->actingAs($viewer)->get('/');
|
||||
$resp->assertElementContains('header', 'Shelves');
|
||||
|
||||
$viewer->roles()->delete();
|
||||
$this->giveUserPermissions($viewer);
|
||||
$resp = $this->actingAs($viewer)->get('/');
|
||||
$resp->assertElementNotContains('header', 'Shelves');
|
||||
|
||||
$this->giveUserPermissions($viewer, ['bookshelf-view-all']);
|
||||
$resp = $this->actingAs($viewer)->get('/');
|
||||
$resp->assertElementContains('header', 'Shelves');
|
||||
|
||||
$viewer->roles()->delete();
|
||||
$this->giveUserPermissions($viewer, ['bookshelf-view-own']);
|
||||
$resp = $this->actingAs($viewer)->get('/');
|
||||
$resp->assertElementContains('header', 'Shelves');
|
||||
}
|
||||
|
||||
public function test_shelves_page_contains_create_link()
|
||||
{
|
||||
$resp = $this->asEditor()->get('/shelves');
|
||||
$resp->assertElementContains('a', 'Create New Shelf');
|
||||
}
|
||||
|
||||
public function test_shelves_create()
|
||||
{
|
||||
$booksToInclude = Book::take(2)->get();
|
||||
$shelfInfo = [
|
||||
'name' => 'My test book' . str_random(4),
|
||||
'description' => 'Test book description ' . str_random(10)
|
||||
];
|
||||
$resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
|
||||
'books' => $booksToInclude->implode('id', ','),
|
||||
'tags' => [
|
||||
[
|
||||
'name' => 'Test Category',
|
||||
'value' => 'Test Tag Value',
|
||||
]
|
||||
],
|
||||
]));
|
||||
$resp->assertRedirect();
|
||||
$editorId = $this->getEditor()->id;
|
||||
$this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['created_by' => $editorId, 'updated_by' => $editorId]));
|
||||
|
||||
$shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first();
|
||||
$shelfPage = $this->get($shelf->getUrl());
|
||||
$shelfPage->assertSee($shelfInfo['name']);
|
||||
$shelfPage->assertSee($shelfInfo['description']);
|
||||
$shelfPage->assertElementContains('.tag-item', 'Test Category');
|
||||
$shelfPage->assertElementContains('.tag-item', 'Test Tag Value');
|
||||
|
||||
$this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[0]->id]);
|
||||
$this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);
|
||||
}
|
||||
|
||||
public function test_shelf_view()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
$resp = $this->asEditor()->get($shelf->getUrl());
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSeeText($shelf->name);
|
||||
$resp->assertSeeText($shelf->description);
|
||||
|
||||
foreach ($shelf->books as $book) {
|
||||
$resp->assertSee($book->name);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_shelf_view_shows_action_buttons()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
$resp = $this->asAdmin()->get($shelf->getUrl());
|
||||
$resp->assertSee($shelf->getUrl('/edit'));
|
||||
$resp->assertSee($shelf->getUrl('/permissions'));
|
||||
$resp->assertSee($shelf->getUrl('/delete'));
|
||||
$resp->assertElementContains('a', 'Edit');
|
||||
$resp->assertElementContains('a', 'Permissions');
|
||||
$resp->assertElementContains('a', 'Delete');
|
||||
|
||||
$resp = $this->asEditor()->get($shelf->getUrl());
|
||||
$resp->assertDontSee($shelf->getUrl('/permissions'));
|
||||
}
|
||||
|
||||
public function test_shelf_edit()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
$resp = $this->asEditor()->get($shelf->getUrl('/edit'));
|
||||
$resp->assertSeeText('Edit Bookshelf');
|
||||
|
||||
$booksToInclude = Book::take(2)->get();
|
||||
$shelfInfo = [
|
||||
'name' => 'My test book' . str_random(4),
|
||||
'description' => 'Test book description ' . str_random(10)
|
||||
];
|
||||
|
||||
$resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
|
||||
'books' => $booksToInclude->implode('id', ','),
|
||||
'tags' => [
|
||||
[
|
||||
'name' => 'Test Category',
|
||||
'value' => 'Test Tag Value',
|
||||
]
|
||||
],
|
||||
]));
|
||||
$shelf = Bookshelf::find($shelf->id);
|
||||
$resp->assertRedirect($shelf->getUrl());
|
||||
$this->assertSessionHas('success');
|
||||
|
||||
$editorId = $this->getEditor()->id;
|
||||
$this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['id' => $shelf->id, 'created_by' => $editorId, 'updated_by' => $editorId]));
|
||||
|
||||
$shelfPage = $this->get($shelf->getUrl());
|
||||
$shelfPage->assertSee($shelfInfo['name']);
|
||||
$shelfPage->assertSee($shelfInfo['description']);
|
||||
$shelfPage->assertElementContains('.tag-item', 'Test Category');
|
||||
$shelfPage->assertElementContains('.tag-item', 'Test Tag Value');
|
||||
|
||||
$this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[0]->id]);
|
||||
$this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);
|
||||
}
|
||||
|
||||
public function test_shelf_delete()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
$resp = $this->asEditor()->get($shelf->getUrl('/delete'));
|
||||
$resp->assertSeeText('Delete Bookshelf');
|
||||
$resp->assertSee("action=\"{$shelf->getUrl()}\"");
|
||||
|
||||
$resp = $this->delete($shelf->getUrl());
|
||||
$resp->assertRedirect('/shelves');
|
||||
$this->assertDatabaseMissing('bookshelves', ['id' => $shelf->id]);
|
||||
$this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]);
|
||||
$this->assertSessionHas('success');
|
||||
}
|
||||
|
||||
public function test_shelf_copy_permissions()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
$resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));
|
||||
$resp->assertSeeText('Copy Permissions');
|
||||
$resp->assertSee("action=\"{$shelf->getUrl('/copy-permissions')}\"");
|
||||
|
||||
$child = $shelf->books()->first();
|
||||
$editorRole = $this->getEditor()->roles()->first();
|
||||
$this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default");
|
||||
$this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default");
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]);
|
||||
$resp = $this->post($shelf->getUrl('/copy-permissions'));
|
||||
$child = $shelf->books()->first();
|
||||
|
||||
$resp->assertRedirect($shelf->getUrl());
|
||||
$this->assertTrue(boolval($child->restricted), "Child book should now be restricted");
|
||||
$this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions");
|
||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
|
||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
|
||||
}
|
||||
|
||||
}
|
|
@ -9,10 +9,13 @@ class ErrorTest extends TestCase
|
|||
// if our custom, middleware-loaded handler fails but this is here
|
||||
// as a reminder and as a general check in the event of other issues.
|
||||
$editor = $this->getEditor();
|
||||
$editor->name = 'tester';
|
||||
$editor->save();
|
||||
|
||||
$this->actingAs($editor);
|
||||
$notFound = $this->get('/fgfdngldfnotfound');
|
||||
$notFound->assertStatus(404);
|
||||
$notFound->assertDontSeeText('Log in');
|
||||
$notFound->assertSeeText($editor->getShortName(9));
|
||||
$notFound->assertSeeText('tester');
|
||||
}
|
||||
}
|
|
@ -10,15 +10,17 @@ class HomepageTest extends TestCase
|
|||
$homeVisit->assertSee('My Recently Viewed');
|
||||
$homeVisit->assertSee('Recently Updated Pages');
|
||||
$homeVisit->assertSee('Recent Activity');
|
||||
$homeVisit->assertSee('home-default');
|
||||
}
|
||||
|
||||
public function test_custom_homepage()
|
||||
{
|
||||
$this->asEditor();
|
||||
$name = 'My custom homepage';
|
||||
$content = 'This is the body content of my custom homepage.';
|
||||
$content = str_repeat('This is the body content of my custom homepage.', 20);
|
||||
$customPage = $this->newPage(['name' => $name, 'html' => $content]);
|
||||
$this->setSettings(['app-homepage' => $customPage->id]);
|
||||
$this->setSettings(['app-homepage-type' => 'page']);
|
||||
|
||||
$homeVisit = $this->get('/');
|
||||
$homeVisit->assertSee($name);
|
||||
|
@ -32,7 +34,7 @@ class HomepageTest extends TestCase
|
|||
{
|
||||
$this->asEditor();
|
||||
$name = 'My custom homepage';
|
||||
$content = 'This is the body content of my custom homepage.';
|
||||
$content = str_repeat('This is the body content of my custom homepage.', 20);
|
||||
$customPage = $this->newPage(['name' => $name, 'html' => $content]);
|
||||
$this->setSettings(['app-homepage' => $customPage->id]);
|
||||
|
||||
|
@ -55,7 +57,7 @@ class HomepageTest extends TestCase
|
|||
$editor = $this->getEditor();
|
||||
setting()->putUser($editor, 'books_view_type', 'grid');
|
||||
|
||||
$this->setSettings(['app-book-homepage' => true]);
|
||||
$this->setSettings(['app-homepage-type' => 'books']);
|
||||
|
||||
$this->asEditor();
|
||||
$homeVisit = $this->get('/');
|
||||
|
@ -65,7 +67,26 @@ class HomepageTest extends TestCase
|
|||
$homeVisit->assertSee('grid-card-footer');
|
||||
$homeVisit->assertSee('featured-image-container');
|
||||
|
||||
$this->setSettings(['app-book-homepage' => false]);
|
||||
$this->setSettings(['app-homepage-type' => false]);
|
||||
$this->test_default_homepage_visible();
|
||||
}
|
||||
|
||||
public function test_set_bookshelves_homepage()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
setting()->putUser($editor, 'bookshelves_view_type', 'grid');
|
||||
|
||||
$this->setSettings(['app-homepage-type' => 'bookshelves']);
|
||||
|
||||
$this->asEditor();
|
||||
$homeVisit = $this->get('/');
|
||||
$homeVisit->assertSee('Shelves');
|
||||
$homeVisit->assertSee('bookshelf-grid-item grid-card');
|
||||
$homeVisit->assertSee('grid-card-content');
|
||||
$homeVisit->assertSee('grid-card-footer');
|
||||
$homeVisit->assertSee('featured-image-container');
|
||||
|
||||
$this->setSettings(['app-homepage-type' => false]);
|
||||
$this->test_default_homepage_visible();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Bookshelf;
|
||||
use BookStack\Entity;
|
||||
use BookStack\User;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
|
@ -34,6 +35,63 @@ class RestrictionsTest extends BrowserKitTest
|
|||
parent::setEntityRestrictions($entity, $actions, $roles);
|
||||
}
|
||||
|
||||
public function test_bookshelf_view_restriction()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->visit($shelf->getUrl())
|
||||
->seePageIs($shelf->getUrl());
|
||||
|
||||
$this->setEntityRestrictions($shelf, []);
|
||||
|
||||
$this->forceVisit($shelf->getUrl())
|
||||
->see('Bookshelf not found');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view']);
|
||||
|
||||
$this->visit($shelf->getUrl())
|
||||
->see($shelf->name);
|
||||
}
|
||||
|
||||
public function test_bookshelf_update_restriction()
|
||||
{
|
||||
$shelf = BookShelf::first();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->visit($shelf->getUrl('/edit'))
|
||||
->see('Edit Book');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'delete']);
|
||||
|
||||
$this->forceVisit($shelf->getUrl('/edit'))
|
||||
->see('You do not have permission')->seePageIs('/');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'update']);
|
||||
|
||||
$this->visit($shelf->getUrl('/edit'))
|
||||
->seePageIs($shelf->getUrl('/edit'));
|
||||
}
|
||||
|
||||
public function test_bookshelf_delete_restriction()
|
||||
{
|
||||
$shelf = Book::first();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->visit($shelf->getUrl('/delete'))
|
||||
->see('Delete Book');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'update']);
|
||||
|
||||
$this->forceVisit($shelf->getUrl('/delete'))
|
||||
->see('You do not have permission')->seePageIs('/');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'delete']);
|
||||
|
||||
$this->visit($shelf->getUrl('/delete'))
|
||||
->seePageIs($shelf->getUrl('/delete'))->see('Delete Book');
|
||||
}
|
||||
|
||||
public function test_book_view_restriction()
|
||||
{
|
||||
$book = Book::first();
|
||||
|
@ -325,6 +383,23 @@ class RestrictionsTest extends BrowserKitTest
|
|||
->seePageIs($pageUrl . '/delete')->see('Delete Page');
|
||||
}
|
||||
|
||||
public function test_bookshelf_restriction_form()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
$this->asAdmin()->visit($shelf->getUrl('/permissions'))
|
||||
->see('Bookshelf Permissions')
|
||||
->check('restricted')
|
||||
->check('restrictions[2][view]')
|
||||
->press('Save Permissions')
|
||||
->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true])
|
||||
->seeInDatabase('entity_permissions', [
|
||||
'restrictable_id' => $shelf->id,
|
||||
'restrictable_type' => 'BookStack\Bookshelf',
|
||||
'role_id' => '2',
|
||||
'action' => 'view'
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_book_restriction_form()
|
||||
{
|
||||
$book = Book::first();
|
||||
|
@ -413,6 +488,44 @@ class RestrictionsTest extends BrowserKitTest
|
|||
->dontSee($page->name);
|
||||
}
|
||||
|
||||
public function test_bookshelf_update_restriction_override()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
|
||||
$this->actingAs($this->viewer)
|
||||
->visit($shelf->getUrl('/edit'))
|
||||
->dontSee('Edit Book');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'delete']);
|
||||
|
||||
$this->forceVisit($shelf->getUrl('/edit'))
|
||||
->see('You do not have permission')->seePageIs('/');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'update']);
|
||||
|
||||
$this->visit($shelf->getUrl('/edit'))
|
||||
->seePageIs($shelf->getUrl('/edit'));
|
||||
}
|
||||
|
||||
public function test_bookshelf_delete_restriction_override()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
|
||||
$this->actingAs($this->viewer)
|
||||
->visit($shelf->getUrl('/delete'))
|
||||
->dontSee('Delete Book');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'update']);
|
||||
|
||||
$this->forceVisit($shelf->getUrl('/delete'))
|
||||
->see('You do not have permission')->seePageIs('/');
|
||||
|
||||
$this->setEntityRestrictions($shelf, ['view', 'delete']);
|
||||
|
||||
$this->visit($shelf->getUrl('/delete'))
|
||||
->seePageIs($shelf->getUrl('/delete'))->see('Delete Book');
|
||||
}
|
||||
|
||||
public function test_book_create_restriction_override()
|
||||
{
|
||||
$book = Book::first();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Bookshelf;
|
||||
use BookStack\Page;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use BookStack\Role;
|
||||
|
@ -16,32 +17,6 @@ class RolesTest extends BrowserKitTest
|
|||
$this->user = $this->getViewer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the given user some permissions.
|
||||
* @param \BookStack\User $user
|
||||
* @param array $permissions
|
||||
*/
|
||||
protected function giveUserPermissions(\BookStack\User $user, $permissions = [])
|
||||
{
|
||||
$newRole = $this->createNewRole($permissions);
|
||||
$user->attachRole($newRole);
|
||||
$user->load('roles');
|
||||
$user->permissions(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new basic role for testing purposes.
|
||||
* @param array $permissions
|
||||
* @return Role
|
||||
*/
|
||||
protected function createNewRole($permissions = [])
|
||||
{
|
||||
$permissionRepo = app(PermissionsRepo::class);
|
||||
$roleData = factory(\BookStack\Role::class)->make()->toArray();
|
||||
$roleData['permissions'] = array_flip($permissions);
|
||||
return $permissionRepo->saveNewRole($roleData);
|
||||
}
|
||||
|
||||
public function test_admin_can_see_settings()
|
||||
{
|
||||
$this->asAdmin()->visit('/settings')->see('Settings');
|
||||
|
@ -203,6 +178,90 @@ class RolesTest extends BrowserKitTest
|
|||
}
|
||||
}
|
||||
|
||||
public function test_bookshelves_create_all_permissions()
|
||||
{
|
||||
$this->checkAccessPermission('bookshelf-create-all', [
|
||||
'/create-shelf'
|
||||
], [
|
||||
'/shelves' => 'Create New Shelf'
|
||||
]);
|
||||
|
||||
$this->visit('/create-shelf')
|
||||
->type('test shelf', 'name')
|
||||
->type('shelf desc', 'description')
|
||||
->press('Save Shelf')
|
||||
->seePageIs('/shelves/test-shelf');
|
||||
}
|
||||
|
||||
public function test_bookshelves_edit_own_permission()
|
||||
{
|
||||
$otherShelf = Bookshelf::first();
|
||||
$ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
|
||||
$ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
|
||||
$this->regenEntityPermissions($ownShelf);
|
||||
|
||||
$this->checkAccessPermission('bookshelf-update-own', [
|
||||
$ownShelf->getUrl('/edit')
|
||||
], [
|
||||
$ownShelf->getUrl() => 'Edit'
|
||||
]);
|
||||
|
||||
$this->visit($otherShelf->getUrl())
|
||||
->dontSeeInElement('.action-buttons', 'Edit')
|
||||
->visit($otherShelf->getUrl('/edit'))
|
||||
->seePageIs('/');
|
||||
}
|
||||
|
||||
public function test_bookshelves_edit_all_permission()
|
||||
{
|
||||
$otherShelf = \BookStack\Bookshelf::first();
|
||||
$this->checkAccessPermission('bookshelf-update-all', [
|
||||
$otherShelf->getUrl('/edit')
|
||||
], [
|
||||
$otherShelf->getUrl() => 'Edit'
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_bookshelves_delete_own_permission()
|
||||
{
|
||||
$this->giveUserPermissions($this->user, ['bookshelf-update-all']);
|
||||
$otherShelf = \BookStack\Bookshelf::first();
|
||||
$ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
|
||||
$ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
|
||||
$this->regenEntityPermissions($ownShelf);
|
||||
|
||||
$this->checkAccessPermission('bookshelf-delete-own', [
|
||||
$ownShelf->getUrl('/delete')
|
||||
], [
|
||||
$ownShelf->getUrl() => 'Delete'
|
||||
]);
|
||||
|
||||
$this->visit($otherShelf->getUrl())
|
||||
->dontSeeInElement('.action-buttons', 'Delete')
|
||||
->visit($otherShelf->getUrl('/delete'))
|
||||
->seePageIs('/');
|
||||
$this->visit($ownShelf->getUrl())->visit($ownShelf->getUrl('/delete'))
|
||||
->press('Confirm')
|
||||
->seePageIs('/shelves')
|
||||
->dontSee($ownShelf->name);
|
||||
}
|
||||
|
||||
public function test_bookshelves_delete_all_permission()
|
||||
{
|
||||
$this->giveUserPermissions($this->user, ['bookshelf-update-all']);
|
||||
$otherShelf = \BookStack\Bookshelf::first();
|
||||
$this->checkAccessPermission('bookshelf-delete-all', [
|
||||
$otherShelf->getUrl('/delete')
|
||||
], [
|
||||
$otherShelf->getUrl() => 'Delete'
|
||||
]);
|
||||
|
||||
$this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete'))
|
||||
->press('Confirm')
|
||||
->seePageIs('/shelves')
|
||||
->dontSee($otherShelf->name);
|
||||
}
|
||||
|
||||
public function test_books_create_all_permissions()
|
||||
{
|
||||
$this->checkAccessPermission('book-create-all', [
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Bookshelf;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\SettingService;
|
||||
|
@ -69,6 +71,25 @@ trait SharedTestHelpers
|
|||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the permission for an entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
protected function regenEntityPermissions(Entity $entity)
|
||||
{
|
||||
$this->app[PermissionService::class]->buildJointPermissionsForEntity($entity);
|
||||
$entity->load('jointPermissions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new bookshelf.
|
||||
* @param array $input
|
||||
* @return Bookshelf
|
||||
*/
|
||||
public function newShelf($input = ['name' => 'test shelf', 'description' => 'My new test shelf']) {
|
||||
return $this->app[EntityRepo::class]->createFromInput('bookshelf', $input, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new book.
|
||||
* @param array $input
|
||||
|
@ -140,4 +161,30 @@ trait SharedTestHelpers
|
|||
$entity->load('jointPermissions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the given user some permissions.
|
||||
* @param \BookStack\User $user
|
||||
* @param array $permissions
|
||||
*/
|
||||
protected function giveUserPermissions(\BookStack\User $user, $permissions = [])
|
||||
{
|
||||
$newRole = $this->createNewRole($permissions);
|
||||
$user->attachRole($newRole);
|
||||
$user->load('roles');
|
||||
$user->permissions(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new basic role for testing purposes.
|
||||
* @param array $permissions
|
||||
* @return Role
|
||||
*/
|
||||
protected function createNewRole($permissions = [])
|
||||
{
|
||||
$permissionRepo = app(PermissionsRepo::class);
|
||||
$roleData = factory(Role::class)->make()->toArray();
|
||||
$roleData['permissions'] = array_flip($permissions);
|
||||
return $permissionRepo->saveNewRole($roleData);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Illuminate\Foundation\Testing\TestResponse;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
use CreatesApplication;
|
||||
use DatabaseTransactions;
|
||||
use SharedTestHelpers;
|
||||
|
||||
/**
|
||||
* The base URL to use while testing the application.
|
||||
* @var string
|
||||
|
@ -18,11 +18,46 @@ abstract class TestCase extends BaseTestCase
|
|||
/**
|
||||
* Assert a permission error has occurred.
|
||||
* @param TestResponse $response
|
||||
* @return TestCase
|
||||
*/
|
||||
protected function assertPermissionError(TestResponse $response)
|
||||
{
|
||||
$response->assertRedirect('/');
|
||||
$this->assertTrue(session()->has('error'));
|
||||
$this->assertSessionHas('error');
|
||||
session()->remove('error');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the session contains a specific entry.
|
||||
* @param string $key
|
||||
* @return $this
|
||||
*/
|
||||
protected function assertSessionHas(string $key)
|
||||
{
|
||||
$this->assertTrue(session()->has($key), "Session does not contain a [{$key}] entry");
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override of the get method so we can get visibility of custom TestResponse methods.
|
||||
* @param string $uri
|
||||
* @param array $headers
|
||||
* @return TestResponse
|
||||
*/
|
||||
public function get($uri, array $headers = [])
|
||||
{
|
||||
return parent::get($uri, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the test response instance from the given response.
|
||||
*
|
||||
* @param \Illuminate\Http\Response $response
|
||||
* @return TestResponse
|
||||
*/
|
||||
protected function createTestResponse($response)
|
||||
{
|
||||
return TestResponse::fromBaseResponse($response);
|
||||
}
|
||||
}
|
141
tests/TestResponse.php
Normal file
141
tests/TestResponse.php
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?php namespace Tests;
|
||||
|
||||
use \Illuminate\Foundation\Testing\TestResponse as BaseTestResponse;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
/**
|
||||
* Class TestResponse
|
||||
* Custom extension of the default Laravel TestResponse class.
|
||||
* @package Tests
|
||||
*/
|
||||
class TestResponse extends BaseTestResponse {
|
||||
|
||||
protected $crawlerInstance;
|
||||
|
||||
/**
|
||||
* Get the DOM Crawler for the response content.
|
||||
* @return Crawler
|
||||
*/
|
||||
protected function crawler()
|
||||
{
|
||||
if (!is_object($this->crawlerInstance)) {
|
||||
$this->crawlerInstance = new Crawler($this->getContent());
|
||||
}
|
||||
return $this->crawlerInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response contains the specified element.
|
||||
* @param string $selector
|
||||
* @return $this
|
||||
*/
|
||||
public function assertElementExists(string $selector)
|
||||
{
|
||||
$elements = $this->crawler()->filter($selector);
|
||||
PHPUnit::assertTrue(
|
||||
$elements->count() > 0,
|
||||
'Unable to find element matching the selector: '.PHP_EOL.PHP_EOL.
|
||||
"[{$selector}]".PHP_EOL.PHP_EOL.
|
||||
'within'.PHP_EOL.PHP_EOL.
|
||||
"[{$this->getContent()}]."
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response does not contain the specified element.
|
||||
* @param string $selector
|
||||
* @return $this
|
||||
*/
|
||||
public function assertElementNotExists(string $selector)
|
||||
{
|
||||
$elements = $this->crawler()->filter($selector);
|
||||
PHPUnit::assertTrue(
|
||||
$elements->count() === 0,
|
||||
'Found elements matching the selector: '.PHP_EOL.PHP_EOL.
|
||||
"[{$selector}]".PHP_EOL.PHP_EOL.
|
||||
'within'.PHP_EOL.PHP_EOL.
|
||||
"[{$this->getContent()}]."
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response includes a specific element containing the given text.
|
||||
* @param string $selector
|
||||
* @param string $text
|
||||
* @return $this
|
||||
*/
|
||||
public function assertElementContains(string $selector, string $text)
|
||||
{
|
||||
$elements = $this->crawler()->filter($selector);
|
||||
$matched = false;
|
||||
$pattern = $this->getEscapedPattern($text);
|
||||
foreach ($elements as $element) {
|
||||
$element = new Crawler($element);
|
||||
if (preg_match("/$pattern/i", $element->html())) {
|
||||
$matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
$matched,
|
||||
'Unable to find element of selector: '.PHP_EOL.PHP_EOL.
|
||||
"[{$selector}]".PHP_EOL.PHP_EOL.
|
||||
'containing text'.PHP_EOL.PHP_EOL.
|
||||
"[{$text}]".PHP_EOL.PHP_EOL.
|
||||
'within'.PHP_EOL.PHP_EOL.
|
||||
"[{$this->getContent()}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response does not include a specific element containing the given text.
|
||||
* @param string $selector
|
||||
* @param string $text
|
||||
* @return $this
|
||||
*/
|
||||
public function assertElementNotContains(string $selector, string $text)
|
||||
{
|
||||
$elements = $this->crawler()->filter($selector);
|
||||
$matched = false;
|
||||
$pattern = $this->getEscapedPattern($text);
|
||||
foreach ($elements as $element) {
|
||||
$element = new Crawler($element);
|
||||
if (preg_match("/$pattern/i", $element->html())) {
|
||||
$matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
!$matched,
|
||||
'Found element of selector: '.PHP_EOL.PHP_EOL.
|
||||
"[{$selector}]".PHP_EOL.PHP_EOL.
|
||||
'containing text'.PHP_EOL.PHP_EOL.
|
||||
"[{$text}]".PHP_EOL.PHP_EOL.
|
||||
'within'.PHP_EOL.PHP_EOL.
|
||||
"[{$this->getContent()}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the escaped text pattern for the constraint.
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
protected function getEscapedPattern($text)
|
||||
{
|
||||
$rawPattern = preg_quote($text, '/');
|
||||
$escapedPattern = preg_quote(e($text), '/');
|
||||
return $rawPattern == $escapedPattern
|
||||
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user