Rails setting data inside a method - ruby-on-rails

I have a strange issue setting data inside an active record..
When I attempt to set the data inside a method, it doesn't seem to affect anything.
Here's my class
class Option < ActiveRecord::Base
serialize :returns_policy_refunds, Array
def reloadRefundOptions!
#returns_policy_refunds = WebSite.get_refund_options #options array
end
end
Simple as' class eh?
To test serialization I'm just spitting out the data on a screen..
-#options.each do |option|
- option.returns_policy_refunds = ["wtf"] #just to reset things
<b>BLOCK 1</b>
= option.reloadRefundOptions!
= option.returns_policy_refunds
<br>
<b>BLOCK 2</b>
= option.returns_policy_refunds = WebSite.get_refund_options
= option.returns_policy_refunds
Now.. I'd expect to see the same in BLOCK1 as in BLOCK2..
The method sets the return policy..
What I actually see in the first option.returns_policy_refunds is ["wtf"]
What am I missing? I must be doing something wrong, but I have no idea what.
Any thoughts?

Leave out the # in your attribute assignment:
class Option < ActiveRecord::Base
serialize :returns_policy_refunds, Array
def reloadRefundOptions!
self.returns_policy_refunds = WebSite.get_refund_options #options array
end
end
Haven't tried it yet but I would say that option.returns_policy_refunds gets the data from the attributes hash defined by ActiveRecord. If you assign a class variable using # it's just defined there and may only be accessed with an attribute reader or a direct send.

Related

Adding a method to an attribute in Ruby

How do you define a method for an attribute of an instance in Ruby?
Let's say we've got a class called HtmlSnippet, which extends ActiveRecord::Base of Rails and has got an attribute content. And, I want to define a method replace_url_to_anchor_tag! for it and get it called in the following way;
html_snippet = HtmlSnippet.find(1)
html_snippet.content = "Link to http://stackoverflow.com"
html_snippet.content.replace_url_to_anchor_tag!
# => "Link to <a href='http://stackoverflow.com'>http://stackoverflow.com</a>"
# app/models/html_snippet.rb
class HtmlSnippet < ActiveRecord::Base
# I expected this bit to do what I want but not
class << #content
def replace_url_to_anchor_tag!
matching = self.match(/(https?:\/\/[\S]+)/)
"<a href='#{matching[0]}'/>#{matching[0]}</a>"
end
end
end
As content is an instance of String class, redefine String class is one option. But I don't feel like to going for it because it overwrites behaviour of all instances of String;
class HtmlSnippet < ActiveRecord::Base
class String
def replace_url_to_anchor_tag!
...
end
end
end
Any suggestions please?
The reason why your code is not working is simple - you are working with #content which is nil in the context of execution (the self is the class, not the instance). So you are basically modifying eigenclass of nil.
So you need to extend the instance of #content when it's set. There are few ways, there is one:
class HtmlSnippet < ActiveRecord::Base
# getter is overrided to extend behaviour of freshly loaded values
def content
value = read_attribute(:content)
decorate_it(value) unless value.respond_to?(:replace_url_to_anchor_tag)
value
end
def content=(value)
dup_value = value.dup
decorate_it(dup_value)
write_attribute(:content, dup_value)
end
private
def decorate_it(value)
class << value
def replace_url_to_anchor_tag
# ...
end
end
end
end
For the sake of simplicity I've ommited the "nil scenario" - you should handle nil values differently. But that's quite simple.
Another thing is that you might ask is why I use dup in the setter. If there is no dup in the code, the behaviour of the following code might be wrong (obviously it depends on your requirements):
x = "something"
s = HtmlSnippet.find(1)
s.content = x
s.content.replace_url_to_anchor_tag # that's ok
x.content.replace_url_to_anchor_tag # that's not ok
Wihtout dup you are extending not only x.content but also original string that you've assigned.

Rails - Model changes not being saved or preserved between controller and model

