DRY way of assigning new object's values from an existing object's values - ruby-on-rails

I created a class method that is called when a new object is created and copied from an old existing object. However, I only want to copy some of the values. Is there some sort of Ruby shorthand I can use to clean this up? It's not entirely necessary, just curious to know if something like this exists?
Here is the method I want to DRY up:
def set_message_settings_from_existing existing
self.can_message = existing.can_message
self.current_format = existing.current_format
self.send_initial_message = existing.send_initial_message
self.send_alert = existing.send_alert
self.location = existing.location
end
Obviously this works perfectly fine, but to me looks a little ugly. Is there any way to clean this up? If I wanted to copy over every value that would be easy enough, but because I only want to copy these 5 (out of 20 something) values, I decided to do it this way.

def set_message_settings_from_existing(existing)
[:can_message, :current_format, :send_initial_message, :send_alert, :location].each do |attribute|
self.send("#{attribute}=", existing.send(attribute))
end
end
Or
def set_message_settings_from_existing(existing)
self.attributes = existing.attributes.slice('can_message', 'current_format', 'send_initial_message', 'send_alert', 'location')
end

a hash might be cleaner:
def set_message_settings_from_existing existing
fields = {
can_message: existing.can_message,
current_format: existing.current_format,
send_initial_message: existing.send_initial_message,
send_alert: existing.send_alert,
location: existing.location
}
self.attributes = fields
end
you can take this further by only selecting the attributes you want:
def set_message_settings_from_existing existing
fields = existing.attributes.slice(
:can_message,
:current_format,
:send_initial_message,
:send_alert,
:location
)
self.attributes = fields
end
at this point you could also have these fields defined somewhere, eg:
SUB_SET_OF_FIELDS = [:can_message, :current_format, :send_initial_message, :send_alert, :location]
and use that for your filter instance.attributes.slice(SUB_SET_OF_FIELDS)

Related

Interpolating an attribute's key before save

I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end

Tracking object changes rails (Active Model Dirty)

I'm trying to track changes on a method just like we are tracking changes on record attributes using the Active Model Dirty.
At the moment I'm using this method to check changes to a set of attributes.
def check_updated_attributes
watch_attrs = ["brand_id", "name", "price", "default_price_tag_id", "terminal_usp", "primary_offer_id", "secondary_offer_id"]
if (self.changed & watch_attrs).any?
self.tag_updated_at = Time.now
end
end
However the attribute price is not a normal attribute with a column but a method defined in the model that combines two different attributes.
This is the method:
def price
if manual_price
price = manual_price
else
price = round_99(base_price)
end
price.to_f
end
Is there a way to track changes on this price method? or does this only work on normal attributes?
Edit: Base price method:
def base_price(rate = Setting.base_margin, mva = Setting.mva)
(cost_price_map / (1 - rate.to_f)) * ( 1 + mva.to_f)
end
cost_price and manual_price are attributes with columns it the terminal table.
Ok, solved it.
I had to create a custom method named price_changed? to check if the price had changed.
def price_changed?
if manual_price
manual_price_changed?
elsif cost_price_map_changed?
round_99(base_price) != round_99(base_price(cost_price = read_attribute(:cost_price_map)))
else
false
end
end
This solved the problem, although not a pretty solution if you have many custom attributes.

Woking with Packed Arrays, accessing data

I'm working with a large image array, so i've packed it. To access the pixels in the array i've implemented two methods.
def get_p(a)
data=a.unpack('S9s')
end
def put_p(array,index_a,value)
index=index_a[0]
k=array.unpack('S9s')
k[index]=value
k.pack('S9s')
end
It works, but i wondered if there was a more elegant way to do this. Makes my code look different than my standard array functions.
If get_p(image_data[i][j+1])[BLOB]==0
vs
if image_data[i][j+1][BLOB]==0
Also, don't know if anyone cares, but unpack doesn't seem to be documented anywhere, i was lucky to find a reference here, but it took some time.
You could craete a class like:
class PackedArray
def initialize(array)
#packed_array = array.pack('S9s')
end
def [](key)
data = #packed_array.unpack('S9s')
data[key]
end
def []=(key, val)
k = #packed_array.unpack('S9s')
k[key]=val
#packed_array = k.pack('S9s')
end
end
Then, fill your image_data[i][j] with an instance of this class. E.g.
for i in [0..image_data.size]
for j in [0..image_data[i].size]
image_data[i][j] = new PackedArray(image_data[i][j])
end
end
And finally you can simply use:
if image_data[i][j+1][BLOB] == 0
Without needing of packing/unpacking manually.

Rails personal class Each routine

I'm kind of a newbie in some areas of ruby and rails. So I I'm writing a class to read excel depending on the extension and return the row in a each routine. Something like this:
class ExcelRead
(dependencies)
def initialize(path, sheet_n = 0)
type = File.extname(path)
if type == JitExcelRead::XLS
Spreadsheet.client_encoding = 'UTF-8'
book = Spreadsheet.open path
book_sheet = book.worksheet sheet_n
elsif type == JitExcelRead::XLSX
book = Creek::Book.new path
book_sheet = book.sheets[sheet_n]
end
#book = book
#book_sheet = book_sheet
#book_rows = book_sheet.rows
#path = path
#type = type
end
end
So this means that I call on my application
xls = ExcelRead.new(uploaded_file.filename_path)
and everything runs smooth. I have the objects I need at my disposal. My problem now is how to iterate through them. I thought that adding a method to may class like this
def each
binding.pry
end
and calling it normally on my app like so
xls.book_rows.each do |row|
end
would make me enter that code, but not really...
help?
If you added a each method to your ExcelRead class, and you create an instance of this class called xls, then you have to access it using xls.each, not xls.book_rows.each.
Using the former, you are calling the each method from the Enumerator, as book_rows is a collection.
I can only guess that you want a custom way to iterate your book_row, so i think something like this should be what you are trying to achieve:
def iterate
self.book_rows.each do |br|
# do stuff
end
end
And you call it like:
xls.iterate
But this is only a wild guess.

metaprogramming for params

How can I update these very similar text fields in a less verbose way? The text fields below are named as given - I haven't edited them for this question.
def update
company = Company.find(current_user.client_id)
company.text11 = params[:content][:text11][:value]
company.text12 = params[:content][:text12][:value]
company.text13 = params[:content][:text13][:value]
# etc
company.save!
render text: ""
end
I've tried using send and to_sym but no luck so far...
[:text11, :text12, :text13].each do |s|
company.send("#{s}=".to_sym, params[:content][s][:value])
end
If they are all incremental numbers, then:
11.upto(13).map{|n| "text#{n}".to_sym}.each do |s|
company.send("#{s}=".to_sym, params[:content][s][:value])
end
I'd consider first cleaning up the params, then move onto dynamically assigning attributes. A wrapper class around your params would allow you to more easily unit test this code. Maybe this helps get you started.
require 'ostruct'
class CompanyParamsWrapper
attr_accessor :text11, :text12, :text13
def initialize(params)
#content = params[:content]
content_struct = OpenStruct.new(#content)
self.text11 = content_struct.text11[:value]
self.text12 = content_struct.text12[:value]
self.text13 = content_struct.text13[:value]
end
end
# Company model
wrapper = CompanyParamsWrapper.new(params)
company.text11 = wrapper.text11
# now easier to use Object#send or other dynamic looping

Resources