diff --git a/.github/assets/swagger.yml b/.github/assets/swagger.yml index 24240ae4..504c8f82 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: - maximum: 30 type: integer + maximum: 50 + default: 25 + - name: page + in: query + description: Page to load. + schema: + type: integer + default: 1 responses: "200": description: OK 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..d3e10514 --- /dev/null +++ b/app/Http/Controllers/ShowController.php @@ -0,0 +1,632 @@ +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(Request $request) + { + static $SORT_OPTIONS = [ + 'id', + 'id:asc', + 'id:desc', + 'start_date', + 'start_date:asc', + 'start_date:desc', + 'end_date', + 'end_date:asc', + 'end_date:desc', + ]; + + $request->validate([ + 'start_date' => ['required', 'date', 'before:end_date'], + 'end_date' => ['required', 'date', 'after:start_date'], + 'live' => 'boolean', + 'moderator' => ['array'], + 'moderator.*' => ['integer', 'distinct', 'exists:users,id'], + 'primary' => ['boolean', 'exclude_without:moderator'], + 'sort' => ['string', 'in:' . implode(',', $SORT_OPTIONS)], + 'per_page' => 'integer', + + ]); + + $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) { + $query->withPivot('primary'); + } + ]); + + if ($user === null) { + if ($request->start_date < today() ) { + return new ApiErrorResponse('Start date must be greater than today.', status: Response::HTTP_BAD_REQUEST); + } + if ($request->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 ($request) { + $query->whereBetween('start_date', + [ + $request->start_date, + $request->end_date, + ]); + $query->orWhereBetween('end_date', + [ + $request->start_date, + $request->end_date, + ]); + }); + + /** + * Hide shows that are not enabled and the primary moderator is not the current user. + */ + 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); + }); + }); + }); + } + + /** + * 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 ($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 ($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', '=', $request->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 ($request->sort !== null) { + $sort = explode(':', $request->sort); + if (in_array($request->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 ($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', + ]); + + // Get the primary moderator. + $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); + } + + /** @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; + }); + + /** + * 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(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.'); + } + } +} diff --git a/app/Http/Resources/ShowResource.php b/app/Http/Resources/ShowResource.php new file mode 100644 index 00000000..1befb503 --- /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 === 1, + ]; + }); + + return $show; + } +} diff --git a/app/Models/Show.php b/app/Models/Show.php new file mode 100644 index 00000000..316c69e7 --- /dev/null +++ b/app/Models/Show.php @@ -0,0 +1,61 @@ + + */ + protected $fillable = [ + 'title', + 'body', + 'enabled', + 'is_live', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'start_date' => 'datetime', + 'end_date' => 'datetime', + 'enabled' => 'boolean', + 'is_live' => 'boolean', + ]; + + 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') + ->withPivot('primary') + ->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/app/Permissions/ShowsPermissions.php b/app/Permissions/ShowsPermissions.php new file mode 100644 index 00000000..9dac6a31 --- /dev/null +++ b/app/Permissions/ShowsPermissions.php @@ -0,0 +1,47 @@ + + */ +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]) + ]; + } + + /** + * 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) { + // 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]); + }); + } +} 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'); + } +}; 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..4c049692 --- /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-moderator.primary', + '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(); + } +}; 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/routes/api.php b/routes/api.php index 2ea2bda2..94c50638 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,5 +24,6 @@ require __DIR__ . '/api/v1/auth.php'; require __DIR__ . '/api/v1/user.php'; require __DIR__ . '/api/v1/role.php'; + require __DIR__ . '/api/v1/show.php'; require __DIR__ . '/api/v1/permission.php'; }); diff --git a/routes/api/v1/show.php b/routes/api/v1/show.php new file mode 100644 index 00000000..2b9f88e1 --- /dev/null +++ b/routes/api/v1/show.php @@ -0,0 +1,17 @@ +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'); + +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'); +}); + diff --git a/tests/Feature/Http/Controllers/ShowControllerTest.php b/tests/Feature/Http/Controllers/ShowControllerTest.php new file mode 100644 index 00000000..25f6a535 --- /dev/null +++ b/tests/Feature/Http/Controllers/ShowControllerTest.php @@ -0,0 +1,88 @@ +count(30)->create(); + + // Create some test data + $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->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($perPage, 'data'); + + // Assert that the response contains the correct show data + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'title', + 'start_date', + 'end_date', + 'is_live', + 'enabled', + 'moderators' => [ + '*' => [ + 'id', + 'name', + 'primary', + ], + ], + ], + ], + ]); + + $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' => $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, + ]); + } +}