Rails:如何通过另一个模型为模型中的字段指定有效值

时间:2012-03-05 02:55:14

标签: ruby-on-rails model controller

我想要做的是定义一个有效值列表,然后在相关表上添加新角色时验证该值列表。

让我举一个具体的例子:

说我有一个'就业'表,其中包含以下字段:

user_id (tied to a user table)
employer_id (tied to an employer table)
position_id (tied to a position table)
details
efbegdt
efenddt

当用户向此表添加新行时,我想确保employees_id和position_id已经存在于其他表中,并且如果在任何一个实例中都不是这样,则不允许保存。

到目前为止,我见过的解决方案采用了以下形式:

class Employment < ActiveRecord::Base
  EMPLOYERS = ['Google', 'Yahoo', 'Microsoft']
  POSITIONS = ['Web Developer', 'Database Admin', 'QA']
  validates_inclusion_of :employer_id, :in => EMPLOYERS
  validates_inclusion_of :position_id, :in => POSITIONS
end

但是,这种方法不够灵活,不足以容纳数以千计的雇主和职位,也不提供一种简单的方法来允许用户添加新的有效条目,如果他们的雇主目前不存在的话。

我也看过这种方法:

class Employment < ActiveRecord::Base
  validate :employer_exists

  protected

  def employer_exists
    ids = Employer.all.map(&:id)
    if !employer_id.blank? && !ids.member?(employer_id)
      errors.add(:employer_id, "invalid employer")
    end
  end
end

这更接近我想要的,但当我使用rspec测试时,检查雇主表上的新行是否有效失败:

Failure/Error: it { should be_valid }
  expected valid? to return true, got false

这个问题是否有“最佳实践”解决方案?

更新

添加另一个示例,详细说明所有设置。在此示例中,用户可以将多个电子邮件地址存储在电子邮件表中,但每种类型(个人,工作,学校等)限制一个地址。另一个表email_dfn定义了所有有效类型:

迁移文件

class CreateEmailDfns < ActiveRecord::Migration
  def change
    create_table :email_dfns do |t|
      t.string :short_description
      t.string :long_description

      t.timestamps
    end
  end
end

class CreateEmails < ActiveRecord::Migration
  def change
    create_table :emails do |t|
      t.integer :user_id
      t.integer :email_dfn_id
      t.string :value
      t.text :notes

      t.timestamps
    end
    add_index :emails, [:user_id, :email_dfn_id]
  end
end

模型

class Email < ActiveRecord::Base
  attr_accessible :value, :notes, :email_dfn_id
  belongs_to :user
  belongs_to :email_dfn

  validates_associated :email_dfn

  valid_email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :value, presence: true,
                    length: { maximum: 256 },
                    format: { with: valid_email_regex },
                    uniqueness: { case_sensitive: false }

  validates :user_id, presence: true
  validates :email_dfn_id, presence: true
end

class EmailDfn < ActiveRecord::Base

  attr_accessible :short_description,
                  :long_description,
  validates_uniqueness_of :short_description,
                          :long_description

  has_many :emails
end

测试

require 'spec_helper'

describe Email do

  let(:user) { FactoryGirl.create(:user) }
  before { @email = user.emails.build(email_dfn_id: 1,
                                      value: "personal_email@test.com",
                                      notes: "My personal email address") }

  subject { @email }

  it { should respond_to(:value) }
  it { should respond_to(:notes) }
  it { should respond_to(:email_dfn_id) }
  it { should respond_to(:user_id) }
  it { should respond_to(:user) }
  its(:user) { should == user }

  it { should be_valid }

  describe "when user id is not present" do
    before { @email.user_id = nil }
    it { should_not be_valid }
  end

  describe "when email id is invalid" do
    before { @email.email_dfn_id = 999 }
    it { should_not be_valid }
  end
end

在当前设置中,最后一次测试(设置email_dfn_id = 999,无效代码)失败。

3 个答案:

答案 0 :(得分:1)

使用validate_associated

class Employment < ActiveRecord::Base
  belongs_to :employee

  validates_associated :employee
end

阅读文档here

答案 1 :(得分:0)

怎么样,

validates :employer_id, presence: true, inclusion: { in: Employer.all.map(&:id), message: 'must be a valid employer.' }

答案 2 :(得分:0)

我在您的关系设置中假设user belongs_to employer

在这种情况下,您可以使用关联方法简化代码。

如果您使用的是默认表单构建器,

<% f.select("user", "employer_id", Employer.all) %> 

这会为您提供一个选择框,其中包含已定义的所有可能的雇主。但是,您的用户仍然可以欺骗创建并插入不存在的新雇主ID。 (旁注:我不知道为什么他们会这样做)。

尽可能地,我建议使用相关的方法,而不是编写自定义验证函数等,因为它使您的代码更容易理解给其他人。

如果你只需要验证,另一个答案可能更好=)

在您的创建操作中,您可以再进行一次检查。

def create
  @user = User.new(params[:id])
  @user.employer = Employer.find(params[:user][:employer_id])

  ..... # Standard save code or your own. 

end

如果更多的是与授权和安全性有关,您可以考虑实施像cancan这样的宝石,以缩小用户可以选择的选择范围。

用于授权和其他权限目的

例如,您不希望人们选择“禁用”/“预览”条目。如果你使用了cancan,你可以使用这样的方法。

<% f.select("user", "employer_id", Employer.accessible_by(current_ability) %> 

如果他们的能力得到了正确定义,那么尝试欺骗系统最终会被拒绝访问。