From b467b66ec1ca287250b99e7058ea1c273e4e35da Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:15:13 +0100 Subject: [PATCH 01/21] add Show model Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Models/Show.php | 50 +++++++++++++++++++ app/Models/User.php | 10 ++++ .../2023_12_20_221341_create_shows_table.php | 34 +++++++++++++ ...036_create_show_moderators_pivot_table.php | 30 +++++++++++ 4 files changed, 124 insertions(+) create mode 100644 app/Models/Show.php create mode 100644 database/migrations/2023_12_20_221341_create_shows_table.php create mode 100644 database/migrations/2023_12_20_223036_create_show_moderators_pivot_table.php diff --git a/app/Models/Show.php b/app/Models/Show.php new file mode 100644 index 00000000..b67d9aea --- /dev/null +++ b/app/Models/Show.php @@ -0,0 +1,50 @@ + + */ + protected $fillable = [ + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + ]; + + public function locked_by() + { + return $this->belongsTo(User::class, 'locked_by'); + } + + public function moderators() + { + return $this->belongsToMany(User::class, 'show_moderators', 'show_id', 'moderator_id')->as('moderators')->withTimestamps(); + } + + public function primary_moderator() + { + return $this->belongsToMany(User::class, 'show_moderators', 'show_id', 'moderator_id')->wherePivot('primary', true); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index db53c86f..c1987ac4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -45,4 +45,14 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; + + public function locked_shows() + { + return $this->hasMany(Show::class, 'locked_by'); + } + + // public function shows() + // { + // return $this->belongsToMany(Show::class, 'show_moderators', 'moderator_id', 'show_id')->withTimestamps(); + // } } diff --git a/database/migrations/2023_12_20_221341_create_shows_table.php b/database/migrations/2023_12_20_221341_create_shows_table.php new file mode 100644 index 00000000..d879d5cf --- /dev/null +++ b/database/migrations/2023_12_20_221341_create_shows_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('title'); + $table->text('body')->nullable(); + $table->dateTime('start_date')->index()->comment('The date the show starts'); + $table->dateTime('end_date')->index()->comment('The date the show ends'); + $table->boolean('is_live'); + $table->boolean('enabled'); + $table->unsignedBigInteger('locked_by')->nullable()->comment('The user who locked the show'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('shows'); + } +}; diff --git a/database/migrations/2023_12_20_223036_create_show_moderators_pivot_table.php b/database/migrations/2023_12_20_223036_create_show_moderators_pivot_table.php new file mode 100644 index 00000000..37d50cb0 --- /dev/null +++ b/database/migrations/2023_12_20_223036_create_show_moderators_pivot_table.php @@ -0,0 +1,30 @@ +unsignedBigInteger('show_id'); + $table->unsignedBigInteger('moderator_id'); + $table->boolean('primary'); + $table->timestamps(); + $table->primary(['show_id', 'moderator_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('show_moderators'); + } +}; From c38c93c324012583ba73b81baf41b6af79103d30 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:15:38 +0100 Subject: [PATCH 02/21] add show index Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/Controller.php | 24 +++ app/Http/Controllers/ShowController.php | 213 ++++++++++++++++++++++++ app/Http/Resources/ShowResource.php | 29 ++++ database/factories/ShowFactory.php | 50 ++++++ 4 files changed, 316 insertions(+) create mode 100644 app/Http/Controllers/ShowController.php create mode 100644 app/Http/Resources/ShowResource.php create mode 100644 database/factories/ShowFactory.php diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 77ec359a..28888812 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -5,8 +5,32 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; +use Illuminate\Support\Facades\Validator; class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; + + /** + * Validates the given parameters based on the provided casts and rules. + * + * @param array $params The parameters to be validated. + * @param array $casts The casting rules for the parameters. + * @param array $rules The validation rules for the parameters. + * @return array The validated parameters. + */ + protected function validateParams($params, $casts, $rules): array + { + foreach ($casts as $key => $cast) { + if (isset($params[$key])) { + if ($cast === 'boolean') { + $params[$key] = filter_var($params[$key], FILTER_VALIDATE_BOOLEAN); + } else { + settype($params[$key], $cast); + } + } + } + + return Validator::make($params, $rules)->validate(); + } } diff --git a/app/Http/Controllers/ShowController.php b/app/Http/Controllers/ShowController.php new file mode 100644 index 00000000..037325a1 --- /dev/null +++ b/app/Http/Controllers/ShowController.php @@ -0,0 +1,213 @@ + 'date', + 'end_date' => 'date', + 'days' => 'integer', + 'live' => 'boolean', + 'moderator' => 'array', + 'moderator.*' => 'integer', + 'primary' => 'boolean', + 'sort' => 'string', + 'per_page' => 'integer', + ]; + + $rules = [ + 'start_date' => ['date'], + 'end_date' => ['exclude_without:start_date', 'date', 'after:start_date'], + 'days' => ['integer', 'min:1'], + 'live' => 'boolean', + 'moderator' => ['array'], + 'moderator.*' => ['integer', 'distinct', 'exists:users,id'], + 'primary' => ['boolean', 'exclude_without:moderator'], + 'sort' => ['string', 'in:' . implode(',', $SORT_OPTIONS)], + 'per_page' => 'integer', + + ]; + + $validated = $this->validateParams(request()->all(), $casts, $rules); + + $shows = Show::query()->with([ + "moderators" => function ($query) { + $query->withPivot('primary'); + } + ]); + + if (!auth()->check()) { + $shows->where('enabled', '=', true); + } + + $shows->where(function ($query) use ($validated) { + $NOW = now(); + if (isset($validated['start_date'])) { + $query->whereBetween('start_date', + [ + $validated['start_date'], + $validated['end_date'] ?? $validated['days'] ?? $validated['start_date']->addDays(7) + ]); + } elseif (isset($validated['days']) && !isset($validated['start_date'])) { + $inXDays = (clone $NOW)->addDays($validated['days']); + $query->whereBetween('start_date', + [ + $NOW, + $inXDays + ]) + ->orWhereBetween('end_date', + [ + $NOW, + $inXDays + ]); + } else { + $in7Days = (clone $NOW)->addDays(7); + $query->whereBetween('start_date', + [ + $NOW, + $in7Days + ]) + ->orWhereBetween('end_date', + [ + $NOW, + $in7Days + ]); + } + }); + + /** + * Hide shows that are not enabled and the primary moderator is not the current user. + */ + if (auth()->check()) { + $user = auth()->user(); + $shows->where(function ($query) use ($user) { + $query->where('enabled', '=', true) + ->orWhere(function ($query) use ($user) { + $query->where('enabled', '=', false) + ->whereHas('moderators', function ($query) use ($user) { + $query->where('moderator_id', '=', $user->id) + ->where('primary', '=', true); + }); + }); + }); + } + + /** + * Check if the query parameter "live" is set. + * If it is, it should return only shows that are live or not live, + * depending on the value of the query parameter "live". + */ + if (isset($validated['live'])) { + $shows->where('is_live', '=', $validated['live']); + } + + /** + * If the query parameter "moderator" is set, + * it should return only shows that have the given user as a moderator. + */ + if (isset($validated['moderator'])) { + $shows->whereHas('moderators', function ($query) use ($validated) { + $query->whereIn('moderator_id', $validated['moderator']); + // If the query parameter "primary" is set, + // it should return only shows that have the given user as a primary moderator or not, + // depending on the value of the query parameter "primary". + if (isset($validated['primary'])) { + $query->where('primary', '=', $validated['primary']); + } + }); + } + + /** + * If the query parameter "sort" is set, + * it should return the shows sorted by the given field. + * If the query parameter "sort" isn't set, it should return the shows sorted by start date. + */ + if (isset($validated['sort'])) { + $sort = explode(':', $validated['sort']); + if (in_array($validated['sort'], $SORT_OPTIONS)) { + if (count($sort) === 1) { + $shows->orderBy($sort[0]); + } else { + $shows->orderBy($sort[0], $sort[1]); + } + } + } else { + $shows->orderBy('start_date'); + } + + + /** + * If the query parameter "per_page" is set, + * it should return the given amount of shows per page. + * If "per_page" isn't set, it should return 25 shows per page, maximum 50. + */ + if (isset($validated['per_page']) && $validated['per_page'] <= 50) { + $res = $shows->paginate($validated['per_page']); + } else { + $res = $shows->paginate(25); + } + + return ShowResource::collection($res); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(Show $show) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Show $show) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Show $show) + { + // + } +} diff --git a/app/Http/Resources/ShowResource.php b/app/Http/Resources/ShowResource.php new file mode 100644 index 00000000..80dee930 --- /dev/null +++ b/app/Http/Resources/ShowResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + $show = parent::toArray($request); + + $show['moderators'] = $this->moderators->map(function ($moderator) { + return [ + 'id' => $moderator->id, + 'name' => $moderator->name, + 'primary' => $moderator->moderators->primary, + ]; + }); + + return $show; + } +} diff --git a/database/factories/ShowFactory.php b/database/factories/ShowFactory.php new file mode 100644 index 00000000..75b2d8bb --- /dev/null +++ b/database/factories/ShowFactory.php @@ -0,0 +1,50 @@ + + */ +class ShowFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + do { + + $start_date = $this->faker->dateTimeBetween('-2 days', '+1 month'); + $end_date = $this->faker->dateTimeBetween($start_date, (clone $start_date)->modify('+48 hours')); + + $show = Show::where('start_date', '<=', $end_date)->where('end_date', '>=', $start_date)->first(); + } while ($show !== null); + + return [ + 'title' => $this->faker->sentence(), + 'body' => $this->faker->paragraph(), + 'start_date' => $start_date, + 'end_date' => $end_date, + 'is_live' => $this->faker->boolean(), + 'enabled' => $this->faker->boolean(), + 'locked_by' => $this->faker->randomElement([null, User::all()->random()->id ?? User::factory()->create()->id]) + ]; + } + + public function withUser() + { + return $this->afterCreating(function (Show $show) { + // Add a user to the show and set it as primary. + $show->moderators()->attach(User::all()->random()->id ?? User::factory()->create()->id, ['primary' => true]); + + // Add a random number of users to the show. Which are not already added. + $show->moderators()->attach(User::all()->random()->pluck('id')->diff($show->moderators()->pluck('moderator_id')), ['primary' => false]); + }); + } +} From 7bb0abd3f38300b6d19fff712d7dfb4aa9a97cee Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:15:49 +0100 Subject: [PATCH 03/21] add show routes Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- routes/api.php | 1 + routes/api/v1/show.php | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 routes/api/v1/show.php diff --git a/routes/api.php b/routes/api.php index 4de97518..c95d4c86 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,4 +24,5 @@ require __DIR__ . '/api/v1/auth.php'; require __DIR__ . '/api/v1/user.php'; require __DIR__ . '/api/v1/role.php'; + require __DIR__ . '/api/v1/show.php'; }); diff --git a/routes/api/v1/show.php b/routes/api/v1/show.php new file mode 100644 index 00000000..6fd95ee3 --- /dev/null +++ b/routes/api/v1/show.php @@ -0,0 +1,11 @@ +name('api.v1.shows.index'); +Route::post('/shows', [ShowController::class, 'store'])->name('api.v1.shows.store'); +Route::get('/shows/{show}', [ShowController::class, 'show'])->name('api.v1.shows.show'); +Route::put('/shows/{show}', [ShowController::class, 'update'])->name('api.v1.shows.update'); +Route::delete('/shows/{show}', [ShowController::class, 'destroy'])->name('api.v1.shows.destroy'); + From da970af71acfafa9e99d3a930affeda7ec6b456d Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:48:07 +0100 Subject: [PATCH 04/21] add show permissions Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Permissions/ShowsPermissions.php | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/Permissions/ShowsPermissions.php diff --git a/app/Permissions/ShowsPermissions.php b/app/Permissions/ShowsPermissions.php new file mode 100644 index 00000000..fc7433b3 --- /dev/null +++ b/app/Permissions/ShowsPermissions.php @@ -0,0 +1,44 @@ + Date: Sat, 23 Dec 2023 20:58:20 +0100 Subject: [PATCH 05/21] changes :D Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/ShowController.php | 38 +++++++++++++------ database/seeders/DatabaseSeeder.php | 11 ++++++ .../Http/Controllers/ShowControllerTest.php | 20 ++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 tests/Feature/Http/Controllers/ShowControllerTest.php diff --git a/app/Http/Controllers/ShowController.php b/app/Http/Controllers/ShowController.php index 037325a1..c3a50fd3 100644 --- a/app/Http/Controllers/ShowController.php +++ b/app/Http/Controllers/ShowController.php @@ -6,8 +6,7 @@ use App\Models\Show; use App\Permissions\ShowsPermissions; use Illuminate\Http\Request; -use Illuminate\Database\Eloquent\Collection; -use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Facades\Cache; class ShowController extends Controller { @@ -19,6 +18,15 @@ class ShowController extends Controller */ public function index() { + /** + * If no query parameters are provided, it should return the cached result. + */ + if (count(request()->all()) === 0 && !auth()->check()) { + $res = Cache::get('shows_index'); + if ($res) { + return ShowResource::collection($res); + } + } static $SORT_OPTIONS = [ 'id', @@ -70,7 +78,7 @@ public function index() } $shows->where(function ($query) use ($validated) { - $NOW = now(); + $NOW = now()->today(); if (isset($validated['start_date'])) { $query->whereBetween('start_date', [ @@ -108,17 +116,20 @@ public function index() * Hide shows that are not enabled and the primary moderator is not the current user. */ if (auth()->check()) { + /** @var \App\Models\User $user */ $user = auth()->user(); - $shows->where(function ($query) use ($user) { - $query->where('enabled', '=', true) - ->orWhere(function ($query) use ($user) { - $query->where('enabled', '=', false) - ->whereHas('moderators', function ($query) use ($user) { - $query->where('moderator_id', '=', $user->id) - ->where('primary', '=', true); + if (!$user->hasPermissionTo(ShowsPermissions::CAN_VIEW_DISABLED_SHOWS_OTHERS)) { + $shows->where(function ($query) use ($user) { + $query->where('enabled', '=', true) + ->orWhere(function ($query) use ($user) { + $query->where('enabled', '=', false) + ->whereHas('moderators', function ($query) use ($user) { + $query->where('moderator_id', '=', $user->id) + ->where('primary', '=', true); }); }); - }); + }); + } } /** @@ -164,7 +175,6 @@ public function index() $shows->orderBy('start_date'); } - /** * If the query parameter "per_page" is set, * it should return the given amount of shows per page. @@ -174,6 +184,10 @@ public function index() $res = $shows->paginate($validated['per_page']); } else { $res = $shows->paginate(25); + // Cache the result if no query parameters are provided + if (count(request()->all()) === 0 && !auth()->check()) { + Cache::put('shows_index', $res, 60); + } } return ShowResource::collection($res); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 0223acf4..a7b149eb 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -20,5 +20,16 @@ public function run(): void // 'email_verified_at' => now(), // 'password' => bcrypt('test1234'), // ]); + + \App\Models\User::factory(30)->create(); + + \App\Models\Show::factory(10)->create([ + 'enabled' => true, + ])->each(function ($show) { + $show->moderators()->attach(\App\Models\User::all()->random()->id ?? \App\Models\User::factory()->create()->id, ['primary' => true]); + + // Add a random number of users to the show. Which are not already added. + $show->moderators()->attach(\App\Models\User::all()->random(rand(0, 10))->pluck('id')->diff($show->moderators()->pluck('moderator_id')), ['primary' => false]); + }); } } diff --git a/tests/Feature/Http/Controllers/ShowControllerTest.php b/tests/Feature/Http/Controllers/ShowControllerTest.php new file mode 100644 index 00000000..e9c721b2 --- /dev/null +++ b/tests/Feature/Http/Controllers/ShowControllerTest.php @@ -0,0 +1,20 @@ +get('/api/v1/shows'); + + $response->assertStatus(200); + } +} From 37bcb84cdb514173e74c218c108fa9807031a920 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:13:18 +0100 Subject: [PATCH 06/21] removed complex request system Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/ShowController.php | 76 ++++++++----------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/app/Http/Controllers/ShowController.php b/app/Http/Controllers/ShowController.php index c3a50fd3..d53fc777 100644 --- a/app/Http/Controllers/ShowController.php +++ b/app/Http/Controllers/ShowController.php @@ -3,10 +3,11 @@ namespace App\Http\Controllers; use App\Http\Resources\ShowResource; +use App\Http\Responses\ApiErrorResponse; use App\Models\Show; use App\Permissions\ShowsPermissions; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Cache; +use Illuminate\Http\Response; class ShowController extends Controller { @@ -14,30 +15,20 @@ class ShowController extends Controller /** * Retrieve a paginated list of shows based on the provided query parameters. * - * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection | \App\Http\Responses\ApiErrorResponse */ public function index() { - /** - * If no query parameters are provided, it should return the cached result. - */ - if (count(request()->all()) === 0 && !auth()->check()) { - $res = Cache::get('shows_index'); - if ($res) { - return ShowResource::collection($res); - } - } - static $SORT_OPTIONS = [ 'id', 'id:asc', 'id:desc', - 'start', - 'start:asc', - 'start:desc', - 'end', - 'end:asc', - 'end:desc', + 'start_date', + 'start_date:asc', + 'start_date:desc', + 'end_date', + 'end_date:asc', + 'end_date:desc', ]; $casts = [ @@ -53,9 +44,8 @@ public function index() ]; $rules = [ - 'start_date' => ['date'], - 'end_date' => ['exclude_without:start_date', 'date', 'after:start_date'], - 'days' => ['integer', 'min:1'], + 'start_date' => ['required', 'date', 'before:end_date'], + 'end_date' => ['required', 'date', 'after:start_date'], 'live' => 'boolean', 'moderator' => ['array'], 'moderator.*' => ['integer', 'distinct', 'exists:users,id'], @@ -74,42 +64,28 @@ public function index() ]); if (!auth()->check()) { + if ($validated['start_date'] < today() ) { + return new ApiErrorResponse('Start date must be greater than today.', status: Response::HTTP_BAD_REQUEST); + } + if ($validated['end_date'] > today()->addMonth() ) { + return new ApiErrorResponse('End date must be less than 30 days from today.', status: Response::HTTP_BAD_REQUEST); + } + $shows->where('enabled', '=', true); } + $shows->where(function ($query) use ($validated) { - $NOW = now()->today(); - if (isset($validated['start_date'])) { - $query->whereBetween('start_date', + $query->whereBetween('start_date', [ $validated['start_date'], - $validated['end_date'] ?? $validated['days'] ?? $validated['start_date']->addDays(7) - ]); - } elseif (isset($validated['days']) && !isset($validated['start_date'])) { - $inXDays = (clone $NOW)->addDays($validated['days']); - $query->whereBetween('start_date', - [ - $NOW, - $inXDays - ]) - ->orWhereBetween('end_date', - [ - $NOW, - $inXDays + $validated['end_date'], ]); - } else { - $in7Days = (clone $NOW)->addDays(7); - $query->whereBetween('start_date', + $query->orWhereBetween('end_date', [ - $NOW, - $in7Days - ]) - ->orWhereBetween('end_date', - [ - $NOW, - $in7Days + $validated['start_date'], + $validated['end_date'], ]); - } }); /** @@ -184,10 +160,6 @@ public function index() $res = $shows->paginate($validated['per_page']); } else { $res = $shows->paginate(25); - // Cache the result if no query parameters are provided - if (count(request()->all()) === 0 && !auth()->check()) { - Cache::put('shows_index', $res, 60); - } } return ShowResource::collection($res); From f82df7a53a20810946894a0cfec8d617fd4d0728 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:13:58 +0100 Subject: [PATCH 07/21] refactore `GET /shows` parameters Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- .github/assets/swagger.yml | 60 +++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/.github/assets/swagger.yml b/.github/assets/swagger.yml index 1659c9c6..e510c3a6 100644 --- a/.github/assets/swagger.yml +++ b/.github/assets/swagger.yml @@ -24,16 +24,55 @@ paths: format: date-time - name: end_date in: query - description: End date of the broadcast schedule. + description: End date of the broadcast schedule. End date must be less than 30 days from today if not logged in schema: type: string format: date-time - - name: days + - name: live in: query - description: Number of days for which to return the broadcast schedule. + description: If only live shows or not live shows should be returned. + schema: + type: boolean + - name: moderator[] + in: query + description: Array of user ids which shows should be returned. + schema: + type: array + items: + type: integer + - name: primary + in: query + description: If only show should returned where `moderator[]` is primary. + schema: + type: boolean + - name: sort + in: query + schema: + type: string + enum: + - id + - id:asc + - id:desc + - start_date + - start_date:asc + - start_date:desc + - end_date + - end_date:asc + - end_date:desc + default: start_date + - name: per_page + in: query + description: Items to load per page. + schema: + type: integer + maximum: 50 + default: 25 + - name: page + in: query + description: Page to load. schema: - maximum: 30 type: integer + default: 1 responses: "200": description: OK @@ -1092,12 +1131,15 @@ components: properties: id: type: integer + readOnly: true created_at: type: string format: date-time + readOnly: true updated_at: type: string format: date-time + readOnly: true title: type: string body: @@ -1127,6 +1169,7 @@ components: type: boolean locked_by: type: integer + nullable: true Request: required: - created_at @@ -1137,6 +1180,7 @@ components: properties: id: type: integer + readOnly: true name: type: string message: @@ -1144,6 +1188,7 @@ components: created_at: type: string format: date-time + readOnly: true User: required: - created_at @@ -1156,12 +1201,15 @@ components: properties: id: type: integer + readOnly: true created_at: type: string format: date-time + readOnly: true updated_at: type: string format: date-time + readOnly: true name: type: string email: @@ -1169,6 +1217,7 @@ components: email_verified_at: type: string format: date-time + readOnly: true password: type: string format: password @@ -1185,12 +1234,15 @@ components: properties: id: type: integer + readOnly: true created_at: type: string format: date-time + readOnly: true updated_at: type: string format: date-time + readOnly: true name: type: string permissions: From 1bbab53d2594286611d52491ddbe7d54c224ae92 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:33:08 +0100 Subject: [PATCH 08/21] Add overlap checking Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/ShowController.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/ShowController.php b/app/Http/Controllers/ShowController.php index d53fc777..97d1cb3d 100644 --- a/app/Http/Controllers/ShowController.php +++ b/app/Http/Controllers/ShowController.php @@ -170,7 +170,26 @@ public function index() */ public function store(Request $request) { - // + + $request->validate([ + 'start_date' => 'required|date|before:end_date', + 'end_date' => 'required|date|after:start_date', + ]); + // Check if there is a show which overlaps with the given start and end date. + $overlapCount = Show::query(); + + $overlapCount->where(function ($query) use ($request) { + $query->whereRaw('? BETWEEN start_date AND end_date', [$request->start_date]); + }); + $overlapCount->orWhere(function ($query) use ($request) { + $query->whereRaw('? BETWEEN start_date AND end_date', [$request->end_date]); + }); + $overlapCount->orWhere(function ($query) use ($request) { + $query->where('start_date', '<=', $request->start_date); + $query->where('end_date', '>=', $request->end_date); + }); + + echo $overlapCount->count(); } /** From 49a50ac2116138c5c73c631fc165af911578990b Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 04:25:13 +0100 Subject: [PATCH 09/21] add auth for routes Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- routes/api/v1/show.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/routes/api/v1/show.php b/routes/api/v1/show.php index 6fd95ee3..2b9f88e1 100644 --- a/routes/api/v1/show.php +++ b/routes/api/v1/show.php @@ -9,3 +9,9 @@ Route::put('/shows/{show}', [ShowController::class, 'update'])->name('api.v1.shows.update'); Route::delete('/shows/{show}', [ShowController::class, 'destroy'])->name('api.v1.shows.destroy'); +Route::group(['middleware' => ['auth:sanctum']], function () { + Route::post('/shows', [ShowController::class, 'store'])->name('api.v1.shows.store'); + Route::put('/shows/{show}', [ShowController::class, 'update'])->name('api.v1.shows.update'); + Route::delete('/shows/{show}', [ShowController::class, 'destroy'])->name('api.v1.shows.destroy'); +}); + From 73bc1f5766ac71a6f5cf1e5454747be57df16d82 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 04:25:48 +0100 Subject: [PATCH 10/21] change permission naming Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Permissions/ShowsPermissions.php | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/app/Permissions/ShowsPermissions.php b/app/Permissions/ShowsPermissions.php index fc7433b3..f5d38a6c 100644 --- a/app/Permissions/ShowsPermissions.php +++ b/app/Permissions/ShowsPermissions.php @@ -10,35 +10,38 @@ class ShowsPermissions { /** Permission for viewing others disabled shows. */ - public const CAN_VIEW_DISABLED_SHOWS_OTHERS = 'view-disabled-shows-others'; + public const CAN_VIEW_DISABLED_SHOWS_OTHERS = 'shows.view-disabled.others'; /** Permission for creating own shows as primary moderator. */ - public const CAN_CREATE_SHOWS = 'create-shows'; + public const CAN_CREATE_SHOWS = 'shows.create'; /** Permission for creating shows for others as primary moderator. */ - public const CAN_CREATE_SHOWS_OTHERS = 'create-shows-others'; + public const CAN_CREATE_SHOWS_OTHERS = 'shows.create.others'; /** Permission for updating own shows when primary moderator. */ - public const CAN_UPDATE_SHOWS = 'update-shows'; + public const CAN_UPDATE_SHOWS = 'shows.update'; /** Permission for updating shows for others. */ - public const CAN_UPDATE_SHOWS_OTHERS = 'update-shows-others'; + public const CAN_UPDATE_SHOWS_OTHERS = 'shows.update.others'; /** Permission for deleting own shows when primary moderator. */ - public const CAN_DELETE_SHOWS = 'delete-shows'; + public const CAN_DELETE_SHOWS = 'shows.delete'; /** Permission for deleting shows for others. */ - public const CAN_DELETE_SHOWS_OTHERS = 'delete-shows-others'; + public const CAN_DELETE_SHOWS_OTHERS = 'shows.delete.others'; - /** Permission for to be primary moderator of a show. */ - public const CAN_BE_PRIMARY_MODERATOR = 'primary-moderator-shows'; + /** Permission to be primary moderator of a show. */ + public const CAN_BE_PRIMARY_MODERATOR = 'shows.be-primary-moderator'; - /** Permission for to be moderator of a show. */ - public const CAN_BE_MODERATOR = 'moderator-shows'; + /** Permission for adding non-primary moderators to a show. */ + public const CAN_ADD_MODERATORS = 'shows.add-moderators'; + + /** Permission to be moderator of a show, but not primary . */ + public const CAN_BE_MODERATOR = 'shows.be-moderator'; /** Permission to change a show's live status. */ - public const CAN_SET_LIVE_SHOWS = 'set-live-shows'; + public const CAN_SET_LIVE_SHOWS = 'shows.set-live'; /** Permission to enable or disable a show. */ - public const CAN_ENABLE_SHOWS = 'enable-shows'; + public const CAN_ENABLE_SHOWS = 'shows.enable'; } From c126de466a9b35477114b929089b50aefa87e2e9 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 04:26:05 +0100 Subject: [PATCH 11/21] changed permission creation Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- ...2023_12_28_223745_add_show_permissions.php | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 database/migrations/2023_12_28_223745_add_show_permissions.php diff --git a/database/migrations/2023_12_28_223745_add_show_permissions.php b/database/migrations/2023_12_28_223745_add_show_permissions.php new file mode 100644 index 00000000..4150b473 --- /dev/null +++ b/database/migrations/2023_12_28_223745_add_show_permissions.php @@ -0,0 +1,84 @@ + 'shows.view-disabled.others', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.create', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.create.others', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.update', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.update.others', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.delete', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.delete.others', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.be-primary-moderator', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.be-moderator', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.add-moderators', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.set-live', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'shows.enable', + 'guard_name' => 'web', + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('permissions')->whereIn('name', [ + 'shows.view-disabled.others', + 'shows.create', + 'shows.create.others', + 'shows.update', + 'shows.update.others', + 'shows.delete', + 'shows.delete.others', + 'shows.be-primary-moderator', + 'shows.be-moderator', + 'shows.add-moderators', + 'shows.set-live', + 'shows.enable', + ])->delete(); + } +}; From 1b06cbd4d46630a9e4a04cf2f613e76d3c7624c1 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 04:26:36 +0100 Subject: [PATCH 12/21] always transform primary to bool Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Resources/ShowResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Resources/ShowResource.php b/app/Http/Resources/ShowResource.php index 80dee930..1befb503 100644 --- a/app/Http/Resources/ShowResource.php +++ b/app/Http/Resources/ShowResource.php @@ -20,7 +20,7 @@ public function toArray(Request $request): array return [ 'id' => $moderator->id, 'name' => $moderator->name, - 'primary' => $moderator->moderators->primary, + 'primary' => $moderator->moderators->primary === 1, ]; }); From 548f01825a6bb8bc68d93f9c63d179609afaa08d Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 04:27:06 +0100 Subject: [PATCH 13/21] add attr casting Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Models/Show.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Models/Show.php b/app/Models/Show.php index b67d9aea..db1f7a0b 100644 --- a/app/Models/Show.php +++ b/app/Models/Show.php @@ -15,6 +15,10 @@ class Show extends Model * @var array */ protected $fillable = [ + 'title', + 'body', + 'enabled', + 'is_live', ]; /** @@ -31,6 +35,10 @@ class Show extends Model * @var array */ protected $casts = [ + 'start_date' => 'datetime', + 'end_date' => 'datetime', + 'enabled' => 'boolean', + 'is_live' => 'boolean', ]; public function locked_by() From f17a13eeb06d8d122e31d27925f58e93ba8ff877 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 04:31:23 +0100 Subject: [PATCH 14/21] Add functionality Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/ShowController.php | 544 +++++++++++++++++++++--- 1 file changed, 479 insertions(+), 65 deletions(-) diff --git a/app/Http/Controllers/ShowController.php b/app/Http/Controllers/ShowController.php index 97d1cb3d..60344641 100644 --- a/app/Http/Controllers/ShowController.php +++ b/app/Http/Controllers/ShowController.php @@ -4,20 +4,87 @@ use App\Http\Resources\ShowResource; use App\Http\Responses\ApiErrorResponse; +use App\Http\Responses\ApiSuccessResponse; use App\Models\Show; +use App\Models\User; use App\Permissions\ShowsPermissions; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class ShowController extends Controller { + /** + * Check if there are any other shows that overlap with the given start and end dates. + * If the start date is the same as the end date of another show, it should return false and vice versa. + * + * @param string $start_date The start date of the show. + * @param string $end_date The end date of the show. + * @param int|null $show_id The id of the show to exclude from the check. + * @return bool Returns true if there are no overlapping shows, false otherwise. + */ + private function _checkForOtherShow(string $start_date, string $end_date, int|null $show_id = null, bool|null $enabled = true): bool + { + $overlapCount = Show::query(); + + if ($enabled !== null) { + $overlapCount->where('enabled', '=', $enabled); + } + + $overlapCount->where(function ($query) use ($start_date, $end_date) { + $query->where(function ($query) use ($start_date) { + $query->where(function ($query) use ($start_date) { + $query->whereRaw('? BETWEEN `start_date` AND `end_date`', [$start_date]); + }); + $query->where(function ($query) use ($start_date) { + $query->where('end_date', '!=', [$start_date]); + }); + }); + + $query->orWhere(function ($query) use ($end_date) { + $query->where(function ($query) use ($end_date) { + $query->whereRaw('? BETWEEN `start_date` AND `end_date`', [$end_date]); + }); + $query->where(function ($query) use ($end_date) { + $query->where('start_date', '!=', [$end_date]); + }); + }); + $query->orWhere(function ($query) use ($start_date, $end_date) { + $query->where('start_date', '<=', $start_date); + $query->where('end_date', '>=', $end_date); + }); + }); + + if ($show_id !== null) { + $overlapCount->where('id', '!=', $show_id); + } + + return $overlapCount->count() === 0; + } + + /** + * ShowController constructor. + * + * This method initializes the ShowController class. + * It sets up the necessary middleware for specific actions. + */ + public function __construct() + { + $this->middleware('permission:' . ShowsPermissions::CAN_CREATE_SHOWS . '|' . ShowsPermissions::CAN_CREATE_SHOWS_OTHERS) + ->only(['store']); + $this->middleware('permission:' . ShowsPermissions::CAN_UPDATE_SHOWS . '|' . ShowsPermissions::CAN_UPDATE_SHOWS_OTHERS) + ->only(['update']); + $this->middleware('permission:' . ShowsPermissions::CAN_DELETE_SHOWS . '|' . ShowsPermissions::CAN_DELETE_SHOWS_OTHERS) + ->only(['delete']); + } + /** * Retrieve a paginated list of shows based on the provided query parameters. * * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection | \App\Http\Responses\ApiErrorResponse */ - public function index() + public function index(Request $request) { static $SORT_OPTIONS = [ 'id', @@ -31,19 +98,7 @@ public function index() 'end_date:desc', ]; - $casts = [ - 'start_date' => 'date', - 'end_date' => 'date', - 'days' => 'integer', - 'live' => 'boolean', - 'moderator' => 'array', - 'moderator.*' => 'integer', - 'primary' => 'boolean', - 'sort' => 'string', - 'per_page' => 'integer', - ]; - - $rules = [ + $request->validate([ 'start_date' => ['required', 'date', 'before:end_date'], 'end_date' => ['required', 'date', 'after:start_date'], 'live' => 'boolean', @@ -53,9 +108,13 @@ public function index() 'sort' => ['string', 'in:' . implode(',', $SORT_OPTIONS)], 'per_page' => 'integer', - ]; + ]); - $validated = $this->validateParams(request()->all(), $casts, $rules); + $request['start_date'] = $request->date('start_date')->toDateTimeString(); + $request['end_date'] = $request->date('end_date')->toDateTimeString(); + + /** @var \App\Models\User $user */ + $user = $request->user('sanctum'); $shows = Show::query()->with([ "moderators" => function ($query) { @@ -63,11 +122,11 @@ public function index() } ]); - if (!auth()->check()) { - if ($validated['start_date'] < today() ) { + if ($user === null) { + if ($request->start_date < today() ) { return new ApiErrorResponse('Start date must be greater than today.', status: Response::HTTP_BAD_REQUEST); } - if ($validated['end_date'] > today()->addMonth() ) { + if ($request->end_date > today()->addMonth() ) { return new ApiErrorResponse('End date must be less than 30 days from today.', status: Response::HTTP_BAD_REQUEST); } @@ -75,37 +134,33 @@ public function index() } - $shows->where(function ($query) use ($validated) { + $shows->where(function ($query) use ($request) { $query->whereBetween('start_date', [ - $validated['start_date'], - $validated['end_date'], + $request->start_date, + $request->end_date, ]); $query->orWhereBetween('end_date', [ - $validated['start_date'], - $validated['end_date'], + $request->start_date, + $request->end_date, ]); }); /** * Hide shows that are not enabled and the primary moderator is not the current user. */ - if (auth()->check()) { - /** @var \App\Models\User $user */ - $user = auth()->user(); - if (!$user->hasPermissionTo(ShowsPermissions::CAN_VIEW_DISABLED_SHOWS_OTHERS)) { - $shows->where(function ($query) use ($user) { - $query->where('enabled', '=', true) - ->orWhere(function ($query) use ($user) { - $query->where('enabled', '=', false) - ->whereHas('moderators', function ($query) use ($user) { - $query->where('moderator_id', '=', $user->id) - ->where('primary', '=', true); - }); - }); + if ($user !== null && !$user->hasPermissionTo(ShowsPermissions::CAN_VIEW_DISABLED_SHOWS_OTHERS)) { + $shows->where(function ($query) use ($user) { + $query->where('enabled', '=', true) + ->orWhere(function ($query) use ($user) { + $query->where('enabled', '=', false) + ->whereHas('moderators', function ($query) use ($user) { + $query->where('moderator_id', '=', $user->id) + ->where('primary', '=', true); + }); }); - } + }); } /** @@ -113,22 +168,22 @@ public function index() * If it is, it should return only shows that are live or not live, * depending on the value of the query parameter "live". */ - if (isset($validated['live'])) { - $shows->where('is_live', '=', $validated['live']); + if ($request->live !== null) { + $shows->where('is_live', '=', $request->live); } /** * If the query parameter "moderator" is set, * it should return only shows that have the given user as a moderator. */ - if (isset($validated['moderator'])) { - $shows->whereHas('moderators', function ($query) use ($validated) { - $query->whereIn('moderator_id', $validated['moderator']); + if ($request->moderator !== null && count($request->moderator) > 0) { + $shows->whereHas('moderators', function ($query) use ($request) { + $query->whereIn('moderator_id', $request->moderator); // If the query parameter "primary" is set, // it should return only shows that have the given user as a primary moderator or not, // depending on the value of the query parameter "primary". if (isset($validated['primary'])) { - $query->where('primary', '=', $validated['primary']); + $query->where('primary', '=', $request->primary); } }); } @@ -138,9 +193,9 @@ public function index() * it should return the shows sorted by the given field. * If the query parameter "sort" isn't set, it should return the shows sorted by start date. */ - if (isset($validated['sort'])) { - $sort = explode(':', $validated['sort']); - if (in_array($validated['sort'], $SORT_OPTIONS)) { + if ($request->sort !== null) { + $sort = explode(':', $request->sort); + if (in_array($request->sort, $SORT_OPTIONS)) { if (count($sort) === 1) { $shows->orderBy($sort[0]); } else { @@ -151,68 +206,427 @@ public function index() $shows->orderBy('start_date'); } + + /** * If the query parameter "per_page" is set, * it should return the given amount of shows per page. * If "per_page" isn't set, it should return 25 shows per page, maximum 50. */ - if (isset($validated['per_page']) && $validated['per_page'] <= 50) { - $res = $shows->paginate($validated['per_page']); + if ($request->sort !== null && $request->per_page <= 50) { + $res = $shows->paginate($request->per_page); } else { $res = $shows->paginate(25); } + if ($user === null) { + $res = collect($res->items())->map(function ($show) { + return $show->makeHidden('locked_by'); + }); + } + return ShowResource::collection($res); } /** * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \App\Http\Resources\ShowResource | \App\Http\Responses\ApiErrorResponse */ public function store(Request $request) { - $request->validate([ + 'title' => 'required|string', + 'body' => 'presemt|string|nullable', 'start_date' => 'required|date|before:end_date', 'end_date' => 'required|date|after:start_date', + 'live' => 'required|boolean', + 'enabled' => 'required|boolean', + 'moderators' => 'required|array', + 'moderators.*' => 'required|array', + 'moderators.*.id' => 'required|integer|distinct|exists:users,id', + 'moderators.*.primary' => 'required|boolean', ]); - // Check if there is a show which overlaps with the given start and end date. - $overlapCount = Show::query(); - $overlapCount->where(function ($query) use ($request) { - $query->whereRaw('? BETWEEN start_date AND end_date', [$request->start_date]); + // Get the primary moderator. + $primaryModerator = collect($request->moderators)->filter(function ($moderator) { + return $moderator['primary'] === true; }); - $overlapCount->orWhere(function ($query) use ($request) { - $query->whereRaw('? BETWEEN start_date AND end_date', [$request->end_date]); - }); - $overlapCount->orWhere(function ($query) use ($request) { - $query->where('start_date', '<=', $request->start_date); - $query->where('end_date', '>=', $request->end_date); + + /** + * A show must have exactly one primary moderator. + */ + if ($primaryModerator->count() !== 1) { + return new ApiErrorResponse('There must be exactly one primary moderator.', status: Response::HTTP_BAD_REQUEST); + } + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + /** + * Check if the user is the primary moderator of the show. + */ + if ($primaryModerator->first()['id'] !== $user->id) { + if (!$user->checkPermissionTo(ShowsPermissions::CAN_CREATE_SHOWS_OTHERS, 'web')) { + return new ApiErrorResponse('You are not allowed to create shows for others.', status: Response::HTTP_FORBIDDEN); + } + } + + /** + * Convert the start and end date to datetime strings. + */ + $request['start_date'] = $request->date('start_date')->toDateTimeString(); + $request['end_date'] = $request->date('end_date')->toDateTimeString(); + + /** + * Check if the show collides with another show. + */ + if (!$this->_checkForOtherShow($request->start_date, $request->end_date)) { + return new ApiErrorResponse('There is already a show scheduled for this time.', status: Response::HTTP_BAD_REQUEST); + } + + // Get the primary moderator. + $primaryModerator = User::find($primaryModerator->first()['id']); + + /** + * Check if the primary moderator has the permission to be a primary moderator. + */ + if (!$primaryModerator->checkPermissionTo(ShowsPermissions::CAN_BE_PRIMARY_MODERATOR)) { + return new ApiErrorResponse('The primary moderator, "'. $primaryModerator->name .'" does not have the permission to be a primary moderator.', status: Response::HTTP_BAD_REQUEST); + } + + // Get the non-primary moderators. + $nonPrimaryModerators = collect($request->moderators)->filter(function ($moderator) { + return $moderator['primary'] === false; }); - echo $overlapCount->count(); + /** + * If there are non-primary moderators, check if the user has the permission to add non-primary moderators. + */ + if ($nonPrimaryModerators->count() > 0) { + if (!$user->checkPermissionTo(ShowsPermissions::CAN_ADD_MODERATORS, 'web')) { + return new ApiErrorResponse('You are not allowed to add non-primary moderators.', status: Response::HTTP_FORBIDDEN); + } + /** + * Check if the non-primary moderators have the permission to be a moderator. + */ + foreach ($nonPrimaryModerators as $nonPrimaryModerator) { + $nonPrimaryModerator = User::find($nonPrimaryModerator['id']); + if (!$nonPrimaryModerator->checkPermissionTo(ShowsPermissions::CAN_BE_MODERATOR)) { + return new ApiErrorResponse('"' . $nonPrimaryModerator->name. '" does not have the permission to be a non-primary moderator.', status: Response::HTTP_BAD_REQUEST); + } + } + } + + /** + * If the show is live, check if the user has the permission to set live shows. + */ + if ($request->live && !$user->checkPermissionTo(ShowsPermissions::CAN_SET_LIVE_SHOWS, 'web')) { + return new ApiErrorResponse('You are not allowed to set shows live.', status: Response::HTTP_FORBIDDEN); + } + + /** + * If the show is enabled, check if the user has the permission to enable shows. + */ + if ($request->enabled && !$user->checkPermissionTo(ShowsPermissions::CAN_ENABLE_SHOWS, 'web')) { + return new ApiErrorResponse('You are not allowed to enable shows.', status: Response::HTTP_FORBIDDEN); + } + + $show = new Show(); + $show->title = $request->title; + $show->body = $request->body; + $show->start_date = $request->start_date; + $show->end_date = $request->end_date; + $show->is_live = $request->live; + $show->enabled = $request->enabled; + + if (!$show->save()) { + return new ApiErrorResponse('Something went wrong.'); + } + + try { + $show->moderators()->sync( + collect($request->moderators)->mapWithKeys(function ($moderator) { + return [$moderator['id'] => ['primary' => $moderator['primary']]]; + }) + ); + } catch (\Exception $e) { + $show->delete(); + return new ApiErrorResponse('Something went wrong.', $e); + } + + $show->load([ + "moderators" => function ($query) { + $query->withPivot('primary'); + } + ]); + + return new ShowResource($show->makeHidden('primary_moderator')); } /** * Display the specified resource. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\Show $show + * @return \App\Http\Resources\ShowResource | \App\Http\Responses\ApiErrorResponse */ - public function show(Show $show) + public function show(Request $request, Show $show) { - // + $IS_USER_LOGGED_IN = $request->user('sanctum') !== null; + /** + * If the show is disabled and the user is not logged in, + * it should return a 404 error. + */ + if (!$IS_USER_LOGGED_IN) { + if ($show->enabled == false) { + throw new NotFoundHttpException('No query results for model [App\\Models\\Show] ' . $show->id, code: Response::HTTP_NOT_FOUND, headers: ['Accept' => 'application/json', 'Content-Type' => 'application/json']); + } + } + + /** + * If the show is disabled and the user is logged in, + * it should return a 404 error if the user is not a moderator of the show + * and doesn't have the permission to view disabled shows of others. + */ + if ($IS_USER_LOGGED_IN) { + /** @var \App\Models\User $user */ + $user = $request->user('sanctum'); + $isPrimaryModerator = $show->moderators()->where('moderator_id', '=', $user->id)->where('primary', '=', true)->exists(); + if (!$isPrimaryModerator) { + if ($show->enabled == false && !$user->checkPermissionTo(ShowsPermissions::CAN_VIEW_DISABLED_SHOWS_OTHERS, 'web')) { + throw new NotFoundHttpException('No query results for model [App\\Models\\Show] ' . $show->id, code: Response::HTTP_NOT_FOUND, headers: ['Accept' => 'application/json', 'Content-Type' => 'application/json']); + } + } + } + + if (!$IS_USER_LOGGED_IN) { + $show->makeHidden('locked_by'); + } + + $show->load([ + "moderators" => function ($query) { + $query->withPivot('primary'); + } + ]); + + return new ShowResource($show); } /** * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\Show $show + * @return \App\Http\Resources\ShowResource | \App\Http\Responses\ApiErrorResponse */ public function update(Request $request, Show $show) { - // + $request->validate([ + 'title' => 'required|string', + 'body' => 'sometimes|string', + 'start_date' => 'required|date|before:end_date', + 'end_date' => 'required|date|after:start_date', + 'live' => 'required|boolean', + 'enabled' => 'required|boolean', + 'moderators' => 'required|array', + 'moderators.*' => 'required|array', + 'moderators.*.id' => 'required|integer|distinct|exists:users,id', + 'moderators.*.primary' => 'required|boolean', + ]); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + // Check if the user is the primary moderator of the show. + $isPrimaryModerator = $show->moderators()->where('moderator_id', '=', $user->id)->where('primary', '=', true)->exists(); + + /** + * If the show is disabled and the user is logged in, + * it should return a 404 error if the user is not a moderator of the show + * and doesn't have the permission to view disabled shows of others. + */ + if (!$isPrimaryModerator && $show->enabled === false && !$user->checkPermissionTo(ShowsPermissions::CAN_VIEW_DISABLED_SHOWS_OTHERS)) { + throw new NotFoundHttpException( + 'No query results for model [App\\Models\\Show] ' . $show->id, + code: Response::HTTP_NOT_FOUND, + headers: ['Accept' => 'application/json', 'Content-Type' => 'application/json'] + ); + } + + /** + * Check if the show is currently locked by another user. + */ + if ($show->locked_by !== null && $show->locked_by !== $user->id) { + return new ApiErrorResponse('The show is currently locked by another user.', status: Response::HTTP_LOCKED); + } + + /** + * If the user is not the primary moderator of the show, and doesn't have the permission to update shows of others, + * it should return a 403 error. + */ + if (!$isPrimaryModerator && !$user->checkPermissionTo(ShowsPermissions::CAN_UPDATE_SHOWS_OTHERS, 'web')) { + return new ApiErrorResponse('You are not allowed to update shows of others.', status: Response::HTTP_FORBIDDEN); + } + + /** + * Check if the show collides with another show. + */ + if (!$this->_checkForOtherShow($request->start_date, $request->end_date, $show->id)) { + return new ApiErrorResponse('There is already a show scheduled for this time.', status: Response::HTTP_BAD_REQUEST); + } + + /** + * Get primary moderators. + */ + $primaryModerator = collect($request->moderators)->filter(function ($moderator) { + return $moderator['primary'] === true; + }); + + /** + * A show must have exactly one primary moderator. + */ + if ($primaryModerator->count() !== 1) { + return new ApiErrorResponse('There must be exactly one primary moderator.', status: Response::HTTP_BAD_REQUEST); + } + + // Get the new primary moderator. + $newPrimaryModerator = User::find($primaryModerator->first()['id']); + + /** + * Check if the primary moderator is the same as the old primary moderator, + * and if not, check if the user has the permission to update shows of others. + */ + if ($newPrimaryModerator->id !== $show->primary_moderator->first()->id) { + if (!$user->checkPermissionTo(ShowsPermissions::CAN_UPDATE_SHOWS_OTHERS, 'web')) { + return new ApiErrorResponse('You are not allowed to update shows of others.', status: Response::HTTP_FORBIDDEN); + } + } + + /** + * Check if the new primary moderator has the permission to be a primary moderator. + */ + if (!$newPrimaryModerator->checkPermissionTo(ShowsPermissions::CAN_BE_PRIMARY_MODERATOR)) { + return new ApiErrorResponse('The new primary moderator, "'. $newPrimaryModerator->name .'" does not have the permission to be a primary moderator.', status: Response::HTTP_BAD_REQUEST); + } + + // Get the non-primary moderators. + $nonPrimaryModerators = collect($request->moderators)->filter(function ($moderator) { + return $moderator['primary'] === false; + }); + + /** + * Check if there are any moderators that are not primary moderators. + * If there are, check if the user has changed the moderators. + * If the user has changed the moderators, check if the user has the permission to add non-primary moderators. + */ + if ($nonPrimaryModerators->count() > 0) { + // Check if the user has changed the moderators itself. + $oldNonPrimaryModerators = $show->moderators()->where('primary', '=', false)->get(); + $oldNonPrimaryModeratorsIds = $oldNonPrimaryModerators->pluck('id')->toArray(); + $newNonPrimaryModeratorsIds = collect($nonPrimaryModerators)->pluck('id')->toArray(); + if ($oldNonPrimaryModeratorsIds !== $newNonPrimaryModeratorsIds) { + if (!$user->checkPermissionTo(ShowsPermissions::CAN_ADD_MODERATORS, 'web')) { + return new ApiErrorResponse('You are not allowed to add non-primary moderators.', status: Response::HTTP_FORBIDDEN); + } + /** + * Check if the non-primary moderators have the permission to be a moderator. + */ + foreach ($nonPrimaryModerators as $nonPrimaryModerator) { + $nonPrimaryModerator = User::find($nonPrimaryModerator['id']); + if (!$nonPrimaryModerator->checkPermissionTo(ShowsPermissions::CAN_BE_MODERATOR)) { + return new ApiErrorResponse('"' . $nonPrimaryModerator->name. '" does not have the permission to be a non-primary moderator.', status: Response::HTTP_BAD_REQUEST); + } + } + } + } + + /** + * Check if the is_live field was changed, and if so, check if the user has the permission to change the live status. + */ + if ($show->is_live !== $request->is_live) { + if (!$user->checkPermissionTo(ShowsPermissions::CAN_SET_LIVE_SHOWS, 'web')) { + return new ApiErrorResponse('You are not allowed to change the live status of shows.', status: Response::HTTP_FORBIDDEN); + } + } + + /** + * Check if the enabled field was changed, and if so, check if the user has the permission to enable or disable shows. + */ + if ($show->enabled !== $request->enabled) { + if (!$user->checkPermissionTo(ShowsPermissions::CAN_ENABLE_SHOWS, 'web')) { + return new ApiErrorResponse('You are not allowed to enable or disable shows.', status: Response::HTTP_FORBIDDEN); + } + } + + $show->title = $request->title; + $show->body = $request->body; + $show->start_date = $request->start_date; + $show->end_date = $request->end_date; + $show->is_live = $request->is_live; + $show->enabled = $request->enabled; + + $show->moderators()->sync( + collect($request->moderators)->mapWithKeys(function ($moderator) { + return [$moderator['id'] => ['primary' => $moderator['primary']]]; + }) + ); + + if ($show->save()) { + return new ShowResource($show->makeHidden('primary_moderator')); + } else { + return new ApiErrorResponse('Something went wrong.'); + } } /** * Remove the specified resource from storage. + * + * @param \App\Models\Show $show + * @return \App\Http\Responses\ApiErrorResponse | \App\Http\Responses\ApiSuccessResponse */ public function destroy(Show $show) { - // + /** @var \App\Models\User $user */ + $user = auth()->user(); + + /** + * If the show is disabled and the user is not the primary moderator of the show, + * it should return a 404 error. + */ + $isPrimaryModerator = $show->moderators()->where('moderator_id', '=', $user->id)->where('primary', '=', true)->exists(); + if (!$isPrimaryModerator && $show->enabled === false && !$user->checkPermissionTo(ShowsPermissions::CAN_VIEW_DISABLED_SHOWS_OTHERS)) { + throw new NotFoundHttpException( + 'No query results for model [App\\Models\\Show] ' . $show->id, + code: Response::HTTP_NOT_FOUND, + headers: ['Accept' => 'application/json', 'Content-Type' => 'application/json'] + ); + } + + /** + * Check if the show is currently locked by another user. + */ + if ($show->locked_by !== null && $show->locked_by !== $user->id) { + return new ApiErrorResponse('The show is currently locked by another user.', status: Response::HTTP_LOCKED); + } + + /** + * If the user is not the primary moderator of the show, and doesn't have the permission to delete shows of others, + * it should return a 403 error. + */ + if (!$isPrimaryModerator && !$user->checkPermissionTo(ShowsPermissions::CAN_DELETE_SHOWS_OTHERS, 'web')) { + return new ApiErrorResponse('You are not allowed to delete shows of others.', status: Response::HTTP_FORBIDDEN); + } + + /** + * If the user is the primary moderator of the show, or has the permission to delete shows of others, + * it should delete the show. + */ + if ($show->delete()) { + return new ApiSuccessResponse('', Response::HTTP_NO_CONTENT); + } else { + return new ApiErrorResponse('Something went wrong.'); + } } } From e68df6f7765ee83edb7c24c8021be46fdb44abe2 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:38:18 +0100 Subject: [PATCH 15/21] Add generated test Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- .../Http/Controllers/ShowControllerTest.php | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/Feature/Http/Controllers/ShowControllerTest.php b/tests/Feature/Http/Controllers/ShowControllerTest.php index e9c721b2..a7e5a24e 100644 --- a/tests/Feature/Http/Controllers/ShowControllerTest.php +++ b/tests/Feature/Http/Controllers/ShowControllerTest.php @@ -2,19 +2,52 @@ namespace Tests\Feature\Http\Controllers; +use App\Models\Show; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase; class ShowControllerTest extends TestCase { + use RefreshDatabase; + /** - * A basic feature test example. + * Test retrieving a paginated list of shows. + * + * @return void */ - public function test_example(): void + public function testIndex(): void { + // Create some test data + $shows = Show::factory()->count(5)->create(); + + // Make a GET request to the index endpoint $response = $this->get('/api/v1/shows'); + // Assert that the response has a successful status code $response->assertStatus(200); + + // Assert that the response contains the correct number of shows + $response->assertJsonCount(5, 'data'); + + // Assert that the response contains the correct show data + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'title', + 'start_date', + 'end_date', + 'live', + 'enabled', + 'moderators' => [ + '*' => [ + 'id', + 'name', + 'primary', + ], + ], + ], + ], + ]); } } From 7dc9a8c4955c7f200d811dba3058a9b6fec902b2 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Sat, 10 Feb 2024 00:04:41 +0100 Subject: [PATCH 16/21] Update show permissions naming convention Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Permissions/ShowsPermissions.php | 4 ++-- .../migrations/2023_12_28_223745_add_show_permissions.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Permissions/ShowsPermissions.php b/app/Permissions/ShowsPermissions.php index f5d38a6c..9dac6a31 100644 --- a/app/Permissions/ShowsPermissions.php +++ b/app/Permissions/ShowsPermissions.php @@ -10,7 +10,7 @@ class ShowsPermissions { /** Permission for viewing others disabled shows. */ - public const CAN_VIEW_DISABLED_SHOWS_OTHERS = 'shows.view-disabled.others'; + public const CAN_VIEW_DISABLED_SHOWS_OTHERS = 'shows.view.disabled.others'; /** Permission for creating own shows as primary moderator. */ public const CAN_CREATE_SHOWS = 'shows.create'; @@ -31,7 +31,7 @@ class ShowsPermissions public const CAN_DELETE_SHOWS_OTHERS = 'shows.delete.others'; /** Permission to be primary moderator of a show. */ - public const CAN_BE_PRIMARY_MODERATOR = 'shows.be-primary-moderator'; + public const CAN_BE_PRIMARY_MODERATOR = 'shows.be-moderator.primary'; /** Permission for adding non-primary moderators to a show. */ public const CAN_ADD_MODERATORS = 'shows.add-moderators'; diff --git a/database/migrations/2023_12_28_223745_add_show_permissions.php b/database/migrations/2023_12_28_223745_add_show_permissions.php index 4150b473..4c049692 100644 --- a/database/migrations/2023_12_28_223745_add_show_permissions.php +++ b/database/migrations/2023_12_28_223745_add_show_permissions.php @@ -12,7 +12,7 @@ public function up(): void { Permission::create([ - 'name' => 'shows.view-disabled.others', + 'name' => 'shows.view.disabled.others', 'guard_name' => 'web', ]); Permission::create([ @@ -40,7 +40,7 @@ public function up(): void 'guard_name' => 'web', ]); Permission::create([ - 'name' => 'shows.be-primary-moderator', + 'name' => 'shows.be-moderator.primary', 'guard_name' => 'web', ]); Permission::create([ From 6d768a11499a2e32bcb75e776628ec0d3663f701 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:39:23 +0100 Subject: [PATCH 17/21] remove sort check for pagination Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Http/Controllers/ShowController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ShowController.php b/app/Http/Controllers/ShowController.php index 60344641..d3e10514 100644 --- a/app/Http/Controllers/ShowController.php +++ b/app/Http/Controllers/ShowController.php @@ -213,7 +213,7 @@ public function index(Request $request) * it should return the given amount of shows per page. * If "per_page" isn't set, it should return 25 shows per page, maximum 50. */ - if ($request->sort !== null && $request->per_page <= 50) { + if ($request->per_page <= 50) { $res = $shows->paginate($request->per_page); } else { $res = $shows->paginate(25); From 4c9678aac2b2f4a849ed87bf8a3845e3c2f79acd Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:39:33 +0100 Subject: [PATCH 18/21] Add user-related functionality to ShowFactory Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- database/factories/ShowFactory.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/database/factories/ShowFactory.php b/database/factories/ShowFactory.php index 75b2d8bb..9609e3ca 100644 --- a/database/factories/ShowFactory.php +++ b/database/factories/ShowFactory.php @@ -37,6 +37,12 @@ public function definition(): array ]; } + /** + * Add a user to the show and set it as primary. + * Add a random number of users to the show, which are not already added. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ public function withUser() { return $this->afterCreating(function (Show $show) { From 26eeb87b5aa765a48b8d85ba12b9736ad8098a1c Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:39:47 +0100 Subject: [PATCH 19/21] Refactor Show.php to include pivot table primary column in moderators relationship Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- app/Models/Show.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Models/Show.php b/app/Models/Show.php index db1f7a0b..316c69e7 100644 --- a/app/Models/Show.php +++ b/app/Models/Show.php @@ -48,7 +48,10 @@ public function locked_by() public function moderators() { - return $this->belongsToMany(User::class, 'show_moderators', 'show_id', 'moderator_id')->as('moderators')->withTimestamps(); + return $this->belongsToMany(User::class, 'show_moderators', 'show_id', 'moderator_id') + ->withPivot('primary') + ->as('moderators') + ->withTimestamps(); } public function primary_moderator() From 20f00c2fedb848e98f6fea218d558101cec57663 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:41:31 +0100 Subject: [PATCH 20/21] Add test for retrieving paginated list of shows Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- .../Http/Controllers/ShowControllerTest.php | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Http/Controllers/ShowControllerTest.php b/tests/Feature/Http/Controllers/ShowControllerTest.php index a7e5a24e..6d18238e 100644 --- a/tests/Feature/Http/Controllers/ShowControllerTest.php +++ b/tests/Feature/Http/Controllers/ShowControllerTest.php @@ -3,9 +3,14 @@ namespace Tests\Feature\Http\Controllers; use App\Models\Show; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Testing\TestResponse; use Tests\TestCase; +/** + * @coversDefaultClass \App\Http\Controllers\ShowController + */ class ShowControllerTest extends TestCase { use RefreshDatabase; @@ -13,21 +18,31 @@ class ShowControllerTest extends TestCase /** * Test retrieving a paginated list of shows. * + * @covers ::index * @return void */ - public function testIndex(): void + public function test_index(): void { + // Create some test user + User::factory()->count(30)->create(); + // Create some test data - $shows = Show::factory()->count(5)->create(); + $shows = Show::factory([ + 'enabled' => true, + ])->withUser()->count(10)->create(); + + $today = today(); + $inAMonth = today()->addMonth(); + $perPage = 5; // Make a GET request to the index endpoint - $response = $this->get('/api/v1/shows'); + $response = $this->getJson('/api/v1/shows?start_date=' . $today . '&end_date=' . $inAMonth . '&per_page=' . $perPage); // Assert that the response has a successful status code $response->assertStatus(200); // Assert that the response contains the correct number of shows - $response->assertJsonCount(5, 'data'); + $response->assertJsonCount($perPage, 'data'); // Assert that the response contains the correct show data $response->assertJsonStructure([ @@ -37,7 +52,7 @@ public function testIndex(): void 'title', 'start_date', 'end_date', - 'live', + 'is_live', 'enabled', 'moderators' => [ '*' => [ @@ -49,5 +64,18 @@ public function testIndex(): void ], ], ]); + + // Sort the shows by start date and take the first 5 + $shows = $shows->sortBy('start_date')->values()->take($perPage); + + // Assert that the response contains the correct show data + $response->assertJsonFragment([ + 'id' => $shows->first()->id, + 'title' => $shows->first()->title, + 'start_date' => $shows->first()->start_date, + 'end_date' => $shows->first()->end_date, + 'is_live' => $shows->first()->is_live, + 'enabled' => $shows->first()->enabled, + ]); } } From 0d4cd74aa10006912a7bdf82900586c0fae9d5e7 Mon Sep 17 00:00:00 2001 From: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> Date: Sat, 10 Feb 2024 02:00:53 +0100 Subject: [PATCH 21/21] Refactor show sorting and filtering in ShowControllerTest.php Signed-off-by: Valentin Sickert <17144397+Lapotor@users.noreply.github.com> --- .../Http/Controllers/ShowControllerTest.php | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/Feature/Http/Controllers/ShowControllerTest.php b/tests/Feature/Http/Controllers/ShowControllerTest.php index 6d18238e..25f6a535 100644 --- a/tests/Feature/Http/Controllers/ShowControllerTest.php +++ b/tests/Feature/Http/Controllers/ShowControllerTest.php @@ -65,17 +65,24 @@ public function test_index(): void ], ]); - // Sort the shows by start date and take the first 5 - $shows = $shows->sortBy('start_date')->values()->take($perPage); + $sortedShows = $shows->sortBy('start_date'); + + $sortedShows->values()->all(); + + // remove shows where the end or start date is not within the range + $sortedShows = $sortedShows->filter(function ($show) use ($today, $inAMonth) { + return $show->start_date->isBetween($today, $inAMonth) || $show->end_date->isBetween($today, $inAMonth); + }); + // Assert that the response contains the correct show data $response->assertJsonFragment([ - 'id' => $shows->first()->id, - 'title' => $shows->first()->title, - 'start_date' => $shows->first()->start_date, - 'end_date' => $shows->first()->end_date, - 'is_live' => $shows->first()->is_live, - 'enabled' => $shows->first()->enabled, + 'id' => $sortedShows->first()->id, + 'title' => $sortedShows->first()->title, + 'start_date' => $sortedShows->first()->start_date, + 'end_date' => $sortedShows->first()->end_date, + 'is_live' => $sortedShows->first()->is_live, + 'enabled' => $sortedShows->first()->enabled, ]); } }