Dan Brown

Added view count tracking with personalised lists

1 +<?php
2 +
3 +namespace BookStack\Console\Commands;
4 +
5 +use Illuminate\Console\Command;
6 +
7 +class ResetViews extends Command
8 +{
9 + /**
10 + * The name and signature of the console command.
11 + *
12 + * @var string
13 + */
14 + protected $signature = 'views:reset';
15 +
16 + /**
17 + * The console command description.
18 + *
19 + * @var string
20 + */
21 + protected $description = 'Reset all view-counts for all entities.';
22 +
23 + /**
24 + * Create a new command instance.
25 + *
26 + */
27 + public function __construct()
28 + {
29 + parent::__construct();
30 + }
31 +
32 + /**
33 + * Execute the console command.
34 + *
35 + * @return mixed
36 + */
37 + public function handle()
38 + {
39 + \Views::resetAll();
40 + }
41 +}
...@@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel ...@@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel
14 */ 14 */
15 protected $commands = [ 15 protected $commands = [
16 \BookStack\Console\Commands\Inspire::class, 16 \BookStack\Console\Commands\Inspire::class,
17 + \BookStack\Console\Commands\ResetViews::class,
17 ]; 18 ];
18 19
19 /** 20 /**
......
...@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; ...@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model;
6 6
7 abstract class Entity extends Model 7 abstract class Entity extends Model
8 { 8 {
9 +
9 /** 10 /**
10 * Relation for the user that created this entity. 11 * Relation for the user that created this entity.
11 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 12 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
...@@ -36,7 +37,7 @@ abstract class Entity extends Model ...@@ -36,7 +37,7 @@ abstract class Entity extends Model
36 } 37 }
37 38
38 /** 39 /**
39 - * Gets the activity for this entity. 40 + * Gets the activity objects for this entity.
40 * @return \Illuminate\Database\Eloquent\Relations\MorphMany 41 * @return \Illuminate\Database\Eloquent\Relations\MorphMany
41 */ 42 */
42 public function activity() 43 public function activity()
...@@ -45,6 +46,24 @@ abstract class Entity extends Model ...@@ -45,6 +46,24 @@ abstract class Entity extends Model
45 } 46 }
46 47
47 /** 48 /**
49 + * Get View objects for this entity.
50 + * @return mixed
51 + */
52 + public function views()
53 + {
54 + return $this->morphMany('BookStack\View', 'viewable');
55 + }
56 +
57 + /**
58 + * Get just the views for the current user.
59 + * @return mixed
60 + */
61 + public function userViews()
62 + {
63 + return $this->views()->where('user_id', '=', auth()->user()->id);
64 + }
65 +
66 + /**
48 * Allows checking of the exact class, Used to check entity type. 67 * Allows checking of the exact class, Used to check entity type.
49 * Cleaner method for is_a. 68 * Cleaner method for is_a.
50 * @param $type 69 * @param $type
......
...@@ -11,6 +11,7 @@ use BookStack\Http\Requests; ...@@ -11,6 +11,7 @@ use BookStack\Http\Requests;
11 use BookStack\Repos\BookRepo; 11 use BookStack\Repos\BookRepo;
12 use BookStack\Repos\ChapterRepo; 12 use BookStack\Repos\ChapterRepo;
13 use BookStack\Repos\PageRepo; 13 use BookStack\Repos\PageRepo;
14 +use Views;
14 15
15 class BookController extends Controller 16 class BookController extends Controller
16 { 17 {
...@@ -41,7 +42,8 @@ class BookController extends Controller ...@@ -41,7 +42,8 @@ class BookController extends Controller
41 public function index() 42 public function index()
42 { 43 {
43 $books = $this->bookRepo->getAllPaginated(10); 44 $books = $this->bookRepo->getAllPaginated(10);
44 - return view('books/index', ['books' => $books]); 45 + $recents = $this->bookRepo->getRecentlyViewed(10, 0);
46 + return view('books/index', ['books' => $books, 'recents' => $recents]);
45 } 47 }
46 48
47 /** 49 /**
...@@ -86,6 +88,7 @@ class BookController extends Controller ...@@ -86,6 +88,7 @@ class BookController extends Controller
86 public function show($slug) 88 public function show($slug)
87 { 89 {
88 $book = $this->bookRepo->getBySlug($slug); 90 $book = $this->bookRepo->getBySlug($slug);
91 + Views::add($book);
89 return view('books/show', ['book' => $book, 'current' => $book]); 92 return view('books/show', ['book' => $book, 'current' => $book]);
90 } 93 }
91 94
......
...@@ -10,6 +10,7 @@ use BookStack\Http\Requests; ...@@ -10,6 +10,7 @@ use BookStack\Http\Requests;
10 use BookStack\Http\Controllers\Controller; 10 use BookStack\Http\Controllers\Controller;
11 use BookStack\Repos\BookRepo; 11 use BookStack\Repos\BookRepo;
12 use BookStack\Repos\ChapterRepo; 12 use BookStack\Repos\ChapterRepo;
13 +use Views;
13 14
14 class ChapterController extends Controller 15 class ChapterController extends Controller
15 { 16 {
...@@ -79,6 +80,7 @@ class ChapterController extends Controller ...@@ -79,6 +80,7 @@ class ChapterController extends Controller
79 { 80 {
80 $book = $this->bookRepo->getBySlug($bookSlug); 81 $book = $this->bookRepo->getBySlug($bookSlug);
81 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 82 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
83 + Views::add($chapter);
82 return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); 84 return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
83 } 85 }
84 86
......
...@@ -2,13 +2,12 @@ ...@@ -2,13 +2,12 @@
2 2
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 +use Activity;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 7
7 use BookStack\Http\Requests; 8 use BookStack\Http\Requests;
8 -use BookStack\Http\Controllers\Controller;
9 use BookStack\Repos\BookRepo; 9 use BookStack\Repos\BookRepo;
10 -use BookStack\Services\ActivityService; 10 +use Views;
11 -use BookStack\Services\Facades\Activity;
12 11
13 class HomeController extends Controller 12 class HomeController extends Controller
14 { 13 {
...@@ -18,12 +17,10 @@ class HomeController extends Controller ...@@ -18,12 +17,10 @@ class HomeController extends Controller
18 17
19 /** 18 /**
20 * HomeController constructor. 19 * HomeController constructor.
21 - * @param ActivityService $activityService
22 * @param BookRepo $bookRepo 20 * @param BookRepo $bookRepo
23 */ 21 */
24 - public function __construct(ActivityService $activityService, BookRepo $bookRepo) 22 + public function __construct(BookRepo $bookRepo)
25 { 23 {
26 - $this->activityService = $activityService;
27 $this->bookRepo = $bookRepo; 24 $this->bookRepo = $bookRepo;
28 parent::__construct(); 25 parent::__construct();
29 } 26 }
...@@ -36,9 +33,9 @@ class HomeController extends Controller ...@@ -36,9 +33,9 @@ class HomeController extends Controller
36 */ 33 */
37 public function index() 34 public function index()
38 { 35 {
39 - $books = $this->bookRepo->getAll(10); 36 + $activity = Activity::latest();
40 - $activity = $this->activityService->latest(); 37 + $recentlyViewed = Views::getUserRecentlyViewed(10, 0);
41 - return view('home', ['books' => $books, 'activity' => $activity]); 38 + return view('home', ['activity' => $activity, 'recents' => $recentlyViewed]);
42 } 39 }
43 40
44 } 41 }
......
...@@ -10,6 +10,7 @@ use BookStack\Http\Requests; ...@@ -10,6 +10,7 @@ use BookStack\Http\Requests;
10 use BookStack\Repos\BookRepo; 10 use BookStack\Repos\BookRepo;
11 use BookStack\Repos\ChapterRepo; 11 use BookStack\Repos\ChapterRepo;
12 use BookStack\Repos\PageRepo; 12 use BookStack\Repos\PageRepo;
13 +use Views;
13 14
14 class PageController extends Controller 15 class PageController extends Controller
15 { 16 {
...@@ -86,6 +87,7 @@ class PageController extends Controller ...@@ -86,6 +87,7 @@ class PageController extends Controller
86 { 87 {
87 $book = $this->bookRepo->getBySlug($bookSlug); 88 $book = $this->bookRepo->getBySlug($bookSlug);
88 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 89 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
90 + Views::add($page);
89 return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page]); 91 return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page]);
90 } 92 }
91 93
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace BookStack\Providers; 3 namespace BookStack\Providers;
4 4
5 +use BookStack\Services\ViewService;
5 use Illuminate\Support\ServiceProvider; 6 use Illuminate\Support\ServiceProvider;
6 use BookStack\Services\ActivityService; 7 use BookStack\Services\ActivityService;
7 use BookStack\Services\SettingService; 8 use BookStack\Services\SettingService;
...@@ -29,6 +30,10 @@ class CustomFacadeProvider extends ServiceProvider ...@@ -29,6 +30,10 @@ class CustomFacadeProvider extends ServiceProvider
29 return new ActivityService($this->app->make('BookStack\Activity')); 30 return new ActivityService($this->app->make('BookStack\Activity'));
30 }); 31 });
31 32
33 + $this->app->bind('views', function() {
34 + return new ViewService($this->app->make('BookStack\View'));
35 + });
36 +
32 $this->app->bind('setting', function() { 37 $this->app->bind('setting', function() {
33 return new SettingService( 38 return new SettingService(
34 $this->app->make('BookStack\Setting'), 39 $this->app->make('BookStack\Setting'),
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 +use BookStack\Activity;
3 use Illuminate\Support\Str; 4 use Illuminate\Support\Str;
4 use BookStack\Book; 5 use BookStack\Book;
6 +use Views;
5 7
6 class BookRepo 8 class BookRepo
7 { 9 {
...@@ -20,18 +22,28 @@ class BookRepo ...@@ -20,18 +22,28 @@ class BookRepo
20 $this->pageRepo = $pageRepo; 22 $this->pageRepo = $pageRepo;
21 } 23 }
22 24
25 + /**
26 + * Get the book that has the given id.
27 + * @param $id
28 + * @return mixed
29 + */
23 public function getById($id) 30 public function getById($id)
24 { 31 {
25 return $this->book->findOrFail($id); 32 return $this->book->findOrFail($id);
26 } 33 }
27 34
35 + /**
36 + * Get all books, Limited by count.
37 + * @param int $count
38 + * @return mixed
39 + */
28 public function getAll($count = 10) 40 public function getAll($count = 10)
29 { 41 {
30 return $this->book->orderBy('name', 'asc')->take($count)->get(); 42 return $this->book->orderBy('name', 'asc')->take($count)->get();
31 } 43 }
32 44
33 /** 45 /**
34 - * Getas 46 + * Get all books paginated.
35 * @param int $count 47 * @param int $count
36 * @return mixed 48 * @return mixed
37 */ 49 */
...@@ -40,6 +52,16 @@ class BookRepo ...@@ -40,6 +52,16 @@ class BookRepo
40 return $this->book->orderBy('name', 'asc')->paginate($count); 52 return $this->book->orderBy('name', 'asc')->paginate($count);
41 } 53 }
42 54
55 + public function getRecentlyViewed($count = 10, $page = 0)
56 + {
57 + return Views::getUserRecentlyViewed($count, $page, $this->book);
58 + }
59 +
60 + /**
61 + * Get a book by slug
62 + * @param $slug
63 + * @return mixed
64 + */
43 public function getBySlug($slug) 65 public function getBySlug($slug)
44 { 66 {
45 return $this->book->where('slug', '=', $slug)->first(); 67 return $this->book->where('slug', '=', $slug)->first();
...@@ -65,11 +87,20 @@ class BookRepo ...@@ -65,11 +87,20 @@ class BookRepo
65 return $this->book->fill($input); 87 return $this->book->fill($input);
66 } 88 }
67 89
90 + /**
91 + * Count the amount of books that have a specific slug.
92 + * @param $slug
93 + * @return mixed
94 + */
68 public function countBySlug($slug) 95 public function countBySlug($slug)
69 { 96 {
70 return $this->book->where('slug', '=', $slug)->count(); 97 return $this->book->where('slug', '=', $slug)->count();
71 } 98 }
72 99
100 + /**
101 + * Destroy a book identified by the given slug.
102 + * @param $bookSlug
103 + */
73 public function destroyBySlug($bookSlug) 104 public function destroyBySlug($bookSlug)
74 { 105 {
75 $book = $this->getBySlug($bookSlug); 106 $book = $this->getBySlug($bookSlug);
...@@ -84,12 +115,22 @@ class BookRepo ...@@ -84,12 +115,22 @@ class BookRepo
84 $book->delete(); 115 $book->delete();
85 } 116 }
86 117
118 + /**
119 + * Get the next child element priority.
120 + * @param Book $book
121 + * @return int
122 + */
87 public function getNewPriority($book) 123 public function getNewPriority($book)
88 { 124 {
89 $lastElem = $book->children()->pop(); 125 $lastElem = $book->children()->pop();
90 return $lastElem ? $lastElem->priority + 1 : 0; 126 return $lastElem ? $lastElem->priority + 1 : 0;
91 } 127 }
92 128
129 + /**
130 + * @param string $slug
131 + * @param bool|false $currentId
132 + * @return bool
133 + */
93 public function doesSlugExist($slug, $currentId = false) 134 public function doesSlugExist($slug, $currentId = false)
94 { 135 {
95 $query = $this->book->where('slug', '=', $slug); 136 $query = $this->book->where('slug', '=', $slug);
...@@ -99,6 +140,13 @@ class BookRepo ...@@ -99,6 +140,13 @@ class BookRepo
99 return $query->count() > 0; 140 return $query->count() > 0;
100 } 141 }
101 142
143 + /**
144 + * Provides a suitable slug for the given book name.
145 + * Ensures the returned slug is unique in the system.
146 + * @param string $name
147 + * @param bool|false $currentId
148 + * @return string
149 + */
102 public function findSuitableSlug($name, $currentId = false) 150 public function findSuitableSlug($name, $currentId = false)
103 { 151 {
104 $originalSlug = Str::slug($name); 152 $originalSlug = Str::slug($name);
...@@ -111,6 +159,11 @@ class BookRepo ...@@ -111,6 +159,11 @@ class BookRepo
111 return $slug; 159 return $slug;
112 } 160 }
113 161
162 + /**
163 + * Get books by search term.
164 + * @param $term
165 + * @return mixed
166 + */
114 public function getBySearch($term) 167 public function getBySearch($term)
115 { 168 {
116 $terms = explode(' ', preg_quote(trim($term))); 169 $terms = explode(' ', preg_quote(trim($term)));
......
...@@ -17,7 +17,7 @@ class ActivityService ...@@ -17,7 +17,7 @@ class ActivityService
17 public function __construct(Activity $activity) 17 public function __construct(Activity $activity)
18 { 18 {
19 $this->activity = $activity; 19 $this->activity = $activity;
20 - $this->user = Auth::user(); 20 + $this->user = auth()->user();
21 } 21 }
22 22
23 /** 23 /**
......
1 +<?php namespace BookStack\Services\Facades;
2 +
3 +use Illuminate\Support\Facades\Facade;
4 +
5 +class Views extends Facade
6 +{
7 + /**
8 + * Get the registered name of the component.
9 + *
10 + * @return string
11 + */
12 + protected static function getFacadeAccessor() { return 'views'; }
13 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php namespace BookStack\Services;
2 +
3 +
4 +use BookStack\Entity;
5 +use BookStack\View;
6 +
7 +class ViewService
8 +{
9 +
10 + protected $view;
11 + protected $user;
12 +
13 + /**
14 + * ViewService constructor.
15 + * @param $view
16 + */
17 + public function __construct(View $view)
18 + {
19 + $this->view = $view;
20 + $this->user = auth()->user();
21 + }
22 +
23 + /**
24 + * Add a view to the given entity.
25 + * @param Entity $entity
26 + * @return int
27 + */
28 + public function add(Entity $entity)
29 + {
30 + $view = $entity->views()->where('user_id', '=', $this->user->id)->first();
31 + // Add view if model exists
32 + if ($view) {
33 + $view->increment('views');
34 + return $view->views;
35 + }
36 +
37 + // Otherwise create new view count
38 + $entity->views()->save($this->view->create([
39 + 'user_id' => $this->user->id,
40 + 'views' => 1
41 + ]));
42 +
43 + return 1;
44 + }
45 +
46 + /**
47 + * Get all recently viewed entities for the current user.
48 + * @param int $count
49 + * @param int $page
50 + * @param Entity|bool $filterModel
51 + * @return mixed
52 + */
53 + public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
54 + {
55 + $skipCount = $count * $page;
56 + $query = $this->view->where('user_id', '=', auth()->user()->id);
57 +
58 + if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
59 +
60 + $views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
61 + $viewedEntities = $views->map(function ($item) {
62 + return $item->viewable()->getResults();
63 + });
64 + return $viewedEntities;
65 + }
66 +
67 +
68 + /**
69 + * Reset all view counts by deleting all views.
70 + */
71 + public function resetAll()
72 + {
73 + $this->view->truncate();
74 + }
75 +
76 +
77 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php
2 +
3 +namespace BookStack;
4 +
5 +use Illuminate\Database\Eloquent\Model;
6 +
7 +class View extends Model
8 +{
9 +
10 + protected $fillable = ['user_id', 'views'];
11 +
12 + /**
13 + * Get all owning viewable models.
14 + * @return \Illuminate\Database\Eloquent\Relations\MorphTo
15 + */
16 + public function viewable()
17 + {
18 + return $this->morphTo();
19 + }
20 +}
...@@ -214,6 +214,7 @@ return [ ...@@ -214,6 +214,7 @@ return [
214 214
215 'Activity' => BookStack\Services\Facades\Activity::class, 215 'Activity' => BookStack\Services\Facades\Activity::class,
216 'Setting' => BookStack\Services\Facades\Setting::class, 216 'Setting' => BookStack\Services\Facades\Setting::class,
217 + 'Views' => BookStack\Services\Facades\Views::class,
217 218
218 ], 219 ],
219 220
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class CreateViewsTable extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::create('views', function (Blueprint $table) {
16 + $table->increments('id');
17 + $table->integer('user_id');
18 + $table->integer('viewable_id');
19 + $table->string('viewable_type');
20 + $table->integer('views');
21 + $table->timestamps();
22 + });
23 + }
24 +
25 + /**
26 + * Reverse the migrations.
27 + *
28 + * @return void
29 + */
30 + public function down()
31 + {
32 + Schema::drop('views');
33 + }
34 +}
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
5 <div class="faded-small"> 5 <div class="faded-small">
6 <div class="container"> 6 <div class="container">
7 <div class="row"> 7 <div class="row">
8 - <div class="col-md-6"></div> 8 + <div class="col-xs-1"></div>
9 - <div class="col-md-6 faded"> 9 + <div class="col-xs-11 faded">
10 <div class="action-buttons"> 10 <div class="action-buttons">
11 @if($currentUser->can('book-create')) 11 @if($currentUser->can('book-create'))
12 <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a> 12 <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
20 20
21 <div class="container"> 21 <div class="container">
22 <div class="row"> 22 <div class="row">
23 - <div class="col-md-8"> 23 + <div class="col-sm-7">
24 <h1>Books</h1> 24 <h1>Books</h1>
25 @if(count($books) > 0) 25 @if(count($books) > 0)
26 @foreach($books as $book) 26 @foreach($books as $book)
...@@ -33,7 +33,11 @@ ...@@ -33,7 +33,11 @@
33 <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> 33 <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
34 @endif 34 @endif
35 </div> 35 </div>
36 - <div class="col-md-4"></div> 36 + <div class="col-sm-4 col-sm-offset-1">
37 + <div class="margin-top large">&nbsp;</div>
38 + <h3>Recently Viewed</h3>
39 + @include('partials/entity-list', ['entities' => $recents])
40 + </div>
37 </div> 41 </div>
38 </div> 42 </div>
39 43
......
...@@ -4,26 +4,18 @@ ...@@ -4,26 +4,18 @@
4 4
5 <div class="container"> 5 <div class="container">
6 <div class="row"> 6 <div class="row">
7 +
7 <div class="col-md-7"> 8 <div class="col-md-7">
8 - <h2>Books</h2> 9 + <h2>My Recently Viewed</h2>
9 - @if(count($books) > 0) 10 + @include('partials/entity-list', ['entities' => $recents])
10 - @foreach($books as $book)
11 - @include('books/list-item', ['book' => $book])
12 - <hr>
13 - @endforeach
14 - @if(count($books) === 10)
15 - <a href="/books">View all books &raquo;</a>
16 - @endif
17 - @else
18 - <p class="text-muted">No books have been created.</p>
19 - <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
20 - @endif
21 </div> 11 </div>
12 +
22 <div class="col-md-4 col-md-offset-1"> 13 <div class="col-md-4 col-md-offset-1">
23 <div class="margin-top large">&nbsp;</div> 14 <div class="margin-top large">&nbsp;</div>
24 <h3>Recent Activity</h3> 15 <h3>Recent Activity</h3>
25 @include('partials/activity-list', ['activity' => $activity]) 16 @include('partials/activity-list', ['activity' => $activity])
26 </div> 17 </div>
18 +
27 </div> 19 </div>
28 </div> 20 </div>
29 21
......
1 +
2 +@if(count($entities) > 0)
3 + @foreach($entities as $entity)
4 + @if($entity->isA('page'))
5 + @include('pages/list-item', ['page' => $entity])
6 + @elseif($entity->isA('book'))
7 + @include('books/list-item', ['book' => $entity])
8 + @elseif($entity->isA('chapter'))
9 + @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true])
10 + @endif
11 + <hr>
12 + @endforeach
13 +@else
14 + <p class="text-muted">
15 + No items available :(
16 + </p>
17 +@endif
...\ No newline at end of file ...\ No newline at end of file