I am a Rails 3 beginner working on an application that allows the user to enter monetary values. I am using a jQuery plugin (http://github.com/plentz/jquery-maskmoney) to display decimal values as monetary values on the edit page. All the decimal attributes are manipulated as currency when editing. When saving the, purchase_price and capital_reserves are saved correctly. The Property before_save function is called and the currency values ($123.45) get converted to decimal values (123.45).
The problem is that the associated rent values are never saved if I just edit the rent prices. I can see the correct values being sent in the parameters but the before_save code in the Rent model is never triggered. If I edit the apartment number value and a rent price, then the rent price is saved correctly. Also, after that I can just edit the price for the apartment number I previously modified. However, any other rent price will not be updated.
I am using MySQL and Rails 3.0.9
Steps to reproduce: Non-Issue
- Edit a property
- Modify the property's purchase price and/or capital reserves value
- Click Update
- These values are converted from currency values ($1,234.56) to decimal (1234.56) by the before_save code in Property
Issue
- Edit a property
- Modify the property's rent current price and/or market price values
- Click Update
- These values do not get saved. The before_save code is not called in the Rent model.
Issue
- Edit a property
- Modify the property's purchase price and/or capital reserves value, but also edit the apartment number
- Click Update
- These values are saved correctly.
- Now you can edit the price values for the row previously saved and those prices are saved. Why?
I made a small project to showcase this if anyone is interested (https://github.com/michaelklem/Money-Test).
Here are my data models.
Property class
class Property < ActiveRecord::Base
before_save :handle_before_save
has_many :rents, :dependent => :destroy
accepts_nested_attributes_for :rents, :allow_destroy => true
def handle_before_save
if new_record?
generate_default_rent_data
end
remove_currency_formatting
end
def generate_default_rent_data
10.times do |i|
self.rents.build(:apartment_number => i+1)
end
end
def remove_currency_formatting
if self.capital_reserves.to_s != self.capital_reserves_before_type_cast.to_s
self.capital_reserves = Property.remove_currency_format(self.capital_reserves_before_type_cast)
end
if self.purchase_price.to_s != self.purchase_price_before_type_cast.to_s
self.purchase_price = Property.remove_currency_format(self.purchase_price_before_type_cast)
end
end
#
# handles removing all characters from currency objects
# except for 0-9 and .
#
def self.remove_currency_format(currency_at开发者_StackOverflow中文版tribute)
currency_attribute.gsub(/[^0-9.]/, "")
end
end
Rent class:
class Rent < ActiveRecord::Base
belongs_to :property
before_save :handle_before_save
def handle_before_save
remove_currency_formatting
end
def remove_currency_formatting
if self.current_price.to_s != self.current_price_before_type_cast.to_s
self.current_price = Property.remove_currency_format(self.current_price_before_type_cast)
end
if self.market_price.to_s != self.market_price_before_type_cast.to_s
self.market_price = Property.remove_currency_format(self.market_price_before_type_cast)
end
end
end
Not sure if I am seeing a bug or missing something obvious. Thanks for looking into this.
Update
After I posted this I found this SO question Stripping the first character of a string that helped me figure this out. It still seems to me that my original issue is a bug.
I was able to simplify my code to the following and everything works.
class Property < ActiveRecord::Base
before_save :handle_before_save
has_many :rents, :dependent => :destroy
accepts_nested_attributes_for :rents, :allow_destroy => true
def handle_before_save
if new_record?
generate_default_rent_data
end
end
def purchase_price=(data)
if data.is_a?(String)
data = Property.remove_currency_format(data)
write_attribute(:purchase_price, data)
end
end
def capital_reserves=(data)
if data.is_a?(String)
data = Property.remove_currency_format(data)
write_attribute(:capital_reserves, data)
end
end
#
# generate some default data
#
def generate_default_rent_data
10.times do |i|
self.rents.build(:apartment_number => i+1) # provide a default value for apartment numbers
end
end
def self.remove_currency_formatting(data)
if data.is_a?(String)
data = Property.remove_currency_format(data)
end
return data
end
#
# handles removing all characters from currency objects
# except for 0-9 and .
#
def self.remove_currency_format(currency_attribute)
currency_attribute.gsub(/[^0-9.]/, "")
end
def purchase_price=(data)
_write_attribute(:purchase_price, data)
end
def capital_reserves=(data)
_write_attribute(:capital_reserves, data)
end
private
def _write_attribute(attribute, data)
write_attribute(attribute, Property.remove_currency_formatting(data))
end
end
class Rent < ActiveRecord::Base
belongs_to :property
def current_price=(data)
_write_attribute(:current_price, data)
end
def market_price=(data)
_write_attribute(:market_price, data)
end
private
def _write_attribute(attribute, data)
write_attribute(attribute, Property.remove_currency_formatting(data))
end
end
This may be helpful, and might clean up a lot of your write_attribute code if you chose to use it.
In our app, we run into this issue frequently, we have TONS of amount fields and it would be hellish to add this logic to each model (almost every table has decimal fields for amounts). So in an initializer, I added the following:
ActiveRecord::ConnectionAdapters::Column.class_eval do
def type_cast_with_commas_removed(value)
if type == :decimal && value.is_a?(String)
value = value.delete(',')
end
type_cast_without_commas_removed(value)
end
alias_method_chain :type_cast, :commas_removed
end
class BigDecimal
alias :old_to_s :to_s
def to_s(s=nil)
if s.nil?
parts = ("%.2f" % self).split('.')
parts[0].gsub!(/(\d)(?=(\d{3})+(?!\d))/, "\\1,")
parts.join('.')
else
old_to_s(s)
end
end
end
This achieves 2 things: First, on insertion to the database, commas are stripped from all decimal field entries if value is a string (you could change to use your regex of only grabbing numbers and period). Second, when a BigDecimal is converted to string the commas are re-inserted (this was just a business requirement in our case as we wanted to display the proper formatting at all times, you may not need).
Anyway, I know you solved your problem, but just wanted to give another approach. :)
Cheers!
精彩评论