Lock value of database column based on boolean value in Rails - ruby-on-rails

I want to let a user edit the field report.plan only if report.published = false. If report.published = true and they try to save a change, I want to throw an error.
I've written the following code to do this:
class Report < ActiveRecord::Base
validate :cannot_update_plan_after_published, on: :publish_plan!
def publish_plan!(plan)
self.plan = plan
self.published = true
self.save
end
private
def cannot_update_plan_after_published
if self.published?
errors.add(:plan, "You cannot change the plan once it has been published.")
end
end
end
However, this is not working. When I call publish_plan! on an already published report, it makes the save. For example:
> f = Report.last
=> #<Report id: 12, plan: "a", published: false>
> f.publish_plan!("b")
(0.1ms) begin transaction
(0.4ms) UPDATE "reports" SET "plan" = 'b', "updated_at" = '2014-09-18 18:43:47.459983' WHERE "reports"."id" = 12
(9.2ms) commit transaction
=> true
> f = Report.last
Report Load (0.2ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" DESC LIMIT 1
=> #<Report id: 12, plan: "b", published: true>
> f.publish_plan!("c")
(0.1ms) begin transaction
(0.4ms) UPDATE "reports" SET "plan" = 'c', "updated_at" = '2014-09-18 18:43:53.996191' WHERE "reports"."id" = 12
(8.7ms) commit transaction
=> true
> Report.last
Report Load (0.2ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" DESC LIMIT 1
=> #<Report id: 12, plan: "c", published: true>
How do I get this field to become uneditable once report.published = true?

Try removing the on: :public_plan!. That way, the validation should be run every time the model is saved.
validate :cannot_update_plan_after_published
See here for more details: Adding a validation error with a before_save callback or custom validator?
Also, for the validation method itself, change it to the following:
def cannot_update_plan_after_published
if self.published? && self.published_changed? == false
errors.add(:plan, "You cannot change the plan once it has been published.")
end
end
This allows you to set it the first time publishing the plan.

Related

Rails ActiveRecord inserting/updating range type (Postgresql)

I have a height_ranges table on my database, which stores numeric ranges.
migration file
class CreateHeightRanges < ActiveRecord::Migration
def change
create_table :height_ranges do |t|
t.references :classification, index: true, foreign_key: true
t.references :tree, index: true, foreign_key: true
t.numrange :h_range
t.integer :diameter
t.timestamps null: false
end
end
end
I can update table via raw sql query,
UPDATE height_ranges SET h_range = numrange(5.6, 9.8, '[]') WHERE id = 1;
But when it comes to ActiveRecord, it surprisely omits lower bound.
[10] pry(main)> hr = HeightRange.first;
HeightRange Load (0.5ms) SELECT "height_ranges".* FROM "height_ranges" ORDER BY "height_ranges"."id" ASC LIMIT 1
[11] pry(main)> hr.h_range = "numrange(6.2, 9.8, '[]')"
=> "numrange(6.2, 9.8, '[]')"
[12] pry(main)> hr.save
(0.2ms) BEGIN
SQL (1.9ms) UPDATE "height_ranges" SET "h_range" = $1, "updated_at" = $2 WHERE "height_ranges"."id" = $3 [["h_range", "[0.0,9.8)"], ["updated_at", "2017-10-09 10:18:03.178971"], ["id", 1]]
(13.6ms) COMMIT
=> true
[13] pry(main)>
another query,
[13] pry(main)> d = {"classification_id"=>"2", "tree_id"=>"4", "diameter"=>"16", "h_range"=>"numrange(5.3, 7.5, '[]')"}
=> {"classification_id"=>"2", "tree_id"=>"4", "diameter"=>"16", "h_range"=>"numrange(5.3, 7.5, '[]')"}
[14] pry(main)> hr.update_attributes(d)
(0.2ms) BEGIN
SQL (0.3ms) UPDATE "height_ranges" SET "h_range" = $1, "updated_at" = $2 WHERE "height_ranges"."id" = $3 [["h_range", "[0.0,7.5)"], ["updated_at", "2017-10-09 10:20:30.638967"], ["id", 1]]
(21.4ms) COMMIT
=> true
A semi AR query that works is,
HeightRange.where(id: hr.id).update_all("h_range = numrange(5.5, 9.4, '[]')")
As a last resort I can use the first (raw) and last (passing raw query to ActiveRecord method) but what is wrong with attribute setting approach?
My current implementation has those helper methods in the model:
def unusual_update dict
HeightRange.where(id: id).update_all(attrs_to_sql_set_stmt(dict))
end
def attrs_to_sql_set_stmt dct
dct.map{|k,v| "#{k} = #{v}" }.join(", ")
end
In the controller, I trigger the update as follows:
def update
r_min = params[:range_min].to_f
r_max = params[:range_max].to_f
h_range = "numrange(#{r_min}, #{r_max}, '[]')"
height_range_params = pre_params.merge({h_range: h_range})
if #height_range.unusual_update(height_range_params)
redirect_to admin_height_ranges_path, notice: 'Height range was successfully updated.'
else
render :edit
end
end
PostgreSQL ranges should be fully supported by Rails.
I manage to reproduce your situation as follows:
Migration:
#db/migrate/20171009152602_create_test_table.rb
class CreateTestTable < ActiveRecord::Migration[5.0]
def up
create_table :test_tables do |t|
t.numrange :h_range
end
end
def down
drop_table :test_tables
end
end
Model:
# app/models/test_table.rb
class TestTable < ActiveRecord::Base
end
The numrange has some challenges in terms of type support:
$ rails c
Loading development environment (Rails 5.0.5)
2.4.0 :001 > c = TestTable.new
=> #<TestTable id: nil, h_range: nil>
# Inserting Integer range
2.4.0 :002 > c.update(h_range:(3..5))
(0.3ms) BEGIN
SQL (0.5ms) INSERT INTO "test_tables" ("h_range") VALUES ($1) RETURNING "id" [["h_range", "[3,5]"]]
(16.7ms) COMMIT
=> true
2.4.0 :003 > c.h_range
=> 0.3e1..0.5e1
2.4.0 :004 > c.h_range.to_a
TypeError: can't iterate from BigDecimal
2.4.0 :005 > c.h_range.step
=> #<Enumerator: 0.3e1..0.5e1:step(1)>
2.4.0 :006 > c.h_range.step.to_a
=> [0.3e1, 0.4e1, 0.5e1]
# Inserting Float range
2.4.0 :007 > c.update(h_range:(3.4..5.8))
(0.3ms) BEGIN
SQL (0.8ms) UPDATE "test_tables" SET "h_range" = $1 WHERE "test_tables"."id" = $2 [["h_range", "[3.4,5.8]"], ["id", 1]]
(20.7ms) COMMIT
=> true
2.4.0 :008 > c.h_range.step.to_a
=> [0.34e1, 0.44e1, 0.54e1]
2.4.0 :009 > c.h_range.step(0.1).to_a
=> [3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8]
I would suggest you to prefer ActiveRecord instance update method than raw sql in your model.
You can eventually add an instance method where you perform .step on your h_range field.
Update
After the code you shared via pastebin, in your controller you should be able to update the values as follows:
def update
hr_params = params.require(:height_range).permit(:classification_id, :tree_id, :diameter, :range_min, :range_max)
r_min = hr_params[:range_min].to_f
r_max = hr_params[:range_max].to_f
#hr = HeighRange.find(params[:id])
hr_options = {
classification_id: hr_params[:classification_id],
tree_id: hr_params[:tree_id],
diameter: hr_params[:diameter],
h_range: (r_min..r_max)
}
#hr.update(hr_options)
if #hr.valid?
#hr.save!
redirect_to admin_height_ranges_path, notice: 'Height range was successfully updated.
else
render action: :edit, params: { id: #hr.id }
end
end

Rails - Optimistic locking always fires StaleObjectError exception

I'm learning rails, And read about optimistic lock. I've added lock_version column of type integer into my articles table.
But now whenever I try to update a record for the first time, I get StaleObjectError exception.
Here's my migration:
class AddLockVersionToArticle < ActiveRecord::Migration
def change
add_column :articles, :lock_version, :integer
end
end
When I try updating an article through rails console:
article = Article.first
=> #<Article id: 1, title: "Ccccc", text: "dfdsfsdfsdf", created_at: "2015-02-20 21:58:45", updated_at: "2015-02-25 20:03:12", lock_version: 0>
And I do:
article.title = "new title"
article.save
I get this:
(0.3ms) begin transaction
(0.3ms) UPDATE "articles" SET "title" = 'dwdwd', "updated_at" = '2015-02-25 20:40:36.537876', "lock_version" = 1 WHERE ("articles"."id" = 1 AND "articles"."lock_version" = 0)
(0.1ms) rollback transaction
ActiveRecord::StaleObjectError: Attempted to update a stale object: Article
You have to initialize all the articles lock_version to 0.
Look at the query:
UPDATE "articles" SET "title" = 'dwdwd', "updated_at" = '2015-02-25 20:40:36.537876', "lock_version" = 1 WHERE ("articles"."id" = 1 AND "articles"."lock_version" = 0)
(0.1ms)
If the query returns 0 records updated, then the framework suppose that you have updated the version or deleted the object in another thread.

How to compare Rails enum types

I have the following enum model in my Rails (4) application:
class Dual < ActiveRecord::Base
enum dual: [:dual, :not_dual]
validates :dual, uniqueness: true
validates :dual, presence: true
end
And I have another model which has many Duals:
class SillColour < ActiveRecord::Base
has_many :sill_colour_duals, dependent: :destroy
has_many :duals, through: :sill_colour_duals
end
I want to be able to test if an instance of SillColour has a Dual enum. This is all I could get to work:
dual = Dual.find(1)
not_dual = Dual.find(2)
sill_colour.duals.include?(dual)
sill_colour.duals.include?(not_dual)
Obviously this is extremely unreliable as the ID of the Duals could be anything in production (for testing IDs are fixed). I tried this:
dual = Dual.where(dual: 0)
not_dual = Dual.where(dual: 1)
and even given the database duals table looks like this:
id | dual
----+------
1 | 0
2 | 1
My tests fail and it seems to be because dual and non_dual are no longer comparing correctly. I've examined them using pry and they appear to be the same as before, but clearly they're not.
Surely there must be a better way? I envisaged being able to do this:
sill_colour.duals.include?(Dual.dual)
sill_colour.duals.include?(Dual.not_dual)
but this doesn't work either.
Any suggestions?
I'll try to answer with a code from my app
class User < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
enum status: [:dual, :not_dual]
# see scopes
scope :dual, -> {where(status: Contact.statuses['dual']) }
scope :not_dual, -> {where(status: Contact.statuses['not_dual']) }
end
Now console, check if a User instance has a contact with dual status:
2.1.5 :001 > u = User.last
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT 1
=> #<User id: 1, email: "...", ...>
2.1.5 :003 > u.contacts.dual
Contact Load (0.3ms) SELECT "contacts".* FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 0 [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation []>
2.1.5 :004 > u.contacts.not_dual
Contact Load (0.3ms) SELECT "contacts".* FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 1 [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation []>
As you can see both return empty array, so calling any? on empty array will return false both. Because I don't have any contact with status dual or not_dual, let's create one.
Find a contact:
2.1.5 :005 > c = Contact.last
Contact Load (0.5ms) SELECT "contacts".* FROM "contacts" ORDER BY "contacts"."id" DESC LIMIT 1
=> #<Contact id: 3, user_id: 1, ..., ...., status: nil>
Set it as dual:
2.1.5 :006 > c.dual!
(0.1ms) BEGIN
SQL (15.1ms) UPDATE "contacts" SET "status" = $1, "updated_at" = $2 WHERE "contacts"."id" = 3 [["status", 0], ["updated_at", "2014-12-29 13:16:45.576778"]]
(25.5ms) COMMIT
=> true
Now check if user has dual or not_dual contacts:
2.1.5 :009 > u.contacts.dual.any?
(0.2ms) SELECT COUNT(*) FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 0 [["user_id", 1]]
=> true
2.1.5 :010 > u.contacts.not_dual.any?
(0.3ms) SELECT COUNT(*) FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 1 [["user_id", 1]]
=> false
2.1.5 :011 >
Checking if user instance has a dual enum returns true. In your case instead of user it will be sill_colour.
If you don't like scopes you can use where:
u.contacts.where(status: Contact.statuses['dual']).any?
=> true
http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html
class Dual < ActiveRecord::Base
enum status: [:dual, :not_dual]
end
Having the class as the one above you can check the status like this:
dual = Dual.find(params[:id])
dual.dual? # will return true or false depends on the status you set.
dual.not_dual? # same, true or false
dual.status = "dual" # if status was set to 0 or not_dual if status was set to 1

How to check for database changes of in-memory records?

I want to check if an ActiveRecord instance was changed database-wise. Something like:
p1 = Product.first
p1.name #=> "some name"
p2 = Product.first
p2.name = "other name"
p2.save #=> true
p1.database_changed? #=> true
I'm currently comparing the record's attributes to the persisted attributes:
class Product < ActiveRecord::Base
def database_changed?
Product.find(id).attributes != attributes
end
end
This seems to work, but I'm wondering if there is a built-in way to find database changes?
After Зелёный's comment I've reviewed ActiveModel::Dirty and realized that it almost does what I want. There's already an in-memory state (the record's attributes) and a database state (handled by ActiveModel::Dirty). I just need a method to update the database state, leaving the in-memory state unchanged:
def refresh
#changed_attributes = {}
fresh_object = self.class.unscoped { self.class.find(id) }
fresh_object.attributes.each do |attr, orig_value|
#changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, #attributes[attr])
end
self
end
#changed_attributes is ActiveModel::Dirty's hash to store changed values. We obviously have to reset it.
fresh_object is the same record, freshly fetched from the database (this line comes from reload, thanks emaillenin).
Within the loop, each (fresh) attribute is compared to the corresponding (in-memory) attribute. If they differ, it is added to the #changed_attributes hash. This line comes from ActiveRecord's dup method. (it's actually from a private method called by dup, and _field_changed is private, too. It might be better to use ActiveRecord's public API, but I was lazy)
Finally, refresh returns self for convenience, just like reload does.
Here's an example usage:
p1 = Product.first
p1.name #=> "some name"
p1.changed? #=> false
p2 = Product.first
p2.name = "other name"
p2.save #=> true
p1.refresh
p1.name #=> "some name"
p1.changed? #=> true
p1.name_changed? #=> true
p1.name_was #=> "other name"
p1.name = "other name"
p1.name_changed? #=> false
p1.changed? #=> true
p1.changes #=> {"updated_at"=> [Tue, 29 Jul 2014 21:58:57 CEST +02:00, Tue, 29 Jul 2014 15:49:54 CEST +02:00]}
def database_changed?
self.class.where(self.class.arel_table[:updated_at].gt(updated_at)).exists? self
end
The Rails way to to do this is to use reload method on the ActiveRecord object.
def database_changed?
attributes != reload.attributes
end
Terminal 1
2.1.2 :001 > c = Car.find(1)
Car Load (0.4ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> #<Car id: 1, name: "Audi", model: "R8", created_at: "2014-07-29 11:14:43", updated_at: "2014-07-29 11:14:43">
2.1.2 :002 > c.database_changed?
Car Load (0.1ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> false
2.1.2 :003 > c.database_changed?
Car Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> false
2.1.2 :004 > c.database_changed?
Car Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> true
Terminal 2
2.1.2 :001 > c = Car.find(1)
Car Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> #<Car id: 1, name: "Audi", model: "R8", created_at: "2014-07-29 11:14:43", updated_at: "2014-07-29 11:14:43">
2.1.2 :002 > c.model = 'A5'
=> "A5"
2.1.2 :003 > c.save!
(0.2ms) begin transaction
SQL (0.3ms) UPDATE "cars" SET "model" = ?, "updated_at" = ? WHERE "cars"."id" = 1 [["model", "A5"], ["updated_at", "2014-07-29 11:15:32.845875"]]
(1.2ms) commit transaction
=> true
2.1.2 :004 >

Rails update_Attributes(params[:user])

I have a User show function that makes an api call and updates the user attributes, but I'm having trouble saving the changes.
def show
#user = User.find(params[:id])
... change attributes of #user ...
if #user.update_attributes(params[:user])
p "Saved"
else
p "Failed"
end
end
I'm updated two attributes (:today_steps and :year). Both are accessible in the user.rb. The challenge is that :year is a Serialized Array, but I'm not sure how that changes anything
this setup has returned a 'Failed' every time. i have also tried 'params[:id]' but I receive the error
undefined method `stringify_keys' for "1":String
Any ideas on how to solve this? thanks
more info
def show
#user = User.find(params[:id])
if (#user.device != nil)
#device = #user.device
client = Fitgem::Client.new(
#api client - haven't figured out my security yet, so I won't post it
)
now = Time.now()
last = #device.lastUpdated
while now.to_date > last.to_date
day = client.activities_on_date last.strftime("%Y-%m-%d")
break if day['errors'] != nil
day = day['summary']['steps']
last = last + 1.days
#user.year = #user.year.unshift(day)
end
day = (client.activities_on_date 'today')
if day['errors'] == nil
#user.today_steps = day['summary']['steps']
if now.day != last.day
#user.year.unshift(#user.today_steps)
else
#user.year[0] = #user.today_steps
end
end
#device.lastUpdated = now
p #user.year
#user.device.save
if #user.update_attributes(params[:user])
p "Updated"
else
p "Failed to Update"
end
end
#competitions = #user.competitions.paginate(page: params[:page])
end
_
User.rb
attr_accessible :email, :name, :password, :password_confirmation, :year, :today_steps
serialize :year, Array
Even More
console, using params[:user]
Started GET "/users/2" for 127.0.0.1 at 2014-02-25 17:15:09 -0800
Processing by UsersController#show as HTML
Parameters: {"id"=>"2"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", "2"]]
Device Load (0.1ms) SELECT "devices".* FROM "devices" WHERE "devices"."user_id" = 2 LIMIT 1
(0.1ms) begin transaction
(0.3ms) UPDATE "devices" SET "lastUpdated" = '2014-02-26 01:15:09.634622', "updated_at" = '2014-02-26 01:15:10.392810' WHERE "devices"."id" = 3
(2.3ms) commit transaction
(0.1ms) begin transaction
User Exists (0.1ms) SELECT 1 FROM "users" WHERE (LOWER("users"."email") = LOWER('fire#fox.com') AND "users"."id" != 2) LIMIT 1
(0.0ms) rollback transaction
Rendered users/show.html.erb within layouts/application (0.4ms)
Rendered layouts/_shim.html.erb (0.0ms)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = '1f47eae1cb002cb1c4e83ce0363fbfb2d33ad8f5' LIMIT 1
Rendered layouts/_header.html.erb (2.2ms)
Rendered layouts/_footer.html.erb (0.1ms)
console #user after find
#<User id: 1, name: "Marcus", email: "mar#comcast.net", points: nil, created_at: "2014-02-25 21:59:12", updated_at: "2014-02-25 21:59:12", password_digest: "$2aGTx..FbLP...", admin: false, remember_token: "e4bdd9c", year: [], lastUpdate: nil, lastUpdated: nil, today_steps: nil>
Your code is as
#user = User.find(params[:id])
... change attributes of #user ...
if #user.update_attributes(params[:user])
#the log
Started GET "/users/2" for 127.0.0.1 at 2014-02-25 17:15:09 -0800
Processing by UsersController#show as HTML
Parameters: {"id"=>"2"}
the log clearly states that the show method receives only 'id' whereas in your controller your expect a hash 'user' which can be supplied to update_attributes.
'show' expects to get an attributes hash to stringify it's keys for the column names.

Resources