Rails 4 how to call accessible_attributes from Model - ruby-on-rails

Hi I'm trying this tuto on Rails cast (Importing csv Excel file). And I'm having an error on this line product.attributes = row.to_hash.slice(*accessible_attributes)
undefined local variable or methodaccessible_attributes' for #`
this is my model.
class Product < ActiveRecord::Base
require 'roo'
validates_presence_of :price
#attr_accessible :name, :price, :released_on #I removed this line (rails 4)
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << column_names
all.each do |product|
csv << product.attributes.values_at(*column_names)
end
end
end
def self.import(file)
spreadsheet = open_spreadsheet(file)
header = spreadsheet.row(1)
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
product = find_by_id(row["id"]) || new
product.attributes = row.to_hash.slice(*accessible_attributes)
product.save!
end
end
def self.open_spreadsheet(file)
case File.extname(file.original_filename)
when ".csv" then Roo::Csv.new(file.path)
when ".xls" then Roo::Csv.new(file.path)
when ".xlsx" then Roo::Csv.new(file.path)
else raise "Unknown file type: #{file.original_filename}"
end
end
end
end on my controller I defined the product_params
def product_params
params.require(:product).permit(:name, :price, :released_on)
end
my csv that I'm trying to import looks like this:
id,name,realased_on,price,created_at,updated_at
1,Acoustic Guitar,2012-10-03,3456.54,2013-12-09 00:00:23 UTC, 2012-12-08 23:45:46 UTC

In working on this Railscast under Rails 4.1, I got the method to work by replacing accessible_attributes with row.to_hash.keys.
def self.import(file)
spreadsheet = open_spreadsheet(file)
header = spreadsheet.row(1)
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
product = find_by_id(row["id"]) || new
product.attributes = row.to_hash.slice(*row.to_hash.keys)
product.save!
end
end

Actually accessible_attributes fetch that columns which are declare attr_accessible in model but in rails 4 they have removed the attr_accessible from model and used the strong_parameter instead of that so for this make an method of same name accessible_attributes in his model then inside that method declare that columns array which are you want. Such as:
def accessible_attributes
[col1_name, col2_name, ....]
end

Using the accepted solution as is didn't work out for me. I needed to add "self." to the method definition to make it work.
Another way, if the only place you are calling accessible_attributes is within self.import, is defining the fields you are looking for as a local array variable.
def self.import(file)
accessible_attributes = ['my_attribute_name1','my_attribute_name2', '...']
spreadsheet = open_spreadsheet(file)
header = spreadsheet.row(1)
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
cae = find_by_id(row["id"]) || new
cae.attributes = row.to_hash.slice(*accessible_attributes)
cae.save!
end
end

To answer the previous question that why validation message shows "Price can't be blank". I think it may due to that the data is saved as General format in Excel. I encountered a similar issue and the error message went away after I changed it to text format. I don't know the reason but give it a try.

Related

How do I add a custom column to my CSV.generate endpoint in Rails app?

