#470 feat: add featured theme model (#574)

* feat: add featured theme model

* style: fix StyleCI findings
This commit is contained in:
paranarimasu
2023-05-14 02:04:21 -05:00
committed by GitHub
parent 088ea287c5
commit d168afd4e7
36 changed files with 3405 additions and 0 deletions
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\FeaturedTheme;
use App\Events\Base\Admin\AdminCreatedEvent;
use App\Models\Admin\FeaturedTheme;
/**
* Class FeaturedThemeCreated.
*
* @extends AdminCreatedEvent<FeaturedTheme>
*/
class FeaturedThemeCreated extends AdminCreatedEvent
{
/**
* Create a new event instance.
*
* @param FeaturedTheme $featuredTheme
*/
public function __construct(FeaturedTheme $featuredTheme)
{
parent::__construct($featuredTheme);
}
/**
* Get the model that has fired this event.
*
* @return FeaturedTheme
*/
public function getModel(): FeaturedTheme
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Featured Theme '**{$this->getModel()->getName()}**' has been created.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\FeaturedTheme;
use App\Events\Base\Admin\AdminDeletedEvent;
use App\Models\Admin\FeaturedTheme;
/**
* Class FeaturedThemeDeleted.
*
* @extends AdminDeletedEvent<FeaturedTheme>
*/
class FeaturedThemeDeleted extends AdminDeletedEvent
{
/**
* Create a new event instance.
*
* @param FeaturedTheme $featuredTheme
*/
public function __construct(FeaturedTheme $featuredTheme)
{
parent::__construct($featuredTheme);
}
/**
* Get the model that has fired this event.
*
* @return FeaturedTheme
*/
public function getModel(): FeaturedTheme
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Featured Theme '**{$this->getModel()->getName()}**' has been deleted.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\FeaturedTheme;
use App\Events\Base\Admin\AdminRestoredEvent;
use App\Models\Admin\FeaturedTheme;
/**
* Class FeaturedThemeRestored.
*
* @extends AdminRestoredEvent<FeaturedTheme>
*/
class FeaturedThemeRestored extends AdminRestoredEvent
{
/**
* Create a new event instance.
*
* @param FeaturedTheme $featuredTheme
*/
public function __construct(FeaturedTheme $featuredTheme)
{
parent::__construct($featuredTheme);
}
/**
* Get the model that has fired this event.
*
* @return FeaturedTheme
*/
public function getModel(): FeaturedTheme
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Featured Theme '**{$this->getModel()->getName()}**' has been restored.";
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\FeaturedTheme;
use App\Events\Base\Admin\AdminUpdatedEvent;
use App\Models\Admin\FeaturedTheme;
/**
* Class FeaturedThemeUpdated.
*
* @extends AdminUpdatedEvent<FeaturedTheme>
*/
class FeaturedThemeUpdated extends AdminUpdatedEvent
{
/**
* Create a new event instance.
*
* @param FeaturedTheme $featuredTheme
*/
public function __construct(FeaturedTheme $featuredTheme)
{
parent::__construct($featuredTheme);
$this->initializeEmbedFields($featuredTheme);
}
/**
* Get the model that has fired this event.
*
* @return FeaturedTheme
*/
public function getModel(): FeaturedTheme
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Featured Theme '**{$this->getModel()->getName()}**' has been updated.";
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\FeaturedTheme;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Http\Api\Field\DateField;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
/**
* Class FeaturedThemeEndAtField.
*/
class FeaturedThemeEndAtField extends DateField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FeaturedTheme::ATTRIBUTE_END_AT);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
Str::of('date_format:')
->append(implode(',', AllowedDateFormat::getValues()))
->__toString(),
Str::of('after:')
->append($this->resolveStartAt($request))
->__toString(),
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
Str::of('date_format:')
->append(implode(',', AllowedDateFormat::getValues()))
->__toString(),
Str::of('after:')
->append($this->resolveStartAt($request))
->__toString(),
];
}
/**
* Get dependent start_at field.
*
* @param Request $request
* @return string|null
*/
private function resolveStartAt(Request $request): ?string
{
if ($request->has(FeaturedTheme::ATTRIBUTE_START_AT)) {
return $request->get(FeaturedTheme::ATTRIBUTE_START_AT);
}
/** @var FeaturedTheme|null $featuredTheme */
$featuredTheme = $request->route('featuredtheme');
return $featuredTheme?->start_at?->format(AllowedDateFormat::YMDHISU);
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\FeaturedTheme;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\FeaturedTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class FeaturedThemeEntryIdField.
*/
class FeaturedThemeEntryIdField extends Field implements CreatableField, SelectableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FeaturedTheme::ATTRIBUTE_ENTRY);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
$videoId = $this->resolveVideoId($request);
return [
'sometimes',
'required',
'integer',
Rule::exists(AnimeThemeEntry::TABLE, AnimeThemeEntry::ATTRIBUTE_ID),
Rule::when(
! empty($videoId),
[
Rule::exists(AnimeThemeEntryVideo::TABLE, AnimeThemeEntryVideo::ATTRIBUTE_ENTRY)
->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, $this->resolveVideoId($request)),
]
),
];
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match entry relation.
return true;
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
$videoId = $this->resolveVideoId($request);
return [
'sometimes',
'required',
'integer',
Rule::exists(AnimeThemeEntry::TABLE, AnimeThemeEntry::ATTRIBUTE_ID),
Rule::when(
! empty($videoId),
[
Rule::exists(AnimeThemeEntryVideo::TABLE, AnimeThemeEntryVideo::ATTRIBUTE_ENTRY)
->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, $this->resolveVideoId($request)),
]
),
];
}
/**
* Get dependent video_id field.
*
* @param Request $request
* @return mixed
*/
private function resolveVideoId(Request $request): mixed
{
if ($request->has(FeaturedTheme::ATTRIBUTE_VIDEO)) {
return $request->get(FeaturedTheme::ATTRIBUTE_VIDEO);
}
/** @var FeaturedTheme|null $featuredTheme */
$featuredTheme = $request->route('featuredtheme');
return $featuredTheme?->video_id;
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\FeaturedTheme;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Http\Api\Field\DateField;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
/**
* Class FeaturedThemeStartAtField.
*/
class FeaturedThemeStartAtField extends DateField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FeaturedTheme::ATTRIBUTE_START_AT);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
Str::of('date_format:')
->append(implode(',', AllowedDateFormat::getValues()))
->__toString(),
Str::of('before:')
->append($this->resolveEndAt($request))
->__toString(),
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
Str::of('date_format:')
->append(implode(',', AllowedDateFormat::getValues()))
->__toString(),
Str::of('before:')
->append($this->resolveEndAt($request))
->__toString(),
];
}
/**
* Get dependent end_at field.
*
* @param Request $request
* @return string|null
*/
private function resolveEndAt(Request $request): ?string
{
if ($request->has(FeaturedTheme::ATTRIBUTE_END_AT)) {
return $request->get(FeaturedTheme::ATTRIBUTE_END_AT);
}
/** @var FeaturedTheme|null $featuredTheme */
$featuredTheme = $request->route('featuredtheme');
return $featuredTheme?->end_at?->format(AllowedDateFormat::YMDHISU);
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\FeaturedTheme;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class FeaturedThemeUserIdField.
*/
class FeaturedThemeUserIdField extends Field implements CreatableField, SelectableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FeaturedTheme::ATTRIBUTE_USER);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'sometimes',
'required',
'integer',
Rule::exists(User::TABLE, User::ATTRIBUTE_ID),
];
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match user relation.
return true;
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
'integer',
Rule::exists(User::TABLE, User::ATTRIBUTE_ID),
];
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\FeaturedTheme;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\FeaturedTheme;
use App\Models\Wiki\Video;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class FeaturedThemeVideoIdField.
*/
class FeaturedThemeVideoIdField extends Field implements CreatableField, SelectableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FeaturedTheme::ATTRIBUTE_VIDEO);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
$entryId = $this->resolveEntryId($request);
return [
'sometimes',
'required',
'integer',
Rule::exists(Video::TABLE, Video::ATTRIBUTE_ID),
Rule::when(
! empty($entryId),
[
Rule::exists(AnimeThemeEntryVideo::TABLE, AnimeThemeEntryVideo::ATTRIBUTE_VIDEO)
->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, $this->resolveEntryId($request)),
]
),
];
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match video relation.
return true;
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
$entryId = $this->resolveEntryId($request);
return [
'sometimes',
'required',
'integer',
Rule::exists(Video::TABLE, Video::ATTRIBUTE_ID),
Rule::when(
! empty($entryId),
[
Rule::exists(AnimeThemeEntryVideo::TABLE, AnimeThemeEntryVideo::ATTRIBUTE_VIDEO)
->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, $this->resolveEntryId($request)),
]
),
];
}
/**
* Get dependent entry_id field.
*
* @param Request $request
* @return mixed
*/
private function resolveEntryId(Request $request): mixed
{
if ($request->has(FeaturedTheme::ATTRIBUTE_ENTRY)) {
return $request->get(FeaturedTheme::ATTRIBUTE_ENTRY);
}
/** @var FeaturedTheme|null $featuredTheme */
$featuredTheme = $request->route('featuredtheme');
return $featuredTheme?->entry_id;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\Admin;
use App\Http\Api\Field\Admin\FeaturedTheme\FeaturedThemeEndAtField;
use App\Http\Api\Field\Admin\FeaturedTheme\FeaturedThemeEntryIdField;
use App\Http\Api\Field\Admin\FeaturedTheme\FeaturedThemeStartAtField;
use App\Http\Api\Field\Admin\FeaturedTheme\FeaturedThemeUserIdField;
use App\Http\Api\Field\Admin\FeaturedTheme\FeaturedThemeVideoIdField;
use App\Http\Api\Field\Base\IdField;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\Auth\UserSchema;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\Anime\Theme\EntrySchema;
use App\Http\Api\Schema\Wiki\AnimeSchema;
use App\Http\Api\Schema\Wiki\ArtistSchema;
use App\Http\Api\Schema\Wiki\ImageSchema;
use App\Http\Api\Schema\Wiki\SongSchema;
use App\Http\Api\Schema\Wiki\VideoSchema;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Models\Admin\FeaturedTheme;
/**
* Class FeaturedThemeSchema.
*/
class FeaturedThemeSchema extends EloquentSchema
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return FeaturedThemeResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [
new AllowedInclude(new AnimeSchema(), FeaturedTheme::RELATION_ANIME),
new AllowedInclude(new ArtistSchema(), FeaturedTheme::RELATION_ARTISTS),
new AllowedInclude(new EntrySchema(), FeaturedTheme::RELATION_ENTRY),
new AllowedInclude(new ImageSchema(), FeaturedTheme::RELATION_IMAGES),
new AllowedInclude(new SongSchema(), FeaturedTheme::RELATION_SONG),
new AllowedInclude(new UserSchema(), FeaturedTheme::RELATION_USER),
new AllowedInclude(new VideoSchema(), FeaturedTheme::RELATION_VIDEO),
];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*/
public function fields(): array
{
return array_merge(
parent::fields(),
[
new IdField($this, FeaturedTheme::ATTRIBUTE_ID),
new FeaturedThemeStartAtField($this),
new FeaturedThemeEndAtField($this),
new FeaturedThemeEntryIdField($this),
new FeaturedThemeUserIdField($this),
new FeaturedThemeVideoIdField($this),
],
);
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Http\Api\ShowAction;
use App\Contracts\Http\Api\InteractsWithSchema;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Admin\FeaturedThemeSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Support\Facades\Date;
/**
* Class CurrentFeaturedThemeController.
*/
class CurrentFeaturedThemeController extends Controller implements InteractsWithSchema
{
/**
* Display the specified resource.
*
* @param ShowRequest $request
* @param ShowAction $action
* @return FeaturedThemeResource
*/
public function show(ShowRequest $request, ShowAction $action): FeaturedThemeResource
{
$query = new Query($request->validated());
$featuredtheme = FeaturedTheme::query()
->whereNotNull(FeaturedTheme::ATTRIBUTE_START_AT)
->whereNotNull(FeaturedTheme::ATTRIBUTE_END_AT)
->whereDate(FeaturedTheme::ATTRIBUTE_START_AT, ComparisonOperator::LTE, Date::now())
->whereDate(FeaturedTheme::ATTRIBUTE_END_AT, ComparisonOperator::GT, Date::now())
->firstOrFail();
$show = $action->show($featuredtheme, $query, $request->schema());
return new FeaturedThemeResource($show, $query);
}
/**
* Get the underlying schema.
*
* @return Schema
*/
public function schema(): Schema
{
return new FeaturedThemeSchema();
}
}
@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Http\Api\DestroyAction;
use App\Actions\Http\Api\ForceDeleteAction;
use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\RestoreAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Http\Api\UpdateAction;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\StoreRequest;
use App\Http\Requests\Api\UpdateRequest;
use App\Http\Resources\Admin\Collection\FeaturedThemeCollection;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Date;
/**
* Class FeaturedThemeController.
*/
class FeaturedThemeController extends BaseController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
parent::__construct(FeaturedTheme::class, 'featuredtheme');
}
/**
* Display a listing of the resource.
*
* @param IndexRequest $request
* @param IndexAction $action
* @return FeaturedThemeCollection
*/
public function index(IndexRequest $request, IndexAction $action): FeaturedThemeCollection
{
$query = new Query($request->validated());
$builder = FeaturedTheme::query()
->whereNotNull(FeaturedTheme::ATTRIBUTE_START_AT)
->whereDate(FeaturedTheme::ATTRIBUTE_START_AT, ComparisonOperator::LTE, Date::now());
$featuredthemes = $action->index($builder, $query, $request->schema());
return new FeaturedThemeCollection($featuredthemes, $query);
}
/**
* Store a newly created resource.
*
* @param StoreRequest $request
* @param StoreAction $action
* @return FeaturedThemeResource
*/
public function store(StoreRequest $request, StoreAction $action): FeaturedThemeResource
{
$featuredtheme = $action->store(FeaturedTheme::query(), $request->validated());
return new FeaturedThemeResource($featuredtheme, new Query());
}
/**
* Display the specified resource.
*
* @param ShowRequest $request
* @param FeaturedTheme $featuredtheme
* @param ShowAction $action
* @return FeaturedThemeResource
*/
public function show(ShowRequest $request, FeaturedTheme $featuredtheme, ShowAction $action): FeaturedThemeResource
{
$query = new Query($request->validated());
$show = $action->show($featuredtheme, $query, $request->schema());
return new FeaturedThemeResource($show, $query);
}
/**
* Update the specified resource.
*
* @param UpdateRequest $request
* @param FeaturedTheme $featuredtheme
* @param UpdateAction $action
* @return FeaturedThemeResource
*/
public function update(UpdateRequest $request, FeaturedTheme $featuredtheme, UpdateAction $action): FeaturedThemeResource
{
$updated = $action->update($featuredtheme, $request->validated());
return new FeaturedThemeResource($updated, new Query());
}
/**
* Remove the specified resource.
*
* @param FeaturedTheme $featuredtheme
* @param DestroyAction $action
* @return FeaturedThemeResource
*/
public function destroy(FeaturedTheme $featuredtheme, DestroyAction $action): FeaturedThemeResource
{
$deleted = $action->destroy($featuredtheme);
return new FeaturedThemeResource($deleted, new Query());
}
/**
* Restore the specified resource.
*
* @param FeaturedTheme $featuredtheme
* @param RestoreAction $action
* @return FeaturedThemeResource
*/
public function restore(FeaturedTheme $featuredtheme, RestoreAction $action): FeaturedThemeResource
{
$restored = $action->restore($featuredtheme);
return new FeaturedThemeResource($restored, new Query());
}
/**
* Hard-delete the specified resource.
*
* @param FeaturedTheme $featuredtheme
* @param ForceDeleteAction $action
* @return JsonResponse
*/
public function forceDelete(FeaturedTheme $featuredtheme, ForceDeleteAction $action): JsonResponse
{
$message = $action->forceDelete($featuredtheme);
return new JsonResponse([
'message' => $message,
]);
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Admin\Collection;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Http\Resources\BaseCollection;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Http\Request;
/**
* Class FeaturedThemeCollection.
*/
class FeaturedThemeCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'featuredthemes';
/**
* Transform the resource collection into an array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray(Request $request): array
{
return $this->collection->map(
fn (FeaturedTheme $featuredTheme) => new FeaturedThemeResource($featuredTheme, $this->query)
)->all();
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Admin\Resource;
use App\Http\Api\Schema\Admin\FeaturedThemeSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
/**
* Class FeaturedThemeResource.
*/
class FeaturedThemeResource extends BaseResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'featuredtheme';
/**
* Get the resource schema.
*
* @return Schema
*/
protected function schema(): Schema
{
return new FeaturedThemeSchema();
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace App\Models\Admin;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Events\Admin\FeaturedTheme\FeaturedThemeCreated;
use App\Events\Admin\FeaturedTheme\FeaturedThemeDeleted;
use App\Events\Admin\FeaturedTheme\FeaturedThemeRestored;
use App\Events\Admin\FeaturedTheme\FeaturedThemeUpdated;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use Database\Factories\Admin\FeaturedThemeFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Laravel\Nova\Actions\Actionable;
/**
* Class FeaturedTheme.
*
* @property Carbon|null $end_at
* @property AnimeThemeEntry|null $entry
* @property int $entry_id
* @property int $feature_id
* @property Carbon|null $start_at
* @property User|null $user
* @property int|null $user_id
* @property Video|null $video
* @property int|null $video_id
*
* @method static FeaturedThemeFactory factory(...$parameters)
*/
class FeaturedTheme extends BaseModel
{
use Actionable;
final public const TABLE = 'featured_themes';
final public const ATTRIBUTE_END_AT = 'end_at';
final public const ATTRIBUTE_ENTRY = 'entry_id';
final public const ATTRIBUTE_ID = 'featured_theme_id';
final public const ATTRIBUTE_START_AT = 'start_at';
final public const ATTRIBUTE_USER = 'user_id';
final public const ATTRIBUTE_VIDEO = 'video_id';
final public const RELATION_ANIME = 'animethemeentry.animetheme.anime';
final public const RELATION_ARTISTS = 'animethemeentry.animetheme.song.artists';
final public const RELATION_ENTRY = 'animethemeentry';
final public const RELATION_IMAGES = 'animethemeentry.animetheme.anime.images';
final public const RELATION_SONG = 'animethemeentry.animetheme.song';
final public const RELATION_USER = 'user';
final public const RELATION_VIDEO = 'video';
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
FeaturedTheme::ATTRIBUTE_END_AT,
FeaturedTheme::ATTRIBUTE_ENTRY,
FeaturedTheme::ATTRIBUTE_START_AT,
FeaturedTheme::ATTRIBUTE_USER,
FeaturedTheme::ATTRIBUTE_VIDEO,
];
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array
*/
protected $dispatchesEvents = [
'created' => FeaturedThemeCreated::class,
'deleted' => FeaturedThemeDeleted::class,
'restored' => FeaturedThemeRestored::class,
'updated' => FeaturedThemeUpdated::class,
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = FeaturedTheme::TABLE;
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = FeaturedTheme::ATTRIBUTE_ID;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
FeaturedTheme::ATTRIBUTE_END_AT => 'datetime',
FeaturedTheme::ATTRIBUTE_START_AT => 'datetime',
];
/**
* Get name.
*
* @return string
*/
public function getName(): string
{
return Str::of($this->start_at->format(AllowedDateFormat::YMD))
->append(' - ')
->append($this->end_at->format(AllowedDateFormat::YMD))
->__toString();
}
/**
* Get the user that recommended the featured theme.
*
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, FeaturedTheme::ATTRIBUTE_USER);
}
/**
* Get the entry for the featured video.
*
* @return BelongsTo
*/
public function animethemeentry(): BelongsTo
{
return $this->belongsTo(AnimeThemeEntry::class, FeaturedTheme::ATTRIBUTE_ENTRY);
}
/**
* Get the video to feature.
*
* @return BelongsTo
*/
public function video(): BelongsTo
{
return $this->belongsTo(Video::class, FeaturedTheme::ATTRIBUTE_VIDEO);
}
}
+245
View File
@@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
namespace App\Nova\Resources\Admin;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Admin\FeaturedTheme as FeaturedThemeModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video as VideoModel;
use App\Nova\Resources\Auth\User;
use App\Nova\Resources\BaseResource;
use App\Nova\Resources\Wiki\Anime\Theme\Entry;
use App\Nova\Resources\Wiki\Video;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Date;
use Laravel\Nova\Fields\FormData;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Panel;
use Laravel\Nova\Query\Search\SearchableRelation;
/**
* Class FeaturedTheme.
*/
class FeaturedTheme extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static string $model = FeaturedThemeModel::class;
/**
* Get the value that should be displayed to represent the resource.
*
* @return string
*/
public function title(): string
{
$featuredTheme = $this->model();
if ($featuredTheme instanceof FeaturedThemeModel) {
return $featuredTheme->getName();
}
return parent::title();
}
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function group(): string
{
return __('nova.resources.group.admin');
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function label(): string
{
return __('nova.resources.label.featured_themes');
}
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function singularLabel(): string
{
return __('nova.resources.singularLabel.featured_theme');
}
/**
* Get the searchable columns for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function searchableColumns(): array
{
return [
new SearchableRelation(FeaturedThemeModel::RELATION_VIDEO, VideoModel::ATTRIBUTE_FILENAME),
];
}
/**
* Indicates if the resource should be globally searchable.
*
* @var bool
*/
public static $globallySearchable = false;
/**
* Determine if this resource uses Laravel Scout.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function usesScout(): bool
{
return false;
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* @param NovaRequest $request
* @param Builder $query
* @return Builder
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function indexQuery(NovaRequest $request, $query): Builder
{
return $query->with([
FeaturedThemeModel::RELATION_ENTRY,
FeaturedThemeModel::RELATION_USER,
FeaturedThemeModel::RELATION_VIDEO,
]);
}
/**
* Get the fields displayed by the resource.
*
* @param NovaRequest $request
* @return array
*
* @throws Exception
*/
public function fields(NovaRequest $request): array
{
return [
ID::make(__('nova.fields.base.id'), FeaturedThemeModel::ATTRIBUTE_ID)
->sortable()
->showOnPreview()
->showWhenPeeking(),
Date::make(__('nova.fields.featured_theme.start_at.name'), FeaturedThemeModel::ATTRIBUTE_START_AT)
->sortable()
->required()
->rules([
'required',
Str::of('date_format:')
->append(implode(',', AllowedDateFormat::getValues()))
->__toString(),
Str::of('before:')
->append(FeaturedThemeModel::ATTRIBUTE_END_AT)
->__toString(),
])
->help(__('nova.fields.featured_theme.start_at.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
Date::make(__('nova.fields.featured_theme.end_at.name'), FeaturedThemeModel::ATTRIBUTE_END_AT)
->sortable()
->required()
->rules([
'required',
Str::of('date_format:')
->append(implode(',', AllowedDateFormat::getValues()))
->__toString(),
Str::of('after:')
->append(FeaturedThemeModel::ATTRIBUTE_START_AT)
->__toString(),
])
->help(__('nova.fields.featured_theme.end_at.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
BelongsTo::make(__('nova.resources.singularLabel.video'), FeaturedThemeModel::RELATION_VIDEO, Video::class)
->sortable()
->filterable()
->searchable()
->nullable()
->showOnPreview()
->dependsOn(
[FeaturedThemeModel::RELATION_ENTRY],
function (BelongsTo $field, NovaRequest $novaRequest, FormData $formData) {
if ($formData->offsetExists(FeaturedThemeModel::RELATION_ENTRY)) {
$field->relatableQueryUsing(function (NovaRequest $relatableRequest, Builder $query) use ($formData) {
return $query->whereHas(VideoModel::RELATION_ANIMETHEMEENTRIES, function (Builder $relationBuilder) use ($formData) {
$relationBuilder->where(AnimeThemeEntry::ATTRIBUTE_ID, $formData->offsetGet(FeaturedThemeModel::RELATION_ENTRY));
});
});
} else {
$field->relatableQueryUsing(null);
}
}
),
BelongsTo::make(__('nova.resources.singularLabel.anime_theme_entry'), FeaturedThemeModel::RELATION_ENTRY, Entry::class)
->sortable()
->filterable()
->searchable()
->nullable()
->showOnPreview()
->dependsOn(
[FeaturedThemeModel::RELATION_VIDEO],
function (BelongsTo $field, NovaRequest $novaRequest, FormData $formData) {
if ($formData->offsetExists(FeaturedThemeModel::RELATION_VIDEO)) {
$field->relatableQueryUsing(function (NovaRequest $relatableRequest, Builder $query) use ($formData) {
return $query->whereHas(AnimeThemeEntry::RELATION_VIDEOS, function (Builder $relationBuilder) use ($formData) {
$relationBuilder->where(VideoModel::ATTRIBUTE_ID, $formData->offsetGet(FeaturedThemeModel::RELATION_VIDEO));
});
});
} else {
$field->relatableQueryUsing(null);
}
}
),
BelongsTo::make(__('nova.resources.singularLabel.user'), FeaturedThemeModel::RELATION_USER, User::class)
->sortable()
->filterable()
->searchable()
->nullable()
->showOnPreview(),
Panel::make(__('nova.fields.base.timestamps'), $this->timestamps())
->collapsable(),
];
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Policies\Admin;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Support\Facades\Date;
use Laravel\Nova\Nova;
/**
* Class FeaturedThemePolicy.
*/
class FeaturedThemePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param User|null $user
* @return bool
*/
public function viewAny(?User $user): bool
{
return Nova::whenServing(
fn (): bool => $user !== null && $user->can(CrudPermission::VIEW()->format(FeaturedTheme::class)),
fn (): bool => true
);
}
/**
* Determine whether the user can view the model.
*
* @param User|null $user
* @param FeaturedTheme $featuredtheme
* @return bool
*/
public function view(?User $user, FeaturedTheme $featuredtheme): bool
{
return Nova::whenServing(
fn (): bool => $user !== null && $user->can(CrudPermission::VIEW()->format(FeaturedTheme::class)),
fn (): bool => $featuredtheme->start_at->isBefore(Date::now())
);
}
/**
* Determine whether the user can create models.
*
* @param User $user
* @return bool
*/
public function create(User $user): bool
{
return $user->can(CrudPermission::CREATE()->format(FeaturedTheme::class));
}
/**
* Determine whether the user can update the model.
*
* @param User $user
* @param FeaturedTheme $featuredtheme
* @return bool
*/
public function update(User $user, FeaturedTheme $featuredtheme): bool
{
return ! $featuredtheme->trashed() && $user->can(CrudPermission::UPDATE()->format(FeaturedTheme::class));
}
/**
* Determine whether the user can delete the model.
*
* @param User $user
* @param FeaturedTheme $featuredtheme
* @return bool
*/
public function delete(User $user, FeaturedTheme $featuredtheme): bool
{
return ! $featuredtheme->trashed() && $user->can(CrudPermission::DELETE()->format(FeaturedTheme::class));
}
/**
* Determine whether the user can restore the model.
*
* @param User $user
* @param FeaturedTheme $featuredtheme
* @return bool
*/
public function restore(User $user, FeaturedTheme $featuredtheme): bool
{
return $featuredtheme->trashed() && $user->can(ExtendedCrudPermission::RESTORE()->format(FeaturedTheme::class));
}
/**
* Determine whether the user can permanently delete the model.
*
* @param User $user
* @return bool
*/
public function forceDelete(User $user): bool
{
return $user->can(ExtendedCrudPermission::FORCE_DELETE()->format(FeaturedTheme::class));
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Database\Factories\Admin;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* Class FeaturedThemeFactory.
*
* @method FeaturedTheme createOne($attributes = [])
* @method FeaturedTheme makeOne($attributes = [])
*
* @extends Factory<FeaturedTheme>
*/
class FeaturedThemeFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<FeaturedTheme>
*/
protected $model = FeaturedTheme::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
return [
FeaturedTheme::ATTRIBUTE_START_AT => fake()->dateTimeBetween()->format(AllowedDateFormat::YMDHISU),
FeaturedTheme::ATTRIBUTE_END_AT => fake()->dateTimeBetween('+1 day', '+30 years')->format(AllowedDateFormat::YMDHISU),
];
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use App\Models\Admin\Feature;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasTable(FeaturedTheme::TABLE)) {
Schema::create(FeaturedTheme::TABLE, function (Blueprint $table) {
$table->id(FeaturedTheme::ATTRIBUTE_ID);
$table->timestamps(6);
$table->softDeletes(BaseModel::ATTRIBUTE_DELETED_AT, 6);
$table->timestamp(FeaturedTheme::ATTRIBUTE_START_AT, 6)->nullable();
$table->timestamp(FeaturedTheme::ATTRIBUTE_END_AT, 6)->nullable();
$table->unsignedBigInteger(FeaturedTheme::ATTRIBUTE_USER)->nullable();
$table->foreign(FeaturedTheme::ATTRIBUTE_USER)->references(User::ATTRIBUTE_ID)->on(User::TABLE)->nullOnDelete();
$table->unsignedBigInteger(FeaturedTheme::ATTRIBUTE_ENTRY)->nullable();
$table->foreign(FeaturedTheme::ATTRIBUTE_ENTRY)->references(AnimeThemeEntry::ATTRIBUTE_ID)->on(AnimeThemeEntry::TABLE)->nullOnDelete();
$table->unsignedBigInteger(FeaturedTheme::ATTRIBUTE_VIDEO)->nullable();
$table->foreign(FeaturedTheme::ATTRIBUTE_VIDEO)->references(Video::ATTRIBUTE_ID)->on(Video::TABLE)->nullOnDelete();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists(FeaturedTheme::TABLE);
}
};
@@ -10,6 +10,7 @@ use App\Enums\Auth\SpecialPermission;
use App\Models\Admin\Announcement;
use App\Models\Admin\Dump;
use App\Models\Admin\Feature;
use App\Models\Admin\FeaturedTheme;
use App\Models\Admin\Setting;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
@@ -51,6 +52,7 @@ class PermissionSeeder extends Seeder
$this->registerResource(Dump::class, ExtendedCrudPermission::getInstances());
$this->registerResource(Setting::class, CrudPermission::getInstances());
$this->registerResource(Feature::class, CrudPermission::getInstances());
$this->registerResource(FeaturedTheme::class, ExtendedCrudPermission::getInstances());
// Auth Resources
$this->registerResource(Permission::class, [CrudPermission::VIEW()]);
@@ -10,6 +10,7 @@ use App\Enums\Auth\SpecialPermission;
use App\Models\Admin\Announcement;
use App\Models\Admin\Dump;
use App\Models\Admin\Feature;
use App\Models\Admin\FeaturedTheme;
use App\Models\Admin\Setting;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
@@ -53,6 +54,7 @@ class AdminSeeder extends RoleSeeder
$this->configureResource($role, Dump::class, ExtendedCrudPermission::getInstances());
$this->configureResource($role, Feature::class, [CrudPermission::VIEW(), CrudPermission::UPDATE()]);
$this->configureResource($role, Setting::class, CrudPermission::getInstances());
$this->configureResource($role, FeaturedTheme::class, ExtendedCrudPermission::getInstances());
// Auth Resources
$this->configureResource($role, Permission::class, [CrudPermission::VIEW()]);
+12
View File
@@ -502,6 +502,16 @@ return [
'name' => 'Value',
],
],
'featured_theme' => [
'end_at' => [
'help' => 'The datetime that the featured theme should stop being featured.',
'name' => 'End At',
],
'start_at' => [
'help' => 'The datetime that the featured theme should start being featured.',
'name' => 'Start At',
],
],
'image' => [
'facet' => [
'help' => 'The page component that the image is intended for. Example: Is this a small cover image or a large cover image?',
@@ -812,6 +822,7 @@ return [
'dumps' => 'Dumps',
'external_resources' => 'External Resources',
'features' => 'Features',
'featured_themes' => 'Featured Themes',
'groups' => 'Groups',
'images' => 'Images',
'members' => 'Members',
@@ -841,6 +852,7 @@ return [
'dump' => 'Dump',
'external_resource' => 'External Resource',
'feature' => 'Feature',
'featured_theme' => 'Featured Theme',
'image' => 'Image',
'page' => 'Page',
'permission' => 'Permission',
+1
View File
@@ -22,6 +22,7 @@ parameters:
- '#Strict comparison using === between class-string<Laravel\\Nova\\Resource> and null will always evaluate to false.#'
- '#Call to an undefined method ProtoneMedia\\LaravelFFMpeg\\Drivers\\PHPFFMpeg::export\(\).#'
- '#Call to an undefined method ProtoneMedia\\LaravelFFMpeg\\Drivers\\PHPFFMpeg::getProcessOutput\(\).#'
- '#Call to an undefined method Database\\Factories.*::trashed\(\).#'
-
message: '#Method App\\Rules\\Wiki\\Submission\\SubmissionRule::#'
path: app/Rules/Wiki/Submission/SubmissionRule.php
+5
View File
@@ -3,8 +3,10 @@
declare(strict_types=1);
use App\Http\Controllers\Api\Admin\AnnouncementController;
use App\Http\Controllers\Api\Admin\CurrentFeaturedThemeController;
use App\Http\Controllers\Api\Admin\DumpController;
use App\Http\Controllers\Api\Admin\FeatureController;
use App\Http\Controllers\Api\Admin\FeaturedThemeController;
use App\Http\Controllers\Api\Auth\User\Me\List\MyPlaylistController;
use App\Http\Controllers\Api\Auth\User\Me\MyController;
use App\Http\Controllers\Api\Billing\BalanceController;
@@ -222,6 +224,9 @@ if (! function_exists('apiPivotResourceUri')) {
// Admin Routes
apiResource('announcement', AnnouncementController::class);
apiResource('dump', DumpController::class);
apiResource('featuredtheme', FeaturedThemeController::class);
Route::get('current/featuredtheme', [CurrentFeaturedThemeController::class, 'show'])
->name('featuredtheme.current.show');
Route::apiResource('feature', FeatureController::class)
->only(['index', 'show', 'update']);
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Events\Admin;
use App\Events\Admin\FeaturedTheme\FeaturedThemeCreated;
use App\Events\Admin\FeaturedTheme\FeaturedThemeDeleted;
use App\Events\Admin\FeaturedTheme\FeaturedThemeRestored;
use App\Events\Admin\FeaturedTheme\FeaturedThemeUpdated;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
/**
* Class FeaturedThemeTest.
*/
class FeaturedThemeTest extends TestCase
{
/**
* When a Featured Theme is created, a FeaturedThemeCreated event shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeCreatedEventDispatched(): void
{
FeaturedTheme::factory()->create();
Event::assertDispatched(FeaturedThemeCreated::class);
}
/**
* When a Featured Theme is deleted, a FeaturedThemeDeleted event shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeDeletedEventDispatched(): void
{
$featuredTheme = FeaturedTheme::factory()->create();
$featuredTheme->delete();
Event::assertDispatched(FeaturedThemeDeleted::class);
}
/**
* When a Featured Theme is restored, a FeaturedThemeRestored event shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeRestoredEventDispatched(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$featuredTheme->restore();
Event::assertDispatched(FeaturedThemeRestored::class);
}
/**
* When a FeaturedTheme is restored, a FeaturedThemeUpdated event shall not be dispatched.
* Note: This is a customization that overrides default framework behavior.
* An updated event is fired on restore.
*
* @return void
*/
public function testFeaturedThemeRestoresQuietly(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$featuredTheme->restore();
Event::assertNotDispatched(FeaturedThemeUpdated::class);
}
/**
* When a Featured Theme is updated, a FeaturedThemeUpdated event shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeUpdatedEventDispatched(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$changes = FeaturedTheme::factory()->makeOne();
$featuredTheme->fill($changes->getAttributes());
$featuredTheme->save();
Event::assertDispatched(FeaturedThemeUpdated::class);
}
/**
* The FeaturedThemeUpdated event shall contain embed fields.
*
* @return void
*/
public function testFeaturedThemeUpdatedEventEmbedFields(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$changes = FeaturedTheme::factory()->makeOne();
$featuredTheme->fill($changes->getAttributes());
$featuredTheme->save();
Event::assertDispatched(FeaturedThemeUpdated::class, function (FeaturedThemeUpdated $event) {
$message = $event->getDiscordMessage();
return ! empty(Arr::get($message->embed, 'fields'));
});
}
}
@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Admin\FeaturedThemeSchema;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Image;
use App\Models\Wiki\Song;
use App\Models\Wiki\Video;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Collection;
use Tests\TestCase;
/**
* Class CurrentFeaturedThemeShowTest.
*/
class CurrentFeaturedThemeShowTest extends TestCase
{
use WithFaker;
/**
* The Current Featured Theme Show Endpoint shall return a Not Found exception if there are no featured themes.
*
* @return void
*/
public function testNotFoundIfNoFeaturedThemes(): void
{
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertNotFound();
}
/**
* The Current Featured Theme Show Endpoint shall return a Not Found exception if the featured theme has no start date.
*
* @return void
*/
public function testNotFoundIfThemeStartAtNull(): void
{
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => null,
]);
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertNotFound();
}
/**
* The Current Featured Theme Show Endpoint shall return a Not Found exception if the featured theme has no end date.
*
* @return void
*/
public function testNotFoundIfThemeEndAtNull(): void
{
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_END_AT => null,
]);
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertNotFound();
}
/**
* The Current Featured Theme Show Endpoint shall return a Not Found exception if the featured theme starts after today.
*
* @return void
*/
public function testNotFoundIfThemeStartAtAfterNow(): void
{
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => $this->faker->dateTimeBetween('+1 day', '+30 years'),
]);
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertNotFound();
}
/**
* The Current Featured Theme Show Endpoint shall return a Not Found exception if the featured theme ends before today.
*
* @return void
*/
public function testNotFoundIfThemeEndAtBeforeNow(): void
{
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_END_AT => $this->faker->dateTimeBetween(),
]);
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertNotFound();
}
/**
* By default, the Current Featured Theme Show Endpoint shall return a Featured Theme Resource.
*
* @return void
*/
public function testDefault(): void
{
Collection::times($this->faker->randomDigitNotNull(), function () {
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => null,
]);
});
Collection::times($this->faker->randomDigitNotNull(), function () {
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_END_AT => null,
]);
});
Collection::times($this->faker->randomDigitNotNull(), function () {
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => $this->faker->dateTimeBetween('+1 day', '+30 years'),
]);
});
Collection::times($this->faker->randomDigitNotNull(), function () {
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_END_AT => $this->faker->dateTimeBetween('-30 years', '-1 day'),
]);
});
$currentTheme = FeaturedTheme::factory()->create();
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($currentTheme, new Query()))
->response()
->getData()
),
true
)
);
}
/**
* The Current Featured Theme Show Endpoint shall return a Not Found exception if the featured theme is soft deleted.
*
* @return void
*/
public function testSoftDelete(): void
{
FeaturedTheme::factory()->trashed()->create();
$response = $this->get(route('api.featuredtheme.current.show'));
$response->assertNotFound();
}
/**
* The Featured Theme Show Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new FeaturedThemeSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
$currentTheme = FeaturedTheme::factory()
->for(
AnimeThemeEntry::factory()
->for(
AnimeTheme::factory()
->for(Anime::factory()->has(Image::factory()->count($this->faker->randomDigitNotNull())))
->for(Song::factory()->has(Artist::factory()->count($this->faker->randomDigitNotNull())))
)
)
->for(Video::factory())
->for(User::factory())
->createOne();
$response = $this->get(route('api.featuredtheme.current.show', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($currentTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Current Featured Theme Show Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new FeaturedThemeSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
FeaturedThemeResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
$currentTheme = FeaturedTheme::factory()->create();
$response = $this->get(route('api.featuredtheme.current.show', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($currentTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Enums\Auth\CrudPermission;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class FeaturedThemeDestroyTest.
*/
class FeaturedThemeDestroyTest extends TestCase
{
/**
* The Featured Theme Destroy Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$response = $this->delete(route('api.featuredtheme.destroy', ['featuredtheme' => $featuredTheme]));
$response->assertUnauthorized();
}
/**
* The Featured Theme Destroy Endpoint shall forbid users without the delete featured theme permission.
*
* @return void
*/
public function testForbidden(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.featuredtheme.destroy', ['featuredtheme' => $featuredTheme]));
$response->assertForbidden();
}
/**
* The Featured Theme Destroy Endpoint shall forbid users from updating a featured theme that is trashed.
*
* @return void
*/
public function testTrashed(): void
{
$featuredTheme = FeaturedTheme::factory()->trashed()->createOne();
$user = User::factory()->withPermissions(CrudPermission::DELETE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.featuredtheme.destroy', ['featuredtheme' => $featuredTheme]));
$response->assertNotFound();
}
/**
* The FeaturedTheme Destroy Endpoint shall delete the featured theme.
*
* @return void
*/
public function testDeleted(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$user = User::factory()->withPermissions(CrudPermission::DELETE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.featuredtheme.destroy', ['featuredtheme' => $featuredTheme]));
$response->assertOk();
static::assertSoftDeleted($featuredTheme);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class FeaturedThemeForceDeleteTest.
*/
class FeaturedThemeForceDeleteTest extends TestCase
{
/**
* The Featured Theme Force Delete Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$response = $this->delete(route('api.featuredtheme.forceDelete', ['featuredtheme' => $featuredTheme]));
$response->assertUnauthorized();
}
/**
* The Featured Theme Force Delete Endpoint shall forbid users without the force delete featured theme permission.
*
* @return void
*/
public function testForbidden(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.featuredtheme.forceDelete', ['featuredtheme' => $featuredTheme]));
$response->assertForbidden();
}
/**
* The Featured Theme Force Delete Endpoint shall force delete the featured theme.
*
* @return void
*/
public function testDeleted(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$user = User::factory()->withPermissions(ExtendedCrudPermission::FORCE_DELETE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.featuredtheme.forceDelete', ['featuredtheme' => $featuredTheme]));
$response->assertOk();
static::assertModelMissing($featuredTheme);
}
}
@@ -0,0 +1,466 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Concerns\Actions\Http\Api\SortsModels;
use App\Contracts\Http\Api\Field\SortableField;
use App\Enums\Http\Api\Filter\TrashedStatus;
use App\Enums\Http\Api\Sort\Direction;
use App\Http\Api\Criteria\Filter\TrashedCriteria;
use App\Http\Api\Criteria\Paging\Criteria;
use App\Http\Api\Criteria\Paging\OffsetCriteria;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\FilterParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Parser\PagingParser;
use App\Http\Api\Parser\SortParser;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Admin\FeaturedThemeSchema;
use App\Http\Api\Sort\Sort;
use App\Http\Resources\Admin\Collection\FeaturedThemeCollection;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Image;
use App\Models\Wiki\Song;
use App\Models\Wiki\Video;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Tests\TestCase;
/**
* Class FeaturedThemeIndexTest.
*/
class FeaturedThemeIndexTest extends TestCase
{
use SortsModels;
use WithFaker;
/**
* By default, the Featured Theme Index Endpoint shall return a collection of Featured Theme Resources.
*
* @return void
*/
public function testDefault(): void
{
$publicCount = $this->faker->randomDigitNotNull();
$featuredThemes = FeaturedTheme::factory()->count($publicCount)->create();
Collection::times($this->faker->randomDigitNotNull(), function () {
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => $this->faker->dateTimeBetween('+1 day', '+30 years'),
]);
});
Collection::times($this->faker->randomDigitNotNull(), function () {
FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => null,
]);
});
$response = $this->get(route('api.featuredtheme.index'));
$response->assertJsonCount($publicCount, FeaturedThemeCollection::$wrap);
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredThemes, new Query()))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall be paginated.
*
* @return void
*/
public function testPaginated(): void
{
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
$response = $this->get(route('api.featuredtheme.index'));
$response->assertJsonStructure([
FeaturedThemeCollection::$wrap,
'links',
'meta',
]);
}
/**
* The Featured Theme Index Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new FeaturedThemeSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
FeaturedTheme::factory()
->for(
AnimeThemeEntry::factory()
->for(
AnimeTheme::factory()
->for(Anime::factory()->has(Image::factory()->count($this->faker->randomDigitNotNull())))
->for(Song::factory()->has(Artist::factory()->count($this->faker->randomDigitNotNull())))
)
)
->for(Video::factory())
->for(User::factory())
->count($this->faker->randomDigitNotNull())
->create();
$featuredThemes = FeaturedTheme::with($includedPaths->all())->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredThemes, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new FeaturedThemeSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
FeaturedThemeResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
$featuredThemes = FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredThemes, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support sorting resources.
*
* @return void
*/
public function testSorts(): void
{
$schema = new FeaturedThemeSchema();
/** @var Sort $sort */
$sort = collect($schema->fields())
->filter(fn (Field $field) => $field instanceof SortableField)
->map(fn (SortableField $field) => $field->getSort())
->random();
$parameters = [
SortParser::param() => $sort->format(Direction::getRandomInstance()),
];
$query = new Query($parameters);
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$featuredThemes = $this->sort(FeaturedTheme::query(), $query, $schema)->get();
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredThemes, $query))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support filtering by created_at.
*
* @return void
*/
public function testCreatedAtFilter(): void
{
$createdFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BaseModel::ATTRIBUTE_CREATED_AT => $createdFilter,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($createdFilter, function () {
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
});
Carbon::withTestNow($excludedDate, function () {
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
});
$featuredTheme = FeaturedTheme::query()->where(BaseModel::ATTRIBUTE_CREATED_AT, $createdFilter)->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support filtering by updated_at.
*
* @return void
*/
public function testUpdatedAtFilter(): void
{
$updatedFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BaseModel::ATTRIBUTE_UPDATED_AT => $updatedFilter,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($updatedFilter, function () {
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
});
Carbon::withTestNow($excludedDate, function () {
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
});
$featuredTheme = FeaturedTheme::query()->where(BaseModel::ATTRIBUTE_UPDATED_AT, $updatedFilter)->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support filtering by trashed.
*
* @return void
*/
public function testWithoutTrashedFilter(): void
{
$parameters = [
FilterParser::param() => [
TrashedCriteria::PARAM_VALUE => TrashedStatus::WITHOUT,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
FeaturedTheme::factory()->trashed()->count($this->faker->randomDigitNotNull())->create();
$featuredTheme = FeaturedTheme::withoutTrashed()->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support filtering by trashed.
*
* @return void
*/
public function testWithTrashedFilter(): void
{
$parameters = [
FilterParser::param() => [
TrashedCriteria::PARAM_VALUE => TrashedStatus::WITH,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
FeaturedTheme::factory()->trashed()->count($this->faker->randomDigitNotNull())->create();
$featuredTheme = FeaturedTheme::withTrashed()->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support filtering by trashed.
*
* @return void
*/
public function testOnlyTrashedFilter(): void
{
$parameters = [
FilterParser::param() => [
TrashedCriteria::PARAM_VALUE => TrashedStatus::ONLY,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
FeaturedTheme::factory()->count($this->faker->randomDigitNotNull())->create();
FeaturedTheme::factory()->trashed()->count($this->faker->randomDigitNotNull())->create();
$featuredTheme = FeaturedTheme::onlyTrashed()->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Index Endpoint shall support filtering by deleted_at.
*
* @return void
*/
public function testDeletedAtFilter(): void
{
$deletedFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BaseModel::ATTRIBUTE_DELETED_AT => $deletedFilter,
TrashedCriteria::PARAM_VALUE => TrashedStatus::WITH,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($deletedFilter, function () {
FeaturedTheme::factory()->trashed()->count($this->faker->randomDigitNotNull())->create();
});
Carbon::withTestNow($excludedDate, function () {
FeaturedTheme::factory()->trashed()->count($this->faker->randomDigitNotNull())->create();
});
$featuredTheme = FeaturedTheme::withTrashed()->where(BaseModel::ATTRIBUTE_DELETED_AT, $deletedFilter)->get();
$response = $this->get(route('api.featuredtheme.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeCollection($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class FeaturedThemeRestoreTest.
*/
class FeaturedThemeRestoreTest extends TestCase
{
/**
* The Featured Theme Restore Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$featuredTheme = FeaturedTheme::factory()->trashed()->createOne();
$response = $this->patch(route('api.featuredtheme.restore', ['featuredtheme' => $featuredTheme]));
$response->assertUnauthorized();
}
/**
* The Featured Theme Restore Endpoint shall forbid users without the restore featured theme permission.
*
* @return void
*/
public function testForbidden(): void
{
$featuredTheme = FeaturedTheme::factory()->trashed()->createOne();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->patch(route('api.featuredtheme.restore', ['featuredtheme' => $featuredTheme]));
$response->assertForbidden();
}
/**
* The Featured Theme Restore Endpoint shall forbid users from restoring a featured theme that isn't trashed.
*
* @return void
*/
public function testTrashed(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$user = User::factory()->withPermissions(ExtendedCrudPermission::RESTORE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->patch(route('api.featuredtheme.restore', ['featuredtheme' => $featuredTheme]));
$response->assertForbidden();
}
/**
* The Featured Theme Restore Endpoint shall restore the featured theme.
*
* @return void
*/
public function testRestored(): void
{
$featuredTheme = FeaturedTheme::factory()->trashed()->createOne();
$user = User::factory()->withPermissions(ExtendedCrudPermission::RESTORE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->patch(route('api.featuredtheme.restore', ['featuredtheme' => $featuredTheme]));
$response->assertOk();
static::assertNotSoftDeleted($featuredTheme);
}
}
@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Admin\FeaturedThemeSchema;
use App\Http\Resources\Admin\Resource\FeaturedThemeResource;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Image;
use App\Models\Wiki\Song;
use App\Models\Wiki\Video;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
/**
* Class FeaturedThemeShowTest.
*/
class FeaturedThemeShowTest extends TestCase
{
use WithFaker;
/**
* The Featured Theme Show Endpoint shall forbid the user from viewing a featured theme whose start date is in the future.
*
* @return void
*/
public function testForbiddenIfFutureStartDate(): void
{
$featuredTheme = FeaturedTheme::factory()->create([
FeaturedTheme::ATTRIBUTE_START_AT => $this->faker->dateTimeBetween('+1 day', '+30 years'),
]);
$response = $this->get(route('api.featuredtheme.show', ['featuredtheme' => $featuredTheme]));
$response->assertForbidden();
}
/**
* By default, the Featured Theme Show Endpoint shall return a Featured Theme Resource.
*
* @return void
*/
public function testDefault(): void
{
$featuredTheme = FeaturedTheme::factory()->create();
$response = $this->get(route('api.featuredtheme.show', ['featuredtheme' => $featuredTheme]));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($featuredTheme, new Query()))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Show Endpoint shall return a Featured Theme Resource for soft deleted featured themes.
*
* @return void
*/
public function testSoftDelete(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$featuredTheme->delete();
$featuredTheme->unsetRelations();
$response = $this->get(route('api.featuredtheme.show', ['featuredtheme' => $featuredTheme]));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($featuredTheme, new Query()))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Show Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new FeaturedThemeSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
$featuredTheme = FeaturedTheme::factory()
->for(
AnimeThemeEntry::factory()
->for(
AnimeTheme::factory()
->for(Anime::factory()->has(Image::factory()->count($this->faker->randomDigitNotNull())))
->for(Song::factory()->has(Artist::factory()->count($this->faker->randomDigitNotNull())))
)
)
->for(Video::factory())
->for(User::factory())
->createOne();
$response = $this->get(route('api.featuredtheme.show', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Featured Theme Show Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new FeaturedThemeSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
FeaturedThemeResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
$featuredTheme = FeaturedTheme::factory()->create();
$response = $this->get(route('api.featuredtheme.show', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertJson(
json_decode(
json_encode(
(new FeaturedThemeResource($featuredTheme, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Enums\Auth\CrudPermission;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class FeaturedThemeStoreTest.
*/
class FeaturedThemeStoreTest extends TestCase
{
use WithFaker;
/**
* The Featured Theme Store Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$featuredTheme = FeaturedTheme::factory()->makeOne();
$response = $this->post(route('api.featuredtheme.store', $featuredTheme->toArray()));
$response->assertUnauthorized();
}
/**
* The Featured Theme Store Endpoint shall forbid users without the create featured theme permission.
*
* @return void
*/
public function testForbidden(): void
{
$featuredTheme = FeaturedTheme::factory()->makeOne();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.featuredtheme.store', $featuredTheme->toArray()));
$response->assertForbidden();
}
/**
* The Featured Theme Store Endpoint shall require the end_at and start_at fields.
*
* @return void
*/
public function testRequiredFields(): void
{
$user = User::factory()->withPermissions(CrudPermission::CREATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.featuredtheme.store'));
$response->assertJsonValidationErrors([
FeaturedTheme::ATTRIBUTE_END_AT,
FeaturedTheme::ATTRIBUTE_START_AT,
]);
}
/**
* The Featured Theme Store Endpoint shall require the start_at field to be before the end_at field and vice versa.
*
* @return void
*/
public function testStartAtBeforeEndDate(): void
{
$parameters = FeaturedTheme::factory()->raw([
FeaturedTheme::ATTRIBUTE_START_AT => $this->faker->dateTimeBetween('+1 day', '+30 years')->format(AllowedDateFormat::YMDHISU),
FeaturedTheme::ATTRIBUTE_END_AT => $this->faker->dateTimeBetween('-30 years', '-1 day')->format(AllowedDateFormat::YMDHISU),
]);
$user = User::factory()->withPermissions(CrudPermission::CREATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.featuredtheme.store', $parameters));
$response->assertJsonValidationErrors([
FeaturedTheme::ATTRIBUTE_START_AT,
FeaturedTheme::ATTRIBUTE_END_AT,
]);
}
/**
* The Featured Theme Store Endpoint shall require the entry and video to have an association.
*
* @return void
*/
public function testAnimeThemeEntryVideoExists(): void
{
$entry = AnimeThemeEntry::factory()
->for(AnimeTheme::factory()->for(Anime::factory()))
->create();
$video = Video::factory()->create();
$parameters = FeaturedTheme::factory()->raw([
FeaturedTheme::ATTRIBUTE_ENTRY => $entry->getKey(),
FeaturedTheme::ATTRIBUTE_VIDEO => $video->getKey(),
]);
$user = User::factory()->withPermissions(CrudPermission::CREATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.featuredtheme.store', $parameters));
$response->assertJsonValidationErrors([
FeaturedTheme::ATTRIBUTE_ENTRY,
FeaturedTheme::ATTRIBUTE_VIDEO,
]);
}
/**
* The Featured Theme Store Endpoint shall create a featured theme.
*
* @return void
*/
public function testCreate(): void
{
$entryVideo = AnimeThemeEntryVideo::factory()
->for(AnimeThemeEntry::factory()->for(AnimeTheme::factory()->for(Anime::factory())))
->for(Video::factory())
->createOne();
$parameters = FeaturedTheme::factory()->raw([
FeaturedTheme::ATTRIBUTE_ENTRY => $entryVideo->entry_id,
FeaturedTheme::ATTRIBUTE_VIDEO => $entryVideo->video_id,
]);
$user = User::factory()->withPermissions(CrudPermission::CREATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.featuredtheme.store', $parameters));
$response->assertCreated();
static::assertDatabaseCount(FeaturedTheme::TABLE, 1);
}
}
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Http\Api\Admin\FeaturedTheme;
use App\Enums\Auth\CrudPermission;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class FeaturedThemeUpdateTest.
*/
class FeaturedThemeUpdateTest extends TestCase
{
use WithFaker;
/**
* The Featured Theme Update Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$parameters = FeaturedTheme::factory()->raw();
$response = $this->put(route('api.featuredtheme.update', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertUnauthorized();
}
/**
* The Featured Theme Update Endpoint shall forbid users without the update featured theme permission.
*
* @return void
*/
public function testForbidden(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$parameters = FeaturedTheme::factory()->raw();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.featuredtheme.update', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertForbidden();
}
/**
* The Featured Theme Update Endpoint shall forbid users from updating a featured theme that is trashed.
*
* @return void
*/
public function testTrashed(): void
{
$featuredTheme = FeaturedTheme::factory()->trashed()->createOne();
$parameters = FeaturedTheme::factory()->raw();
$user = User::factory()->withPermissions(CrudPermission::UPDATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.featuredtheme.update', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertForbidden();
}
/**
* The Featured Update Store Endpoint shall require the start_at field to be before the end_at field and vice versa.
*
* @return void
*/
public function testStartAtBeforeEndDate(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$parameters = FeaturedTheme::factory()->raw([
FeaturedTheme::ATTRIBUTE_START_AT => $this->faker->dateTimeBetween('+1 day', '+30 years')->format(AllowedDateFormat::YMDHISU),
FeaturedTheme::ATTRIBUTE_END_AT => $this->faker->dateTimeBetween('-30 years', '-1 day')->format(AllowedDateFormat::YMDHISU),
]);
$user = User::factory()->withPermissions(CrudPermission::UPDATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.featuredtheme.update', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertJsonValidationErrors([
FeaturedTheme::ATTRIBUTE_START_AT,
FeaturedTheme::ATTRIBUTE_END_AT,
]);
}
/**
* The Featured Theme Update Endpoint shall require the entry and video to have an association.
*
* @return void
*/
public function testAnimeThemeEntryVideoExists(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$entry = AnimeThemeEntry::factory()
->for(AnimeTheme::factory()->for(Anime::factory()))
->create();
$video = Video::factory()->create();
$parameters = FeaturedTheme::factory()->raw([
FeaturedTheme::ATTRIBUTE_ENTRY => $entry->getKey(),
FeaturedTheme::ATTRIBUTE_VIDEO => $video->getKey(),
]);
$user = User::factory()->withPermissions(CrudPermission::UPDATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.featuredtheme.update', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertJsonValidationErrors([
FeaturedTheme::ATTRIBUTE_ENTRY,
FeaturedTheme::ATTRIBUTE_VIDEO,
]);
}
/**
* The Featured Theme Update Endpoint shall update a featured theme.
*
* @return void
*/
public function testUpdate(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
$entryVideo = AnimeThemeEntryVideo::factory()
->for(AnimeThemeEntry::factory()->for(AnimeTheme::factory()->for(Anime::factory())))
->for(Video::factory())
->createOne();
$parameters = FeaturedTheme::factory()->raw([
FeaturedTheme::ATTRIBUTE_ENTRY => $entryVideo->entry_id,
FeaturedTheme::ATTRIBUTE_VIDEO => $entryVideo->video_id,
]);
$user = User::factory()->withPermissions(CrudPermission::UPDATE()->format(FeaturedTheme::class))->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.featuredtheme.update', ['featuredtheme' => $featuredTheme] + $parameters));
$response->assertOk();
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Jobs\Admin;
use App\Constants\FeatureConstants;
use App\Events\Admin\FeaturedTheme\FeaturedThemeCreated;
use App\Events\Admin\FeaturedTheme\FeaturedThemeDeleted;
use App\Events\Admin\FeaturedTheme\FeaturedThemeRestored;
use App\Events\Admin\FeaturedTheme\FeaturedThemeUpdated;
use App\Jobs\SendDiscordNotificationJob;
use App\Models\Admin\FeaturedTheme;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Event;
use Laravel\Pennant\Feature;
use Tests\TestCase;
/**
* Class FeaturedThemeTest.
*/
class FeaturedThemeTest extends TestCase
{
/**
* When a featured theme is created, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeCreatedSendsDiscordNotification(): void
{
Feature::activate(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
Bus::fake(SendDiscordNotificationJob::class);
Event::fakeExcept(FeaturedThemeCreated::class);
FeaturedTheme::factory()->createOne();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
/**
* When a featured theme is deleted, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeDeletedSendsDiscordNotification(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
Feature::activate(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
Bus::fake(SendDiscordNotificationJob::class);
Event::fakeExcept(FeaturedThemeDeleted::class);
$featuredTheme->delete();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
/**
* When a featured theme is restored, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeRestoredSendsDiscordNotification(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
Feature::activate(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
Bus::fake(SendDiscordNotificationJob::class);
Event::fakeExcept(FeaturedThemeRestored::class);
$featuredTheme->restore();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
/**
* When a featured theme is updated, a SendDiscordNotification job shall be dispatched.
*
* @return void
*/
public function testFeaturedThemeUpdatedSendsDiscordNotification(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
Feature::activate(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
Bus::fake(SendDiscordNotificationJob::class);
Event::fakeExcept(FeaturedThemeUpdated::class);
$changes = FeaturedTheme::factory()->makeOne();
$featuredTheme->fill($changes->getAttributes());
$featuredTheme->save();
Bus::assertDispatched(SendDiscordNotificationJob::class);
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Models\Admin;
use App\Models\Admin\FeaturedTheme;
use App\Models\Auth\User;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Tests\TestCase;
/**
* Class FeaturedThemeTest.
*/
class FeaturedThemeTest extends TestCase
{
/**
* Featured Themes shall be nameable.
*
* @return void
*/
public function testNameable(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
static::assertIsString($featuredTheme->getName());
}
/**
* Featured Themes shall cast the end_at attribute to datetime.
*
* @return void
*/
public function testCastsEndAt(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
static::assertInstanceOf(Carbon::class, $featuredTheme->end_at);
}
/**
* Featured Themes shall cast the start_at attribute to datetime.
*
* @return void
*/
public function testCastsStartAt(): void
{
$featuredTheme = FeaturedTheme::factory()->createOne();
static::assertInstanceOf(Carbon::class, $featuredTheme->start_at);
}
/**
* Featured themes shall belong to a User.
*
* @return void
*/
public function testUser(): void
{
$featuredTheme = FeaturedTheme::factory()
->for(User::factory())
->createOne();
static::assertInstanceOf(BelongsTo::class, $featuredTheme->user());
static::assertInstanceOf(User::class, $featuredTheme->user()->first());
}
/**
* Featured themes shall belong to a Video.
*
* @return void
*/
public function testVideo(): void
{
$featuredTheme = FeaturedTheme::factory()
->for(Video::factory())
->createOne();
static::assertInstanceOf(BelongsTo::class, $featuredTheme->video());
static::assertInstanceOf(Video::class, $featuredTheme->video()->first());
}
/**
* Featured themes shall belong to an Entry.
*
* @return void
*/
public function testEntry(): void
{
$featuredTheme = FeaturedTheme::factory()
->for(AnimeThemeEntry::factory()->for(AnimeTheme::factory()->for(Anime::factory())))
->createOne();
static::assertInstanceOf(BelongsTo::class, $featuredTheme->animethemeentry());
static::assertInstanceOf(AnimeThemeEntry::class, $featuredTheme->animethemeentry()->first());
}
}