Improved sort efficiency by a factor of 10

Fixes #145
This commit is contained in:
Dan Brown 2016-08-26 20:20:58 +01:00
parent f83de5f834
commit 7973412c29
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 152 additions and 47 deletions

View File

@ -3,7 +3,6 @@
use Activity; use Activity;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests; use BookStack\Http\Requests;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo; use BookStack\Repos\ChapterRepo;
@ -180,21 +179,31 @@ class BookController extends Controller
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
$sortedBooks = [];
// Sort pages and chapters // Sort pages and chapters
$sortedBooks = [];
$updatedModels = collect();
$sortMap = json_decode($request->get('sort-tree')); $sortMap = json_decode($request->get('sort-tree'));
$defaultBookId = $book->id; $defaultBookId = $book->id;
foreach ($sortMap as $index => $bookChild) {
$id = $bookChild->id; // Loop through contents of provided map and update entities accordingly
foreach ($sortMap as $bookChild) {
$priority = $bookChild->sort;
$id = intval($bookChild->id);
$isPage = $bookChild->type == 'page'; $isPage = $bookChild->type == 'page';
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId; $bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id); $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
$model->priority = $index; // Update models only if there's a change in parent chain or ordering.
if ($isPage) { if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter; $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
$model->priority = $priority;
if ($isPage) $model->chapter_id = $chapterId;
$model->save();
$updatedModels->push($model);
} }
$model->save();
// Store involved books to be sorted later
if (!in_array($bookId, $sortedBooks)) { if (!in_array($bookId, $sortedBooks)) {
$sortedBooks[] = $bookId; $sortedBooks[] = $bookId;
} }
@ -203,10 +212,12 @@ class BookController extends Controller
// Add activity for books // Add activity for books
foreach ($sortedBooks as $bookId) { foreach ($sortedBooks as $bookId) {
$updatedBook = $this->bookRepo->getById($bookId); $updatedBook = $this->bookRepo->getById($bookId);
$this->bookRepo->updateBookPermissions($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id); Activity::add($updatedBook, 'book_sort', $updatedBook->id);
} }
// Update permissions on changed models
$this->bookRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }

View File

@ -204,7 +204,7 @@ class ChapterController extends Controller
return redirect()->back(); return redirect()->back();
} }
$this->chapterRepo->changeBook($parent->id, $chapter); $this->chapterRepo->changeBook($parent->id, $chapter, true);
Activity::add($chapter, 'chapter_move', $chapter->book->id); Activity::add($chapter, 'chapter_move', $chapter->book->id);
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name)); session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));

View File

@ -2,6 +2,7 @@
use Alpha\B; use Alpha\B;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Book; use BookStack\Book;
use Views; use Views;
@ -173,15 +174,6 @@ class BookRepo extends EntityRepo
$book->delete(); $book->delete();
} }
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Book $book
*/
public function updateBookPermissions(Book $book)
{
$this->permissionService->buildJointPermissionsForEntity($book);
}
/** /**
* Get the next child element priority. * Get the next child element priority.
* @param Book $book * @param Book $book

View File

@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo
/** /**
* Changes the book relation of this chapter. * Changes the book relation of this chapter.
* @param $bookId * @param $bookId
* @param Chapter $chapter * @param Chapter $chapter
* @param bool $rebuildPermissions
* @return Chapter * @return Chapter
*/ */
public function changeBook($bookId, Chapter $chapter) public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
{ {
$chapter->book_id = $bookId; $chapter->book_id = $bookId;
// Update related activity // Update related activity
@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo
foreach ($chapter->pages as $page) { foreach ($chapter->pages as $page) {
$this->pageRepo->changeBook($bookId, $page); $this->pageRepo->changeBook($bookId, $page);
} }
// Update permissions
$chapter->load('book'); // Update permissions if applicable
$this->permissionService->buildJointPermissionsForEntity($chapter->book); if ($rebuildPermissions) {
$chapter->load('book');
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
}
return $chapter; return $chapter;
} }

View File

@ -6,6 +6,7 @@ use BookStack\Entity;
use BookStack\Page; use BookStack\Page;
use BookStack\Services\PermissionService; use BookStack\Services\PermissionService;
use BookStack\User; use BookStack\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class EntityRepo class EntityRepo
@ -260,6 +261,15 @@ class EntityRepo
return $query; return $query;
} }
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Collection $collection collection on entities
*/
public function buildJointPermissions(Collection $collection)
{
$this->permissionService->buildJointPermissionsForEntities($collection);
}
} }

View File