I followed this example but I'm having issues with a nil value showing up in each row (in between the original values and the additional value I'm adding.
Here's my controller endpoint:
def daily_grocery_carts_overview_export
#data = DailyRetailerShop.all.order("start_date ASC")
respond_to do |format|
format.html { redirect_to root_path }
format.csv { send_data #data.to_csv, filename: "DailyGroceryCarts-#{Time.now.strftime("%Y%m%d%H%M%S")}.csv" }
end
end
Here's my model:
class DailyRetailerShop < ActiveRecord::Base
def self.to_csv
# generate site abbreviations & name to add to CSV file
site_abbreviations = {}
Partner.all.each do |p|
site_abbreviations[p[:site_abbreviation]] = p[:name]
end
CSV.generate do |csv|
# remove certain columns
export_columns = column_names - %w(id site_abbreviation created_at updated_at)
# add custom column header
headers = export_columns << 'Website'
# add to csv file
csv << headers
all.each do |item|
row = item.attributes.values_at(*export_columns).insert(-1, site_abbreviations[item.site_abbreviation])
csv << row
end
end
end
end
When I download the CSV file and open it up I see a blank value followed by the custom value I appended in each row. If I read the downloaded CSV file, here's what I see in the first few rows:
data = CSV.read("downloaded_file.csv")
puts data[0..3]
=> [["start_date", "grocery_retailer", "retailer_shops", "Website"], ["2019-10-15", "walmart", "25", nil, "Website1"], ["2019-10-15", "walmart", "24", nil, "Website2"], ["2019-10-15", "instacart", "23", nil, "Website3"]]
Notice there is a nil value for each row (not in the headers). If I exclude my custom header name and then append values as I did above those nil values (blanks when I open the file) are no longer there.
So, it appears the custom header creating a nil value for each row. How do I get rid of that?
I never found out why those nil values are being included in each row but I figured out how to restrict them from showing up. Here's how I modified the function within my model:
class DailyRetailerShop < ActiveRecord::Base
def self.to_csv
# generate site abbreviations & name to add to CSV file
site_abbreviations = {}
Partner.all.each do |p|
site_abbreviations[p[:site_abbreviation]] = p[:name]
end
CSV.generate do |csv|
# remove certain columns
export_columns = column_names - %w(id site_abbreviation created_at updated_at)
# add custom column header
headers = export_columns << 'Website'
# add to csv file
csv << headers
all.each do |item|
# exclude the last item before inserting site name (custom header)
row = item.attributes.values_at(*export_columns)[0..-2].insert(-1, site_abbreviations[item.site_abbreviation])
csv << row
end
end
end
end
Hope this helps someone in the future!

Is there any optimized way to export CSV of more than 100K record using Rails?

I have 200k locations in my database. So I want to export all the locations into CSV format. While doing this it is taking too much time to download. What is the best way to optimize code in rails?
In controller:
def index
all_locations = Location.all
respond_to do |format|
format.csv { send_data all_locations.to_csv, filename: "locations-#{Date.today}.csv" }
end
end
In model
def self.to_csv
attributes = %w{id city address}
CSV.generate(headers: true) do |csv|
csv << ['Id', 'City', 'Address']
all.each do |location|
csv << attributes.map{ |attr| location.send(attr) }
end
end
end
I ran your code with some adjustments with my own data. I made the following changes, and using benchmarking I came to a 7x increase.
Your model:
def self.to_csv
attributes = %w{id city address}
CSV.generate(headers: true) do |csv|
csv << ['Id', 'City', 'Address']
all.pluck(attributes).each { |data| csv << data }
end
end
By using pluck you only get the data you want, and then you push all that data into the csv array.
if you are using Postgresql then you can use this in application_record.rb
def self.to_csv_copy(attrs="*", header=[])
rc = connection.raw_connection
rv = header.empty? ? [] : ["#{header.join(',')}\n"]
sql = self.all.select(attrs).to_sql
rc.copy_data("copy (#{sql}) to stdout with csv") do
# rubocop:disable AssignmentInCondition
while line = rc.get_copy_data
rv << line
end
end
rv.join
end
and then do
Location.to_csv_copy(%w{id city address}, ['Id', 'City', 'Address'])
It is even faster than the above solution.

rails Import csv file, but saving blank

My github address https://github.com/ParkHyunDo/ImportCsv
I am studying how to import an excel file using roo. The import works fine, but everything is blank.
Like this....
Here is my code
product.rb
class Product < ActiveRecord::Base
acts_as_xlsx
def self.import(file)
spreadsheet = open_spreadsheet(file)
header = spreadsheet.row(1)
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
product = find_by_id(row["id"]) || new
product.attributes = row.to_hash.slice(accepts_nested_attributes_for)
product.save!
end
end
def self.open_spreadsheet(file)
case File.extname(file.original_filename)
# You're using a tab seperated file, so specify seperator as a tab with \t
when ".csv" then Roo::CSV.new(file.path, csv_options: {col_sep: "\t"})
when ".xls" then Roo::Excel.new(file.path)
when ".xlsx" then Roo::Excelx.new(file.path)
else raise "Unknown file type: #{file.original_filename}"
end
end
end
products_controller.rb
def import
Product.import(params[:file])
redirect_to root_url, notice: "Products imported."
end
Please help me!
This line seems strange:
product.attributes = row.to_hash.slice(accepts_nested_attributes_for)
The class method accepts_nested_attributes_for has a completely different purpose than listing the attribute names of Product. But you could use attribute_names. Try this:
product.attributes = row.to_hash.stringify_keys.slice(*attribute_names)
Note that stringify_keys might be unnecessary, depending on how exactly the hash returned by row.to_hash looks. Also note that slice takes a list of attribute names, not an array. The asterisk * allows us to use the elements of an array as individual arguments to a function.

Rails CSV Export - Adding custom field

I'm building an ecommerce app in Rails. I have a method to export the Order model with shipping details. The below method works as-is but I want to add a column into the export from another model.
My order model has a product_id which joins with the product table that has the product name. I want to add the product name. So something like order.product.name. How would I go about adding that into the below?
Here is my method in my order.rb:
def self.to_csv(orders)
wanted_columns = [:id, :shipname, :shipaddress, :shipcity, :shipstate, :shipzip]
CSV.generate do |csv|
csv << wanted_columns
orders.each do |order|
csv << order.attributes.values_at(*wanted_columns)
end
end
end
You can simply add it to your line, like so:
def self.to_csv(orders)
wanted_columns = [:id, :shipname, :shipaddress, :shipcity, :shipstate, :shipzip]
CSV.generate do |csv|
csv << wanted_columns.insert(-1, :product_name)
orders.each do |order|
csv << order.attributes.values_at(*wanted_columns).insert(-1, order.product.name)
end
end
end
The insert method inserts the given values before the element with the given index.
Negative indices count backwards from the end of the array, where -1 is the last element.
It returns the resulting array.
To simplify, in this case, this:
wanted_columns = [:id, :shipname, :shipaddress, :shipcity, :shipstate, :shipzip]
CSV.generate do |csv|
csv << wanted_columns.insert(-1, :product_name)
end
Should behave exactly the same as:
CSV.generate do |csv|
csv << [:id, :shipname, :shipaddress, :shipcity, :shipstate, :shipzip, :product_name]
end
The same principal applies to the array generated by:
order.attributes.values_at(*wanted_columns)
If this is not working, try the simplified example, and inspect the array for correctness prior to adding it to the array. You may additionally simplify:
orders.each do |order|
csv << order.attributes.values_at(*wanted_columns).insert(-1, order.product.name)
end
to:
csv << orders.first.attributes.values_at(*wanted_columns).insert(-1, orders.first.product.name)
For purposes of troubleshooting...

created_at attribute doesn't update when importing CSV

I tried to import a CSV file to my database. I need the app to
use the exact created_at attribute from the CSV file, but it won't work.
I see only Time.now.
What am I doing wrong?
The CSV import code in the model:
def self.import(file, current_user)
allowed_attributes = [ "id","created_at","updated_at"]
#current_user = current_user
CSV.foreach(file.path, headers: true) do |row|
energy = find_by_id(row["id"]) || new
h1 = { "user_id" => #current_user.id }
h2 = row.to_hash.slice(*accessible_attributes)
h3 = h1.merge(h2)
energy.attributes = h3
energy.save!
end
end
From the documentation:
attr_accessible(*args) public
Specifies a white list of model attributes that can be set via mass-assignment.
Mass-assignment will only set attributes in this list, to assign to the rest of attributes you can use direct writer methods.
You need to add to attr_accessible all the attributes you are willing to update, including created_at.

Resources