I'd appreciate any help I can get with a somewhat strange phenonemon going on in my code. The controller's create method is (roughly) as follows:
def create
#session ||= Session.new
#session.date = params[:date]
#session.generate_string
#session.save
# etc
end
And the model:
class Session < ActiveRecord::Base # table is 'sessions' with 3 columns :id, :str, :date
include MyHelper
def generate_string(num_chars)
#str ||= ""
num_chars.to_i.times do
#str += some_method_in_MyHelper() # method returns a string
end
end
end
With some logging I found out that although the generate_string is working correctly, the resulting #session (in the controller) has the date set as expected but the value of str is a blank string. Sure enough, when the .save is hit, the database is told to insert a record consisting of a blank string and the correct date.
I found this Why do my changes to model instances not get saved sometimes in Rails 3? that suggests I should be using the "self" prefix instead of #. This seems to make the code work as expected, but seems strange because I thought self.xxx referred to the class, not the class instance. I'd be grateful if anyone could clarify what's going on - thanks!
self refers to the instance when used inside an instance method. It refers to the class outside an instance method, when it (self) is the class that's being defined.
# is an instance variable, which is different than an ActiveRecord column.
In order to store it in the str field to be saved to the database, you need to use self.str method. I think this is what you are looking for
class Session < ActiveRecord::Base # table is 'sessions' with 3 columns :id, :str, :date
include MyHelper
def generate_string(num_chars)
str = ""
num_chars.to_i.times do
str += some_method_in_MyHelper() # method returns a string
end
self.str = str # store it in attribute to be saved into db
end
end
Notice I removed the instance variable #str and changed it to local variable str instead because it seems like you want to generate a new string everytime this method is called? Also, this variable caching is useless
#session ||= Session.new
because instance variables only stick around for a single request. It should be
#session = Session.new

Order of params stops *_attributes= from working

So I've implemented a hack and I want to know what the "proper" way is to do it.
The issue is that I have an *_attributes=() method that uses an instance variable. The reason this is a problem is that at the time the method is called, that instance variable hasn't been set. Here is the method in question:
def proposed_times_attributes=(attributes)
attributes.each do |key,value|
value[:timezone] = timezone
end
assign_nested_attributes_for_collection_association(:proposed_times, attributes)
end
The timezone is in the params hash after proposed_times_attributes. Therefore my hack is to delete it from params, then add it back, thus moving it to the end of the line.
def create
p = params[:consultation]
a = p.delete(:proposed_times_attributes)
p[:proposed_times_attributes] = a
#consultation = current_user.advised_consultations.new(p)
...
end
What is the proper way that I should be doing this?
new() calls load() where the loop is that goes through each key/value pair.
Thankfully I'm using Ruby 1.9.2 which keeps the order, but it would be nice to know how to do this so that it wouldn't depend on this fact.
If the next operation after new will always be a save operation, you can store the attributes in an accessor, and use a before_validation callback to operate on them as you wish.
class Consultations < ActiveRecord::Base
attr_accessor :proposed_times_attributes
before_validation :assign_proposed_times
def assign_proposed_times
proposed_times_attributes.each do |key,value|
value[:timezone] = timezone
end
assign_nested_attributes_for_collection_association(:proposed_times, attributes)
end
end
Now in your controller you simply have:
def create
#consultation = current_user.advised_consultations.new(params[:consultation])
...
end
If you wish to do other operations before calling save, then pulling out the param as you did in your example, then passing it to an appropriate method after calling new would be the way to go.

Executing Rails virtual attribute setters in order

I have an ActiveRecord model with several virtual attribute setters. I want to build an object but not save it to the database. One setter must execute before the others. How to do?
As a workaround, I build the object in two steps
#other_model = #some_model.build_other_model
#other_model.setup(params[:other_model)
Where setup is:
class OtherModel < ActiveRecord::Base
def setup(other_params)
# execute the important_attribute= setter first
important_attribute = other_params.delete(:important_attribute)
# set the other attributes in whatever order they occur in the params hash
other_params.each { |k,v| self.send("#{k}=",v) }
end
end
This seems to work, but looks kludgy. Is there a better way?
EDIT
per neutrino's suggestion, I added a method to SomeModel:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = OtherModel.new(:some_model=>self)
other_model.setup(other_params)
other_model
end
end
It's a good thing that you have this manipulations done in an OtherModel's method, because you can just call this method and not worry about the order of assignments. So I would leave this part but just call it from a SomeModel's method:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = build_other_model
other_model.setup(other_params)
other_model
end
end
So then you would have
#other_model = #some_model.build_other_model(params[:other_model])
I took your idea of deleting the important attribute first in your setup method, but used alias_chain_method instead to make it more of a transparent process:
def attributes_with_set_important_attribute_first=(attributes = {})
# Make sure not to accidentally blank out the important_attribute when none is passed in
if attributes.symbolize_keys!.include?(:important_attribute)
self.important_attribute = attributes.delete(:important_attribute)
end
self.attributes_without_set_important_attribute_first = attributes
end
alias_method_chain :attributes=, :set_important_attribute_first
This way none of your code should change from the normal Rails style
#other_model = #some_model.other_models.build(params[:other_model])

Rails: Initializing attributes that are dependent on one another

