Dan Brown

Improved sort efficiency by a factor of 10

Fixes #145
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
3 use Activity; 3 use Activity;
4 use BookStack\Repos\UserRepo; 4 use BookStack\Repos\UserRepo;
5 use Illuminate\Http\Request; 5 use Illuminate\Http\Request;
6 -use Illuminate\Support\Facades\Auth;
7 use BookStack\Http\Requests; 6 use BookStack\Http\Requests;
8 use BookStack\Repos\BookRepo; 7 use BookStack\Repos\BookRepo;
9 use BookStack\Repos\ChapterRepo; 8 use BookStack\Repos\ChapterRepo;
...@@ -180,21 +179,31 @@ class BookController extends Controller ...@@ -180,21 +179,31 @@ class BookController extends Controller
180 return redirect($book->getUrl()); 179 return redirect($book->getUrl());
181 } 180 }
182 181
183 - $sortedBooks = [];
184 // Sort pages and chapters 182 // Sort pages and chapters
183 + $sortedBooks = [];
184 + $updatedModels = collect();
185 $sortMap = json_decode($request->get('sort-tree')); 185 $sortMap = json_decode($request->get('sort-tree'));
186 $defaultBookId = $book->id; 186 $defaultBookId = $book->id;
187 - foreach ($sortMap as $index => $bookChild) { 187 +
188 - $id = $bookChild->id; 188 + // Loop through contents of provided map and update entities accordingly
189 + foreach ($sortMap as $bookChild) {
190 + $priority = $bookChild->sort;
191 + $id = intval($bookChild->id);
189 $isPage = $bookChild->type == 'page'; 192 $isPage = $bookChild->type == 'page';
190 - $bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId; 193 + $bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
194 + $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
191 $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id); 195 $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
192 - $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model); 196 +
193 - $model->priority = $index; 197 + // Update models only if there's a change in parent chain or ordering.
194 - if ($isPage) { 198 + if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
195 - $model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter; 199 + $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
200 + $model->priority = $priority;
201 + if ($isPage) $model->chapter_id = $chapterId;
202 + $model->save();
203 + $updatedModels->push($model);
196 } 204 }
197 - $model->save(); 205 +
206 + // Store involved books to be sorted later
198 if (!in_array($bookId, $sortedBooks)) { 207 if (!in_array($bookId, $sortedBooks)) {
199 $sortedBooks[] = $bookId; 208 $sortedBooks[] = $bookId;
200 } 209 }
...@@ -203,10 +212,12 @@ class BookController extends Controller ...@@ -203,10 +212,12 @@ class BookController extends Controller
203 // Add activity for books 212 // Add activity for books
204 foreach ($sortedBooks as $bookId) { 213 foreach ($sortedBooks as $bookId) {
205 $updatedBook = $this->bookRepo->getById($bookId); 214 $updatedBook = $this->bookRepo->getById($bookId);
206 - $this->bookRepo->updateBookPermissions($updatedBook);
207 Activity::add($updatedBook, 'book_sort', $updatedBook->id); 215 Activity::add($updatedBook, 'book_sort', $updatedBook->id);
208 } 216 }
209 217
218 + // Update permissions on changed models
219 + $this->bookRepo->buildJointPermissions($updatedModels);
220 +
210 return redirect($book->getUrl()); 221 return redirect($book->getUrl());
211 } 222 }
212 223
......
...@@ -204,7 +204,7 @@ class ChapterController extends Controller ...@@ -204,7 +204,7 @@ class ChapterController extends Controller
204 return redirect()->back(); 204 return redirect()->back();
205 } 205 }
206 206
207 - $this->chapterRepo->changeBook($parent->id, $chapter); 207 + $this->chapterRepo->changeBook($parent->id, $chapter, true);
208 Activity::add($chapter, 'chapter_move', $chapter->book->id); 208 Activity::add($chapter, 'chapter_move', $chapter->book->id);
209 session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name)); 209 session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
210 210
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 use Alpha\B; 3 use Alpha\B;
4 use BookStack\Exceptions\NotFoundException; 4 use BookStack\Exceptions\NotFoundException;
5 +use Illuminate\Database\Eloquent\Collection;
5 use Illuminate\Support\Str; 6 use Illuminate\Support\Str;
6 use BookStack\Book; 7 use BookStack\Book;
7 use Views; 8 use Views;
...@@ -174,15 +175,6 @@ class BookRepo extends EntityRepo ...@@ -174,15 +175,6 @@ class BookRepo extends EntityRepo
174 } 175 }
175 176
176 /** 177 /**
177 - * Alias method to update the book jointPermissions in the PermissionService.
178 - * @param Book $book
179 - */
180 - public function updateBookPermissions(Book $book)
181 - {
182 - $this->permissionService->buildJointPermissionsForEntity($book);
183 - }
184 -
185 - /**
186 * Get the next child element priority. 178 * Get the next child element priority.
187 * @param Book $book 179 * @param Book $book
188 * @return int 180 * @return int
......
...@@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo ...@@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo
195 195
196 /** 196 /**
197 * Changes the book relation of this chapter. 197 * Changes the book relation of this chapter.
198 - * @param $bookId 198 + * @param $bookId
199 * @param Chapter $chapter 199 * @param Chapter $chapter
200 + * @param bool $rebuildPermissions
200 * @return Chapter 201 * @return Chapter
201 */ 202 */
202 - public function changeBook($bookId, Chapter $chapter) 203 + public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
203 { 204 {
204 $chapter->book_id = $bookId; 205 $chapter->book_id = $bookId;
205 // Update related activity 206 // Update related activity
...@@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo ...@@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo
213 foreach ($chapter->pages as $page) { 214 foreach ($chapter->pages as $page) {
214 $this->pageRepo->changeBook($bookId, $page); 215 $this->pageRepo->changeBook($bookId, $page);
215 } 216 }
216 - // Update permissions 217 +
217 - $chapter->load('book'); 218 + // Update permissions if applicable
218 - $this->permissionService->buildJointPermissionsForEntity($chapter->book); 219 + if ($rebuildPermissions) {
220 + $chapter->load('book');
221 + $this->permissionService->buildJointPermissionsForEntity($chapter->book);
222 + }
219 223
220 return $chapter; 224 return $chapter;
221 } 225 }
......
...@@ -6,6 +6,7 @@ use BookStack\Entity; ...@@ -6,6 +6,7 @@ use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 use BookStack\Services\PermissionService; 7 use BookStack\Services\PermissionService;
8 use BookStack\User; 8 use BookStack\User;
9 +use Illuminate\Support\Collection;
9 use Illuminate\Support\Facades\Log; 10 use Illuminate\Support\Facades\Log;
10 11
11 class EntityRepo 12 class EntityRepo
...@@ -260,6 +261,15 @@ class EntityRepo ...@@ -260,6 +261,15 @@ class EntityRepo
260 return $query; 261 return $query;
261 } 262 }
262 263
264 + /**
265 + * Alias method to update the book jointPermissions in the PermissionService.
266 + * @param Collection $collection collection on entities
267 + */
268 + public function buildJointPermissions(Collection $collection)
269 + {
270 + $this->permissionService->buildJointPermissionsForEntities($collection);
271 + }
272 +
263 } 273 }
264 274
265 275
......
...@@ -8,7 +8,7 @@ use BookStack\Ownable; ...@@ -8,7 +8,7 @@ use BookStack\Ownable;
8 use BookStack\Page; 8 use BookStack\Page;
9 use BookStack\Role; 9 use BookStack\Role;
10 use BookStack\User; 10 use BookStack\User;
11 -use Illuminate\Database\Eloquent\Collection; 11 +use Illuminate\Support\Collection;
12 12
13 class PermissionService 13 class PermissionService
14 { 14 {
...@@ -25,6 +25,8 @@ class PermissionService ...@@ -25,6 +25,8 @@ class PermissionService
25 protected $jointPermission; 25 protected $jointPermission;
26 protected $role; 26 protected $role;
27 27
28 + protected $entityCache;
29 +
28 /** 30 /**
29 * PermissionService constructor. 31 * PermissionService constructor.
30 * @param JointPermission $jointPermission 32 * @param JointPermission $jointPermission
...@@ -49,6 +51,57 @@ class PermissionService ...@@ -49,6 +51,57 @@ class PermissionService
49 } 51 }
50 52
51 /** 53 /**
54 + * Prepare the local entity cache and ensure it's empty
55 + */
56 + protected function readyEntityCache()
57 + {
58 + $this->entityCache = [
59 + 'books' => collect(),
60 + 'chapters' => collect()
61 + ];
62 + }
63 +
64 + /**
65 + * Get a book via ID, Checks local cache
66 + * @param $bookId
67 + * @return Book
68 + */
69 + protected function getBook($bookId)
70 + {
71 + if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
72 + return $this->entityCache['books']->get($bookId);
73 + }
74 +
75 + $book = $this->book->find($bookId);
76 + if ($book === null) $book = false;
77 + if (isset($this->entityCache['books'])) {
78 + $this->entityCache['books']->put($bookId, $book);
79 + }
80 +
81 + return $book;
82 + }
83 +
84 + /**
85 + * Get a chapter via ID, Checks local cache
86 + * @param $chapterId
87 + * @return Book
88 + */
89 + protected function getChapter($chapterId)
90 + {
91 + if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
92 + return $this->entityCache['chapters']->get($chapterId);
93 + }
94 +
95 + $chapter = $this->chapter->find($chapterId);
96 + if ($chapter === null) $chapter = false;
97 + if (isset($this->entityCache['chapters'])) {
98 + $this->entityCache['chapters']->put($chapterId, $chapter);
99 + }
100 +
101 + return $chapter;
102 + }
103 +
104 + /**
52 * Get the roles for the current user; 105 * Get the roles for the current user;
53 * @return array|bool 106 * @return array|bool
54 */ 107 */
...@@ -76,6 +129,7 @@ class PermissionService ...@@ -76,6 +129,7 @@ class PermissionService
76 public function buildJointPermissions() 129 public function buildJointPermissions()
77 { 130 {
78 $this->jointPermission->truncate(); 131 $this->jointPermission->truncate();
132 + $this->readyEntityCache();
79 133
80 // Get all roles (Should be the most limited dimension) 134 // Get all roles (Should be the most limited dimension)
81 $roles = $this->role->with('permissions')->get(); 135 $roles = $this->role->with('permissions')->get();
...@@ -97,7 +151,7 @@ class PermissionService ...@@ -97,7 +151,7 @@ class PermissionService
97 } 151 }
98 152
99 /** 153 /**
100 - * Create the entity jointPermissions for a particular entity. 154 + * Rebuild the entity jointPermissions for a particular entity.
101 * @param Entity $entity 155 * @param Entity $entity
102 */ 156 */
103 public function buildJointPermissionsForEntity(Entity $entity) 157 public function buildJointPermissionsForEntity(Entity $entity)
...@@ -117,6 +171,17 @@ class PermissionService ...@@ -117,6 +171,17 @@ class PermissionService
117 } 171 }
118 172
119 /** 173 /**
174 + * Rebuild the entity jointPermissions for a collection of entities.
175 + * @param Collection $entities
176 + */
177 + public function buildJointPermissionsForEntities(Collection $entities)
178 + {
179 + $roles = $this->role->with('jointPermissions')->get();
180 + $this->deleteManyJointPermissionsForEntities($entities);
181 + $this->createManyJointPermissions($entities, $roles);
182 + }
183 +
184 + /**
120 * Build the entity jointPermissions for a particular role. 185 * Build the entity jointPermissions for a particular role.
121 * @param Role $role 186 * @param Role $role
122 */ 187 */
...@@ -177,9 +242,14 @@ class PermissionService ...@@ -177,9 +242,14 @@ class PermissionService
177 */ 242 */
178 protected function deleteManyJointPermissionsForEntities($entities) 243 protected function deleteManyJointPermissionsForEntities($entities)
179 { 244 {
245 + $query = $this->jointPermission->newQuery();
180 foreach ($entities as $entity) { 246 foreach ($entities as $entity) {
181 - $entity->jointPermissions()->delete(); 247 + $query->orWhere(function($query) use ($entity) {
248 + $query->where('entity_id', '=', $entity->id)
249 + ->where('entity_type', '=', $entity->getMorphClass());
250 + });
182 } 251 }
252 + $query->delete();
183 } 253 }
184 254
185 /** 255 /**
...@@ -189,6 +259,7 @@ class PermissionService ...@@ -189,6 +259,7 @@ class PermissionService
189 */ 259 */
190 protected function createManyJointPermissions($entities, $roles) 260 protected function createManyJointPermissions($entities, $roles)
191 { 261 {
262 + $this->readyEntityCache();
192 $jointPermissions = []; 263 $jointPermissions = [];
193 foreach ($entities as $entity) { 264 foreach ($entities as $entity) {
194 foreach ($roles as $role) { 265 foreach ($roles as $role) {
...@@ -248,8 +319,9 @@ class PermissionService ...@@ -248,8 +319,9 @@ class PermissionService
248 } elseif ($entity->isA('chapter')) { 319 } elseif ($entity->isA('chapter')) {
249 320
250 if (!$entity->restricted) { 321 if (!$entity->restricted) {
251 - $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); 322 + $book = $this->getBook($entity->book_id);
252 - $hasPermissiveAccessToBook = !$entity->book->restricted; 323 + $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
324 + $hasPermissiveAccessToBook = !$book->restricted;
253 return $this->createJointPermissionDataArray($entity, $role, $action, 325 return $this->createJointPermissionDataArray($entity, $role, $action,
254 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)), 326 ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
255 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook))); 327 ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
...@@ -261,11 +333,14 @@ class PermissionService ...@@ -261,11 +333,14 @@ class PermissionService
261 } elseif ($entity->isA('page')) { 333 } elseif ($entity->isA('page')) {
262 334
263 if (!$entity->restricted) { 335 if (!$entity->restricted) {
264 - $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); 336 + $book = $this->getBook($entity->book_id);
265 - $hasPermissiveAccessToBook = !$entity->book->restricted; 337 + $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
266 - $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction); 338 + $hasPermissiveAccessToBook = !$book->restricted;
267 - $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted; 339 +
268 - $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted); 340 + $chapter = $this->getChapter($entity->chapter_id);
341 + $hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
342 + $hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
343 + $acknowledgeChapter = ($chapter && $chapter->restricted);
269 344
270 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook; 345 $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
271 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook; 346 $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
50 var sortableOptions = { 50 var sortableOptions = {
51 group: 'serialization', 51 group: 'serialization',
52 onDrop: function($item, container, _super) { 52 onDrop: function($item, container, _super) {
53 - var pageMap = buildPageMap(); 53 + var pageMap = buildEntityMap();
54 $('#sort-tree-input').val(JSON.stringify(pageMap)); 54 $('#sort-tree-input').val(JSON.stringify(pageMap));
55 _super($item, container); 55 _super($item, container);
56 }, 56 },
...@@ -74,29 +74,42 @@ ...@@ -74,29 +74,42 @@
74 $link.remove(); 74 $link.remove();
75 }); 75 });
76 76
77 - function buildPageMap() { 77 + /**
78 - var pageMap = []; 78 + * Build up a mapping of entities with their ordering and nesting.
79 + * @returns {Array}
80 + */
81 + function buildEntityMap() {
82 + var entityMap = [];
79 var $lists = $('.sort-list'); 83 var $lists = $('.sort-list');
80 $lists.each(function(listIndex) { 84 $lists.each(function(listIndex) {
81 var list = $(this); 85 var list = $(this);
82 var bookId = list.closest('[data-type="book"]').attr('data-id'); 86 var bookId = list.closest('[data-type="book"]').attr('data-id');
83 - var $childElements = list.find('[data-type="page"], [data-type="chapter"]'); 87 + var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
84 - $childElements.each(function(childIndex) { 88 + $directChildren.each(function(directChildIndex) {
85 var $childElem = $(this); 89 var $childElem = $(this);
86 var type = $childElem.attr('data-type'); 90 var type = $childElem.attr('data-type');
87 var parentChapter = false; 91 var parentChapter = false;
88 - if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) { 92 + var childId = $childElem.attr('data-id');
89 - parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id'); 93 + entityMap.push({
90 - } 94 + id: childId,
91 - pageMap.push({ 95 + sort: directChildIndex,
92 - id: $childElem.attr('data-id'),
93 parentChapter: parentChapter, 96 parentChapter: parentChapter,
94 type: type, 97 type: type,
95 book: bookId 98 book: bookId
96 }); 99 });
100 + $chapterChildren = $childElem.find('[data-type="page"]').each(function(pageIndex) {
101 + var $chapterChild = $(this);
102 + entityMap.push({
103 + id: $chapterChild.attr('data-id'),
104 + sort: pageIndex,
105 + parentChapter: childId,
106 + type: 'page',
107 + book: bookId
108 + });
109 + });
97 }); 110 });
98 }); 111 });
99 - return pageMap; 112 + return entityMap;
100 } 113 }
101 114
102 }); 115 });
......