diff --git a/app/blueprints/api/v1/summon_series_blueprint.rb b/app/blueprints/api/v1/summon_series_blueprint.rb new file mode 100644 index 0000000..e90753c --- /dev/null +++ b/app/blueprints/api/v1/summon_series_blueprint.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Api + module V1 + class SummonSeriesBlueprint < ApiBlueprint + field :name do |ss| + { + en: ss.name_en, + ja: ss.name_jp + } + end + + fields :slug, :order + + view :full do + field :summon_count do |ss| + ss.summons.count + end + end + end + end +end diff --git a/app/controllers/api/v1/summon_series_controller.rb b/app/controllers/api/v1/summon_series_controller.rb new file mode 100644 index 0000000..f1deb68 --- /dev/null +++ b/app/controllers/api/v1/summon_series_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Api + module V1 + class SummonSeriesController < Api::V1::ApiController + before_action :set_summon_series, only: %i[show update destroy] + before_action :ensure_editor_role, only: %i[create update destroy] + + # GET /summon_series + def index + summon_series = SummonSeries.ordered + render json: SummonSeriesBlueprint.render(summon_series) + end + + # GET /summon_series/:id + def show + render json: SummonSeriesBlueprint.render(@summon_series, view: :full) + end + + # POST /summon_series + def create + summon_series = SummonSeries.new(summon_series_params) + + if summon_series.save + render json: SummonSeriesBlueprint.render(summon_series, view: :full), status: :created + else + render_validation_error_response(summon_series) + end + end + + # PATCH/PUT /summon_series/:id + def update + if @summon_series.update(summon_series_params) + render json: SummonSeriesBlueprint.render(@summon_series, view: :full) + else + render_validation_error_response(@summon_series) + end + end + + # DELETE /summon_series/:id + def destroy + if @summon_series.summons.exists? + render json: ErrorBlueprint.render(nil, error: { + message: 'Cannot delete series with associated summons', + code: 'has_dependencies' + }), status: :unprocessable_entity + else + @summon_series.destroy! + head :no_content + end + end + + private + + def set_summon_series + # Support lookup by slug or UUID + @summon_series = SummonSeries.find_by(slug: params[:id]) || SummonSeries.find(params[:id]) + end + + def ensure_editor_role + return if current_user&.role && current_user.role >= 7 + + Rails.logger.warn "[SUMMON_SERIES] Unauthorized access attempt by user #{current_user&.id}" + render json: { error: 'Unauthorized - Editor role required' }, status: :unauthorized + end + + def summon_series_params + params.require(:summon_series).permit(:name_en, :name_jp, :slug, :order) + end + end + end +end diff --git a/app/models/summon_series.rb b/app/models/summon_series.rb new file mode 100644 index 0000000..d1901e3 --- /dev/null +++ b/app/models/summon_series.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class SummonSeries < ApplicationRecord + has_many :summons, dependent: :restrict_with_error + + 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 + PROVIDENCE = 'providence' + GENESIS = 'genesis' + MAGNA = 'magna' + OPTIMUS = 'optimus' + ARCARUM = 'arcarum' + + def blueprint + SummonSeriesBlueprint + end +end diff --git a/db/data/20251214104722_create_summon_series_records.rb b/db/data/20251214104722_create_summon_series_records.rb new file mode 100644 index 0000000..00f95ea --- /dev/null +++ b/db/data/20251214104722_create_summon_series_records.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class CreateSummonSeriesRecords < ActiveRecord::Migration[8.0] + def up + summon_series_data = [ + { order: 0, slug: 'providence', name_en: 'Providence Series', name_jp: 'プロヴィデンスシリーズ' }, + { order: 1, slug: 'genesis', name_en: 'Genesis Series', name_jp: 'ジェネシスシリーズ' }, + { order: 2, slug: 'magna', name_en: 'Magna Series', name_jp: 'マグナシリーズ' }, + { order: 3, slug: 'optimus', name_en: 'Optimus Series', name_jp: 'オプティマスシリーズ' }, + { order: 4, slug: 'demi-optimus', name_en: 'Demi Optimus Series', name_jp: 'オプティマス・ディヴィジョン' }, + { order: 5, slug: 'archangel', name_en: 'Archangel Series', name_jp: '天司シリーズ' }, + { order: 6, slug: 'arcarum', name_en: 'Arcarum Series', name_jp: 'アーカルムシリーズ' }, + { order: 7, slug: 'epic', name_en: 'Epic Series', name_jp: 'エピックシリーズ' }, + { order: 8, slug: 'carbuncle', name_en: 'Carbuncle Series', name_jp: 'カーバンクルシリーズ' }, + { order: 9, slug: 'dynamis', name_en: 'Dynamis Series', name_jp: 'デュナミスシリーズ' }, + { order: 10, slug: 'cryptid', name_en: 'Cryptid Series', name_jp: 'UMAシリーズ' }, + { order: 11, slug: 'six-dragons', name_en: 'Six Dragons', name_jp: '六竜シリーズ' }, + { order: 12, slug: 'bellum', name_en: 'Bellum Series', name_jp: 'ベルムシリーズ' }, + { order: 13, slug: 'crest', name_en: 'Crest Series', name_jp: 'クレストシリーズ' }, + { order: 14, slug: 'robur', name_en: 'Robur Series', name_jp: 'ロブルシリーズ' }, + { order: 15, slug: 'summer', name_en: 'Summer', name_jp: '水着' }, + { order: 16, slug: 'yukata', name_en: 'Yukata', name_jp: '浴衣' }, + { order: 17, slug: 'holiday', name_en: 'Holiday', name_jp: 'クリスマス' }, + { order: 18, slug: 'collab', name_en: 'Collab', name_jp: 'コラボ' } + ] + + puts 'Creating summon series records...' + summon_series_data.each do |data| + ss = SummonSeries.find_or_initialize_by(slug: data[:slug]) + ss.assign_attributes(data) + ss.save! + puts " #{ss.slug}: #{ss.name_en}" + end + + puts "\nCreated #{SummonSeries.count} summon series records" + end + + def down + SummonSeries.delete_all + end +end diff --git a/db/migrate/20251214104720_create_summon_series.rb b/db/migrate/20251214104720_create_summon_series.rb new file mode 100644 index 0000000..ceae315 --- /dev/null +++ b/db/migrate/20251214104720_create_summon_series.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateSummonSeries < ActiveRecord::Migration[8.0] + def change + create_table :summon_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 :summon_series, :slug, unique: true + add_index :summon_series, :order + end +end diff --git a/db/migrate/20251214104721_add_summon_series_id_to_summons.rb b/db/migrate/20251214104721_add_summon_series_id_to_summons.rb new file mode 100644 index 0000000..12d4156 --- /dev/null +++ b/db/migrate/20251214104721_add_summon_series_id_to_summons.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddSummonSeriesIdToSummons < ActiveRecord::Migration[8.0] + def change + add_column :summons, :summon_series_id, :uuid + add_index :summons, :summon_series_id + add_foreign_key :summons, :summon_series + end +end