add character series lookup table
This commit is contained in:
parent
1caffecdad
commit
e7e9bd0f86
9 changed files with 267 additions and 0 deletions
22
app/blueprints/api/v1/character_series_blueprint.rb
Normal file
22
app/blueprints/api/v1/character_series_blueprint.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class CharacterSeriesBlueprint < ApiBlueprint
|
||||
field :name do |cs|
|
||||
{
|
||||
en: cs.name_en,
|
||||
ja: cs.name_jp
|
||||
}
|
||||
end
|
||||
|
||||
fields :slug, :order
|
||||
|
||||
view :full do
|
||||
field :character_count do |cs|
|
||||
cs.characters.count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
72
app/controllers/api/v1/character_series_controller.rb
Normal file
72
app/controllers/api/v1/character_series_controller.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class CharacterSeriesController < Api::V1::ApiController
|
||||
before_action :set_character_series, only: %i[show update destroy]
|
||||
before_action :ensure_editor_role, only: %i[create update destroy]
|
||||
|
||||
# GET /character_series
|
||||
def index
|
||||
character_series = CharacterSeries.ordered
|
||||
render json: CharacterSeriesBlueprint.render(character_series)
|
||||
end
|
||||
|
||||
# GET /character_series/:id
|
||||
def show
|
||||
render json: CharacterSeriesBlueprint.render(@character_series, view: :full)
|
||||
end
|
||||
|
||||
# POST /character_series
|
||||
def create
|
||||
character_series = CharacterSeries.new(character_series_params)
|
||||
|
||||
if character_series.save
|
||||
render json: CharacterSeriesBlueprint.render(character_series, view: :full), status: :created
|
||||
else
|
||||
render_validation_error_response(character_series)
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /character_series/:id
|
||||
def update
|
||||
if @character_series.update(character_series_params)
|
||||
render json: CharacterSeriesBlueprint.render(@character_series, view: :full)
|
||||
else
|
||||
render_validation_error_response(@character_series)
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /character_series/:id
|
||||
def destroy
|
||||
if @character_series.characters.exists?
|
||||
render json: ErrorBlueprint.render(nil, error: {
|
||||
message: 'Cannot delete series with associated characters',
|
||||
code: 'has_dependencies'
|
||||
}), status: :unprocessable_entity
|
||||
else
|
||||
@character_series.destroy!
|
||||
head :no_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_character_series
|
||||
# Support lookup by slug or UUID
|
||||
@character_series = CharacterSeries.find_by(slug: params[:id]) || CharacterSeries.find(params[:id])
|
||||
end
|
||||
|
||||
def ensure_editor_role
|
||||
return if current_user&.role && current_user.role >= 7
|
||||
|
||||
Rails.logger.warn "[CHARACTER_SERIES] Unauthorized access attempt by user #{current_user&.id}"
|
||||
render json: { error: 'Unauthorized - Editor role required' }, status: :unauthorized
|
||||
end
|
||||
|
||||
def character_series_params
|
||||
params.require(:character_series).permit(:name_en, :name_jp, :slug, :order)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/models/character_series.rb
Normal file
25
app/models/character_series.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CharacterSeries < ApplicationRecord
|
||||
has_many :character_series_memberships, dependent: :destroy
|
||||
has_many :characters, through: :character_series_memberships
|
||||
|
||||
validates :name_en, presence: true
|
||||
validates :name_jp, presence: true
|
||||
validates :slug, presence: true, uniqueness: true
|
||||
validates :order, numericality: { only_integer: true }
|
||||
|
||||
scope :ordered, -> { order(:order) }
|
||||
|
||||
# Slug constants for commonly referenced series
|
||||
STANDARD = 'standard'
|
||||
GRAND = 'grand'
|
||||
ZODIAC = 'zodiac'
|
||||
ETERNAL = 'eternal'
|
||||
EVOKER = 'evoker'
|
||||
SAINT = 'saint'
|
||||
|
||||
def blueprint
|
||||
CharacterSeriesBlueprint
|
||||
end
|
||||
end
|
||||
8
app/models/character_series_membership.rb
Normal file
8
app/models/character_series_membership.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CharacterSeriesMembership < ApplicationRecord
|
||||
belongs_to :character
|
||||
belongs_to :character_series
|
||||
|
||||
validates :character_id, uniqueness: { scope: :character_series_id }
|
||||
end
|
||||
37
db/data/20251214104718_create_character_series_records.rb
Normal file
37
db/data/20251214104718_create_character_series_records.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCharacterSeriesRecords < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
character_series_data = [
|
||||
{ order: 0, slug: 'standard', name_en: 'Standard', name_jp: 'スタンダード' },
|
||||
{ order: 1, slug: 'grand', name_en: 'Grand', name_jp: 'リミテッド' },
|
||||
{ order: 2, slug: 'zodiac', name_en: 'Zodiac', name_jp: '十二神将' },
|
||||
{ order: 3, slug: 'promo', name_en: 'Promo', name_jp: 'プロモ' },
|
||||
{ order: 4, slug: 'collab', name_en: 'Collab', name_jp: 'コラボ' },
|
||||
{ order: 5, slug: 'eternal', name_en: 'Eternal', name_jp: '十天衆' },
|
||||
{ order: 6, slug: 'evoker', name_en: 'Evoker', name_jp: '賢者' },
|
||||
{ order: 7, slug: 'saint', name_en: 'Saint', name_jp: '六竜の使徒' },
|
||||
{ order: 8, slug: 'fantasy', name_en: 'Fantasy', name_jp: 'ファンタジー' },
|
||||
{ order: 9, slug: 'summer', name_en: 'Summer', name_jp: '水着' },
|
||||
{ order: 10, slug: 'yukata', name_en: 'Yukata', name_jp: '浴衣' },
|
||||
{ order: 11, slug: 'valentine', name_en: 'Valentine', name_jp: 'バレンタイン' },
|
||||
{ order: 12, slug: 'halloween', name_en: 'Halloween', name_jp: 'ハロウィン' },
|
||||
{ order: 13, slug: 'formal', name_en: 'Formal', name_jp: 'フォーマル' },
|
||||
{ order: 14, slug: 'event', name_en: 'Event', name_jp: 'イベント' }
|
||||
]
|
||||
|
||||
puts 'Creating character series records...'
|
||||
character_series_data.each do |data|
|
||||
cs = CharacterSeries.find_or_initialize_by(slug: data[:slug])
|
||||
cs.assign_attributes(data)
|
||||
cs.save!
|
||||
puts " #{cs.slug}: #{cs.name_en}"
|
||||
end
|
||||
|
||||
puts "\nCreated #{CharacterSeries.count} character series records"
|
||||
end
|
||||
|
||||
def down
|
||||
CharacterSeries.delete_all
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MigrateCharacterSeriesToMemberships < ActiveRecord::Migration[8.0]
|
||||
# Mapping from legacy integer values to slugs
|
||||
LEGACY_TO_SLUG = {
|
||||
1 => 'standard',
|
||||
2 => 'grand',
|
||||
3 => 'zodiac',
|
||||
4 => 'promo',
|
||||
5 => 'collab',
|
||||
6 => 'eternal',
|
||||
7 => 'evoker',
|
||||
8 => 'saint',
|
||||
9 => 'fantasy',
|
||||
10 => 'summer',
|
||||
11 => 'yukata',
|
||||
12 => 'valentine',
|
||||
13 => 'halloween',
|
||||
14 => 'formal',
|
||||
15 => 'event'
|
||||
}.freeze
|
||||
|
||||
def up
|
||||
# Build lookup hash: slug -> UUID
|
||||
slug_to_uuid = CharacterSeries.pluck(:slug, :id).to_h
|
||||
|
||||
migrated = 0
|
||||
memberships_created = 0
|
||||
|
||||
Character.where.not(series: []).find_each do |character|
|
||||
character.series.each do |series_int|
|
||||
slug = LEGACY_TO_SLUG[series_int]
|
||||
next unless slug
|
||||
|
||||
series_id = slug_to_uuid[slug]
|
||||
next unless series_id
|
||||
|
||||
CharacterSeriesMembership.find_or_create_by!(
|
||||
character_id: character.id,
|
||||
character_series_id: series_id
|
||||
)
|
||||
memberships_created += 1
|
||||
end
|
||||
migrated += 1
|
||||
end
|
||||
|
||||
puts "Migrated #{migrated} characters, created #{memberships_created} memberships"
|
||||
end
|
||||
|
||||
def down
|
||||
CharacterSeriesMembership.delete_all
|
||||
end
|
||||
end
|
||||
16
db/data/20251214193836_add_holiday_character_series.rb
Normal file
16
db/data/20251214193836_add_holiday_character_series.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddHolidayCharacterSeries < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
CharacterSeries.find_or_create_by!(slug: 'holiday') do |cs|
|
||||
cs.order = 15
|
||||
cs.name_en = 'Holiday'
|
||||
cs.name_jp = 'クリスマス'
|
||||
end
|
||||
puts 'Created holiday character series'
|
||||
end
|
||||
|
||||
def down
|
||||
CharacterSeries.find_by(slug: 'holiday')&.destroy
|
||||
end
|
||||
end
|
||||
15
db/migrate/20251214104716_create_character_series.rb
Normal file
15
db/migrate/20251214104716_create_character_series.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCharacterSeries < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :character_series, id: :uuid do |t|
|
||||
t.string :name_en, null: false
|
||||
t.string :name_jp, null: false
|
||||
t.string :slug, null: false
|
||||
t.integer :order, default: 0, null: false
|
||||
end
|
||||
|
||||
add_index :character_series, :slug, unique: true
|
||||
add_index :character_series, :order
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCharacterSeriesMemberships < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :character_series_memberships, id: :uuid do |t|
|
||||
t.uuid :character_id, null: false
|
||||
t.uuid :character_series_id, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :character_series_memberships, :character_id
|
||||
add_index :character_series_memberships, :character_series_id
|
||||
add_index :character_series_memberships, %i[character_id character_series_id],
|
||||
unique: true, name: 'idx_char_series_membership_unique'
|
||||
|
||||
add_foreign_key :character_series_memberships, :characters
|
||||
add_foreign_key :character_series_memberships, :character_series
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue