From eb265c839b9c90e0606f5fe915bb4e62795cfd4e Mon Sep 17 00:00:00 2001 From: Kyrch Date: Mon, 18 Aug 2025 03:23:37 -0300 Subject: [PATCH] feat: added entry <-> resource relation (#927) --- .../Models/Wiki/AttachResourceAction.php | 2 +- app/Enums/Models/Wiki/ResourceSite.php | 27 +++++ .../Theme/Entry/AttachEntryResourceAction.php | 31 ++++++ .../Resources/Wiki/Anime/Theme/Entry.php | 46 +++++++- .../Resources/Wiki/ExternalResource.php | 2 + .../EntryResourceRelationManager.php | 61 +++++++++++ .../Wiki/Anime/Theme/AnimeThemeEntryType.php | 4 + .../Wiki/Anime/Theme/AnimeThemeEntry.php | 22 +++- app/Models/Wiki/ExternalResource.php | 16 +++ app/Pivots/Morph/Resourceable.php | 2 + app/Providers/AppServiceProvider.php | 2 +- .../AnimeThemeEntryResourceLinkFormatRule.php | 31 ++++++ lang/en/filament.php | 3 + .../Wiki/Anime/Theme/AnimeThemeEntryTest.php | 17 +++ .../Unit/Models/Wiki/ExternalResourceTest.php | 14 +++ .../AnimeThemeEntryResourceLinkFormatTest.php | 101 ++++++++++++++++++ 16 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 app/Filament/Actions/Models/Wiki/Anime/Theme/Entry/AttachEntryResourceAction.php create mode 100644 app/Filament/Resources/Wiki/ExternalResource/RelationManagers/EntryResourceRelationManager.php create mode 100644 app/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatRule.php create mode 100644 tests/Unit/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatTest.php diff --git a/app/Actions/Models/Wiki/AttachResourceAction.php b/app/Actions/Models/Wiki/AttachResourceAction.php index ca488e17a..701d74c75 100644 --- a/app/Actions/Models/Wiki/AttachResourceAction.php +++ b/app/Actions/Models/Wiki/AttachResourceAction.php @@ -25,7 +25,7 @@ class AttachResourceAction foreach ($sites as $resourceSite) { $url = Arr::get($fields, $resourceSite->name); - if (empty($url)) { + if (blank($url)) { continue; } diff --git a/app/Enums/Models/Wiki/ResourceSite.php b/app/Enums/Models/Wiki/ResourceSite.php index d6db3cd25..9de1222e5 100644 --- a/app/Enums/Models/Wiki/ResourceSite.php +++ b/app/Enums/Models/Wiki/ResourceSite.php @@ -7,10 +7,12 @@ namespace App\Enums\Models\Wiki; use App\Concerns\Enums\LocalizesName; use App\Contracts\Models\HasResources; use App\Models\Wiki\Anime; +use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; use App\Models\Wiki\Artist; use App\Models\Wiki\Song; use App\Models\Wiki\Studio; use App\Rules\Wiki\Resource\AnimeResourceLinkFormatRule; +use App\Rules\Wiki\Resource\AnimeThemeEntryResourceLinkFormatRule; use App\Rules\Wiki\Resource\ArtistResourceLinkFormatRule; use App\Rules\Wiki\Resource\SongResourceLinkFormatRule; use App\Rules\Wiki\Resource\StudioResourceLinkFormatRule; @@ -197,6 +199,7 @@ enum ResourceSite: int implements HasLabel { return match (true) { $model instanceof Anime => new AnimeResourceLinkFormatRule($this), + $model instanceof AnimeThemeEntry => new AnimeThemeEntryResourceLinkFormatRule($this), $model instanceof Artist => new ArtistResourceLinkFormatRule($this), $model instanceof Song => new SongResourceLinkFormatRule($this), $model instanceof Studio => new StudioResourceLinkFormatRule($this), @@ -232,6 +235,9 @@ enum ResourceSite: int implements HasLabel ResourceSite::WIKI, ResourceSite::LIVECHART, ], + AnimeThemeEntry::class => [ + ResourceSite::YOUTUBE, + ], Artist::class => [ ResourceSite::X, ResourceSite::ANIDB, @@ -297,6 +303,13 @@ enum ResourceSite: int implements HasLabel }; } + if ($modelClass === AnimeThemeEntry::class) { + return match ($this) { + ResourceSite::YOUTUBE => "https://www.youtube.com/watch?v=$slug", + default => null, + }; + } + if ($modelClass === Artist::class) { return match ($this) { ResourceSite::X => "https://x.com/$slug", @@ -377,6 +390,13 @@ enum ResourceSite: int implements HasLabel }; } + if ($model instanceof AnimeThemeEntry) { + return match ($this) { + ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/(watch)\?v=([\w-]+)$/', + default => '/^$/', + }; + } + if ($model instanceof Artist) { return match ($this) { ResourceSite::X => '/^https:\/\/(x)\.com\/(\w+)$/', @@ -449,6 +469,13 @@ enum ResourceSite: int implements HasLabel }; } + if ($modelClass === AnimeThemeEntry::class) { + return match ($this) { + ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/watch\?v=[\w-]+$/', + default => '/$.^/', + }; + } + if ($modelClass === Artist::class) { return match ($this) { ResourceSite::X => '/^https:\/\/x\.com\/\w+$/', diff --git a/app/Filament/Actions/Models/Wiki/Anime/Theme/Entry/AttachEntryResourceAction.php b/app/Filament/Actions/Models/Wiki/Anime/Theme/Entry/AttachEntryResourceAction.php new file mode 100644 index 000000000..19fe25b0e --- /dev/null +++ b/app/Filament/Actions/Models/Wiki/Anime/Theme/Entry/AttachEntryResourceAction.php @@ -0,0 +1,31 @@ +sites([ + ResourceSite::YOUTUBE, + ]); + } +} diff --git a/app/Filament/Resources/Wiki/Anime/Theme/Entry.php b/app/Filament/Resources/Wiki/Anime/Theme/Entry.php index 5665e2dd1..035cf3fbc 100644 --- a/app/Filament/Resources/Wiki/Anime/Theme/Entry.php +++ b/app/Filament/Resources/Wiki/Anime/Theme/Entry.php @@ -4,7 +4,10 @@ declare(strict_types=1); namespace App\Filament\Resources\Wiki\Anime\Theme; +use App\Actions\Models\Wiki\AttachResourceAction; +use App\Enums\Models\Wiki\ResourceSite; use App\Enums\Models\Wiki\ThemeType; +use App\Filament\Actions\Models\Wiki\Anime\Theme\Entry\AttachEntryResourceAction; use App\Filament\Components\Columns\BelongsToColumn; use App\Filament\Components\Columns\TextColumn; use App\Filament\Components\Fields\BelongsTo; @@ -16,6 +19,7 @@ use App\Filament\Components\Filters\TextFilter; use App\Filament\Components\Infolist\BelongsToEntry; use App\Filament\Components\Infolist\TextEntry; use App\Filament\Components\Infolist\TimestampSection; +use App\Filament\RelationManagers\Wiki\ResourceRelationManager; use App\Filament\Resources\BaseResource; use App\Filament\Resources\Wiki\Anime as AnimeResource; use App\Filament\Resources\Wiki\Anime\Theme as ThemeResource; @@ -23,9 +27,11 @@ use App\Filament\Resources\Wiki\Anime\Theme\Entry\Pages\ListEntries; use App\Filament\Resources\Wiki\Anime\Theme\Entry\Pages\ViewEntry; use App\Filament\Resources\Wiki\Anime\Theme\Entry\RelationManagers\VideoEntryRelationManager; use App\Filament\Resources\Wiki\Anime\Theme\RelationManagers\EntryThemeRelationManager; +use App\Filament\Resources\Wiki\Song as SongResource; use App\Models\Wiki\Anime\AnimeTheme as ThemeModel; use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; use App\Models\Wiki\Anime\Theme\AnimeThemeEntry as EntryModel; +use App\Rules\Wiki\Resource\AnimeThemeEntryResourceLinkFormatRule; use Filament\Forms\Components\Checkbox; use Filament\Infolists\Components\IconEntry; use Filament\Resources\RelationManagers\RelationGroup; @@ -37,6 +43,7 @@ use Filament\Tables\Filters\Filter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Uri; class Entry extends BaseResource { @@ -129,7 +136,11 @@ class Entry extends BaseResource $query = parent::getEloquentQuery(); // Necessary to prevent lazy loading when loading related resources - return $query->with([AnimeThemeEntry::RELATION_ANIME_SHALLOW, AnimeThemeEntry::RELATION_THEME]); + return $query->with([ + AnimeThemeEntry::RELATION_ANIME_SHALLOW, + AnimeThemeEntry::RELATION_SONG, + AnimeThemeEntry::RELATION_THEME, + ]); } /** @@ -185,6 +196,21 @@ class Entry extends BaseResource ->label(__('filament.fields.anime_theme_entry.notes.name')) ->helperText(__('filament.fields.anime_theme_entry.notes.help')) ->maxLength(192), + + TextInput::make(ResourceSite::YOUTUBE->name) + ->label(ResourceSite::YOUTUBE->localize()) + ->helperText(__('filament.fields.anime_theme_entry.youtube.help')) + ->url() + ->maxLength(255) + ->rule(new AnimeThemeEntryResourceLinkFormatRule(ResourceSite::YOUTUBE)) + ->uri() + ->saveRelationshipsUsing(function (EntryModel $record, AttachResourceAction $action, Uri $state) { + $fields = [ + ResourceSite::YOUTUBE->name => $state, + ]; + + $action->handle($record, $fields, [ResourceSite::YOUTUBE]); + }), ]) ->columns(1); } @@ -222,6 +248,9 @@ class Entry extends BaseResource ->label(__('filament.fields.anime_theme_entry.notes.name')) ->limit(50) ->tooltip(fn (TextColumn $column) => $column->getState()), + + BelongsToColumn::make(AnimeThemeEntry::RELATION_SONG, SongResource::class) + ->hiddenOn(EntryThemeRelationManager::class), ]) ->searchable(); } @@ -241,6 +270,8 @@ class Entry extends BaseResource BelongsToEntry::make(EntryModel::RELATION_THEME, ThemeResource::class, true), + BelongsToEntry::make(EntryModel::RELATION_SONG, SongResource::class, true), + TextEntry::make(EntryModel::ATTRIBUTE_VERSION) ->label(__('filament.fields.anime_theme_entry.version.name')), @@ -280,6 +311,7 @@ class Entry extends BaseResource return [ RelationGroup::make(static::getModelLabel(), [ VideoEntryRelationManager::class, + ResourceRelationManager::class, ...parent::getBaseRelations(), ]), @@ -315,6 +347,18 @@ class Entry extends BaseResource ]; } + /** + * Get the actions available for the resource. + * + * @return array + */ + public static function getRecordActions(): array + { + return [ + AttachEntryResourceAction::make(), + ]; + } + /** * Get the pages available for the resource. * diff --git a/app/Filament/Resources/Wiki/ExternalResource.php b/app/Filament/Resources/Wiki/ExternalResource.php index eef2653c1..cb9a6bda5 100644 --- a/app/Filament/Resources/Wiki/ExternalResource.php +++ b/app/Filament/Resources/Wiki/ExternalResource.php @@ -16,6 +16,7 @@ use App\Filament\Resources\Wiki\ExternalResource\Pages\ListExternalResources; use App\Filament\Resources\Wiki\ExternalResource\Pages\ViewExternalResource; use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\AnimeResourceRelationManager; use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\ArtistResourceRelationManager; +use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\EntryResourceRelationManager; use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\SongResourceRelationManager; use App\Filament\Resources\Wiki\ExternalResource\RelationManagers\StudioResourceRelationManager; use App\Models\Wiki\ExternalResource as ExternalResourceModel; @@ -195,6 +196,7 @@ class ExternalResource extends BaseResource return [ RelationGroup::make(static::getModelLabel(), [ AnimeResourceRelationManager::class, + EntryResourceRelationManager::class, ArtistResourceRelationManager::class, SongResourceRelationManager::class, StudioResourceRelationManager::class, diff --git a/app/Filament/Resources/Wiki/ExternalResource/RelationManagers/EntryResourceRelationManager.php b/app/Filament/Resources/Wiki/ExternalResource/RelationManagers/EntryResourceRelationManager.php new file mode 100644 index 000000000..7e93a8b7e --- /dev/null +++ b/app/Filament/Resources/Wiki/ExternalResource/RelationManagers/EntryResourceRelationManager.php @@ -0,0 +1,61 @@ +label(__('filament.fields.resourceable.as.name')) + ->helperText(__('filament.fields.resourceable.as.help')), + ]; + } + + /** + * Get the pivot columns of the relation. + * + * @return Column[] + */ + public function getPivotColumns(): array + { + return [ + TextColumn::make(Resourceable::ATTRIBUTE_AS) + ->label(__('filament.fields.resourceable.as.name')), + ]; + } + + /** + * The index page of the resource. + */ + public function table(Table $table): Table + { + return parent::table( + $table + ->inverseRelationship(AnimeThemeEntry::RELATION_RESOURCES) + ); + } +} diff --git a/app/GraphQL/Definition/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php b/app/GraphQL/Definition/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php index 27673d5a5..28d56e544 100644 --- a/app/GraphQL/Definition/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php +++ b/app/GraphQL/Definition/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php @@ -16,11 +16,14 @@ use App\GraphQL\Definition\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntryNsfwFiel use App\GraphQL\Definition\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntrySpoilerField; use App\GraphQL\Definition\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntryVersionField; use App\GraphQL\Definition\Types\EloquentType; +use App\GraphQL\Definition\Types\Pivot\Morph\ResourceableType; use App\GraphQL\Definition\Types\Pivot\Wiki\AnimeThemeEntryVideoType; use App\GraphQL\Definition\Types\Wiki\Anime\AnimeThemeType; +use App\GraphQL\Definition\Types\Wiki\ExternalResourceType; use App\GraphQL\Definition\Types\Wiki\VideoType; use App\GraphQL\Support\Relations\BelongsToManyRelation; use App\GraphQL\Support\Relations\BelongsToRelation; +use App\GraphQL\Support\Relations\MorphToManyRelation; use App\GraphQL\Support\Relations\Relation; use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; @@ -45,6 +48,7 @@ class AnimeThemeEntryType extends EloquentType implements ReportableType new BelongsToRelation(new AnimeThemeType(), AnimeThemeEntry::RELATION_THEME) ->notNullable(), new BelongsToManyRelation($this, VideoType::class, AnimeThemeEntry::RELATION_VIDEOS, AnimeThemeEntryVideoType::class), + new MorphToManyRelation($this, ExternalResourceType::class, AnimeThemeEntry::RELATION_RESOURCES, ResourceableType::class), ]; } diff --git a/app/Models/Wiki/Anime/Theme/AnimeThemeEntry.php b/app/Models/Wiki/Anime/Theme/AnimeThemeEntry.php index 313b204b4..9611eec07 100644 --- a/app/Models/Wiki/Anime/Theme/AnimeThemeEntry.php +++ b/app/Models/Wiki/Anime/Theme/AnimeThemeEntry.php @@ -7,6 +7,7 @@ namespace App\Models\Wiki\Anime\Theme; use App\Concerns\Models\Reportable; use App\Concerns\Models\SoftDeletes; use App\Contracts\Http\Api\InteractsWithSchema; +use App\Contracts\Models\HasResources; use App\Contracts\Models\SoftDeletable; use App\Enums\Models\Wiki\ThemeType; use App\Events\Wiki\Anime\Theme\Entry\EntryCreated; @@ -20,7 +21,9 @@ use App\Http\Resources\Pivot\Wiki\Resource\AnimeThemeEntryVideoResource; use App\Models\BaseModel; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeTheme; +use App\Models\Wiki\ExternalResource; use App\Models\Wiki\Video; +use App\Pivots\Morph\Resourceable; use App\Pivots\Wiki\AnimeThemeEntryVideo; use App\Scopes\WithoutInsertSongScope; use Database\Factories\Wiki\Anime\Theme\AnimeThemeEntryFactory; @@ -29,6 +32,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Znck\Eloquent\Relations\BelongsToThrough; @@ -43,6 +47,7 @@ use Znck\Eloquent\Traits\BelongsToThrough as ZnckBelongsToThrough; * @property string|null $episodes * @property string|null $notes * @property bool $nsfw + * @property Collection $resources * @property bool $spoiler * @property int $theme_id * @property int|null $version @@ -50,7 +55,7 @@ use Znck\Eloquent\Traits\BelongsToThrough as ZnckBelongsToThrough; * * @method static AnimeThemeEntryFactory factory(...$parameters) */ -class AnimeThemeEntry extends BaseModel implements InteractsWithSchema, SoftDeletable +class AnimeThemeEntry extends BaseModel implements HasResources, InteractsWithSchema, SoftDeletable { use HasFactory; use Reportable; @@ -70,6 +75,7 @@ class AnimeThemeEntry extends BaseModel implements InteractsWithSchema, SoftDele final public const RELATION_ANIME = 'animetheme.anime'; final public const RELATION_ANIME_SHALLOW = 'anime'; + final public const RELATION_RESOURCES = 'resources'; final public const RELATION_SONG = 'animetheme.song'; final public const RELATION_SYNONYMS = 'animetheme.anime.animesynonyms'; final public const RELATION_THEME = 'animetheme'; @@ -245,6 +251,20 @@ class AnimeThemeEntry extends BaseModel implements InteractsWithSchema, SoftDele ->withTimestamps(); } + /** + * Get the resources for the entry through the resourceable morph pivot. + * + * @return MorphToMany + */ + public function resources(): MorphToMany + { + return $this->morphToMany(ExternalResource::class, Resourceable::RELATION_RESOURCEABLE, Resourceable::TABLE, Resourceable::ATTRIBUTE_RESOURCEABLE_ID, Resourceable::ATTRIBUTE_RESOURCE) + ->using(Resourceable::class) + ->withPivot(Resourceable::ATTRIBUTE_AS) + ->as('entryresource') + ->withTimestamps(); + } + /** * Get the anime that owns the entry through the theme. * diff --git a/app/Models/Wiki/ExternalResource.php b/app/Models/Wiki/ExternalResource.php index b30352c43..740932ca6 100644 --- a/app/Models/Wiki/ExternalResource.php +++ b/app/Models/Wiki/ExternalResource.php @@ -13,6 +13,7 @@ use App\Events\Wiki\ExternalResource\ExternalResourceDeleted; use App\Events\Wiki\ExternalResource\ExternalResourceRestored; use App\Events\Wiki\ExternalResource\ExternalResourceUpdated; use App\Models\BaseModel; +use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; use App\Pivots\Morph\Resourceable; use Database\Factories\Wiki\ExternalResourceFactory; use Illuminate\Database\Eloquent\Casts\AsUri; @@ -49,6 +50,7 @@ class ExternalResource extends BaseModel implements SoftDeletable final public const ATTRIBUTE_SITE = 'site'; final public const RELATION_ANIME = 'anime'; + final public const RELATION_ANIMETHEMEENTRIES = 'animethemeentries'; final public const RELATION_ARTISTS = 'artists'; final public const RELATION_SONGS = 'songs'; final public const RELATION_STUDIOS = 'studios'; @@ -136,6 +138,20 @@ class ExternalResource extends BaseModel implements SoftDeletable ->withTimestamps(); } + /** + * Get the entry that reference this resource. + * + * @return MorphToMany + */ + public function animethemeentries(): MorphToMany + { + return $this->morphedByMany(AnimeThemeEntry::class, Resourceable::RELATION_RESOURCEABLE, Resourceable::TABLE, Resourceable::ATTRIBUTE_RESOURCE, Resourceable::ATTRIBUTE_RESOURCEABLE_ID) + ->using(Resourceable::class) + ->withPivot(Resourceable::ATTRIBUTE_AS) + ->as('entryresource') + ->withTimestamps(); + } + /** * Get the artists that reference this resource. * diff --git a/app/Pivots/Morph/Resourceable.php b/app/Pivots/Morph/Resourceable.php index c8c6fc04a..76b7d14cc 100644 --- a/app/Pivots/Morph/Resourceable.php +++ b/app/Pivots/Morph/Resourceable.php @@ -10,6 +10,7 @@ use App\Events\Pivot\Morph\Resourceable\ResourceableCreated; use App\Events\Pivot\Morph\Resourceable\ResourceableDeleted; use App\Events\Pivot\Morph\Resourceable\ResourceableUpdated; use App\Models\Wiki\Anime; +use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; use App\Models\Wiki\Artist; use App\Models\Wiki\ExternalResource; use App\Models\Wiki\Song; @@ -51,6 +52,7 @@ class Resourceable extends BaseMorphPivot */ public static $resourceables = [ Anime::class, + AnimeThemeEntry::class, Artist::class, Song::class, Studio::class, diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1e6fc65ac..89d5c5543 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -64,7 +64,7 @@ class AppServiceProvider extends ServiceProvider DB::listen(function (QueryExecuted $query) { if (app()->isLocal()) { - Log::debug($query->toRawSql()); + Log::debug($query->sql); } }); } diff --git a/app/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatRule.php b/app/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatRule.php new file mode 100644 index 000000000..035d9dc78 --- /dev/null +++ b/app/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatRule.php @@ -0,0 +1,31 @@ +site->getPattern(AnimeThemeEntry::class); + + if ($pattern !== null && Str::match($pattern, $value) !== $value) { + $fail(__('validation.regex')); + } + } +} diff --git a/lang/en/filament.php b/lang/en/filament.php index f36007722..e23236c89 100644 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -516,6 +516,9 @@ return [ 'help' => 'The Version number of the Theme. Can be left blank if there is only one version. Version is only required if there exist at least 2 in the sequence.', 'name' => 'Version', ], + 'youtube' => [ + 'help' => 'An YouTube URL to attach to the entry.', + ], ], 'anime_theme' => [ 'sequence' => [ diff --git a/tests/Unit/Models/Wiki/Anime/Theme/AnimeThemeEntryTest.php b/tests/Unit/Models/Wiki/Anime/Theme/AnimeThemeEntryTest.php index 0bf3c9af3..e8c72fb52 100644 --- a/tests/Unit/Models/Wiki/Anime/Theme/AnimeThemeEntryTest.php +++ b/tests/Unit/Models/Wiki/Anime/Theme/AnimeThemeEntryTest.php @@ -5,10 +5,13 @@ declare(strict_types=1); use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeTheme; use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; +use App\Models\Wiki\ExternalResource; use App\Models\Wiki\Video; +use App\Pivots\Morph\Resourceable; use App\Pivots\Wiki\AnimeThemeEntryVideo; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Foundation\Testing\WithFaker; use Znck\Eloquent\Relations\BelongsToThrough; @@ -55,6 +58,20 @@ test('theme', function () { $this->assertInstanceOf(AnimeTheme::class, $entry->animetheme()->first()); }); +test('external resources', function () { + $resourcesCount = fake()->randomDigitNotNull(); + + $entry = AnimeThemeEntry::factory() + ->for(AnimeTheme::factory()->for(Anime::factory())) + ->has(ExternalResource::factory()->count($resourcesCount)) + ->createOne(); + + $this->assertInstanceOf(MorphToMany::class, $entry->resources()); + $this->assertEquals($resourcesCount, $entry->resources()->count()); + $this->assertInstanceOf(ExternalResource::class, $entry->resources()->first()); + $this->assertEquals(Resourceable::class, $entry->resources()->getPivotClass()); +}); + test('videos', function () { $videoCount = fake()->randomDigitNotNull(); diff --git a/tests/Unit/Models/Wiki/ExternalResourceTest.php b/tests/Unit/Models/Wiki/ExternalResourceTest.php index 50076b317..6899ae302 100644 --- a/tests/Unit/Models/Wiki/ExternalResourceTest.php +++ b/tests/Unit/Models/Wiki/ExternalResourceTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); use App\Enums\Models\Wiki\ResourceSite; use App\Models\Wiki\Anime; +use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; use App\Models\Wiki\Artist; use App\Models\Wiki\ExternalResource; use App\Models\Wiki\Song; @@ -47,6 +48,19 @@ test('anime', function () { $this->assertEquals(Resourceable::class, $resource->anime()->getPivotClass()); }); +test('anime theme entry', function () { + $entryCount = fake()->randomDigitNotNull(); + + $resource = ExternalResource::factory() + ->has(AnimeThemeEntry::factory()->forAnime()->count($entryCount)) + ->createOne(); + + $this->assertInstanceOf(MorphToMany::class, $resource->animethemeentries()); + $this->assertEquals($entryCount, $resource->animethemeentries()->count()); + $this->assertInstanceOf(AnimeThemeEntry::class, $resource->animethemeentries()->first()); + $this->assertEquals(Resourceable::class, $resource->animethemeentries()->getPivotClass()); +}); + test('artists', function () { $artistCount = fake()->randomDigitNotNull(); diff --git a/tests/Unit/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatTest.php b/tests/Unit/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatTest.php new file mode 100644 index 000000000..1686de281 --- /dev/null +++ b/tests/Unit/Rules/Wiki/Resource/AnimeThemeEntryResourceLinkFormatTest.php @@ -0,0 +1,101 @@ +word(); + + $validator = Validator::make( + [$attribute => fake()->url()], + [$attribute => new AnimeThemeEntryResourceLinkFormatRule(ResourceSite::YOUTUBE_MUSIC)], + ); + + $this->assertFalse($validator->passes()); +}); + +test('passes for pattern', function () { + /** @var ResourceSite $site */ + $site = Arr::random(ResourceSite::getForModel(AnimeThemeEntry::class)); + + $url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word(), 'null'); + + $attribute = fake()->word(); + + $validator = Validator::make( + [$attribute => $url], + [$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)], + ); + + $this->assertTrue($validator->passes()); +}); + +test('fails for trailing slash', function () { + /** @var ResourceSite $site */ + $site = Arr::random(ResourceSite::getForModel(AnimeThemeEntry::class)); + + $url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word()); + + $url = Str::of($url) + ->append('/') + ->__toString(); + + $attribute = fake()->word(); + + $validator = Validator::make( + [$attribute => $url], + [$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)], + ); + + $this->assertFalse($site->getPattern(AnimeThemeEntry::class) && $validator->passes()); +}); + +test('fails for trailing slug', function () { + /** @var ResourceSite $site */ + $site = Arr::random(ResourceSite::getForModel(AnimeThemeEntry::class)); + + $url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word()); + + $url = Str::of($url) + ->append('/') + ->append(fake()->word()) + ->__toString(); + + $attribute = fake()->word(); + + $validator = Validator::make( + [$attribute => $url], + [$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)], + ); + + $this->assertFalse($site->getPattern(AnimeThemeEntry::class) && $validator->passes()); +}); + +test('fails for other resources', function () { + /** @var ResourceSite $site */ + $site = Arr::random( + array_filter( + ResourceSite::cases(), + fn ($value) => ! in_array($value, ResourceSite::getForModel(AnimeThemeEntry::class)) + ) + ); + + $url = $site->formatResourceLink(AnimeThemeEntry::class, fake()->randomDigitNotNull(), fake()->word()); + + $attribute = fake()->word(); + + $validator = Validator::make( + [$attribute => $url], + [$attribute => new AnimeThemeEntryResourceLinkFormatRule($site)], + ); + + $this->assertFalse($validator->passes()); +});