Rails Replace Attributes in Arrays - ruby-on-rails

I have a list of incorrect cities name in Philippines:
>> a = City.find_all_by_country_id(4)
=> [#<City id: 91, name: "Alaminos", country_id: 4, created_at: "2009-11-12 04:06:14", updated_at: "2009-11-12 04:06:14">, #<City id: 92, name: "Angeles", country_id: 4, created_at: "2009-11-12 04:06:14", ...
And I wanted to replace all the names with the correct one:
=> b = ["Abra", "Agusan del Norte", "Agusan del Sur", ...
I wanted to use the replace method because I wanted to update the existing city id, inserting/truncating them only if necessary.
But I still can't figure this one out, since a is an array of arrays (correct me if I am wrong) while b is just a simple, down-to-earth array.

a should be an array of City models. For example if you wanted to change the city name of the city id 91 (the first record) to "Abra" (the first element in the array) you would just do a[0].name = b[0]. I'm a little unclear on what exactly you're trying to do, but hopefully this will get you over the syntax part of the problem.

Have a look at at Array#zip, and ActiveRecord::Base#update_attribute. As Andy Gaskell points out, a is an array of City objects. So zip can be called on a, and update_attribute can be called on any element of a.
The short simple way of doing what you want is this:
a.zip(b){|array| array[0].update_attribute(:name, array[1])}
Zip will turn multiple arrays into an array of arrays. Where each index of the new array is an array composed of elements in the source arrays of the same index.
a.zip(b) = c #=> ∀i: c[i] = [a[i],b[i]]
If you pass a block to zip, Ruby will yield each array in c to the block. It's a handy shortcut for a.zip(b).collect(&block).
In the code above, array[0] = a[i], and array[1] = b[i], each iteration supplies a different value of i. update_attributes will update the record in the database bypassing validations and callbacks.
Caveats:
If b has more elements then a you will get No Method Errors.
If a has more elements than b, the extra elements will have their name set to "".
Again, Update_attributes bypasses validations and saves the updated record automatically. If this is not too your liking you can replace the innards of the block with:
array[0].name = array[1]; array[0].save

I decided to use a migration file instead, so this is the code:
class AddProvinces < ActiveRecord::Migration
def self.up
philippines = Country.find_by_name('Philippines')
old_cities = philippines.cities
new_cities = (['Abra', 'Agusan del Norte', 'And all the cities...'])
old_cities.each do |c|
unless new_cities.blank?
City.update(c.id, :name => new_cities.first)
new_cities.delete(new_cities.first)
else
City.delete(c.id)
end
end
unless new_cities.blank?
new_cities.each do |c|
City.create(:name => c, :country_id => 'philippines.id')
end
end
end
def self.down
end
end

Related

How to apply "to_f" to a column for all results in an active record query inside rails console

I have this query:
p = Payment.where(:client_id => 1)
Then only some fields, so I execute this:
p.select(:id, :created_at, :amount)
The issue is that every amount in the result comes as BigDecimal, and it's not readable. I need to apply to_f function inside rails console to have a readable set of results.
I tried map,like this:
p.map {|p| p.amount.to_f}
But it returns to me an array only with the amounts, and I need it to be along with the other attributes.
You can try like this,
p.map { |item| { id: item.id, created_at: item.created_at, amount: item.amount.to_f}}
To solve you problem and get all fields:
p.map { |i| [i.id, i.created_at, i.amount.to_f] }
Also you can take a look on this article. Maybe you just need float type for amount field in your DB
if you always want float of amount you can set callback after_find
class Payment < ActiveRecord::Base
after_find do |payment|
self.amount = self.amount.to_f
end
end
it will always give you float value when you find object
It will not update value in your db.
Please Look on this Rails Callback

Nice array from pluck

I have a model and I love the pluck method I can use. If I do this:
#x = AwesomeModel.all.pluck(:column_one, :column_two)
then I get a multidimensional array: #x[][]. With my sad skills, I work with them using the numbers:
#x[0][1]
how can I can use pluck or a similar method to access the array something like this:
#x[0][:column_two]
If you are concerned about the structure of what you get back from the db, you should simply do:
#x = AwesomeModel.all.select(:column_one, :column_two)
Then you'd keep the fast db query advantage + have AwesomeModel instances, but with only column_one and column_two filled
Or if you desire to do it manually:
#x = AwesomeModel.all.pluck(:column_one, :column_two).map do |array|
OpenStruct.new({column_one: array[0], column_two: array[1] }) }
end
Then you can use it like a regular model:
#x[0].column_one
# or even
#x[0][:column_two]
You could do
class ActiveRecord::Base
def self.pluck_hash(*args)
plucked = pluck(*args)
plucked.map {|ary| Hash[args.zip ary]}
end
end
AwesomeModel.all.pluck_hash(:column_one, :column_two)
#=> [{:column_one => 'value', :column_two => 'value}, {...}, ... ]
First of all, don't use .all.pluck, because it returns an array of values, and that makes you loose all the advantages of ActiveRecord::Relation.
Instead use AwsomeModel.method directly, it would create the query but not run it until you need it, AwsomeModel.select(:column_1, :column_2) would create a
select (awesome_models.column_1, awsome_models.column_2)
query, and the result would be an array of ActiveRecord::Relation objects, which are still chainable, and values are still under keys of the column name eg:
AwsomeModel.select(:column_1, :column_2).first.column_1
Instead of
AwesomeModel.all.pluck(:column_1, :column_2).first[0] # or .first.first

Split one data row into multiple based on # of instances in one of the cells

Right now, when a user creates a Request object, the output looks like this:
<Request id: 1, email: "abc#yahoo.com", items: ["one item", "two item"], created_at: "2014-04-24 05:14:24", edit_id: "gwe3EX4q2EUVk7FQCRUJug">
I am trying to convert this so that if items > 1, the output is split into two Request instances like so:
<Request id: 1, email: "abc#yahoo.com", items: "one item", created_at: "2014-04-24 05:14:24", edit_id: "gwe3EX4q2EUVk7FQCRUJug">
<Request id: 2, email: "abc#yahoo.com", items: "two item", created_at: "2014-04-24 05:14:24", edit_id: "gwe3EX4q2EUVk7FQCRUJug">
What's complicating this further is that also I want the Request.id to increment as per usual, but NOT the created_at and edit_id, both of which are automatically generated when a Request.create is called.
How can I do this? The code snippet of the method I've worked on so far (certainly not working, I'm stumped)...
self.items.each_with_index do |item, index|
index = Request.save(:email => self.email, :items => item, :created_at => self.created_at, :edit_id => self.edit_id)
end
Thanks!
FYI the code for the edit_id method:
def Request.new_edit_id
SecureRandom.urlsafe_base64
end
def create_edit_id
self.edit_id = Request.new_edit_id
end
UPDATE:
Also, the clone or dup methods that create shallow copies don't work in this case. The first answer on this question: How do I copy a hash in Ruby? works for a simple array, but for a complex hash like what I have, changing the cloned copy will actually change the original as well.
I'm going to explore a deep copy and see if I can get that to work!
In the process of figuring this out, I learned that it's bad practice to store array data like this in an object. I should just be making another data table and relating the two.
That said, if any bit of this code is helpful, here's what did it:
#requestrecord.items = ["water filter", "tent"]
num_of_requests = #requestrecord.items.count
i = 0
cloned_request = Hash.new
while i < num_of_requests do
cloned_request[i] = Marshal.load(Marshal.dump(#requestrecord)) #creates a cloned_request[0] = #requestrecord and cloned_request[1] = #requestrecord
cloned_request[i].items = #requestrecord.items.slice(i) #disaggregates the items into the cloned_requests; cloned_request[0].items = "water filter", cloned_request[1].items = "tent"
i += 1
end
Note the Marshal.load(Marshal.dump(#requestrecord)) creates a deep copy and therefore works where clone and dup would not. (They would create references to the original hash. If the original hash changed, so would they. Moreover, I found out that in complex hashes like what I was building, changing a cloned hash actually changes the original as well!)

rails increment non-primary ID column in after create callback

I'm trying to create a record and increment two non primary id columns in an after create callback with a integer and a date. The first column needs to be auto incremented with an integer from 1 to given number. The second column needs to be incremented with a date from the start date specified and by either 7, 14, or 30 days which is also specified. I tried creating the first record with the first value then incrementing from there but all records just have the same integer or date saved.
Here's the code
def create_positions
#slots = (self.slots - 1)
#payout_date = (self.start_date)
#position = Position.create(:susu_id => self.id, :user_id => 2, :position_number => 1, :pay_in => self.contribution, :payout_date => (#payout_date + self.frequency_in_days.days))
#positions= self.positions.map { |p| p["position_number"] }.last.to_i
#position_number = (#positions += 1)
#dates = self.positions.map { |d| d["payout_date"] }.last
#add_date = (#dates + self.frequency_in_days.days)
#all_positions = #slots.times {Position.create(:susu_id => self.id, :user_id => 2, :position_number => #position_number, :pay_in => self.contribution, :payout_date => #add_date) }
end
What i get is: 1 2 2 2 and 1/2/14 1/9/14 1/9/14 1/9/14
instead of: 1 2 3 4 and 1/2/14 1/9/14 1/16/14 1/23/14
My lack of reputation prohibits me from commenting on your original post, so I'll just have to do my best to answer your question without asking for clarification (for I don't fully understand your explanation of what you're trying to do).
It sounds like you are trying to create four Position instances, each subsequent position having a position_number that is one greater than the previous one and having a payout_date that is 7 days later than the previous one.
If this is what you're trying to do, please consider the following:
# in the top of your susu model:
after_create :create_positions
# later in your susu model
def create_positions
4.times do |n|
Position.create(susu: self,
user_id: 2,
position_number: n,
pay_in: contribution,
payout_date: start_date + n * frequency_in_days.days)
end
end
You have a lot of logic in your version of this method, so let me know if I have missed something that you need.
And finally, let me point out a few things about my version of this code:
It uses Ruby 1.9 hash syntax ("susu: self" instead of ":susu => self")
It removes unnecessary calls to self, which is considered bad ruby style
It still has hardcoded user_id: 2, which seems like you're going to need to change at some point
Doing this in an after_create hook will be in the same transaction as the original object being created. This means that if a Position cannot be created for whatever reason (e.g., a database-level unique key or a validates_uniqueness_of on the Position model failing), it will roll back the creation of the original object. If you don't like that behavior you can use the after_commit callback.
Cheers!

Get columns names with ActiveRecord

Is there a way to get the actual columns name with ActiveRecord?
When I call find_by_sql or select_all with a join, if there are columns with the same name, the first one get overridden:
select locations.*, s3_images.* from locations left join s3_images on s3_images.imageable_id = locations.id and s3_images.imageable_type = 'Location' limit 1
In the example above, I get the following:
#<Location id: 22, name: ...
>
Where id is that of the last s3_image. select_rows is the only thing that worked as expected:
Model.connection.select_rows("SELECT id,name FROM users") => [["1","amy"],["2","bob"],["3","cam"]]
I need to get the field names for the rows above.
This post gets close to what I want but looks outdated (fetch_fields doesn't seem to exist anymore How do you get the rows and the columns in the result of a query with ActiveRecord? )
The ActiveRecord join method creates multiple objects. I'm trying to achieve the same result "includes" would return but with a left join.
I am attempting to return a whole lot of results (and sometimes whole tables) this is why includes does not suit my needs.
Active Record provides a #column_names method that returns an array of column names.
Usage example: User.column_names
two options
Model.column_names
or
Model.columns.map(&:name)
Example
Model named Rabbit with columns name, age, on_facebook
Rabbit.column_names
Rabbit.columns.map(&:name)
returns
["id", "name", "age", "on_facebook", "created_at", "updated_at"]
This is just way active record's inspect method works: it only lists the column's from the model's table. The attributes are still there though
record.blah
will return the blah attribute, even if it is from another table. You can also use
record.attributes
to get a hash with all the attributes.
However, if you have multiple columns with the same name (e.g. both tables have an id column) then active record just mashes things together, ignoring the table name.You'll have to alias the column names to make them unique.
Okay I have been wanting to do something that's more efficient for a while.
Please note that for very few results, include works just fine. The code below works better when you have a lot of columns you'd like to join.
In order to make it easier to understand the code, I worked out an easy version first and expanded on it.
First method:
# takes a main array of ActiveRecord::Base objects
# converts it into a hash with the key being that object's id method call
# loop through the second array (arr)
# and call lamb (a lambda { |hash, itm| ) for each item in it. Gets called on the main
# hash and each itm in the second array
# i.e: You have Users who have multiple Pets
# You can call merge(User.all, Pet.all, lambda { |hash, pet| hash[pet.owner_id].pets << pet }
def merge(mainarray, arr, lamb)
hash = {}
mainarray.each do |i|
hash[i.id] = i.dup
end
arr.each do |i|
lamb.call(i, hash)
end
return hash.values
end
I then noticed that we can have "through" tables (nxm relationships)
merge_through! addresses this issue:
# this works for tables that have the equivalent of
# :through =>
# an example would be a location with keywords
# through locations_keywords
#
# the middletable should should return as id an array of the left and right ids
# the left table is the main table
# the lambda fn should store in the lefthash the value from the righthash
#
# if an array is passed instead of a lefthash or a righthash, they'll be conveniently converted
def merge_through!(lefthash, righthash, middletable, lamb)
if (lefthash.class == Array)
lhash = {}
lefthash.each do |i|
lhash[i.id] = i.dup
end
lefthash = lhash
end
if (righthash.class == Array)
rhash = {}
righthash.each do |i|
rhash[i.id] = i.dup
end
righthash = rhash
end
middletable.each do |i|
lamb.call(lefthash, righthash, i.id[0], i.id[1])
end
return lefthash
end
This is how I call it:
lambmerge = lambda do |lhash, rhash, lid, rid|
lhash[lid].keywords << rhash[rid]
end
Location.merge_through!(Location.all, Keyword.all, LocationsKeyword.all, lambmerge)
Now for the complete method (which makes use of merge_through)
# merges multiple arrays (or hashes) with the main array (or hash)
# each arr in the arrs is a hash, each must have
# a :value and a :proc
# the procs will be called on values and main hash
#
# :middletable will merge through the middle table if provided
# :value will contain the right table when :middletable is provided
#
def merge_multi!(mainarray, arrs)
hash = {}
if (mainarray.class == Hash)
hash = mainarray
elsif (mainarray.class == Array)
mainarray.each do |i|
hash[i.id] = i.dup
end
end
arrs.each do |h|
arr = h[:value]
proc = h[:proc]
if (h[:middletable])
middletable = h[:middletable]
merge_through!(hash, arr, middletable, proc)
else
arr.each do |i|
proc.call(i, hash)
end
end
end
return hash.values
end
Here's how I use my code:
def merge_multi_test()
merge_multi!(Location.all,
[
# each one location has many s3_images (one to many)
{ :value => S3Image.all,
:proc => lambda do |img, hash|
if (img.imageable_type == 'Location')
hash[img.imageable_id].s3_images << img
end
end
},
# each location has many LocationsKeywords. Keywords is the right table and LocationsKeyword is the middletable.
# (many to many)
{ :value => Keyword.all,
:middletable => LocationsKeyword.all,
:proc => lambda do |lhash, rhash, lid, rid|
lhash[lid].keywords << rhash[rid]
end
}
])
end
You can modify the code if you wish to lazy load attributes that are one to many (such as a City is to a Location) Basically, the code above won't work because you'll have to loop through the main hash and set the city from the second hash (There is no "city_id, location_id" table). You could reverse the City and Location to get all the locations in the city hash then extract back. I don't need that code yet so I skipped it =)

Resources