I have looked over the other posts about creating a CSV with Paperclip but am still a bit lost on why this isn't working. I have a method that generates a CSV string (using CSV.generate), and I try to save it to a Report in the Reports Controller with the following method:
def create(type)
case type
when "Geo"
csv_string = Report.generate_geo_report
end
#report = Report.new(type: type)
#report.csv_file = StringIO.new(csv_string)
if #report.save
puts #report.csv_file_file_name
else
#report.errors.full_messages.to_sentence
end
end
However, upon execution, I get a undefined method 'stringify_keys' for "Geo":String error. Here is the Report model:
class Report < ActiveRecord::Base
attr_accessible :csv_file, :type
has_attached_file :csv_file, PAPERCLIP_OPTIONS.merge(
:default_url => "//s3.amazonaws.com/production-recruittalk/media/avatar-placeholder.gif",
:styles => {
:"259x259" => "259x259^"
},
:convert_options => {
:"259x259" => "-background transparent -auto-orient -gravity center -extent 259x259"
}
)
def self.generate_geo_report
male_count = 0
female_count = 0
csv_string = CSV.generate do |csv|
csv << ["First Name", "Last Name", "Email", "Gender", "City", "State", "School", "Created At", "Updated At"]
Athlete.all.sort_by{ |a| a.id }.each do |athlete|
first_name = athlete.first_name || ""
last_name = athlete.last_name || ""
email = athlete.email || ""
if !athlete.sports.blank?
if athlete.sports.first.name.split(" ", 2).first.include?("Women's")
gender = "Female"
female_count += 1
else
gender = "Male"
male_count += 1
end
else
gender = ""
end
city = athlete.city_id? ? athlete.city.name : ""
state = athlete.state || ""
school = athlete.school_id? ? athlete.school.name : ""
created_at = "#{athlete.created_at.to_date.to_s[0..10].gsub(" ", "0")} #{athlete.created_at.to_s.strip}"
updated_at = "#{athlete.updated_at.to_date.to_s[0..10].gsub(" ", "0")} #{athlete.updated_at.to_s.strip}"
csv << [first_name, last_name, email, gender, city, state, school, created_at, updated_at]
end
csv << []
csv << []
csv << ["#{male_count}/#{Athlete.count} athletes are men"]
csv << ["#{female_count}/#{Athlete.count} athletes are women"]
csv << ["#{Athlete.count-male_count-female_count}/#{Athlete.count} athletes have not declared a gender"]
end
return csv_string
end
end
This is being called from a cron job rake task:
require 'csv'
namespace :reports do
desc "Geo-report"
task :generate_nightly => :environment do
Report.create("Geo")
end
end
Not sure where to begin on getting this functional. Any suggestions? I've been reading Paperclip's doc but I'm a bit of a newbie to it.
Thank you!
There's a lot going on here :)
First, it looks like you're getting your controller and model confused. In the rake task, Report is the model, but you're calling create as if it was the controller method. Models (aka ActiveRecord classes) take a key/value pair:
Report.create(type: "Geo")
Another issue is that you're using "type" for the name of your column, and this will tell ActiveRecord that you're using single table inheritance. That means that you have subclasses of Report. Unless you really want STI, you should rename this column.
Finally, you shouldn't have a controller method that takes an argument. I'm not really sure what you're trying to do there, but controller get their arguments via the params hash.
Related
I have an array of objects. I am trying to create CSV data and allow the user to download that file but I get the following error:
Undefined method 'first_name' for Hash:0x007f946fc76590
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
This is the button that allows a user to download the CSV:
<%= link_to "Download Employee CSV", download_employee_csv_path %>
Controller:
def download_employee_csv
employee_csv_data = []
employees.each do |employee|
employee_csv_data << {
first_name: employee[:first_name],
last_name: employee[:last_name],
email: employee_email,
phone1: employee[:phone1],
gender: employee[:gender],
veteran: employee[:veteran].to_s,
dob: employee[:dob],
core_score: service_score,
performance_rank: rank,
industry_modules_passed: industry_modules_passed
}
end
respond_to do |format|
format.html
format.csv { send_data Employer.to_csv(employee_csv_data), filename: "download_employee_csv.csv" }
end
end
employee_csv_data:
=> [{:first_name=>"Christopher",
:last_name=>"Pelnar",
:email=>"pelnar#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1988-09-09",
:core_score=>"No Score",
:performance_rank=>"No Rank",
:industry_modules_passed=>"No Industry Modules Passed"},
{:first_name=>"chris",
:last_name=>"pelnar",
:email=>"chris#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1998-09-09",
:core_score=>"729",
:performance_rank=>"Good",
:industry_modules_passed=>"Entry-Service, Entry-Tech"}]
Model:
def self.to_csv(employee_csv_data)
attributes = %w(first_name last_name email phone gender veteran dob core_score performance_rank industry_modules_passed)
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
When I click the button, it takes me to the blank HTML page without any problem. When I add .csv to the filename in the URL on that page I get the error.
It looks like it's an array of Hashes. To access properties of a hash in Ruby you need to use brackets. Try updating your code to this:
csv << attributes.map{ |attr| obj.send([], attr) }
or more concisely:
csv << attributes.map{ |attr| obj[attr] }
One more thing, in the example you provided, the keys in the hash are symbols which means you may need to convert your attributes to symbols when trying to access them, like this:
csv << attributes.map{ |attr| obj[attr.to_sym] }
I adapted #Ctpelnar1988's answer to determine the attributes dynamically and allow each array item to have different columns:
def array_of_hashes_to_csv(array)
array_keys = array.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << array_keys
array.each do |obj|
csv << array_keys.map{ |attr| obj[attr] }
end
end
end
Example:
puts array_of_hashes_to_csv([
{attr_a: 1, attr_b: 2},
{attr_a: 3, attr_c: 4}
])
attr_a,attr_b,attr_c
1,2,
3,,4
In the more specific "employee_csv_data" context, I think it'd look like this:
def self.to_csv(employee_csv_data)
attributes = employee_csv_data.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map { |attr| obj[attr] }
end
end
end
I am trying to build a dynamic querying method to filter search results.
My models:
class Order < ActiveRecord::Base
scope :by_state, -> (state) { joins(:states).where("states.id = ?", state) }
scope :by_counsel, -> (counsel) { where("counsel_id = ?", counsel) }
scope :by_sales_rep, -> (sales) { where("sales_id = ?", sales) }
scope :by_year, -> (year) { where("title_number LIKE ?", "%NYN#{year}%") }
has_many :properties, :dependent => :destroy
has_many :documents, :dependent => :destroy
has_many :participants, :dependent => :destroy
has_many :states, through: :properties
belongs_to :action
belongs_to :role
belongs_to :type
belongs_to :sales, :class_name => 'Member'
belongs_to :counsel, :class_name => 'Member'
belongs_to :deal_name
end
class Property < ActiveRecord::Base
belongs_to :order
belongs_to :state
end
class State < ActiveRecord::Base
has_many :properties
has_many :orders, through: :properties
end
I have a page where I display ALL orders by default. I want to have check boxes to allow for filtering of the results. The filters are: Year, State, Sales, and Counsel. an example of a query is: All orders in 2016, 2015("order.title_number LIKE ?", "%NYN#{year}%") in states (has_many through) NJ, PA, CA, etc with sales_id unlimited ids and counsel_id unlimited counsel_ids.
In a nut shell I am trying to figure out how to create ONE query that takes into account ALL options the user checks. Here is my current query code:
def Order.query(opt = {})
results = []
orders = []
if !opt["state"].empty?
opt["state"].each do |value|
if orders.empty?
orders = Order.send("by_state", value)
else
orders << Order.send("by_state", value)
end
end
orders = orders.flatten
end
if !opt["year"].empty?
new_orders = []
opt["year"].each do |y|
new_orders = orders.by_year(y)
results << new_orders
end
end
if !opt["sales_id"].empty?
end
if !opt["counsel_id"].empty?
end
if !results.empty?
results.flatten
else
orders.flatten
end
end
Here is the solution I have come up with to allow for unlimited amount of filtering.
def self.query(opts = {})
orders = Order.all
opts.delete_if { |key, value| value.blank? }
const_query = ""
state_query = nil
counsel_query = nil
sales_query = nil
year_query = nil
queries = []
if opts["by_year"]
year_query = opts["by_year"].map do |val|
" title_number LIKE '%NYN#{val}%' "
end.join(" or ")
queries << year_query
end
if opts["by_sales_rep"]
sales_query = opts["by_sales_rep"].map do |val|
" sales_id = '#{val}' "
end.join(" or ")
queries << sales_query
end
if opts["by_counsel"]
counsel_query = opts["by_counsel"].map do |val|
" counsel_id = '#{val}' "
end.join(" or ")
queries << counsel_query
end
if opts["by_state"]
state_query = opts["by_state"].map do |val|
"states.id = '#{val}'"
end.join(" or ")
end
query_string = queries.join(" AND ")
if state_query
#orders = Order.joins(:states).where("#{state_query}")
#orders = #orders.where(query_string)
else
#orders = orders.where("#{query_string}")
end
#orders.order("title_number DESC")
end
What you're looking for a query/filter object, which is a common pattern. I wrote an answer similar to this, but I'll try to extract the important parts.
First you should move those logic to it's own object. When the search/filter object is initialized it should start with a relation query (Order.all or some base query) and then filter that as you go.
Here is a super basic example that isn't fleshed out but should get you on the right track. You would call it like so, orders = OrderQuery.call(params).
# /app/services/order_query.rb
class OrderQuery
def call(opts)
new(opts).results
end
private
attr_reader :opts, :orders
def new(opts={})
#opts = opts
#orders = Order.all # If using Rails 3 you'll need to use something like
# Order.where(1=1) to get a Relation instead of an Array.
end
def results
if !opt['state'].empty?
opt['state'].each do |state|
#orders = orders.by_state(state)
end
end
if !opt['year'].empty?
opt['year'].each do |year|
#orders = orders.by_year(year)
end
end
# ... all filtering logic
# you could also put this in private functions for each
# type of filter you support.
orders
end
end
EDIT: Using OR logic instead of AND logic
# /app/services/order_query.rb
class OrderQuery
def call(opts)
new(opts).results
end
private
attr_reader :opts, :orders
def new(opts={})
#opts = opts
#orders = Order.all # If using Rails 3 you'll need to use something like
# Order.where(1=1) to get a Relation instead of an Array.
end
def results
if !opt['state'].empty?
#orders = orders.where(state: opt['state'])
end
if !opt['year'].empty?
#orders = orders.where(year: opt['year'])
end
# ... all filtering logic
# you could also put this in private functions for each
# type of filter you support.
orders
end
end
The above syntax basically filters sayings if state is in this array of states and year is within this array of years.
In my case, the filter options came from the Controller's params, so I've done something like this:
The ActionController::Parameters structure:
{
all: <Can be true or false>,
has_planned_tasks: <Can be true or false>
... future filters params
}
The filter method:
def self.filter(filter_params)
filter_params.reduce(all) do |queries, filter_pair|
filter_key = filter_pair[0]
filter_value = filter_pair[1]
return {
all: proc { queries.where(deleted_at: nil) if filter_value == false },
has_planned_tasks: proc { queries.joins(:planned_tasks).distinct if filter_value == true },
}.fetch(filter_key).call || queries
end
end
Then I call the ModelName.filter(filter_params.to_h) in the Controller. I was able to add more conditional filters easily doing like this.
There's space for improving here, like extract the filters logic or the whole filter object, but I let you decide what is better in your context.
Here is one I built for an ecommerce order dashboard in Rails with the parameters coming from the controller.
This query will execute twice, once to count the orders and once to return the requested orders according to the parameters in the request.
This query supports:
Sort by column
Sort direction
Incremental Search - It'll search the beginning of a given field and returns those records that match enabling real-time suggestions while searching
Pagination (limited by 100 records per page)
I also have predefined values to sanitize some of the data.
This style is extremely clean and easy for others to read and modify.
Here's a sample query:
api/shipping/orders?pageNumber=1&orderStatus=unprocessedOrders&filters=standard,second_day&stores=82891&sort_column=Date&sort_direction=dsc&search_query=916
And here's the controller code:
user_id = session_user.id
order_status = params[:orderStatus]
status = {
"unprocessedOrders" => ["0", "1", "4", "5"],
"processedOrders" => ["2", "3", "6"],
"printedOrders" => ["3"],
"ratedOrders" => ["1"],
}
services = [
"standard",
"expedited",
"next_day",
"second_day"
]
countries = [
"domestic",
"international"
]
country_defs = {
domestic: ['US'],
international: ['CA', 'AE', 'EU', 'GB', 'MX', 'FR']
}
columns = {
Number: "order_number",
QTY: "order_qty",
Weight: "weight",
Status: "order_status",
Date: "order_date",
Carrier: "ship_with_carrier",
Service: "ship_with_carrier_code",
Shipping: "requestedShippingService",
Rate: "cheapest_rate",
Domestic: "country",
Batch: "print_batch_id",
Skus: "skus"
}
# sort_column=${sortColumn}&sort_direction=${sortDirection}&search_query=${searchQuery}
filters = params[:filters].split(',')
stores = params[:stores].split(',')
sort_column = params[:sort_column]
sort_direction = params[:sort_direction]
search_query = params[:search_query]
sort_by_column = columns[params[:sort_column].to_sym]
sort_direction = params[:sort_direction] == "asc" ? "asc" : "desc"
service_params = filters.select{ |p| services.include?(p) }
country_params = filters.select{ |p| countries.include?(p) }
order_status_params = filters.select{ |p| status[p] != nil }
query_countries = []
query_countries << country_defs[:"#{country_params[0]}"] if country_params[0]
query_countries << country_defs[:"#{country_params[1]}"] if country_params[1]
active_filters = [service_params, country_params].flatten
query = Order.where(user_id: user_id)
query = query.where(order_status: status[order_status]) if order_status_params.empty?
query = query.where("order_number ILIKE ? OR order_id::TEXT ILIKE ? OR order_info->'advancedOptions'->>'customField2' ILIKE ?", "%#{search_query}%", "%#{search_query}%", "%#{search_query}%") unless search_query.gsub(/\s+/, "").length == 0
query = query.where(requestedShippingService: service_params) unless service_params.empty?
query = query.where(country: "US") if country_params.include?("domestic") && !country_params.include?("international")
query = query.where.not(country: "US") if country_params.include?("international") && !country_params.include?("domestic")
query = query.where(order_status: status[order_status_params[0]]) unless order_status_params.empty?
query = query.where(store_id: stores) unless stores.empty?\
order_count = query.count
num_of_pages = (order_count.to_f / 100).ceil()
requested_page = params[:pageNumber].to_i
formatted_number = (requested_page.to_s + "00").to_i
query = query.offset(formatted_number - 100) unless requested_page == 1
query = query.limit(100)
query = query.order("#{sort_by_column}": :"#{sort_direction}") unless sort_by_column == "skus"
query = query.order("skus[1] #{sort_direction}") if sort_by_column == "skus"
query = query.order(order_number: :"#{sort_direction}")
orders = query.all
puts "After querying orders mem:" + mem.mb.to_s
requested_page = requested_page <= num_of_pages ? requested_page : 1
options = {}
options[:meta] = {
page_number: requested_page,
pages: num_of_pages,
type: order_status,
count: order_count,
active_filters: active_filters
}
render json: OrderSerializer.new(orders, options).serialized_json
I've got below code:
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << %w{ id email title first_name last_name position work_phone company state industry mobile origin terms events roles booths }
all.each do |user|
events = '', roles = '', booths = ''
events = user.events.first.name.to_s if user.events.present?
roles = user.roles.first.name.to_s if user.roles.present?
booths = user.booths.first.name.to_s if user.booths.present?
csv << user.attributes.values_at("id", "email", "title", "first_name", "last_name", "position", "work_phone", "company", "state", "industry", "mobile", "origin", "terms")
csv << events
csv << roles
csv << booths
end
end
end
I want to be able to generate csv and add those values in the extra columns but I'm getting undefined method 'map' for "admin":String error.
Is there a way to append this to the csv on the same row?
CSV#<< says :
The primary write method for wrapped Strings and IOs, row (an Array or CSV::Row) is converted to CSV and appended to the data source. When a CSV::Row is passed, only the row’s fields() are appended to the output.
But you are passing stirngs. see below :
csv << events # string
csv << roles # string
csv << booths # string
Tried to replicate the erro :
require 'csv'
a = CSV.generate("") do |csv|
csv << "foo"
end
# `<<': undefined method `map' for "foo":String (NoMethodError)
Here is a fix :
require 'csv'
a = CSV.generate("") do |csv|
csv << ["foo"] # just wrapped the string into an Array as doc is saying.
end
a # => "foo\n"
Write your code as :
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << %w{ id email title first_name last_name position work_phone company state industry mobile origin terms events roles booths }
all.each do |user|
ary = %w[events,roles,booths].map do |item|
user.send(item).first.name if user.send(item).present?
end
row = user.attributes.values_at("id", "email", "title", "first_name", "last_name", "position", "work_phone", "company", "state", "industry", "mobile", "origin", "terms")
row.push(*ary)
csv << row
end
end
end
When you append to csv it's expecting an array that represents a row or a CSV::Row object. First, build the array, then append that to csv as follows:
row = user.attributes.values_at("id", "email", "title", "first_name", "last_name", "position", "work_phone", "company", "state", "industry", "mobile", "origin", "terms")
row << events
row << roles
row << booths
csv << row
I have a piece of code that seems to work. I just think there may be a much better way to achieve the desired work. The problem is to build an ActiveRecord query with an unknown list of parameters.
Here is the code:
query_string = String.new
query_values = []
unless params[:organization][:name].blank?
query_string << 'name = ?'
query_values << params[:organization][:name]
end
unless params[:organization][:national_id].blank? && params[:organization][:vat_id].blank?
raise RequestParamsException.new('National ID or Vat ID given without country') if params[:organization][:country].nil?
country_id = Country.find_by_name(params[:organization][:country]).pluck(:id)
unless params[:organization][:national_id].blank?
query_string << ' OR ' unless query_string.empty?
query_string << '(national_id = ?'
query_values << params[:organization][:national_id]
query_string << ' AND ' << 'country_id = ?)'
query_values << country_id
end
unless params[:organization][:vat_id].blank?
query_string << ' OR ' unless query_string.empty?
query_string << '(vat_id = ?'
query_values << params[:organization][:vat_id]
query_string << ' AND ' << 'country_id = ?)'
query_values << country_id
end
end
known_organizations = query_string.blank? ? [] : Organization.where(query_string, query_values).uniq
Country is needed when a Vat or National Id are given since these are scoped in the model:
class Organization < ActiveRecord::Base
#======================VALIDATIONS=========================
validates :national_id, :uniqueness => { :scope => :country_id }, :allow_blank => true
validates :vat_id, :uniqueness => { :scope => :country_id }, :allow_blank => true
validates :country, :presence => true
end
You can take advantage of Arel. For example when you write:
posts = Post.where(author_id: 12)
this query isn't executed unless you start iterating on posts or call posts.all. So you can write something like this:
def search_posts
posts = Post.where(active: true)
posts = posts.where('body ilike ?', "%#{params[:query]%") unless params[:query].blank?
posts
end
This simple example shows how to achieve behavior you are looking for.
I have a method within my main model, which should return specific values based on the set params:
def self.retrieve_profiles( params )
case params[:view]
when 'image_only'
fields = %w{ avatar }
when 'profile_minimal'
fields = %w{ avatar username age }
when 'profile_medium'
fields = %w{ avatar username age first_name last_name bio relationship_status seeking }
when 'profile_extended'
fields = %w{ avatar username age first_name last_name bio relationship_status seeking country city photos }
end
profiles = Profile.that_has_photos
profiles = profiles.where( :country_id => params['country'] ) unless params['country'].nil?
profiles = profiles.order( "RAND()" ).limit( params['count'] )
# works fine up to here (returns all fields)
profiles.each do |a|
fields.each do |field|
puts a.eval(field)
end
end
# above block makes it fail (private method `eval' called)
end
My question is: how do I return only the values specified by the fields hash ?
Use send instead of eval. This code should work fine.
profiles.each do |a|
fields.each do |field|
puts a.send(field)
end
end