Added view count tracking with personalised lists

This commit is contained in:
Dan Brown 2015-11-21 17:22:14 +00:00
parent 76eb8fc5d7
commit ea55b7f141
18 changed files with 311 additions and 30 deletions

View File

@ -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();
}
}

View File

@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel
*/ */
protected $commands = [ protected $commands = [
\BookStack\Console\Commands\Inspire::class, \BookStack\Console\Commands\Inspire::class,
\BookStack\Console\Commands\ResetViews::class,
]; ];
/** /**

View File

@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model;
abstract class Entity extends Model abstract class Entity extends Model
{ {
/** /**
* Relation for the user that created this entity. * Relation for the user that created this entity.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @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 * @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/ */
public function activity() public function activity()
@ -44,6 +45,24 @@ abstract class Entity extends Model
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc'); 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. * Allows checking of the exact class, Used to check entity type.
* Cleaner method for is_a. * Cleaner method for is_a.

View File

@ -11,6 +11,7 @@ use BookStack\Http\Requests;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo; use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo; use BookStack\Repos\PageRepo;
use Views;
class BookController extends Controller class BookController extends Controller
{ {
@ -41,7 +42,8 @@ class BookController extends Controller
public function index() public function index()
{ {
$books = $this->bookRepo->getAllPaginated(10); $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) public function show($slug)
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->bookRepo->getBySlug($slug);
Views::add($book);
return view('books/show', ['book' => $book, 'current' => $book]); return view('books/show', ['book' => $book, 'current' => $book]);
} }

View File

@ -10,6 +10,7 @@ use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller; use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo; use BookStack\Repos\ChapterRepo;
use Views;
class ChapterController extends Controller class ChapterController extends Controller
{ {
@ -79,6 +80,7 @@ class ChapterController extends Controller
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
Views::add($chapter);
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
} }

View File

@ -2,13 +2,12 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use Activity;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests; use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Services\ActivityService; use Views;
use BookStack\Services\Facades\Activity;
class HomeController extends Controller class HomeController extends Controller
{ {
@ -18,12 +17,10 @@ class HomeController extends Controller
/** /**
* HomeController constructor. * HomeController constructor.
* @param ActivityService $activityService
* @param BookRepo $bookRepo * @param BookRepo $bookRepo
*/ */
public function __construct(ActivityService $activityService, BookRepo $bookRepo) public function __construct(BookRepo $bookRepo)
{ {
$this->activityService = $activityService;
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
parent::__construct(); parent::__construct();
} }
@ -36,9 +33,9 @@ class HomeController extends Controller
*/ */
public function index() public function index()
{ {
$books = $this->bookRepo->getAll(10); $activity = Activity::latest();
$activity = $this->activityService->latest(); $recentlyViewed = Views::getUserRecentlyViewed(10, 0);
return view('home', ['books' => $books, 'activity' => $activity]); return view('home', ['activity' => $activity, 'recents' => $recentlyViewed]);
} }
} }

View File

@ -10,6 +10,7 @@ use BookStack\Http\Requests;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo; use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo; use BookStack\Repos\PageRepo;
use Views;
class PageController extends Controller class PageController extends Controller
{ {
@ -86,6 +87,7 @@ class PageController extends Controller
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
Views::add($page);
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page]); return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page]);
} }

View File

