mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-22 20:30:18 +08:00
Added view count tracking with personalised lists
This commit is contained in:
parent
76eb8fc5d7
commit
ea55b7f141
41
app/Console/Commands/ResetViews.php
Normal file
41
app/Console/Commands/ResetViews.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\Inspire::class,
|
||||
\BookStack\Console\Commands\ResetViews::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -17,7 +17,7 @@ class ActivityService
|
|||
public function __construct(Activity $activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->user = Auth::user();
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
13
app/Services/Facades/Views.php
Normal file
13
app/Services/Facades/Views.php
Normal 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'; }
|
||||
}
|
77
app/Services/ViewService.php
Normal file
77
app/Services/ViewService.php
Normal 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
20
app/View.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -214,6 +214,7 @@ return [
|
|||
|
||||
'Activity' => BookStack\Services\Facades\Activity::class,
|
||||
'Setting' => BookStack\Services\Facades\Setting::class,
|
||||
'Views' => BookStack\Services\Facades\Views::class,
|
||||
|
||||
],
|
||||
|
||||
|
|
34
database/migrations/2015_11_21_145609_create_views_table.php
Normal file
34
database/migrations/2015_11_21_145609_create_views_table.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
17
resources/views/partials/entity-list.blade.php
Normal file
17
resources/views/partials/entity-list.blade.php
Normal 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
|
Loading…
Reference in New Issue
Block a user