diff --git a/app/Events/Wiki/Group/GroupCreated.php b/app/Events/Wiki/Group/GroupCreated.php new file mode 100644 index 000000000..60a003837 --- /dev/null +++ b/app/Events/Wiki/Group/GroupCreated.php @@ -0,0 +1,68 @@ + + */ +class GroupCreated extends WikiCreatedEvent implements UpdateRelatedIndicesEvent +{ + /** + * Create a new event instance. + * + * @param Group $group + */ + public function __construct(Group $group) + { + parent::__construct($group); + } + + /** + * Get the model that has fired this event. + * + * @return Group + */ + public function getModel(): Group + { + return $this->model; + } + + /** + * Get the description for the Discord message payload. + * + * @return string + */ + protected function getDiscordMessageDescription(): string + { + return "Group '**{$this->getModel()->getName()}**' has been created."; + } + + /** + * Perform updates on related indices. + * + * @return void + */ + public function updateRelatedIndices(): void + { + $group = $this->getModel()->load(Group::RELATION_VIDEOS); + + $group->animethemes->each(function (AnimeTheme $theme) { + $theme->searchable(); + $theme->animethemeentries->each(function (AnimeThemeEntry $entry) { + $entry->searchable(); + $entry->videos->each(fn (Video $video) => $video->searchable()); + }); + }); + } +} diff --git a/app/Events/Wiki/Group/GroupDeleted.php b/app/Events/Wiki/Group/GroupDeleted.php new file mode 100644 index 000000000..40e770330 --- /dev/null +++ b/app/Events/Wiki/Group/GroupDeleted.php @@ -0,0 +1,91 @@ + + */ +class GroupDeleted extends WikiDeletedEvent implements UpdateRelatedIndicesEvent +{ + /** + * Create a new event instance. + * + * @param Group $group + */ + public function __construct(Group $group) + { + parent::__construct($group); + } + + /** + * Get the model that has fired this event. + * + * @return Group + */ + public function getModel(): Group + { + return $this->model; + } + + /** + * Get the description for the Discord message payload. + * + * @return string + */ + protected function getDiscordMessageDescription(): string + { + return "Group '**{$this->getModel()->getName()}**' has been deleted."; + } + + /** + * Get the message for the nova notification. + * + * @return string + */ + protected function getNotificationMessage(): string + { + return "Group '{$this->getModel()->getName()}' has been deleted. It will be automatically pruned in one week. Please review."; + } + + /** + * Get the URL for the nova notification. + * + * @return string + */ + protected function getNovaNotificationUrl(): string + { + $uriKey = GroupResource::uriKey(); + + return "/resources/$uriKey/{$this->getModel()->getKey()}"; + } + + /** + * Perform updates on related indices. + * + * @return void + */ + public function updateRelatedIndices(): void + { + $group = $this->getModel()->load(Group::RELATION_VIDEOS); + + $group->animethemes->each(function (AnimeTheme $theme) { + $theme->searchable(); + $theme->animethemeentries->each(function (AnimeThemeEntry $entry) { + $entry->searchable(); + $entry->videos->each(fn (Video $video) => $video->searchable()); + }); + }); + } +} diff --git a/app/Events/Wiki/Group/GroupDeleting.php b/app/Events/Wiki/Group/GroupDeleting.php new file mode 100644 index 000000000..161fb2475 --- /dev/null +++ b/app/Events/Wiki/Group/GroupDeleting.php @@ -0,0 +1,64 @@ + + */ +class GroupDeleting extends BaseEvent implements UpdateRelatedIndicesEvent +{ + /** + * Create a new event instance. + * + * @param Group $group + */ + public function __construct(Group $group) + { + parent::__construct($group); + } + + /** + * Get the model that has fired this event. + * + * @return Group + */ + public function getModel(): Group + { + return $this->model; + } + + /** + * Perform cascading deletes. + * + * @return void + */ + public function updateRelatedIndices(): void + { + $group = $this->getModel()->load(Group::RELATION_VIDEOS); + + if ($group->isForceDeleting()) { + $group->animethemes->each(function (AnimeTheme $theme) { + AnimeTheme::withoutEvents(function () use ($theme) { + $theme->theme_group()->dissociate(); + $theme->save(); + }); + $theme->searchable(); + $theme->animethemeentries->each(function (AnimeThemeEntry $entry) { + $entry->searchable(); + $entry->videos->each(fn (Video $video) => $video->searchable()); + }); + }); + } + } +} diff --git a/app/Events/Wiki/Group/GroupRestored.php b/app/Events/Wiki/Group/GroupRestored.php new file mode 100644 index 000000000..4bd826e4f --- /dev/null +++ b/app/Events/Wiki/Group/GroupRestored.php @@ -0,0 +1,68 @@ + + */ +class GroupRestored extends WikiRestoredEvent implements UpdateRelatedIndicesEvent +{ + /** + * Create a new event instance. + * + * @param Group $group + */ + public function __construct(Group $group) + { + parent::__construct($group); + } + + /** + * Get the model that has fired this event. + * + * @return Group + */ + public function getModel(): Group + { + return $this->model; + } + + /** + * Get the description for the Discord message payload. + * + * @return string + */ + protected function getDiscordMessageDescription(): string + { + return "Group '**{$this->getModel()->getName()}**' has been restored."; + } + + /** + * Perform cascading deletes. + * + * @return void + */ + public function updateRelatedIndices(): void + { + $group = $this->getModel()->load(Group::RELATION_VIDEOS); + + $group->animethemes->each(function (AnimeTheme $theme) { + $theme->searchable(); + $theme->animethemeentries->each(function (AnimeThemeEntry $entry) { + $entry->searchable(); + $entry->videos->each(fn (Video $video) => $video->searchable()); + }); + }); + } +} diff --git a/app/Events/Wiki/Group/GroupUpdated.php b/app/Events/Wiki/Group/GroupUpdated.php new file mode 100644 index 000000000..aba125452 --- /dev/null +++ b/app/Events/Wiki/Group/GroupUpdated.php @@ -0,0 +1,69 @@ + + */ +class GroupUpdated extends WikiUpdatedEvent implements UpdateRelatedIndicesEvent +{ + /** + * Create a new event instance. + * + * @param Group $group + */ + public function __construct(Group $group) + { + parent::__construct($group); + $this->initializeEmbedFields($group); + } + + /** + * Get the model that has fired this event. + * + * @return Group + */ + public function getModel(): Group + { + return $this->model; + } + + /** + * Get the description for the Discord message payload. + * + * @return string + */ + protected function getDiscordMessageDescription(): string + { + return "Group '**{$this->getModel()->getName()}**' has been updated."; + } + + /** + * Perform updates on related indices. + * + * @return void + */ + public function updateRelatedIndices(): void + { + $group = $this->getModel()->load(Group::RELATION_VIDEOS); + + $group->animethemes->each(function (AnimeTheme $theme) { + $theme->searchable(); + $theme->animethemeentries->each(function (AnimeThemeEntry $entry) { + $entry->searchable(); + $entry->videos->each(fn (Video $video) => $video->searchable()); + }); + }); + } +} diff --git a/app/Http/Api/Field/Wiki/Anime/Theme/ThemeGroupIdField.php b/app/Http/Api/Field/Wiki/Anime/Theme/ThemeGroupIdField.php new file mode 100644 index 000000000..8b8dbaffc --- /dev/null +++ b/app/Http/Api/Field/Wiki/Anime/Theme/ThemeGroupIdField.php @@ -0,0 +1,59 @@ +validated()); + + $groups = $query->hasSearchCriteria() + ? $action->search($query, $request->schema()) + : $action->index(Group::query(), $query, $request->schema()); + + return new GroupCollection($groups, $query); + } + + /** + * Store a newly created resource. + * + * @param StoreRequest $request + * @param StoreAction $action + * @return GroupResource + */ + public function store(StoreRequest $request, StoreAction $action): GroupResource + { + $group = $action->store(Group::query(), $request->validated()); + + return new GroupResource($group, new Query()); + } + + /** + * Display the specified resource. + * + * @param ShowRequest $request + * @param Group $group + * @param ShowAction $action + * @return GroupResource + */ + public function show(ShowRequest $request, Group $group, ShowAction $action): GroupResource + { + $query = new Query($request->validated()); + + $show = $action->show($group, $query, $request->schema()); + + return new GroupResource($show, $query); + } + + /** + * Update the specified resource. + * + * @param UpdateRequest $request + * @param Group $group + * @param UpdateAction $action + * @return GroupResource + */ + public function update(UpdateRequest $request, Group $group, UpdateAction $action): GroupResource + { + $updated = $action->update($group, $request->validated()); + + return new GroupResource($updated, new Query()); + } + + /** + * Remove the specified resource. + * + * @param Group $group + * @param DestroyAction $action + * @return GroupResource + */ + public function destroy(Group $group, DestroyAction $action): GroupResource + { + $deleted = $action->destroy($group); + + return new GroupResource($deleted, new Query()); + } + + /** + * Restore the specified resource. + * + * @param Group $group + * @param RestoreAction $action + * @return GroupResource + */ + public function restore(Group $group, RestoreAction $action): GroupResource + { + $restored = $action->restore($group); + + return new GroupResource($restored, new Query()); + } + + /** + * Hard-delete the specified resource. + * + * @param Group $group + * @param ForceDeleteAction $action + * @return JsonResponse + */ + public function forceDelete(Group $group, ForceDeleteAction $action): JsonResponse + { + $message = $action->forceDelete($group); + + return new JsonResponse([ + 'message' => $message, + ]); + } +} diff --git a/app/Http/Resources/Wiki/Collection/GroupCollection.php b/app/Http/Resources/Wiki/Collection/GroupCollection.php new file mode 100644 index 000000000..f4ae6b750 --- /dev/null +++ b/app/Http/Resources/Wiki/Collection/GroupCollection.php @@ -0,0 +1,36 @@ +collection->map(fn (Group $group) => new GroupResource($group, $this->query))->all(); + } +} diff --git a/app/Http/Resources/Wiki/Resource/GroupResource.php b/app/Http/Resources/Wiki/Resource/GroupResource.php new file mode 100644 index 000000000..467f838d4 --- /dev/null +++ b/app/Http/Resources/Wiki/Resource/GroupResource.php @@ -0,0 +1,32 @@ + $animethemeentries * @property string|null $group + * @property Group|null $theme_group + * @property int|null $group_id * @property int|null $sequence * @property string $slug * @property Song|null $song @@ -51,12 +54,14 @@ class AnimeTheme extends BaseModel final public const ATTRIBUTE_SEQUENCE = 'sequence'; final public const ATTRIBUTE_SLUG = 'slug'; final public const ATTRIBUTE_SONG = 'song_id'; + final public const ATTRIBUTE_THEME_GROUP = 'group_id'; final public const ATTRIBUTE_TYPE = 'type'; final public const RELATION_ANIME = 'anime'; final public const RELATION_ARTISTS = 'song.artists'; final public const RELATION_AUDIO = 'animethemeentries.videos.audio'; final public const RELATION_ENTRIES = 'animethemeentries'; + final public const RELATION_GROUP = 'theme_group'; final public const RELATION_IMAGES = 'anime.images'; final public const RELATION_SONG = 'song'; final public const RELATION_SYNONYMS = 'anime.animesynonyms'; @@ -70,6 +75,7 @@ class AnimeTheme extends BaseModel protected $fillable = [ AnimeTheme::ATTRIBUTE_ANIME, AnimeTheme::ATTRIBUTE_GROUP, + AnimeTheme::ATTRIBUTE_THEME_GROUP, AnimeTheme::ATTRIBUTE_SEQUENCE, AnimeTheme::ATTRIBUTE_SLUG, AnimeTheme::ATTRIBUTE_SONG, @@ -165,6 +171,16 @@ public function anime(): BelongsTo return $this->belongsTo(Anime::class, AnimeTheme::ATTRIBUTE_ANIME); } + /** + * Gets the group that the theme uses. + * + * @return BelongsTo + */ + public function theme_group(): BelongsTo + { + return $this->belongsTo(Group::class, AnimeTheme::ATTRIBUTE_THEME_GROUP); + } + /** * Gets the song that the theme uses. * diff --git a/app/Models/Wiki/Group.php b/app/Models/Wiki/Group.php new file mode 100644 index 000000000..61dbf9e79 --- /dev/null +++ b/app/Models/Wiki/Group.php @@ -0,0 +1,106 @@ + $animethemes + * @property int $group_id + * @property string $name + * @property string $slug + * @property string|null $video_filename + * + * @method static GroupFactory factory(...$parameters) + */ +class Group extends BaseModel +{ + use Actionable; + use Searchable; + + final public const TABLE = 'groups'; + + final public const ATTRIBUTE_ID = 'group_id'; + final public const ATTRIBUTE_NAME = 'name'; + final public const ATTRIBUTE_SLUG = 'slug'; + final public const ATTRIBUTE_VIDEO_FILENAME = 'video_filename'; + + final public const RELATION_ANIME = 'animethemes.anime'; + final public const RELATION_THEMES = 'animethemes'; + final public const RELATION_VIDEOS = 'animethemes.animethemeentries.videos'; + + /** + * The attributes that are mass assignable. + * + * @var string[] + */ + protected $fillable = [ + Group::ATTRIBUTE_NAME, + Group::ATTRIBUTE_SLUG, + Group::ATTRIBUTE_VIDEO_FILENAME, + ]; + + /** + * The event map for the model. + * + * Allows for object-based events for native Eloquent events. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => GroupCreated::class, + 'deleted' => GroupDeleted::class, + 'deleting' => GroupDeleting::class, + 'restored' => GroupRestored::class, + 'updated' => GroupUpdated::class, + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = Group::TABLE; + + /** + * The primary key associated with the table. + * + * @var string + */ + protected $primaryKey = Group::ATTRIBUTE_ID; + + /** + * Get name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the themes for the group. + * + * @return HasMany + */ + public function animethemes(): HasMany + { + return $this->hasMany(AnimeTheme::class, AnimeTheme::ATTRIBUTE_THEME_GROUP); + } +} diff --git a/app/Nova/Resources/Wiki/Anime/Theme.php b/app/Nova/Resources/Wiki/Anime/Theme.php index 7ca5398d3..5e5504be7 100644 --- a/app/Nova/Resources/Wiki/Anime/Theme.php +++ b/app/Nova/Resources/Wiki/Anime/Theme.php @@ -9,6 +9,7 @@ use App\Nova\Resources\BaseResource; use App\Nova\Resources\Wiki\Anime; use App\Nova\Resources\Wiki\Anime\Theme\Entry; +use App\Nova\Resources\Wiki\Group; use App\Nova\Resources\Wiki\Song; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Str; @@ -232,6 +233,15 @@ function (Text $field, NovaRequest $novaRequest, FormData $formData) { } ), + BelongsTo::make(__('nova.resources.singularLabel.group'), AnimeTheme::RELATION_GROUP, Group::class) + ->sortable() + ->filterable() + ->searchable() + ->withSubtitles() + ->nullable() + ->showCreateRelationButton() + ->showOnPreview(), + BelongsTo::make(__('nova.resources.singularLabel.song'), AnimeTheme::RELATION_SONG, Song::class) ->sortable() ->filterable() diff --git a/app/Nova/Resources/Wiki/Group.php b/app/Nova/Resources/Wiki/Group.php new file mode 100644 index 000000000..d88582ebe --- /dev/null +++ b/app/Nova/Resources/Wiki/Group.php @@ -0,0 +1,249 @@ +model(); + if ($group instanceof GroupModel) { + $theme = $group->animethemes->first(); + if ($theme instanceof AnimeTheme) { + return $theme->anime->getName(); + } + } + + return null; + } + + /** + * Build a "relatable" query for the given resource. + * + * This query determines which instances of the model may be attached to other resources. + * + * @param NovaRequest $request + * @param Builder $query + * @return Builder + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function relatableQuery(NovaRequest $request, $query): Builder + { + return $query->with(GroupModel::RELATION_ANIME); + } + + /** + * Build an "index" query for the given resource. + * + * @param NovaRequest $request + * @param Builder $query + * @return Builder + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function indexQuery(NovaRequest $request, $query): Builder + { + return $query->with(GroupModel::RELATION_ANIME); + } + + /** + * The logical group associated with the resource. + * + * @return string + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function group(): string + { + return __('nova.resources.group.wiki'); + } + + /** + * Get the displayable label of the resource. + * + * @return string + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function label(): string + { + return __('nova.resources.label.groups'); + } + + /** + * Get the displayable singular label of the resource. + * + * @return string + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function singularLabel(): string + { + return __('nova.resources.singularLabel.group'); + } + + /** + * Get the URI key for the resource. + * + * @return string + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function uriKey(): string + { + return 'groups'; + } + + /** + * Get the searchable columns for the resource. + * + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function searchableColumns(): array + { + return [ + new Column(GroupModel::ATTRIBUTE_NAME), + ]; + } + + /** + * Determine if this resource uses Laravel Scout. + * + * @return bool + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public static function usesScout(): bool + { + return false; + } + + /** + * Get the fields displayed by the resource. + * + * @param NovaRequest $request + * @return array + * + * @throws Exception + */ + public function fields(NovaRequest $request): array + { + return [ + ID::make(__('nova.fields.base.id'), GroupModel::ATTRIBUTE_ID) + ->sortable() + ->showOnPreview() + ->showWhenPeeking(), + + Text::make(__('nova.fields.group.name.name'), GroupModel::ATTRIBUTE_NAME) + ->sortable() + ->copyable() + ->required() + ->rules(['required', 'max:192']) + ->help(__('nova.fields.group.name.help')) + ->showOnPreview() + ->filterable() + ->maxlength(192) + ->enforceMaxlength() + ->showWhenPeeking(), + + Text::make(__('nova.fields.group.slug.name'), GroupModel::ATTRIBUTE_SLUG) + ->sortable() + ->copyable() + ->required() + ->rules(['required', 'max:192']) + ->help(__('nova.fields.group.slug.help')) + ->showOnPreview() + ->filterable() + ->maxlength(192) + ->enforceMaxlength() + ->showWhenPeeking(), + + Text::make(__('nova.fields.group.video_filename.name'), GroupModel::ATTRIBUTE_VIDEO_FILENAME) + ->sortable() + ->copyable() + ->nullable() + ->rules(['nullable', 'max:192']) + ->help(__('nova.fields.group.video_filename.help')) + ->showOnPreview() + ->filterable() + ->maxlength(192) + ->enforceMaxlength() + ->showWhenPeeking(), + + HasMany::make(__('nova.resources.label.anime_themes'), GroupModel::RELATION_THEMES, Theme::class), + + Panel::make(__('nova.fields.base.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), + [] + ); + } + + /** + * Get the lenses available for the resource. + * + * @param NovaRequest $request + * @return array + */ + public function lenses(NovaRequest $request): array + { + return array_merge( + parent::lenses($request), + [] + ); + } +} diff --git a/app/Policies/Wiki/GroupPolicy.php b/app/Policies/Wiki/GroupPolicy.php new file mode 100644 index 000000000..1cc0fd97c --- /dev/null +++ b/app/Policies/Wiki/GroupPolicy.php @@ -0,0 +1,117 @@ + $user !== null && $user->can(CrudPermission::VIEW->format(Group::class)), + fn (): bool => true + ); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @return bool + */ + public function view(?User $user): bool + { + return Nova::whenServing( + fn (): bool => $user !== null && $user->can(CrudPermission::VIEW->format(Group::class)), + fn (): bool => true + ); + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @return bool + */ + public function create(User $user): bool + { + return $user->can(CrudPermission::CREATE->format(Group::class)); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param Group $group + * @return bool + */ + public function update(User $user, Group $group): bool + { + return ! $group->trashed() && $user->can(CrudPermission::UPDATE->format(Group::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param Group $group + * @return bool + */ + public function delete(User $user, Group $group): bool + { + return ! $group->trashed() && $user->can(CrudPermission::DELETE->format(Group::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param Group $group + * @return bool + */ + public function restore(User $user, Group $group): bool + { + return $group->trashed() && $user->can(ExtendedCrudPermission::RESTORE->format(Group::class)); + } + + /** + * Determine whether the user can permanently delete the model. + * + * @param User $user + * @return bool + */ + public function forceDelete(User $user): bool + { + return $user->can(ExtendedCrudPermission::FORCE_DELETE->format(Group::class)); + } + + /** + * Determine whether the user can add a theme to the group. + * + * @param User $user + * @return bool + */ + public function addAnimeTheme(User $user): bool + { + return $user->can(CrudPermission::UPDATE->format(Group::class)); + } +} diff --git a/database/factories/Wiki/GroupFactory.php b/database/factories/Wiki/GroupFactory.php new file mode 100644 index 000000000..257658477 --- /dev/null +++ b/database/factories/Wiki/GroupFactory.php @@ -0,0 +1,41 @@ + + */ +class GroupFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Group::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + Group::ATTRIBUTE_NAME => fake()->words(3, true), + Group::ATTRIBUTE_SLUG => Str::slug(fake()->text(191), '_'), + Group::ATTRIBUTE_VIDEO_FILENAME => fake()->words(3, true), + ]; + } +} diff --git a/database/migrations/2024_04_26_135545_create_groups_table.php b/database/migrations/2024_04_26_135545_create_groups_table.php new file mode 100644 index 000000000..075aec7a4 --- /dev/null +++ b/database/migrations/2024_04_26_135545_create_groups_table.php @@ -0,0 +1,37 @@ +id(Group::ATTRIBUTE_ID); + $table->timestamps(6); + $table->softDeletes(BaseModel::ATTRIBUTE_DELETED_AT, 6); + $table->string(Group::ATTRIBUTE_NAME); + $table->string(Group::ATTRIBUTE_SLUG); + $table->string(Group::ATTRIBUTE_VIDEO_FILENAME)->nullable(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists(Group::TABLE); + } +}; diff --git a/database/migrations/2024_04_26_140035_add_theme_group_attribute_to_anime_theme.php b/database/migrations/2024_04_26_140035_add_theme_group_attribute_to_anime_theme.php new file mode 100644 index 000000000..9170551fb --- /dev/null +++ b/database/migrations/2024_04_26_140035_add_theme_group_attribute_to_anime_theme.php @@ -0,0 +1,37 @@ +unsignedBigInteger(AnimeTheme::ATTRIBUTE_THEME_GROUP)->nullable(); + $table->foreign(AnimeTheme::ATTRIBUTE_THEME_GROUP)->references(Group::ATTRIBUTE_ID)->on(Group::TABLE)->nullOnDelete(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasColumn(AnimeTheme::TABLE, AnimeTheme::ATTRIBUTE_THEME_GROUP)) { + Schema::table(AnimeTheme::TABLE, function (Blueprint $table) { + $table->dropConstrainedForeignId(AnimeTheme::ATTRIBUTE_THEME_GROUP); + }); + } + } +}; diff --git a/database/seeders/Auth/Permission/PermissionSeeder.php b/database/seeders/Auth/Permission/PermissionSeeder.php index 751305f3c..ab27f01e4 100644 --- a/database/seeders/Auth/Permission/PermissionSeeder.php +++ b/database/seeders/Auth/Permission/PermissionSeeder.php @@ -24,6 +24,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -71,6 +72,7 @@ public function run(): void $this->registerResource(AnimeThemeEntry::class, $extendedCrudPermissions); $this->registerResource(Artist::class, $extendedCrudPermissions); $this->registerResource(Audio::class, $extendedCrudPermissions); + $this->registerResource(Group::class, $extendedCrudPermissions); $this->registerResource(ExternalResource::class, $extendedCrudPermissions); $this->registerResource(Image::class, $extendedCrudPermissions); $this->registerResource(Page::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/AdminSeeder.php b/database/seeders/Auth/Role/AdminSeeder.php index c1988df5f..1e0ad4b36 100644 --- a/database/seeders/Auth/Role/AdminSeeder.php +++ b/database/seeders/Auth/Role/AdminSeeder.php @@ -24,6 +24,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -75,6 +76,7 @@ public function run(): void $this->configureResource($role, AnimeThemeEntry::class, $extendedCrudPermissions); $this->configureResource($role, Artist::class, $extendedCrudPermissions); $this->configureResource($role, Audio::class, $extendedCrudPermissions); + $this->configureResource($role, Group::class, $extendedCrudPermissions); $this->configureResource($role, ExternalResource::class, $extendedCrudPermissions); $this->configureResource($role, Image::class, $extendedCrudPermissions); $this->configureResource($role, Page::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/DeveloperRoleSeeder.php b/database/seeders/Auth/Role/DeveloperRoleSeeder.php index f3c8e2b16..0486c6eba 100644 --- a/database/seeders/Auth/Role/DeveloperRoleSeeder.php +++ b/database/seeders/Auth/Role/DeveloperRoleSeeder.php @@ -18,6 +18,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -58,6 +59,7 @@ public function run(): void $this->configureResource($role, AnimeThemeEntry::class, [CrudPermission::VIEW]); $this->configureResource($role, Artist::class, [CrudPermission::VIEW]); $this->configureResource($role, Audio::class, [CrudPermission::VIEW]); + $this->configureResource($role, Group::class, [CrudPermission::VIEW]); $this->configureResource($role, ExternalResource::class, [CrudPermission::VIEW]); $this->configureResource($role, Image::class, [CrudPermission::VIEW]); $this->configureResource($role, Page::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/EncoderRoleSeeder.php b/database/seeders/Auth/Role/EncoderRoleSeeder.php index 2e8ca7c44..329e15bcb 100644 --- a/database/seeders/Auth/Role/EncoderRoleSeeder.php +++ b/database/seeders/Auth/Role/EncoderRoleSeeder.php @@ -18,6 +18,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -65,6 +66,7 @@ public function run(): void $this->configureResource($role, AnimeThemeEntry::class, $extendedCrudPermissions); $this->configureResource($role, Artist::class, $extendedCrudPermissions); $this->configureResource($role, Audio::class, $extendedCrudPermissions); + $this->configureResource($role, Group::class, $extendedCrudPermissions); $this->configureResource($role, ExternalResource::class, $extendedCrudPermissions); $this->configureResource($role, Image::class, $extendedCrudPermissions); $this->configureResource($role, Page::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/PatronRoleSeeder.php b/database/seeders/Auth/Role/PatronRoleSeeder.php index ead2a17d0..1511feb37 100644 --- a/database/seeders/Auth/Role/PatronRoleSeeder.php +++ b/database/seeders/Auth/Role/PatronRoleSeeder.php @@ -18,6 +18,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -58,6 +59,7 @@ public function run(): void $this->configureResource($role, AnimeThemeEntry::class, [CrudPermission::VIEW]); $this->configureResource($role, Artist::class, [CrudPermission::VIEW]); $this->configureResource($role, Audio::class, [CrudPermission::VIEW]); + $this->configureResource($role, Group::class, [CrudPermission::VIEW]); $this->configureResource($role, ExternalResource::class, [CrudPermission::VIEW]); $this->configureResource($role, Image::class, [CrudPermission::VIEW]); $this->configureResource($role, Page::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php index ca9cb106a..f9fa5ef12 100644 --- a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php +++ b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php @@ -18,6 +18,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -65,6 +66,7 @@ public function run(): void $this->configureResource($role, AnimeThemeEntry::class, $extendedCrudPermissions); $this->configureResource($role, Artist::class, $extendedCrudPermissions); $this->configureResource($role, Audio::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Group::class, $extendedCrudPermissions); $this->configureResource($role, ExternalResource::class, $extendedCrudPermissions); $this->configureResource($role, Image::class, $extendedCrudPermissions); $this->configureResource($role, Page::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php index c9d3e6336..3c8f52c45 100644 --- a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php +++ b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php @@ -18,6 +18,7 @@ use App\Models\Wiki\Artist; use App\Models\Wiki\Audio; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Group; use App\Models\Wiki\Image; use App\Models\Wiki\Series; use App\Models\Wiki\Song; @@ -58,6 +59,7 @@ public function run(): void $this->configureResource($role, AnimeThemeEntry::class, [CrudPermission::VIEW]); $this->configureResource($role, Artist::class, [CrudPermission::VIEW]); $this->configureResource($role, Audio::class, [CrudPermission::VIEW]); + $this->configureResource($role, Group::class, [CrudPermission::VIEW]); $this->configureResource($role, ExternalResource::class, [CrudPermission::VIEW]); $this->configureResource($role, Image::class, [CrudPermission::VIEW]); $this->configureResource($role, Page::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Wiki/Group/GroupMigratingSeeder.php b/database/seeders/Wiki/Group/GroupMigratingSeeder.php new file mode 100644 index 000000000..c9935fd7e --- /dev/null +++ b/database/seeders/Wiki/Group/GroupMigratingSeeder.php @@ -0,0 +1,98 @@ +englishVersionGroup(); + $this->tvVersionGroup(); + $this->bdVersionGroup(); + $this->koreanVersionGroup(); + $this->hdRemasterGroup(); + $this->gintamaYorinukiGroup(); + } + + protected function englishVersionGroup() + { + $dubbedGroupThemes = AnimeTheme::query() + ->where(AnimeTheme::ATTRIBUTE_GROUP, 'Dubbed Version') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, 'Dubbed Version - Funimation') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, 'English Version') + ->get(); + + $dubbedGroup = Group::query()->where(Group::ATTRIBUTE_NAME, 'English Version')->get(); + $dubbedGroupThemes->each(fn (AnimeTheme $theme) => $theme->theme_group()->associate($dubbedGroup)->save()); + } + + protected function tvVersionGroup() + { + $tvVersionThemes = AnimeTheme::query() + ->where(AnimeTheme::ATTRIBUTE_GROUP, 'Original Broadcast Version') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, 'Original Japanese Version') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, 'Japanese Terrestrial Broadcast') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, '2005 Japanese Terrestrial Broadcast') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, 'TV version') + ->orWhere(AnimeTheme::ATTRIBUTE_GROUP, 'TV Broadcast') + ->get(); + + $tvVersionGroup = Group::query()->where(Group::ATTRIBUTE_NAME, 'TV Version')->get(); + $tvVersionThemes->each(fn (AnimeTheme $theme) => $theme->theme_group()->associate($tvVersionGroup)->save()); + + } + + protected function bdVersionGroup() + { + $bdVersionThemes = AnimeTheme::query() + ->where(AnimeTheme::ATTRIBUTE_GROUP, 'BD version') + ->get(); + + $bdVersionGroup = Group::query()->where(Group::ATTRIBUTE_NAME, 'BD Version')->get(); + $bdVersionThemes->each(fn (AnimeTheme $theme) => $theme->theme_group()->associate($bdVersionGroup)->save()); + } + + protected function koreanVersionGroup() + { + $koreanVersionThemes = AnimeTheme::query() + ->where(AnimeTheme::ATTRIBUTE_GROUP, 'Korean Version') + ->get(); + + $koreanVersionGroup = Group::query()->where(Group::ATTRIBUTE_NAME, 'Korean Version')->get(); + $koreanVersionThemes->each(fn (AnimeTheme $theme) => $theme->theme_group()->associate($koreanVersionGroup)->save()); + } + + protected function hdRemasterGroup() + { + $hdRemasterThemes = AnimeTheme::query() + ->where(AnimeTheme::ATTRIBUTE_GROUP, 'HD Remaster') + ->get(); + + $hdRemasterGroup = Group::query()->where(Group::ATTRIBUTE_NAME, 'HD Remaster')->get(); + $hdRemasterThemes->each(fn (AnimeTheme $theme) => $theme->theme_group()->associate($hdRemasterGroup)->save()); + } + + protected function gintamaYorinukiGroup() + { + $gintamaYorinukiThemes = AnimeTheme::query() + ->where(AnimeTheme::ATTRIBUTE_GROUP, 'Yorinuki Gintama-san') + ->get(); + + $gintamaYorinukiGroup = Group::query()->where(Group::ATTRIBUTE_NAME, 'Yorinuki Gintama-san')->get(); + $gintamaYorinukiThemes->each(fn (AnimeTheme $theme) => $theme->theme_group()->associate($gintamaYorinukiGroup)->save()); + } +} diff --git a/database/seeders/Wiki/Image/MoveImageToAppropriateFolderSeeder.php b/database/seeders/Wiki/Image/MoveImageToAppropriateFolderSeeder.php deleted file mode 100644 index 518622545..000000000 --- a/database/seeders/Wiki/Image/MoveImageToAppropriateFolderSeeder.php +++ /dev/null @@ -1,255 +0,0 @@ -grillFacetImageSeeder(); - //$this->animeImageSeeder(); - $this->artistImageSeeder(); - //$this->studioImageSeeder(); - } - - protected function grillFacetImageSeeder(): void - { - try { - DB::beginTransaction(); - - $chunkSize = 100; - /** @var FilesystemAdapter $fs */ - $fs = Storage::disk(Config::get('image.disk')); - $images = Image::query()->where(Image::ATTRIBUTE_FACET, ImageFacet::GRILL)->get(); - - foreach ($images->chunk($chunkSize) as $chunk) { - foreach ($chunk as $image) { - if ($image instanceof Image) { - $oldPath = $image->path; - $newPath = "grill/{$image->path}"; - - $fs->move($oldPath, $newPath); - - $image->update([ - Image::ATTRIBUTE_PATH => $newPath, - ]); - - DB::commit(); - - echo $oldPath . ' moved to ' . $newPath . "\n"; - } - } - sleep(5); - } - - echo "grill facet images done\n"; - - } catch (Exception $e) { - echo 'error ' . $e->getMessage() . "\n"; - - DB::rollBack(); - - throw $e; - } - } - - protected function animeImageSeeder(): void - { - try { - DB::beginTransaction(); - - $chunkSize = 100; - /** @var FilesystemAdapter $fs */ - $fs = Storage::disk(Config::get('image.disk')); - $animes = Anime::query()->where(Anime::ATTRIBUTE_ID, '>', 0)->whereHas(Image::TABLE, function (Builder $query) { - $query->where(Image::ATTRIBUTE_PATH, 'not like', '%/%'); - })->get(); - - foreach ($animes->chunk($chunkSize) as $chunk) { - foreach ($chunk as $anime) { - if ($anime instanceof Anime) { - $images = $anime->images()->get(); - - if (count($images) === 0) continue; - - $largeCover = $images->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE)->first(); - $smallCover = $images->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_SMALL)->first(); - - if (!empty($largeCover)) { - $largeCoverOldPath = $largeCover->path; - $largeCoverNewPath = "anime/large-cover/{$largeCover->path}"; - - $fs->move($largeCoverOldPath, $largeCoverNewPath); - - $largeCover->update([ - Image::ATTRIBUTE_PATH => $largeCoverNewPath, - ]); - - echo $largeCoverOldPath . ' moved to ' . $largeCoverNewPath . "\n"; - } - - if (!empty($smallCover)) { - $smallCoverOldPath = $smallCover->path; - $smallCoverNewPath = "anime/small-cover/{$smallCover->path}"; - - $fs->move($smallCoverOldPath, $smallCoverNewPath); - - $smallCover->update([ - Image::ATTRIBUTE_PATH => $smallCoverNewPath, - ]); - - echo $smallCoverOldPath . ' moved to ' . $smallCoverNewPath . "\n"; - } - - DB::commit(); - } - } - sleep(5); - } - - echo "anime images done\n"; - - - } catch (Exception $e) { - echo 'error ' . $e->getMessage() . "\n"; - - DB::rollBack(); - - throw $e; - } - } - - protected function artistImageSeeder(): void - { - try { - DB::beginTransaction(); - - $chunkSize = 100; - /** @var FilesystemAdapter $fs */ - $fs = Storage::disk(Config::get('image.disk')); - $artists = Artist::query()->where(Artist::ATTRIBUTE_ID, '>', 0)->whereHas(Image::TABLE, function (Builder $query) { - $query->where(Image::ATTRIBUTE_PATH, 'not like', '%/%'); - })->get(); - - foreach ($artists->chunk($chunkSize) as $chunk) { - foreach ($chunk as $artist) { - if ($artist instanceof Artist) { - $images = $artist->images()->get(); - - if (count($images) === 0) continue; - - $largeCover = $images->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE)->first(); - $smallCover = $images->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_SMALL)->first(); - - if (!empty($largeCover)) { - $largeCoverOldPath = $largeCover->path; - $largeCoverNewPath = "artist/large-cover/{$largeCover->path}"; - - $fs->move($largeCoverOldPath, $largeCoverNewPath); - - $largeCover->update([ - Image::ATTRIBUTE_PATH => $largeCoverNewPath, - ]); - - echo $largeCoverOldPath . ' moved to ' . $largeCoverNewPath . "\n"; - } - - if (!empty($smallCover)) { - $smallCoverOldPath = $smallCover->path; - $smallCoverNewPath = "artist/small-cover/{$smallCover->path}"; - - $fs->move($smallCoverOldPath, $smallCoverNewPath); - - $smallCover->update([ - Image::ATTRIBUTE_PATH => $smallCoverNewPath, - ]); - - echo $smallCoverOldPath . ' moved to ' . $smallCoverNewPath . "\n"; - } - - DB::commit(); - } - } - sleep(5); - } - - echo "artist images done\n"; - - } catch (Exception $e) { - echo 'error ' . $e->getMessage() . "\n"; - - DB::rollBack(); - - throw $e; - } - } - - protected function studioImageSeeder(): void - { - try { - DB::beginTransaction(); - - $chunkSize = 100; - /** @var FilesystemAdapter $fs */ - $fs = Storage::disk(Config::get('image.disk')); - $studios = Studio::query()->where(Studio::ATTRIBUTE_ID, '>', 0)->whereHas(Image::TABLE, function (Builder $query) { - $query->where(Image::ATTRIBUTE_PATH, 'not like', '%/%'); - })->get(); - - foreach ($studios->chunk($chunkSize) as $chunk) { - foreach ($chunk as $studio) { - if ($studio instanceof Studio) { - $images = $studio->images()->get(); - - if (count($images) === 0) continue; - - $largeCover = $images->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE)->first(); - - if (!empty($largeCover)) { - $oldPath = $largeCover->path; - $newPath = "studio/large-cover/{$largeCover->path}"; - - $fs->move($oldPath, $newPath); - - $largeCover->update([ - Image::ATTRIBUTE_PATH => $newPath, - ]); - - DB::commit(); - - echo $oldPath . ' moved to ' . $newPath . "\n"; - } - } - } - sleep(5); - } - - echo "studio images done\n"; - - } catch (Exception $e) { - echo 'error ' . $e->getMessage() . "\n"; - - DB::rollBack(); - - throw $e; - } - } -} \ No newline at end of file diff --git a/lang/en/nova.php b/lang/en/nova.php index 81db5f72d..a29e32aa5 100644 --- a/lang/en/nova.php +++ b/lang/en/nova.php @@ -567,6 +567,20 @@ 'name' => 'Start At', ], ], + 'group' => [ + 'name' => [ + 'name' => 'Name', + 'help' => 'The name of the group.', + ], + 'slug' => [ + 'name' => 'Slug', + 'help' => 'The slug that will be appended to the slug of the theme that has the group.', + ], + 'video_filename' => [ + 'name' => 'Video Filename', + 'help' => 'The filename that will be appended to the filaname of the video that is related to the group.', + ], + ], 'image' => [ 'facet' => [ 'help' => 'The page component that the image is intended for. Example: Is this a small cover image or a large cover image?', @@ -890,6 +904,7 @@ 'external_resource' => 'External Resource', 'feature' => 'Feature', 'featured_theme' => 'Featured Theme', + 'group' => 'Group', 'image' => 'Image', 'page' => 'Page', 'permission' => 'Permission', diff --git a/routes/api.php b/routes/api.php index 450ca93ae..5c62b5bf8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -38,6 +38,7 @@ use App\Http\Controllers\Api\Wiki\ArtistController; use App\Http\Controllers\Api\Wiki\AudioController; use App\Http\Controllers\Api\Wiki\ExternalResourceController; +use App\Http\Controllers\Api\Wiki\GroupController; use App\Http\Controllers\Api\Wiki\ImageController; use App\Http\Controllers\Api\Wiki\SeriesController; use App\Http\Controllers\Api\Wiki\SongController; @@ -273,6 +274,7 @@ function apiPivotResourceUri(string $name, string $related, string $foreign): st apiResource('anime', AnimeController::class); apiResource('artist', ArtistController::class); apiResource('audio', AudioController::class); +apiResource('group', GroupController::class); apiResource('image', ImageController::class); apiResource('resource', ExternalResourceController::class); Route::get('search', [SearchController::class, 'show'])->name('search.show');