diff --git a/app/Console/Commands/ResetViews.php b/app/Console/Commands/ResetViews.php new file mode 100644 index 000000000..3a3903ff8 --- /dev/null +++ b/app/Console/Commands/ResetViews.php @@ -0,0 +1,41 @@ +<?php + +namespace BookStack\Console\Commands; + +use Illuminate\Console\Command; + +class ResetViews extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'views:reset'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Reset all view-counts for all entities.'; + + /** + * Create a new command instance. + * + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + \Views::resetAll(); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index bfad4f762..e3a71bd14 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel */ protected $commands = [ \BookStack\Console\Commands\Inspire::class, + \BookStack\Console\Commands\ResetViews::class, ]; /** diff --git a/app/Entity.php b/app/Entity.php index 7aee4d3b0..1a8b02a4b 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; abstract class Entity extends Model { + /** * Relation for the user that created this entity. * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -36,7 +37,7 @@ abstract class Entity extends Model } /** - * Gets the activity for this entity. + * Gets the activity objects for this entity. * @return \Illuminate\Database\Eloquent\Relations\MorphMany */ public function activity() @@ -44,6 +45,24 @@ abstract class Entity extends Model return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc'); } + /** + * Get View objects for this entity. + * @return mixed + */ + public function views() + { + return $this->morphMany('BookStack\View', 'viewable'); + } + + /** + * Get just the views for the current user. + * @return mixed + */ + public function userViews() + { + return $this->views()->where('user_id', '=', auth()->user()->id); + } + /** * Allows checking of the exact class, Used to check entity type. * Cleaner method for is_a. diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index eb9bf7aba..ff172f3b2 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -11,6 +11,7 @@ use BookStack\Http\Requests; use BookStack\Repos\BookRepo; use BookStack\Repos\ChapterRepo; use BookStack\Repos\PageRepo; +use Views; class BookController extends Controller { @@ -41,7 +42,8 @@ class BookController extends Controller public function index() { $books = $this->bookRepo->getAllPaginated(10); - return view('books/index', ['books' => $books]); + $recents = $this->bookRepo->getRecentlyViewed(10, 0); + return view('books/index', ['books' => $books, 'recents' => $recents]); } /** @@ -86,6 +88,7 @@ class BookController extends Controller public function show($slug) { $book = $this->bookRepo->getBySlug($slug); + Views::add($book); return view('books/show', ['book' => $book, 'current' => $book]); } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index f7b4f6dab..a8076cdf2 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -10,6 +10,7 @@ use BookStack\Http\Requests; use BookStack\Http\Controllers\Controller; use BookStack\Repos\BookRepo; use BookStack\Repos\ChapterRepo; +use Views; class ChapterController extends Controller { @@ -79,6 +80,7 @@ class ChapterController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + Views::add($chapter); return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index d0eb16399..3b04a1f41 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,13 +2,12 @@ namespace BookStack\Http\Controllers; +use Activity; use Illuminate\Http\Request; use BookStack\Http\Requests; -use BookStack\Http\Controllers\Controller; use BookStack\Repos\BookRepo; -use BookStack\Services\ActivityService; -use BookStack\Services\Facades\Activity; +use Views; class HomeController extends Controller { @@ -18,12 +17,10 @@ class HomeController extends Controller /** * HomeController constructor. - * @param ActivityService $activityService * @param BookRepo $bookRepo */ - public function __construct(ActivityService $activityService, BookRepo $bookRepo) + public function __construct(BookRepo $bookRepo) { - $this->activityService = $activityService; $this->bookRepo = $bookRepo; parent::__construct(); } @@ -36,9 +33,9 @@ class HomeController extends Controller */ public function index() { - $books = $this->bookRepo->getAll(10); - $activity = $this->activityService->latest(); - return view('home', ['books' => $books, 'activity' => $activity]); + $activity = Activity::latest(); + $recentlyViewed = Views::getUserRecentlyViewed(10, 0); + return view('home', ['activity' => $activity, 'recents' => $recentlyViewed]); } } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 16a44e4c5..006d84f6e 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -10,6 +10,7 @@ use BookStack\Http\Requests; use BookStack\Repos\BookRepo; use BookStack\Repos\ChapterRepo; use BookStack\Repos\PageRepo; +use Views; class PageController extends Controller { @@ -86,6 +87,7 @@ class PageController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + Views::add($page); return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page]); } diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php index e99b236c4..bd4b2b515 100644 --- a/app/Providers/CustomFacadeProvider.php +++ b/app/Providers/CustomFacadeProvider.php @@ -2,6 +2,7 @@ namespace BookStack\Providers; +use BookStack\Services\ViewService; use Illuminate\Support\ServiceProvider; use BookStack\Services\ActivityService; use BookStack\Services\SettingService; @@ -29,6 +30,10 @@ class CustomFacadeProvider extends ServiceProvider return new ActivityService($this->app->make('BookStack\Activity')); }); + $this->app->bind('views', function() { + return new ViewService($this->app->make('BookStack\View')); + }); + $this->app->bind('setting', function() { return new SettingService( $this->app->make('BookStack\Setting'), diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 87309501e..55dbb86dc 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -1,7 +1,9 @@ <?php namespace BookStack\Repos; +use BookStack\Activity; use Illuminate\Support\Str; use BookStack\Book; +use Views; class BookRepo { @@ -20,18 +22,28 @@ class BookRepo $this->pageRepo = $pageRepo; } + /** + * Get the book that has the given id. + * @param $id + * @return mixed + */ public function getById($id) { return $this->book->findOrFail($id); } + /** + * Get all books, Limited by count. + * @param int $count + * @return mixed + */ public function getAll($count = 10) { return $this->book->orderBy('name', 'asc')->take($count)->get(); } /** - * Getas + * Get all books paginated. * @param int $count * @return mixed */ @@ -40,6 +52,16 @@ class BookRepo return $this->book->orderBy('name', 'asc')->paginate($count); } + public function getRecentlyViewed($count = 10, $page = 0) + { + return Views::getUserRecentlyViewed($count, $page, $this->book); + } + + /** + * Get a book by slug + * @param $slug + * @return mixed + */ public function getBySlug($slug) { return $this->book->where('slug', '=', $slug)->first(); @@ -65,11 +87,20 @@ class BookRepo return $this->book->fill($input); } + /** + * Count the amount of books that have a specific slug. + * @param $slug + * @return mixed + */ public function countBySlug($slug) { return $this->book->where('slug', '=', $slug)->count(); } + /** + * Destroy a book identified by the given slug. + * @param $bookSlug + */ public function destroyBySlug($bookSlug) { $book = $this->getBySlug($bookSlug); @@ -84,12 +115,22 @@ class BookRepo $book->delete(); } + /** + * Get the next child element priority. + * @param Book $book + * @return int + */ public function getNewPriority($book) { $lastElem = $book->children()->pop(); return $lastElem ? $lastElem->priority + 1 : 0; } + /** + * @param string $slug + * @param bool|false $currentId + * @return bool + */ public function doesSlugExist($slug, $currentId = false) { $query = $this->book->where('slug', '=', $slug); @@ -99,6 +140,13 @@ class BookRepo return $query->count() > 0; } + /** + * Provides a suitable slug for the given book name. + * Ensures the returned slug is unique in the system. + * @param string $name + * @param bool|false $currentId + * @return string + */ public function findSuitableSlug($name, $currentId = false) { $originalSlug = Str::slug($name); @@ -111,6 +159,11 @@ class BookRepo return $slug; } + /** + * Get books by search term. + * @param $term + * @return mixed + */ public function getBySearch($term) { $terms = explode(' ', preg_quote(trim($term))); diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index 5bfee81ab..4da928fad 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -17,7 +17,7 @@ class ActivityService public function __construct(Activity $activity) { $this->activity = $activity; - $this->user = Auth::user(); + $this->user = auth()->user(); } /** diff --git a/app/Services/Facades/Views.php b/app/Services/Facades/Views.php new file mode 100644 index 000000000..484ee8145 --- /dev/null +++ b/app/Services/Facades/Views.php @@ -0,0 +1,13 @@ +<?php namespace BookStack\Services\Facades; + +use Illuminate\Support\Facades\Facade; + +class Views extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() { return 'views'; } +} \ No newline at end of file diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php new file mode 100644 index 000000000..55e32fe1c --- /dev/null +++ b/app/Services/ViewService.php @@ -0,0 +1,77 @@ +<?php namespace BookStack\Services; + + +use BookStack\Entity; +use BookStack\View; + +class ViewService +{ + + protected $view; + protected $user; + + /** + * ViewService constructor. + * @param $view + */ + public function __construct(View $view) + { + $this->view = $view; + $this->user = auth()->user(); + } + + /** + * Add a view to the given entity. + * @param Entity $entity + * @return int + */ + public function add(Entity $entity) + { + $view = $entity->views()->where('user_id', '=', $this->user->id)->first(); + // Add view if model exists + if ($view) { + $view->increment('views'); + return $view->views; + } + + // Otherwise create new view count + $entity->views()->save($this->view->create([ + 'user_id' => $this->user->id, + 'views' => 1 + ])); + + return 1; + } + + /** + * Get all recently viewed entities for the current user. + * @param int $count + * @param int $page + * @param Entity|bool $filterModel + * @return mixed + */ + public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) + { + $skipCount = $count * $page; + $query = $this->view->where('user_id', '=', auth()->user()->id); + + if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); + + $views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get(); + $viewedEntities = $views->map(function ($item) { + return $item->viewable()->getResults(); + }); + return $viewedEntities; + } + + + /** + * Reset all view counts by deleting all views. + */ + public function resetAll() + { + $this->view->truncate(); + } + + +} \ No newline at end of file diff --git a/app/View.php b/app/View.php new file mode 100644 index 000000000..50dd06012 --- /dev/null +++ b/app/View.php @@ -0,0 +1,20 @@ +<?php + +namespace BookStack; + +use Illuminate\Database\Eloquent\Model; + +class View extends Model +{ + + protected $fillable = ['user_id', 'views']; + + /** + * Get all owning viewable models. + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function viewable() + { + return $this->morphTo(); + } +} diff --git a/config/app.php b/config/app.php index 44b3542a6..196a5c19b 100644 --- a/config/app.php +++ b/config/app.php @@ -214,6 +214,7 @@ return [ 'Activity' => BookStack\Services\Facades\Activity::class, 'Setting' => BookStack\Services\Facades\Setting::class, + 'Views' => BookStack\Services\Facades\Views::class, ], diff --git a/database/migrations/2015_11_21_145609_create_views_table.php b/database/migrations/2015_11_21_145609_create_views_table.php new file mode 100644 index 000000000..2baef7317 --- /dev/null +++ b/database/migrations/2015_11_21_145609_create_views_table.php @@ -0,0 +1,34 @@ +<?php + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class CreateViewsTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('views', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id'); + $table->integer('viewable_id'); + $table->string('viewable_type'); + $table->integer('views'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('views'); + } +} diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index 8163f5105..344835132 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -5,8 +5,8 @@ <div class="faded-small"> <div class="container"> <div class="row"> - <div class="col-md-6"></div> - <div class="col-md-6 faded"> + <div class="col-xs-1"></div> + <div class="col-xs-11 faded"> <div class="action-buttons"> @if($currentUser->can('book-create')) <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a> @@ -20,7 +20,7 @@ <div class="container"> <div class="row"> - <div class="col-md-8"> + <div class="col-sm-7"> <h1>Books</h1> @if(count($books) > 0) @foreach($books as $book) @@ -33,7 +33,11 @@ <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> @endif </div> - <div class="col-md-4"></div> + <div class="col-sm-4 col-sm-offset-1"> + <div class="margin-top large"> </div> + <h3>Recently Viewed</h3> + @include('partials/entity-list', ['entities' => $recents]) + </div> </div> </div> diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 2a97c4839..0ffc8b3bf 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -4,26 +4,18 @@ <div class="container"> <div class="row"> + <div class="col-md-7"> - <h2>Books</h2> - @if(count($books) > 0) - @foreach($books as $book) - @include('books/list-item', ['book' => $book]) - <hr> - @endforeach - @if(count($books) === 10) - <a href="/books">View all books »</a> - @endif - @else - <p class="text-muted">No books have been created.</p> - <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> - @endif + <h2>My Recently Viewed</h2> + @include('partials/entity-list', ['entities' => $recents]) </div> + <div class="col-md-4 col-md-offset-1"> <div class="margin-top large"> </div> <h3>Recent Activity</h3> @include('partials/activity-list', ['activity' => $activity]) </div> + </div> </div> diff --git a/resources/views/partials/entity-list.blade.php b/resources/views/partials/entity-list.blade.php new file mode 100644 index 000000000..a4b2bdfb6 --- /dev/null +++ b/resources/views/partials/entity-list.blade.php @@ -0,0 +1,17 @@ + +@if(count($entities) > 0) + @foreach($entities as $entity) + @if($entity->isA('page')) + @include('pages/list-item', ['page' => $entity]) + @elseif($entity->isA('book')) + @include('books/list-item', ['book' => $entity]) + @elseif($entity->isA('chapter')) + @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true]) + @endif + <hr> + @endforeach +@else + <p class="text-muted"> + No items available :( + </p> +@endif \ No newline at end of file