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

View File

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

View File

@ -2,6 +2,7 @@
use Alpha\B;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
@ -173,15 +174,6 @@ class BookRepo extends EntityRepo
$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.
* @param Book $book

View File

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

View File

@ -6,6 +6,7 @@ use BookStack\Entity;
use BookStack\Page;
use BookStack\Services\PermissionService;
use BookStack\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class EntityRepo
@ -260,6 +261,15 @@ class EntityRepo
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\Role;
use BookStack\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection;
class PermissionService
{
@ -25,6 +25,8 @@ class PermissionService
protected $jointPermission;
protected $role;
protected $entityCache;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
@ -48,6 +50,57 @@ class PermissionService
$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;
* @return array|bool
@ -76,6 +129,7 @@ class PermissionService
public function buildJointPermissions()
{
$this->jointPermission->truncate();
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension)
$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
*/
public function buildJointPermissionsForEntity(Entity $entity)
@ -116,6 +170,17 @@ class PermissionService
$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.
* @param Role $role
@ -177,9 +242,14 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
$query = $this->jointPermission->newQuery();
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)
{
$this->readyEntityCache();
$jointPermissions = [];
foreach ($entities as $entity) {
foreach ($roles as $role) {
@ -248,8 +319,9 @@ class PermissionService
} elseif ($entity->isA('chapter')) {
if (!$entity->restricted) {
$hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$entity->book->restricted;
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
@ -261,11 +333,14 @@ class PermissionService
} elseif ($entity->isA('page')) {
if (!$entity->restricted) {
$hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$entity->book->restricted;
$hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
$acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->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;
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;

View File

@ -50,7 +50,7 @@
var sortableOptions = {
group: 'serialization',
onDrop: function($item, container, _super) {
var pageMap = buildPageMap();
var pageMap = buildEntityMap();
$('#sort-tree-input').val(JSON.stringify(pageMap));
_super($item, container);
},
@ -74,29 +74,42 @@
$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');
$lists.each(function(listIndex) {
var list = $(this);
var bookId = list.closest('[data-type="book"]').attr('data-id');
var $childElements = list.find('[data-type="page"], [data-type="chapter"]');
$childElements.each(function(childIndex) {
var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
$directChildren.each(function(directChildIndex) {
var $childElem = $(this);
var type = $childElem.attr('data-type');
var parentChapter = false;
if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) {
parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id');
}
pageMap.push({
id: $childElem.attr('data-id'),
var childId = $childElem.attr('data-id');
entityMap.push({
id: childId,
sort: directChildIndex,
parentChapter: parentChapter,
type: type,
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;
}
});