Rails Postgresql JSONB column check if key value was changed? - ruby-on-rails

Given I have data_config as jsonb column
I want to check the value of specific key if was changed before saving.
Test Data:
data_config: {
"status"=>true,
"text"=>"sample"
}
Rails Model:
attr_json_config(default_container_attribute: :data_config)
attr_json :status, :boolean, default: true
before_save :check_config
def check_config
if self.status_changed? # => want to have something like this to check if value changed?
# ... do something
end
end
This works in my case, but I believe there's a better way.
status_val = self.attr_json_changes.changes_to_save.try(:[], "status")
# above returns [true, false] (old & new value)
if status_val.try(:first) != status_val.try(:last)
...do something
end

Why don't you compare the params with the current value?
Because you give very little information, I'll assume you want to change other key in jsonb if the status changed. In this case, you should make your record immutable before the updated value is ready to commit.
So in your controller, it should be like this:
class MyController < ApplicationController
def update
if params[:data_config][:status] != #model.['data_config']['status'] # check if value change
# ... do something
new_params = do_something
end
if #model.update(new_params)
# render success
else
# render failed
end
end
end
I hope this approach can help you.

Related

Rails 5 - iterate until field matches regex

In my app that I am building to learn Rails and Ruby, I have below iteration/loop which is not functioning as it should.
What am I trying to achieve?
I am trying to find the business partner (within only the active once (uses a scope)) where the value of the field business_partner.bank_account is contained in the field self_extracted_data and then set the business partner found as self.sender (self here is a Document).
So once a match is found, I want to end the loop. A case exists where no match is found and sender = nil so a user needs to set it manually.
What happens now, is that on which ever record of the object I save (it is called as a callback before_save), it uses the last identified business partner as sender and the method does not execute again.
Current code:
def set_sender
BusinessPartner.active.where.not(id: self.receiver_id).each do |business_partner|
bp_bank_account = business_partner.bank_account.gsub(/\s+/, '')
rgx = /(?<!\w)(#{Regexp.escape(bp_bank_account)})?(?!\‌​w)/
if self.extracted_data.gsub(/\s+/, '') =~ rgx
self.sender = business_partner
else
self.sender = nil
end
end
end
Thanks for helping me understand how to do this kind of case.
p.s. have the pickaxe book here yet this is so much that some help / guidance would be great. The regex works.
Using feedback from #moveson, this code works:
def match_with_extracted_data?(rgx_to_match)
extracted_data.gsub(/\s+/, '') =~ rgx_to_match
end
def set_sender
self.sender_id = matching_business_partner.try(:id) #unless self.sender.id.present? # Returns nil if no matching_business_partner exists
end
def matching_business_partner
BusinessPartner.active.excluding_receiver(receiver_id).find { |business_partner| sender_matches?(business_partner) }
end
def sender_matches?(business_partner)
rgx_registrations = /(#{Regexp.escape(business_partner.bank_account.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.registration.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.vat_id.gsub(/\s+/, ''))})/
match_with_extracted_data?(rgx_registrations)
end
In Ruby you generally want to avoid loops and #each and long, procedural methods in favor of Enumerable iterators like #map, #find, and #select, and short, descriptive methods that each do a single job. Without knowing more about your project I can't be sure exactly what will work, but I think you want something like this:
# /models/document.rb
class Document < ActiveRecord::Base
def set_sender
self.sender = matching_business_partner.try(:id) || BusinessPartner.active.default.id
end
def matching_business_partners
other_business_partners.select { |business_partner| account_matches?(business_partner) }
end
def matching_business_partner
matching_business_partners.first
end
def other_business_partners
BusinessPartner.excluding_receiver_id(receiver_id)
end
def account_matches?(business_partner)
rgx = /(?<!\w)(#{Regexp.escape(business_partner.stripped_bank_account)})?(?!\‌​w)/
data_matches_bank_account?(rgx)
end
def data_matches_bank_account?(rgx)
extracted_data.gsub(/\s+/, '') =~ rgx
end
end
# /models/business_partner.rb
class BusinessPartner < ActiveRecord::Base
scope :excluding_receiver_id, -> (receiver_id) { where.not(id: receiver_id) }
def stripped_bank_account
bank_account.gsub(/\s+/, '')
end
end
Note that I am assigning an integer id, rather than an ActiveRecord object, to self.sender. I think that's what you want.
I didn't try to mess with the database relations here, but it does seem like Document could include a belongs_to :business_partner, which would give you the benefit of Rails methods to help you find one from the other.
EDIT: Added Document#matching_business_partners method and changed Document#set_sender method to return nil if no matching_business_partner exists.
EDIT: Added BusinessPartner.active.default.id as the return value if no matching_business_partner exists.

How to format values before saving to database in rails 3

I have a User model with Profit field. Profit field is a DECIMAL (11,0) type. I have a masked input on the form which allows user to input something like $1,000. I want to format that value and remove everything except numbers from it so i will have 1000 saved. Here is what i have so far:
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
end
But it keeps saving 0 in database. Looks like it is converting it to decimal before my formatting function.
Try this:
def profit=(new_profit)
self[:profit] = new_profit.gsub(/[^0-9]/, '')
end
First of all, this:
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
is pretty much the same as this:
def format_values
return if(self.profit.nil?)
p = self.profit
s = p.to_s
s.delete!('^0-9')
end
So there's no reason to expect your format_values method to have any effect whatsoever on self.profit.
You could of course change format_values to assign the processed string to self.profit but that won't help because your cleansing logic is in the wrong place and it will be executed after '$1,000' has been turned into a zero.
When you assign a value to a property, ActiveRecord will apply some type conversions along the way. What happens when you try to convert '$1,000' to a number? You get zero of course. You can watch this happening if you play around in the console:
> a = M.find(id)
> puts a.some_number
11
> a.some_number = 'pancakes'
=> "pancakes"
> puts a.some_number
0
> a.some_number = '$1,000'
=> "1,000"
> puts a.some_number
0
> a.some_number = '1000'
=> "1000"
> puts a.some_number
1000
So, your data cleanup has to take place before the data goes into the model instance because as soon as AR gets its hands on the value, your '$1,000' will become 0 and all is lost. I'd put the logic in the controller, the controller's job is to mediate between the outside world and the models and data formatting and mangling certainly counts as mediation. So you could have something like this in your controller:
def some_controller
fix_numbers_in(:profit)
# assign from params as usual...
end
private
def fix_numbers_in(*which)
which.select { |p| params.has_key?(p) }.each do |p|
params[p] = params[p].gsub(/\D/, '') # Or whatever works for you
end
end
Then everything would be clean before ActiveRecord gets its grubby little hands on your data and makes a mess of things.
You could do similar things by overriding the profit= method in your model but that's really not the model's job.
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit = profit.to_s.gsub(/\D/,'') if profit
end
end
def format_values
self.profit.to_d!
end
I recommend you to write custom setter for this particular instance variable #profit:
class User
attr_accessor :profit
def profit= value
#profit = value.gsub(/\D/,'')
end
end
u = User.new
u.profit = "$1,000"
p u.profit # => "1000"
I would suggest using the rails helper of number with precision. Below is some code.
Generic Example:
number_with_precision(111.2345, :precision => 1, :significant => true) # => 100
Rails code Example:
def profit=(new_profit)
number_with_precision(self[:profit], :precision => 1, :significant => true)
end

Convert User input to integer

So I have a form where users can input a price. I'm trying to make a before_validation that normalizes the data, clipping the $ if the user puts it.
before_validation do
unless self.price.blank? then self.price= self.price.to_s.gsub(/\D/, '').to_i end
end
If user inputs $50 This code is giving me 0. If user inputs 50$ this code gives me 50. I think since the data type is integer that rails is running .to_i prior to my before_validation and clipping everything after the $. This same code works fine if the data type is a string.
Anyone have a solution that will let me keep the integer datatype?
One way is to override the mechanism on the model that sets the price, like this:
def price=(val)
write_attribute :price, val.to_s.gsub(/\D/, '').to_i
end
So when you do #model.price = whatever, it will go to this method instead of the rails default attribute writer. Then you can convert the number and use write_attribute to do the actual writing (you have to do it this way because the standard price= is now this method!).
I like this method best, but for reference another way to do it is in your controller before assigning it to the model. The parameter comes in as a string, but the model is converting that string to a number, so work with the parameter directly. Something like this (just adapt it to your controller code):
def create
#model = Model.new(params[:model])
#model.price = params[:model][:price].gsub(/\D/, '').to_i
#model.save
end
For either solution, remove that before_validation.
I would define a virtual attribute and do my manipulation there allowing you to format and modify both the getter and setter at will:
class Model < ActiveRecord::Base
def foo_price=(price)
self.price = price... #=> Mods to string here
end
def foo_price
"$#{price}"
end
You also might want to note that:
"$50.00".gsub(/\D/, '').to_i #=> 5000
My soluction
colum price type decimal
t.decimal :price, precision: 12, scale: 6
# app/concern/sanitize_fields.rb
module SanitizeFields
extend ActiveSupport::Concern
def clear_decimal(field)
return (field.to_s.gsub(/[^\d]/, '').to_d / 100.to_d) unless field.blank?
end
def clear_integer(field)
field.to_s.strip.gsub(/[^\d]/, '') unless field.blank?
end
# module ClassMethods
# def filter(filtering_params)
# results = self.where(nil)
# filtering_params.each do |key, value|
# results = results.public_send(key, value) if value.present?
# end
# results
# end
#
# #use
# #def index
# # #products = Product.filter(params.slice(:status, :location, :starts_with))
# #end
#
# end
end
#app/controllers/products_controller.rb
include SanitizeFields
params[:product][:price] = clear_decimal(params[:product][:price])

after_commit for an attribute

I am using an after_commit in my application.
I would like it to trigger only when a particular field is updated in my model. Anyone know how to do that?
Old question, but this is one method that I've found that might work with the after_commit callback (working off paukul's answer). At least, the values both persist post-commit in IRB.
after_commit :callback,
if: proc { |record|
record.previous_changes.key?(:attribute) &&
record.previous_changes[:attribute].first != record.previous_changes[:attribute].last
}
Answering this old question because it still pops up in search results
you can use the previous_changes method which returnes a hash of the format:
{ "changed_attribute" => ["old value", "new value"] }
it's what changes was until the record gets actually saved (from active_record/attribute_methods/dirty.rb):
def save(*) #:nodoc:
if status = super
#previously_changed = changes
#changed_attributes.clear
# .... whatever goes here
so in your case you can check for previous_changes.key? "your_attribute" or something like that
Old question but still pops up in search results.
As of Rails 5 attribute_changed? was deprecated. Using saved_change_to_attribute? instead of attribute_changed? is recommended.
I don't think you can do it in after_commit
The after_commit is called after the transaction is commited Rails Transactions
For example in my rails console
> record = MyModel.find(1)
=> #<MyModel id: 1, label: "test", created_at: "2011-08-19 22:57:54", updated_at: "2011-08-19 22:57:54">
> record.label = "Changing text"
=> "Changing text"
> record.label_changed?
=> true
> record.save
=> true
> record.label_changed?
=> false
Therefore you won't be able to use the :if condition on after_commit because the attribute will not be marked as changed anymore as it has been saved. You may need to track whether the field you are after is changed? in another callback before the record is saved?
This is a very old problem, but the accepted previous_changes solution just isn't robust enough. In an ActiveRecord transaction, there are many reasons why you might save a Model twice. previous_changes only reflects the result of the final save. Consider this example
class Test < ActiveRecord::Base
after_commit: :after_commit_test
def :after_commit_test
puts previous_changes.inspect
end
end
test = Test.create(number: 1, title: "1")
test = Test.find(test.id) # to initialize a fresh object
test.transaction do
test.update(number: 2)
test.update(title: "2")
end
which outputs:
{"title"=>["1", "2"], "updated_at"=>[...]}
but, what you need is:
{"title"=>["1", "2"], "number"=>[1, 2], "updated_at"=>[...]}
So, my solution is this:
module TrackSavedChanges
extend ActiveSupport::Concern
included do
# expose the details if consumer wants to do more
attr_reader :saved_changes_history, :saved_changes_unfiltered
after_initialize :reset_saved_changes
after_save :track_saved_changes
end
# on initalize, but useful for fine grain control
def reset_saved_changes
#saved_changes_unfiltered = {}
#saved_changes_history = []
end
# filter out any changes that result in the original value
def saved_changes
#saved_changes_unfiltered.reject { |k,v| v[0] == v[1] }
end
private
# on save
def track_saved_changes
# maintain an array of ActiveModel::Dirty.changes
#saved_changes_history << changes.dup
# accumulate the most recent changes
#saved_changes_history.last.each_pair { |k, v| track_saved_change k, v }
end
# v is an an array of [prev, current]
def track_saved_change(k, v)
if #saved_changes_unfiltered.key? k
#saved_changes_unfiltered[k][1] = track_saved_value v[1]
else
#saved_changes_unfiltered[k] = v.dup
end
end
# type safe dup inspred by http://stackoverflow.com/a/20955038
def track_saved_value(v)
begin
v.dup
rescue TypeError
v
end
end
end
which you can try out here: https://github.com/ccmcbeck/after-commit
It sounds like you want something like a conditional callback. If you had posted some code I could have pointed you in the right direction however I think you would want to use something like this:
after_commit :callback,
:if => Proc.new { |record| record.field_modified? }
Use gem ArTransactionChanges. previous_changes is not working for me in Rails 4.0.x
Usage:
class User < ActiveRecord::Base
include ArTransactionChanges
after_commit :print_transaction_changes
def print_transaction_changes
transaction_changed_attributes.each do |name, old_value|
puts "attribute #{name}: #{old_value.inspect} -> #{send(name).inspect}"
end
end
end

Decimals and commas when entering a number into a Ruby on Rails form

What's the best Ruby/Rails way to allow users to use decimals or commas when entering a number into a form? In other words, I would like the user be able to enter 2,000.99 and not get 2.00 in my database.
Is there a best practice for this?
Does gsub work with floats or bigintegers? Or does rails automatically cut the number off at the , when entering floats or ints into a form? I tried using self.price.gsub(",", "") but get "undefined method `gsub' for 8:Fixnum" where 8 is whatever number I entered in the form.
I had a similar problem trying to use localized content inside forms. Localizing output is relatively simple using ActionView::Helpers::NumberHelper built-in methods, but parsing localized input it is not supported by ActiveRecord.
This is my solution, please, tell me if I'm doing anything wrong. It seems to me too simple to be the right solution. Thanks! :)
First of all, let's add a method to String.
class String
def to_delocalized_decimal
delimiter = I18n::t('number.format.delimiter')
separator = I18n::t('number.format.separator')
self.gsub(/[#{delimiter}#{separator}]/, delimiter => '', separator => '.')
end
end
Then let's add a class method to ActiveRecord::Base
class ActiveRecord::Base
def self.attr_localized(*fields)
fields.each do |field|
define_method("#{field}=") do |value|
self[field] = value.is_a?(String) ? value.to_delocalized_decimal : value
end
end
end
end
Finally, let's declare what fields should have an input localized.
class Article < ActiveRecord::Base
attr_localized :price
end
Now, in your form you can enter "1.936,27" and ActiveRecord will not raise errors on invalid number, because it becomes 1936.27.
Here's some code I copied from Greg Brown (author of Ruby Best Practices) a few years back. In your model, you identify which items are "humanized".
class LineItem < ActiveRecord::Base
humanized_integer_accessor :quantity
humanized_money_accessor :price
end
In your view templates, you need to reference the humanized fields:
= form_for #line_item do |f|
Price:
= f.text_field :price_humanized
This is driven by the following:
class ActiveRecord::Base
def self.humanized_integer_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? val.to_i.with_commas : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(","))
end
end
end
def self.humanized_float_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? val.to_f.with_commas : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(","))
end
end
end
def self.humanized_money_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? ("$" + val.to_f.with_commas) : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(",$"))
end
end
end
end
You can try stripping out the commas before_validation or before_save
Oops, you want to do that on the text field before it gets converted. You can use a virtual attribute:
def price=(price)
price = price.gsub(",", "")
self[:price] = price # or perhaps price.to_f
end
Take a look at the i18n_alchemy gem for date & number parsing and localization.
I18nAlchemy aims to handle date, time and number parsing, based on current I18n locale format. The main idea is to have ORMs, such as ActiveRecord for now, to automatically accept dates/numbers given in the current locale format, and return these values localized as well.
I have written following code in my project. This solved all of my problems.
config/initializers/decimal_with_comma.rb
# frozen_string_literal: true
module ActiveRecord
module Type
class Decimal
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
class Float
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
class Integer
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
end
end
module ActiveModel
module Validations
class NumericalityValidator
protected
def parse_raw_value_as_a_number(raw_value)
raw_value = raw_value.gsub(',', '') if raw_value.is_a?(::String)
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
end
end
end
end
I was unable to implement the earlier def price=(price) virtual attribute suggestion because the method seems to call itself recursively.
I ended up removing the comma from the attributes hash, since as you suspect ActiveRecord seems to truncate input with commas that gets slotted into DECIMAL fields.
In my model:
before_validation :remove_comma
def remove_comma
#attributes["current_balance"].gsub!(',', '') # current_balance here corresponds to the text field input in the form view
logger.debug "WAS COMMA REMOVED? ==> #{self.current_balance}"
end
Here's something simple that makes sure that number input is read correctly. The output will still be with a point instead of a comma. That's not beautiful, but at least not critical in some cases.
It requires one method call in the controller where you want to enable the comma delimiter. Maybe not perfect in terms of MVC but pretty simple, e.g.:
class ProductsController < ApplicationController
def create
# correct the comma separation:
allow_comma(params[:product][:gross_price])
#product = Product.new(params[:product])
if #product.save
redirect_to #product, :notice => 'Product was successfully created.'
else
render :action => "new"
end
end
end
The idea is to modify the parameter string, e.g.:
class ApplicationController < ActionController::Base
def allow_comma(number_string)
number_string.sub!(".", "").sub!(",", ".")
end
end
You can try this:
def price=(val)
val = val.gsub(',', '')
super
end

Resources