I'm looking for a way to use ActiveRecord::Base#valid? on only one attribute. Consider the following example, inspired by RailsGuides:
class Product < ActiveRecord::Base
belongs_to :company
validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/ }
validates :name, uniqueness: { scope: :company }
end
Calling Product#valid? will trigger a SQL query because of the name validation:
Product.new(legacy_code: "helloworld", company: c).valid?
Product Exists (0.3ms) SELECT 1 AS one FROM "products" WHERE ("products"."name" IS NULL AND "products"."company_id" = 1) LIMIT 1
=> true
Now, I'm only looking to validate the legacy_code attribute:
Product.new(legacy_code: "helloworld").valid?
Product Exists (0.2ms) SELECT 1 AS one FROM "products" WHERE ("products"."name" IS NULL AND "products"."company_id" IS NULL) LIMIT 1
=> true
See the issue? I'd love to use something like Product#valid?(:legacy_code) and avoid the query that's trying to validate an attribute I don't care about.
So far, my solution is to use a Product#valid_legacy_code? method that I can call independently, and call this method as well in the validation declarations on my model.
I ended up writing a gem for that: https://github.com/KevinBongart/valid_attribute
# Test only one attribute:
product.valid_attribute?(:company) # => true
product.valid_attribute?(:name) # => true
product.valid_attribute?(:legacy_code) # => false
# Test several attributes at once, like a boss:
product.valid_attribute?(:company, :name) # => true
product.valid_attribute?(:company, :name, :legacy_code) # => false
# Wow, you can even filter down to a specific validator:
product.valid_attribute?(name: :format) # => true
product.valid_attribute?(name: [:format, :uniqueness]) # => true
product.valid_attribute?(name: :format, company: :presence) # => true
Related
The DeployedSkill object references a ValuesList object which provides a limited value domain to users answers. An additional "filter" field contains an expression to filter even more the values of the domain. Values are instances of the Value class, belonging to the ValuesList object.
The model looks like this:
class DeployedSkill < Skill
### before filter
before_validation :set_code
# Custom validation of the filter attribute
validate :values_filter
validates :code, presence: true, uniqueness: {scope: :business_object_id, case_sensitive: false}, length: { maximum: 32 }
belongs_to :template_skill, :class_name => "DefinedSkill", :foreign_key => "template_skill_id" # helps retrieving the skill template
belongs_to :parent, :class_name => "DeployedObject", :foreign_key => "business_object_id"
belongs_to :values_list, inverse_of: :deployed_skills
### private functions definitions
private
### format code
def set_code
if self.code[0, 3] == "DV_"
self.code = self.code[3 .. -1]
end
self.code = "#{code.gsub(/[^0-9A-Za-z]/, '_')}".upcase[0,32]
end
def values_filter
begin
puts "----- Test the filter -----"
Value.where("#{filter}").first
rescue
puts "----- Filter test failed -----"
self.errors.add(:filter, "is not correctly specified")
puts self.errors.map {|error, message| [error, message]}.join(', ')
end
end
end
Before saving the updated DeployedSkill, I validate that the filter expression is compatible with a database query.
Using ByeBug, I can see that the test works fine, and an error is added to ActiveModel::Errors list when filter's syntax is not correct:
#<ActiveModel::Errors:0x0000000012bf03a8 #base=#<DeployedSkill id: 638, playground_id: 0,
business_object_id: 632, code: "SAKO1_METHODE", blablabla ... ,
type: "DeployedSkill", filter: "'xxx'", global_identifier: nil, sort_code: "SAKO1_METHODE",
physical_name: nil>,
#messages={:filter=>["is not correctly specified"]},
#details={:filter=>[{:error=>"is not correctly specified"}]}>
The console echoes it:
----- Test the filter -----
Value Load (10.2ms) SELECT "values".* FROM "values" WHERE (code like 'S%) ORDER BY "values"."id" ASC LIMIT $1 [["LIMIT", 1]]
----- Filter test failed -----
filter, is not correctly specified
(0.8ms) ROLLBACK
Nevertheless, the update method instruction:
if #deployed_skill.update_attributes(deployed_skill_params)
breaks into an error:
PG::InFailedSqlTransaction: ERROR: current transaction was aborted, commands are ignored until the end of the transaction :
SELECT exists(
SELECT * FROM pg_proc
WHERE proname = 'lower' AND proargtypes = ARRAY['character varying(255)'::regtype]::oidvector )
OR exists( SELECT * FROM pg_proc INNER JOIN pg_cast ON ARRAY[casttarget]::oidvector = proargtypes
WHERE proname = 'lower' AND castsource = 'character varying(255)'::regtype )
At this point, I have no idea of what's going on, and I don't know where to search deeper.
I hope you can help me to understand the issue and find the solution!
Thanks!
I'm in the process of trying to duplicate a record and all of its subsequent child associations which are deeply nested. Below is a simplification of the problem.
I have the following Foo model:
class Foo < ActiveRecord::Base
belongs_to :project,
inverse_of::foos
validates :name,presence:true,uniqueness:{ scope: :project_id },
format:{ with: /[0-9a-zA-Z\s\/_:\-.|]*/ }
end
I'm duplicating the project like this:
origin = Project.first
clone = origin.dup
clone.foos << clone_records(origin.foos)
clone.save
def clone_records(records)
clones = []
records.each do |record|
cloned_record = record.dup
cloned_record.project_id = nil
cloned_record.name = "Copy of " + record.name
# Which Generates:
#<Foo:0x007f94353fc200> {
# :id => nil,
# :name => "Copy of Some Foo",
# :project_id => nil
#
# }
clones.push(cloned_record)
end
return clones
end
The problem is, when I duplicate the project and assign the newly generated, renamed, foos, upon saving I get the error:
Foo Exists (0.4ms) SELECT 1 AS one FROM "foos" WHERE ("foos"."name" = 'Copy of Some Foo' AND "foos"."project_id" = 1) LIMIT 1
However, no foo with that name exists:
Foo.where(name: "Copy of Some Foo")
# Foo Load (0.6ms) SELECT "foos".* FROM "foos" WHERE "foos"."name" = $1 ORDER BY "foos"."id" ASC [["name", "Copy of Some Foo"]]
# []
Can anyone tell me what might be going on here? I gather that it has something to do with the validation, but I don't understand why: 1) It thinks this new record exists, and 2) why the new record has a project_id set to the origin project when I've explicitly nullified that field.
The problem is that uniqueness: { scope: :project_id } also applies to nil values. So if you have two records and they both have project_id == nil that would violate the uniqueness constraint.
Here is one way to allow nil values while still keeping the uniqueness constraint:
uniqueness: { scope: :project_id }, unless: Proc.new { |record| record.project_id.blank? }
When ActiveRecord complains that Foo Exists, it then rolls back the commit, so your cloned Foos do not persist to the database.
First, this line:
Foo Exists (0.4ms) SELECT 1 AS one FROM "foos" WHERE ("foos"."name" = 'Copy of Some Foo' AND "foos"."project_id" = 1) LIMIT 1
is not a error at all, it's just a log indicated Rails is running your uniqueness validation. And,
2) why the new record has a project_id set to the origin project when I've explicitly nullified that field.
Are you sure? it's project_id did set to the origin project? It's seems to me, the project_id is just set to the new created project's id.
Given this simple model:
class Article < ActiveRecord::Base
validates :title, presence: true
validates :url, presence: true, uniqueness: true
validates :topic, presence: true
before_create :some_filter
private
def some_filter
self.noframe = false
end
end
And the fact that there are:
no observers
no indexes on the "noframe" column
How is this possible?
attrs = { title: "a", url: "http://www.example.com", topic: "test" }
Article.where(attrs).count
=> 0
Article.where(url:"http://www.example.com").count
=> 0
article = Article.new(attrs)
article.save
(0.2ms) BEGIN
Article Exists (0.6ms) SELECT 1 AS one FROM "articles" WHERE "articles"."url" = 'http://www.example.com' LIMIT 1
(0.3ms) ROLLBACK
article.errors.full_messages
[]
The debugger
When putting the debugger inside of "some_filter" method, this is what's happening.
[12] pry(#<Article>)> self.noframe = nil
=> nil
[13] pry(#<Article>)> self.valid?
Article Exists (0.5ms) SELECT 1 AS one FROM "articles" WHERE "articles"."url" = 'http://www.example.com' LIMIT 1
=> true
[14] pry(#<Article>)> self.noframe = false
=> false
[15] pry(#<Article>)> self.valid?
Article Exists (0.5ms) SELECT 1 AS one FROM "articles" WHERE "articles"."url" = 'http://www.example.com' LIMIT 1
=> true
[16] pry(#<Article>)> self.save
Article Exists (0.5ms) SELECT 1 AS one FROM "articles" WHERE "articles"."url" = 'http://www.example.com' LIMIT 1
=> false
More information
To make it more interesting, when I change "some_filter" to set noframe to either nil or to true, I can create as many records as I want without "Article Exists" error.
It also works when I set noframe attribute directly and not inside of before_create filter.
Why is this workaround working?
I'm able to "fix" it by replacing "before_create" with "after_create" and updating the noframe attribute additionally with update_attributes(noframe:false). But why is this a solution? update_attributes also calls all callbacks and validation so why is this not working in before_create?
before_create has one not that obvious property - if it returns false value it terminates whole saving chain. Same applies to all before-* callbacks. You can find it in callback documentation. Your method returns false, so whole transaction rolls back. Change it to:
def some_filter
self.noframe = false
true
end
And all will work.
I want to use STI in Rails 4. I already had a model Boilerplate and now I want to inherit a BoilerplateOriginal and a BoilerplateCopy model from it. So I added a column type to my table:
class AddTypeToBoilerplates < ActiveRecord::Migration
def up
add_column :boilerplates, :type, :string
Boilerplate.update_all type: 'BoilerplateOriginal'
change_column :boilerplates, :type, :string, null: false
end
def down
remove_column :boilerplates, :type
end
end
Sadly, this column doesn't seem to be filled by Rails automatically:
[1] a4aa2 » x = Boilerplate.new
=> #<Boilerplate:0x00000101609580> {
:id => nil,
:title => nil,
:type => nil
}
[2] a4aa2 » x.valid?
Boilerplate Exists (0.4ms) SELECT 1 AS one FROM "boilerplates" WHERE "boilerplates"."title" IS NULL LIMIT 1
=> false
[3] a4aa2 » x.errors
=> {
:title => [
[0] "must not be empty"
]
}
[4] a4aa2 » x.title = 'test'
=> "test"
[5] a4aa2 » x.valid?
Boilerplate Exists (0.1ms) SELECT 1 AS one FROM "boilerplates" WHERE "boilerplates"."title" = 'test' LIMIT 1
=> true
[6] a4aa2 » x.save
(0.1ms) begin transaction
Boilerplate Exists (0.2ms) SELECT 1 AS one FROM "boilerplates" WHERE "boilerplates"."title" = 'test' LIMIT 1
SQL (1.4ms) INSERT INTO "boilerplates" ("title") VALUES (?) [["title", "test"]]
SQLite3::ConstraintException: NOT NULL constraint failed: boilerplates.type: INSERT INTO "boilerplates" ("title") VALUES (?)
(0.1ms) rollback transaction
from /Users/josh/.rvm/gems/ruby-2.1.0#a4aa2/gems/sqlite3-1.3.10/lib/sqlite3/statement.rb:108:in `step'
Did I miss something important? I thought Rails simply fills the column :type when it's available?
Well. You are doing it wrong of-course. The type column will be set, only when you will be creating the objects by using any of the child class of the parent Boilerplate. But if you use Boilerplate.new, you need to pass the type value manually. On the other hand, when you will do BoilerplateCopy.new or BoilerplateOriginal.new, the type will be set by ActiveRecord by default for you to the class name of the child class.
Read the official documentation of Single table inheritance.
Active Record allows inheritance by storing the name of the class in a column that by default is named type (can be changed by overwriting Base.inheritance_column). This means that an inheritance looking like this:
class Company < ActiveRecord::Base; end
class Firm < Company; end
class Client < Company; end
class PriorityClient < Client; end
When you do Firm.create(name: "37signals"), this record will be saved in the companies table with type = “Firm”. You can then fetch this row again using Company.where(name: '37signals').first and it will return a Firm object.
I'm using the Canard gem (CanCan + RoleModel) for authentication on Ruby on Rails, but I'm having some issues with making an initial user admin. An Account has Users. When an Account is created, there's an Owner User who has billing details. I'd like that Owner to have the :admin role.
class SomewhereElseInSystem
...
account = Account.register(hash) # Company Admin submits form with
# details of account, owning user,
# and a load more objects we don't
# show here.
end
class Account < ActiveRecord::Base
has_many :users, dependent: :destroy, include: [:profile]
has_one :owner, class_name: 'User', conditions: { owner: true }
accepts_nested_attributes_for :owner
def self.register(details)
...
hash = { name: details[:client_name],
domain: details[:subdomain],
plan: details[:plan],
owner_attributes: {
name: details[:owner_name],
password: details[:owner_password],
password_confirmation: details[:owner_password],
email: details[:owner_email] } }
account = Account.create hash
...
end
end
class User < ActiveRecord::Base
belongs_to :account
acts_as_user roles: [:saas_admin, :admin, :manager]
# owner is a boolean DB field
before_validation :owners_are_admins
protected
def owners_are_admins
return unless owner
self.roles << :admin
end
end
However, I'm finding that when I make an account the associated owner is not an admin, and has no roles set. I've tried several variations on the above and am convinced that I'm missing something basic. Help?
Update:
If I go into the console, I can have great fun seeing 4 non-admins instead of 1 admin and 3 non-admins via the scopes, but a different answer via admin? and the actual fields.
[46] subscriptions » account.users.count
(0.5ms) SELECT COUNT(*) FROM "users" WHERE "users"."account_id" = 1
=> 4
[47] subscriptions » account.users.admins.count
(0.4ms) SELECT COUNT(*) FROM "users" WHERE "users"."account_id" = 1 AND ("users"."roles_mask" & 2 > 0)
[48] subscriptions » account.users.non_admins.count
[48] subscriptions » account.users.non_admins.count
(0.6ms) SELECT COUNT(*) FROM "users" WHERE "users"."account_id" = 1 AND ("users"."roles_mask" & 2 = 0 or "users"."roles_mask" is null)
=> 4
[49] subscriptions » account.users.map(&:admin?)
=> [
[0] false,
[1] false,
[2] false,
[3] true
]
[51] subscriptions » account.users.select{ |user| user.roles_mask & 2 > 0 }.count 0;34# Same as query above
=> 1
But better yet, if I do
account.users.admins # 0
owner = account.owner
owner.roles << :admin # Should be a no-op
owner.save!
account.user.admins # 1!
Clearly my callback isn't doing the equivalent of the << in the console after all. Which I don't understand.