Dan Brown

Merge branch 'master' into add_role_view_permissions

Showing 74 changed files with 169 additions and 125 deletions
......@@ -36,7 +36,6 @@ class BookController extends Controller
/**
* Display a listing of the book.
*
* @return Response
*/
public function index()
......@@ -50,7 +49,6 @@ class BookController extends Controller
/**
* Show the form for creating a new book.
*
* @return Response
*/
public function create()
......@@ -84,7 +82,6 @@ class BookController extends Controller
/**
* Display the specified book.
*
* @param $slug
* @return Response
*/
......@@ -100,7 +97,6 @@ class BookController extends Controller
/**
* Show the form for editing the specified book.
*
* @param $slug
* @return Response
*/
......@@ -114,7 +110,6 @@ class BookController extends Controller
/**
* Update the specified book in storage.
*
* @param Request $request
* @param $slug
* @return Response
......@@ -157,7 +152,7 @@ class BookController extends Controller
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->bookRepo->getChildren($book);
$bookChildren = $this->bookRepo->getChildren($book, true);
$books = $this->bookRepo->getAll(false);
$this->setPageTitle('Sort Book ' . $book->getShortName());
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
......
......@@ -4,6 +4,7 @@ use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
......@@ -216,8 +217,14 @@ class PageController extends Controller
} else {
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
}
$updateTime = $draft->updated_at->format('H:i');
return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => 'Draft saved at ',
'timestamp' => $utcUpdateTimestamp
]);
}
/**
......
......@@ -198,16 +198,23 @@ class BookRepo extends EntityRepo
* Returns a sorted collection of Pages and Chapters.
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @param bool $filterDrafts
* @return mixed
*/
public function getChildren(Book $book)
public function getChildren(Book $book, $filterDrafts = false)
{
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
if ($filterDrafts) {
$pageQuery = $pageQuery->where('draft', '=', false);
}
$pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with(['pages' => function($query) {
$chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) {
$this->restrictionService->enforcePageRestrictions($query, 'view');
if ($filterDrafts) $query->where('draft', '=', false);
}]);
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
......
......@@ -84,7 +84,7 @@ class EntityRepo
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->skip($page * $count)->take($count)->get();
return $query->with('book')->skip($page * $count)->take($count)->get();
}
/**
......@@ -114,7 +114,7 @@ class EntityRepo
{
return $this->restrictionService->enforcePageRestrictions($this->page)
->where('draft', '=', false)
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
}
/**
......
......@@ -154,10 +154,10 @@ class PageRepo extends EntityRepo
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|null $chapter
* @param Chapter|bool $chapter
* @return static
*/
public function getDraftPage(Book $book, $chapter)
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = 'New Page';
......
<?php namespace BookStack\Services;
use BookStack\Entity;
use BookStack\View;
......@@ -47,7 +46,6 @@ class ViewService
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
......@@ -58,17 +56,13 @@ class ViewService
{
$skipCount = $count * $page;
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults();
});
return $viewedEntities;
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
}
/**
......@@ -81,21 +75,18 @@ class ViewService
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
if ($this->user === null) return collect();
$skipCount = $count * $page;
$query = $this->restrictionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', auth()->user()->id);
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable;
});
return $viewedEntities;
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable');
return $viewables;
}
/**
* Reset all view counts by deleting all views.
*/
......@@ -104,5 +95,4 @@ class ViewService
$this->view->truncate();
}
}
\ No newline at end of file
......
......@@ -139,6 +139,6 @@ return [
|
*/
'redis' => $redisConfig,
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
];
......
......@@ -13,6 +13,7 @@
"dropzone": "^4.0.1",
"laravel-elixir": "^3.4.0",
"marked": "^0.3.5",
"moment": "^2.12.0",
"zeroclipboard": "^2.2.0"
}
}
......
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
"use strict";
var moment = require('moment');
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
......@@ -367,7 +369,8 @@ module.exports = function (ngApp, events) {
if (isMarkdown) data.markdown = $scope.editContent;
$http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => {
$scope.draftText = responseData.data.message;
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
});
}
......
......@@ -11,7 +11,7 @@ var mceOptions = module.exports = {
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
plugins: "image table textcolor paste link fullscreen imagetools code hr autosave",
plugins: "image table textcolor paste link fullscreen imagetools code hr autosave lists",
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
......
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
// Generated using https://google-webfonts-helper.herokuapp.com
/* roboto-100 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bold-webfont.eot');
src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
url('/fonts/roboto-bold-webfont.woff') format('woff'),
url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
font-weight: bold;
font-style: normal;
font-weight: 100;
src: local('Roboto Thin'), local('Roboto-Thin'),
url('/fonts/roboto-v15-cyrillic_latin-100.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-100.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-100italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bolditalic-webfont.eot');
src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
font-weight: bold;
font-style: italic;
font-weight: 100;
src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'),
url('/fonts/roboto-v15-cyrillic_latin-100italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-100italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-300 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-italic-webfont.eot');
src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
url('/fonts/roboto-italic-webfont.woff') format('woff'),
url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
font-weight: normal;
font-style: italic;
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url('/fonts/roboto-v15-cyrillic_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-300italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-light-webfont.eot');
src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-light-webfont.woff2') format('woff2'),
url('/fonts/roboto-light-webfont.woff') format('woff'),
url('/fonts/roboto-light-webfont.ttf') format('truetype'),
url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url('/fonts/roboto-v15-cyrillic_latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-regular - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'),
url('/fonts/roboto-v15-cyrillic_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-lightitalic-webfont.eot');
src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
font-weight: 300;
font-style: italic;
font-weight: 400;
src: local('Roboto Italic'), local('Roboto-Italic'),
url('/fonts/roboto-v15-cyrillic_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-500 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-medium-webfont.eot');
src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
url('/fonts/roboto-medium-webfont.woff') format('woff'),
url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
font-weight: 500;
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'),
url('/fonts/roboto-v15-cyrillic_latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-500italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-mediumitalic-webfont.eot');
src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
font-weight: 500;
font-style: italic;
font-weight: 500;
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
url('/fonts/roboto-v15-cyrillic_latin-500italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-500italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-700 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular-webfont.eot');
src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
url('/fonts/roboto-regular-webfont.woff') format('woff'),
url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
font-weight: normal;
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url('/fonts/roboto-v15-cyrillic_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-700italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url('/fonts/roboto-v15-cyrillic_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('/fonts/roboto-v15-cyrillic_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-mono-regular - latin */
// https://google-webfonts-helper.herokuapp.com
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
......
......@@ -25,3 +25,12 @@ table {
font-weight: 500;
}
}
table.list-table {
margin: 0 -$-xs;
td {
border: 0;
vertical-align: middle;
padding: $-xs;
}
}
\ No newline at end of file
......
......@@ -2,7 +2,9 @@
<div class="row">
<div class="col-md-6">
<div class="col-md-9">
<div class="row">
<div class="col-md-5">
<h3>Role Details</h3>
<div class="form-group">
<label for="name">Role Name</label>
......@@ -13,43 +15,24 @@
@include('form/text', ['name' => 'description'])
</div>
<h3>System Permissions</h3>
<div class="row">
<div class="col-md-6">
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
</div>
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage user roles</label>
</div>
</div>
<hr class="even">
<div class="row">
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage roles & role permissions</label>
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label>
</div>
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage permissions on own Book, Chapter & Pages</label>
</div>
</div>
<hr class="even">
<div class="form-group">
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
</div>
<hr class="even">
</div>
<div class="col-md-6">
<h3>Asset Permissions</h3>
<p>
These permissions control default access to the assets within the system. <br>
These permissions control default access to the assets within the system.
Permissions on Books, Chapters and Pages will override these permissions.
</p>
<table class="table">
<tr>
<th></th>
<th>Create</th>
<th>View</th>
<th>Edit</th>
<th>Delete</th>
</tr>
......@@ -112,7 +95,6 @@
<tr>
<td>Images</td>
<td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
<td></td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
......@@ -124,8 +106,38 @@
</tr>
</table>
</div>
</div>
<a href="/settings/roles" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Role</button>
</div>
<div class="col-md-3">
<h3>Users in this role</h3>
</div>
@if(isset($role) && count($role->users) > 0)
<table class="list-table">
@foreach($role->users as $user)
<tr>
<td style="line-height: 0;"><img class="avatar small" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
<td>
@if(userCan('users-manage') || $currentUser->id == $user->id)
<a href="/settings/users/{{$user->id}}">
@endif
{{ $user->name }}
@if(userCan('users-manage') || $currentUser->id == $user->id)
</a>
@endif
</td>
</tr>
@endforeach
</table>
@else
<p class="text-muted">
No users currently in this role.
</p>
@endif
</div>
<a href="/settings/roles" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Role</button>
\ No newline at end of file
</div>
\ No newline at end of file
......
<?php
class SortTest extends TestCase
{
protected $book;
public function setUp()
{
parent::setUp();
$this->book = \BookStack\Book::first();
}
public function test_drafts_do_not_show_up()
{
$this->asAdmin();
$pageRepo = app('\BookStack\Repos\PageRepo');
$draft = $pageRepo->getDraftPage($this->book);
$this->visit($this->book->getUrl())
->see($draft->name)
->visit($this->book->getUrl() . '/sort')
->dontSee($draft->name);
}
}
\ No newline at end of file