Allow Only Specific Words for Attribute - ruby-on-rails

How do you restrict strings allowed in a string field to specific words?
Example: I have a model attribute animal:string that I would like to exclusively accept ["dog", "cat", "bird", "fish"]. Every thing else would make the object invalid.

Add an inclusion validation to your model:
validates :animal, inclusion: { in: %w(dog cat bird fish) }

As I said, I'd go with Rails Enum feature.
class ModelName < ActiveRecord::Base
enum animals: %w(dog cat)
# ...
end
There is one gotcha which you might notice since this is called enum: you will need to update your database column to be an integer value. Rails will do implicit mapping between the integer value and the index of the value in the array automatically
If you go with Enum feature, having the single line of code, Rails will generate several helper methods for you:
# Query method
model_name.dog?
model_name.cat?
#..
# Action method
model_name.cat!
model_name.dog!
#..
# List of statuses and their corresponding values in the database.
model_name.animals
Tried and Tested :
[arup#app]$ rails c
Loading development environment (Rails 4.1.1)
[1] pry(main)> Pet.create!(animals: 'cat')
(0.3ms) BEGIN
SQL (0.9ms) INSERT INTO "pets" ("animals", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["animals", 1], ["created_at", "2015-02-19 18:27:28.074640"], ["updated_at", "2015-02-19 18:27:28.074640"]]
(42.2ms) COMMIT
=> #<Pet id: 5, animals: 1, created_at: "2015-02-19 18:27:28", updated_at: "2015-02-19 18:27:28">
[2] pry(main)> Pet.create!(animals: 'cow')
ArgumentError: 'cow' is not a valid animals
from /home/arup/.rvm/gems/ruby-2.1.2#app/gems/activerecord-4.1.1/lib/active_record/enum.rb:103:in 'block (3 levels) in enum'
[3] pry(main)> Pet.animals
=> {"dog"=>0, "cat"=>1}
[5] pry(main)> Pet.first.dog?
Pet Load (0.8ms) SELECT "pets".* FROM "pets" ORDER BY "pets"."id" ASC LIMIT 1
=> false
[6] pry(main)> Pet.first.cat?
Pet Load (0.7ms) SELECT "pets".* FROM "pets" ORDER BY "pets"."id" ASC LIMIT 1
=> true
[7] pry(main)> Pet.first.cow?
Pet Load (0.7ms) SELECT "pets".* FROM "pets" ORDER BY "pets"."id" ASC LIMIT 1
NoMethodError: undefined method 'cow?' for #<Pet:0xbf8455c>
from /home/arup/.rvm/gems/ruby-2.1.2#app/gems/activemodel-4.1.1/lib/active_model/attribute_methods.rb:435:in `method_missing'
[8] pry(main)>
Pet.create!(animals: 'cow') throws error, which confirmed that, Pet model wouldn't accept anything other than the enum values.

you can use select field in your form, and write this in your model:
module Animal
dog = 1
cat = 2
bird = 3
fish = 4
end
and in tour form:
<%= f.select :animal, { "dog" => 1, "cat" => 2, "bird" => 3, "fish" => 4} %>

Related

Update to array in ActiveRecord column not saving

I have a Postgres database. One of the columns in one of my tables consists of arrays. It's called aliases. I'm trying to merge two instances but when I try to merge the arrays they are not saving.
irb(main):001:0> original
=> #<Thing id: 1, name: "Foo", aliases: ["Foo"]>
irb(main):002:0> duplicate
=> #<Thing id: 2, name: "Bar", aliases: ["Bar"]>
irb(main):003:0> original.aliases | duplicate.aliases
=> ["Foo", "Bar"]
irb(main):004:0> original.save!
(0.3ms) BEGIN
Thing Exists (0.8ms) SELECT 1 AS one FROM "thing" WHERE "things"."name" = $1 AND ("thing"."id" != $2) LIMIT $3 [["name", "Foo"], ["id", 1], ["LIMIT", 1]]
(0.3ms) COMMIT
Thing Store (6.1ms) {"id":1}
=> true
But then when I check original the aliases have not merged.
irb(main):005:0> original
=> #<Thing id: 1, name: "Foo", aliases: ["Foo"]>
I tried several different ways to insert duplicate.aliases into original.aliases but nothing seems to be saving. Any ideas why? Am I missing something?
Edit:
I'm on Rails 5.1.4 and Ruby 2.5.0p0
Are you on Rails 3? I've noticed that Rails3 activerecord array handling does not like inline updates. You need to use a temporary variable, augment that variable and then assign it back.
aliases = original.aliases # make a temp variable
aliases = aliases | duplicate.aliases # modify temp variable
original.aliases = aliases # assign back
original.save! # now you can save
I encountered this myself when I was trying to push onto an array:
original.aliases << "something" # aliases would never get changed

ActiveRecord has_many relationships forget changes to children?

I have an instance of type A that has_many Bs. When the A.foo = value method gets called, I actually want to write a method that delegates to that foo= call the first of the A's Bs.
class A < ActiveRecord::Base
has_many :bs, autosave: true
def foo
bs.first.foo
end
def foo=(val)
bs.first.foo = val
end
end
class B < ActiveRecord::Base
belongs_to A
end
rails generate model A
rails generate model B a:references foo:string
2.3.0 :001 > a = A.create!
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "as" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2016-10-08 18:03:18.255107"], ["updated_at", "2016-10-08 18:03:18.255107"]]
(7.8ms) commit transaction
=> #<A id: 1, created_at: "2016-10-08 18:03:18", updated_at: "2016-10-08 18:03:18">
Create an A and call it a.
2.3.0 :002 > b = B.create!(a: a, foo: "initial")
(0.4ms) begin transaction
SQL (0.4ms) INSERT INTO "bs" ("a_id", "foo", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["a_id", 1], ["foo", "initial"], ["created_at", "2016-10-08 18:03:40.658035"], ["updated_at", "2016-10-08 18:03:40.658035"]]
(8.3ms) commit transaction
=> #<B id: 1, a_id: 1, foo: "initial", created_at: "2016-10-08 18:03:40", updated_at: "2016-10-08 18:03:40">
Create a B and call it b. Make it a child of A. Set it's foo property to "initial".
2.3.0 :003 > a.reload.foo
A Load (0.2ms) SELECT "as".* FROM "as" WHERE "as"."id" = ? LIMIT 1 [["id", 1]]
B Load (0.2ms) SELECT "bs".* FROM "bs" WHERE "bs"."a_id" = ? ORDER BY "bs"."id" ASC LIMIT 1 [["a_id", 1]]
=> "initial"
Check that a sees it's new child's foo: yes. As expected.
2.3.0 :004 > a.foo = "set"
B Load (0.1ms) SELECT "bs".* FROM "bs" WHERE "bs"."a_id" = ? ORDER BY "bs"."id" ASC LIMIT 1 [["a_id", 1]]
=> "set"
2.3.0 :005 > a.foo
B Load (0.4ms) SELECT "bs".* FROM "bs" WHERE "bs"."a_id" = ? ORDER BY "bs"."id" ASC LIMIT 1 [["a_id", 1]]
=> "initial"
Whaaat? I just called a.foo = "set". Now when I call a.foo again to read the value back, I get "initial"? That's not the way it works for has_one relationships. Why is ActiveRecord reloading from the DB every time, instead of caching it's queries?
Ultimately, my intention is to call a.save!, and have it autosave down to the b. But that's not possible if the relationship gets amnesia about every pending change. What's going on here?!
Set up a has_one relationship between A and B and delegate :foo to the has_one association.
class A
has_many :bs
has_one :first_b, -> { first },
class_name: 'B'
delegate :foo, to: :first_b
end
To avoid the query for B you can use .joins, includes or eager_load.

Array of strings not converted to arrays of integers

I'm trying to understand the following behaviour:
irb(main):016:0* pg = ProductGroup.find(1)
irb(main):017:0> pg.good_type_ids
=> [1]
irb(main):018:0> pg.update({"good_type_ids"=>["", "1"]})
irb(main):019:0> pg.changed?
=> false
irb(main):020:0> pg.good_type_ids
=> ["1"]
The code above mimics a form update. The good_type_ids are selected ids's of a multi select. Although the value of the array changes from [1] to ["1"], the changed? method returns false. The update method seems also be smart enough to strip the empty string.
irb(main):021:0> pg = ProductGroup.find(1)
irb(main):022:0> pg.good_type_ids
=> [1]
irb(main):023:0> pg.attributes = {"good_type_ids"=>["", "1"]}
=> {"good_type_ids"=>["", "1"]}
irb(main):024:0> pg.changed?
=> true
irb(main):025:0> pg.good_type_ids
=> ["", "1"]
In this second example I'm trying to apply the changed params from the form to the object. I don't want to save it to the dbs!
Somehow the attributes method behaves different than the update method and good_type_ids will be ["","1"] after the method was invoked.
It seems that the type of the column in the dbs is taken into account by the "update" and "attributes" method, only for arrays (postgress) this doesn't seem to work.
irb(main):030:0* pg.attributes = {"recourse_days"=>5}
=> {"recourse_days"=>5}
irb(main):031:0> pg.changed?
=> true
irb(main):039:0* pg.recourse_days
=> 5
recourse_days are defined as integer, during post the controller receives a string and converts it correctly to an integer.
Schema.rb: t.integer "good_type_ids", default: [], array: true
As temporary work arround I have put this in the update method of the controller
params["good_type_ids"].reject!(&:blank?)
params["good_type_ids"].map!(&:to_i)
UPD
Strings conversion issues and nil elements saving to pgsql seems to be fixed in rails 4.2.0, i've heard whole type casting system is pretty much revised there:
> doc.test_digit = ["1"] #=> ["1"]
> doc.test_digit #=> [1]
> doc.test_string #=> ["2"]
> doc.test_string = [2] #=> [2]
> doc.test_string #=> ["2"]
> doc.attributes = {'test_digit'=>["", "2"]}
=> {"test_digit"=>["", "2"]}
> doc.test_digit #=> [nil, 2]
> doc.save
(0.9ms) BEGIN
SQL (1.8ms) UPDATE "documents" SET "test_digit" = $1, "updated_at" = $2 WHERE "documents"."id" = $3 [["test_digit", "{NULL,2}"], ["updated_at", "2015-02-26 00:32:25.326735"], ["id", 1]]
(3.7ms) COMMIT
=> true
> doc.reload
Document Load (1.7ms) SELECT "documents".* FROM "documents" WHERE "documents"."id" = $1 LIMIT 1 [["id", 1]]
=> #<Document id: 1, name: "test1", test_digit: [nil, 2], test_string: ["2"], created_at: "2015-02-25 23:51:32", updated_at: "2015-02-26 00:32:25">
> doc.test_digit #=> [nil, 2]
I used following migrations:
t.integer :test_digit, array: true, default: []
t.string :test_string, array: true, default: []
Update method updates the db, thus your model attributes are no longer dirty.
Good guide on this
The update method seems also be smart enough to strip the empty string.
This is not normal behavior, according to the source of pgsql adapter/array_parser, empty array elements should be saved and loaded as any other values. (this functionality was added on Aug 20, 2012 to the adapter just in case you are using an older version)

ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord

I'm trying to create a Rails plugin. For the most part, what I've written works. However, there's a problem with associations. When I try to call an association, I get this error:
ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
At the moment, the plugin looks like this:
module ControlledVersioning
module ActsAsVersionable
extend ActiveSupport::Concern
included do
has_many :versions, as: :versionable
after_create :create_initial_version
end
module ClassMethods
def acts_as_versionable(options = {})
cattr_accessor :versionable_attributes
self.versionable_attributes = options[:versionable_attributes]
end
end
private
def create_initial_version
version = versions.create
end
end
end
ActiveRecord::Base.send :include, ControlledVersioning::ActsAsVersionable
Again, the error message is triggered whenever I try to call the association. I used debugger in the after_create callback and tried running:
> versions.create
*** ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
> versions
*** ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
> Version.new
#<Version id: nil, versionable_id: nil, versionable_type: nil>
There are a few things you need to change in your code in order for it to work.
First, versions is a reserved keyboard from rails -- you can't have a relationship with that name - (I used the name versionings in order to make it work)
Also, you want to make sure to just add has_many versionings for the models that want to acts_as_versionable - meaning, move has_many :versionings, as: :versionable, class_name: 'Version' and after_create :create_initial_version calls to inside the acts_as_versionable method.
Here's how all together will look like:
module ControlledVersioning
module ActsAsVersionable
extend ActiveSupport::Concern
module ClassMethods
def acts_as_versionable(options = {})
has_many :versionings, as: :versionable, class_name: 'Version'
after_create :create_initial_version
cattr_accessor :versionable_attributes
self.versionable_attributes = options[:versionable_attributes]
end
end
private
def create_initial_version
version = versionings.create
end
end
end
ActiveRecord::Base.send :include, ControlledVersioning::ActsAsVersionable
Doing those changes made the plugin work for me:
irb(main):003:0> Post.create!
(0.1ms) begin transaction
Post Create (0.7ms) INSERT INTO "posts" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2019-07-16 08:55:13.768196"], ["updated_at", "2019-07-16 08:55:13.768196"]]
Version Create (0.2ms) INSERT INTO "versions" ("versionable_type", "versionable_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["versionable_type", "Post"], ["versionable_id", 3], ["created_at", "2019-07-16 08:55:13.772246"], ["updated_at", "2019-07-16 08:55:13.772246"]]
(2.0ms) commit transaction
=> #<Post id: 3, created_at: "2019-07-16 08:55:13", updated_at: "2019-07-16 08:55:13", name: nil>
irb(main):004:0> Post.last.versionings
Post Load (0.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT ? [["LIMIT", 1]]
Version Load (0.2ms) SELECT "versions".* FROM "versions" WHERE "versions"."versionable_id" = ? AND "versions"."versionable_type" = ? LIMIT ? [["versionable_id", 3], ["versionable_type", "Post"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Version id: 2, versionable_type: "Post", versionable_id: 3, created_at: "2019-07-16 08:55:13", updated_at: "2019-07-16 08:55:13">]>
irb(main):005:0>
I would try extending active record in an initializer instead of including it.
initializers/acts_as_versionable.rb
ActiveRecord::Base.extend(ControlledVersioning::ActsAsVersionable)
Also in development; or any environment that reloads the files you'll likely see an error like has been removed from the module tree but is still active. Make sure you're plugin file is in config.eager_load_paths and not actually in a concern path.

How to validate that payment can never cause invoice amount payable to be less than zero?

I have this class:
class Payment < ActiveRecord::Base
attr_accessible :amount, :invoice_id
belongs_to :invoice
validates :amount, :numericality => { :greater_than => 0, :less_than_or_equal_to => :maximum_amount }
after_save :update_amount_payable
after_destroy :update_amount_payable
private
def maximum_amount
invoice.amount_payable
end
def update_amount_payable
invoice.update_amount_payable
end
end
class Invoice < ActiveRecord::Base
has_many :payments
after_save :update_amount_payable
def update_amount_payable
update_column(:amount_payable_in_cents, new_amount_payable)
end
private
def new_amount_payable
(total - payments.map(&:amount).sum) * 100
end
end
The code above works. But how can I validate that no payment amount can ever cause invoice.amount_payable to be less than 0?
Especially when multiple payments for the same invoice are possible, this turns out to be tricky.
I've been trying to get my head around this for hours, but to no avail. Maybe an after callback to rollback the database can be used here?
Thanks for any help.
One cross-database solution that will work is to use optimistic locking. Essentially, it requires a special lock_version column, that is checked whenever an update is made. If the lock_version at the time an UPDATE is called is different than what the model is expecting, it throws an error noting that something outside of this model caused the record to change (thus invalidating the update). ActiveRecord supports this out of the box, and it will likely suffice for your needs if you don't mind blocking concurrent transactions altogether.
A case that it won't work is where you want to allow concurrent updates. In this case, you'll need to manually check the result during your update:
def update_amount_payable
new_value = new_amount_payable
raise "Payment amounts can't be greater than total invoice amount" if new_value < 0
count = Invoice.where(id: id, amount_payable_in_cents: amount_payable_in_cents).
update_all(amount_payable_in_cents: new_value)
raise ActiveRecord::StaleObjectError.new(self, 'update amount_payable_in_cents') if count != 1
end
private
def new_amount_payable
(total - payments.sum(:amount)) * 100 # get the amount sum from the database
end
I would change the field names. But given the current database schema try the following code:
app/models/invoice.rb
class Invoice < ActiveRecord::Base
has_many :payments
def still_open_amount
self.amount_payable_in_cents - self.payments.sum('amount_in_cents')
end
end
app/models/payment.rb
class Payment < ActiveRecord::Base
belongs_to :invoice
validates :amount_in_cents, :numericality => { :greater_than => 0 }
before_validation :check_all_payments
private
def check_all_payments
if self.new_record?
if (self.invoice.payments.sum('amount_in_cents') + self.amount_in_cents) > self.invoice.amount_payable_in_cents
errors.add(:amount, 'the invoice would be overpaid')
end
else
if (self.invoice.payments.sum('amount_in_cents') - self.amount_in_cents_was + self.amount_in_cents) > self.invoice.amount_payable_in_cents
errors.add(:amount, 'the invoice would be overpaid')
end
end
end
end
This will through a validation error if you try to create a overpaying payment:
~/Desktop/testapp ᐅ rails c
Loading development environment (Rails 4.0.0.beta1)
1.9.3-p286 :001 > i = Invoice.create(amount_payable_in_cents: 100)
(0.1ms) begin transaction
SQL (6.8ms) INSERT INTO "invoices" ("amount_payable_in_cents", "created_at", "updated_at") VALUES (?, ?, ?) [["amount_payable_in_cents", 100], ["created_at", Mon, 13 May 2013 19:23:24 UTC +00:00], ["updated_at", Mon, 13 May 2013 19:23:24 UTC +00:00]]
(0.8ms) commit transaction
=> #<Invoice id: 1, amount_payable_in_cents: 100, created_at: "2013-05-13 19:23:24", updated_at: "2013-05-13 19:23:24">
1.9.3-p286 :003 > p1 = i.payments.create(amount_in_cents: 90)
(0.1ms) begin transaction
Invoice Load (0.2ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."id" = ? ORDER BY "invoices"."id" ASC LIMIT 1 [["id", 1]]
(0.2ms) SELECT SUM("payments"."amount_in_cents") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = ? [["invoice_id", 1]]
SQL (0.4ms) INSERT INTO "payments" ("amount_in_cents", "created_at", "invoice_id", "updated_at") VALUES (?, ?, ?, ?) [["amount_in_cents", 90], ["created_at", Mon, 13 May 2013 19:24:10 UTC +00:00], ["invoice_id", 1], ["updated_at", Mon, 13 May 2013 19:24:10 UTC +00:00]]
(1.0ms) commit transaction
=> #<Payment id: 1, invoice_id: 1, amount_in_cents: 90, created_at: "2013-05-13 19:24:10", updated_at: "2013-05-13 19:24:10">
1.9.3-p286 :004 > p2 = i.payments.create(amount_in_cents: 20)
(0.1ms) begin transaction
Invoice Load (0.2ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."id" = ? ORDER BY "invoices"."id" ASC LIMIT 1 [["id", 1]]
(0.1ms) SELECT SUM("payments"."amount_in_cents") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = ? [["invoice_id", 1]]
(0.1ms) commit transaction
=> #<Payment id: nil, invoice_id: 1, amount_in_cents: 20, created_at: nil, updated_at: nil>
1.9.3-p286 :005 > p2.errors
=> #<ActiveModel::Errors:0x007fd57b8e36d8 #base=#<Payment id: nil, invoice_id: 1, amount_in_cents: 20, created_at: nil, updated_at: nil>, #messages={:amount=>["the invoice would be overpaid"]}>
1.9.3-p286 :006 >

Resources