mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-06 00:59:39 +03:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
54e3122540 | ||
|
|
d339ab1125 | ||
|
|
3ab09ef708 | ||
|
|
c86a122d80 | ||
|
|
3a58e37838 | ||
|
|
6bd49bcd4b | ||
|
|
61577cf6bf | ||
|
|
b4dec2a99c | ||
|
|
fe0b122aca | ||
|
|
8eb2960950 | ||
|
|
c2369a740d | ||
|
|
bab6fd1f2f | ||
|
|
86fbc9a936 | ||
|
|
4d9726dbdd | ||
|
|
4442a2e6d1 | ||
|
|
293be7093c | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
354912a1df | ||
|
|
eacff3a9f0 | ||
|
|
990acbb9ac | ||
|
|
17d4533e45 | ||
|
|
d6c00a85ad | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
e0279f93f9 | ||
|
|
9b83c57316 | ||
|
|
5d73d17c74 | ||
|
|
d32460070f | ||
|
|
105500e506 | ||
|
|
8296782149 | ||
|
|
8e8d582bc6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,7 +7,6 @@ Homestead.yaml
|
||||
/public/plugins
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/uploads
|
||||
/public/bower
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
|
||||
@@ -98,7 +98,7 @@ abstract class Entity extends Model
|
||||
* @param string[] array $wheres
|
||||
* @return mixed
|
||||
*/
|
||||
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
|
||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||
{
|
||||
$termString = '';
|
||||
foreach ($terms as $term) {
|
||||
@@ -107,7 +107,7 @@ abstract class Entity extends Model
|
||||
$fields = implode(',', $fieldsToSearch);
|
||||
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
|
||||
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
@@ -115,10 +115,13 @@ abstract class Entity extends Model
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if (!static::isA('book')) $search = $search->with('book');
|
||||
if (static::isA('page')) $search = $search->with('chapter');
|
||||
if (static::isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if (static::isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy('title_relevance', 'desc')->get();
|
||||
return $search->orderBy('title_relevance', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class ConfirmationEmailException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class ConfirmationEmailException extends NotifyException {}
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Exceptions;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use PhpSpec\Exception\Example\ErrorException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
@@ -38,17 +39,26 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Exception $e)
|
||||
{
|
||||
if($e instanceof NotifyException) {
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($e instanceof NotifyException) {
|
||||
\Session::flash('error', $e->message);
|
||||
return response()->redirectTo($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
||||
return response()->view('errors/500', ['message' => $message], 500);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class ImageUploadException extends Exception {}
|
||||
class ImageUploadException extends PrettyException {}
|
||||
@@ -1,9 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class LdapException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
class LdapException extends PrettyException {}
|
||||
5
app/Exceptions/PrettyException.php
Normal file
5
app/Exceptions/PrettyException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class SocialDriverNotConfigured extends \Exception
|
||||
{
|
||||
}
|
||||
class SocialDriverNotConfigured extends PrettyException {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class SocialSignInException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class SocialSignInException extends NotifyException {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class UserRegistrationException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class UserRegistrationException extends NotifyException {}
|
||||
@@ -157,7 +157,7 @@ class BookController extends Controller
|
||||
$this->checkPermission('book-update');
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$bookChildren = $this->bookRepo->getChildren($book);
|
||||
$books = $this->bookRepo->getAll();
|
||||
$books = $this->bookRepo->getAll(false);
|
||||
$this->setPageTitle('Sort Book ' . $book->getShortName());
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,21 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use Views;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
|
||||
protected $activityService;
|
||||
protected $bookRepo;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* HomeController constructor.
|
||||
* @param BookRepo $bookRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->entityRepo = $entityRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -33,9 +29,16 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$activity = Activity::latest();
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10);
|
||||
return view('home', ['activity' => $activity, 'recents' => $recents]);
|
||||
$activity = Activity::latest(10);
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12, 0) : $this->entityRepo->getRecentlyCreatedBooks(10);
|
||||
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
|
||||
return view('home', [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyCreatedPages' => $recentlyCreatedPages,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use BookStack\Http\Requests;
|
||||
use BookStack\Repos\BookRepo;
|
||||
use BookStack\Repos\ChapterRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Views;
|
||||
|
||||
class PageController extends Controller
|
||||
@@ -81,6 +82,8 @@ class PageController extends Controller
|
||||
|
||||
/**
|
||||
* Display the specified page.
|
||||
* If the page is not found via the slug the
|
||||
* revisions are searched for a match.
|
||||
*
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
@@ -89,7 +92,15 @@ class PageController extends Controller
|
||||
public function show($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
|
||||
try {
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
@@ -278,4 +289,30 @@ class PageController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of recently created pages
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRecentlyCreated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Created Pages',
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a listing of recently created pages
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => 'Recently Updated Pages',
|
||||
'pages' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,11 +42,77 @@ class SearchController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm);
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle('Search For ' . $searchTerm);
|
||||
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the pages in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchPages(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Page Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $pages,
|
||||
'title' => 'Page Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the chapters in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchChapters(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle('Chapter Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $chapters,
|
||||
'title' => 'Chapter Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the books in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchBooks(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
|
||||
$this->setPageTitle('Book Search For ' . $searchTerm);
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $books,
|
||||
'title' => 'Book Search Results',
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
@@ -92,10 +93,9 @@ class UserController extends Controller
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return redirect('/users');
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified user.
|
||||
* @param int $id
|
||||
@@ -130,8 +130,8 @@ class UserController extends Controller
|
||||
});
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email,' . $id,
|
||||
'name' => 'min:2',
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password',
|
||||
'role' => 'exists:roles,id'
|
||||
@@ -159,7 +159,7 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return redirect('/users');
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +197,25 @@ class UserController extends Controller
|
||||
}
|
||||
$this->userRepo->destroy($user);
|
||||
|
||||
return redirect('/users');
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showProfilePage($id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
$userActivity = $this->userRepo->getActivity($user);
|
||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||
return view('users/profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
// Authenticated routes...
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
Route::group(['prefix' => 'pages'], function() {
|
||||
Route::get('/recently-created', 'PageController@showRecentlyCreated');
|
||||
Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'books'], function () {
|
||||
|
||||
// Books
|
||||
@@ -47,14 +52,8 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
});
|
||||
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
Route::get('/users/{id}/delete', 'UserController@delete');
|
||||
Route::post('/users/create', 'UserController@store');
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
Route::delete('/users/{id}', 'UserController@destroy');
|
||||
// User Profile routes
|
||||
Route::get('/user/{userId}', 'UserController@showProfilePage');
|
||||
|
||||
// Image routes
|
||||
Route::group(['prefix' => 'images'], function() {
|
||||
@@ -75,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
// Search
|
||||
Route::get('/search/all', 'SearchController@searchAll');
|
||||
Route::get('/search/pages', 'SearchController@searchPages');
|
||||
Route::get('/search/books', 'SearchController@searchBooks');
|
||||
Route::get('/search/chapters', 'SearchController@searchChapters');
|
||||
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
|
||||
|
||||
// Other Pages
|
||||
@@ -82,8 +84,18 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/home', 'HomeController@index');
|
||||
|
||||
// Settings
|
||||
Route::get('/settings', 'SettingController@index');
|
||||
Route::post('/settings', 'SettingController@update');
|
||||
Route::group(['prefix' => 'settings'], function() {
|
||||
Route::get('/', 'SettingController@index');
|
||||
Route::post('/', 'SettingController@update');
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
Route::get('/users/{id}/delete', 'UserController@delete');
|
||||
Route::post('/users/create', 'UserController@store');
|
||||
Route::get('/users/{id}', 'UserController@edit');
|
||||
Route::put('/users/{id}', 'UserController@update');
|
||||
Route::delete('/users/{id}', 'UserController@destroy');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ class Page extends Entity
|
||||
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
return strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ class BookRepo
|
||||
|
||||
/**
|
||||
* BookRepo constructor.
|
||||
* @param Book $book
|
||||
* @param PageRepo $pageRepo
|
||||
* @param Book $book
|
||||
* @param PageRepo $pageRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
*/
|
||||
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
|
||||
@@ -42,7 +42,9 @@ class BookRepo
|
||||
*/
|
||||
public function getAll($count = 10)
|
||||
{
|
||||
return $this->book->orderBy('name', 'asc')->take($count)->get();
|
||||
$bookQuery = $this->book->orderBy('name', 'asc');
|
||||
if (!$count) return $bookQuery->get();
|
||||
return $bookQuery->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,7 +161,7 @@ class BookRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
* @param string $slug
|
||||
* @param bool|false $currentId
|
||||
* @return bool
|
||||
*/
|
||||
@@ -175,7 +177,7 @@ class BookRepo
|
||||
/**
|
||||
* Provides a suitable slug for the given book name.
|
||||
* Ensures the returned slug is unique in the system.
|
||||
* @param string $name
|
||||
* @param string $name
|
||||
* @param bool|false $currentId
|
||||
* @return string
|
||||
*/
|
||||
@@ -218,12 +220,15 @@ class BookRepo
|
||||
/**
|
||||
* Get books by search term.
|
||||
* @param $term
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term)
|
||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$books = $this->book->fullTextSearch(['name', 'description'], $terms);
|
||||
$books = $this->book->fullTextSearchQuery(['name', 'description'], $terms)
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($books as $book) {
|
||||
//highlight
|
||||
|
||||
@@ -125,12 +125,15 @@ class ChapterRepo
|
||||
* Get chapters by the given search term.
|
||||
* @param $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms);
|
||||
$chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
foreach ($chapters as $chapter) {
|
||||
//highlight
|
||||
|
||||
71
app/Repos/EntityRepo.php
Normal file
71
app/Repos/EntityRepo.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Page;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
|
||||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
* @param $book
|
||||
* @param $chapter
|
||||
* @param $page
|
||||
*/
|
||||
public function __construct(Book $book, Chapter $chapter, Page $page)
|
||||
{
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest books added to the system.
|
||||
* @param $count
|
||||
* @param $page
|
||||
*/
|
||||
public function getRecentlyCreatedBooks($count = 20, $page = 0)
|
||||
{
|
||||
return $this->book->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently updated books.
|
||||
* @param $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
|
||||
{
|
||||
return $this->book->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
* @param $page
|
||||
*/
|
||||
public function getRecentlyCreatedPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->page->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently updated pages.
|
||||
* @param $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->page->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class PageRepo
|
||||
{
|
||||
@@ -65,11 +66,28 @@ class PageRepo
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
|
||||
if ($page === null) abort(404);
|
||||
if ($page === null) throw new NotFoundHttpException('Page not found');
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through page revisions and retrieve
|
||||
* the last page in the current book that
|
||||
* has a slug equal to the one given.
|
||||
* @param $pageSlug
|
||||
* @param $bookSlug
|
||||
* @return null | Page
|
||||
*/
|
||||
public function findPageUsingOldSlug($pageSlug, $bookSlug)
|
||||
{
|
||||
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
|
||||
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
|
||||
->with('page')->first();
|
||||
return $revision !== null ? $revision->page : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new Page instance from the given input.
|
||||
* @param $input
|
||||
* @return Page
|
||||
*/
|
||||
@@ -125,21 +143,20 @@ class PageRepo
|
||||
if($htmlText == '') return $htmlText;
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new \DOMDocument();
|
||||
$doc->loadHTML($htmlText);
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
|
||||
// Ensure no duplicate ids are used
|
||||
$lastId = false;
|
||||
$idArray = [];
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') continue;
|
||||
|
||||
// Overwrite id if not a bookstack custom id
|
||||
// Overwrite id if not a BookStack custom id
|
||||
if ($childNode->hasAttribute('id')) {
|
||||
$id = $childNode->getAttribute('id');
|
||||
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
|
||||
@@ -149,13 +166,18 @@ class PageRepo
|
||||
}
|
||||
|
||||
// Create an unique id for the element
|
||||
do {
|
||||
$id = 'bkmrk-' . substr(uniqid(), -5);
|
||||
} while ($id == $lastId);
|
||||
$lastId = $id;
|
||||
// Uses the content as a basis to ensure output is the same every time
|
||||
// the same content is passed through.
|
||||
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
|
||||
$newId = urlencode($contentId);
|
||||
$loopIndex = 0;
|
||||
while (in_array($newId, $idArray)) {
|
||||
$newId = urlencode($contentId . '-' . $loopIndex);
|
||||
$loopIndex++;
|
||||
}
|
||||
|
||||
$childNode->setAttribute('id', $id);
|
||||
$idArray[] = $id;
|
||||
$childNode->setAttribute('id', $newId);
|
||||
$idArray[] = $newId;
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
@@ -171,14 +193,17 @@ class PageRepo
|
||||
/**
|
||||
* Gets pages by a search term.
|
||||
* Highlights page content for showing in results.
|
||||
* @param string $term
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($term, $whereTerms = [])
|
||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = explode(' ', $term);
|
||||
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
|
||||
$pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)
|
||||
->paginate($count)->appends($paginationAppends);
|
||||
|
||||
// Add highlights to page text.
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
@@ -238,9 +263,13 @@ class PageRepo
|
||||
$this->saveRevision($page);
|
||||
}
|
||||
|
||||
// Prevent slug being updated if no name change
|
||||
if ($page->name !== $input['name']) {
|
||||
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$page->fill($input);
|
||||
$page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = auth()->user()->id;
|
||||
@@ -276,6 +305,8 @@ class PageRepo
|
||||
{
|
||||
$revision = $this->pageRevision->fill($page->toArray());
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
$revision->created_by = auth()->user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->save();
|
||||
@@ -358,5 +389,22 @@ class PageRepo
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($count = 20)
|
||||
{
|
||||
return $this->page->orderBy('created_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Get the latest pages added to the system.
|
||||
* @param $count
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($count = 20)
|
||||
{
|
||||
return $this->page->orderBy('updated_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Setting;
|
||||
@@ -10,15 +9,19 @@ class UserRepo
|
||||
|
||||
protected $user;
|
||||
protected $role;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* UserRepo constructor.
|
||||
* @param $user
|
||||
* @param User $user
|
||||
* @param Role $role
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(User $user, Role $role)
|
||||
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->role = $role;
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,4 +115,49 @@ class UserRepo
|
||||
$user->socialAccounts()->delete();
|
||||
$user->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest activity for a user.
|
||||
* @param User $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function getActivity(User $user, $count = 20, $page = 0)
|
||||
{
|
||||
return \Activity::userActivity($user, $count, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recently created content for this given user.
|
||||
* @param User $user
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreated(User $user, $count = 20)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
|
||||
->take($count)->get(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
|
||||
->take($count)->get(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
|
||||
->take($count)->get()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset created counts for the give user.
|
||||
* @param User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetCounts(User $user)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,18 +29,19 @@ class ActivityService
|
||||
*/
|
||||
public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
|
||||
{
|
||||
$this->activity->user_id = $this->user->id;
|
||||
$this->activity->book_id = $bookId;
|
||||
$this->activity->key = strtolower($activityKey);
|
||||
$activity = $this->activity->newInstance();
|
||||
$activity->user_id = $this->user->id;
|
||||
$activity->book_id = $bookId;
|
||||
$activity->key = strtolower($activityKey);
|
||||
if ($extra !== false) {
|
||||
$this->activity->extra = $extra;
|
||||
$activity->extra = $extra;
|
||||
}
|
||||
$entity->activity()->save($this->activity);
|
||||
$entity->activity()->save($activity);
|
||||
$this->setNotification($activityKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a activity history with a message & without binding to a entitiy.
|
||||
* Adds a activity history with a message & without binding to a entity.
|
||||
* @param $activityKey
|
||||
* @param int $bookId
|
||||
* @param bool|false $extra
|
||||
@@ -91,14 +92,14 @@ class ActivityService
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest activity for an entitiy, Filtering out similar
|
||||
* Gets the latest activity for an entity, Filtering out similar
|
||||
* items to prevent a message activity list.
|
||||
* @param Entity $entity
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
function entityActivity($entity, $count = 20, $page = 0)
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
@@ -107,15 +108,30 @@ class ActivityService
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out similar activity.
|
||||
* @param Activity[] $activity
|
||||
* Get latest activity for a user, Filtering out similar
|
||||
* items.
|
||||
* @param $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar($activity)
|
||||
public function userActivity($user, $count = 20, $page = 0)
|
||||
{
|
||||
$activity = $this->activity->where('user_id', '=', $user->id)
|
||||
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
|
||||
return $this->filterSimilar($activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out similar activity.
|
||||
* @param Activity[] $activities
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar($activities)
|
||||
{
|
||||
$newActivity = [];
|
||||
$previousItem = false;
|
||||
foreach ($activity as $activityItem) {
|
||||
foreach ($activities as $activityItem) {
|
||||
if ($previousItem === false) {
|
||||
$previousItem = $activityItem;
|
||||
$newActivity[] = $activityItem;
|
||||
|
||||
@@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Image;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
@@ -119,10 +120,12 @@ class ImageService
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*
|
||||
* @param Image $image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @return string
|
||||
* @throws Exception
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
@@ -139,8 +142,16 @@ class ImageService
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
// Otherwise create the thumbnail
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($keepRatio) {
|
||||
$thumb->resize($width, null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
|
||||
@@ -46,7 +46,7 @@ class LdapService
|
||||
|
||||
$user = $users[0];
|
||||
return [
|
||||
'uid' => $user['uid'][0],
|
||||
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
|
||||
'name' => $user['cn'][0],
|
||||
'dn' => $user['dn'],
|
||||
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
|
||||
|
||||
@@ -164,6 +164,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return '/users/' . $this->id;
|
||||
return '/settings/users/' . $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
189
composer.lock
generated
189
composer.lock
generated
@@ -9,16 +9,16 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.14.2",
|
||||
"version": "3.15.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "2970cb63e7b7b37dd8c07a4fa4e4e18a110ed4e2"
|
||||
"reference": "5e6078913293576de969703481994b77c380ca30"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2970cb63e7b7b37dd8c07a4fa4e4e18a110ed4e2",
|
||||
"reference": "2970cb63e7b7b37dd8c07a4fa4e4e18a110ed4e2",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5e6078913293576de969703481994b77c380ca30",
|
||||
"reference": "5e6078913293576de969703481994b77c380ca30",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -40,7 +40,8 @@
|
||||
"ext-simplexml": "*",
|
||||
"ext-spl": "*",
|
||||
"nette/neon": "^2.3",
|
||||
"phpunit/phpunit": "~4.0|~5.0"
|
||||
"phpunit/phpunit": "~4.0|~5.0",
|
||||
"psr/cache": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
|
||||
@@ -84,7 +85,7 @@
|
||||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"time": "2016-01-28 21:33:18"
|
||||
"time": "2016-02-11 23:23:31"
|
||||
},
|
||||
{
|
||||
"name": "barryvdh/laravel-debugbar",
|
||||
@@ -918,16 +919,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v5.2.12",
|
||||
"version": "v5.2.16",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "6b6255ad7bfbdb721b8d00b09d52b146c5d363d7"
|
||||
"reference": "39e89553c124dce266da03ee3c0260bdd62f1848"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/6b6255ad7bfbdb721b8d00b09d52b146c5d363d7",
|
||||
"reference": "6b6255ad7bfbdb721b8d00b09d52b146c5d363d7",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/39e89553c124dce266da03ee3c0260bdd62f1848",
|
||||
"reference": "39e89553c124dce266da03ee3c0260bdd62f1848",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1042,7 +1043,7 @@
|
||||
"framework",
|
||||
"laravel"
|
||||
],
|
||||
"time": "2016-01-26 04:15:37"
|
||||
"time": "2016-02-15 17:46:58"
|
||||
},
|
||||
{
|
||||
"name": "laravel/socialite",
|
||||
@@ -1629,16 +1630,16 @@
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
"version": "1.1.6",
|
||||
"version": "v1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/random_compat.git",
|
||||
"reference": "e6f80ab77885151908d0ec743689ca700886e8b0"
|
||||
"reference": "b0e69d10852716b2ccbdff69c75c477637220790"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/e6f80ab77885151908d0ec743689ca700886e8b0",
|
||||
"reference": "e6f80ab77885151908d0ec743689ca700886e8b0",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/b0e69d10852716b2ccbdff69c75c477637220790",
|
||||
"reference": "b0e69d10852716b2ccbdff69c75c477637220790",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1673,7 +1674,7 @@
|
||||
"pseudorandom",
|
||||
"random"
|
||||
],
|
||||
"time": "2016-01-29 16:19:52"
|
||||
"time": "2016-02-06 03:52:05"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-font-lib",
|
||||
@@ -2024,16 +2025,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "ebcdc507829df915f4ca23067bd59ee4ef61f6c3"
|
||||
"reference": "5a02eaadaa285e2bb727eb6bbdfb8201fcd971b0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/ebcdc507829df915f4ca23067bd59ee4ef61f6c3",
|
||||
"reference": "ebcdc507829df915f4ca23067bd59ee4ef61f6c3",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/5a02eaadaa285e2bb727eb6bbdfb8201fcd971b0",
|
||||
"reference": "5a02eaadaa285e2bb727eb6bbdfb8201fcd971b0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2080,20 +2081,20 @@
|
||||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-22 10:39:06"
|
||||
"time": "2016-02-02 13:44:19"
|
||||
},
|
||||
{
|
||||
"name": "symfony/debug",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/debug.git",
|
||||
"reference": "73612266ac709769effdbfc0762e5b07cfd2ac2a"
|
||||
"reference": "29606049ced1ec715475f88d1bbe587252a3476e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/debug/zipball/73612266ac709769effdbfc0762e5b07cfd2ac2a",
|
||||
"reference": "73612266ac709769effdbfc0762e5b07cfd2ac2a",
|
||||
"url": "https://api.github.com/repos/symfony/debug/zipball/29606049ced1ec715475f88d1bbe587252a3476e",
|
||||
"reference": "29606049ced1ec715475f88d1bbe587252a3476e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2137,20 +2138,20 @@
|
||||
],
|
||||
"description": "Symfony Debug Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-26 13:39:53"
|
||||
"time": "2016-01-27 05:14:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "d36355e026905fa5229e1ed7b4e9eda2e67adfcf"
|
||||
"reference": "4dd5df31a28c0f82b41cb1e1599b74b5dcdbdafa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d36355e026905fa5229e1ed7b4e9eda2e67adfcf",
|
||||
"reference": "d36355e026905fa5229e1ed7b4e9eda2e67adfcf",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4dd5df31a28c0f82b41cb1e1599b74b5dcdbdafa",
|
||||
"reference": "4dd5df31a28c0f82b41cb1e1599b74b5dcdbdafa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2197,20 +2198,20 @@
|
||||
],
|
||||
"description": "Symfony EventDispatcher Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-10-30 23:35:59"
|
||||
"time": "2016-01-27 05:14:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "8617895eb798b6bdb338321ce19453dc113e5675"
|
||||
"reference": "623bda0abd9aa29e529c8e9c08b3b84171914723"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/8617895eb798b6bdb338321ce19453dc113e5675",
|
||||
"reference": "8617895eb798b6bdb338321ce19453dc113e5675",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/623bda0abd9aa29e529c8e9c08b3b84171914723",
|
||||
"reference": "623bda0abd9aa29e529c8e9c08b3b84171914723",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2246,20 +2247,20 @@
|
||||
],
|
||||
"description": "Symfony Finder Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-05 11:13:14"
|
||||
"time": "2016-01-27 05:14:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "939c8c28a5b1e4ab7317bc30c1f9aa881c4b06b5"
|
||||
"reference": "9344a87ceedfc50354a39653e54257ee9aa6a77d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/939c8c28a5b1e4ab7317bc30c1f9aa881c4b06b5",
|
||||
"reference": "939c8c28a5b1e4ab7317bc30c1f9aa881c4b06b5",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/9344a87ceedfc50354a39653e54257ee9aa6a77d",
|
||||
"reference": "9344a87ceedfc50354a39653e54257ee9aa6a77d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2298,20 +2299,20 @@
|
||||
],
|
||||
"description": "Symfony HttpFoundation Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-18 15:43:53"
|
||||
"time": "2016-02-02 13:44:19"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-kernel",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-kernel.git",
|
||||
"reference": "f7933e9f19e26e7baba7ec04735b466fedd3a6db"
|
||||
"reference": "cec02604450481ac26710ca4249cc61b57b23942"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/f7933e9f19e26e7baba7ec04735b466fedd3a6db",
|
||||
"reference": "f7933e9f19e26e7baba7ec04735b466fedd3a6db",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/cec02604450481ac26710ca4249cc61b57b23942",
|
||||
"reference": "cec02604450481ac26710ca4249cc61b57b23942",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2380,7 +2381,7 @@
|
||||
],
|
||||
"description": "Symfony HttpKernel Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-26 16:46:13"
|
||||
"time": "2016-02-03 12:38:44"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
@@ -2551,16 +2552,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "f4794f1d00f0746621be3020ffbd8c5e0b217ee3"
|
||||
"reference": "dfecef47506179db2501430e732adbf3793099c8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/f4794f1d00f0746621be3020ffbd8c5e0b217ee3",
|
||||
"reference": "f4794f1d00f0746621be3020ffbd8c5e0b217ee3",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/dfecef47506179db2501430e732adbf3793099c8",
|
||||
"reference": "dfecef47506179db2501430e732adbf3793099c8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2596,20 +2597,20 @@
|
||||
],
|
||||
"description": "Symfony Process Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-23 11:04:02"
|
||||
"time": "2016-02-02 13:44:19"
|
||||
},
|
||||
{
|
||||
"name": "symfony/routing",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/routing.git",
|
||||
"reference": "3b1bac52f42cb0f54df1a2dbabd55a1d214e2a59"
|
||||
"reference": "4686baa55a835e1c1ede9b86ba02415c8c8d6166"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/3b1bac52f42cb0f54df1a2dbabd55a1d214e2a59",
|
||||
"reference": "3b1bac52f42cb0f54df1a2dbabd55a1d214e2a59",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/4686baa55a835e1c1ede9b86ba02415c8c8d6166",
|
||||
"reference": "4686baa55a835e1c1ede9b86ba02415c8c8d6166",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2670,20 +2671,20 @@
|
||||
"uri",
|
||||
"url"
|
||||
],
|
||||
"time": "2015-12-23 08:00:11"
|
||||
"time": "2016-01-27 05:14:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/translation.git",
|
||||
"reference": "dff0867826a7068d673801b7522f8e2634016ef9"
|
||||
"reference": "2de0b6f7ebe43cffd8a06996ebec6aab79ea9e91"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/dff0867826a7068d673801b7522f8e2634016ef9",
|
||||
"reference": "dff0867826a7068d673801b7522f8e2634016ef9",
|
||||
"url": "https://api.github.com/repos/symfony/translation/zipball/2de0b6f7ebe43cffd8a06996ebec6aab79ea9e91",
|
||||
"reference": "2de0b6f7ebe43cffd8a06996ebec6aab79ea9e91",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2734,20 +2735,20 @@
|
||||
],
|
||||
"description": "Symfony Translation Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-05 17:45:07"
|
||||
"time": "2016-02-02 13:44:19"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "87db8700deb12ba2b65e858f656a1f885530bcb0"
|
||||
"reference": "24bb94807eff00db49374c37ebf56a0304e8aef3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/87db8700deb12ba2b65e858f656a1f885530bcb0",
|
||||
"reference": "87db8700deb12ba2b65e858f656a1f885530bcb0",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/24bb94807eff00db49374c37ebf56a0304e8aef3",
|
||||
"reference": "24bb94807eff00db49374c37ebf56a0304e8aef3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2797,7 +2798,7 @@
|
||||
"debug",
|
||||
"dump"
|
||||
],
|
||||
"time": "2015-12-05 11:13:14"
|
||||
"time": "2016-01-07 13:38:51"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
@@ -3182,22 +3183,24 @@
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "v1.5.0",
|
||||
"version": "v1.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy.git",
|
||||
"reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7"
|
||||
"reference": "3c91bdf81797d725b14cb62906f9a4ce44235972"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7",
|
||||
"reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972",
|
||||
"reference": "3c91bdf81797d725b14cb62906f9a4ce44235972",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.0.2",
|
||||
"php": "^5.3|^7.0",
|
||||
"phpdocumentor/reflection-docblock": "~2.0",
|
||||
"sebastian/comparator": "~1.1"
|
||||
"sebastian/comparator": "~1.1",
|
||||
"sebastian/recursion-context": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/phpspec": "~2.0"
|
||||
@@ -3205,7 +3208,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.4.x-dev"
|
||||
"dev-master": "1.5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -3238,7 +3241,7 @@
|
||||
"spy",
|
||||
"stub"
|
||||
],
|
||||
"time": "2015-08-13 10:07:40"
|
||||
"time": "2016-02-15 07:46:21"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -3482,16 +3485,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "4.8.21",
|
||||
"version": "4.8.23",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "ea76b17bced0500a28098626b84eda12dbcf119c"
|
||||
"reference": "6e351261f9cd33daf205a131a1ba61c6d33bd483"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea76b17bced0500a28098626b84eda12dbcf119c",
|
||||
"reference": "ea76b17bced0500a28098626b84eda12dbcf119c",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e351261f9cd33daf205a131a1ba61c6d33bd483",
|
||||
"reference": "6e351261f9cd33daf205a131a1ba61c6d33bd483",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3550,7 +3553,7 @@
|
||||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2015-12-12 07:45:58"
|
||||
"time": "2016-02-11 14:56:33"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit-mock-objects",
|
||||
@@ -3981,16 +3984,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/css-selector.git",
|
||||
"reference": "4613311fd46e146f506403ce2f8a0c71d402d2a3"
|
||||
"reference": "6605602690578496091ac20ec7a5cbd160d4dff4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/4613311fd46e146f506403ce2f8a0c71d402d2a3",
|
||||
"reference": "4613311fd46e146f506403ce2f8a0c71d402d2a3",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/6605602690578496091ac20ec7a5cbd160d4dff4",
|
||||
"reference": "6605602690578496091ac20ec7a5cbd160d4dff4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4030,20 +4033,20 @@
|
||||
],
|
||||
"description": "Symfony CssSelector Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-05 17:45:07"
|
||||
"time": "2016-01-27 05:14:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dom-crawler",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dom-crawler.git",
|
||||
"reference": "7c622b0c9fb8bdb146d6dfa86c5f91dcbfdbc11d"
|
||||
"reference": "b693a9650aa004576b593ff2e91ae749dc90123d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7c622b0c9fb8bdb146d6dfa86c5f91dcbfdbc11d",
|
||||
"reference": "7c622b0c9fb8bdb146d6dfa86c5f91dcbfdbc11d",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b693a9650aa004576b593ff2e91ae749dc90123d",
|
||||
"reference": "b693a9650aa004576b593ff2e91ae749dc90123d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4086,20 +4089,20 @@
|
||||
],
|
||||
"description": "Symfony DomCrawler Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-26 13:42:31"
|
||||
"time": "2016-01-25 09:56:57"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v3.0.1",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "3df409958a646dad2bc5046c3fb671ee24a1a691"
|
||||
"reference": "3cf0709d7fe936e97bee9e954382e449003f1d9a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/3df409958a646dad2bc5046c3fb671ee24a1a691",
|
||||
"reference": "3df409958a646dad2bc5046c3fb671ee24a1a691",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/3cf0709d7fe936e97bee9e954382e449003f1d9a",
|
||||
"reference": "3cf0709d7fe936e97bee9e954382e449003f1d9a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4135,7 +4138,7 @@
|
||||
],
|
||||
"description": "Symfony Yaml Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-12-26 13:39:53"
|
||||
"time": "2016-02-02 13:44:19"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateUsersTable extends Migration
|
||||
$table->string('email')->unique();
|
||||
$table->string('password', 60);
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
\BookStack\User::forceCreate([
|
||||
|
||||
@@ -17,7 +17,7 @@ class CreateBooksTable extends Migration
|
||||
$table->string('name');
|
||||
$table->string('slug')->indexed();
|
||||
$table->text('description');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class CreatePagesTable extends Migration
|
||||
$table->longText('html');
|
||||
$table->longText('text');
|
||||
$table->integer('priority');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class CreateImagesTable extends Migration
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->string('url');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class CreateChaptersTable extends Migration
|
||||
$table->text('name');
|
||||
$table->text('description');
|
||||
$table->integer('priority');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration
|
||||
$table->longText('html');
|
||||
$table->longText('text');
|
||||
$table->integer('created_by');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration
|
||||
$table->integer('user_id');
|
||||
$table->integer('entity_id');
|
||||
$table->string('entity_type');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class AddRolesAndPermissions extends Migration
|
||||
$table->string('name')->unique();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
// Create table for associating roles to users (Many-to-Many)
|
||||
@@ -50,7 +50,7 @@ class AddRolesAndPermissions extends Migration
|
||||
$table->string('name')->unique();
|
||||
$table->string('display_name')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
|
||||
// Create table for associating permissions to roles (Many-to-Many)
|
||||
|
||||
@@ -15,7 +15,7 @@ class CreateSettingsTable extends Migration
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
$table->string('setting_key')->primary()->indexed();
|
||||
$table->text('value');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration
|
||||
$table->string('driver')->index();
|
||||
$table->string('driver_id');
|
||||
$table->string('avatar');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration
|
||||
$table->increments('id');
|
||||
$table->integer('user_id')->index();
|
||||
$table->string('token')->index();
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateViewsTable extends Migration
|
||||
$table->integer('viewable_id');
|
||||
$table->string('viewable_type');
|
||||
$table->integer('views');
|
||||
$table->timestamps();
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
|
||||
$table->dropColumn('external_auth_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSlugToRevisions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->string('slug');
|
||||
$table->index('slug');
|
||||
$table->string('book_slug');
|
||||
$table->index('book_slug');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('page_revisions', function (Blueprint $table) {
|
||||
$table->dropColumn('slug');
|
||||
$table->dropColumn('book_slug');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,13 @@
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||
<env name="MAIL_PRETEND" value="true"/>
|
||||
<env name="MAIL_DRIVER" value="log"/>
|
||||
<env name="AUTH_METHOD" value="standard"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
||||
<env name="LDAP_VERSION" value="3"/>
|
||||
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||
<env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"css/styles.css": "css/styles.css?version=8d91c6c",
|
||||
"css/print-styles.css": "css/print-styles.css?version=8d91c6c",
|
||||
"js/common.js": "js/common.js?version=8d91c6c"
|
||||
"css/styles.css": "css/styles.css?version=a50dcfa",
|
||||
"css/print-styles.css": "css/print-styles.css?version=a50dcfa",
|
||||
"js/common.js": "js/common.js?version=a50dcfa"
|
||||
}
|
||||
2
public/css/export-styles.css
vendored
2
public/css/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/css/styles.css
vendored
2
public/css/styles.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/uploads/.gitignore
vendored
Normal file
2
public/uploads/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
19
readme.md
19
readme.md
@@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS
|
||||
|
||||
## Requirements
|
||||
|
||||
BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing.
|
||||
BookStack has similar requirements to Laravel:
|
||||
|
||||
* PHP >= 5.5.9, Will need to be usable from the command line.
|
||||
* OpenSSL PHP Extension
|
||||
* PDO PHP Extension
|
||||
* MBstring PHP Extension
|
||||
* Tokenizer PHP Extension
|
||||
* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD`
|
||||
* MySQL >= 5.6
|
||||
* Git (Not strictly required but helps manage updates)
|
||||
* [Composer](https://getcomposer.org/)
|
||||
* [Node.js](https://nodejs.org/en/) **Development Only**
|
||||
* [Gulp](http://gulpjs.com/) **Development Only**
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui
|
||||
|
||||
You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID.
|
||||
|
||||
## Testing
|
||||
## Development & Testing
|
||||
|
||||
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
|
||||
|
||||
* [Node.js](https://nodejs.org/en/) **Development Only**
|
||||
* [Gulp](http://gulpjs.com/) **Development Only**
|
||||
|
||||
SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp.
|
||||
|
||||
BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function (ngApp) {
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||
@@ -17,21 +17,40 @@ module.exports = function (ngApp) {
|
||||
var dataLoaded = false;
|
||||
var callback = false;
|
||||
|
||||
/**
|
||||
* Simple returns the appropriate upload url depending on the image type set.
|
||||
* @returns {string}
|
||||
*/
|
||||
$scope.getUploadUrl = function () {
|
||||
return '/images/' + $scope.imageType + '/upload';
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs on image upload, Adds an image to local list of images
|
||||
* and shows a success message to the user.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccess = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
$scope.images.unshift(data);
|
||||
});
|
||||
events.emit('success', 'Image uploaded');
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs the callback and hides the image manager.
|
||||
* @param returnData
|
||||
*/
|
||||
function callbackAndHide(returnData) {
|
||||
if (callback) callback(returnData);
|
||||
$scope.showing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image select action. Checks if a double-click was fired.
|
||||
* @param image
|
||||
*/
|
||||
$scope.imageSelect = function (image) {
|
||||
var dblClickTime = 300;
|
||||
var currentTime = Date.now();
|
||||
@@ -48,10 +67,19 @@ module.exports = function (ngApp) {
|
||||
previousClickTime = currentTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Action that runs when the 'Select image' button is clicked.
|
||||
* Runs the callback and hides the image manager.
|
||||
*/
|
||||
$scope.selectButtonClick = function () {
|
||||
callbackAndHide($scope.selectedImage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the image manager.
|
||||
* Takes a callback to execute later on.
|
||||
* @param doneCallback
|
||||
*/
|
||||
function show(doneCallback) {
|
||||
callback = doneCallback;
|
||||
$scope.showing = true;
|
||||
@@ -62,6 +90,8 @@ module.exports = function (ngApp) {
|
||||
}
|
||||
}
|
||||
|
||||
// Connects up the image manger so it can be used externally
|
||||
// such as from TinyMCE.
|
||||
imageManagerService.show = show;
|
||||
imageManagerService.showExternal = function (doneCallback) {
|
||||
$scope.$apply(() => {
|
||||
@@ -70,10 +100,16 @@ module.exports = function (ngApp) {
|
||||
};
|
||||
window.ImageManager = imageManagerService;
|
||||
|
||||
/**
|
||||
* Hide the image manager
|
||||
*/
|
||||
$scope.hide = function () {
|
||||
$scope.showing = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the list image data from the server.
|
||||
*/
|
||||
function fetchData() {
|
||||
var url = '/images/' + $scope.imageType + '/all/' + page;
|
||||
$http.get(url).then((response) => {
|
||||
@@ -82,28 +118,33 @@ module.exports = function (ngApp) {
|
||||
page++;
|
||||
});
|
||||
}
|
||||
$scope.fetchData = fetchData;
|
||||
|
||||
/**
|
||||
* Save the details of an image.
|
||||
* @param event
|
||||
*/
|
||||
$scope.saveImageDetails = function (event) {
|
||||
event.preventDefault();
|
||||
var url = '/images/update/' + $scope.selectedImage.id;
|
||||
$http.put(url, this.selectedImage).then((response) => {
|
||||
$scope.imageUpdateSuccess = true;
|
||||
$timeout(() => {
|
||||
$scope.imageUpdateSuccess = false;
|
||||
}, 3000);
|
||||
events.emit('success', 'Image details updated');
|
||||
}, (response) => {
|
||||
var errors = response.data;
|
||||
var message = '';
|
||||
Object.keys(errors).forEach((key) => {
|
||||
message += errors[key].join('\n');
|
||||
});
|
||||
$scope.imageUpdateFailure = message;
|
||||
$timeout(() => {
|
||||
$scope.imageUpdateFailure = false;
|
||||
}, 5000);
|
||||
events.emit('error', message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an image from system and notify of success.
|
||||
* Checks if it should force delete when an image
|
||||
* has dependant pages.
|
||||
* @param event
|
||||
*/
|
||||
$scope.deleteImage = function (event) {
|
||||
event.preventDefault();
|
||||
var force = $scope.dependantPages !== false;
|
||||
@@ -112,10 +153,7 @@ module.exports = function (ngApp) {
|
||||
$http.delete(url).then((response) => {
|
||||
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
|
||||
$scope.selectedImage = false;
|
||||
$scope.imageDeleteSuccess = true;
|
||||
$timeout(() => {
|
||||
$scope.imageDeleteSuccess = false;
|
||||
}, 3000);
|
||||
events.emit('success', 'Image successfully deleted');
|
||||
}, (response) => {
|
||||
// Pages failure
|
||||
if (response.status === 400) {
|
||||
@@ -124,6 +162,15 @@ module.exports = function (ngApp) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple date creator used to properly format dates.
|
||||
* @param stringDate
|
||||
* @returns {Date}
|
||||
*/
|
||||
$scope.getDate = function(stringDate) {
|
||||
return new Date(stringDate);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html');
|
||||
var imagePickerTemplate = require('./components/image-picker.html');
|
||||
var dropZoneTemplate = require('./components/drop-zone.html');
|
||||
|
||||
module.exports = function (ngApp) {
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
/**
|
||||
* Toggle Switches
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
var angular = require('angular');
|
||||
@@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate');
|
||||
var ngSanitize = require('angular-sanitize');
|
||||
|
||||
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
|
||||
var services = require('./services')(ngApp);
|
||||
var directives = require('./directives')(ngApp);
|
||||
var controllers = require('./controllers')(ngApp);
|
||||
|
||||
|
||||
// Global Event System
|
||||
var Events = {
|
||||
listeners: {},
|
||||
emit: function (eventName, eventData) {
|
||||
if (typeof this.listeners[eventName] === 'undefined') return this;
|
||||
var eventsToStart = this.listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
var event = eventsToStart[i];
|
||||
event(eventData);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
listen: function (eventName, callback) {
|
||||
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
|
||||
this.listeners[eventName].push(callback);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
window.Events = Events;
|
||||
|
||||
var services = require('./services')(ngApp, Events);
|
||||
var directives = require('./directives')(ngApp, Events);
|
||||
var controllers = require('./controllers')(ngApp, Events);
|
||||
|
||||
//Global jQuery Config & Extensions
|
||||
|
||||
@@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
|
||||
// Global jQuery Elements
|
||||
$(function () {
|
||||
|
||||
|
||||
var notifications = $('.notification');
|
||||
var successNotification = notifications.filter('.pos');
|
||||
var errorNotification = notifications.filter('.neg');
|
||||
// Notification Events
|
||||
window.Events.listen('success', function (text) {
|
||||
successNotification.hide();
|
||||
successNotification.find('span').text(text);
|
||||
setTimeout(() => {
|
||||
successNotification.show();
|
||||
}, 1);
|
||||
});
|
||||
window.Events.listen('error', function (text) {
|
||||
errorNotification.find('span').text(text);
|
||||
errorNotification.show();
|
||||
});
|
||||
|
||||
// Notification hiding
|
||||
$('.notification').click(function () {
|
||||
notifications.click(function () {
|
||||
$(this).fadeOut(100);
|
||||
});
|
||||
|
||||
@@ -44,6 +83,35 @@ $(function () {
|
||||
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
|
||||
});
|
||||
|
||||
// Back to top button
|
||||
$('#back-to-top').click(function() {
|
||||
$('#header').smoothScrollTo();
|
||||
});
|
||||
var scrollTopShowing = false;
|
||||
var scrollTop = document.getElementById('back-to-top');
|
||||
var scrollTopBreakpoint = 1200;
|
||||
window.addEventListener('scroll', function() {
|
||||
if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
|
||||
scrollTop.style.display = 'block';
|
||||
scrollTopShowing = true;
|
||||
setTimeout(() => {
|
||||
scrollTop.style.opacity = 1;
|
||||
}, 1);
|
||||
} else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
|
||||
scrollTop.style.opacity = 0;
|
||||
scrollTopShowing = false;
|
||||
setTimeout(() => {
|
||||
scrollTop.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Common jQuery actions
|
||||
$('[data-action="expand-entity-list-details"]').click(function() {
|
||||
$('.entity-list.compact').find('p').slideToggle(240);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ module.exports = {
|
||||
statusbar: false,
|
||||
menubar: false,
|
||||
paste_data_images: false,
|
||||
//height: 700,
|
||||
extended_valid_elements: 'pre[*]',
|
||||
automatic_uploads: false,
|
||||
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
|
||||
@@ -31,7 +30,7 @@ module.exports = {
|
||||
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
|
||||
},
|
||||
file_browser_callback: function (field_name, url, type, win) {
|
||||
ImageManager.show(function (image) {
|
||||
window.ImageManager.showExternal(function (image) {
|
||||
win.document.getElementById(field_name).value = image.url;
|
||||
if ("createEvent" in document) {
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
@@ -40,6 +39,10 @@ module.exports = {
|
||||
} else {
|
||||
win.document.getElementById(field_name).fireEvent("onchange");
|
||||
}
|
||||
var html = '<a href="' + image.url + '" target="_blank">';
|
||||
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
|
||||
html += '</a>';
|
||||
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
|
||||
});
|
||||
},
|
||||
paste_preprocess: function (plugin, args) {
|
||||
|
||||
@@ -13,7 +13,7 @@ window.setupPageShow = module.exports = function (pageId) {
|
||||
var isSelection = false;
|
||||
|
||||
// Select all contents on input click
|
||||
$pointer.on('click', 'input', function(e) {
|
||||
$pointer.on('click', 'input', function (e) {
|
||||
$(this).select();
|
||||
e.stopPropagation();
|
||||
});
|
||||
@@ -30,6 +30,7 @@ window.setupPageShow = module.exports = function (pageId) {
|
||||
|
||||
// Show pointer when selecting a single block of tagged content
|
||||
$('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
|
||||
e.stopPropagation();
|
||||
var selection = window.getSelection();
|
||||
if (selection.toString().length === 0) return;
|
||||
|
||||
@@ -47,8 +48,6 @@ window.setupPageShow = module.exports = function (pageId) {
|
||||
var pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
|
||||
$pointerInner.css('left', pointerLeftOffsetPercent + '%');
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
isSelection = true;
|
||||
setTimeout(() => {
|
||||
isSelection = false;
|
||||
@@ -72,4 +71,43 @@ window.setupPageShow = module.exports = function (pageId) {
|
||||
goToText(text);
|
||||
}
|
||||
|
||||
};
|
||||
// Make the book-tree sidebar stick in view on scroll
|
||||
var $window = $(window);
|
||||
var $bookTree = $(".book-tree");
|
||||
// Check the page is scrollable and the content is taller than the tree
|
||||
var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
|
||||
// Get current tree's width and header height
|
||||
var headerHeight = $("#header").height() + $(".toolbar").height();
|
||||
var isFixed = $window.scrollTop() > headerHeight;
|
||||
var bookTreeWidth = $bookTree.width();
|
||||
// Function to fix the tree as a sidebar
|
||||
function stickTree() {
|
||||
$bookTree.width(bookTreeWidth + 48 + 15);
|
||||
$bookTree.addClass("fixed");
|
||||
isFixed = true;
|
||||
}
|
||||
// Function to un-fix the tree back into position
|
||||
function unstickTree() {
|
||||
$bookTree.css('width', 'auto');
|
||||
$bookTree.removeClass("fixed");
|
||||
isFixed = false;
|
||||
}
|
||||
// Checks if the tree stickiness state should change
|
||||
function checkTreeStickiness(skipCheck) {
|
||||
var shouldBeFixed = $window.scrollTop() > headerHeight;
|
||||
if (shouldBeFixed && (!isFixed || skipCheck)) {
|
||||
stickTree();
|
||||
} else if (!shouldBeFixed && (isFixed || skipCheck)) {
|
||||
unstickTree();
|
||||
}
|
||||
}
|
||||
// If the page is scrollable and the window is wide enough listen to scroll events
|
||||
// and evaluate tree stickiness.
|
||||
if (pageScrollable && $window.width() > 1000) {
|
||||
$window.scroll(function() {
|
||||
checkTreeStickiness(false);
|
||||
});
|
||||
checkTreeStickiness(true);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(ngApp) {
|
||||
module.exports = function(ngApp, events) {
|
||||
|
||||
ngApp.factory('imageManagerService', function() {
|
||||
return {
|
||||
|
||||
@@ -139,54 +139,6 @@ form.search-box {
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-container ul {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
list-style: none;
|
||||
right: 0;
|
||||
margin: $-m 0;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: 1px;
|
||||
border: 1px solid #EEE;
|
||||
min-width: 180px;
|
||||
padding: $-xs 0;
|
||||
color: #555;
|
||||
text-align: left !important;
|
||||
&.wide {
|
||||
min-width: 220px;
|
||||
}
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
padding: $-xs $-m;
|
||||
color: #555;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #EEE;
|
||||
}
|
||||
i {
|
||||
margin-right: $-m;
|
||||
padding-right: 0;
|
||||
display: inline;
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
li.border-bottom {
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs span.sep {
|
||||
color: #aaa;
|
||||
padding: 0 $-xs;
|
||||
@@ -209,7 +161,7 @@ form.search-box {
|
||||
.faded-small {
|
||||
color: #000;
|
||||
font-size: 0.9em;
|
||||
background-color: rgba(21, 101, 192, 0.15);
|
||||
background-color: $primary-faded;
|
||||
}
|
||||
|
||||
.breadcrumbs .text-button, .action-buttons .text-button {
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
max-width: 1340px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -44,18 +43,49 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.image-manager-list img {
|
||||
.image-manager-list .image {
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
float: left;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: (100%/6);
|
||||
height: auto;
|
||||
border: 1px solid #FFF;
|
||||
border: 1px solid #DDD;
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
|
||||
overflow: hidden;
|
||||
&.selected {
|
||||
transform: scale3d(0.92, 0.92, 0.92);
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.image-meta {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #EEE;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
font-size: 10px;
|
||||
padding: 3px 4px;
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include smaller-than($xl) {
|
||||
width: (100%/4);
|
||||
}
|
||||
@include smaller-than($m) {
|
||||
.image-meta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,27 @@
|
||||
|
||||
// Sidebar list
|
||||
.book-tree {
|
||||
margin-top: $-xl;
|
||||
padding: $-xl 0 0 0;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: ease-in-out 240ms;
|
||||
transition-property: right, border;
|
||||
border-left: 0px solid #FFF;
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-left: $-l;
|
||||
padding-right: $-l + 15;
|
||||
width: 30%;
|
||||
right: -15px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
-ms-overflow-style: none;
|
||||
//background-color: $primary-faded;
|
||||
border-left: 1px solid #DDD;
|
||||
&::-webkit-scrollbar { width: 0 !important }
|
||||
}
|
||||
}
|
||||
.book-tree h4 {
|
||||
padding: $-m $-s 0 $-s;
|
||||
@@ -105,16 +125,14 @@
|
||||
}
|
||||
.book-tree .sidebar-page-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin-top: $-xs;
|
||||
margin: $-xs 0 0;
|
||||
padding-left: 0;
|
||||
border-left: 5px solid $color-book;
|
||||
li a {
|
||||
display: block;
|
||||
border-bottom: none;
|
||||
padding-left: $-s;
|
||||
padding: $-xs 0 $-xs $-s;
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -165,6 +183,7 @@
|
||||
}
|
||||
.sub-menu {
|
||||
display: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
.sub-menu.open {
|
||||
display: block;
|
||||
@@ -264,4 +283,87 @@ ul.pagination {
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-list {
|
||||
>div {
|
||||
padding: $-m 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
margin: $-xs 0 0 0;
|
||||
}
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
.text-small.text-muted {
|
||||
color: #AAA;
|
||||
font-size: 0.75em;
|
||||
margin-top: $-xs;
|
||||
}
|
||||
}
|
||||
.entity-list.compact {
|
||||
font-size: 0.6em;
|
||||
h3, a {
|
||||
line-height: 1.2;
|
||||
}
|
||||
p {
|
||||
display: none;
|
||||
font-size: $fs-m * 0.8;
|
||||
padding-top: $-xs;
|
||||
margin: 0;
|
||||
}
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-container ul {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
list-style: none;
|
||||
right: 0;
|
||||
margin: $-m 0;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: 1px;
|
||||
border: 1px solid #EEE;
|
||||
min-width: 180px;
|
||||
padding: $-xs 0;
|
||||
color: #555;
|
||||
text-align: left !important;
|
||||
&.wide {
|
||||
min-width: 220px;
|
||||
}
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
padding: $-xs $-m;
|
||||
color: #555;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #EEE;
|
||||
}
|
||||
i {
|
||||
margin-right: $-m;
|
||||
padding-right: 0;
|
||||
display: inline;
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
li.border-bottom {
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
background-color: #FFF;
|
||||
border: 1px solid #DDD;
|
||||
color: #666;
|
||||
width: 180px;
|
||||
width: 172px;
|
||||
z-index: 40;
|
||||
}
|
||||
input, button {
|
||||
|
||||
@@ -20,4 +20,8 @@ table.table {
|
||||
|
||||
table {
|
||||
max-width: 100%;
|
||||
thead {
|
||||
background-color: #F8F8F8;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,8 @@ pre {
|
||||
box-shadow: 0 1px 2px 0px rgba(10, 10, 10, 0.06);
|
||||
border: 1px solid rgba(221, 221, 221, 0.66);
|
||||
background-color: #fdf6e3;
|
||||
padding: 0.5em;
|
||||
padding: $-s;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@@ -223,13 +224,13 @@ span.highlight {
|
||||
* Lists
|
||||
*/
|
||||
ul {
|
||||
list-style: disc;
|
||||
margin-left: $-m*1.5;
|
||||
padding-left: $-m * 1.5;
|
||||
list-style: disc inside;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
margin-left: $-m*1.5;
|
||||
list-style: decimal inside;
|
||||
padding-left: $-m * 1.5;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -251,6 +252,18 @@ ol {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-bigger {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.text-large {
|
||||
font-size: 1.6666em;
|
||||
}
|
||||
|
||||
.no-color {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouping
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,7 @@ $primary-dark: #0288D1;
|
||||
$secondary: #e27b41;
|
||||
$positive: #52A256;
|
||||
$negative: #E84F4F;
|
||||
$primary-faded: rgba(21, 101, 192, 0.15);
|
||||
|
||||
// Item Colors
|
||||
$color-book: #009688;
|
||||
|
||||
@@ -9,4 +9,9 @@
|
||||
@import "tables";
|
||||
@import "header";
|
||||
@import "lists";
|
||||
@import "pages";
|
||||
@import "pages";
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
@@ -47,6 +47,13 @@ body.dragging, body.dragging * {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
&.huge {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
&.square {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
// System wide notifications
|
||||
@@ -126,4 +133,43 @@ $loadingSize: 10px;
|
||||
i {
|
||||
padding-right: $-s;
|
||||
}
|
||||
}
|
||||
|
||||
// Back to top link
|
||||
$btt-size: 40px;
|
||||
#back-to-top {
|
||||
background-color: rgba($primary, 0.4);
|
||||
position: fixed;
|
||||
bottom: $-m;
|
||||
right: $-l;
|
||||
padding: $-xs $-s;
|
||||
cursor: pointer;
|
||||
color: #FFF;
|
||||
width: $btt-size;
|
||||
height: $btt-size;
|
||||
border-radius: $btt-size;
|
||||
transition: all ease-in-out 180ms;
|
||||
opacity: 0;
|
||||
z-index: 999;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
width: $btt-size*3.4;
|
||||
background-color: rgba($primary, 1);
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.inner {
|
||||
width: $btt-size*3.4;
|
||||
}
|
||||
i {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
padding: 0 $-s 0 0;
|
||||
}
|
||||
span {
|
||||
line-height: 12px;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
'page_update' => 'updated page',
|
||||
'page_update_notification' => 'Page Successfully Updated',
|
||||
'page_delete' => 'deleted page',
|
||||
'page_delete_notification' => 'Page Successfully Created',
|
||||
'page_delete_notification' => 'Page Successfully Deleted',
|
||||
'page_restore' => 'restored page',
|
||||
'page_restore_notification' => 'Page Successfully Restored',
|
||||
|
||||
@@ -35,4 +35,4 @@ return [
|
||||
'book_sort' => 'sorted book',
|
||||
'book_sort_notification' => 'Book Successfully Re-sorted',
|
||||
|
||||
];
|
||||
];
|
||||
|
||||
@@ -58,10 +58,13 @@
|
||||
</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-lg"></i>Edit Profile</a>
|
||||
<a href="/user/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-account zmdi-hc-fw zmdi-hc-lg"></i>View Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-lg"></i>Logout</a>
|
||||
<a href="/settings/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-fw zmdi-hc-lg"></i>Edit Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-fw zmdi-hc-lg"></i>Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -77,6 +80,11 @@
|
||||
@yield('content')
|
||||
</section>
|
||||
|
||||
<div id="back-to-top">
|
||||
<div class="inner">
|
||||
<i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span>
|
||||
</div>
|
||||
</div>
|
||||
@yield('bottom')
|
||||
<script src="{{ versioned_asset('js/common.js') }}"></script>
|
||||
@yield('scripts')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small">
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-1"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small" ng-non-bindable>
|
||||
<div class="faded-small toolbar" ng-non-bindable>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<p class="text-muted">{{ $chapter->getExcerpt() }}</p>
|
||||
@endif
|
||||
|
||||
@if(count($chapter->pages) > 0 && !isset($hidePages))
|
||||
@if(!isset($hidePages) && count($chapter->pages) > 0)
|
||||
<p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
|
||||
<div class="inset-list">
|
||||
@foreach($chapter->pages as $page)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small" ng-non-bindable>
|
||||
<div class="faded-small toolbar" ng-non-bindable>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 faded">
|
||||
|
||||
10
resources/views/errors/500.blade.php
Normal file
10
resources/views/errors/500.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
@extends('base')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<h1 class="text-muted">An Error Occurred</h1>
|
||||
<p>{{ $message }}</p>
|
||||
</div>
|
||||
|
||||
@stop
|
||||
@@ -2,20 +2,44 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 faded">
|
||||
<div class="action-buttons text-left">
|
||||
<a data-action="expand-entity-list-details" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>Toggle Details</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8 faded">
|
||||
<div class="action-buttons">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" ng-non-bindable>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-7">
|
||||
<div class="col-sm-4">
|
||||
@if($signedIn)
|
||||
<h2>My Recently Viewed</h2>
|
||||
<h3>My Recently Viewed</h3>
|
||||
@else
|
||||
<h2>Recent Books</h2>
|
||||
<h3>Recent Books</h3>
|
||||
@endif
|
||||
@include('partials/entity-list', ['entities' => $recents])
|
||||
@include('partials/entity-list', ['entities' => $recents, 'style' => 'compact'])
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<div class="margin-top large"> </div>
|
||||
<div class="col-sm-4">
|
||||
<h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
|
||||
|
||||
<h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" id="recent-activity">
|
||||
<h3>Recent Activity</h3>
|
||||
@include('partials/activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
|
||||
18
resources/views/pages/detailed-listing.blade.php
Normal file
18
resources/views/pages/detailed-listing.blade.php
Normal file
@@ -0,0 +1,18 @@
|
||||
@extends('base')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-7">
|
||||
<h1>{{ $title }}</h1>
|
||||
@include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
|
||||
{!! $pages->links() !!}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 col-sm-offset-1"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
@@ -4,15 +4,15 @@
|
||||
<div class="page-editor flex-fill flex" ng-non-bindable>
|
||||
|
||||
{{ csrf_field() }}
|
||||
<div class="faded-small">
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 faded">
|
||||
<div class="col-sm-4 faded">
|
||||
<div class="action-buttons text-left">
|
||||
<a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 faded">
|
||||
<div class="col-sm-8 faded">
|
||||
<div class="action-buttons">
|
||||
<a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a>
|
||||
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
|
||||
|
||||
@@ -3,18 +3,29 @@
|
||||
<a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a>
|
||||
</h3>
|
||||
|
||||
@if(isset($showMeta) && $showMeta)
|
||||
<div class="meta">
|
||||
<span class="text-book"><i class="zmdi zmdi-book"></i> {{ $page->book->name }}</span>
|
||||
@if($page->chapter)
|
||||
<span class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i> {{ $page->chapter->name }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(isset($page->searchSnippet))
|
||||
<p class="text-muted">{!! $page->searchSnippet !!}</p>
|
||||
@else
|
||||
<p class="text-muted">{{ $page->getExcerpt() }}</p>
|
||||
@endif
|
||||
|
||||
@if(isset($style) && $style === 'detailed')
|
||||
<div class="row meta text-muted text-small">
|
||||
<div class="col-md-4">
|
||||
Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif <br>
|
||||
Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a>
|
||||
<br>
|
||||
@if($page->chapter)
|
||||
<a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName(30) }}</a>
|
||||
@else
|
||||
<i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
</div>
|
||||
@@ -20,5 +20,11 @@
|
||||
table td {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.page-content img.align-left, .page-content img.align-right {
|
||||
float: none !important;
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@stop
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small">
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 faded">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small">
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 faded">
|
||||
@@ -22,9 +22,9 @@
|
||||
<span dropdown class="dropdown-container">
|
||||
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
|
||||
<ul class="wide">
|
||||
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
|
||||
</ul>
|
||||
</span>
|
||||
@if($currentUser->can('page-update'))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="right" ng-non-bindable>
|
||||
@if($activity->user)
|
||||
{{$activity->user->name}}
|
||||
<a href="/user/{{ $activity->user->id }}">{{$activity->user->name}}</a>
|
||||
@else
|
||||
A deleted user
|
||||
@endif
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
|
||||
@if(count($entities) > 0)
|
||||
@foreach($entities as $index => $entity)
|
||||
@if($entity->isA('page'))
|
||||
@include('pages/list-item', ['page' => $entity])
|
||||
@elseif($entity->isA('book'))
|
||||
@include('books/list-item', ['book' => $entity])
|
||||
@elseif($entity->isA('chapter'))
|
||||
@include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true])
|
||||
@endif
|
||||
<div class="entity-list @if(isset($style)){{ $style }}@endif" ng-non-bindable>
|
||||
@if(count($entities) > 0)
|
||||
@foreach($entities as $index => $entity)
|
||||
@if($entity->isA('page'))
|
||||
@include('pages/list-item', ['page' => $entity])
|
||||
@elseif($entity->isA('book'))
|
||||
@include('books/list-item', ['book' => $entity])
|
||||
@elseif($entity->isA('chapter'))
|
||||
@include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true])
|
||||
@endif
|
||||
|
||||
@if($index !== count($entities) - 1)
|
||||
<hr>
|
||||
@endif
|
||||
@if($index !== count($entities) - 1)
|
||||
<hr>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
@else
|
||||
<p class="text-muted">
|
||||
No items available
|
||||
</p>
|
||||
@endif
|
||||
@endforeach
|
||||
@else
|
||||
<p class="text-muted">
|
||||
No items available
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@@ -5,11 +5,14 @@
|
||||
<div class="image-manager-content">
|
||||
<div class="image-manager-list">
|
||||
<div ng-repeat="image in images">
|
||||
<img class="anim fadeIn"
|
||||
ng-class="{selected: (image==selectedImage)}"
|
||||
ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"
|
||||
ng-click="imageSelect(image)"
|
||||
ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}">
|
||||
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
||||
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
|
||||
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
|
||||
<div class="image-meta">
|
||||
<span class="name" ng-bind="image.name"></span>
|
||||
<span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
|
||||
</div>
|
||||
@@ -19,18 +22,20 @@
|
||||
|
||||
<div class="image-manager-sidebar">
|
||||
<h2>Images</h2>
|
||||
<hr class="even">
|
||||
<drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone>
|
||||
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
|
||||
|
||||
<hr class="even">
|
||||
|
||||
<form ng-submit="saveImageDetails($event)">
|
||||
<div>
|
||||
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
|
||||
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">Image Name</label>
|
||||
<input type="text" id="name" name="name" ng-model="selectedImage.name">
|
||||
<p class="text-pos text-small" ng-show="imageUpdateSuccess"><i class="fa fa-check"></i> Image name updated</p>
|
||||
<p class="text-neg text-small" ng-show="imageUpdateFailure"><i class="fa fa-times"></i> <span ng-bind="imageUpdateFailure"></span></p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -53,8 +58,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-pos" ng-show="imageDeleteSuccess"><i class="fa fa-check"></i> Image deleted</p>
|
||||
|
||||
<div class="image-manager-bottom">
|
||||
<button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
|
||||
<i class="zmdi zmdi-square-right"></i>Select Image
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
@if(Session::has('success'))
|
||||
<div class="notification anim pos">
|
||||
<i class="zmdi zmdi-mood"></i> <span>{{ Session::get('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(Session::has('error'))
|
||||
<div class="notification anim neg stopped">
|
||||
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif>
|
||||
<i class="zmdi zmdi-check-circle"></i> <span>{{ Session::get('success') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif>
|
||||
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
|
||||
</div>
|
||||
|
||||
@@ -6,41 +6,36 @@
|
||||
|
||||
<h1>Search Results <span class="text-muted">{{$searchTerm}}</span></h1>
|
||||
|
||||
<p>
|
||||
<a href="/search/pages?term={{$searchTerm}}" class="text-page"><i class="zmdi zmdi-file-text"></i>View all matched pages</a>
|
||||
|
||||
@if(count($chapters) > 0)
|
||||
|
||||
<a href="/search/chapters?term={{$searchTerm}}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>View all matched chapters</a>
|
||||
@endif
|
||||
|
||||
@if(count($books) > 0)
|
||||
|
||||
<a href="/search/books?term={{$searchTerm}}" class="text-book"><i class="zmdi zmdi-book"></i>View all matched books</a>
|
||||
@endif
|
||||
</p>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>Matching Pages</h3>
|
||||
<div class="page-list">
|
||||
@if(count($pages) > 0)
|
||||
@foreach($pages as $page)
|
||||
@include('pages/list-item', ['page' => $page, 'showMeta' => true])
|
||||
<hr>
|
||||
@endforeach
|
||||
@else
|
||||
<p class="text-muted">No pages matched this search</p>
|
||||
@endif
|
||||
</div>
|
||||
<h3><a href="/search/pages?term={{$searchTerm}}" class="no-color">Matching Pages</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
|
||||
</div>
|
||||
|
||||
<div class="col-md-5 col-md-offset-1">
|
||||
|
||||
@if(count($books) > 0)
|
||||
<h3>Matching Books</h3>
|
||||
<div class="page-list">
|
||||
@foreach($books as $book)
|
||||
@include('books/list-item', ['book' => $book])
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
<h3><a href="/search/books?term={{$searchTerm}}" class="no-color">Matching Books</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $books])
|
||||
@endif
|
||||
|
||||
@if(count($chapters) > 0)
|
||||
<h3>Matching Chapters</h3>
|
||||
<div class="page-list">
|
||||
@foreach($chapters as $chapter)
|
||||
@include('chapters/list-item', ['chapter' => $chapter, 'hidePages' => true])
|
||||
@endforeach
|
||||
</div>
|
||||
<h3><a href="/search/chapters?term={{$searchTerm}}" class="no-color">Matching Chapters</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $chapters])
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
18
resources/views/search/entity-search-list.blade.php
Normal file
18
resources/views/search/entity-search-list.blade.php
Normal file
@@ -0,0 +1,18 @@
|
||||
@extends('base')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-7">
|
||||
<h1>{{ $title }} <small>{{$searchTerm}}</small></h1>
|
||||
@include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
|
||||
{!! $entities->links() !!}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 col-sm-offset-1"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
<div class="faded-small">
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12 setting-nav">
|
||||
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
|
||||
<a href="/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
|
||||
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="container small" ng-non-bindable>
|
||||
<h1>Create User</h1>
|
||||
|
||||
<form action="/users/create" method="post">
|
||||
<form action="/settings/users/create" method="post">
|
||||
{!! csrf_field() !!}
|
||||
@include('users.forms.' . $authMethod)
|
||||
</form>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<p>This will fully delete this user with the name '<span class="text-neg">{{$user->name}}</span>' from the system.</p>
|
||||
<p class="text-neg">Are you sure you want to delete this user?</p>
|
||||
|
||||
<form action="/users/{{$user->id}}" method="POST">
|
||||
<form action="/settings/users/{{$user->id}}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<a href="/users/{{$user->id}}" class="button muted">Cancel</a>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small">
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6"></div>
|
||||
<div class="col-md-6 faded">
|
||||
<div class="col-sm-6"></div>
|
||||
<div class="col-sm-6 faded">
|
||||
<div class="action-buttons">
|
||||
<a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
|
||||
<a href="/settings/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
|
||||
<div class="container small">
|
||||
<form action="/users/{{$user->id}}" method="post">
|
||||
<form action="/settings/users/{{$user->id}}" method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-6" ng-non-bindable>
|
||||
<h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<a href="/users" class="button muted">Cancel</a>
|
||||
<a href="/settings/users" class="button muted">Cancel</a>
|
||||
<button class="button pos" type="submit">Save</button>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<a href="/users" class="button muted">Cancel</a>
|
||||
<a href="/settings/users" class="button muted">Cancel</a>
|
||||
<button class="button pos" type="submit">Save</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h1>Users</h1>
|
||||
@if($currentUser->can('user-create'))
|
||||
<p>
|
||||
<a href="/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a>
|
||||
<a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a>
|
||||
</p>
|
||||
@endif
|
||||
<table class="table">
|
||||
@@ -25,14 +25,22 @@
|
||||
<td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
|
||||
<td>
|
||||
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
|
||||
<a href="/users/{{$user->id}}">
|
||||
<a href="/settings/users/{{$user->id}}">
|
||||
@endif
|
||||
{{$user->name}}
|
||||
{{ $user->name }}
|
||||
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
|
||||
</a>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
|
||||
<a href="/settings/users/{{$user->id}}">
|
||||
@endif
|
||||
{{ $user->email }}
|
||||
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
|
||||
</a>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{$user->email}}</td>
|
||||
<td>{{ $user->role->display_name }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
77
resources/views/users/profile.blade.php
Normal file
77
resources/views/users/profile.blade.php
Normal file
@@ -0,0 +1,77 @@
|
||||
@extends('base')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container" ng-non-bindable>
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
|
||||
<div class="padded-top large"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="clearfix">
|
||||
<div class="padded-right float left">
|
||||
<img class="avatar square huge" src="{{$user->getAvatar(120)}}" alt="{{ $user->name }}">
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin-top: 0;">{{ $user->name }}</h3>
|
||||
<p class="text-muted">
|
||||
User for {{ $user->created_at->diffForHumans(null, true) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 text-bigger" id="content-counts">
|
||||
<div class="text-muted">Created Content</div>
|
||||
<div class="text-book">
|
||||
<i class="zmdi zmdi-book zmdi-hc-fw"></i> {{ $assetCounts['books'] }} {{ str_plural('Book', $assetCounts['books']) }}
|
||||
</div>
|
||||
<div class="text-chapter">
|
||||
<i class="zmdi zmdi-collection-bookmark zmdi-hc-fw"></i> {{ $assetCounts['chapters'] }} {{ str_plural('Chapter', $assetCounts['chapters']) }}
|
||||
</div>
|
||||
<div class="text-page">
|
||||
<i class="zmdi zmdi-file-text zmdi-hc-fw"></i> {{ $assetCounts['pages'] }} {{ str_plural('Page', $assetCounts['pages']) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr class="even">
|
||||
|
||||
<h3>Recently Created Pages</h3>
|
||||
@if (count($recentlyCreated['pages']) > 0)
|
||||
@include('partials/entity-list', ['entities' => $recentlyCreated['pages']])
|
||||
@else
|
||||
<p class="text-muted">{{ $user->name }} has not created any pages</p>
|
||||
@endif
|
||||
|
||||
<hr class="even">
|
||||
|
||||
<h3>Recently Created Chapters</h3>
|
||||
@if (count($recentlyCreated['chapters']) > 0)
|
||||
@include('partials/entity-list', ['entities' => $recentlyCreated['chapters']])
|
||||
@else
|
||||
<p class="text-muted">{{ $user->name }} has not created any chapters</p>
|
||||
@endif
|
||||
|
||||
<hr class="even">
|
||||
|
||||
<h3>Recently Created Books</h3>
|
||||
@if (count($recentlyCreated['books']) > 0)
|
||||
@include('partials/entity-list', ['entities' => $recentlyCreated['books']])
|
||||
@else
|
||||
<p class="text-muted">{{ $user->name }} has not created any books</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 col-sm-offset-1" id="recent-activity">
|
||||
<h3>Recent Activity</h3>
|
||||
@include('partials/activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@stop
|
||||
0
storage/fonts/.gitignore
vendored
Normal file → Executable file
0
storage/fonts/.gitignore
vendored
Normal file → Executable file
@@ -129,7 +129,7 @@ class AuthTest extends TestCase
|
||||
$user = factory(\BookStack\User::class)->make();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/users')
|
||||
->visit('/settings/users')
|
||||
->click('Add new user')
|
||||
->type($user->name, '#name')
|
||||
->type($user->email, '#email')
|
||||
@@ -138,7 +138,7 @@ class AuthTest extends TestCase
|
||||
->type($user->password, '#password-confirm')
|
||||
->press('Save')
|
||||
->seeInDatabase('users', $user->toArray())
|
||||
->seePageIs('/users')
|
||||
->seePageIs('/settings/users')
|
||||
->see($user->name);
|
||||
}
|
||||
|
||||
@@ -147,13 +147,13 @@ class AuthTest extends TestCase
|
||||
$user = \BookStack\User::all()->last();
|
||||
$password = $user->password;
|
||||
$this->asAdmin()
|
||||
->visit('/users')
|
||||
->visit('/settings/users')
|
||||
->click($user->name)
|
||||
->seePageIs('/users/' . $user->id)
|
||||
->seePageIs('/settings/users/' . $user->id)
|
||||
->see($user->email)
|
||||
->type('Barry Scott', '#name')
|
||||
->press('Save')
|
||||
->seePageIs('/users')
|
||||
->seePageIs('/settings/users')
|
||||
->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
|
||||
->notSeeInDatabase('users', ['name' => $user->name]);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ class AuthTest extends TestCase
|
||||
public function test_user_password_update()
|
||||
{
|
||||
$user = \BookStack\User::all()->last();
|
||||
$userProfilePage = '/users/' . $user->id;
|
||||
$userProfilePage = '/settings/users/' . $user->id;
|
||||
$this->asAdmin()
|
||||
->visit($userProfilePage)
|
||||
->type('newpassword', '#password')
|
||||
@@ -172,7 +172,7 @@ class AuthTest extends TestCase
|
||||
->type('newpassword', '#password')
|
||||
->type('newpassword', '#password-confirm')
|
||||
->press('Save')
|
||||
->seePageIs('/users');
|
||||
->seePageIs('/settings/users');
|
||||
|
||||
$userPassword = \BookStack\User::find($user->id)->password;
|
||||
$this->assertTrue(Hash::check('newpassword', $userPassword));
|
||||
@@ -184,11 +184,11 @@ class AuthTest extends TestCase
|
||||
$user = $this->getNewUser($userDetails->toArray());
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/users/' . $user->id)
|
||||
->visit('/settings/users/' . $user->id)
|
||||
->click('Delete User')
|
||||
->see($user->name)
|
||||
->press('Confirm')
|
||||
->seePageIs('/users')
|
||||
->seePageIs('/settings/users')
|
||||
->notSeeInDatabase('users', ['name' => $user->name]);
|
||||
}
|
||||
|
||||
@@ -199,10 +199,10 @@ class AuthTest extends TestCase
|
||||
$this->assertEquals(1, $adminRole->users()->count());
|
||||
$user = $adminRole->users->first();
|
||||
|
||||
$this->asAdmin()->visit('/users/' . $user->id)
|
||||
$this->asAdmin()->visit('/settings/users/' . $user->id)
|
||||
->click('Delete User')
|
||||
->press('Confirm')
|
||||
->seePageIs('/users/' . $user->id)
|
||||
->seePageIs('/settings/users/' . $user->id)
|
||||
->see('You cannot delete the only admin');
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class LdapTest extends \TestCase
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test'.config('services.ldap.base_dn')]
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
|
||||
@@ -46,6 +46,30 @@ class LdapTest extends \TestCase
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
|
||||
}
|
||||
|
||||
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setOption')->once();
|
||||
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => $ldapDn,
|
||||
'mail' => [$this->mockUser->email]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
|
||||
|
||||
$this->visit('/login')
|
||||
->see('Username')
|
||||
->type($this->mockUser->name, '#username')
|
||||
->type($this->mockUser->password, '#password')
|
||||
->press('Sign In')
|
||||
->seePageIs('/')
|
||||
->see($this->mockUser->name)
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]);
|
||||
}
|
||||
|
||||
public function test_initial_incorrect_details()
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
@@ -55,7 +79,7 @@ class LdapTest extends \TestCase
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test'.config('services.ldap.base_dn')]
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
|
||||
|
||||
@@ -70,7 +94,7 @@ class LdapTest extends \TestCase
|
||||
|
||||
public function test_create_user_form()
|
||||
{
|
||||
$this->asAdmin()->visit('/users/create')
|
||||
$this->asAdmin()->visit('/settings/users/create')
|
||||
->dontSee('Password')
|
||||
->type($this->mockUser->name, '#name')
|
||||
->type($this->mockUser->email, '#email')
|
||||
@@ -78,19 +102,19 @@ class LdapTest extends \TestCase
|
||||
->see('The external auth id field is required.')
|
||||
->type($this->mockUser->name, '#external_auth_id')
|
||||
->press('Save')
|
||||
->seePageIs('/users')
|
||||
->seePageIs('/settings/users')
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
|
||||
}
|
||||
|
||||
public function test_user_edit_form()
|
||||
{
|
||||
$editUser = User::all()->last();
|
||||
$this->asAdmin()->visit('/users/' . $editUser->id)
|
||||
$this->asAdmin()->visit('/settings/users/' . $editUser->id)
|
||||
->see('Edit User')
|
||||
->dontSee('Password')
|
||||
->type('test_auth_id', '#external_auth_id')
|
||||
->press('Save')
|
||||
->seePageIs('/users')
|
||||
->seePageIs('/settings/users')
|
||||
->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
|
||||
}
|
||||
|
||||
@@ -103,7 +127,7 @@ class LdapTest extends \TestCase
|
||||
public function test_non_admins_cannot_change_auth_id()
|
||||
{
|
||||
$testUser = User::all()->last();
|
||||
$this->actingAs($testUser)->visit('/users/' . $testUser->id)
|
||||
$this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
|
||||
->dontSee('External Authentication');
|
||||
}
|
||||
|
||||
|
||||
85
tests/EntitySearchTest.php
Normal file
85
tests/EntitySearchTest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EntitySearchTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_page_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->first();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->type($page->name, 'term')
|
||||
->press('header-search-box-button')
|
||||
->see('Search Results')
|
||||
->see($page->name)
|
||||
->click($page->name)
|
||||
->seePageIs($page->getUrl());
|
||||
}
|
||||
|
||||
public function test_invalid_page_search()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->type('<p>test</p>', 'term')
|
||||
->press('header-search-box-button')
|
||||
->see('Search Results')
|
||||
->seeStatusCode(200);
|
||||
}
|
||||
|
||||
public function test_empty_search_redirects_back()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->visit('/search/all')
|
||||
->seePageIs('/');
|
||||
}
|
||||
|
||||
public function test_book_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->last();
|
||||
$chapter = $book->chapters->last();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
|
||||
->see($page->name)
|
||||
|
||||
->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
|
||||
->see($chapter->name);
|
||||
}
|
||||
|
||||
public function test_empty_book_search_redirects_back()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$this->asAdmin()
|
||||
->visit('/books')
|
||||
->visit('/search/book/' . $book->id . '?term=')
|
||||
->seePageIs('/books');
|
||||
}
|
||||
|
||||
|
||||
public function test_pages_search_listing()
|
||||
{
|
||||
$page = \BookStack\Page::all()->last();
|
||||
$this->asAdmin()->visit('/search/pages?term=' . $page->name)
|
||||
->see('Page Search Results')->see('.entity-list', $page->name);
|
||||
}
|
||||
|
||||
public function test_chapters_search_listing()
|
||||
{
|
||||
$chapter = \BookStack\Chapter::all()->last();
|
||||
$this->asAdmin()->visit('/search/chapters?term=' . $chapter->name)
|
||||
->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
|
||||
}
|
||||
|
||||
public function test_books_search_listing()
|
||||
{
|
||||
$book = \BookStack\Book::all()->last();
|
||||
$this->asAdmin()->visit('/search/books?term=' . $book->name)
|
||||
->see('Book Search Results')->see('.entity-list', $book->name);
|
||||
}
|
||||
}
|
||||
@@ -155,63 +155,6 @@ class EntityTest extends TestCase
|
||||
return $book;
|
||||
}
|
||||
|
||||
public function test_page_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->first();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->type($page->name, 'term')
|
||||
->press('header-search-box-button')
|
||||
->see('Search Results')
|
||||
->see($page->name)
|
||||
->click($page->name)
|
||||
->seePageIs($page->getUrl());
|
||||
}
|
||||
|
||||
public function test_invalid_page_search()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->type('<p>test</p>', 'term')
|
||||
->press('header-search-box-button')
|
||||
->see('Search Results')
|
||||
->seeStatusCode(200);
|
||||
}
|
||||
|
||||
public function test_empty_search_redirects_back()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->visit('/search/all')
|
||||
->seePageIs('/');
|
||||
}
|
||||
|
||||
public function test_book_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->last();
|
||||
$chapter = $book->chapters->last();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
|
||||
->see($page->name)
|
||||
|
||||
->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
|
||||
->see($chapter->name);
|
||||
}
|
||||
|
||||
public function test_empty_book_search_redirects_back()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$this->asAdmin()
|
||||
->visit('/books')
|
||||
->visit('/search/book/' . $book->id . '?term=')
|
||||
->seePageIs('/books');
|
||||
}
|
||||
|
||||
|
||||
public function test_entities_viewable_after_creator_deletion()
|
||||
{
|
||||
// Create required assets and revisions
|
||||
@@ -250,5 +193,36 @@ class EntityTest extends TestCase
|
||||
->click('Revisions')->seeStatusCode(200);
|
||||
}
|
||||
|
||||
public function test_recently_created_pages_view()
|
||||
{
|
||||
$user = $this->getNewUser();
|
||||
$content = $this->createEntityChainBelongingToUser($user);
|
||||
|
||||
$this->asAdmin()->visit('/pages/recently-created')
|
||||
->seeInNthElement('.entity-list .page', 0, $content['page']->name);
|
||||
}
|
||||
|
||||
public function test_recently_updated_pages_view()
|
||||
{
|
||||
$user = $this->getNewUser();
|
||||
$content = $this->createEntityChainBelongingToUser($user);
|
||||
|
||||
$this->asAdmin()->visit('/pages/recently-updated')
|
||||
->seeInNthElement('.entity-list .page', 0, $content['page']->name);
|
||||
}
|
||||
|
||||
public function test_old_page_slugs_redirect_to_new_pages()
|
||||
{
|
||||
$page = \BookStack\Page::all()->first();
|
||||
$pageUrl = $page->getUrl();
|
||||
$newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
|
||||
$this->asAdmin()->visit($pageUrl)
|
||||
->clickInElement('#content', 'Edit')
|
||||
->type('super test page', '#name')
|
||||
->press('Save Page')
|
||||
->seePageIs($newPageUrl)
|
||||
->visit($pageUrl)
|
||||
->seePageIs($newPageUrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -109,4 +109,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the text within the selected element.
|
||||
* @param $parentElement
|
||||
* @param $linkText
|
||||
* @return $this
|
||||
*/
|
||||
protected function clickInElement($parentElement, $linkText)
|
||||
{
|
||||
$elem = $this->crawler->filter($parentElement);
|
||||
$link = $elem->selectLink($linkText);
|
||||
$this->visit($link->link()->getUri());
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
80
tests/UserProfileTest.php
Normal file
80
tests/UserProfileTest.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
class UserProfileTest extends TestCase
|
||||
{
|
||||
protected $user;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->user = \BookStack\User::all()->last();
|
||||
}
|
||||
|
||||
public function test_profile_page_shows_name()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/user/' . $this->user->id)
|
||||
->see($this->user->name);
|
||||
}
|
||||
|
||||
public function test_profile_page_shows_recent_entities()
|
||||
{
|
||||
$content = $this->createEntityChainBelongingToUser($this->user, $this->user);
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/user/' . $this->user->id)
|
||||
// Check the recently created page is shown
|
||||
->see($content['page']->name)
|
||||
// Check the recently created chapter is shown
|
||||
->see($content['chapter']->name)
|
||||
// Check the recently created book is shown
|
||||
->see($content['book']->name);
|
||||
}
|
||||
|
||||
public function test_profile_page_shows_created_content_counts()
|
||||
{
|
||||
$newUser = $this->getNewUser();
|
||||
|
||||
$this->asAdmin()->visit('/user/' . $newUser->id)
|
||||
->see($newUser->name)
|
||||
->seeInElement('#content-counts', '0 Books')
|
||||
->seeInElement('#content-counts', '0 Chapters')
|
||||
->seeInElement('#content-counts', '0 Pages');
|
||||
|
||||
$this->createEntityChainBelongingToUser($newUser, $newUser);
|
||||
|
||||
$this->asAdmin()->visit('/user/' . $newUser->id)
|
||||
->see($newUser->name)
|
||||
->seeInElement('#content-counts', '1 Book')
|
||||
->seeInElement('#content-counts', '1 Chapter')
|
||||
->seeInElement('#content-counts', '1 Page');
|
||||
}
|
||||
|
||||
public function test_profile_page_shows_recent_activity()
|
||||
{
|
||||
$newUser = $this->getNewUser();
|
||||
$this->actingAs($newUser);
|
||||
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
|
||||
Activity::add($entities['book'], 'book_update', $entities['book']->id);
|
||||
Activity::add($entities['page'], 'page_create', $entities['book']->id);
|
||||
|
||||
$this->asAdmin()->visit('/user/' . $newUser->id)
|
||||
->seeInElement('#recent-activity', 'updated book')
|
||||
->seeInElement('#recent-activity', 'created page')
|
||||
->seeInElement('#recent-activity', $entities['page']->name);
|
||||
}
|
||||
|
||||
public function test_clicking_user_name_in_activity_leads_to_profile_page()
|
||||
{
|
||||
$newUser = $this->getNewUser();
|
||||
$this->actingAs($newUser);
|
||||
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
|
||||
Activity::add($entities['book'], 'book_update', $entities['book']->id);
|
||||
Activity::add($entities['page'], 'page_create', $entities['book']->id);
|
||||
|
||||
$this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name)
|
||||
->seePageIs('/user/' . $newUser->id)
|
||||
->see($newUser->name);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user