2020-04-21 11:21:10 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# Generates CSV when given a collection and a mapping.
|
|
|
|
#
|
|
|
|
# Example:
|
|
|
|
#
|
|
|
|
# columns = {
|
|
|
|
# 'Title' => 'title',
|
|
|
|
# 'Comment' => 'comment',
|
|
|
|
# 'Author' => -> (post) { post.author.full_name }
|
|
|
|
# 'Created At (UTC)' => -> (post) { post.created_at&.strftime('%Y-%m-%d %H:%M:%S') }
|
|
|
|
# }
|
|
|
|
#
|
|
|
|
# CsvBuilder.new(@posts, columns).render
|
|
|
|
#
|
|
|
|
class CsvBuilder
|
2020-05-15 02:08:40 -04:00
|
|
|
DEFAULT_ORDER_BY = 'id'.freeze
|
|
|
|
DEFAULT_BATCH_SIZE = 1000
|
2021-02-17 19:09:31 -05:00
|
|
|
PREFIX_REGEX = /^[=\+\-@;]/.freeze
|
2020-05-15 02:08:40 -04:00
|
|
|
|
2020-04-21 11:21:10 -04:00
|
|
|
attr_reader :rows_written
|
|
|
|
|
|
|
|
#
|
|
|
|
# * +collection+ - The data collection to be used
|
|
|
|
# * +header_to_hash_value+ - A hash of 'Column Heading' => 'value_method'.
|
|
|
|
#
|
|
|
|
# The value method will be called once for each object in the collection, to
|
|
|
|
# determine the value for that row. It can either be the name of a method on
|
|
|
|
# the object, or a lamda to call passing in the object.
|
|
|
|
def initialize(collection, header_to_value_hash)
|
|
|
|
@header_to_value_hash = header_to_value_hash
|
|
|
|
@collection = collection
|
|
|
|
@truncated = false
|
|
|
|
@rows_written = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
# Renders the csv to a string
|
|
|
|
def render(truncate_after_bytes = nil)
|
|
|
|
Tempfile.open(['csv']) do |tempfile|
|
|
|
|
csv = CSV.new(tempfile)
|
|
|
|
|
|
|
|
write_csv csv, until_condition: -> do
|
|
|
|
truncate_after_bytes && tempfile.size > truncate_after_bytes
|
|
|
|
end
|
|
|
|
|
|
|
|
if block_given?
|
|
|
|
yield tempfile
|
|
|
|
else
|
|
|
|
tempfile.rewind
|
|
|
|
tempfile.read
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def truncated?
|
|
|
|
@truncated
|
|
|
|
end
|
|
|
|
|
|
|
|
def rows_expected
|
|
|
|
if truncated? || rows_written == 0
|
|
|
|
@collection.count
|
|
|
|
else
|
|
|
|
rows_written
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def status
|
|
|
|
{
|
|
|
|
truncated: truncated?,
|
|
|
|
rows_written: rows_written,
|
|
|
|
rows_expected: rows_expected
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-05-15 02:08:40 -04:00
|
|
|
protected
|
|
|
|
|
|
|
|
def each(&block)
|
|
|
|
@collection.find_each(&block) # rubocop: disable CodeReuse/ActiveRecord
|
|
|
|
end
|
|
|
|
|
2020-04-21 11:21:10 -04:00
|
|
|
private
|
|
|
|
|
|
|
|
def headers
|
|
|
|
@headers ||= @header_to_value_hash.keys
|
|
|
|
end
|
|
|
|
|
|
|
|
def attributes
|
|
|
|
@attributes ||= @header_to_value_hash.values
|
|
|
|
end
|
|
|
|
|
|
|
|
def row(object)
|
|
|
|
attributes.map do |attribute|
|
|
|
|
if attribute.respond_to?(:call)
|
|
|
|
excel_sanitize(attribute.call(object))
|
|
|
|
else
|
|
|
|
excel_sanitize(object.public_send(attribute)) # rubocop:disable GitlabSecurity/PublicSend
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def write_csv(csv, until_condition:)
|
|
|
|
csv << headers
|
|
|
|
|
2020-05-15 02:08:40 -04:00
|
|
|
each do |object|
|
2020-04-21 11:21:10 -04:00
|
|
|
csv << row(object)
|
|
|
|
|
|
|
|
@rows_written += 1
|
|
|
|
|
|
|
|
if until_condition.call
|
|
|
|
@truncated = true
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def excel_sanitize(line)
|
|
|
|
return if line.nil?
|
2021-02-17 19:09:31 -05:00
|
|
|
return line unless line.is_a?(String) && line.match?(PREFIX_REGEX)
|
2020-04-21 11:21:10 -04:00
|
|
|
|
2021-02-17 19:09:31 -05:00
|
|
|
["'", line].join
|
2020-04-21 11:21:10 -04:00
|
|
|
end
|
|
|
|
end
|