Revamped some complex queries, added favourites to home
- Removed old view system and started use of new query classes instead. - Finished off RelationMultiModelQuery but found it was less efficient than x-many queries due to the amount of tables being scanned. Adding now for history but will delete as not used. - Updated recently viewed to use same query system as popular items rather than running and joining x-entities queries. - Added "Most Viewed Faviourites" listing to homepages.
This commit is contained in:
		
							parent
							
								
									3de02566bf
								
							
						
					
					
						commit
						d0ff79ea60
					
				| 
						 | 
					@ -42,7 +42,7 @@ class View extends Model
 | 
				
			||||||
            'user_id' => $user->id,
 | 
					            'user_id' => $user->id,
 | 
				
			||||||
        ], ['views' => 0]);
 | 
					        ], ['views' => 0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $view->save(['views' => $view->views + 1]);
 | 
					        $view->forceFill(['views' => $view->views + 1])->save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $view->views;
 | 
					        return $view->views;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,81 +0,0 @@
 | 
				
			||||||
<?php namespace BookStack\Actions;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use BookStack\Auth\Permissions\PermissionService;
 | 
					 | 
				
			||||||
use BookStack\Entities\Models\Entity;
 | 
					 | 
				
			||||||
use BookStack\Entities\EntityProvider;
 | 
					 | 
				
			||||||
use DB;
 | 
					 | 
				
			||||||
use Illuminate\Support\Collection;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ViewService
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    protected $view;
 | 
					 | 
				
			||||||
    protected $permissionService;
 | 
					 | 
				
			||||||
    protected $entityProvider;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * ViewService constructor.
 | 
					 | 
				
			||||||
     * @param View $view
 | 
					 | 
				
			||||||
     * @param PermissionService $permissionService
 | 
					 | 
				
			||||||
     * @param EntityProvider $entityProvider
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->view = $view;
 | 
					 | 
				
			||||||
        $this->permissionService = $permissionService;
 | 
					 | 
				
			||||||
        $this->entityProvider = $entityProvider;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get the entities with the most views.
 | 
					 | 
				
			||||||
     * @param int $count
 | 
					 | 
				
			||||||
     * @param int $page
 | 
					 | 
				
			||||||
     * @param string|array $filterModels
 | 
					 | 
				
			||||||
     * @param string $action - used for permission checking
 | 
					 | 
				
			||||||
     * @return Collection
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $skipCount = $count * $page;
 | 
					 | 
				
			||||||
        $query = $this->permissionService
 | 
					 | 
				
			||||||
            ->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
 | 
					 | 
				
			||||||
            ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
 | 
					 | 
				
			||||||
            ->groupBy('viewable_id', 'viewable_type')
 | 
					 | 
				
			||||||
            ->orderBy('view_count', 'desc');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($filterModels) {
 | 
					 | 
				
			||||||
            $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $query->with('viewable')
 | 
					 | 
				
			||||||
            ->skip($skipCount)
 | 
					 | 
				
			||||||
            ->take($count)
 | 
					 | 
				
			||||||
            ->get()
 | 
					 | 
				
			||||||
            ->pluck('viewable')
 | 
					 | 
				
			||||||
            ->filter();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get all recently viewed entities for the current user.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getUserRecentlyViewed(int $count = 10, int $page = 1)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $user = user();
 | 
					 | 
				
			||||||
        if ($user === null || $user->isDefault()) {
 | 
					 | 
				
			||||||
            return collect();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $all = collect();
 | 
					 | 
				
			||||||
        /** @var Entity $instance */
 | 
					 | 
				
			||||||
        foreach ($this->entityProvider->all() as $name => $instance) {
 | 
					 | 
				
			||||||
            $items = $instance::visible()->withLastView()
 | 
					 | 
				
			||||||
                ->having('last_viewed_at', '>', 0)
 | 
					 | 
				
			||||||
                ->orderBy('last_viewed_at', 'desc')
 | 
					 | 
				
			||||||
                ->skip($count * ($page - 1))
 | 
					 | 
				
			||||||
                ->take($count)
 | 
					 | 
				
			||||||
                ->get();
 | 
					 | 
				
			||||||
            $all = $all->concat($items);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $all->sortByDesc('last_viewed_at')->slice(0, $count);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -184,11 +184,9 @@ return [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Custom BookStack
 | 
					        // Custom BookStack
 | 
				
			||||||
        'Activity' => BookStack\Facades\Activity::class,
 | 
					        'Activity' => BookStack\Facades\Activity::class,
 | 
				
			||||||
        'Views'    => BookStack\Facades\Views::class,
 | 
					 | 
				
			||||||
        'Images'   => BookStack\Facades\Images::class,
 | 
					        'Images'   => BookStack\Facades\Images::class,
 | 
				
			||||||
        'Permissions' => BookStack\Facades\Permissions::class,
 | 
					        'Permissions' => BookStack\Facades\Permissions::class,
 | 
				
			||||||
        'Theme'    => BookStack\Facades\Theme::class,
 | 
					        'Theme'    => BookStack\Facades\Theme::class,
 | 
				
			||||||
 | 
					 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Proxy configuration
 | 
					    // Proxy configuration
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					<?php namespace BookStack\Entities\Queries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Auth\Permissions\PermissionService;
 | 
				
			||||||
 | 
					use BookStack\Entities\EntityProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class EntityQuery
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    protected function permissionService(): PermissionService
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return app()->make(PermissionService::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function entityProvider(): EntityProvider
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return app()->make(EntityProvider::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					<?php namespace BookStack\Entities\Queries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\View;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Popular extends EntityQuery
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $query = $this->permissionService()
 | 
				
			||||||
 | 
					            ->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
 | 
				
			||||||
 | 
					            ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
 | 
				
			||||||
 | 
					            ->groupBy('viewable_id', 'viewable_type')
 | 
				
			||||||
 | 
					            ->orderBy('view_count', 'desc');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($filterModels) {
 | 
				
			||||||
 | 
					            $query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $query->with('viewable')
 | 
				
			||||||
 | 
					            ->skip($count * ($page - 1))
 | 
				
			||||||
 | 
					            ->take($count)
 | 
				
			||||||
 | 
					            ->get()
 | 
				
			||||||
 | 
					            ->pluck('viewable')
 | 
				
			||||||
 | 
					            ->filter();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<?php namespace BookStack\Entities\Queries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\View;
 | 
				
			||||||
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecentlyViewed extends EntityQuery
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function run(int $count, int $page): Collection
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $user = user();
 | 
				
			||||||
 | 
					        if ($user === null || $user->isDefault()) {
 | 
				
			||||||
 | 
					            return collect();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $query = $this->permissionService()->filterRestrictedEntityRelations(
 | 
				
			||||||
 | 
					            View::query(),
 | 
				
			||||||
 | 
					            'views',
 | 
				
			||||||
 | 
					            'viewable_id',
 | 
				
			||||||
 | 
					            'viewable_type',
 | 
				
			||||||
 | 
					            'view'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					            ->orderBy('views.updated_at', 'desc')
 | 
				
			||||||
 | 
					            ->where('user_id', '=', user()->id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $query->with('viewable')
 | 
				
			||||||
 | 
					            ->skip(($page - 1) * $count)
 | 
				
			||||||
 | 
					            ->take($count)
 | 
				
			||||||
 | 
					            ->get()
 | 
				
			||||||
 | 
					            ->pluck('viewable')
 | 
				
			||||||
 | 
					            ->filter();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<?php namespace BookStack\Entities\Queries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\View;
 | 
				
			||||||
 | 
					use Illuminate\Database\Query\JoinClause;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TopFavourites extends EntityQuery
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function run(int $count, int $page)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $user = user();
 | 
				
			||||||
 | 
					        if ($user === null || $user->isDefault()) {
 | 
				
			||||||
 | 
					            return collect();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $query = $this->permissionService()
 | 
				
			||||||
 | 
					            ->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', 'view')
 | 
				
			||||||
 | 
					            ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
 | 
				
			||||||
 | 
					            ->groupBy('viewable_id', 'viewable_type')
 | 
				
			||||||
 | 
					            ->rightJoin('favourites', function (JoinClause $join) {
 | 
				
			||||||
 | 
					                $join->on('views.viewable_id', '=', 'favourites.favouritable_id');
 | 
				
			||||||
 | 
					                $join->on('views.viewable_type', '=', 'favourites.favouritable_type');
 | 
				
			||||||
 | 
					                $join->where('favourites.user_id', '=', user()->id);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            ->orderBy('view_count', 'desc');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $query->with('viewable')
 | 
				
			||||||
 | 
					            ->skip($count * ($page - 1))
 | 
				
			||||||
 | 
					            ->take($count)
 | 
				
			||||||
 | 
					            ->get()
 | 
				
			||||||
 | 
					            ->pluck('viewable')
 | 
				
			||||||
 | 
					            ->filter();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -18,28 +18,6 @@ use Illuminate\Support\Collection;
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class RelationMultiModelQuery
 | 
					class RelationMultiModelQuery
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // TODO - Hydrate results to models
 | 
					 | 
				
			||||||
    // TODO - Allow setting additional wheres and all-model columns (From the core relation - eg, last_viewed_at)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//select views.updated_at as last_viewed_at,
 | 
					 | 
				
			||||||
//b.id as book_id, b.name as book_name, b.slug as book_slug, b.description as book_description,
 | 
					 | 
				
			||||||
//s.id as bookshelf_id, s.name as bookshelf_name, s.slug as bookshelf_slug, s.description as bookshelf_description,
 | 
					 | 
				
			||||||
//c.id as chapter_id, c.name as chapter_name, c.slug as chapter_slug, c.description as chapter_description,
 | 
					 | 
				
			||||||
//p.id as page_id, p.name as page_name, p.slug as page_slug, p.text as page_description
 | 
					 | 
				
			||||||
//from views
 | 
					 | 
				
			||||||
//left join bookshelves s on (s.id = views.viewable_id and views.viewable_type = 'BookStack\\Bookshelf' and s.deleted_at is null)
 | 
					 | 
				
			||||||
//left join books b on (b.id = views.viewable_id and views.viewable_type = 'BookStack\\Book' and b.deleted_at is null)
 | 
					 | 
				
			||||||
//left join chapters c on (c.id = views.viewable_id and views.viewable_type = 'BookStack\\Chapter' and c.deleted_at is null)
 | 
					 | 
				
			||||||
//left join pages p on (p.id = views.viewable_id and views.viewable_type = 'BookStack\\Page' and p.deleted_at is null)
 | 
					 | 
				
			||||||
//#     Permissions
 | 
					 | 
				
			||||||
//where exists(
 | 
					 | 
				
			||||||
//select * from joint_permissions jp where jp.entity_id = views.viewable_id and jp.entity_type = views.viewable_type
 | 
					 | 
				
			||||||
//and jp.action = 'view' and jp.role_id in (1, 2, 3, 6, 12) and (jp.has_permission = 1 or (jp.has_permission_own = 1 and jp.owned_by = 1))
 | 
					 | 
				
			||||||
//)
 | 
					 | 
				
			||||||
//and (s.id is not null or b.id is not null or c.id is not null or p.id is not null)
 | 
					 | 
				
			||||||
//and views.user_id = 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** @var array<string, array> */
 | 
					    /** @var array<string, array> */
 | 
				
			||||||
    protected $lookupModels = [];
 | 
					    protected $lookupModels = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,9 +27,52 @@ class RelationMultiModelQuery
 | 
				
			||||||
    /** @var string */
 | 
					    /** @var string */
 | 
				
			||||||
    protected $polymorphicFieldName;
 | 
					    protected $polymorphicFieldName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct(Model $relation, string $polymorphicFieldName)
 | 
					    /**
 | 
				
			||||||
 | 
					     * The keys are relation fields to fetch.
 | 
				
			||||||
 | 
					     * The values are the name to use for the resulting model attribute.
 | 
				
			||||||
 | 
					     * @var array<string, string>
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $relationFields = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * An array of [string $col, string $operator, mixed $value] where conditions.
 | 
				
			||||||
 | 
					     * @var array<array>>
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $relationWheres = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Field on the relation field to order by.
 | 
				
			||||||
 | 
					     * @var ?array[string $column, string $direction]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $orderByRelationField = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Number of results to take
 | 
				
			||||||
 | 
					     * @var ?int
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $take = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Number of results to skip.
 | 
				
			||||||
 | 
					     * @var ?int
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $skip = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Callback that will receive the query for any advanced customization.
 | 
				
			||||||
 | 
					     * @var ?callable
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $queryCustomizer = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @throws \Exception
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function __construct(string $relation, string $polymorphicFieldName)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->relation = $relation;
 | 
					        $this->relation = (new $relation);
 | 
				
			||||||
 | 
					        if (!$this->relation instanceof Model) {
 | 
				
			||||||
 | 
					            throw new \Exception('Given relation must be a model instance class');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        $this->polymorphicFieldName = $polymorphicFieldName;
 | 
					        $this->polymorphicFieldName = $polymorphicFieldName;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,6 +97,78 @@ class RelationMultiModelQuery
 | 
				
			||||||
        return $this;
 | 
					        return $this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Bring back a field from the relation object with the model results.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function withRelationField(string $fieldName, string $modelAttributeName): self
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->relationFields[$fieldName] = $modelAttributeName;
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Add a where condition to the query for the main relation table.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function whereRelation(string $column, string $operator, $value): self
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->relationWheres[] = [$column, $operator, $value];
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Order by the given relation column.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function orderByRelation(string $column, string $direction = 'asc'): self
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->orderByRelationField = [$column, $direction];
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Skip the given $count of results in the query.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function skip(?int $count): self
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->skip = $count;
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Take the given $count of results in the query.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function take(?int $count): self
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->take = $count;
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Pass a callable, which will receive the base query
 | 
				
			||||||
 | 
					     * to perform additional custom operations on the query.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function customizeUsing(callable $customizer): self
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->queryCustomizer = $customizer;
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get the SQL from the core query being ran.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function toSql(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->build()->toSql();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the query and get the results.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function run(): Collection
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $results = $this->build()->get();
 | 
				
			||||||
 | 
					        return $this->hydrateModelsFromResults($results);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Build the core query to run.
 | 
					     * Build the core query to run.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					@ -85,6 +178,14 @@ class RelationMultiModelQuery
 | 
				
			||||||
        $relationTable = $this->relation->getTable();
 | 
					        $relationTable = $this->relation->getTable();
 | 
				
			||||||
        $modelTables = [];
 | 
					        $modelTables = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Load relation fields
 | 
				
			||||||
 | 
					        foreach ($this->relationFields as $relationField => $alias) {
 | 
				
			||||||
 | 
					            $query->addSelect(
 | 
				
			||||||
 | 
					                $relationTable . '.' . $relationField . ' as '
 | 
				
			||||||
 | 
					                . $relationTable . '@' . $relationField
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Load model selects & joins
 | 
					        // Load model selects & joins
 | 
				
			||||||
        foreach ($this->lookupModels as $lookupModel => $columns) {
 | 
					        foreach ($this->lookupModels as $lookupModel => $columns) {
 | 
				
			||||||
            /** @var Entity $model */
 | 
					            /** @var Entity $model */
 | 
				
			||||||
| 
						 | 
					@ -107,11 +208,34 @@ class RelationMultiModelQuery
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add relation wheres
 | 
				
			||||||
 | 
					        foreach ($this->relationWheres as [$column, $operator, $value]) {
 | 
				
			||||||
 | 
					            $query->where($relationTable . '.' . $column, $operator, $value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Skip and take
 | 
				
			||||||
 | 
					        if (!is_null($this->skip)) {
 | 
				
			||||||
 | 
					            $query->skip($this->skip);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!is_null($this->take)) {
 | 
				
			||||||
 | 
					            $query->take($this->take);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!is_null($this->queryCustomizer)) {
 | 
				
			||||||
 | 
					            $customizer = $this->queryCustomizer;
 | 
				
			||||||
 | 
					            $customizer($query);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!is_null($this->orderByRelationField)) {
 | 
				
			||||||
 | 
					            $query->orderBy($relationTable . '.' . $this->orderByRelationField[0], $this->orderByRelationField[1]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->applyPermissionsToQuery($query, 'view');
 | 
					        $this->applyPermissionsToQuery($query, 'view');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $query;
 | 
					        return $query;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the query through the permission system.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    protected function applyPermissionsToQuery(Builder $query, string $action)
 | 
					    protected function applyPermissionsToQuery(Builder $query, string $action)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $permissions = app()->make(PermissionService::class);
 | 
					        $permissions = app()->make(PermissionService::class);
 | 
				
			||||||
| 
						 | 
					@ -131,24 +255,54 @@ class RelationMultiModelQuery
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $selectArray = [];
 | 
					        $selectArray = [];
 | 
				
			||||||
        foreach ($columns as $column) {
 | 
					        foreach ($columns as $column) {
 | 
				
			||||||
            $selectArray[] = $table . '.' . $column . ' as '.  $table . '_' . $column;
 | 
					            $selectArray[] = $table . '.' . $column . ' as ' . $table . '@' . $column;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return $selectArray;
 | 
					        return $selectArray;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get the SQL from the core query being ran.
 | 
					     * Hydrate a collection of result data into models.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function toSql(): string
 | 
					    protected function hydrateModelsFromResults(Collection $results): Collection
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->build()->toSql();
 | 
					        $modelByIdColumn = [];
 | 
				
			||||||
 | 
					        foreach ($this->lookupModels as $lookupModel => $columns) {
 | 
				
			||||||
 | 
					            /** @var Model $model */
 | 
				
			||||||
 | 
					            $model = new $lookupModel;
 | 
				
			||||||
 | 
					            $modelByIdColumn[$model->getTable() . '@id'] = $model;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $results->map(function ($result) use ($modelByIdColumn) {
 | 
				
			||||||
 | 
					            foreach ($modelByIdColumn as $idColumn => $modelInstance) {
 | 
				
			||||||
 | 
					                if (isset($result->$idColumn)) {
 | 
				
			||||||
 | 
					                    return $this->hydrateModelFromResult($modelInstance, $result);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Run the query and get the results.
 | 
					     * Hydrate the given model type with the database result.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function run(): Collection
 | 
					    protected function hydrateModelFromResult(Model $model, \stdClass $result): Model
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->build()->get();
 | 
					        $modelPrefix = $model->getTable() . '@';
 | 
				
			||||||
 | 
					        $relationPrefix = $this->relation->getTable() . '@';
 | 
				
			||||||
 | 
					        $attrs = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ((array) $result as $col => $value) {
 | 
				
			||||||
 | 
					            if (strpos($col, $modelPrefix) === 0) {
 | 
				
			||||||
 | 
					                $attrName = substr($col, strlen($modelPrefix));
 | 
				
			||||||
 | 
					                $attrs[$attrName] = $value;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (strpos($col, $relationPrefix) === 0) {
 | 
				
			||||||
 | 
					                $col = substr($col, strlen($relationPrefix));
 | 
				
			||||||
 | 
					                $attrName = $this->relationFields[$col];
 | 
				
			||||||
 | 
					                $attrs[$attrName] = $value;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $model->newInstance()->forceFill($attrs);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
<?php namespace BookStack\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';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -2,11 +2,12 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Activity;
 | 
					use Activity;
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
 | 
					use BookStack\Entities\Queries\RecentlyViewed;
 | 
				
			||||||
 | 
					use BookStack\Entities\Queries\TopFavourites;
 | 
				
			||||||
use BookStack\Entities\Tools\PageContent;
 | 
					use BookStack\Entities\Tools\PageContent;
 | 
				
			||||||
use BookStack\Entities\Models\Page;
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
use BookStack\Entities\Repos\BookRepo;
 | 
					use BookStack\Entities\Repos\BookRepo;
 | 
				
			||||||
use BookStack\Entities\Repos\BookshelfRepo;
 | 
					use BookStack\Entities\Repos\BookshelfRepo;
 | 
				
			||||||
use Illuminate\Http\Response;
 | 
					 | 
				
			||||||
use Views;
 | 
					use Views;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HomeController extends Controller
 | 
					class HomeController extends Controller
 | 
				
			||||||
| 
						 | 
					@ -32,12 +33,13 @@ class HomeController extends Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
 | 
					        $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
 | 
				
			||||||
        $recents = $this->isSignedIn() ?
 | 
					        $recents = $this->isSignedIn() ?
 | 
				
			||||||
              Views::getUserRecentlyViewed(12*$recentFactor, 1)
 | 
					            (new RecentlyViewed)->run(12*$recentFactor, 1)
 | 
				
			||||||
            : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
 | 
					            : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
 | 
				
			||||||
 | 
					        $faviourites = (new TopFavourites)->run(6, 1);
 | 
				
			||||||
        $recentlyUpdatedPages = Page::visible()->with('book')
 | 
					        $recentlyUpdatedPages = Page::visible()->with('book')
 | 
				
			||||||
            ->where('draft', false)
 | 
					            ->where('draft', false)
 | 
				
			||||||
            ->orderBy('updated_at', 'desc')
 | 
					            ->orderBy('updated_at', 'desc')
 | 
				
			||||||
            ->take(12)
 | 
					            ->take($faviourites->count() > 0 ? 6 : 12)
 | 
				
			||||||
            ->get();
 | 
					            ->get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
 | 
					        $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
 | 
				
			||||||
| 
						 | 
					@ -51,6 +53,7 @@ class HomeController extends Controller
 | 
				
			||||||
            'recents' => $recents,
 | 
					            'recents' => $recents,
 | 
				
			||||||
            'recentlyUpdatedPages' => $recentlyUpdatedPages,
 | 
					            'recentlyUpdatedPages' => $recentlyUpdatedPages,
 | 
				
			||||||
            'draftPages' => $draftPages,
 | 
					            'draftPages' => $draftPages,
 | 
				
			||||||
 | 
					            'favourites' => $faviourites,
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Add required list ordering & sorting for books & shelves views.
 | 
					        // Add required list ordering & sorting for books & shelves views.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
<?php namespace BookStack\Http\Controllers;
 | 
					<?php namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Actions\ViewService;
 | 
					use BookStack\Actions\ViewService;
 | 
				
			||||||
 | 
					use BookStack\Entities\Queries\Popular;
 | 
				
			||||||
use BookStack\Entities\Tools\SearchRunner;
 | 
					use BookStack\Entities\Tools\SearchRunner;
 | 
				
			||||||
use BookStack\Entities\Tools\ShelfContext;
 | 
					use BookStack\Entities\Tools\ShelfContext;
 | 
				
			||||||
use BookStack\Entities\Tools\SearchOptions;
 | 
					use BookStack\Entities\Tools\SearchOptions;
 | 
				
			||||||
| 
						 | 
					@ -82,7 +83,7 @@ class SearchController extends Controller
 | 
				
			||||||
            $searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
 | 
					            $searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
 | 
				
			||||||
            $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
 | 
					            $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            $entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
 | 
					            $entities = (new Popular)->run(20, 0, $entityTypes, $permission);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return view('search.entity-ajax-list', ['entities' => $entities]);
 | 
					        return view('search.entity-ajax-list', ['entities' => $entities]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,7 @@ return [
 | 
				
			||||||
    'images' => 'Images',
 | 
					    'images' => 'Images',
 | 
				
			||||||
    'my_recent_drafts' => 'My Recent Drafts',
 | 
					    'my_recent_drafts' => 'My Recent Drafts',
 | 
				
			||||||
    'my_recently_viewed' => 'My Recently Viewed',
 | 
					    'my_recently_viewed' => 'My Recently Viewed',
 | 
				
			||||||
 | 
					    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
 | 
				
			||||||
    'no_pages_viewed' => 'You have not viewed any pages',
 | 
					    'no_pages_viewed' => 'You have not viewed any pages',
 | 
				
			||||||
    'no_pages_recently_created' => 'No pages have been recently created',
 | 
					    'no_pages_recently_created' => 'No pages have been recently created',
 | 
				
			||||||
    'no_pages_recently_updated' => 'No pages have been recently updated',
 | 
					    'no_pages_recently_updated' => 'No pages have been recently updated',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,18 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@endif
 | 
					@endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@if(count($favourites) > 0)
 | 
				
			||||||
 | 
					    <div id="top-favourites" class="card mb-xl">
 | 
				
			||||||
 | 
					        <h3 class="card-title">{{ trans('entities.my_most_viewed_favourites') }}</h3>
 | 
				
			||||||
 | 
					        <div class="px-m">
 | 
				
			||||||
 | 
					            @include('partials.entity-list', [
 | 
				
			||||||
 | 
					            'entities' => $favourites,
 | 
				
			||||||
 | 
					            'style' => 'compact',
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					@endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="mb-xl">
 | 
					<div class="mb-xl">
 | 
				
			||||||
    <h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
 | 
					    <h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
 | 
				
			||||||
    @include('partials.entity-list', [
 | 
					    @include('partials.entity-list', [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,18 @@
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
 | 
					                @if(count($favourites) > 0)
 | 
				
			||||||
 | 
					                    <div id="top-favourites" class="card mb-xl">
 | 
				
			||||||
 | 
					                        <h3 class="card-title">{{ trans('entities.my_most_viewed_favourites') }}</h3>
 | 
				
			||||||
 | 
					                        <div class="px-m">
 | 
				
			||||||
 | 
					                            @include('partials.entity-list', [
 | 
				
			||||||
 | 
					                            'entities' => $favourites,
 | 
				
			||||||
 | 
					                            'style' => 'compact',
 | 
				
			||||||
 | 
					                            ])
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div id="recent-pages" class="card mb-xl">
 | 
					                <div id="recent-pages" class="card mb-xl">
 | 
				
			||||||
                    <h3 class="card-title"><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
 | 
					                    <h3 class="card-title"><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
 | 
				
			||||||
                    <div id="recently-updated-pages" class="px-m">
 | 
					                    <div id="recently-updated-pages" class="px-m">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@
 | 
				
			||||||
                <div class="card mb-xl">
 | 
					                <div class="card mb-xl">
 | 
				
			||||||
                    <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
 | 
					                    <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
 | 
				
			||||||
                    <div class="px-m">
 | 
					                    <div class="px-m">
 | 
				
			||||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['page']), 'style' => 'compact'])
 | 
					                        @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@
 | 
				
			||||||
                <div class="card mb-xl">
 | 
					                <div class="card mb-xl">
 | 
				
			||||||
                    <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
 | 
					                    <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
 | 
				
			||||||
                    <div class="px-m">
 | 
					                    <div class="px-m">
 | 
				
			||||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['book']), 'style' => 'compact'])
 | 
					                        @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,7 @@
 | 
				
			||||||
                <div class="card mb-xl">
 | 
					                <div class="card mb-xl">
 | 
				
			||||||
                    <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
 | 
					                    <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
 | 
				
			||||||
                    <div class="px-m">
 | 
					                    <div class="px-m">
 | 
				
			||||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['chapter']), 'style' => 'compact'])
 | 
					                        @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue