mike9182
mike9182

Reputation: 289

Single Table Inheritance Errors - ActiveRecord::SubclassNotFound

My intent is to implement STI with two types: Staff and Clinician. My previous implementation was using roles with enums, and after doing my best to follow answers to similar questions, take out all references in tests etc. to enum roles and replace with references to types , I am getting many versions of the following error when I run my testing suite:

ERROR["test_valid_signup_information_with_account_activation", UsersSignupTest, 1.01794000000001]
 test_valid_signup_information_with_account_activation#UsersSignupTest (1.02s)
ActiveRecord::SubclassNotFound:         ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: 'Staff'. This error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this column if you didn't intend it to be used for storing the inheritance class or overwrite User.inheritance_column to use another column for that information.
            app/controllers/users_controller.rb:19:in `create'
            test/integration/users_signup_test.rb:27:in `block (2 levels) in <class:UsersSignupTest>'
            test/integration/users_signup_test.rb:26:in `block in <class:UsersSignupTest>'

Here are a couple areas where I am confused that could potentially be hiding issues:

In my user model user.rb, I think I am defining the sub Classes correctly (Staff and Clinician), but I'm unsure if I'm wrapping everything correctly. Does all the other code have to be contained in one of these classes? Am I misusing "end"?

class User < ApplicationRecord
end

class Staff < User
end

class Clinician < User
end

belongs_to :university
has_many :referral_requests




  attr_accessor :remember_token, :activation_token, :reset_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :type, presence: true
  validates :university_id, presence: true, if: lambda { self.type == 'Staff' }

  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true






  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
        return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end


  # Forgets a user.
  def forget
    update_attribute(:remember_digest, nil)
  end

    # Returns true if the given token matches the digest.
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

  # Activates an account.
  def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end

  # Sends activation email.
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

 # Sets the password reset attributes.
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # Sends password reset email.
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

   # Returns true if a password reset has expired.
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

 def feed
    ReferralRequest.where("user_id = ?", id)
  end


 private

    # Converts email to all lower-case.
    def downcase_email
    self.email = email.downcase
    end

    # Creates and assigns the activation token and digest.
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

Here's the specific test code that is failing (one of many in the test suite that are failing - all the user parameters are defined similarly though). Am I passing the staff parameter appropriately?

 test "valid signup information with account activation" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "[email protected]",
                                         university_id: 1 ,
                                         type: "Staff",
                                         password:              "password",
                                         password_confirmation: "password" } }

Here is my users table schema:

create_table "users", force: :cascade do |t|
    t.string   "name"
    t.string   "email"
    t.datetime "created_at",                        null: false
    t.datetime "updated_at",                        null: false
    t.string   "password_digest"
    t.string   "remember_digest"
    t.string   "activation_digest"
    t.boolean  "activated",         default: false
    t.datetime "activated_at"
    t.string   "reset_digest"
    t.datetime "reset_sent_at"
    t.integer  "university_id"
    t.integer  "role"
    t.string   "type"
    t.index ["email"], name: "index_users_on_email", unique: true
  end

Thanks very much for any ideas! I ask a lot of questions on here but it's only after trying to work through similar answers for quite a while.

Upvotes: 0

Views: 2640

Answers (1)

Adam Lassek
Adam Lassek

Reputation: 35505

Assuming that the code sample above is accurate, you are seeing this error because the user.rb file is invalid Ruby and failing to parse. You should also be seeing an interpreter error about that.

class User < ApplicationRecord
  belongs_to :university
  has_many :referral_requests

  attr_accessor :remember_token, :activation_token, :reset_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :type, presence: true

  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  # etc...
end

class Staff < User
  validates :university_id, presence: true
end

class Clinician < User
end

Standard class-inheritance practices apply, so if there is code in there that is only appropriate for a specific subclass it should move there (e.g. university_id validation moving to Staff).

# Returns the hash digest of the given string.
def User.digest(string)
  cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost
  BCrypt::Password.create(string, cost: cost)
end

# Returns a random token.
def User.new_token
  SecureRandom.urlsafe_base64
end

These should be written as

def self.digest(string)
  # ...
end

def self.new_token
  # ...
end

or, alternatively,

class << self
  def digest(string)
    # ...
  end

  def new_token
    # ...
  end
end

Upvotes: 1

Related Questions