diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index 8690dfe..98c22ce 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -12,6 +12,7 @@ use Filament\Forms\Get; use Filament\Pages\Dashboard as BaseDashboard; use Filament\Pages\Dashboard\Concerns\HasFiltersForm; +use Illuminate\Contracts\View\View; class Dashboard extends BaseDashboard { @@ -22,6 +23,11 @@ public function getColumns(): int|string|array return 2; } + public function getFooter(): ?View + { + return view('filament.dashboard.footer'); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Widgets/TopViews.php b/app/Filament/Widgets/TopViews.php index 0520e44..ca883a6 100644 --- a/app/Filament/Widgets/TopViews.php +++ b/app/Filament/Widgets/TopViews.php @@ -82,7 +82,7 @@ public function table(Table $table): Table ->emptyStateDescription(null) ->emptyStateActions([ Action::make('create') - ->label('Create Presentation') + ->label('New Presentation') ->url(route('filament.admin.resources.presentations.create')) ->icon('heroicon-m-plus') ->button(), diff --git a/app/Http/Controllers/PresentationController.php b/app/Http/Controllers/PresentationController.php index f1d6241..e6f68a5 100644 --- a/app/Http/Controllers/PresentationController.php +++ b/app/Http/Controllers/PresentationController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use App\Models\Presentation; use App\Models\User; use Inertia\Inertia; use Inertia\Response; @@ -16,13 +15,15 @@ public function show(User $user, string $slug): Response ->where('slug', $slug) ->firstOrFail(); - if (! $this->canViewPresentation($presentation)) { + if (! $presentation->canBeViewed) { abort(403); } - dispatch(function () use ($presentation) { - $presentation->addDailyView(); - })->afterResponse(); + if ($presentation->shouldTrackView) { + dispatch(function () use ($presentation) { + $presentation->addDailyView(); + })->afterResponse(); + } return Inertia::render('Slides', [ 'content' => $presentation->content, @@ -33,21 +34,4 @@ public function show(User $user, string $slug): Response ], ]); } - - private function canViewPresentation(Presentation $presentation): bool - { - // If the presentation is published, then anyone can see it. - if ($presentation->is_published) { - return true; - } - - // If the user is not logged in, then they can't see any draft - // presentations. - if (! auth()->check()) { - return false; - } - - // Default to the normal view policy function - return auth()->user()->can('view', $presentation); - } } diff --git a/app/Models/Presentation.php b/app/Models/Presentation.php index 63690b1..032ec5c 100644 --- a/app/Models/Presentation.php +++ b/app/Models/Presentation.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -30,6 +31,73 @@ class Presentation extends Model implements HasMedia 'deleted_at', ]; + /** + * Determine if this presentation be viewed, based on published status and + * the authenticated user. + * + * @return Attribute + */ + protected function canBeViewed(): Attribute + { + return Attribute::make( + get: function (mixed $value, array $attributes): bool { + // If the presentation is published, then anyone can see it. + if ($this->is_published) { + return true; + } + + // If the user is not logged in, then they can't see any draft + // presentations. + if (! auth()->check()) { + return false; + } + + // Default to the normal view policy function + return auth()->user()->can('view', $this); + }, + ); + } + + /** + * Determine if this presentation should track a daily view, based on + * published status and the authenticated user. + * + * @return Attribute + */ + protected function shouldTrackView(): Attribute + { + return Attribute::make( + get: function (mixed $value, array $attributes): bool { + // If the presentation is not published, then a view should not + // be tracked. + if (! $this->is_published) { + return false; + } + + // If the user is not logged in, then a view should be tracked. + if (! auth()->check()) { + return true; + } + + // If the user is an admin, then a view should not be tracked. + if (auth()->user()->isAdministrator()) { + return false; + } + + // If the user is the creator of the presentation, then a view + // should not be tracked. + if (auth()->id() === $this->user_id) { + return false; + } + + // Otherwise, a user would be logged in, but not as an admin or + // the creator of the presentation, and thus should track a + // daily view. This would be a pretty rare case. + return true; + }, + ); + } + /** * Scope a query to only include presentations for the authenticated user. * diff --git a/resources/views/filament/dashboard/footer.blade.php b/resources/views/filament/dashboard/footer.blade.php new file mode 100644 index 0000000..1b4c7a0 --- /dev/null +++ b/resources/views/filament/dashboard/footer.blade.php @@ -0,0 +1,7 @@ + +

+ Note: Views are only tracked on published presentations. + Additionally, if you are logged in and view one of your own + presentations, a view will not be tracked. +

+
diff --git a/tests/Feature/PresentationControllerTest.php b/tests/Feature/PresentationControllerTest.php index 1f68be4..d05cdbb 100644 --- a/tests/Feature/PresentationControllerTest.php +++ b/tests/Feature/PresentationControllerTest.php @@ -7,96 +7,153 @@ beforeEach(function () { $this->user = User::factory()->create(); - $this->guest = User::factory()->create(); - - $this->publishedPresentation = Presentation::factory()->create([ - 'is_published' => true, - 'user_id' => $this->user->id, - ]); - - $this->draftPresentation = Presentation::factory()->create([ - 'is_published' => false, - 'user_id' => $this->user->id, - ]); }); -test('published presentation show screen can be rendered for unauthenticated user', function () { - $response = $this->get(route('presentations.show', [ - 'user' => $this->user->username, - 'slug' => $this->publishedPresentation->slug, - ])); - - $response->assertStatus(200); -}); +describe('published presentation', function () { + beforeEach(function () { + $this->publishedPresentation = Presentation::factory()->create([ + 'is_published' => true, + 'user_id' => $this->user->id, + ]); + }); -test('published presentation show action generates daily view', function () { - $response = $this->get(route('presentations.show', [ - 'user' => $this->user->username, - 'slug' => $this->publishedPresentation->slug, - ])); + test('show screen can be rendered for unauthenticated user', function () { + $response = $this->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->publishedPresentation->slug, + ])); - $this->assertDatabaseHas(DailyView::class, [ - 'presentation_id' => $this->publishedPresentation->id, - ]); -}); + $response->assertStatus(200); + }); -test('published presentation show screen returns the right view and data', function () { - $response = $this->get(route('presentations.show', [ - 'user' => $this->user->username, - 'slug' => $this->publishedPresentation->slug, - ])); + test('show action generates daily view for unauthenticated user', function () { + $response = $this->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->publishedPresentation->slug, + ])); - $response->assertInertia(fn (Assert $page) => $page - ->component('Slides') - ->where('content', $this->publishedPresentation->content) - ->has('meta', fn (Assert $page) => $page - ->where('title', $this->publishedPresentation->title) - ->where('description', $this->publishedPresentation->description) - ->where('imageUrl', $this->publishedPresentation->getFirstMediaUrl('thumbnail')) - ) - ); -}); + $this->assertDatabaseHas(DailyView::class, [ + 'presentation_id' => $this->publishedPresentation->id, + ]); + }); + + test('show action does not generate daily view for admin user', function () { + $adminUser = User::factory()->create(['is_admin' => true]); + + $response = $this + ->actingAs($adminUser) + ->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->publishedPresentation->slug, + ])); + + $this->assertDatabaseMissing(DailyView::class, [ + 'presentation_id' => $this->publishedPresentation->id, + ]); + }); + + test('show action does not generate daily view for creating user', function () { + $response = $this + ->actingAs($this->user) + ->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->publishedPresentation->slug, + ])); + + $this->assertDatabaseMissing(DailyView::class, [ + 'presentation_id' => $this->publishedPresentation->id, + ]); + }); + + test('show action does generate daily view for non-creating user', function () { + $response = $this + ->actingAs(User::factory()->create()) + ->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->publishedPresentation->slug, + ])); + + $this->assertDatabaseHas(DailyView::class, [ + 'presentation_id' => $this->publishedPresentation->id, + ]); + }); + + test('show screen returns the right view and data', function () { + $response = $this->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->publishedPresentation->slug, + ])); -test('draft presentation show screen is not rendered for unauthenticated user', function () { - $response = $this->get(route('presentations.show', [ - 'user' => $this->user->username, - 'slug' => $this->draftPresentation->slug, - ])); + $response->assertInertia(fn (Assert $page) => $page + ->component('Slides') + ->where('content', $this->publishedPresentation->content) + ->has('meta', fn (Assert $page) => $page + ->where('title', $this->publishedPresentation->title) + ->where('description', $this->publishedPresentation->description) + ->where('imageUrl', $this->publishedPresentation->getFirstMediaUrl('thumbnail')) + ) + ); + }); + + test('non-existing username for user shows 404', function () { + $response = $this->get(route('presentations.show', [ + 'user' => 'foo', + 'slug' => $this->publishedPresentation->slug, + ])); - $response->assertStatus(403); + $response->assertStatus(404); + }); }); -test('draft presentation show action does not generate daily view', function () { - $response = $this->get(route('presentations.show', [ - 'user' => $this->user->username, - 'slug' => $this->draftPresentation->slug, - ])); - - $this->assertDatabaseMissing(DailyView::class, [ - 'presentation_id' => $this->draftPresentation->id, - ]); -}); +describe('draft presentation', function () { + beforeEach(function () { + $this->draftPresentation = Presentation::factory()->create([ + 'is_published' => false, + 'user_id' => $this->user->id, + ]); + }); -test('draft presentation show screen can be rendered for author', function () { - $response = $this - ->actingAs($this->user) - ->get(route('presentations.show', [ + test('show screen is not rendered for unauthenticated user', function () { + $response = $this->get(route('presentations.show', [ 'user' => $this->user->username, 'slug' => $this->draftPresentation->slug, ])); - $response->assertStatus(200); -}); + $response->assertStatus(403); + }); -test('draft presentation show screen is not rendered for non-author', function () { - $response = $this - ->actingAs($this->guest) - ->get(route('presentations.show', [ + test('show action does not generate daily view', function () { + $response = $this->get(route('presentations.show', [ 'user' => $this->user->username, 'slug' => $this->draftPresentation->slug, ])); - $response->assertStatus(403); + $this->assertDatabaseMissing(DailyView::class, [ + 'presentation_id' => $this->draftPresentation->id, + ]); + }); + + test('show screen can be rendered for author', function () { + $response = $this + ->actingAs($this->user) + ->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->draftPresentation->slug, + ])); + + $response->assertStatus(200); + }); + + test('show screen is not rendered for non-author', function () { + $response = $this + ->actingAs(User::factory()->create()) + ->get(route('presentations.show', [ + 'user' => $this->user->username, + 'slug' => $this->draftPresentation->slug, + ])); + + $response->assertStatus(403); + }); }); test('non-existing slug for presentation shows 404', function () { @@ -107,12 +164,3 @@ $response->assertStatus(404); }); - -test('non-existing username for user shows 404', function () { - $response = $this->get(route('presentations.show', [ - 'user' => 'foo', - 'slug' => $this->publishedPresentation->slug, - ])); - - $response->assertStatus(404); -});