Merge branch 'master' into release

This commit is contained in:
Dan Brown 2016-02-25 21:23:06 +00:00
commit 14b131e850
61 changed files with 1121 additions and 380 deletions

View File

@ -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');
}
/**

View File

@ -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]);
}

View File

@ -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
]);
}
}

View File

@ -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
]);
}
}

View File

@ -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
]);
}
/**

View File

@ -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
@ -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
]);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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(),
];
}
}

View File

@ -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;

View File

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

@ -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": [],

View File

@ -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([

View File

@ -17,7 +17,7 @@ class CreateBooksTable extends Migration
$table->string('name');
$table->string('slug')->indexed();
$table->text('description');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -21,7 +21,7 @@ class CreatePagesTable extends Migration
$table->longText('html');
$table->longText('text');
$table->integer('priority');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -16,7 +16,7 @@ class CreateImagesTable extends Migration
$table->increments('id');
$table->string('name');
$table->string('url');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -19,7 +19,7 @@ class CreateChaptersTable extends Migration
$table->text('name');
$table->text('description');
$table->integer('priority');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration
$table->longText('html');
$table->longText('text');
$table->integer('created_by');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration
$table->integer('user_id');
$table->integer('entity_id');
$table->string('entity_type');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -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)

View File

@ -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();
});
}

View File

@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration
$table->string('driver')->index();
$table->string('driver_id');
$table->string('avatar');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration
$table->increments('id');
$table->integer('user_id')->index();
$table->string('token')->index();
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@ -18,7 +18,7 @@ class CreateViewsTable extends Migration
$table->integer('viewable_id');
$table->string('viewable_type');
$table->integer('views');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

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

View File

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

View File

@ -106,6 +106,12 @@ $(function () {
}
});
// Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(function() {
$('.entity-list.compact').find('p').slideToggle(240);
});
});

View File

@ -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;

View File

@ -283,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;
}
}

View File

@ -100,7 +100,7 @@
background-color: #FFF;
border: 1px solid #DDD;
color: #666;
width: 180px;
width: 172px;
z-index: 40;
}
input, button {

View File

@ -20,4 +20,8 @@ table.table {
table {
max-width: 100%;
thead {
background-color: #F8F8F8;
font-weight: 500;
}
}

View File

@ -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 {
@ -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
*/

View File

@ -47,6 +47,13 @@ body.dragging, body.dragging * {
width: 80px;
height: 80px;
}
&.huge {
width: 120px;
height: 120px;
}
&.square {
border-radius: 3px;
}
}
// System wide notifications

View File

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

View File

@ -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)

View File

@ -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">&nbsp;</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>

View 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

View File

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

View File

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

View File

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

View File

@ -6,41 +6,36 @@
<h1>Search Results&nbsp;&nbsp;&nbsp; <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)
&nbsp; &nbsp;&nbsp;
<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)
&nbsp; &nbsp;&nbsp;
<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>

View 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

View File

@ -4,7 +4,7 @@
<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>

View File

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

View File

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

View File

@ -9,7 +9,7 @@
<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>

View File

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

View File

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

View File

@ -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,7 +25,7 @@
<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 }}
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
@ -34,7 +34,7 @@
</td>
<td>
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
<a href="/users/{{$user->id}}">
<a href="/settings/users/{{$user->id}}">
@endif
{{ $user->email }}
@if($currentUser->can('user-update') || $currentUser->id == $user->id)

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

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

View File

@ -94,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')
@ -102,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']);
}
@ -127,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');
}

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

View File

@ -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);
}
}

View File

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