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