diff --git a/lib/statesman/multi_state/active_record_macro.rb b/lib/statesman/multi_state/active_record_macro.rb index c145607..773b8e1 100644 --- a/lib/statesman/multi_state/active_record_macro.rb +++ b/lib/statesman/multi_state/active_record_macro.rb @@ -9,13 +9,15 @@ module ActiveRecordMacro class_methods do def has_one_state_machine(field_name, state_machine_klass:, transition_klass:, - transition_name: transition_klass.to_s.underscore.pluralize.to_sym, virtual_attribute_name: "#{field_name}_state_form") + transition_name: transition_klass.to_s.underscore.pluralize.to_sym, virtual_attribute_name: "#{field_name}_state_form", + transition_foreign_key: nil) state_machine_name = "#{field_name}_state_machine" # To handle STI, this needs to be done to get the base klass base_klass = caller_locations.first.label.split(':').last[...-1] - has_many transition_name, class_name: transition_klass.to_s, autosave: false, dependent: :destroy + association_options = { class_name: transition_klass.to_s, autosave: false, dependent: :destroy }.merge(transition_foreign_key ? { foreign_key: transition_foreign_key.to_s } : {} ) + has_many transition_name, **association_options include Statesman::Adapters::CustomActiveRecordQueries[ transition_class: transition_klass.constantize, @@ -94,7 +96,8 @@ def save_with_state(**options) field_name, nil, { state_machine_klass: state_machine_klass, transition_klass: transition_klass, - transition_name: transition_name, virtual_attribute_name: virtual_attribute_name }, + transition_name: transition_name, virtual_attribute_name: virtual_attribute_name, + transition_foreign_key: transition_foreign_key }, self ) diff --git a/test/dummy/app/models/foreign_key_status_order_transition.rb b/test/dummy/app/models/foreign_key_status_order_transition.rb new file mode 100644 index 0000000..75a29ef --- /dev/null +++ b/test/dummy/app/models/foreign_key_status_order_transition.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ForeignKeyStatusOrderTransition < ApplicationRecord + include Statesman::Adapters::ActiveRecordTransition + + belongs_to :order, foreign_key: 'custom_fk_id', inverse_of: :foreign_key_status_order_transitions + + after_destroy :update_most_recent, if: :most_recent? + + private + + def update_most_recent + last_transition = order.foreign_key_status_order_transitions.order(:sort_key).last + return unless last_transition.present? + last_transition.update_column(:most_recent, true) + end +end diff --git a/test/dummy/app/models/order.rb b/test/dummy/app/models/order.rb index 94fd70d..cd30f4c 100644 --- a/test/dummy/app/models/order.rb +++ b/test/dummy/app/models/order.rb @@ -7,4 +7,8 @@ class Order < ApplicationRecord transition_klass: 'AdminStatusOrderTransition' has_one_state_machine :custom_status, state_machine_klass: 'AdminStatusOrderStateMachine', transition_klass: 'AdminStatusOrderTransition', transition_name: :transitions, virtual_attribute_name: 'my_attribute' + has_one_state_machine :foreign_key_status, state_machine_klass: 'UserStatusOrderStateMachine', + transition_klass: 'ForeignKeyStatusOrderTransition', + transition_foreign_key: 'custom_fk_id' + end diff --git a/test/dummy/db/migrate/20231004142606_create_foreign_key_status_order_transitions.rb b/test/dummy/db/migrate/20231004142606_create_foreign_key_status_order_transitions.rb new file mode 100644 index 0000000..e368825 --- /dev/null +++ b/test/dummy/db/migrate/20231004142606_create_foreign_key_status_order_transitions.rb @@ -0,0 +1,29 @@ +class CreateForeignKeyStatusOrderTransitions < ActiveRecord::Migration[7.0] + def change + create_table :foreign_key_status_order_transitions do |t| + t.string :to_state, null: false + t.text :metadata, default: '{}' + t.integer :sort_key, null: false + t.integer :custom_fk_id, null: false + t.boolean :most_recent, null: false + + # If you decide not to include an updated timestamp column in your transition + # table, you'll need to configure the `updated_timestamp_column` setting in your + # migration class. + t.timestamps null: false + end + + # Foreign keys are optional, but highly recommended + add_foreign_key :foreign_key_status_order_transitions, :orders, column: :custom_fk_id + + add_index(:foreign_key_status_order_transitions, + %i(custom_fk_id sort_key), + unique: true, + name: "index_foreign_key_status_order_transitions_parent_sort") + add_index(:foreign_key_status_order_transitions, + %i(custom_fk_id most_recent), + unique: true, + where: "most_recent", + name: "index_foreign_key_status_order_transitions_parent_most_recent") + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 6edf29d..15a131c 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,38 +10,49 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 20_221_005_223_432) do - create_table 'admin_status_order_transitions', force: :cascade do |t| - t.string 'to_state', null: false - t.text 'metadata', default: '{}' - t.integer 'sort_key', null: false - t.integer 'order_id', null: false - t.boolean 'most_recent', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index %w[order_id most_recent], name: 'index_admin_status_order_transitions_parent_most_recent', unique: true, - where: 'most_recent' - t.index %w[order_id sort_key], name: 'index_admin_status_order_transitions_parent_sort', unique: true +ActiveRecord::Schema[7.0].define(version: 2023_10_04_142606) do + create_table "admin_status_order_transitions", force: :cascade do |t| + t.string "to_state", null: false + t.text "metadata", default: "{}" + t.integer "sort_key", null: false + t.integer "order_id", null: false + t.boolean "most_recent", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["order_id", "most_recent"], name: "index_admin_status_order_transitions_parent_most_recent", unique: true, where: "most_recent" + t.index ["order_id", "sort_key"], name: "index_admin_status_order_transitions_parent_sort", unique: true + end + + create_table "foreign_key_status_order_transitions", force: :cascade do |t| + t.string "to_state", null: false + t.text "metadata", default: "{}" + t.integer "sort_key", null: false + t.integer "custom_fk_id", null: false + t.boolean "most_recent", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["custom_fk_id", "most_recent"], name: "index_foreign_key_status_order_transitions_parent_most_recent", unique: true, where: "most_recent" + t.index ["custom_fk_id", "sort_key"], name: "index_foreign_key_status_order_transitions_parent_sort", unique: true end - create_table 'orders', force: :cascade do |t| - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false + create_table "orders", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'user_status_order_transitions', force: :cascade do |t| - t.string 'to_state', null: false - t.text 'metadata', default: '{}' - t.integer 'sort_key', null: false - t.integer 'order_id', null: false - t.boolean 'most_recent', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index %w[order_id most_recent], name: 'index_user_status_order_transitions_parent_most_recent', unique: true, - where: 'most_recent' - t.index %w[order_id sort_key], name: 'index_user_status_order_transitions_parent_sort', unique: true + create_table "user_status_order_transitions", force: :cascade do |t| + t.string "to_state", null: false + t.text "metadata", default: "{}" + t.integer "sort_key", null: false + t.integer "order_id", null: false + t.boolean "most_recent", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["order_id", "most_recent"], name: "index_user_status_order_transitions_parent_most_recent", unique: true, where: "most_recent" + t.index ["order_id", "sort_key"], name: "index_user_status_order_transitions_parent_sort", unique: true end - add_foreign_key 'admin_status_order_transitions', 'orders' - add_foreign_key 'user_status_order_transitions', 'orders' + add_foreign_key "admin_status_order_transitions", "orders" + add_foreign_key "foreign_key_status_order_transitions", "orders", column: "custom_fk_id" + add_foreign_key "user_status_order_transitions", "orders" end diff --git a/test/statesman/multi_state/reflection_test.rb b/test/statesman/multi_state/reflection_test.rb index 36b5201..3340e19 100644 --- a/test/statesman/multi_state/reflection_test.rb +++ b/test/statesman/multi_state/reflection_test.rb @@ -17,6 +17,9 @@ class ReflectionTest < ActiveSupport::TestCase reflection = Order.reflect_on_state_machine(:custom_status) assert_equal :transitions, reflection.options[:transition_name] assert_equal 'my_attribute', reflection.options[:virtual_attribute_name] + + reflection = Order.reflect_on_state_machine(:foreign_key_status) + assert_equal 'custom_fk_id', reflection.options[:transition_foreign_key] end test 'reflection on a singular state machine with the same name as a state machine on another model' do @@ -34,9 +37,9 @@ class ReflectionTest < ActiveSupport::TestCase reflections = Order.reflect_on_all_state_machines.sort_by(&:name) assert_equal [Order], reflections.collect(&:active_record).uniq - assert_equal %i[admin_status custom_status user_status], reflections.collect(&:name) - assert_equal %i[has_one_state_machine has_one_state_machine has_one_state_machine], reflections.collect(&:macro) - assert_equal %i[admin_status_order_transitions transitions user_status_order_transitions], reflections.collect { |reflection| + assert_equal %i[admin_status custom_status foreign_key_status user_status], reflections.collect(&:name) + assert_equal %i[has_one_state_machine has_one_state_machine has_one_state_machine has_one_state_machine], reflections.collect(&:macro) + assert_equal %i[admin_status_order_transitions transitions foreign_key_status_order_transitions user_status_order_transitions], reflections.collect { |reflection| reflection.options[:transition_name] } end