--- /dev/null
+<?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();
+ }
+}
*/
protected $commands = [
\BookStack\Console\Commands\Inspire::class,
+ \BookStack\Console\Commands\ResetViews::class,
];
/**
abstract class Entity extends Model
{
+
/**
* Relation for the user that created this entity.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
}
/**
- * Gets the activity for this entity.
+ * Gets the activity objects for this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function activity()
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.
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
+use Views;
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]);
}
/**
public function show($slug)
{
$book = $this->bookRepo->getBySlug($slug);
+ Views::add($book);
return view('books/show', ['book' => $book, 'current' => $book]);
}
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
+use Views;
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]);
}
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
{
/**
* 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();
}
*/
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]);
}
}
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
+use Views;
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]);
}
namespace BookStack\Providers;
+use BookStack\Services\ViewService;
use Illuminate\Support\ServiceProvider;
use BookStack\Services\ActivityService;
use BookStack\Services\SettingService;
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'),
<?php namespace BookStack\Repos;
+use BookStack\Activity;
use Illuminate\Support\Str;
use BookStack\Book;
+use Views;
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
*/
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();
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);
$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);
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);
return $slug;
}
+ /**
+ * Get books by search term.
+ * @param $term
+ * @return mixed
+ */
public function getBySearch($term)
{
$terms = explode(' ', preg_quote(trim($term)));
public function __construct(Activity $activity)
{
$this->activity = $activity;
- $this->user = Auth::user();
+ $this->user = auth()->user();
}
/**
--- /dev/null
+<?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
--- /dev/null
+<?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
--- /dev/null
+<?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();
+ }
+}
'Activity' => BookStack\Services\Facades\Activity::class,
'Setting' => BookStack\Services\Facades\Setting::class,
+ 'Views' => BookStack\Services\Facades\Views::class,
],
--- /dev/null
+<?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');
+ }
+}
<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>
<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)
<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>
<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>
--- /dev/null
+
+@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