@ -2,6 +2,7 @@
namespace BookStack\Providers; namespace BookStack\Providers;
use BookStack\Services\ViewService;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use BookStack\Services\ActivityService; use BookStack\Services\ActivityService;
use BookStack\Services\SettingService; use BookStack\Services\SettingService;
@ -29,6 +30,10 @@ class CustomFacadeProvider extends ServiceProvider
return new ActivityService($this->app->make('BookStack\Activity')); 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() { $this->app->bind('setting', function() {
return new SettingService( return new SettingService(
$this->app->make('BookStack\Setting'), $this->app->make('BookStack\Setting'),

View File

@ -1,7 +1,9 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Repos;
use BookStack\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Book; use BookStack\Book;
use Views;
class BookRepo class BookRepo
{ {
@ -20,18 +22,28 @@ class BookRepo
$this->pageRepo = $pageRepo; $this->pageRepo = $pageRepo;
} }
/**
* Get the book that has the given id.
* @param $id
* @return mixed
*/
public function getById($id) public function getById($id)
{ {
return $this->book->findOrFail($id); return $this->book->findOrFail($id);
} }
/**
* Get all books, Limited by count.
* @param int $count
* @return mixed
*/
public function getAll($count = 10) public function getAll($count = 10)
{ {
return $this->book->orderBy('name', 'asc')->take($count)->get(); return $this->book->orderBy('name', 'asc')->take($count)->get();
} }
/** /**
* Getas * Get all books paginated.
* @param int $count * @param int $count
* @return mixed * @return mixed
*/ */
@ -40,6 +52,16 @@ class BookRepo
return $this->book->orderBy('name', 'asc')->paginate($count); 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) public function getBySlug($slug)
{ {
return $this->book->where('slug', '=', $slug)->first(); return $this->book->where('slug', '=', $slug)->first();
@ -65,11 +87,20 @@ class BookRepo
return $this->book->fill($input); return $this->book->fill($input);
} }
/**
* Count the amount of books that have a specific slug.
* @param $slug
* @return mixed
*/
public function countBySlug($slug) public function countBySlug($slug)
{ {
return $this->book->where('slug', '=', $slug)->count(); return $this->book->where('slug', '=', $slug)->count();
} }
/**
* Destroy a book identified by the given slug.
* @param $bookSlug
*/
public function destroyBySlug($bookSlug) public function destroyBySlug($bookSlug)
{ {
$book = $this->getBySlug($bookSlug); $book = $this->getBySlug($bookSlug);
@ -84,12 +115,22 @@ class BookRepo
$book->delete(); $book->delete();
} }
/**
* Get the next child element priority.
* @param Book $book
* @return int
*/
public function getNewPriority($book) public function getNewPriority($book)
{ {
$lastElem = $book->children()->pop(); $lastElem = $book->children()->pop();
return $lastElem ? $lastElem->priority + 1 : 0; return $lastElem ? $lastElem->priority + 1 : 0;
} }
/**
* @param string $slug
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $currentId = false) public function doesSlugExist($slug, $currentId = false)
{ {
$query = $this->book->where('slug', '=', $slug); $query = $this->book->where('slug', '=', $slug);
@ -99,6 +140,13 @@ class BookRepo
return $query->count() > 0; 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) public function findSuitableSlug($name, $currentId = false)
{ {
$originalSlug = Str::slug($name); $originalSlug = Str::slug($name);
@ -111,6 +159,11 @@ class BookRepo
return $slug; return $slug;
} }
/**
* Get books by search term.
* @param $term
* @return mixed
*/
public function getBySearch($term) public function getBySearch($term)
{ {
$terms = explode(' ', preg_quote(trim($term))); $terms = explode(' ', preg_quote(trim($term)));

View File

@ -17,7 +17,7 @@ class ActivityService
public function __construct(Activity $activity) public function __construct(Activity $activity)
{ {
$this->activity = $activity; $this->activity = $activity;
$this->user = Auth::user(); $this->user = auth()->user();
} }
/** /**

View File

@ -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'; }
}

View File

@ -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();
}
}

20
app/View.php Normal file
View File

@ -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();
}
}

View File

@ -214,6 +214,7 @@ return [
'Activity' => BookStack\Services\Facades\Activity::class, 'Activity' => BookStack\Services\Facades\Activity::class,
'Setting' => BookStack\Services\Facades\Setting::class, 'Setting' => BookStack\Services\Facades\Setting::class,
'Views' => BookStack\Services\Facades\Views::class,
], ],

View File

@ -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');
}
}

View File

@ -5,8 +5,8 @@
<div class="faded-small"> <div class="faded-small">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6"></div> <div class="col-xs-1"></div>
<div class="col-md-6 faded"> <div class="col-xs-11 faded">
<div class="action-buttons"> <div class="action-buttons">
@if($currentUser->can('book-create')) @if($currentUser->can('book-create'))
<a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a> <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="container">
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-sm-7">
<h1>Books</h1> <h1>Books</h1>
@if(count($books) > 0) @if(count($books) > 0)
@foreach($books as $book) @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> <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
@endif @endif
</div> </div>
<div class="col-md-4"></div> <div class="col-sm-4 col-sm-offset-1">
<div class="margin-top large">&nbsp;</div>
<h3>Recently Viewed</h3>
@include('partials/entity-list', ['entities' => $recents])
</div>
</div> </div>
</div> </div>

View File

@ -4,26 +4,18 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-7"> <div class="col-md-7">
<h2>Books</h2> <h2>My Recently Viewed</h2>
@if(count($books) > 0) @include('partials/entity-list', ['entities' => $recents])
@foreach($books as $book)
@include('books/list-item', ['book' => $book])
<hr>
@endforeach
@if(count($books) === 10)
<a href="/books">View all books &raquo;</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
</div> </div>
<div class="col-md-4 col-md-offset-1"> <div class="col-md-4 col-md-offset-1">
<div class="margin-top large">&nbsp;</div> <div class="margin-top large">&nbsp;</div>
<h3>Recent Activity</h3> <h3>Recent Activity</h3>
@include('partials/activity-list', ['activity' => $activity]) @include('partials/activity-list', ['activity' => $activity])
</div> </div>
</div> </div>
</div> </div>

View File

@ -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