Compare commits

...

25 Commits

Author SHA1 Message Date
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
54e3122540 Added smarter page finding so changing the page name does not break old urls
Added page & book slug history to revisions so they can be looked up if a page is not found.
2016-02-25 20:01:59 +00:00
Dan Brown
d339ab1125 Updated phpunit testing mail settings due to laravel 5.2 changes 2016-02-22 21:28:53 +00:00
Dan Brown
3ab09ef708 Fixed issue with the book sort not showing all books in sidebar 2016-02-22 21:28:20 +00:00
Dan Brown
c86a122d80 Added table header row styling, made code blocks horizontally scrollbable fixed link popup on firefox 2016-02-22 20:18:08 +00:00
Dan Brown
3a58e37838 Updated phpunit environment variables with some required defaults 2016-02-22 19:39:51 +00:00
Dan Brown
6bd49bcd4b Fixed page listing excerpts not showing due to encoding issues 2016-02-21 13:15:46 +00:00
Dan Brown
61577cf6bf Added entity-specific search results pages. Cleaned & Fixed search results bugs
Added search result pages for pages, chapters and books.
Limited the results on the global search as it just listed out an infinate amount.
Fixed styling on new detailed page listings and also removed the 'bars' from the side to create  a cleaner view.
Fixed bad sql fulltext query format that may have thrown off searches.
Reduced the number of database queries down a thousand or so.
2016-02-21 12:53:58 +00:00
Dan Brown
b4dec2a99c Made page anchor hashes more relevant to the page content
This will help when adding support for new kinds of page content such as markdown as we won't be able to reference the same ID's as before thus they would break on every save.
2016-02-21 11:29:46 +00:00
Dan Brown
fe0b122aca Merge pull request #63 from AkibaWolf/patch-1
Fixes encoding issues the page HTML is formatted on save
2016-02-20 19:24:32 +00:00
Dan Brown
8eb2960950 Added recently created & updated page listings
Closes #46.
2016-02-20 18:51:01 +00:00
AkibaWolf
c2369a740d Update PageRepo.php
Fix encoding problem.

By default DOMDocument::loadHTML treats a string as being encoded with ISO-8859-1. This causes a problem with saving cyrillic pages' text that becomes completely unreadable (like Проверка instead of normal symbols).
2016-02-20 21:31:21 +05:00
Dan Brown
bab6fd1f2f Added recent pages to home view and made the home content more compact 2016-02-20 12:37:06 +00:00
Dan Brown
86fbc9a936 Added tests for profile pages 2016-02-18 19:32:07 +00:00
Dan Brown
4d9726dbdd Added content to user profile pages 2016-02-17 22:11:48 +00:00
Dan Brown
4442a2e6d1 Started work on user profile pages 2016-02-16 21:25:11 +00:00
Dan Brown
293be7093c Merged origin/master into master 2016-02-11 22:36:49 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
354912a1df Made book-navigation sidebar on pages sticky 2016-02-11 22:23:19 +00:00
Nick Walke
eacff3a9f0 Fixes #45 2016-02-11 14:02:17 -06:00
Dan Brown
990acbb9ac Merge pull request #59 from nwalke/master
Updated email on users list to be a link
2016-02-11 18:53:12 +00:00
Nick Walke
17d4533e45 Fixes #58 2016-02-11 01:18:01 -06:00
Dan Brown
d6c00a85ad Fixed incorrect notification when deleting a page 2016-02-10 12:48:29 +00:00
74 changed files with 1216 additions and 408 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

@@ -1,5 +1,5 @@
{
"css/styles.css": "css/styles.css?version=2a2a428",
"css/print-styles.css": "css/print-styles.css?version=2a2a428",
"js/common.js": "js/common.js?version=2a2a428"
"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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

View File

@@ -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;
@@ -111,10 +131,8 @@
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;
}
}

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

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

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
@@ -134,7 +141,7 @@ $btt-size: 40px;
background-color: rgba($primary, 0.4);
position: fixed;
bottom: $-m;
right: $-m;
right: $-l;
padding: $-xs $-s;
cursor: pointer;
color: #FFF;
@@ -144,6 +151,7 @@ $btt-size: 40px;
transition: all ease-in-out 180ms;
opacity: 0;
z-index: 999;
overflow: hidden;
&:hover {
width: $btt-size*3.4;
background-color: rgba($primary, 1);

View File

@@ -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',
];
];

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

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

View File

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

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,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">

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

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

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

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

View File

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

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

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

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

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

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

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