Dan Brown

Merge branch 'master' into release

Showing 61 changed files with 1024 additions and 286 deletions
...@@ -98,7 +98,7 @@ abstract class Entity extends Model ...@@ -98,7 +98,7 @@ abstract class Entity extends Model
98 * @param string[] array $wheres 98 * @param string[] array $wheres
99 * @return mixed 99 * @return mixed
100 */ 100 */
101 - public static function fullTextSearch($fieldsToSearch, $terms, $wheres = []) 101 + public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
102 { 102 {
103 $termString = ''; 103 $termString = '';
104 foreach ($terms as $term) { 104 foreach ($terms as $term) {
...@@ -107,7 +107,7 @@ abstract class Entity extends Model ...@@ -107,7 +107,7 @@ abstract class Entity extends Model
107 $fields = implode(',', $fieldsToSearch); 107 $fields = implode(',', $fieldsToSearch);
108 $termStringEscaped = \DB::connection()->getPdo()->quote($termString); 108 $termStringEscaped = \DB::connection()->getPdo()->quote($termString);
109 $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance')); 109 $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
110 - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]); 110 + $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
111 111
112 // Add additional where terms 112 // Add additional where terms
113 foreach ($wheres as $whereTerm) { 113 foreach ($wheres as $whereTerm) {
...@@ -115,10 +115,13 @@ abstract class Entity extends Model ...@@ -115,10 +115,13 @@ abstract class Entity extends Model
115 } 115 }
116 116
117 // Load in relations 117 // Load in relations
118 - if (!static::isA('book')) $search = $search->with('book'); 118 + if (static::isA('page')) {
119 - if (static::isA('page')) $search = $search->with('chapter'); 119 + $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
120 + } else if (static::isA('chapter')) {
121 + $search = $search->with('book');
122 + }
120 123
121 - return $search->orderBy('title_relevance', 'desc')->get(); 124 + return $search->orderBy('title_relevance', 'desc');
122 } 125 }
123 126
124 /** 127 /**
......
...@@ -157,7 +157,7 @@ class BookController extends Controller ...@@ -157,7 +157,7 @@ class BookController extends Controller
157 $this->checkPermission('book-update'); 157 $this->checkPermission('book-update');
158 $book = $this->bookRepo->getBySlug($bookSlug); 158 $book = $this->bookRepo->getBySlug($bookSlug);
159 $bookChildren = $this->bookRepo->getChildren($book); 159 $bookChildren = $this->bookRepo->getChildren($book);
160 - $books = $this->bookRepo->getAll(); 160 + $books = $this->bookRepo->getAll(false);
161 $this->setPageTitle('Sort Book ' . $book->getShortName()); 161 $this->setPageTitle('Sort Book ' . $book->getShortName());
162 return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); 162 return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
163 } 163 }
......
...@@ -3,25 +3,21 @@ ...@@ -3,25 +3,21 @@
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 use Activity; 5 use Activity;
6 -use Illuminate\Http\Request; 6 +use BookStack\Repos\EntityRepo;
7 -
8 use BookStack\Http\Requests; 7 use BookStack\Http\Requests;
9 -use BookStack\Repos\BookRepo;
10 use Views; 8 use Views;
11 9
12 class HomeController extends Controller 10 class HomeController extends Controller
13 { 11 {
14 - 12 + protected $entityRepo;
15 - protected $activityService;
16 - protected $bookRepo;
17 13
18 /** 14 /**
19 * HomeController constructor. 15 * HomeController constructor.
20 - * @param BookRepo $bookRepo 16 + * @param EntityRepo $entityRepo
21 */ 17 */
22 - public function __construct(BookRepo $bookRepo) 18 + public function __construct(EntityRepo $entityRepo)
23 { 19 {
24 - $this->bookRepo = $bookRepo; 20 + $this->entityRepo = $entityRepo;
25 parent::__construct(); 21 parent::__construct();
26 } 22 }
27 23
...@@ -33,9 +29,16 @@ class HomeController extends Controller ...@@ -33,9 +29,16 @@ class HomeController extends Controller
33 */ 29 */
34 public function index() 30 public function index()
35 { 31 {
36 - $activity = Activity::latest(); 32 + $activity = Activity::latest(10);
37 - $recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10); 33 + $recents = $this->signedIn ? Views::getUserRecentlyViewed(12, 0) : $this->entityRepo->getRecentlyCreatedBooks(10);
38 - return view('home', ['activity' => $activity, 'recents' => $recents]); 34 + $recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
35 + $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
36 + return view('home', [
37 + 'activity' => $activity,
38 + 'recents' => $recents,
39 + 'recentlyCreatedPages' => $recentlyCreatedPages,
40 + 'recentlyUpdatedPages' => $recentlyUpdatedPages
41 + ]);
39 } 42 }
40 43
41 } 44 }
......
...@@ -11,6 +11,7 @@ use BookStack\Http\Requests; ...@@ -11,6 +11,7 @@ use BookStack\Http\Requests;
11 use BookStack\Repos\BookRepo; 11 use BookStack\Repos\BookRepo;
12 use BookStack\Repos\ChapterRepo; 12 use BookStack\Repos\ChapterRepo;
13 use BookStack\Repos\PageRepo; 13 use BookStack\Repos\PageRepo;
14 +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
14 use Views; 15 use Views;
15 16
16 class PageController extends Controller 17 class PageController extends Controller
...@@ -81,6 +82,8 @@ class PageController extends Controller ...@@ -81,6 +82,8 @@ class PageController extends Controller
81 82
82 /** 83 /**
83 * Display the specified page. 84 * Display the specified page.
85 + * If the page is not found via the slug the
86 + * revisions are searched for a match.
84 * 87 *
85 * @param $bookSlug 88 * @param $bookSlug
86 * @param $pageSlug 89 * @param $pageSlug
...@@ -89,7 +92,15 @@ class PageController extends Controller ...@@ -89,7 +92,15 @@ class PageController extends Controller
89 public function show($bookSlug, $pageSlug) 92 public function show($bookSlug, $pageSlug)
90 { 93 {
91 $book = $this->bookRepo->getBySlug($bookSlug); 94 $book = $this->bookRepo->getBySlug($bookSlug);
92 - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 95 +
96 + try {
97 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
98 + } catch (NotFoundHttpException $e) {
99 + $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
100 + if ($page === null) abort(404);
101 + return redirect($page->getUrl());
102 + }
103 +
93 $sidebarTree = $this->bookRepo->getChildren($book); 104 $sidebarTree = $this->bookRepo->getChildren($book);
94 Views::add($page); 105 Views::add($page);
95 $this->setPageTitle($page->getShortName()); 106 $this->setPageTitle($page->getShortName());
...@@ -278,4 +289,30 @@ class PageController extends Controller ...@@ -278,4 +289,30 @@ class PageController extends Controller
278 ]); 289 ]);
279 } 290 }
280 291
292 + /**
293 + * Show a listing of recently created pages
294 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
295 + */
296 + public function showRecentlyCreated()
297 + {
298 + $pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
299 + return view('pages/detailed-listing', [
300 + 'title' => 'Recently Created Pages',
301 + 'pages' => $pages
302 + ]);
303 + }
304 +
305 + /**
306 + * Show a listing of recently created pages
307 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
308 + */
309 + public function showRecentlyUpdated()
310 + {
311 + $pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
312 + return view('pages/detailed-listing', [
313 + 'title' => 'Recently Updated Pages',
314 + 'pages' => $pages
315 + ]);
316 + }
317 +
281 } 318 }
......
...@@ -42,11 +42,77 @@ class SearchController extends Controller ...@@ -42,11 +42,77 @@ class SearchController extends Controller
42 return redirect()->back(); 42 return redirect()->back();
43 } 43 }
44 $searchTerm = $request->get('term'); 44 $searchTerm = $request->get('term');
45 - $pages = $this->pageRepo->getBySearch($searchTerm); 45 + $paginationAppends = $request->only('term');
46 - $books = $this->bookRepo->getBySearch($searchTerm); 46 + $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
47 - $chapters = $this->chapterRepo->getBySearch($searchTerm); 47 + $books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
48 + $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
48 $this->setPageTitle('Search For ' . $searchTerm); 49 $this->setPageTitle('Search For ' . $searchTerm);
49 - return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); 50 + return view('search/all', [
51 + 'pages' => $pages,
52 + 'books' => $books,
53 + 'chapters' => $chapters,
54 + 'searchTerm' => $searchTerm
55 + ]);
56 + }
57 +
58 + /**
59 + * Search only the pages in the system.
60 + * @param Request $request
61 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
62 + */
63 + public function searchPages(Request $request)
64 + {
65 + if (!$request->has('term')) return redirect()->back();
66 +
67 + $searchTerm = $request->get('term');
68 + $paginationAppends = $request->only('term');
69 + $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
70 + $this->setPageTitle('Page Search For ' . $searchTerm);
71 + return view('search/entity-search-list', [
72 + 'entities' => $pages,
73 + 'title' => 'Page Search Results',
74 + 'searchTerm' => $searchTerm
75 + ]);
76 + }
77 +
78 + /**
79 + * Search only the chapters in the system.
80 + * @param Request $request
81 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
82 + */
83 + public function searchChapters(Request $request)
84 + {
85 + if (!$request->has('term')) return redirect()->back();
86 +
87 + $searchTerm = $request->get('term');
88 + $paginationAppends = $request->only('term');
89 + $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
90 + $this->setPageTitle('Chapter Search For ' . $searchTerm);
91 + return view('search/entity-search-list', [
92 + 'entities' => $chapters,
93 + 'title' => 'Chapter Search Results',
94 + 'searchTerm' => $searchTerm
95 + ]);
96 + }
97 +
98 + /**
99 + * Search only the books in the system.
100 + * @param Request $request
101 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
102 + */
103 + public function searchBooks(Request $request)
104 + {
105 + if (!$request->has('term')) return redirect()->back();
106 +
107 + $searchTerm = $request->get('term');
108 + $paginationAppends = $request->only('term');
109 + $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
110 + $this->setPageTitle('Book Search For ' . $searchTerm);
111 + return view('search/entity-search-list', [
112 + 'entities' => $books,
113 + 'title' => 'Book Search Results',
114 + 'searchTerm' => $searchTerm
115 + ]);
50 } 116 }
51 117
52 /** 118 /**
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 +use BookStack\Activity;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 7
7 use Illuminate\Http\Response; 8 use Illuminate\Http\Response;
...@@ -92,10 +93,9 @@ class UserController extends Controller ...@@ -92,10 +93,9 @@ class UserController extends Controller
92 $user->save(); 93 $user->save();
93 } 94 }
94 95
95 - return redirect('/users'); 96 + return redirect('/settings/users');
96 } 97 }
97 98
98 -
99 /** 99 /**
100 * Show the form for editing the specified user. 100 * Show the form for editing the specified user.
101 * @param int $id 101 * @param int $id
...@@ -159,7 +159,7 @@ class UserController extends Controller ...@@ -159,7 +159,7 @@ class UserController extends Controller
159 } 159 }
160 160
161 $user->save(); 161 $user->save();
162 - return redirect('/users'); 162 + return redirect('/settings/users');
163 } 163 }
164 164
165 /** 165 /**
...@@ -197,6 +197,25 @@ class UserController extends Controller ...@@ -197,6 +197,25 @@ class UserController extends Controller
197 } 197 }
198 $this->userRepo->destroy($user); 198 $this->userRepo->destroy($user);
199 199
200 - return redirect('/users'); 200 + return redirect('/settings/users');
201 + }
202 +
203 + /**
204 + * Show the user profile page
205 + * @param $id
206 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
207 + */
208 + public function showProfilePage($id)
209 + {
210 + $user = $this->userRepo->getById($id);
211 + $userActivity = $this->userRepo->getActivity($user);
212 + $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
213 + $assetCounts = $this->userRepo->getAssetCounts($user);
214 + return view('users/profile', [
215 + 'user' => $user,
216 + 'activity' => $userActivity,
217 + 'recentlyCreated' => $recentlyCreated,
218 + 'assetCounts' => $assetCounts
219 + ]);
201 } 220 }
202 } 221 }
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
3 // Authenticated routes... 3 // Authenticated routes...
4 Route::group(['middleware' => 'auth'], function () { 4 Route::group(['middleware' => 'auth'], function () {
5 5
6 + Route::group(['prefix' => 'pages'], function() {
7 + Route::get('/recently-created', 'PageController@showRecentlyCreated');
8 + Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
9 + });
10 +
6 Route::group(['prefix' => 'books'], function () { 11 Route::group(['prefix' => 'books'], function () {
7 12
8 // Books 13 // Books
...@@ -47,14 +52,8 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -47,14 +52,8 @@ Route::group(['middleware' => 'auth'], function () {
47 52
48 }); 53 });
49 54
50 - // Users 55 + // User Profile routes
51 - Route::get('/users', 'UserController@index'); 56 + Route::get('/user/{userId}', 'UserController@showProfilePage');
52 - Route::get('/users/create', 'UserController@create');
53 - Route::get('/users/{id}/delete', 'UserController@delete');
54 - Route::post('/users/create', 'UserController@store');
55 - Route::get('/users/{id}', 'UserController@edit');
56 - Route::put('/users/{id}', 'UserController@update');
57 - Route::delete('/users/{id}', 'UserController@destroy');
58 57
59 // Image routes 58 // Image routes
60 Route::group(['prefix' => 'images'], function() { 59 Route::group(['prefix' => 'images'], function() {
...@@ -75,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -75,6 +74,9 @@ Route::group(['middleware' => 'auth'], function () {
75 74
76 // Search 75 // Search
77 Route::get('/search/all', 'SearchController@searchAll'); 76 Route::get('/search/all', 'SearchController@searchAll');
77 + Route::get('/search/pages', 'SearchController@searchPages');
78 + Route::get('/search/books', 'SearchController@searchBooks');
79 + Route::get('/search/chapters', 'SearchController@searchChapters');
78 Route::get('/search/book/{bookId}', 'SearchController@searchBook'); 80 Route::get('/search/book/{bookId}', 'SearchController@searchBook');
79 81
80 // Other Pages 82 // Other Pages
...@@ -82,8 +84,18 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -82,8 +84,18 @@ Route::group(['middleware' => 'auth'], function () {
82 Route::get('/home', 'HomeController@index'); 84 Route::get('/home', 'HomeController@index');
83 85
84 // Settings 86 // Settings
85 - Route::get('/settings', 'SettingController@index'); 87 + Route::group(['prefix' => 'settings'], function() {
86 - Route::post('/settings', 'SettingController@update'); 88 + Route::get('/', 'SettingController@index');
89 + Route::post('/', 'SettingController@update');
90 + // Users
91 + Route::get('/users', 'UserController@index');
92 + Route::get('/users/create', 'UserController@create');
93 + Route::get('/users/{id}/delete', 'UserController@delete');
94 + Route::post('/users/create', 'UserController@store');
95 + Route::get('/users/{id}', 'UserController@edit');
96 + Route::put('/users/{id}', 'UserController@update');
97 + Route::delete('/users/{id}', 'UserController@destroy');
98 + });
87 99
88 }); 100 });
89 101
......
...@@ -45,7 +45,8 @@ class Page extends Entity ...@@ -45,7 +45,8 @@ class Page extends Entity
45 45
46 public function getExcerpt($length = 100) 46 public function getExcerpt($length = 100)
47 { 47 {
48 - return strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text; 48 + $text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
49 + return mb_convert_encoding($text, 'UTF-8');
49 } 50 }
50 51
51 } 52 }
......
...@@ -14,8 +14,8 @@ class BookRepo ...@@ -14,8 +14,8 @@ class BookRepo
14 14
15 /** 15 /**
16 * BookRepo constructor. 16 * BookRepo constructor.
17 - * @param Book $book 17 + * @param Book $book
18 - * @param PageRepo $pageRepo 18 + * @param PageRepo $pageRepo
19 * @param ChapterRepo $chapterRepo 19 * @param ChapterRepo $chapterRepo
20 */ 20 */
21 public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo) 21 public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
...@@ -42,7 +42,9 @@ class BookRepo ...@@ -42,7 +42,9 @@ class BookRepo
42 */ 42 */
43 public function getAll($count = 10) 43 public function getAll($count = 10)
44 { 44 {
45 - return $this->book->orderBy('name', 'asc')->take($count)->get(); 45 + $bookQuery = $this->book->orderBy('name', 'asc');
46 + if (!$count) return $bookQuery->get();
47 + return $bookQuery->take($count)->get();
46 } 48 }
47 49
48 /** 50 /**
...@@ -159,7 +161,7 @@ class BookRepo ...@@ -159,7 +161,7 @@ class BookRepo
159 } 161 }
160 162
161 /** 163 /**
162 - * @param string $slug 164 + * @param string $slug
163 * @param bool|false $currentId 165 * @param bool|false $currentId
164 * @return bool 166 * @return bool
165 */ 167 */
...@@ -175,7 +177,7 @@ class BookRepo ...@@ -175,7 +177,7 @@ class BookRepo
175 /** 177 /**
176 * Provides a suitable slug for the given book name. 178 * Provides a suitable slug for the given book name.
177 * Ensures the returned slug is unique in the system. 179 * Ensures the returned slug is unique in the system.
178 - * @param string $name 180 + * @param string $name
179 * @param bool|false $currentId 181 * @param bool|false $currentId
180 * @return string 182 * @return string
181 */ 183 */
...@@ -218,12 +220,15 @@ class BookRepo ...@@ -218,12 +220,15 @@ class BookRepo
218 /** 220 /**
219 * Get books by search term. 221 * Get books by search term.
220 * @param $term 222 * @param $term
223 + * @param int $count
224 + * @param array $paginationAppends
221 * @return mixed 225 * @return mixed
222 */ 226 */
223 - public function getBySearch($term) 227 + public function getBySearch($term, $count = 20, $paginationAppends = [])
224 { 228 {
225 $terms = explode(' ', $term); 229 $terms = explode(' ', $term);
226 - $books = $this->book->fullTextSearch(['name', 'description'], $terms); 230 + $books = $this->book->fullTextSearchQuery(['name', 'description'], $terms)
231 + ->paginate($count)->appends($paginationAppends);
227 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 232 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
228 foreach ($books as $book) { 233 foreach ($books as $book) {
229 //highlight 234 //highlight
......
...@@ -125,12 +125,15 @@ class ChapterRepo ...@@ -125,12 +125,15 @@ class ChapterRepo
125 * Get chapters by the given search term. 125 * Get chapters by the given search term.
126 * @param $term 126 * @param $term
127 * @param array $whereTerms 127 * @param array $whereTerms
128 + * @param int $count
129 + * @param array $paginationAppends
128 * @return mixed 130 * @return mixed
129 */ 131 */
130 - public function getBySearch($term, $whereTerms = []) 132 + public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
131 { 133 {
132 $terms = explode(' ', $term); 134 $terms = explode(' ', $term);
133 - $chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms); 135 + $chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)
136 + ->paginate($count)->appends($paginationAppends);
134 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 137 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
135 foreach ($chapters as $chapter) { 138 foreach ($chapters as $chapter) {
136 //highlight 139 //highlight
......
1 +<?php namespace BookStack\Repos;
2 +
3 +
4 +use BookStack\Book;
5 +use BookStack\Chapter;
6 +use BookStack\Page;
7 +
8 +class EntityRepo
9 +{
10 +
11 + public $book;
12 + public $chapter;
13 + public $page;
14 +
15 + /**
16 + * EntityService constructor.
17 + * @param $book
18 + * @param $chapter
19 + * @param $page
20 + */
21 + public function __construct(Book $book, Chapter $chapter, Page $page)
22 + {
23 + $this->book = $book;
24 + $this->chapter = $chapter;
25 + $this->page = $page;
26 + }
27 +
28 + /**
29 + * Get the latest books added to the system.
30 + * @param $count
31 + * @param $page
32 + */
33 + public function getRecentlyCreatedBooks($count = 20, $page = 0)
34 + {
35 + return $this->book->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get();
36 + }
37 +
38 + /**
39 + * Get the most recently updated books.
40 + * @param $count
41 + * @param int $page
42 + * @return mixed
43 + */
44 + public function getRecentlyUpdatedBooks($count = 20, $page = 0)
45 + {
46 + return $this->book->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get();
47 + }
48 +
49 + /**
50 + * Get the latest pages added to the system.
51 + * @param $count
52 + * @param $page
53 + */
54 + public function getRecentlyCreatedPages($count = 20, $page = 0)
55 + {
56 + return $this->page->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get();
57 + }
58 +
59 + /**
60 + * Get the most recently updated pages.
61 + * @param $count
62 + * @param int $page
63 + * @return mixed
64 + */
65 + public function getRecentlyUpdatedPages($count = 20, $page = 0)
66 + {
67 + return $this->page->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get();
68 + }
69 +
70 +
71 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Log; ...@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Log;
10 use Illuminate\Support\Str; 10 use Illuminate\Support\Str;
11 use BookStack\Page; 11 use BookStack\Page;
12 use BookStack\PageRevision; 12 use BookStack\PageRevision;
13 +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
13 14
14 class PageRepo 15 class PageRepo
15 { 16 {
...@@ -65,11 +66,28 @@ class PageRepo ...@@ -65,11 +66,28 @@ class PageRepo
65 public function getBySlug($slug, $bookId) 66 public function getBySlug($slug, $bookId)
66 { 67 {
67 $page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); 68 $page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
68 - if ($page === null) abort(404); 69 + if ($page === null) throw new NotFoundHttpException('Page not found');
69 return $page; 70 return $page;
70 } 71 }
71 72
72 /** 73 /**
74 + * Search through page revisions and retrieve
75 + * the last page in the current book that
76 + * has a slug equal to the one given.
77 + * @param $pageSlug
78 + * @param $bookSlug
79 + * @return null | Page
80 + */
81 + public function findPageUsingOldSlug($pageSlug, $bookSlug)
82 + {
83 + $revision = $this->pageRevision->where('slug', '=', $pageSlug)
84 + ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
85 + ->with('page')->first();
86 + return $revision !== null ? $revision->page : null;
87 + }
88 +
89 + /**
90 + * Get a new Page instance from the given input.
73 * @param $input 91 * @param $input
74 * @return Page 92 * @return Page
75 */ 93 */
...@@ -125,21 +143,20 @@ class PageRepo ...@@ -125,21 +143,20 @@ class PageRepo
125 if($htmlText == '') return $htmlText; 143 if($htmlText == '') return $htmlText;
126 libxml_use_internal_errors(true); 144 libxml_use_internal_errors(true);
127 $doc = new \DOMDocument(); 145 $doc = new \DOMDocument();
128 - $doc->loadHTML($htmlText); 146 + $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
129 147
130 $container = $doc->documentElement; 148 $container = $doc->documentElement;
131 $body = $container->childNodes->item(0); 149 $body = $container->childNodes->item(0);
132 $childNodes = $body->childNodes; 150 $childNodes = $body->childNodes;
133 151
134 // Ensure no duplicate ids are used 152 // Ensure no duplicate ids are used
135 - $lastId = false;
136 $idArray = []; 153 $idArray = [];
137 154
138 foreach ($childNodes as $index => $childNode) { 155 foreach ($childNodes as $index => $childNode) {
139 /** @var \DOMElement $childNode */ 156 /** @var \DOMElement $childNode */
140 if (get_class($childNode) !== 'DOMElement') continue; 157 if (get_class($childNode) !== 'DOMElement') continue;
141 158
142 - // Overwrite id if not a bookstack custom id 159 + // Overwrite id if not a BookStack custom id
143 if ($childNode->hasAttribute('id')) { 160 if ($childNode->hasAttribute('id')) {
144 $id = $childNode->getAttribute('id'); 161 $id = $childNode->getAttribute('id');
145 if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { 162 if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
...@@ -149,13 +166,18 @@ class PageRepo ...@@ -149,13 +166,18 @@ class PageRepo
149 } 166 }
150 167
151 // Create an unique id for the element 168 // Create an unique id for the element
152 - do { 169 + // Uses the content as a basis to ensure output is the same every time
153 - $id = 'bkmrk-' . substr(uniqid(), -5); 170 + // the same content is passed through.
154 - } while ($id == $lastId); 171 + $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
155 - $lastId = $id; 172 + $newId = urlencode($contentId);
173 + $loopIndex = 0;
174 + while (in_array($newId, $idArray)) {
175 + $newId = urlencode($contentId . '-' . $loopIndex);
176 + $loopIndex++;
177 + }
156 178
157 - $childNode->setAttribute('id', $id); 179 + $childNode->setAttribute('id', $newId);
158 - $idArray[] = $id; 180 + $idArray[] = $newId;
159 } 181 }
160 182
161 // Generate inner html as a string 183 // Generate inner html as a string
...@@ -171,14 +193,17 @@ class PageRepo ...@@ -171,14 +193,17 @@ class PageRepo
171 /** 193 /**
172 * Gets pages by a search term. 194 * Gets pages by a search term.
173 * Highlights page content for showing in results. 195 * Highlights page content for showing in results.
174 - * @param string $term 196 + * @param string $term
175 * @param array $whereTerms 197 * @param array $whereTerms
198 + * @param int $count
199 + * @param array $paginationAppends
176 * @return mixed 200 * @return mixed
177 */ 201 */
178 - public function getBySearch($term, $whereTerms = []) 202 + public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
179 { 203 {
180 $terms = explode(' ', $term); 204 $terms = explode(' ', $term);
181 - $pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms); 205 + $pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)
206 + ->paginate($count)->appends($paginationAppends);
182 207
183 // Add highlights to page text. 208 // Add highlights to page text.
184 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 209 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
...@@ -238,9 +263,13 @@ class PageRepo ...@@ -238,9 +263,13 @@ class PageRepo
238 $this->saveRevision($page); 263 $this->saveRevision($page);
239 } 264 }
240 265
266 + // Prevent slug being updated if no name change
267 + if ($page->name !== $input['name']) {
268 + $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
269 + }
270 +
241 // Update with new details 271 // Update with new details
242 $page->fill($input); 272 $page->fill($input);
243 - $page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
244 $page->html = $this->formatHtml($input['html']); 273 $page->html = $this->formatHtml($input['html']);
245 $page->text = strip_tags($page->html); 274 $page->text = strip_tags($page->html);
246 $page->updated_by = auth()->user()->id; 275 $page->updated_by = auth()->user()->id;
...@@ -276,6 +305,8 @@ class PageRepo ...@@ -276,6 +305,8 @@ class PageRepo
276 { 305 {
277 $revision = $this->pageRevision->fill($page->toArray()); 306 $revision = $this->pageRevision->fill($page->toArray());
278 $revision->page_id = $page->id; 307 $revision->page_id = $page->id;
308 + $revision->slug = $page->slug;
309 + $revision->book_slug = $page->book->slug;
279 $revision->created_by = auth()->user()->id; 310 $revision->created_by = auth()->user()->id;
280 $revision->created_at = $page->updated_at; 311 $revision->created_at = $page->updated_at;
281 $revision->save(); 312 $revision->save();
...@@ -358,5 +389,22 @@ class PageRepo ...@@ -358,5 +389,22 @@ class PageRepo
358 $page->delete(); 389 $page->delete();
359 } 390 }
360 391
392 + /**
393 + * Get the latest pages added to the system.
394 + * @param $count
395 + */
396 + public function getRecentlyCreatedPaginated($count = 20)
397 + {
398 + return $this->page->orderBy('created_at', 'desc')->paginate($count);
399 + }
400 +
401 + /**
402 + * Get the latest pages added to the system.
403 + * @param $count
404 + */
405 + public function getRecentlyUpdatedPaginated($count = 20)
406 + {
407 + return $this->page->orderBy('updated_at', 'desc')->paginate($count);
408 + }
361 409
362 -}
...\ No newline at end of file ...\ No newline at end of file
410 +}
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 -
4 use BookStack\Role; 3 use BookStack\Role;
5 use BookStack\User; 4 use BookStack\User;
6 use Setting; 5 use Setting;
...@@ -10,15 +9,19 @@ class UserRepo ...@@ -10,15 +9,19 @@ class UserRepo
10 9
11 protected $user; 10 protected $user;
12 protected $role; 11 protected $role;
12 + protected $entityRepo;
13 13
14 /** 14 /**
15 * UserRepo constructor. 15 * UserRepo constructor.
16 - * @param $user 16 + * @param User $user
17 + * @param Role $role
18 + * @param EntityRepo $entityRepo
17 */ 19 */
18 - public function __construct(User $user, Role $role) 20 + public function __construct(User $user, Role $role, EntityRepo $entityRepo)
19 { 21 {
20 $this->user = $user; 22 $this->user = $user;
21 $this->role = $role; 23 $this->role = $role;
24 + $this->entityRepo = $entityRepo;
22 } 25 }
23 26
24 /** 27 /**
...@@ -112,4 +115,49 @@ class UserRepo ...@@ -112,4 +115,49 @@ class UserRepo
112 $user->socialAccounts()->delete(); 115 $user->socialAccounts()->delete();
113 $user->delete(); 116 $user->delete();
114 } 117 }
118 +
119 + /**
120 + * Get the latest activity for a user.
121 + * @param User $user
122 + * @param int $count
123 + * @param int $page
124 + * @return array
125 + */
126 + public function getActivity(User $user, $count = 20, $page = 0)
127 + {
128 + return \Activity::userActivity($user, $count, $page);
129 + }
130 +
131 + /**
132 + * Get the recently created content for this given user.
133 + * @param User $user
134 + * @param int $count
135 + * @return mixed
136 + */
137 + public function getRecentlyCreated(User $user, $count = 20)
138 + {
139 + return [
140 + 'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
141 + ->take($count)->get(),
142 + 'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
143 + ->take($count)->get(),
144 + 'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->orderBy('created_at', 'desc')
145 + ->take($count)->get()
146 + ];
147 + }
148 +
149 + /**
150 + * Get asset created counts for the give user.
151 + * @param User $user
152 + * @return array
153 + */
154 + public function getAssetCounts(User $user)
155 + {
156 + return [
157 + 'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
158 + 'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
159 + 'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
160 + ];
161 + }
162 +
115 } 163 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -29,18 +29,19 @@ class ActivityService ...@@ -29,18 +29,19 @@ class ActivityService
29 */ 29 */
30 public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false) 30 public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
31 { 31 {
32 - $this->activity->user_id = $this->user->id; 32 + $activity = $this->activity->newInstance();
33 - $this->activity->book_id = $bookId; 33 + $activity->user_id = $this->user->id;
34 - $this->activity->key = strtolower($activityKey); 34 + $activity->book_id = $bookId;
35 + $activity->key = strtolower($activityKey);
35 if ($extra !== false) { 36 if ($extra !== false) {
36 - $this->activity->extra = $extra; 37 + $activity->extra = $extra;
37 } 38 }
38 - $entity->activity()->save($this->activity); 39 + $entity->activity()->save($activity);
39 $this->setNotification($activityKey); 40 $this->setNotification($activityKey);
40 } 41 }
41 42
42 /** 43 /**
43 - * Adds a activity history with a message & without binding to a entitiy. 44 + * Adds a activity history with a message & without binding to a entity.
44 * @param $activityKey 45 * @param $activityKey
45 * @param int $bookId 46 * @param int $bookId
46 * @param bool|false $extra 47 * @param bool|false $extra
...@@ -91,14 +92,14 @@ class ActivityService ...@@ -91,14 +92,14 @@ class ActivityService
91 } 92 }
92 93
93 /** 94 /**
94 - * Gets the latest activity for an entitiy, Filtering out similar 95 + * Gets the latest activity for an entity, Filtering out similar
95 * items to prevent a message activity list. 96 * items to prevent a message activity list.
96 * @param Entity $entity 97 * @param Entity $entity
97 * @param int $count 98 * @param int $count
98 * @param int $page 99 * @param int $page
99 * @return array 100 * @return array
100 */ 101 */
101 - function entityActivity($entity, $count = 20, $page = 0) 102 + public function entityActivity($entity, $count = 20, $page = 0)
102 { 103 {
103 $activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc') 104 $activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
104 ->skip($count * $page)->take($count)->get(); 105 ->skip($count * $page)->take($count)->get();
...@@ -107,15 +108,30 @@ class ActivityService ...@@ -107,15 +108,30 @@ class ActivityService
107 } 108 }
108 109
109 /** 110 /**
111 + * Get latest activity for a user, Filtering out similar
112 + * items.
113 + * @param $user
114 + * @param int $count
115 + * @param int $page
116 + * @return array
117 + */
118 + public function userActivity($user, $count = 20, $page = 0)
119 + {
120 + $activity = $this->activity->where('user_id', '=', $user->id)
121 + ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
122 + return $this->filterSimilar($activity);
123 + }
124 +
125 + /**
110 * Filters out similar activity. 126 * Filters out similar activity.
111 - * @param Activity[] $activity 127 + * @param Activity[] $activities
112 * @return array 128 * @return array
113 */ 129 */
114 - protected function filterSimilar($activity) 130 + protected function filterSimilar($activities)
115 { 131 {
116 $newActivity = []; 132 $newActivity = [];
117 $previousItem = false; 133 $previousItem = false;
118 - foreach ($activity as $activityItem) { 134 + foreach ($activities as $activityItem) {
119 if ($previousItem === false) { 135 if ($previousItem === false) {
120 $previousItem = $activityItem; 136 $previousItem = $activityItem;
121 $newActivity[] = $activityItem; 137 $newActivity[] = $activityItem;
......
...@@ -164,6 +164,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -164,6 +164,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
164 */ 164 */
165 public function getEditUrl() 165 public function getEditUrl()
166 { 166 {
167 - return '/users/' . $this->id; 167 + return '/settings/users/' . $this->id;
168 } 168 }
169 } 169 }
......
...@@ -18,7 +18,7 @@ class CreateUsersTable extends Migration ...@@ -18,7 +18,7 @@ class CreateUsersTable extends Migration
18 $table->string('email')->unique(); 18 $table->string('email')->unique();
19 $table->string('password', 60); 19 $table->string('password', 60);
20 $table->rememberToken(); 20 $table->rememberToken();
21 - $table->timestamps(); 21 + $table->nullableTimestamps();
22 }); 22 });
23 23
24 \BookStack\User::forceCreate([ 24 \BookStack\User::forceCreate([
......
...@@ -17,7 +17,7 @@ class CreateBooksTable extends Migration ...@@ -17,7 +17,7 @@ class CreateBooksTable extends Migration
17 $table->string('name'); 17 $table->string('name');
18 $table->string('slug')->indexed(); 18 $table->string('slug')->indexed();
19 $table->text('description'); 19 $table->text('description');
20 - $table->timestamps(); 20 + $table->nullableTimestamps();
21 }); 21 });
22 } 22 }
23 23
......
...@@ -21,7 +21,7 @@ class CreatePagesTable extends Migration ...@@ -21,7 +21,7 @@ class CreatePagesTable extends Migration
21 $table->longText('html'); 21 $table->longText('html');
22 $table->longText('text'); 22 $table->longText('text');
23 $table->integer('priority'); 23 $table->integer('priority');
24 - $table->timestamps(); 24 + $table->nullableTimestamps();
25 }); 25 });
26 } 26 }
27 27
......
...@@ -16,7 +16,7 @@ class CreateImagesTable extends Migration ...@@ -16,7 +16,7 @@ class CreateImagesTable extends Migration
16 $table->increments('id'); 16 $table->increments('id');
17 $table->string('name'); 17 $table->string('name');
18 $table->string('url'); 18 $table->string('url');
19 - $table->timestamps(); 19 + $table->nullableTimestamps();
20 }); 20 });
21 } 21 }
22 22
......
...@@ -19,7 +19,7 @@ class CreateChaptersTable extends Migration ...@@ -19,7 +19,7 @@ class CreateChaptersTable extends Migration
19 $table->text('name'); 19 $table->text('name');
20 $table->text('description'); 20 $table->text('description');
21 $table->integer('priority'); 21 $table->integer('priority');
22 - $table->timestamps(); 22 + $table->nullableTimestamps();
23 }); 23 });
24 } 24 }
25 25
......
...@@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration ...@@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration
19 $table->longText('html'); 19 $table->longText('html');
20 $table->longText('text'); 20 $table->longText('text');
21 $table->integer('created_by'); 21 $table->integer('created_by');
22 - $table->timestamps(); 22 + $table->nullableTimestamps();
23 }); 23 });
24 } 24 }
25 25
......
...@@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration ...@@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration
20 $table->integer('user_id'); 20 $table->integer('user_id');
21 $table->integer('entity_id'); 21 $table->integer('entity_id');
22 $table->string('entity_type'); 22 $table->string('entity_type');
23 - $table->timestamps(); 23 + $table->nullableTimestamps();
24 }); 24 });
25 } 25 }
26 26
......
...@@ -28,7 +28,7 @@ class AddRolesAndPermissions extends Migration ...@@ -28,7 +28,7 @@ class AddRolesAndPermissions extends Migration
28 $table->string('name')->unique(); 28 $table->string('name')->unique();
29 $table->string('display_name')->nullable(); 29 $table->string('display_name')->nullable();
30 $table->string('description')->nullable(); 30 $table->string('description')->nullable();
31 - $table->timestamps(); 31 + $table->nullableTimestamps();
32 }); 32 });
33 33
34 // Create table for associating roles to users (Many-to-Many) 34 // Create table for associating roles to users (Many-to-Many)
...@@ -50,7 +50,7 @@ class AddRolesAndPermissions extends Migration ...@@ -50,7 +50,7 @@ class AddRolesAndPermissions extends Migration
50 $table->string('name')->unique(); 50 $table->string('name')->unique();
51 $table->string('display_name')->nullable(); 51 $table->string('display_name')->nullable();
52 $table->string('description')->nullable(); 52 $table->string('description')->nullable();
53 - $table->timestamps(); 53 + $table->nullableTimestamps();
54 }); 54 });
55 55
56 // Create table for associating permissions to roles (Many-to-Many) 56 // Create table for associating permissions to roles (Many-to-Many)
......
...@@ -15,7 +15,7 @@ class CreateSettingsTable extends Migration ...@@ -15,7 +15,7 @@ class CreateSettingsTable extends Migration
15 Schema::create('settings', function (Blueprint $table) { 15 Schema::create('settings', function (Blueprint $table) {
16 $table->string('setting_key')->primary()->indexed(); 16 $table->string('setting_key')->primary()->indexed();
17 $table->text('value'); 17 $table->text('value');
18 - $table->timestamps(); 18 + $table->nullableTimestamps();
19 }); 19 });
20 } 20 }
21 21
......
...@@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration ...@@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration
18 $table->string('driver')->index(); 18 $table->string('driver')->index();
19 $table->string('driver_id'); 19 $table->string('driver_id');
20 $table->string('avatar'); 20 $table->string('avatar');
21 - $table->timestamps(); 21 + $table->nullableTimestamps();
22 }); 22 });
23 } 23 }
24 24
......
...@@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration ...@@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration
20 $table->increments('id'); 20 $table->increments('id');
21 $table->integer('user_id')->index(); 21 $table->integer('user_id')->index();
22 $table->string('token')->index(); 22 $table->string('token')->index();
23 - $table->timestamps(); 23 + $table->nullableTimestamps();
24 }); 24 });
25 } 25 }
26 26
......
...@@ -18,7 +18,7 @@ class CreateViewsTable extends Migration ...@@ -18,7 +18,7 @@ class CreateViewsTable extends Migration
18 $table->integer('viewable_id'); 18 $table->integer('viewable_id');
19 $table->string('viewable_type'); 19 $table->string('viewable_type');
20 $table->integer('views'); 20 $table->integer('views');
21 - $table->timestamps(); 21 + $table->nullableTimestamps();
22 }); 22 });
23 } 23 }
24 24
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddSlugToRevisions extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('page_revisions', function (Blueprint $table) {
16 + $table->string('slug');
17 + $table->index('slug');
18 + $table->string('book_slug');
19 + $table->index('book_slug');
20 + });
21 + }
22 +
23 + /**
24 + * Reverse the migrations.
25 + *
26 + * @return void
27 + */
28 + public function down()
29 + {
30 + Schema::table('page_revisions', function (Blueprint $table) {
31 + $table->dropColumn('slug');
32 + $table->dropColumn('book_slug');
33 + });
34 + }
35 +}
...@@ -25,8 +25,13 @@ ...@@ -25,8 +25,13 @@
25 <env name="SESSION_DRIVER" value="array"/> 25 <env name="SESSION_DRIVER" value="array"/>
26 <env name="QUEUE_DRIVER" value="sync"/> 26 <env name="QUEUE_DRIVER" value="sync"/>
27 <env name="DB_CONNECTION" value="mysql_testing"/> 27 <env name="DB_CONNECTION" value="mysql_testing"/>
28 - <env name="MAIL_PRETEND" value="true"/> 28 + <env name="MAIL_DRIVER" value="log"/>
29 <env name="AUTH_METHOD" value="standard"/> 29 <env name="AUTH_METHOD" value="standard"/>
30 <env name="DISABLE_EXTERNAL_SERVICES" value="false"/> 30 <env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
31 + <env name="LDAP_VERSION" value="3"/>
32 + <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
33 + <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
34 + <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
35 + <env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
31 </php> 36 </php>
32 </phpunit> 37 </phpunit>
......
...@@ -106,6 +106,12 @@ $(function () { ...@@ -106,6 +106,12 @@ $(function () {
106 } 106 }
107 }); 107 });
108 108
109 + // Common jQuery actions
110 + $('[data-action="expand-entity-list-details"]').click(function() {
111 + $('.entity-list.compact').find('p').slideToggle(240);
112 + });
113 +
114 +
109 }); 115 });
110 116
111 117
......
...@@ -139,54 +139,6 @@ form.search-box { ...@@ -139,54 +139,6 @@ form.search-box {
139 height: 43px; 139 height: 43px;
140 } 140 }
141 141
142 -.dropdown-container {
143 - display: inline-block;
144 - vertical-align: top;
145 - position: relative;
146 -}
147 -
148 -.dropdown-container ul {
149 - display: none;
150 - position: absolute;
151 - z-index: 999;
152 - top: 0;
153 - list-style: none;
154 - right: 0;
155 - margin: $-m 0;
156 - background-color: #FFFFFF;
157 - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
158 - border-radius: 1px;
159 - border: 1px solid #EEE;
160 - min-width: 180px;
161 - padding: $-xs 0;
162 - color: #555;
163 - text-align: left !important;
164 - &.wide {
165 - min-width: 220px;
166 - }
167 - .text-muted {
168 - color: #999;
169 - }
170 - a {
171 - display: block;
172 - padding: $-xs $-m;
173 - color: #555;
174 - &:hover {
175 - text-decoration: none;
176 - background-color: #EEE;
177 - }
178 - i {
179 - margin-right: $-m;
180 - padding-right: 0;
181 - display: inline;
182 - width: 22px;
183 - }
184 - }
185 - li.border-bottom {
186 - border-bottom: 1px solid #DDD;
187 - }
188 -}
189 -
190 .breadcrumbs span.sep { 142 .breadcrumbs span.sep {
191 color: #aaa; 143 color: #aaa;
192 padding: 0 $-xs; 144 padding: 0 $-xs;
......
...@@ -283,4 +283,87 @@ ul.pagination { ...@@ -283,4 +283,87 @@ ul.pagination {
283 a { 283 a {
284 color: $primary; 284 color: $primary;
285 } 285 }
286 -}
...\ No newline at end of file ...\ No newline at end of file
286 +}
287 +
288 +.entity-list {
289 + >div {
290 + padding: $-m 0;
291 + }
292 + h3 {
293 + margin: 0;
294 + }
295 + p {
296 + margin: $-xs 0 0 0;
297 + }
298 + hr {
299 + margin: 0;
300 + }
301 + .text-small.text-muted {
302 + color: #AAA;
303 + font-size: 0.75em;
304 + margin-top: $-xs;
305 + }
306 +}
307 +.entity-list.compact {
308 + font-size: 0.6em;
309 + h3, a {
310 + line-height: 1.2;
311 + }
312 + p {
313 + display: none;
314 + font-size: $fs-m * 0.8;
315 + padding-top: $-xs;
316 + margin: 0;
317 + }
318 + hr {
319 + margin: 0;
320 + }
321 +}
322 +
323 +.dropdown-container {
324 + display: inline-block;
325 + vertical-align: top;
326 + position: relative;
327 +}
328 +
329 +.dropdown-container ul {
330 + display: none;
331 + position: absolute;
332 + z-index: 999;
333 + top: 0;
334 + list-style: none;
335 + right: 0;
336 + margin: $-m 0;
337 + background-color: #FFFFFF;
338 + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
339 + border-radius: 1px;
340 + border: 1px solid #EEE;
341 + min-width: 180px;
342 + padding: $-xs 0;
343 + color: #555;
344 + text-align: left !important;
345 + &.wide {
346 + min-width: 220px;
347 + }
348 + .text-muted {
349 + color: #999;
350 + }
351 + a {
352 + display: block;
353 + padding: $-xs $-m;
354 + color: #555;
355 + &:hover {
356 + text-decoration: none;
357 + background-color: #EEE;
358 + }
359 + i {
360 + margin-right: $-m;
361 + padding-right: 0;
362 + display: inline;
363 + width: 22px;
364 + }
365 + }
366 + li.border-bottom {
367 + border-bottom: 1px solid #DDD;
368 + }
369 +}
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
100 background-color: #FFF; 100 background-color: #FFF;
101 border: 1px solid #DDD; 101 border: 1px solid #DDD;
102 color: #666; 102 color: #666;
103 - width: 180px; 103 + width: 172px;
104 z-index: 40; 104 z-index: 40;
105 } 105 }
106 input, button { 106 input, button {
......
...@@ -20,4 +20,8 @@ table.table { ...@@ -20,4 +20,8 @@ table.table {
20 20
21 table { 21 table {
22 max-width: 100%; 22 max-width: 100%;
23 + thead {
24 + background-color: #F8F8F8;
25 + font-weight: 500;
26 + }
23 } 27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -115,7 +115,8 @@ pre { ...@@ -115,7 +115,8 @@ pre {
115 box-shadow: 0 1px 2px 0px rgba(10, 10, 10, 0.06); 115 box-shadow: 0 1px 2px 0px rgba(10, 10, 10, 0.06);
116 border: 1px solid rgba(221, 221, 221, 0.66); 116 border: 1px solid rgba(221, 221, 221, 0.66);
117 background-color: #fdf6e3; 117 background-color: #fdf6e3;
118 - padding: 0.5em; 118 + padding: $-s;
119 + overflow-x: scroll;
119 } 120 }
120 121
121 blockquote { 122 blockquote {
...@@ -251,6 +252,18 @@ ol { ...@@ -251,6 +252,18 @@ ol {
251 text-align: right; 252 text-align: right;
252 } 253 }
253 254
255 +.text-bigger {
256 + font-size: 1.1em;
257 +}
258 +
259 +.text-large {
260 + font-size: 1.6666em;
261 +}
262 +
263 +.no-color {
264 + color: inherit;
265 +}
266 +
254 /** 267 /**
255 * Grouping 268 * Grouping
256 */ 269 */
......
...@@ -47,6 +47,13 @@ body.dragging, body.dragging * { ...@@ -47,6 +47,13 @@ body.dragging, body.dragging * {
47 width: 80px; 47 width: 80px;
48 height: 80px; 48 height: 80px;
49 } 49 }
50 + &.huge {
51 + width: 120px;
52 + height: 120px;
53 + }
54 + &.square {
55 + border-radius: 3px;
56 + }
50 } 57 }
51 58
52 // System wide notifications 59 // System wide notifications
......
...@@ -58,10 +58,13 @@ ...@@ -58,10 +58,13 @@
58 </span> 58 </span>
59 <ul> 59 <ul>
60 <li> 60 <li>
61 - <a href="/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-lg"></i>Edit Profile</a> 61 + <a href="/user/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-account zmdi-hc-fw zmdi-hc-lg"></i>View Profile</a>
62 </li> 62 </li>
63 <li> 63 <li>
64 - <a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-lg"></i>Logout</a> 64 + <a href="/settings/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-fw zmdi-hc-lg"></i>Edit Profile</a>
65 + </li>
66 + <li>
67 + <a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-fw zmdi-hc-lg"></i>Logout</a>
65 </li> 68 </li>
66 </ul> 69 </ul>
67 </div> 70 </div>
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
10 <p class="text-muted">{{ $chapter->getExcerpt() }}</p> 10 <p class="text-muted">{{ $chapter->getExcerpt() }}</p>
11 @endif 11 @endif
12 12
13 - @if(count($chapter->pages) > 0 && !isset($hidePages)) 13 + @if(!isset($hidePages) && count($chapter->pages) > 0)
14 <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> 14 <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>
15 <div class="inset-list"> 15 <div class="inset-list">
16 @foreach($chapter->pages as $page) 16 @foreach($chapter->pages as $page)
......
...@@ -2,20 +2,44 @@ ...@@ -2,20 +2,44 @@
2 2
3 @section('content') 3 @section('content')
4 4
5 + <div class="faded-small toolbar">
6 + <div class="container">
7 + <div class="row">
8 + <div class="col-sm-4 faded">
9 + <div class="action-buttons text-left">
10 + <a data-action="expand-entity-list-details" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>Toggle Details</a>
11 + </div>
12 + </div>
13 + <div class="col-sm-8 faded">
14 + <div class="action-buttons">
15 +
16 + </div>
17 + </div>
18 + </div>
19 + </div>
20 + </div>
21 +
5 <div class="container" ng-non-bindable> 22 <div class="container" ng-non-bindable>
6 <div class="row"> 23 <div class="row">
7 24
8 - <div class="col-md-7"> 25 + <div class="col-sm-4">
9 @if($signedIn) 26 @if($signedIn)
10 - <h2>My Recently Viewed</h2> 27 + <h3>My Recently Viewed</h3>
11 @else 28 @else
12 - <h2>Recent Books</h2> 29 + <h3>Recent Books</h3>
13 @endif 30 @endif
14 - @include('partials/entity-list', ['entities' => $recents]) 31 + @include('partials/entity-list', ['entities' => $recents, 'style' => 'compact'])
32 + </div>
33 +
34 + <div class="col-sm-4">
35 + <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3>
36 + @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
37 +
38 + <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3>
39 + @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
15 </div> 40 </div>
16 41
17 - <div class="col-md-4 col-md-offset-1"> 42 + <div class="col-sm-4" id="recent-activity">
18 - <div class="margin-top large">&nbsp;</div>
19 <h3>Recent Activity</h3> 43 <h3>Recent Activity</h3>
20 @include('partials/activity-list', ['activity' => $activity]) 44 @include('partials/activity-list', ['activity' => $activity])
21 </div> 45 </div>
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="container">
6 + <div class="row">
7 +
8 + <div class="col-sm-7">
9 + <h1>{{ $title }}</h1>
10 + @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
11 + {!! $pages->links() !!}
12 + </div>
13 +
14 + <div class="col-sm-4 col-sm-offset-1"></div>
15 +
16 + </div>
17 + </div>
18 +@stop
...\ No newline at end of file ...\ No newline at end of file
...@@ -3,18 +3,29 @@ ...@@ -3,18 +3,29 @@
3 <a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a> 3 <a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a>
4 </h3> 4 </h3>
5 5
6 - @if(isset($showMeta) && $showMeta)
7 - <div class="meta">
8 - <span class="text-book"><i class="zmdi zmdi-book"></i> {{ $page->book->name }}</span>
9 - @if($page->chapter)
10 - <span class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i> {{ $page->chapter->name }}</span>
11 - @endif
12 - </div>
13 - @endif
14 -
15 @if(isset($page->searchSnippet)) 6 @if(isset($page->searchSnippet))
16 <p class="text-muted">{!! $page->searchSnippet !!}</p> 7 <p class="text-muted">{!! $page->searchSnippet !!}</p>
17 @else 8 @else
18 <p class="text-muted">{{ $page->getExcerpt() }}</p> 9 <p class="text-muted">{{ $page->getExcerpt() }}</p>
19 @endif 10 @endif
11 +
12 + @if(isset($style) && $style === 'detailed')
13 + <div class="row meta text-muted text-small">
14 + <div class="col-md-4">
15 + Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif <br>
16 + Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
17 + </div>
18 + <div class="col-md-8">
19 + <a class="text-book" href="{{ $page->book->getUrl() }}"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName(30) }}</a>
20 + <br>
21 + @if($page->chapter)
22 + <a class="text-chapter" href="{{ $page->chapter->getUrl() }}"><i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName(30) }}</a>
23 + @else
24 + <i class="zmdi zmdi-collection-bookmark"></i> Page is not in a chapter
25 + @endif
26 + </div>
27 + </div>
28 + @endif
29 +
30 +
20 </div> 31 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
9 9
10 <div class="right" ng-non-bindable> 10 <div class="right" ng-non-bindable>
11 @if($activity->user) 11 @if($activity->user)
12 - {{$activity->user->name}} 12 + <a href="/user/{{ $activity->user->id }}">{{$activity->user->name}}</a>
13 @else 13 @else
14 A deleted user 14 A deleted user
15 @endif 15 @endif
......
1 1
2 -@if(count($entities) > 0) 2 +<div class="entity-list @if(isset($style)){{ $style }}@endif" ng-non-bindable>
3 - @foreach($entities as $index => $entity) 3 + @if(count($entities) > 0)
4 - @if($entity->isA('page')) 4 + @foreach($entities as $index => $entity)
5 - @include('pages/list-item', ['page' => $entity]) 5 + @if($entity->isA('page'))
6 - @elseif($entity->isA('book')) 6 + @include('pages/list-item', ['page' => $entity])
7 - @include('books/list-item', ['book' => $entity]) 7 + @elseif($entity->isA('book'))
8 - @elseif($entity->isA('chapter')) 8 + @include('books/list-item', ['book' => $entity])
9 - @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true]) 9 + @elseif($entity->isA('chapter'))
10 - @endif 10 + @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true])
11 + @endif
11 12
12 - @if($index !== count($entities) - 1) 13 + @if($index !== count($entities) - 1)
13 - <hr> 14 + <hr>
14 - @endif 15 + @endif
15 16
16 - @endforeach
17 -@else
18 - <p class="text-muted">
19 - No items available
20 - </p>
21 -@endif
...\ No newline at end of file ...\ No newline at end of file
17 + @endforeach
18 + @else
19 + <p class="text-muted">
20 + No items available
21 + </p>
22 + @endif
23 +</div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -6,41 +6,36 @@ ...@@ -6,41 +6,36 @@
6 6
7 <h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{$searchTerm}}</span></h1> 7 <h1>Search Results&nbsp;&nbsp;&nbsp; <span class="text-muted">{{$searchTerm}}</span></h1>
8 8
9 + <p>
10 + <a href="/search/pages?term={{$searchTerm}}" class="text-page"><i class="zmdi zmdi-file-text"></i>View all matched pages</a>
11 +
12 + @if(count($chapters) > 0)
13 + &nbsp; &nbsp;&nbsp;
14 + <a href="/search/chapters?term={{$searchTerm}}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>View all matched chapters</a>
15 + @endif
16 +
17 + @if(count($books) > 0)
18 + &nbsp; &nbsp;&nbsp;
19 + <a href="/search/books?term={{$searchTerm}}" class="text-book"><i class="zmdi zmdi-book"></i>View all matched books</a>
20 + @endif
21 + </p>
9 <div class="row"> 22 <div class="row">
10 23
11 <div class="col-md-6"> 24 <div class="col-md-6">
12 - <h3>Matching Pages</h3> 25 + <h3><a href="/search/pages?term={{$searchTerm}}" class="no-color">Matching Pages</a></h3>
13 - <div class="page-list"> 26 + @include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
14 - @if(count($pages) > 0)
15 - @foreach($pages as $page)
16 - @include('pages/list-item', ['page' => $page, 'showMeta' => true])
17 - <hr>
18 - @endforeach
19 - @else
20 - <p class="text-muted">No pages matched this search</p>
21 - @endif
22 - </div>
23 </div> 27 </div>
24 28
25 <div class="col-md-5 col-md-offset-1"> 29 <div class="col-md-5 col-md-offset-1">
26 30
27 @if(count($books) > 0) 31 @if(count($books) > 0)
28 - <h3>Matching Books</h3> 32 + <h3><a href="/search/books?term={{$searchTerm}}" class="no-color">Matching Books</a></h3>
29 - <div class="page-list"> 33 + @include('partials/entity-list', ['entities' => $books])
30 - @foreach($books as $book)
31 - @include('books/list-item', ['book' => $book])
32 - <hr>
33 - @endforeach
34 - </div>
35 @endif 34 @endif
36 35
37 @if(count($chapters) > 0) 36 @if(count($chapters) > 0)
38 - <h3>Matching Chapters</h3> 37 + <h3><a href="/search/chapters?term={{$searchTerm}}" class="no-color">Matching Chapters</a></h3>
39 - <div class="page-list"> 38 + @include('partials/entity-list', ['entities' => $chapters])
40 - @foreach($chapters as $chapter)
41 - @include('chapters/list-item', ['chapter' => $chapter, 'hidePages' => true])
42 - @endforeach
43 - </div>
44 @endif 39 @endif
45 40
46 </div> 41 </div>
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="container">
6 + <div class="row">
7 +
8 + <div class="col-sm-7">
9 + <h1>{{ $title }} <small>{{$searchTerm}}</small></h1>
10 + @include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
11 + {!! $entities->links() !!}
12 + </div>
13 +
14 + <div class="col-sm-4 col-sm-offset-1"></div>
15 +
16 + </div>
17 + </div>
18 +@stop
...\ No newline at end of file ...\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 <div class="row"> 4 <div class="row">
5 <div class="col-md-12 setting-nav"> 5 <div class="col-md-12 setting-nav">
6 <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a> 6 <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
7 - <a href="/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a> 7 + <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
8 </div> 8 </div>
9 </div> 9 </div>
10 </div> 10 </div>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 <div class="container small" ng-non-bindable> 6 <div class="container small" ng-non-bindable>
7 <h1>Create User</h1> 7 <h1>Create User</h1>
8 8
9 - <form action="/users/create" method="post"> 9 + <form action="/settings/users/create" method="post">
10 {!! csrf_field() !!} 10 {!! csrf_field() !!}
11 @include('users.forms.' . $authMethod) 11 @include('users.forms.' . $authMethod)
12 </form> 12 </form>
......
...@@ -7,7 +7,7 @@ ...@@ -7,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> 7 <p>This will fully delete this user with the name '<span class="text-neg">{{$user->name}}</span>' from the system.</p>
8 <p class="text-neg">Are you sure you want to delete this user?</p> 8 <p class="text-neg">Are you sure you want to delete this user?</p>
9 9
10 - <form action="/users/{{$user->id}}" method="POST"> 10 + <form action="/settings/users/{{$user->id}}" method="POST">
11 {!! csrf_field() !!} 11 {!! csrf_field() !!}
12 <input type="hidden" name="_method" value="DELETE"> 12 <input type="hidden" name="_method" value="DELETE">
13 <a href="/users/{{$user->id}}" class="button muted">Cancel</a> 13 <a href="/users/{{$user->id}}" class="button muted">Cancel</a>
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
9 <div class="col-sm-6"></div> 9 <div class="col-sm-6"></div>
10 <div class="col-sm-6 faded"> 10 <div class="col-sm-6 faded">
11 <div class="action-buttons"> 11 <div class="action-buttons">
12 - <a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a> 12 + <a href="/settings/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
13 </div> 13 </div>
14 </div> 14 </div>
15 </div> 15 </div>
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
19 19
20 20
21 <div class="container small"> 21 <div class="container small">
22 - <form action="/users/{{$user->id}}" method="post"> 22 + <form action="/settings/users/{{$user->id}}" method="post">
23 <div class="row"> 23 <div class="row">
24 <div class="col-md-6" ng-non-bindable> 24 <div class="col-md-6" ng-non-bindable>
25 <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1> 25 <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
......
...@@ -25,6 +25,6 @@ ...@@ -25,6 +25,6 @@
25 @endif 25 @endif
26 26
27 <div class="form-group"> 27 <div class="form-group">
28 - <a href="/users" class="button muted">Cancel</a> 28 + <a href="/settings/users" class="button muted">Cancel</a>
29 <button class="button pos" type="submit">Save</button> 29 <button class="button pos" type="submit">Save</button>
30 </div> 30 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 35
36 <div class="form-group"> 36 <div class="form-group">
37 - <a href="/users" class="button muted">Cancel</a> 37 + <a href="/settings/users" class="button muted">Cancel</a>
38 <button class="button pos" type="submit">Save</button> 38 <button class="button pos" type="submit">Save</button>
39 </div> 39 </div>
40 40
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
10 <h1>Users</h1> 10 <h1>Users</h1>
11 @if($currentUser->can('user-create')) 11 @if($currentUser->can('user-create'))
12 <p> 12 <p>
13 - <a href="/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a> 13 + <a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a>
14 </p> 14 </p>
15 @endif 15 @endif
16 <table class="table"> 16 <table class="table">
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
25 <td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td> 25 <td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
26 <td> 26 <td>
27 @if($currentUser->can('user-update') || $currentUser->id == $user->id) 27 @if($currentUser->can('user-update') || $currentUser->id == $user->id)
28 - <a href="/users/{{$user->id}}"> 28 + <a href="/settings/users/{{$user->id}}">
29 @endif 29 @endif
30 {{ $user->name }} 30 {{ $user->name }}
31 @if($currentUser->can('user-update') || $currentUser->id == $user->id) 31 @if($currentUser->can('user-update') || $currentUser->id == $user->id)
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
34 </td> 34 </td>
35 <td> 35 <td>
36 @if($currentUser->can('user-update') || $currentUser->id == $user->id) 36 @if($currentUser->can('user-update') || $currentUser->id == $user->id)
37 - <a href="/users/{{$user->id}}"> 37 + <a href="/settings/users/{{$user->id}}">
38 @endif 38 @endif
39 {{ $user->email }} 39 {{ $user->email }}
40 @if($currentUser->can('user-update') || $currentUser->id == $user->id) 40 @if($currentUser->can('user-update') || $currentUser->id == $user->id)
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="container" ng-non-bindable>
6 + <div class="row">
7 + <div class="col-sm-7">
8 +
9 + <div class="padded-top large"></div>
10 +
11 + <div class="row">
12 + <div class="col-md-7">
13 + <div class="clearfix">
14 + <div class="padded-right float left">
15 + <img class="avatar square huge" src="{{$user->getAvatar(120)}}" alt="{{ $user->name }}">
16 + </div>
17 + <div>
18 + <h3 style="margin-top: 0;">{{ $user->name }}</h3>
19 + <p class="text-muted">
20 + User for {{ $user->created_at->diffForHumans(null, true) }}
21 + </p>
22 + </div>
23 + </div>
24 + </div>
25 + <div class="col-md-5 text-bigger" id="content-counts">
26 + <div class="text-muted">Created Content</div>
27 + <div class="text-book">
28 + <i class="zmdi zmdi-book zmdi-hc-fw"></i> {{ $assetCounts['books'] }} {{ str_plural('Book', $assetCounts['books']) }}
29 + </div>
30 + <div class="text-chapter">
31 + <i class="zmdi zmdi-collection-bookmark zmdi-hc-fw"></i> {{ $assetCounts['chapters'] }} {{ str_plural('Chapter', $assetCounts['chapters']) }}
32 + </div>
33 + <div class="text-page">
34 + <i class="zmdi zmdi-file-text zmdi-hc-fw"></i> {{ $assetCounts['pages'] }} {{ str_plural('Page', $assetCounts['pages']) }}
35 + </div>
36 + </div>
37 + </div>
38 +
39 +
40 + <hr class="even">
41 +
42 + <h3>Recently Created Pages</h3>
43 + @if (count($recentlyCreated['pages']) > 0)
44 + @include('partials/entity-list', ['entities' => $recentlyCreated['pages']])
45 + @else
46 + <p class="text-muted">{{ $user->name }} has not created any pages</p>
47 + @endif
48 +
49 + <hr class="even">
50 +
51 + <h3>Recently Created Chapters</h3>
52 + @if (count($recentlyCreated['chapters']) > 0)
53 + @include('partials/entity-list', ['entities' => $recentlyCreated['chapters']])
54 + @else
55 + <p class="text-muted">{{ $user->name }} has not created any chapters</p>
56 + @endif
57 +
58 + <hr class="even">
59 +
60 + <h3>Recently Created Books</h3>
61 + @if (count($recentlyCreated['books']) > 0)
62 + @include('partials/entity-list', ['entities' => $recentlyCreated['books']])
63 + @else
64 + <p class="text-muted">{{ $user->name }} has not created any books</p>
65 + @endif
66 + </div>
67 +
68 + <div class="col-sm-4 col-sm-offset-1" id="recent-activity">
69 + <h3>Recent Activity</h3>
70 + @include('partials/activity-list', ['activity' => $activity])
71 + </div>
72 +
73 + </div>
74 + </div>
75 +
76 +
77 +@stop
...\ No newline at end of file ...\ No newline at end of file
File mode changed
...@@ -129,7 +129,7 @@ class AuthTest extends TestCase ...@@ -129,7 +129,7 @@ class AuthTest extends TestCase
129 $user = factory(\BookStack\User::class)->make(); 129 $user = factory(\BookStack\User::class)->make();
130 130
131 $this->asAdmin() 131 $this->asAdmin()
132 - ->visit('/users') 132 + ->visit('/settings/users')
133 ->click('Add new user') 133 ->click('Add new user')
134 ->type($user->name, '#name') 134 ->type($user->name, '#name')
135 ->type($user->email, '#email') 135 ->type($user->email, '#email')
...@@ -138,7 +138,7 @@ class AuthTest extends TestCase ...@@ -138,7 +138,7 @@ class AuthTest extends TestCase
138 ->type($user->password, '#password-confirm') 138 ->type($user->password, '#password-confirm')
139 ->press('Save') 139 ->press('Save')
140 ->seeInDatabase('users', $user->toArray()) 140 ->seeInDatabase('users', $user->toArray())
141 - ->seePageIs('/users') 141 + ->seePageIs('/settings/users')
142 ->see($user->name); 142 ->see($user->name);
143 } 143 }
144 144
...@@ -147,13 +147,13 @@ class AuthTest extends TestCase ...@@ -147,13 +147,13 @@ class AuthTest extends TestCase
147 $user = \BookStack\User::all()->last(); 147 $user = \BookStack\User::all()->last();
148 $password = $user->password; 148 $password = $user->password;
149 $this->asAdmin() 149 $this->asAdmin()
150 - ->visit('/users') 150 + ->visit('/settings/users')
151 ->click($user->name) 151 ->click($user->name)
152 - ->seePageIs('/users/' . $user->id) 152 + ->seePageIs('/settings/users/' . $user->id)
153 ->see($user->email) 153 ->see($user->email)
154 ->type('Barry Scott', '#name') 154 ->type('Barry Scott', '#name')
155 ->press('Save') 155 ->press('Save')
156 - ->seePageIs('/users') 156 + ->seePageIs('/settings/users')
157 ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]) 157 ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
158 ->notSeeInDatabase('users', ['name' => $user->name]); 158 ->notSeeInDatabase('users', ['name' => $user->name]);
159 } 159 }
...@@ -161,7 +161,7 @@ class AuthTest extends TestCase ...@@ -161,7 +161,7 @@ class AuthTest extends TestCase
161 public function test_user_password_update() 161 public function test_user_password_update()
162 { 162 {
163 $user = \BookStack\User::all()->last(); 163 $user = \BookStack\User::all()->last();
164 - $userProfilePage = '/users/' . $user->id; 164 + $userProfilePage = '/settings/users/' . $user->id;
165 $this->asAdmin() 165 $this->asAdmin()
166 ->visit($userProfilePage) 166 ->visit($userProfilePage)
167 ->type('newpassword', '#password') 167 ->type('newpassword', '#password')
...@@ -172,7 +172,7 @@ class AuthTest extends TestCase ...@@ -172,7 +172,7 @@ class AuthTest extends TestCase
172 ->type('newpassword', '#password') 172 ->type('newpassword', '#password')
173 ->type('newpassword', '#password-confirm') 173 ->type('newpassword', '#password-confirm')
174 ->press('Save') 174 ->press('Save')
175 - ->seePageIs('/users'); 175 + ->seePageIs('/settings/users');
176 176
177 $userPassword = \BookStack\User::find($user->id)->password; 177 $userPassword = \BookStack\User::find($user->id)->password;
178 $this->assertTrue(Hash::check('newpassword', $userPassword)); 178 $this->assertTrue(Hash::check('newpassword', $userPassword));
...@@ -184,11 +184,11 @@ class AuthTest extends TestCase ...@@ -184,11 +184,11 @@ class AuthTest extends TestCase
184 $user = $this->getNewUser($userDetails->toArray()); 184 $user = $this->getNewUser($userDetails->toArray());
185 185
186 $this->asAdmin() 186 $this->asAdmin()
187 - ->visit('/users/' . $user->id) 187 + ->visit('/settings/users/' . $user->id)
188 ->click('Delete User') 188 ->click('Delete User')
189 ->see($user->name) 189 ->see($user->name)
190 ->press('Confirm') 190 ->press('Confirm')
191 - ->seePageIs('/users') 191 + ->seePageIs('/settings/users')
192 ->notSeeInDatabase('users', ['name' => $user->name]); 192 ->notSeeInDatabase('users', ['name' => $user->name]);
193 } 193 }
194 194
...@@ -199,10 +199,10 @@ class AuthTest extends TestCase ...@@ -199,10 +199,10 @@ class AuthTest extends TestCase
199 $this->assertEquals(1, $adminRole->users()->count()); 199 $this->assertEquals(1, $adminRole->users()->count());
200 $user = $adminRole->users->first(); 200 $user = $adminRole->users->first();
201 201
202 - $this->asAdmin()->visit('/users/' . $user->id) 202 + $this->asAdmin()->visit('/settings/users/' . $user->id)
203 ->click('Delete User') 203 ->click('Delete User')
204 ->press('Confirm') 204 ->press('Confirm')
205 - ->seePageIs('/users/' . $user->id) 205 + ->seePageIs('/settings/users/' . $user->id)
206 ->see('You cannot delete the only admin'); 206 ->see('You cannot delete the only admin');
207 } 207 }
208 208
......
...@@ -94,7 +94,7 @@ class LdapTest extends \TestCase ...@@ -94,7 +94,7 @@ class LdapTest extends \TestCase
94 94
95 public function test_create_user_form() 95 public function test_create_user_form()
96 { 96 {
97 - $this->asAdmin()->visit('/users/create') 97 + $this->asAdmin()->visit('/settings/users/create')
98 ->dontSee('Password') 98 ->dontSee('Password')
99 ->type($this->mockUser->name, '#name') 99 ->type($this->mockUser->name, '#name')
100 ->type($this->mockUser->email, '#email') 100 ->type($this->mockUser->email, '#email')
...@@ -102,19 +102,19 @@ class LdapTest extends \TestCase ...@@ -102,19 +102,19 @@ class LdapTest extends \TestCase
102 ->see('The external auth id field is required.') 102 ->see('The external auth id field is required.')
103 ->type($this->mockUser->name, '#external_auth_id') 103 ->type($this->mockUser->name, '#external_auth_id')
104 ->press('Save') 104 ->press('Save')
105 - ->seePageIs('/users') 105 + ->seePageIs('/settings/users')
106 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]); 106 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
107 } 107 }
108 108
109 public function test_user_edit_form() 109 public function test_user_edit_form()
110 { 110 {
111 $editUser = User::all()->last(); 111 $editUser = User::all()->last();
112 - $this->asAdmin()->visit('/users/' . $editUser->id) 112 + $this->asAdmin()->visit('/settings/users/' . $editUser->id)
113 ->see('Edit User') 113 ->see('Edit User')
114 ->dontSee('Password') 114 ->dontSee('Password')
115 ->type('test_auth_id', '#external_auth_id') 115 ->type('test_auth_id', '#external_auth_id')
116 ->press('Save') 116 ->press('Save')
117 - ->seePageIs('/users') 117 + ->seePageIs('/settings/users')
118 ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']); 118 ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
119 } 119 }
120 120
...@@ -127,7 +127,7 @@ class LdapTest extends \TestCase ...@@ -127,7 +127,7 @@ class LdapTest extends \TestCase
127 public function test_non_admins_cannot_change_auth_id() 127 public function test_non_admins_cannot_change_auth_id()
128 { 128 {
129 $testUser = User::all()->last(); 129 $testUser = User::all()->last();
130 - $this->actingAs($testUser)->visit('/users/' . $testUser->id) 130 + $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
131 ->dontSee('External Authentication'); 131 ->dontSee('External Authentication');
132 } 132 }
133 133
......
1 +<?php
2 +
3 +use Illuminate\Support\Facades\DB;
4 +
5 +class EntitySearchTest extends TestCase
6 +{
7 +
8 + public function test_page_search()
9 + {
10 + $book = \BookStack\Book::all()->first();
11 + $page = $book->pages->first();
12 +
13 + $this->asAdmin()
14 + ->visit('/')
15 + ->type($page->name, 'term')
16 + ->press('header-search-box-button')
17 + ->see('Search Results')
18 + ->see($page->name)
19 + ->click($page->name)
20 + ->seePageIs($page->getUrl());
21 + }
22 +
23 + public function test_invalid_page_search()
24 + {
25 + $this->asAdmin()
26 + ->visit('/')
27 + ->type('<p>test</p>', 'term')
28 + ->press('header-search-box-button')
29 + ->see('Search Results')
30 + ->seeStatusCode(200);
31 + }
32 +
33 + public function test_empty_search_redirects_back()
34 + {
35 + $this->asAdmin()
36 + ->visit('/')
37 + ->visit('/search/all')
38 + ->seePageIs('/');
39 + }
40 +
41 + public function test_book_search()
42 + {
43 + $book = \BookStack\Book::all()->first();
44 + $page = $book->pages->last();
45 + $chapter = $book->chapters->last();
46 +
47 + $this->asAdmin()
48 + ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
49 + ->see($page->name)
50 +
51 + ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
52 + ->see($chapter->name);
53 + }
54 +
55 + public function test_empty_book_search_redirects_back()
56 + {
57 + $book = \BookStack\Book::all()->first();
58 + $this->asAdmin()
59 + ->visit('/books')
60 + ->visit('/search/book/' . $book->id . '?term=')
61 + ->seePageIs('/books');
62 + }
63 +
64 +
65 + public function test_pages_search_listing()
66 + {
67 + $page = \BookStack\Page::all()->last();
68 + $this->asAdmin()->visit('/search/pages?term=' . $page->name)
69 + ->see('Page Search Results')->see('.entity-list', $page->name);
70 + }
71 +
72 + public function test_chapters_search_listing()
73 + {
74 + $chapter = \BookStack\Chapter::all()->last();
75 + $this->asAdmin()->visit('/search/chapters?term=' . $chapter->name)
76 + ->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
77 + }
78 +
79 + public function test_books_search_listing()
80 + {
81 + $book = \BookStack\Book::all()->last();
82 + $this->asAdmin()->visit('/search/books?term=' . $book->name)
83 + ->see('Book Search Results')->see('.entity-list', $book->name);
84 + }
85 +}
...@@ -155,63 +155,6 @@ class EntityTest extends TestCase ...@@ -155,63 +155,6 @@ class EntityTest extends TestCase
155 return $book; 155 return $book;
156 } 156 }
157 157
158 - public function test_page_search()
159 - {
160 - $book = \BookStack\Book::all()->first();
161 - $page = $book->pages->first();
162 -
163 - $this->asAdmin()
164 - ->visit('/')
165 - ->type($page->name, 'term')
166 - ->press('header-search-box-button')
167 - ->see('Search Results')
168 - ->see($page->name)
169 - ->click($page->name)
170 - ->seePageIs($page->getUrl());
171 - }
172 -
173 - public function test_invalid_page_search()
174 - {
175 - $this->asAdmin()
176 - ->visit('/')
177 - ->type('<p>test</p>', 'term')
178 - ->press('header-search-box-button')
179 - ->see('Search Results')
180 - ->seeStatusCode(200);
181 - }
182 -
183 - public function test_empty_search_redirects_back()
184 - {
185 - $this->asAdmin()
186 - ->visit('/')
187 - ->visit('/search/all')
188 - ->seePageIs('/');
189 - }
190 -
191 - public function test_book_search()
192 - {
193 - $book = \BookStack\Book::all()->first();
194 - $page = $book->pages->last();
195 - $chapter = $book->chapters->last();
196 -
197 - $this->asAdmin()
198 - ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
199 - ->see($page->name)
200 -
201 - ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
202 - ->see($chapter->name);
203 - }
204 -
205 - public function test_empty_book_search_redirects_back()
206 - {
207 - $book = \BookStack\Book::all()->first();
208 - $this->asAdmin()
209 - ->visit('/books')
210 - ->visit('/search/book/' . $book->id . '?term=')
211 - ->seePageIs('/books');
212 - }
213 -
214 -
215 public function test_entities_viewable_after_creator_deletion() 158 public function test_entities_viewable_after_creator_deletion()
216 { 159 {
217 // Create required assets and revisions 160 // Create required assets and revisions
...@@ -250,5 +193,36 @@ class EntityTest extends TestCase ...@@ -250,5 +193,36 @@ class EntityTest extends TestCase
250 ->click('Revisions')->seeStatusCode(200); 193 ->click('Revisions')->seeStatusCode(200);
251 } 194 }
252 195
196 + public function test_recently_created_pages_view()
197 + {
198 + $user = $this->getNewUser();
199 + $content = $this->createEntityChainBelongingToUser($user);
200 +
201 + $this->asAdmin()->visit('/pages/recently-created')
202 + ->seeInNthElement('.entity-list .page', 0, $content['page']->name);
203 + }
204 +
205 + public function test_recently_updated_pages_view()
206 + {
207 + $user = $this->getNewUser();
208 + $content = $this->createEntityChainBelongingToUser($user);
209 +
210 + $this->asAdmin()->visit('/pages/recently-updated')
211 + ->seeInNthElement('.entity-list .page', 0, $content['page']->name);
212 + }
213 +
214 + public function test_old_page_slugs_redirect_to_new_pages()
215 + {
216 + $page = \BookStack\Page::all()->first();
217 + $pageUrl = $page->getUrl();
218 + $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
219 + $this->asAdmin()->visit($pageUrl)
220 + ->clickInElement('#content', 'Edit')
221 + ->type('super test page', '#name')
222 + ->press('Save Page')
223 + ->seePageIs($newPageUrl)
224 + ->visit($pageUrl)
225 + ->seePageIs($newPageUrl);
226 + }
253 227
254 } 228 }
......
...@@ -109,4 +109,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -109,4 +109,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
109 109
110 return $this; 110 return $this;
111 } 111 }
112 +
113 + /**
114 + * Click the text within the selected element.
115 + * @param $parentElement
116 + * @param $linkText
117 + * @return $this
118 + */
119 + protected function clickInElement($parentElement, $linkText)
120 + {
121 + $elem = $this->crawler->filter($parentElement);
122 + $link = $elem->selectLink($linkText);
123 + $this->visit($link->link()->getUri());
124 + return $this;
125 + }
112 } 126 }
......
1 +<?php
2 +
3 +class UserProfileTest extends TestCase
4 +{
5 + protected $user;
6 +
7 + public function setUp()
8 + {
9 + parent::setUp();
10 + $this->user = \BookStack\User::all()->last();
11 + }
12 +
13 + public function test_profile_page_shows_name()
14 + {
15 + $this->asAdmin()
16 + ->visit('/user/' . $this->user->id)
17 + ->see($this->user->name);
18 + }
19 +
20 + public function test_profile_page_shows_recent_entities()
21 + {
22 + $content = $this->createEntityChainBelongingToUser($this->user, $this->user);
23 +
24 + $this->asAdmin()
25 + ->visit('/user/' . $this->user->id)
26 + // Check the recently created page is shown
27 + ->see($content['page']->name)
28 + // Check the recently created chapter is shown
29 + ->see($content['chapter']->name)
30 + // Check the recently created book is shown
31 + ->see($content['book']->name);
32 + }
33 +
34 + public function test_profile_page_shows_created_content_counts()
35 + {
36 + $newUser = $this->getNewUser();
37 +
38 + $this->asAdmin()->visit('/user/' . $newUser->id)
39 + ->see($newUser->name)
40 + ->seeInElement('#content-counts', '0 Books')
41 + ->seeInElement('#content-counts', '0 Chapters')
42 + ->seeInElement('#content-counts', '0 Pages');
43 +
44 + $this->createEntityChainBelongingToUser($newUser, $newUser);
45 +
46 + $this->asAdmin()->visit('/user/' . $newUser->id)
47 + ->see($newUser->name)
48 + ->seeInElement('#content-counts', '1 Book')
49 + ->seeInElement('#content-counts', '1 Chapter')
50 + ->seeInElement('#content-counts', '1 Page');
51 + }
52 +
53 + public function test_profile_page_shows_recent_activity()
54 + {
55 + $newUser = $this->getNewUser();
56 + $this->actingAs($newUser);
57 + $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
58 + Activity::add($entities['book'], 'book_update', $entities['book']->id);
59 + Activity::add($entities['page'], 'page_create', $entities['book']->id);
60 +
61 + $this->asAdmin()->visit('/user/' . $newUser->id)
62 + ->seeInElement('#recent-activity', 'updated book')
63 + ->seeInElement('#recent-activity', 'created page')
64 + ->seeInElement('#recent-activity', $entities['page']->name);
65 + }
66 +
67 + public function test_clicking_user_name_in_activity_leads_to_profile_page()
68 + {
69 + $newUser = $this->getNewUser();
70 + $this->actingAs($newUser);
71 + $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
72 + Activity::add($entities['book'], 'book_update', $entities['book']->id);
73 + Activity::add($entities['page'], 'page_create', $entities['book']->id);
74 +
75 + $this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name)
76 + ->seePageIs('/user/' . $newUser->id)
77 + ->see($newUser->name);
78 + }
79 +
80 +}