I have the following classes in my ActiveRecord model:
def Property < ActiveRecord::Base
# attribute: value_type (can hold values like :integer, :string)
end
def PropertyValue < ActiveRecord::Base
belongs_to property
# attribute: string_value
# attribute: integer_value
end
A PropertyValue object is intended to hold only a string value or an integer value, depending on the type, specified in the value_type attribute of the associated Property object. Obviously, we shouldn't bother the user of the PropertyValue class with this underlying string_value/integer_value mechanism. So I'd like to use a virtual attribute "value" on PropertyValue, that does something like this:
def value
unless property.nil? || property.value_type.nil?
read_attribute((property.value_type.to_s + "_value").to_sym)
end
end
def value=(v)
unless property.nil? || property.value_type.nil?
write_attribute((property.value_type.to_s + "_value").to_sym, v)
end
end
I want to offer the user a view to fill in a bunch of property values, and when the view is posted, I'd like to have PropertyValue objects instantiated based on the list of attributes that is passed in from the view. I'm used to using the build(attributes) operation for this. However, the problem now occurs that I don't have any control over the order in which the attribute initialization takes place. Thus the assignment of the value attribute will not work when the association with the Property attribute has not yet been made, since the value_type cannot be determined. What is the correct "Rails" way to deal with this?
BTW, as a workaround I have tried the following:
def value=(v)
if property.nil? || property.value_type.nil?
#temp_value = v
else
write_attribute((property.value_type.to_s + "_value").to_sym, v)
end
end
def after_initialize
value = #temp_value
end
Apart from the fact that I think this is quite an ugly solution, it doesn't actually work with the "build" operation. The #temp_value gets set in the "value=(v)" operation. Also, the "after_initialize" in executed. But, the "value = #temp_value" does not call the "value=(v)" operation strangely enough! So I'm really stuck.
EDIT: build code
I indeed realized that the code to build the Property objects would be handy. I'm doing that from a Product class, that has a has_many association with Property. The code then looks like this:
def property_value_attributes=(property_value_attributes)
property_value_attributes.each do |attributes|
product_property_values.build(attributes)
end
end
At the same time I figured out what I did wrong in the after_initialize operation; it should read:
def after_initialize
#value = #temp_value
end
The other problem is that the property association on the newly built property_value object will never be set until the actual save() takes place, which is after the "after_initialize". I got this to work by adding the value_type of the respective property object to the view and then having it passed in through the attributes set upon post. That way I don't have to instantiate a Property object just to fetch the value_type. Drawback: I need a redundant "value_type" accessor on the PropertyValue class.
So it works, but I'm still very interested in if there's a cleaner way to do this. One other way is to make sure the property object is attached first to the new PropertyValue before initializing it with the other attributes, but then the mechanism is leaked into the "client object", which not too clean either.
I would expect some sort of way to override the initializer functionality in such a way that I could affect the order in which attributes get assigned. Something very common in languages like C# or Java. But in Rails...?
One option is to save the Property objects first, and then add the PropertyValue objects afterwards. If you need to you could wrap the whole thing in a transaction to ensure that the Properties are rolled back if their corresponding PropertyValues could not be saved.
I don't know what your collected data from the form looks like, but assuming it looks like the following:
#to_create = { :integer => 3, :string => "hello", :string => "world" }
You could do something like this:
Property.transaction do
#to_create.keys.each do |key|
p = Properties.create( :value_type => key.to_s )
p.save
pval = p.property_value.build( :value => #to_create[key] )
pval.save
end
end
That way you don't have to worry about the nil check for Property or Property.value_type.
As a side note, are you sure you need to be doing all this in the first place? Most database designs I've seen that have this kind of really generic meta-information end up being highly non-scalable and are almost always the wrong solution to the problem. It will require a lot of joins to get a relatively simple set of information.
Suppose you have a parent class Foo that holds the property/value pairs. If Foo has ten properties, that requires 20 joins. That's a lot of DB overhead.
Unless you actually need to run SQL queries against PropertyValues (e.g. "get all Foos that have the property "bar"), you could probably simplify this a lot by just adding an attribute called "properties" to Foo, then serializing your Properties hash and putting it in that field. This will simplify your code, your database design, and speed up your application as well.
Oh jeeezzzz... this is insanely simple, now that I puzzled on it a little more. I just need to override the "initialize(attributes = {})" method on the PropertyValue class like so:
def initialize(attributes = {})
property = Property.find(attributes[:property_id]) unless attributes[:property_id].blank?
super(attributes)
end
Now I'm always sure that the property association is filled before the other attributes are set. I just didn't realize soon enough that Rails' "build(attributes = {})" and "create(attributes = {})" operations eventually boil down to "new(attributes = {})".
Probably you should try to use ActiveRecord get/set methods, i.e.:
def value
send("#{property.value_type}_value") unless property || property.value_type
end
def value=(v)
send("#{property.value_type}_value=", value) unless property || property.value_type
end

Resources