mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
* feat: support images for studios * style: fix StyleCI findings * fix(admin): throw exception for image request * style: fix StyleCI findings * style: remove extraneous constant * fix(admin): resources mapping may not exist when sending pipe notification * style: fix StyleCI findings
This commit is contained in:
@@ -25,6 +25,7 @@ use App\Pivots\ArtistImage;
|
||||
use App\Pivots\ArtistMember;
|
||||
use App\Pivots\ArtistResource;
|
||||
use App\Pivots\ArtistSong;
|
||||
use App\Pivots\StudioImage;
|
||||
use App\Pivots\StudioResource;
|
||||
|
||||
/**
|
||||
@@ -89,6 +90,7 @@ class WikiDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
Series::TABLE,
|
||||
Song::TABLE,
|
||||
Studio::TABLE,
|
||||
StudioImage::TABLE,
|
||||
StudioResource::TABLE,
|
||||
Video::TABLE,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Pivot\StudioImage;
|
||||
|
||||
use App\Events\Base\Pivot\PivotCreatedEvent;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioImage;
|
||||
|
||||
/**
|
||||
* Class StudioImageCreated.
|
||||
*
|
||||
* @extends PivotCreatedEvent<Studio, Image>
|
||||
*/
|
||||
class StudioImageCreated extends PivotCreatedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param StudioImage $studioImage
|
||||
*/
|
||||
public function __construct(StudioImage $studioImage)
|
||||
{
|
||||
parent::__construct($studioImage->studio, $studioImage->image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
$foreign = $this->getForeign();
|
||||
$related = $this->getRelated();
|
||||
|
||||
return "Image '**{$foreign->getName()}**' has been attached to Studio '**{$related->getName()}**'.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Pivot\StudioImage;
|
||||
|
||||
use App\Events\Base\Pivot\PivotDeletedEvent;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioImage;
|
||||
|
||||
/**
|
||||
* Class StudioImageDeleted.
|
||||
*
|
||||
* @extends PivotDeletedEvent<Studio, Image>
|
||||
*/
|
||||
class StudioImageDeleted extends PivotDeletedEvent
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param StudioImage $studioImage
|
||||
*/
|
||||
public function __construct(StudioImage $studioImage)
|
||||
{
|
||||
parent::__construct($studioImage->studio, $studioImage->image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description for the Discord message payload.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDiscordMessageDescription(): string
|
||||
{
|
||||
$foreign = $this->getForeign();
|
||||
$related = $this->getRelated();
|
||||
|
||||
return "Image '**{$foreign->getName()}**' has been detached from Studio '**{$related->getName()}**'.";
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class ImageSchema extends EloquentSchema
|
||||
return [
|
||||
new AllowedInclude(new AnimeSchema(), Image::RELATION_ANIME),
|
||||
new AllowedInclude(new ArtistSchema(), Image::RELATION_ARTISTS),
|
||||
new AllowedInclude(new StudioSchema(), Image::RELATION_STUDIOS),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class StudioSchema extends EloquentSchema
|
||||
return [
|
||||
new AllowedInclude(new AnimeSchema(), Studio::RELATION_ANIME),
|
||||
new AllowedInclude(new ExternalResourceSchema(), Studio::RELATION_RESOURCES),
|
||||
new AllowedInclude(new ImageSchema(), Studio::RELATION_IMAGES),
|
||||
|
||||
// Undocumented paths needed for client builds
|
||||
new AllowedInclude(new ImageSchema(), 'anime.images'),
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Api\Query\ReadQuery;
|
||||
use App\Http\Resources\BaseResource;
|
||||
use App\Http\Resources\Wiki\Collection\AnimeCollection;
|
||||
use App\Http\Resources\Wiki\Collection\ArtistCollection;
|
||||
use App\Http\Resources\Wiki\Collection\StudioCollection;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -84,6 +85,7 @@ class ImageResource extends BaseResource
|
||||
|
||||
$result[Image::RELATION_ARTISTS] = new ArtistCollection($this->whenLoaded(Image::RELATION_ARTISTS), $this->query);
|
||||
$result[Image::RELATION_ANIME] = new AnimeCollection($this->whenLoaded(Image::RELATION_ANIME), $this->query);
|
||||
$result[Image::RELATION_STUDIOS] = new StudioCollection($this->whenLoaded(Image::RELATION_STUDIOS), $this->query);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Api\Query\ReadQuery;
|
||||
use App\Http\Resources\BaseResource;
|
||||
use App\Http\Resources\Wiki\Collection\AnimeCollection;
|
||||
use App\Http\Resources\Wiki\Collection\ExternalResourceCollection;
|
||||
use App\Http\Resources\Wiki\Collection\ImageCollection;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioResource as StudioResourcePivot;
|
||||
@@ -78,6 +79,7 @@ class StudioResource extends BaseResource
|
||||
|
||||
$result[Studio::RELATION_ANIME] = new AnimeCollection($this->whenLoaded(Studio::RELATION_ANIME), $this->query);
|
||||
$result[Studio::RELATION_RESOURCES] = new ExternalResourceCollection($this->whenLoaded(Studio::RELATION_RESOURCES), $this->query);
|
||||
$result[Studio::RELATION_IMAGES] = new ImageCollection($this->whenLoaded(Studio::RELATION_IMAGES), $this->query);
|
||||
|
||||
if ($this->isAllowedField(StudioResourcePivot::ATTRIBUTE_AS)) {
|
||||
$result[StudioResourcePivot::ATTRIBUTE_AS] = $this->whenPivotLoaded(StudioResourcePivot::TABLE, fn () => $this->pivot->getAttribute(StudioResourcePivot::ATTRIBUTE_AS));
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Events\Wiki\Image\ImageUpdated;
|
||||
use App\Models\BaseModel;
|
||||
use App\Pivots\AnimeImage;
|
||||
use App\Pivots\ArtistImage;
|
||||
use App\Pivots\StudioImage;
|
||||
use BenSampo\Enum\Enum;
|
||||
use Database\Factories\Wiki\ImageFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@@ -29,6 +30,7 @@ use Laravel\Nova\Actions\Actionable;
|
||||
* @property string $mimetype
|
||||
* @property string $path
|
||||
* @property int $size
|
||||
* @property Collection<int, Studio> $studios
|
||||
*
|
||||
* @method static ImageFactory factory(...$parameters)
|
||||
*/
|
||||
@@ -44,6 +46,7 @@ class Image extends BaseModel
|
||||
|
||||
final public const RELATION_ANIME = 'anime';
|
||||
final public const RELATION_ARTISTS = 'artists';
|
||||
final public const RELATION_STUDIOS = 'studios';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -126,4 +129,16 @@ class Image extends BaseModel
|
||||
->using(ArtistImage::class)
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the studios that use this image.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function studios(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Studio::class, StudioImage::TABLE, Image::ATTRIBUTE_ID, Studio::ATTRIBUTE_ID)
|
||||
->using(StudioImage::class)
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Events\Wiki\Studio\StudioUpdated;
|
||||
use App\Models\BaseModel;
|
||||
use App\Pivots\AnimeStudio;
|
||||
use App\Pivots\BasePivot;
|
||||
use App\Pivots\StudioImage;
|
||||
use App\Pivots\StudioResource;
|
||||
use Database\Factories\Wiki\StudioFactory;
|
||||
use ElasticScoutDriverPlus\Searchable;
|
||||
@@ -22,6 +23,7 @@ use Laravel\Nova\Actions\Actionable;
|
||||
* Class Studio.
|
||||
*
|
||||
* @property Collection<int, Anime> $anime
|
||||
* @property Collection<int, Image> $images
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property int $studio_id
|
||||
@@ -41,6 +43,7 @@ class Studio extends BaseModel
|
||||
final public const ATTRIBUTE_SLUG = 'slug';
|
||||
|
||||
final public const RELATION_ANIME = 'anime';
|
||||
final public const RELATION_IMAGES = 'images';
|
||||
final public const RELATION_RESOURCES = 'resources';
|
||||
|
||||
/**
|
||||
@@ -127,4 +130,16 @@ class Studio extends BaseModel
|
||||
->withPivot(StudioResource::ATTRIBUTE_AS)
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the images for the studio.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function images(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Image::class, StudioImage::TABLE, Studio::ATTRIBUTE_ID, Image::ATTRIBUTE_ID)
|
||||
->using(StudioImage::class)
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimePipe;
|
||||
use App\Pipes\BasePipe;
|
||||
use App\Pipes\Wiki\Anime\Image\BackfillLargeCoverImage;
|
||||
use App\Pipes\Wiki\Anime\Image\BackfillSmallCoverImage;
|
||||
use App\Pipes\Wiki\Anime\Resource\BackfillAnidbResource;
|
||||
@@ -165,7 +165,7 @@ class BackfillAnimeAction extends Action implements ShouldQueue
|
||||
*
|
||||
* @param ActionFields $fields
|
||||
* @param Anime $anime
|
||||
* @return BackfillAnimePipe[]
|
||||
* @return BasePipe[]
|
||||
*/
|
||||
protected function getPipes(ActionFields $fields, Anime $anime): array
|
||||
{
|
||||
@@ -184,7 +184,7 @@ class BackfillAnimeAction extends Action implements ShouldQueue
|
||||
* Get the mapping of anime pipes to their form fields.
|
||||
*
|
||||
* @param Anime $anime
|
||||
* @return array<string, BackfillAnimePipe>
|
||||
* @return array<string, BasePipe>
|
||||
*/
|
||||
protected function getPipeMapping(Anime $anime): array
|
||||
{
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Nova\Actions\Wiki\Studio;
|
||||
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pipes\BasePipe;
|
||||
use App\Pipes\Wiki\Studio\Image\BackfillLargeCoverImage;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Nova\Actions\Action;
|
||||
use Laravel\Nova\Fields\ActionFields;
|
||||
use Laravel\Nova\Fields\Boolean;
|
||||
use Laravel\Nova\Fields\Heading;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* Class BackfillStudioAction.
|
||||
*/
|
||||
class BackfillStudioAction extends Action implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
|
||||
final public const BACKFILL_LARGE_COVER = 'backfill_large_cover';
|
||||
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(protected User $user)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable name of the action.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return __('nova.backfill_studio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the action on the given models.
|
||||
*
|
||||
* @param ActionFields $fields
|
||||
* @param Collection<int, Studio> $models
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(ActionFields $fields, Collection $models): mixed
|
||||
{
|
||||
foreach ($models as $studio) {
|
||||
if ($studio->resources()->doesntExist()) {
|
||||
$this->markAsFailed($studio, 'At least one Resource is required to backfill Studio');
|
||||
continue;
|
||||
}
|
||||
|
||||
$pipes = $this->getPipes($fields, $studio);
|
||||
|
||||
$pipeline = new Pipeline(Container::getInstance());
|
||||
|
||||
try {
|
||||
$pipeline->send($this->user)
|
||||
->through($pipes)
|
||||
->then(fn () => $this->markAsFinished($studio));
|
||||
} catch (Exception $e) {
|
||||
$this->markAsFailed($studio, $e);
|
||||
} finally {
|
||||
// Try not to upset third-party APIs
|
||||
sleep(rand(3, 5));
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields available on the action.
|
||||
*
|
||||
* @param NovaRequest $request
|
||||
* @return array
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function fields(NovaRequest $request): array
|
||||
{
|
||||
$studio = $request->resourceId !== null
|
||||
? $request->findModel()
|
||||
: null;
|
||||
|
||||
return [
|
||||
Heading::make(__('nova.backfill_images')),
|
||||
|
||||
Boolean::make(__('nova.backfill_large_cover'), self::BACKFILL_LARGE_COVER)
|
||||
->help(__('nova.backfill_studio_large_cover_help'))
|
||||
->default(fn () => $studio instanceof Studio && $studio->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE)->doesntExist()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected pipes for backfilling studios.
|
||||
*
|
||||
* @param ActionFields $fields
|
||||
* @param Studio $studio
|
||||
* @return BasePipe[]
|
||||
*/
|
||||
protected function getPipes(ActionFields $fields, Studio $studio): array
|
||||
{
|
||||
$pipes = [];
|
||||
|
||||
foreach ($this->getPipeMapping($studio) as $field => $pipe) {
|
||||
if (Arr::get($fields, $field) === true) {
|
||||
$pipes[] = $pipe;
|
||||
}
|
||||
}
|
||||
|
||||
return $pipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping of studio pipes to their form fields.
|
||||
*
|
||||
* @param Studio $studio
|
||||
* @return array<string, BasePipe>
|
||||
*/
|
||||
protected function getPipeMapping(Studio $studio): array
|
||||
{
|
||||
return [
|
||||
self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImage($studio),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@ class ImageUnlinkedLens extends BaseLens
|
||||
{
|
||||
return $query->whereNot(Image::ATTRIBUTE_FACET, ImageFacet::GRILL)
|
||||
->whereDoesntHave(Image::RELATION_ANIME)
|
||||
->whereDoesntHave(Image::RELATION_ARTISTS);
|
||||
->whereDoesntHave(Image::RELATION_ARTISTS)
|
||||
->whereDoesntHave(Image::RELATION_STUDIOS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Nova\Lenses\Studio;
|
||||
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Nova\Lenses\BaseLens;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
use Laravel\Nova\Http\Requests\LensRequest;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* Class StudioCoverLargeLens.
|
||||
*/
|
||||
class StudioCoverLargeLens extends BaseLens
|
||||
{
|
||||
/**
|
||||
* Get the displayable name of the lens.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return __('nova.studio_image_lens', ['facet' => ImageFacet::getDescription(ImageFacet::COVER_LARGE)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query builder / paginator for the lens.
|
||||
*
|
||||
* @param LensRequest $request
|
||||
* @param Builder $query
|
||||
* @return Builder
|
||||
*/
|
||||
public static function query(LensRequest $request, $query): Builder
|
||||
{
|
||||
return $request->withOrdering($request->withFilters(
|
||||
static::criteria($query)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* The criteria used to refine the models for the lens.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return Builder
|
||||
*/
|
||||
public static function criteria(Builder $query): Builder
|
||||
{
|
||||
return $query->whereDoesntHave(Studio::RELATION_IMAGES, function (Builder $imageQuery) {
|
||||
$imageQuery->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields available to the lens.
|
||||
*
|
||||
* @param NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function fields(NovaRequest $request): array
|
||||
{
|
||||
return [
|
||||
ID::make(__('nova.id'), Studio::ATTRIBUTE_ID)
|
||||
->sortable(),
|
||||
|
||||
Text::make(__('nova.name'), Studio::ATTRIBUTE_NAME)
|
||||
->sortable()
|
||||
->filterable(),
|
||||
|
||||
Text::make(__('nova.slug'), Studio::ATTRIBUTE_SLUG)
|
||||
->sortable()
|
||||
->filterable(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions available on the lens.
|
||||
*
|
||||
* @param NovaRequest $request
|
||||
* @return array
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function actions(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI key for the lens.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function uriKey(): string
|
||||
{
|
||||
return 'studio-cover-large-lens';
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Nova\Resources\Admin;
|
||||
|
||||
use App\Models\Admin\Announcement as AnnouncementModel;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use Laravel\Nova\Fields\Code;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
@@ -16,7 +16,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Announcement.
|
||||
*/
|
||||
class Announcement extends Resource
|
||||
class Announcement extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Auth;
|
||||
use App\Enums\Models\Auth\InvitationStatus;
|
||||
use App\Models\Auth\Invitation as InvitationModel;
|
||||
use App\Nova\Actions\Auth\Invitation\ResendInvitationAction;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use BenSampo\Enum\Enum;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Exception;
|
||||
@@ -23,7 +23,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Invitation.
|
||||
*/
|
||||
class Invitation extends Resource
|
||||
class Invitation extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Auth;
|
||||
use App\Models\Auth\Permission as PermissionModel;
|
||||
use App\Nova\Actions\Auth\Permission\GiveRoleAction;
|
||||
use App\Nova\Actions\Auth\Permission\RevokeRoleAction;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use Exception;
|
||||
use Laravel\Nova\Fields\BelongsToMany;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
@@ -18,7 +18,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Permission.
|
||||
*/
|
||||
class Permission extends Resource
|
||||
class Permission extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Auth;
|
||||
use App\Models\Auth\Role as RoleModel;
|
||||
use App\Nova\Actions\Auth\Role\GivePermissionAction;
|
||||
use App\Nova\Actions\Auth\Role\RevokePermissionAction;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use Exception;
|
||||
use Laravel\Nova\Fields\BelongsToMany;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
@@ -18,7 +18,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Role.
|
||||
*/
|
||||
class Role extends Resource
|
||||
class Role extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Nova\Actions\Auth\User\GivePermissionAction;
|
||||
use App\Nova\Actions\Auth\User\GiveRoleAction;
|
||||
use App\Nova\Actions\Auth\User\RevokePermissionAction;
|
||||
use App\Nova\Actions\Auth\User\RevokeRoleAction;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use Exception;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Nova\Fields\BelongsToMany;
|
||||
@@ -24,7 +24,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class User.
|
||||
*/
|
||||
class User extends Resource
|
||||
class User extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -11,7 +11,7 @@ use Laravel\Nova\Resource as NovaResource;
|
||||
/**
|
||||
* Class Resource.
|
||||
*/
|
||||
abstract class Resource extends NovaResource
|
||||
abstract class BaseResource extends NovaResource
|
||||
{
|
||||
/**
|
||||
* The number of results to display when searching relatable resource without Scout.
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Billing;
|
||||
use App\Enums\Models\Billing\BalanceFrequency;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use App\Models\Billing\Balance as BalanceModel;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use BenSampo\Enum\Enum;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Laravel\Nova\Fields\Currency;
|
||||
@@ -21,7 +21,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Balance.
|
||||
*/
|
||||
class Balance extends Resource
|
||||
class Balance extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Nova\Resources\Billing;
|
||||
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use App\Models\Billing\Transaction as TransactionModel;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use BenSampo\Enum\Enum;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Exception;
|
||||
@@ -22,7 +22,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Transaction.
|
||||
*/
|
||||
class Transaction extends Resource
|
||||
class Transaction extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Nova\Resources\Document;
|
||||
|
||||
use App\Models\Document\Page as PageModel;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use Exception;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
@@ -19,7 +19,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Page.
|
||||
*/
|
||||
class Page extends Resource
|
||||
class Page extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -19,7 +19,7 @@ use App\Nova\Lenses\Anime\AnimePlanetResourceLens;
|
||||
use App\Nova\Lenses\Anime\AnimeStudioLens;
|
||||
use App\Nova\Metrics\Anime\AnimePerDay;
|
||||
use App\Nova\Metrics\Anime\NewAnime;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime\Synonym;
|
||||
use App\Nova\Resources\Wiki\Anime\Theme;
|
||||
use App\Pivots\AnimeResource;
|
||||
@@ -46,7 +46,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Anime.
|
||||
*/
|
||||
class Anime extends Resource
|
||||
class Anime extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Nova\Resources\Wiki\Anime;
|
||||
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Nova\Fields\BelongsTo;
|
||||
@@ -18,7 +18,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Synonym.
|
||||
*/
|
||||
class Synonym extends Resource
|
||||
class Synonym extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Nova\Resources\Wiki\Anime;
|
||||
|
||||
use App\Enums\Models\Wiki\ThemeType;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime;
|
||||
use App\Nova\Resources\Wiki\Anime\Theme\Entry;
|
||||
use App\Nova\Resources\Wiki\Song;
|
||||
@@ -28,7 +28,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Theme.
|
||||
*/
|
||||
class Theme extends Resource
|
||||
class Theme extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Nova\Resources\Wiki\Anime\Theme;
|
||||
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime;
|
||||
use App\Nova\Resources\Wiki\Anime\Theme;
|
||||
use App\Nova\Resources\Wiki\Video;
|
||||
@@ -25,7 +25,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Entry.
|
||||
*/
|
||||
class Entry extends Resource
|
||||
class Entry extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Nova\Lenses\Artist\ArtistMalResourceLens;
|
||||
use App\Nova\Lenses\Artist\ArtistSongLens;
|
||||
use App\Nova\Metrics\Artist\ArtistsPerDay;
|
||||
use App\Nova\Metrics\Artist\NewArtists;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Pivots\ArtistMember;
|
||||
use App\Pivots\ArtistResource;
|
||||
use App\Pivots\ArtistSong;
|
||||
@@ -34,7 +34,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Artist.
|
||||
*/
|
||||
class Artist extends Resource
|
||||
class Artist extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Wiki;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource as ExternalResourceModel;
|
||||
use App\Nova\Lenses\ExternalResource\ExternalResourceUnlinkedLens;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Pivots\AnimeResource;
|
||||
use App\Pivots\ArtistResource;
|
||||
use App\Pivots\BasePivot;
|
||||
@@ -31,7 +31,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class ExternalResource.
|
||||
*/
|
||||
class ExternalResource extends Resource
|
||||
class ExternalResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Wiki;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Wiki\Image as ImageModel;
|
||||
use App\Nova\Lenses\Image\ImageUnlinkedLens;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Pivots\BasePivot;
|
||||
use BenSampo\Enum\Enum;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
@@ -25,7 +25,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Image.
|
||||
*/
|
||||
class Image extends Resource
|
||||
class Image extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
@@ -170,6 +170,19 @@ class Image extends Resource
|
||||
->hideWhenCreating(),
|
||||
]),
|
||||
|
||||
BelongsToMany::make(__('nova.studios'), ImageModel::RELATION_STUDIOS, Studio::class)
|
||||
->searchable()
|
||||
->filterable()
|
||||
->withSubtitles()
|
||||
->showCreateRelationButton()
|
||||
->fields(fn () => [
|
||||
DateTime::make(__('nova.created_at'), BasePivot::ATTRIBUTE_CREATED_AT)
|
||||
->hideWhenCreating(),
|
||||
|
||||
DateTime::make(__('nova.updated_at'), BasePivot::ATTRIBUTE_UPDATED_AT)
|
||||
->hideWhenCreating(),
|
||||
]),
|
||||
|
||||
Panel::make(__('nova.file_properties'), $this->fileProperties())
|
||||
->collapsable(),
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Wiki;
|
||||
use App\Models\Wiki\Series as SeriesModel;
|
||||
use App\Nova\Metrics\Series\NewSeries;
|
||||
use App\Nova\Metrics\Series\SeriesPerDay;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Pivots\BasePivot;
|
||||
use Exception;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -24,7 +24,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Series.
|
||||
*/
|
||||
class Series extends Resource
|
||||
class Series extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Nova\Resources\Wiki;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Song as SongModel;
|
||||
use App\Nova\Lenses\Song\SongArtistLens;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime\Theme;
|
||||
use App\Pivots\ArtistSong;
|
||||
use App\Pivots\BasePivot;
|
||||
@@ -25,7 +25,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Song.
|
||||
*/
|
||||
class Song extends Resource
|
||||
class Song extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -4,17 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Nova\Resources\Wiki;
|
||||
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Studio as StudioModel;
|
||||
use App\Nova\Actions\Wiki\Studio\BackfillStudioAction;
|
||||
use App\Nova\Lenses\Studio\StudioAniDbResourceLens;
|
||||
use App\Nova\Lenses\Studio\StudioAnilistResourceLens;
|
||||
use App\Nova\Lenses\Studio\StudioAnimePlanetResourceLens;
|
||||
use App\Nova\Lenses\Studio\StudioAnnResourceLens;
|
||||
use App\Nova\Lenses\Studio\StudioCoverLargeLens;
|
||||
use App\Nova\Lenses\Studio\StudioMalResourceLens;
|
||||
use App\Nova\Lenses\Studio\StudioUnlinkedLens;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Pivots\BasePivot;
|
||||
use App\Pivots\StudioResource;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Nova\Fields\BelongsToMany;
|
||||
use Laravel\Nova\Fields\DateTime;
|
||||
@@ -28,7 +32,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Studio.
|
||||
*/
|
||||
class Studio extends Resource
|
||||
class Studio extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
@@ -176,11 +180,50 @@ class Studio extends Resource
|
||||
->hideWhenUpdating(),
|
||||
]),
|
||||
|
||||
BelongsToMany::make(__('nova.images'), StudioModel::RELATION_IMAGES, Image::class)
|
||||
->searchable()
|
||||
->filterable()
|
||||
->withSubtitles()
|
||||
->showCreateRelationButton()
|
||||
->fields(fn () => [
|
||||
DateTime::make(__('nova.created_at'), BasePivot::ATTRIBUTE_CREATED_AT)
|
||||
->hideWhenCreating(),
|
||||
|
||||
DateTime::make(__('nova.updated_at'), BasePivot::ATTRIBUTE_UPDATED_AT)
|
||||
->hideWhenCreating(),
|
||||
]),
|
||||
|
||||
Panel::make(__('nova.timestamps'), $this->timestamps())
|
||||
->collapsable(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions available for the resource.
|
||||
*
|
||||
* @param NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function actions(NovaRequest $request): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::actions($request),
|
||||
[
|
||||
(new BackfillStudioAction($request->user()))
|
||||
->confirmButtonText(__('nova.backfill'))
|
||||
->cancelButtonText(__('nova.cancel'))
|
||||
->showOnIndex()
|
||||
->showOnDetail()
|
||||
->showInline()
|
||||
->canSee(function (Request $request) {
|
||||
$user = $request->user();
|
||||
|
||||
return $user instanceof User && $user->can('update studio');
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lenses available for the resource.
|
||||
*
|
||||
@@ -194,6 +237,7 @@ class Studio extends Resource
|
||||
[
|
||||
new StudioAniDbResourceLens(),
|
||||
new StudioAnilistResourceLens(),
|
||||
new StudioCoverLargeLens(),
|
||||
new StudioAnimePlanetResourceLens(),
|
||||
new StudioAnnResourceLens(),
|
||||
new StudioMalResourceLens(),
|
||||
|
||||
@@ -12,7 +12,7 @@ use App\Nova\Lenses\Video\VideoSourceLens;
|
||||
use App\Nova\Lenses\Video\VideoUnlinkedLens;
|
||||
use App\Nova\Metrics\Video\NewVideos;
|
||||
use App\Nova\Metrics\Video\VideosPerDay;
|
||||
use App\Nova\Resources\Resource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime\Theme\Entry;
|
||||
use App\Pivots\BasePivot;
|
||||
use BenSampo\Enum\Enum;
|
||||
@@ -33,7 +33,7 @@ use Laravel\Nova\Query\Search\Column;
|
||||
/**
|
||||
* Class Video.
|
||||
*/
|
||||
class Video extends Resource
|
||||
class Video extends BaseResource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes;
|
||||
|
||||
use App\Contracts\Pipes\Pipe;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\BaseModel;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Notifications\NovaNotification;
|
||||
|
||||
/**
|
||||
* Class BasePipe.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class BasePipe implements Pipe
|
||||
{
|
||||
/**
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected BaseModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model passed into the pipeline.
|
||||
*
|
||||
* @return TModel
|
||||
*/
|
||||
abstract public function getModel(): BaseModel;
|
||||
|
||||
/**
|
||||
* Get the human-friendly label for the underlying model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function label(): string
|
||||
{
|
||||
return Str::headline(class_basename($this->getModel()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification for user to review failure.
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
protected function sendNotification(User $user, string $message): void
|
||||
{
|
||||
$resource = $this->resource();
|
||||
|
||||
$uriKey = $resource::uriKey();
|
||||
|
||||
$user->notify(
|
||||
NovaNotification::make()
|
||||
->icon('flag')
|
||||
->message($message)
|
||||
->type(NovaNotification::WARNING_TYPE)
|
||||
->url("/resources/$uriKey/{$this->getModel()->getKey()}")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to resources.
|
||||
*
|
||||
* @return Relation
|
||||
*/
|
||||
abstract protected function relation(): Relation;
|
||||
|
||||
/**
|
||||
* Get the nova resource.
|
||||
*
|
||||
* @return class-string<BaseResource>
|
||||
*/
|
||||
abstract protected function resource(): string;
|
||||
}
|
||||
@@ -4,80 +4,59 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime;
|
||||
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pivots\AnimeImage;
|
||||
use Closure;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime as AnimeResource;
|
||||
use App\Pipes\Wiki\BackfillImage;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeImage.
|
||||
*
|
||||
* @extends BackfillImage<Anime>
|
||||
*/
|
||||
abstract class BackfillAnimeImage extends BackfillAnimePipe
|
||||
abstract class BackfillAnimeImage extends BackfillImage
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Closure(User): mixed $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestException
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function handle(User $user, Closure $next): mixed
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
if ($this->anime->images()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->exists()) {
|
||||
Log::info("Anime '{$this->anime->getName()}' already has Image of Facet '{$this->getFacet()->value}'.");
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
$image = $this->getImage();
|
||||
|
||||
if ($image !== null) {
|
||||
$this->attachImageToAnime($image);
|
||||
}
|
||||
|
||||
if ($this->anime->images()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->doesntExist()) {
|
||||
$this->sendNotification(
|
||||
$user,
|
||||
"Anime '{$this->anime->getName()}' has no {$this->getFacet()->description} Image after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Image from response.
|
||||
* Get the model passed into the pipeline.
|
||||
*
|
||||
* @param string $url
|
||||
* @return Image
|
||||
* @return Anime
|
||||
*/
|
||||
protected function createImage(string $url): Image
|
||||
public function getModel(): Anime
|
||||
{
|
||||
$imageResponse = Http::get($url);
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
$image = $imageResponse->body();
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->images();
|
||||
}
|
||||
|
||||
$file = File::createWithContent(basename($url), $image);
|
||||
|
||||
$fs = Storage::disk('images');
|
||||
|
||||
$fsFile = $fs->putFile('', $file);
|
||||
|
||||
/** @var Image $image */
|
||||
$image = Image::query()->create([
|
||||
Image::ATTRIBUTE_FACET => $this->getFacet()->value,
|
||||
Image::ATTRIBUTE_PATH => $fsFile,
|
||||
]);
|
||||
|
||||
return $image;
|
||||
/**
|
||||
* Get the nova resource.
|
||||
*
|
||||
* @return class-string<BaseResource>
|
||||
*/
|
||||
protected function resource(): string
|
||||
{
|
||||
return AnimeResource::class;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,31 +65,9 @@ abstract class BackfillAnimeImage extends BackfillAnimePipe
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
protected function attachImageToAnime(Image $image): void
|
||||
protected function attachImage(Image $image): void
|
||||
{
|
||||
if (AnimeImage::query()
|
||||
->where($this->anime->getKeyName(), $this->anime->getKey())
|
||||
->where($image->getKeyName(), $image->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching image '{$image->getName()}' to anime '{$this->anime->getName()}'");
|
||||
$image->anime()->attach($this->anime);
|
||||
}
|
||||
Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
abstract protected function getFacet(): ImageFacet;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getImage(): ?Image;
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime;
|
||||
|
||||
use App\Contracts\Pipes\Pipe;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Nova\Resources\Wiki\Anime as AnimeResource;
|
||||
use Laravel\Nova\Notifications\NovaNotification;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimePipe.
|
||||
*/
|
||||
abstract class BackfillAnimePipe implements Pipe
|
||||
{
|
||||
/**
|
||||
* Create new pipe instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(protected Anime $anime)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification for user to review anime without studios.
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
protected function sendNotification(User $user, string $message): void
|
||||
{
|
||||
$uriKey = AnimeResource::uriKey();
|
||||
|
||||
$user->notify(
|
||||
NovaNotification::make()
|
||||
->icon('flag')
|
||||
->message($message)
|
||||
->type(NovaNotification::WARNING_TYPE)
|
||||
->url("/resources/$uriKey/{$this->anime->getKey()}")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,50 +5,61 @@ declare(strict_types=1);
|
||||
namespace App\Pipes\Wiki\Anime;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime as AnimeNovaResource;
|
||||
use App\Pipes\Wiki\BackfillResource;
|
||||
use App\Pivots\AnimeResource;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeResource.
|
||||
*
|
||||
* @extends BackfillResource<Anime>
|
||||
*/
|
||||
abstract class BackfillAnimeResource extends BackfillAnimePipe
|
||||
abstract class BackfillAnimeResource extends BackfillResource
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Closure(User): mixed $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestException
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function handle(User $user, Closure $next): mixed
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
if ($this->anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->exists()) {
|
||||
Log::info("Anime '{$this->anime->getName()}' already has Resource of Site '{$this->getSite()->value}'.");
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
/**
|
||||
* Get the model passed into the pipeline.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
public function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
$resource = $this->getResource();
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->resources();
|
||||
}
|
||||
|
||||
if ($resource !== null) {
|
||||
$this->attachResourceToAnime($resource);
|
||||
}
|
||||
|
||||
if ($this->anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->doesntExist()) {
|
||||
$this->sendNotification(
|
||||
$user,
|
||||
"Anime '{$this->anime->getName()}' has no {$this->getSite()->description} Resource after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
/**
|
||||
* Get the nova resource.
|
||||
*
|
||||
* @return class-string<BaseResource>
|
||||
*/
|
||||
protected function resource(): string
|
||||
{
|
||||
return AnimeNovaResource::class;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,11 +74,11 @@ abstract class BackfillAnimeResource extends BackfillAnimePipe
|
||||
$resource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->whereHas(ExternalResource::RELATION_ANIME, fn (Builder $animeQuery) => $animeQuery->whereKey($this->anime))
|
||||
->whereHas(ExternalResource::RELATION_ANIME, fn (Builder $animeQuery) => $animeQuery->whereKey($this->getModel()))
|
||||
->first();
|
||||
|
||||
if ($resource === null) {
|
||||
Log::info("Creating {$this->getSite()->description} resource '$id'");
|
||||
Log::info("Creating {$this->getSite()->description} Resource '$id'");
|
||||
|
||||
$resource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
@@ -85,31 +96,15 @@ abstract class BackfillAnimeResource extends BackfillAnimePipe
|
||||
* @param ExternalResource $resource
|
||||
* @return void
|
||||
*/
|
||||
protected function attachResourceToAnime(ExternalResource $resource): void
|
||||
protected function attachResource(ExternalResource $resource): void
|
||||
{
|
||||
if (AnimeResource::query()
|
||||
->where($this->anime->getKeyName(), $this->anime->getKey())
|
||||
->where($this->getModel()->getKeyName(), $this->getModel()->getKey())
|
||||
->where($resource->getKeyName(), $resource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching resource '{$resource->getName()}' to anime '{$this->anime->getName()}'");
|
||||
$resource->anime()->attach($this->anime);
|
||||
Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($resource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
*
|
||||
* @return ResourceSite
|
||||
*/
|
||||
abstract protected function getSite(): ResourceSite;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Resource mapping.
|
||||
*
|
||||
* @return ExternalResource|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getResource(): ?ExternalResource;
|
||||
}
|
||||
|
||||
@@ -37,12 +37,9 @@ class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbImage($anilistResource);
|
||||
if ($anidbResource !== null) {
|
||||
return $anidbResource;
|
||||
}
|
||||
return $this->getAnilistImage($anilistResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -56,7 +53,7 @@ class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnidbImage(ExternalResource $anilistResource): ?Image
|
||||
protected function getAnilistImage(ExternalResource $anilistResource): ?Image
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
|
||||
@@ -37,12 +37,9 @@ class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbImage($anilistResource);
|
||||
if ($anidbResource !== null) {
|
||||
return $anidbResource;
|
||||
}
|
||||
return $this->getAnilistImage($anilistResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -56,7 +53,7 @@ class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnidbImage(ExternalResource $anilistResource): ?Image
|
||||
protected function getAnilistImage(ExternalResource $anilistResource): ?Image
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
|
||||
@@ -37,7 +37,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case AniDB Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbMapping($malResource, 'myanimelist');
|
||||
if ($anidbResource !== null) {
|
||||
@@ -48,7 +48,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbMapping($anilistResource, 'anilist');
|
||||
if ($anidbResource !== null) {
|
||||
@@ -59,7 +59,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getAnidbMapping($kitsuResource, 'kitsu');
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case Anilist Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$anilistResource = $this->getMalAnilistMapping($malResource);
|
||||
if ($anilistResource !== null) {
|
||||
@@ -46,7 +46,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$anilistResource = $this->getKitsuAnilistMapping($kitsuResource);
|
||||
if ($anilistResource !== null) {
|
||||
@@ -54,7 +54,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
return $this->getAnidbAnilistMapping($anidbResource);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class BackfillAnnResource extends BackfillAnimeResource
|
||||
*/
|
||||
protected function getResource(): ?ExternalResource
|
||||
{
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getKitsuAnnMapping($kitsuResource);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case Kitsu Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($malResource, 'MYANIMELIST_ANIME');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -49,7 +49,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($anilistResource, 'ANILIST_ANIME');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -60,7 +60,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($anidbResource, 'ANIDB');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -71,7 +71,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$annResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
$annResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
if ($annResource instanceof ExternalResource) {
|
||||
return $this->getKitsuMapping($annResource, 'ANIMENEWSNETWORK');
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
*/
|
||||
protected function getResource(): ?ExternalResource
|
||||
{
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$malResource = $this->getKitsuMalMapping($kitsuResource);
|
||||
if ($malResource !== null) {
|
||||
@@ -44,7 +44,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$malResource = $this->getAnilistMalMapping($anilistResource);
|
||||
if ($malResource !== null) {
|
||||
@@ -52,7 +52,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
return $this->getAnidbMalMapping($anidbResource);
|
||||
}
|
||||
|
||||
@@ -5,56 +5,66 @@ declare(strict_types=1);
|
||||
namespace App\Pipes\Wiki\Anime\Studio;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimePipe;
|
||||
use App\Pivots\StudioResource;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Anime as AnimeResource;
|
||||
use App\Pipes\Wiki\BackfillStudios;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Orchestra\Parser\Xml\Facade as XmlParser;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeStudios.
|
||||
*
|
||||
* @extends BackfillStudios<Anime>
|
||||
*/
|
||||
class BackfillAnimeStudios extends BackfillAnimePipe
|
||||
class BackfillAnimeStudios extends BackfillStudios
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Closure(User): mixed $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestException
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function handle(User $user, Closure $next): mixed
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
if ($this->anime->studios()->exists()) {
|
||||
Log::info("Anime '{$this->anime->getName()}' already has Studios.");
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
/**
|
||||
* Get the model passed into the pipeline.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
public function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
$studios = $this->getStudios();
|
||||
/**
|
||||
* Get the relation to studios.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->studios();
|
||||
}
|
||||
|
||||
if (! empty($studios)) {
|
||||
$this->attachStudiosToAnime($studios);
|
||||
}
|
||||
|
||||
if ($this->anime->studios()->doesntExist()) {
|
||||
$this->sendNotification($user, "Anime '{$this->anime->getName()}' has no Studios after backfilling. Please review.");
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
/**
|
||||
* Get the nova resource.
|
||||
*
|
||||
* @return class-string<BaseResource>
|
||||
*/
|
||||
protected function resource(): string
|
||||
{
|
||||
return AnimeResource::class;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +76,7 @@ class BackfillAnimeStudios extends BackfillAnimePipe
|
||||
*/
|
||||
protected function getStudios(): array
|
||||
{
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$studios = $this->getMalAnimeStudios($malResource);
|
||||
if (! empty($studios)) {
|
||||
@@ -74,7 +84,7 @@ class BackfillAnimeStudios extends BackfillAnimePipe
|
||||
}
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$studios = $this->getAnilistAnimeStudios($anilistResource);
|
||||
if (! empty($studios)) {
|
||||
@@ -82,7 +92,7 @@ class BackfillAnimeStudios extends BackfillAnimePipe
|
||||
}
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$studios = $this->getKitsuAnimeStudios($kitsuResource);
|
||||
if (! empty($studios)) {
|
||||
@@ -90,7 +100,7 @@ class BackfillAnimeStudios extends BackfillAnimePipe
|
||||
}
|
||||
}
|
||||
|
||||
$annResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
$annResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
if ($annResource instanceof ExternalResource) {
|
||||
return $this->getAnnAnimeStudios($annResource);
|
||||
}
|
||||
@@ -299,84 +309,14 @@ class BackfillAnimeStudios extends BackfillAnimePipe
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create Studio from name (case-insensitive).
|
||||
*
|
||||
* @param string $name
|
||||
* @return Studio
|
||||
*/
|
||||
protected function getOrCreateStudio(string $name): Studio
|
||||
{
|
||||
$column = Studio::ATTRIBUTE_NAME;
|
||||
$studio = Studio::query()
|
||||
->where(DB::raw("lower($column)"), Str::lower($name))
|
||||
->first();
|
||||
|
||||
if (! $studio instanceof Studio) {
|
||||
Log::info("Creating studio '$name'");
|
||||
|
||||
$studio = Studio::query()->create([
|
||||
Studio::ATTRIBUTE_NAME => $name,
|
||||
Studio::ATTRIBUTE_SLUG => Str::slug($name, '_'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $studio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Studio has Resource.
|
||||
*
|
||||
* @param Studio $studio
|
||||
* @param ResourceSite $site
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureStudioHasResource(Studio $studio, ResourceSite $site, int $id): void
|
||||
{
|
||||
$studioResource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $site->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->whereHas(ExternalResource::RELATION_STUDIOS, fn (Builder $studioQuery) => $studioQuery->whereKey($studio))
|
||||
->first();
|
||||
|
||||
if (! $studioResource instanceof ExternalResource) {
|
||||
Log::info("Creating studio resource with site '$site->value' and id '$id'");
|
||||
|
||||
$studioResource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
ExternalResource::ATTRIBUTE_LINK => ResourceSite::formatStudioResourceLink($site, $id),
|
||||
ExternalResource::ATTRIBUTE_SITE => $site->value,
|
||||
]);
|
||||
}
|
||||
|
||||
if (StudioResource::query()
|
||||
->where($studio->getKeyName(), $studio->getKey())
|
||||
->where($studioResource->getKeyName(), $studioResource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching resource '$studioResource->link' to studio '{$studio->getName()}'");
|
||||
$studioResource->studios()->attach($studio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Studios to Anime.
|
||||
* Attach Studios.
|
||||
*
|
||||
* @param Studio[] $studios
|
||||
* @return void
|
||||
*/
|
||||
protected function attachStudiosToAnime(array $studios): void
|
||||
protected function attachStudios(array $studios): void
|
||||
{
|
||||
$results = $this->anime->studios()->sync(Arr::pluck($studios, Studio::ATTRIBUTE_ID));
|
||||
|
||||
if (count($results['attached'])) {
|
||||
Log::info("Attached Studios to anime '{$this->anime->getName()}'", $results['attached']);
|
||||
}
|
||||
if (count($results['updated'])) {
|
||||
Log::info("Updated Studios for anime '{$this->anime->getName()}'", $results['updated']);
|
||||
}
|
||||
if (count($results['detached'])) {
|
||||
Log::info("Detached Studios from anime '{$this->anime->getName()}'", $results['detached']);
|
||||
}
|
||||
Log::info("Attaching studios to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach(Arr::pluck($studios, Studio::ATTRIBUTE_ID));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki;
|
||||
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\BasePipe;
|
||||
use Closure;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BackfillImage.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BasePipe<TModel>
|
||||
*/
|
||||
abstract class BackfillImage extends BasePipe
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Closure(User): mixed $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(User $user, Closure $next): mixed
|
||||
{
|
||||
if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Image of Facet '{$this->getFacet()->value}'.");
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
$image = $this->getImage();
|
||||
|
||||
if ($image !== null) {
|
||||
$this->attachImage($image);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->doesntExist()) {
|
||||
$this->sendNotification(
|
||||
$user,
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no {$this->getFacet()->description} Image after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Image from response.
|
||||
*
|
||||
* @param string $url
|
||||
* @return Image
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function createImage(string $url): Image
|
||||
{
|
||||
$imageResponse = Http::get($url)->throw();
|
||||
|
||||
$image = $imageResponse->body();
|
||||
|
||||
$file = File::createWithContent(basename($url), $image);
|
||||
|
||||
$fs = Storage::disk('images');
|
||||
|
||||
$fsFile = $fs->putFile($this->path(), $file);
|
||||
|
||||
/** @var Image $image */
|
||||
$image = Image::query()->create([
|
||||
Image::ATTRIBUTE_FACET => $this->getFacet()->value,
|
||||
Image::ATTRIBUTE_PATH => $fsFile,
|
||||
]);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to storage image in filesystem.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return Str::of(Str::kebab(class_basename($this->getModel())))
|
||||
->append(DIRECTORY_SEPARATOR)
|
||||
->append(Str::kebab($this->getFacet()->description))
|
||||
->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to model.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachImage(Image $image): void;
|
||||
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
abstract protected function getFacet(): ImageFacet;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getImage(): ?Image;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\BasePipe;
|
||||
use Closure;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillResource.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BasePipe<TModel>
|
||||
*/
|
||||
abstract class BackfillResource extends BasePipe
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Closure(User): mixed $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(User $user, Closure $next): mixed
|
||||
{
|
||||
if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Resource of Site '{$this->getSite()->value}'.");
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
$resource = $this->getResource();
|
||||
|
||||
if ($resource !== null) {
|
||||
$this->attachResource($resource);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->doesntExist()) {
|
||||
$this->sendNotification(
|
||||
$user,
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no {$this->getSite()->description} Resource after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Resource from response.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $slug
|
||||
* @return ExternalResource
|
||||
*/
|
||||
abstract protected function getOrCreateResource(int $id, string $slug = null): ExternalResource;
|
||||
|
||||
/**
|
||||
* Attach External Resource to model.
|
||||
*
|
||||
* @param ExternalResource $resource
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachResource(ExternalResource $resource): void;
|
||||
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
*
|
||||
* @return ResourceSite
|
||||
*/
|
||||
abstract protected function getSite(): ResourceSite;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Resource mapping.
|
||||
*
|
||||
* @return ExternalResource|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getResource(): ?ExternalResource;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki;
|
||||
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pipes\BasePipe;
|
||||
use App\Pivots\StudioResource;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BackfillStudios.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BasePipe<TModel>
|
||||
*/
|
||||
abstract class BackfillStudios extends BasePipe
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Closure(User): mixed $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(User $user, Closure $next): mixed
|
||||
{
|
||||
if ($this->relation()->getQuery()->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Studios.");
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
$studios = $this->getStudios();
|
||||
|
||||
if (! empty($studios)) {
|
||||
$this->attachStudios($studios);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->doesntExist()) {
|
||||
$this->sendNotification($user, "{$this->label()} '{$this->getModel()->getName()}' has no Studios after backfilling. Please review.");
|
||||
}
|
||||
|
||||
return $next($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create Studio from name (case-insensitive).
|
||||
*
|
||||
* @param string $name
|
||||
* @return Studio
|
||||
*/
|
||||
protected function getOrCreateStudio(string $name): Studio
|
||||
{
|
||||
$column = Studio::ATTRIBUTE_NAME;
|
||||
$studio = Studio::query()
|
||||
->where(DB::raw("lower($column)"), Str::lower($name))
|
||||
->first();
|
||||
|
||||
if (! $studio instanceof Studio) {
|
||||
Log::info("Creating studio '$name'");
|
||||
|
||||
$studio = Studio::query()->create([
|
||||
Studio::ATTRIBUTE_NAME => $name,
|
||||
Studio::ATTRIBUTE_SLUG => Str::slug($name, '_'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $studio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Studio has Resource.
|
||||
*
|
||||
* @param Studio $studio
|
||||
* @param ResourceSite $site
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureStudioHasResource(Studio $studio, ResourceSite $site, int $id): void
|
||||
{
|
||||
$studioResource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $site->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->whereHas(ExternalResource::RELATION_STUDIOS, fn (Builder $studioQuery) => $studioQuery->whereKey($studio))
|
||||
->first();
|
||||
|
||||
if (! $studioResource instanceof ExternalResource) {
|
||||
Log::info("Creating studio resource with site '$site->value' and id '$id'");
|
||||
|
||||
$studioResource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
ExternalResource::ATTRIBUTE_LINK => ResourceSite::formatStudioResourceLink($site, $id),
|
||||
ExternalResource::ATTRIBUTE_SITE => $site->value,
|
||||
]);
|
||||
}
|
||||
|
||||
if (StudioResource::query()
|
||||
->where($studio->getKeyName(), $studio->getKey())
|
||||
->where($studioResource->getKeyName(), $studioResource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching resource '$studioResource->link' to studio '{$studio->getName()}'");
|
||||
$studioResource->studios()->attach($studio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Studios.
|
||||
*
|
||||
* @param Studio[] $studios
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachStudios(array $studios): void;
|
||||
|
||||
/**
|
||||
* Query third-party API for Studios.
|
||||
*
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getStudios(): array;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Studio;
|
||||
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Nova\Resources\BaseResource;
|
||||
use App\Nova\Resources\Wiki\Studio as StudioResource;
|
||||
use App\Pipes\Wiki\BackfillImage;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeImage.
|
||||
*
|
||||
* @extends BackfillImage<Studio>
|
||||
*/
|
||||
abstract class BackfillStudioImage extends BackfillImage
|
||||
{
|
||||
/**
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param Studio $studio
|
||||
*/
|
||||
public function __construct(Studio $studio)
|
||||
{
|
||||
parent::__construct($studio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model passed into the pipeline.
|
||||
*
|
||||
* @return Studio
|
||||
*/
|
||||
public function getModel(): Studio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nova resource.
|
||||
*
|
||||
* @return class-string<BaseResource>
|
||||
*/
|
||||
protected function resource(): string
|
||||
{
|
||||
return StudioResource::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to Studio.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
protected function attachImage(Image $image): void
|
||||
{
|
||||
Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Studio\Image;
|
||||
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\Wiki\Studio\BackfillStudioImage;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
/**
|
||||
* Class BackfillLargeCoverImage.
|
||||
*/
|
||||
class BackfillLargeCoverImage extends BackfillStudioImage
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
protected function getFacet(): ImageFacet
|
||||
{
|
||||
return ImageFacet::COVER_LARGE();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
return $this->getMalImage($malResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query MAL API for large cover image.
|
||||
*
|
||||
* @param ExternalResource $malResource
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getMalImage(ExternalResource $malResource): ?Image
|
||||
{
|
||||
return $this->createImage("https://cdn.myanimelist.net/img/common/companies/$malResource->external_id.png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pivots;
|
||||
|
||||
use App\Events\Pivot\StudioImage\StudioImageCreated;
|
||||
use App\Events\Pivot\StudioImage\StudioImageDeleted;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Database\Factories\Pivots\StudioImageFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Class StudioImage.
|
||||
*
|
||||
* @property Studio $studio
|
||||
* @property Image $image
|
||||
*
|
||||
* @method static StudioImageFactory factory(...$parameters)
|
||||
*/
|
||||
class StudioImage extends BasePivot
|
||||
{
|
||||
final public const TABLE = 'studio_image';
|
||||
|
||||
final public const ATTRIBUTE_STUDIO = 'studio_id';
|
||||
final public const ATTRIBUTE_IMAGE = 'image_id';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = StudioImage::TABLE;
|
||||
|
||||
/**
|
||||
* The event map for the model.
|
||||
*
|
||||
* Allows for object-based events for native Eloquent events.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dispatchesEvents = [
|
||||
'created' => StudioImageCreated::class,
|
||||
'deleted' => StudioImageDeleted::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the studio that owns the studio image.
|
||||
*
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function studio(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Studio::class, StudioImage::ATTRIBUTE_STUDIO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image that owns the studio image.
|
||||
*
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function image(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Image::class, StudioImage::ATTRIBUTE_IMAGE);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\AnimeImage;
|
||||
use App\Pivots\ArtistImage;
|
||||
use App\Pivots\StudioImage;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
/**
|
||||
@@ -175,4 +177,44 @@ class ImagePolicy
|
||||
{
|
||||
return $user->can('update image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can attach any studio to the image.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function attachAnyStudio(User $user): bool
|
||||
{
|
||||
return $user->can('update image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can attach a studio to the image.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Image $image
|
||||
* @param Studio $studio
|
||||
* @return bool
|
||||
*/
|
||||
public function attachStudio(User $user, Image $image, Studio $studio): bool
|
||||
{
|
||||
$attached = StudioImage::query()
|
||||
->where($image->getKeyName(), $image->getKey())
|
||||
->where($studio->getKeyName(), $studio->getKey())
|
||||
->exists();
|
||||
|
||||
return ! $attached && $user->can('update image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can detach a studio from the image.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function detachStudio(User $user): bool
|
||||
{
|
||||
return $user->can('update image');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace App\Policies\Wiki;
|
||||
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\AnimeStudio;
|
||||
use App\Pivots\StudioImage;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
/**
|
||||
@@ -166,4 +168,44 @@ class StudioPolicy
|
||||
{
|
||||
return $user->can('update studio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can attach any image to the studio.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function attachAnyImage(User $user): bool
|
||||
{
|
||||
return $user->can('update studio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can attach an image to the studio.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Studio $studio
|
||||
* @param Image $image
|
||||
* @return bool
|
||||
*/
|
||||
public function attachImage(User $user, Studio $studio, Image $image): bool
|
||||
{
|
||||
$attached = StudioImage::query()
|
||||
->where($studio->getKeyName(), $studio->getKey())
|
||||
->where($image->getKeyName(), $image->getKey())
|
||||
->exists();
|
||||
|
||||
return ! $attached && $user->can('update studio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can detach an image from the studio.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function detachImage(User $user): bool
|
||||
{
|
||||
return $user->can('update studio');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Scout\Elasticsearch\Api\Schema\Wiki;
|
||||
|
||||
use App\Http\Api\Include\AllowedInclude;
|
||||
use App\Http\Api\Schema\Wiki\ExternalResourceSchema;
|
||||
use App\Http\Api\Schema\Wiki\ImageSchema;
|
||||
use App\Http\Resources\Wiki\Resource\StudioResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Scout\Elasticsearch\Api\Field\Base\IdField;
|
||||
@@ -49,6 +50,7 @@ class StudioSchema extends Schema
|
||||
return [
|
||||
new AllowedInclude(new AnimeSchema(), Studio::RELATION_ANIME),
|
||||
new AllowedInclude(new ExternalResourceSchema(), Studio::RELATION_RESOURCES),
|
||||
new AllowedInclude(new ImageSchema(), Studio::RELATION_IMAGES),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories\Pivots;
|
||||
|
||||
use App\Pivots\StudioImage;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* Class StudioImageFactory.
|
||||
*
|
||||
* @method StudioImage createOne($attributes = [])
|
||||
* @method StudioImage makeOne($attributes = [])
|
||||
*
|
||||
* @extends Factory<StudioImage>
|
||||
*/
|
||||
class StudioImageFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<StudioImage>
|
||||
*/
|
||||
protected $model = StudioImage::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioImage;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable(StudioImage::TABLE)) {
|
||||
Schema::create(StudioImage::TABLE, function (Blueprint $table) {
|
||||
$table->timestamps(6);
|
||||
$table->unsignedBigInteger(StudioImage::ATTRIBUTE_STUDIO);
|
||||
$table->foreign(StudioImage::ATTRIBUTE_STUDIO)->references(Studio::ATTRIBUTE_ID)->on(Studio::TABLE)->cascadeOnDelete();
|
||||
$table->unsignedBigInteger(StudioImage::ATTRIBUTE_IMAGE);
|
||||
$table->foreign(StudioImage::ATTRIBUTE_IMAGE)->references(Image::ATTRIBUTE_ID)->on(Image::TABLE)->cascadeOnDelete();
|
||||
$table->primary([StudioImage::ATTRIBUTE_STUDIO, StudioImage::ATTRIBUTE_IMAGE]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists(StudioImage::TABLE);
|
||||
}
|
||||
};
|
||||
@@ -67,6 +67,8 @@ return [
|
||||
'backfill_resources' => 'Backfill Resources',
|
||||
'backfill_small_cover_help' => 'Use Anilist Resource to map Small Cover Image',
|
||||
'backfill_small_cover' => 'Backfill Small Cover',
|
||||
'backfill_studio_large_cover_help' => 'Use MAL Resource to map Large Cover Image',
|
||||
'backfill_studio' => 'Backfill Studio',
|
||||
'backfill_studios' => 'Backfill Studios',
|
||||
'balance_balance_help' => 'Current balance of the account with current usage',
|
||||
'balance_date_help' => 'The month and year for the balance that we are tracking',
|
||||
@@ -180,10 +182,11 @@ return [
|
||||
'status' => 'Status',
|
||||
'studio_create_resource_action_success' => 'The Resource has been created and attached to the Studio',
|
||||
'studio_create_resource_action' => 'Create :site Resource for Studio',
|
||||
'studio_image_lens' => 'Studio Without :facet Image',
|
||||
'studio_name_help' => 'The display title of the Studio',
|
||||
'studio_resource_lens' => 'Studio Without :site Resource',
|
||||
'studio_slug_help' => 'Used as the URL Slug / Model Route Key. By default, this should be the Name lowercased and "_" replacing spaces. Shortenings/Abbreviations are also accepted.',
|
||||
'studio_unlinked_lens' => 'Studio Without Anime',
|
||||
'studio_unlinked_lens' => 'Studio Without Anime or Studio',
|
||||
'studio' => 'Studio',
|
||||
'studios' => 'Studios',
|
||||
'subbed' => 'Subbed',
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Events\Pivot;
|
||||
|
||||
use App\Events\Pivot\StudioImage\StudioImageCreated;
|
||||
use App\Events\Pivot\StudioImage\StudioImageDeleted;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Class StudioImageTest.
|
||||
*/
|
||||
class StudioImageTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* When a Studio is attached to an Image or vice versa, a StudioImageCreated event shall be dispatched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testStudioImageCreatedEventDispatched(): void
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
$studio = Studio::factory()->createOne();
|
||||
$image = Image::factory()->createOne();
|
||||
|
||||
$studio->images()->attach($image);
|
||||
|
||||
Event::assertDispatched(StudioImageCreated::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a Studio is detached from an Image or vice versa, a StudioImageDeleted event shall be dispatched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testStudioImageDeletedEventDispatched(): void
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
$studio = Studio::factory()->createOne();
|
||||
$image = Image::factory()->createOne();
|
||||
|
||||
$studio->images()->attach($image);
|
||||
$studio->images()->detach($image);
|
||||
|
||||
Event::assertDispatched(StudioImageDeleted::class);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use App\Contracts\Http\Api\Field\SortableField;
|
||||
use App\Enums\Http\Api\Filter\TrashedStatus;
|
||||
use App\Enums\Http\Api\Sort\Direction;
|
||||
use App\Enums\Models\Wiki\AnimeSeason;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Http\Api\Criteria\Filter\TrashedCriteria;
|
||||
use App\Http\Api\Criteria\Paging\Criteria;
|
||||
use App\Http\Api\Criteria\Paging\OffsetCriteria;
|
||||
@@ -24,6 +26,8 @@ use App\Http\Resources\Wiki\Collection\StudioCollection;
|
||||
use App\Http\Resources\Wiki\Resource\StudioResource;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\Sequence;
|
||||
@@ -536,4 +540,88 @@ class StudioIndexTest extends TestCase
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Studio Index Endpoint shall support constrained eager loading of resources by site.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testResourcesBySite(): void
|
||||
{
|
||||
$siteFilter = ResourceSite::getRandomInstance();
|
||||
|
||||
$parameters = [
|
||||
FilterParser::param() => [
|
||||
ExternalResource::ATTRIBUTE_SITE => $siteFilter->description,
|
||||
],
|
||||
IncludeParser::param() => Studio::RELATION_RESOURCES,
|
||||
];
|
||||
|
||||
Studio::factory()
|
||||
->has(ExternalResource::factory()->count($this->faker->randomDigitNotNull()), Studio::RELATION_RESOURCES)
|
||||
->count($this->faker->randomDigitNotNull())
|
||||
->create();
|
||||
|
||||
$studios = Studio::with([
|
||||
Studio::RELATION_RESOURCES => function (BelongsToMany $query) use ($siteFilter) {
|
||||
$query->where(ExternalResource::ATTRIBUTE_SITE, $siteFilter->value);
|
||||
},
|
||||
])
|
||||
->get();
|
||||
|
||||
$response = $this->get(route('api.studio.index', $parameters));
|
||||
|
||||
$response->assertJson(
|
||||
json_decode(
|
||||
json_encode(
|
||||
(new StudioCollection($studios, new StudioReadQuery($parameters)))
|
||||
->response()
|
||||
->getData()
|
||||
),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Studio Index Endpoint shall support constrained eager loading of images by facet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testImagesByFacet(): void
|
||||
{
|
||||
$facetFilter = ImageFacet::getRandomInstance();
|
||||
|
||||
$parameters = [
|
||||
FilterParser::param() => [
|
||||
Image::ATTRIBUTE_FACET => $facetFilter->description,
|
||||
],
|
||||
IncludeParser::param() => Studio::RELATION_IMAGES,
|
||||
];
|
||||
|
||||
Studio::factory()
|
||||
->has(Image::factory()->count($this->faker->randomDigitNotNull()))
|
||||
->count($this->faker->randomDigitNotNull())
|
||||
->create();
|
||||
|
||||
$anime = Studio::with([
|
||||
Studio::RELATION_IMAGES => function (BelongsToMany $query) use ($facetFilter) {
|
||||
$query->where(Image::ATTRIBUTE_FACET, $facetFilter->value);
|
||||
},
|
||||
])
|
||||
->get();
|
||||
|
||||
$response = $this->get(route('api.studio.index', $parameters));
|
||||
|
||||
$response->assertJson(
|
||||
json_decode(
|
||||
json_encode(
|
||||
(new StudioCollection($anime, new StudioReadQuery($parameters)))
|
||||
->response()
|
||||
->getData()
|
||||
),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Http\Api\Wiki\Studio;
|
||||
|
||||
use App\Enums\Models\Wiki\AnimeSeason;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Http\Api\Field\Field;
|
||||
use App\Http\Api\Include\AllowedInclude;
|
||||
use App\Http\Api\Parser\FieldParser;
|
||||
@@ -14,6 +16,8 @@ use App\Http\Api\Query\Wiki\Studio\StudioReadQuery;
|
||||
use App\Http\Api\Schema\Wiki\StudioSchema;
|
||||
use App\Http\Resources\Wiki\Resource\StudioResource;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Database\Eloquent\Factories\Sequence;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@@ -238,4 +242,84 @@ class StudioShowTest extends TestCase
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Studio Show Endpoint shall support constrained eager loading of resources by site.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testResourcesBySite(): void
|
||||
{
|
||||
$siteFilter = ResourceSite::getRandomInstance();
|
||||
|
||||
$parameters = [
|
||||
FilterParser::param() => [
|
||||
ExternalResource::ATTRIBUTE_SITE => $siteFilter->description,
|
||||
],
|
||||
IncludeParser::param() => Studio::RELATION_RESOURCES,
|
||||
];
|
||||
|
||||
$studio = Studio::factory()
|
||||
->has(ExternalResource::factory()->count($this->faker->randomDigitNotNull()), Studio::RELATION_RESOURCES)
|
||||
->createOne();
|
||||
|
||||
$studio->unsetRelations()->load([
|
||||
Studio::RELATION_RESOURCES => function (BelongsToMany $query) use ($siteFilter) {
|
||||
$query->where(ExternalResource::ATTRIBUTE_SITE, $siteFilter->value);
|
||||
},
|
||||
]);
|
||||
|
||||
$response = $this->get(route('api.studio.show', ['studio' => $studio] + $parameters));
|
||||
|
||||
$response->assertJson(
|
||||
json_decode(
|
||||
json_encode(
|
||||
(new StudioResource($studio, new StudioReadQuery($parameters)))
|
||||
->response()
|
||||
->getData()
|
||||
),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Studio Show Endpoint shall support constrained eager loading of images by facet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testImagesByFacet(): void
|
||||
{
|
||||
$facetFilter = ImageFacet::getRandomInstance();
|
||||
|
||||
$parameters = [
|
||||
FilterParser::param() => [
|
||||
Image::ATTRIBUTE_FACET => $facetFilter->description,
|
||||
],
|
||||
IncludeParser::param() => Studio::RELATION_IMAGES,
|
||||
];
|
||||
|
||||
$studio = Studio::factory()
|
||||
->has(Image::factory()->count($this->faker->randomDigitNotNull()))
|
||||
->createOne();
|
||||
|
||||
$studio->unsetRelations()->load([
|
||||
Studio::RELATION_IMAGES => function (BelongsToMany $query) use ($facetFilter) {
|
||||
$query->where(Image::ATTRIBUTE_FACET, $facetFilter->value);
|
||||
},
|
||||
]);
|
||||
|
||||
$response = $this->get(route('api.studio.show', ['studio' => $studio] + $parameters));
|
||||
|
||||
$response->assertJson(
|
||||
json_decode(
|
||||
json_encode(
|
||||
(new StudioResource($studio, new StudioReadQuery($parameters)))
|
||||
->response()
|
||||
->getData()
|
||||
),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Jobs\Pivot;
|
||||
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Jobs\SendDiscordNotificationJob;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Class StudioImageTest.
|
||||
*/
|
||||
class StudioImageTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* When a Studio is attached to an Image or vice versa, a SendDiscordNotification job shall be dispatched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testStudioImageCreatedSendsDiscordNotification(): void
|
||||
{
|
||||
$studio = Studio::factory()->createOne();
|
||||
$image = Image::factory()->createOne();
|
||||
|
||||
Config::set(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED, true);
|
||||
Bus::fake(SendDiscordNotificationJob::class);
|
||||
|
||||
$studio->images()->attach($image);
|
||||
|
||||
Bus::assertDispatched(SendDiscordNotificationJob::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a Studio is detached from an Image or vice versa, a SendDiscordNotification job shall be dispatched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testStudioImageDeletedSendsDiscordNotification(): void
|
||||
{
|
||||
$studio = Studio::factory()->createOne();
|
||||
$image = Image::factory()->createOne();
|
||||
|
||||
$studio->images()->attach($image);
|
||||
|
||||
Config::set(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED, true);
|
||||
Bus::fake(SendDiscordNotificationJob::class);
|
||||
|
||||
$studio->images()->detach($image);
|
||||
|
||||
Bus::assertDispatched(SendDiscordNotificationJob::class);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\AnimeImage;
|
||||
use App\Pivots\ArtistImage;
|
||||
use App\Pivots\StudioImage;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Http\Testing\File;
|
||||
@@ -102,6 +104,25 @@ class ImageTest extends TestCase
|
||||
static::assertEquals(ArtistImage::class, $image->artists()->getPivotClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Image shall have a many-to-many relationship with the type Studio.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testStudios(): void
|
||||
{
|
||||
$studioCount = $this->faker->randomDigitNotNull();
|
||||
|
||||
$image = Image::factory()
|
||||
->has(Studio::factory()->count($studioCount))
|
||||
->createOne();
|
||||
|
||||
static::assertInstanceOf(BelongsToMany::class, $image->studios());
|
||||
static::assertEquals($studioCount, $image->studios()->count());
|
||||
static::assertInstanceOf(Studio::class, $image->studios()->first());
|
||||
static::assertEquals(StudioImage::class, $image->studios()->getPivotClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* The image shall not be deleted from storage when the Image is deleted.
|
||||
*
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace Tests\Unit\Models\Wiki;
|
||||
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\AnimeStudio;
|
||||
use App\Pivots\StudioImage;
|
||||
use App\Pivots\StudioResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
@@ -108,4 +110,23 @@ class StudioTest extends TestCase
|
||||
static::assertInstanceOf(ExternalResource::class, $studio->resources()->first());
|
||||
static::assertEquals(StudioResource::class, $studio->resources()->getPivotClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Studio shall have a many-to-many relationship with the type Image.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testImages(): void
|
||||
{
|
||||
$imageCount = $this->faker->randomDigitNotNull();
|
||||
|
||||
$studio = Studio::factory()
|
||||
->has(Image::factory()->count($imageCount))
|
||||
->createOne();
|
||||
|
||||
static::assertInstanceOf(BelongsToMany::class, $studio->images());
|
||||
static::assertEquals($imageCount, $studio->images()->count());
|
||||
static::assertInstanceOf(Image::class, $studio->images()->first());
|
||||
static::assertEquals(StudioImage::class, $studio->images()->getPivotClass());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Pivots;
|
||||
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioImage;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Foundation\Testing\WithoutEvents;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Class StudioImageTest.
|
||||
*/
|
||||
class StudioImageTest extends TestCase
|
||||
{
|
||||
use WithoutEvents;
|
||||
|
||||
/**
|
||||
* An StudioImage shall belong to a Studio.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testStudio(): void
|
||||
{
|
||||
$studioImage = StudioImage::factory()
|
||||
->for(Studio::factory())
|
||||
->for(Image::factory())
|
||||
->createOne();
|
||||
|
||||
static::assertInstanceOf(BelongsTo::class, $studioImage->studio());
|
||||
static::assertInstanceOf(Studio::class, $studioImage->studio()->first());
|
||||
}
|
||||
|
||||
/**
|
||||
* An StudioImage shall belong to an Image.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testImage(): void
|
||||
{
|
||||
$studioImage = StudioImage::factory()
|
||||
->for(Studio::factory())
|
||||
->for(Image::factory())
|
||||
->createOne();
|
||||
|
||||
static::assertInstanceOf(BelongsTo::class, $studioImage->image());
|
||||
static::assertInstanceOf(Image::class, $studioImage->image()->first());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user