@ -8,7 +8,7 @@ use BookStack\Ownable;
use BookStack\Page; use BookStack\Page;
use BookStack\Role; use BookStack\Role;
use BookStack\User; use BookStack\User;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Collection;
class PermissionService class PermissionService
{ {
@ -25,6 +25,8 @@ class PermissionService
protected $jointPermission; protected $jointPermission;
protected $role; protected $role;
protected $entityCache;
/** /**
* PermissionService constructor. * PermissionService constructor.
* @param JointPermission $jointPermission * @param JointPermission $jointPermission
@ -48,6 +50,57 @@ class PermissionService
$this->page = $page; $this->page = $page;
} }
/**
* Prepare the local entity cache and ensure it's empty
*/
protected function readyEntityCache()
{
$this->entityCache = [
'books' => collect(),
'chapters' => collect()
];
}
/**
* Get a book via ID, Checks local cache
* @param $bookId
* @return Book
*/
protected function getBook($bookId)
{
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
return $this->entityCache['books']->get($bookId);
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
}
return $book;
}
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return Book
*/
protected function getChapter($chapterId)
{
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
return $this->entityCache['chapters']->get($chapterId);
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
}
return $chapter;
}
/** /**
* Get the roles for the current user; * Get the roles for the current user;
* @return array|bool * @return array|bool
@ -76,6 +129,7 @@ class PermissionService
public function buildJointPermissions() public function buildJointPermissions()
{ {
$this->jointPermission->truncate(); $this->jointPermission->truncate();
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension) // Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get(); $roles = $this->role->with('permissions')->get();
@ -97,7 +151,7 @@ class PermissionService
} }
/** /**
* Create the entity jointPermissions for a particular entity. * Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity * @param Entity $entity
*/ */
public function buildJointPermissionsForEntity(Entity $entity) public function buildJointPermissionsForEntity(Entity $entity)
@ -116,6 +170,17 @@ class PermissionService
$this->createManyJointPermissions($entities, $roles); $this->createManyJointPermissions($entities, $roles);
} }
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->with('jointPermissions')->get();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/** /**
* Build the entity jointPermissions for a particular role. * Build the entity jointPermissions for a particular role.
* @param Role $role * @param Role $role
@ -177,9 +242,14 @@ class PermissionService
*/ */
protected function deleteManyJointPermissionsForEntities($entities) protected function deleteManyJointPermissionsForEntities($entities)
{ {
$query = $this->jointPermission->newQuery();
foreach ($entities as $entity) { foreach ($entities as $entity) {
$entity->jointPermissions()->delete(); $query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
} }
$query->delete();
} }
/** /**
@ -189,6 +259,7 @@ class PermissionService
*/ */
protected function createManyJointPermissions($entities, $roles) protected function createManyJointPermissions($entities, $roles)
{ {
$this->readyEntityCache();
$jointPermissions = []; $jointPermissions = [];
foreach ($entities as $entity) { foreach ($entities as $entity) {
foreach ($roles as $role) { foreach ($roles as $role) {
@ -248,8 +319,9 @@ class PermissionService
} elseif ($entity->isA('chapter')) { } elseif ($entity->isA('chapter')) {
if (!$entity->restricted) { if (!$entity->restricted) {
$hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); $book = $this->getBook($entity->book_id);
$hasPermissiveAccessToBook = !$entity->book->restricted; $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
return $this->createJointPermissionDataArray($entity, $role, $action, return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)), ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook))); ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
@ -261,11 +333,14 @@ class PermissionService
} elseif ($entity->isA('page')) { } elseif ($entity->isA('page')) {
if (!$entity->restricted) { if (!$entity->restricted) {
$hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction); $book = $this->getBook($entity->book_id);
$hasPermissiveAccessToBook = !$entity->book->restricted; $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction); $hasPermissiveAccessToBook = !$book->restricted;
$hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
$acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted); $chapter = $this->getChapter($entity->chapter_id);
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
$acknowledgeChapter = ($chapter && $chapter->restricted);
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook; $hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook; $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;

View File

@ -50,7 +50,7 @@
var sortableOptions = { var sortableOptions = {
group: 'serialization', group: 'serialization',
onDrop: function($item, container, _super) { onDrop: function($item, container, _super) {
var pageMap = buildPageMap(); var pageMap = buildEntityMap();
$('#sort-tree-input').val(JSON.stringify(pageMap)); $('#sort-tree-input').val(JSON.stringify(pageMap));
_super($item, container); _super($item, container);
}, },
@ -74,29 +74,42 @@
$link.remove(); $link.remove();
}); });
function buildPageMap() { /**
var pageMap = []; * Build up a mapping of entities with their ordering and nesting.
* @returns {Array}
*/
function buildEntityMap() {
var entityMap = [];
var $lists = $('.sort-list'); var $lists = $('.sort-list');
$lists.each(function(listIndex) { $lists.each(function(listIndex) {
var list = $(this); var list = $(this);
var bookId = list.closest('[data-type="book"]').attr('data-id'); var bookId = list.closest('[data-type="book"]').attr('data-id');
var $childElements = list.find('[data-type="page"], [data-type="chapter"]'); var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
$childElements.each(function(childIndex) { $directChildren.each(function(directChildIndex) {
var $childElem = $(this); var $childElem = $(this);
var type = $childElem.attr('data-type'); var type = $childElem.attr('data-type');
var parentChapter = false; var parentChapter = false;
if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) { var childId = $childElem.attr('data-id');
parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id'); entityMap.push({
} id: childId,
pageMap.push({ sort: directChildIndex,
id: $childElem.attr('data-id'),
parentChapter: parentChapter, parentChapter: parentChapter,
type: type, type: type,
book: bookId book: bookId
}); });
$chapterChildren = $childElem.find('[data-type="page"]').each(function(pageIndex) {
var $chapterChild = $(this);
entityMap.push({
id: $chapterChild.attr('data-id'),
sort: pageIndex,
parentChapter: childId,
type: 'page',
book: bookId
});
});
}); });
}); });
return pageMap; return entityMap;
} }
}); });