debian-mirror-gitlab/lib/csv_builder.rb

131 lines
3.1 KiB
Ruby
Raw Normal View History

2020-04-22 19:07:51 +05:30
# 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
2021-04-29 21:17:54 +05:30
DEFAULT_ORDER_BY = 'id'
2020-05-24 23:13:21 +05:30
DEFAULT_BATCH_SIZE = 1000
2021-09-04 01:27:46 +05:30
PREFIX_REGEX = /\A[=\+\-@;]/.freeze
2020-05-24 23:13:21 +05:30
2020-04-22 19:07:51 +05:30
attr_reader :rows_written
#
# * +collection+ - The data collection to be used
# * +header_to_hash_value+ - A hash of 'Column Heading' => 'value_method'.
2023-04-23 21:23:45 +05:30
# * +associations_to_preload+ - An array of records to preload with a batch of records.
2020-04-22 19:07:51 +05:30
#
# 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.
2023-04-23 21:23:45 +05:30
def initialize(collection, header_to_value_hash, associations_to_preload = [])
2020-04-22 19:07:51 +05:30
@header_to_value_hash = header_to_value_hash
@collection = collection
@truncated = false
@rows_written = 0
2023-04-23 21:23:45 +05:30
@associations_to_preload = associations_to_preload
2020-04-22 19:07:51 +05:30
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-24 23:13:21 +05:30
protected
def each(&block)
2023-04-23 21:23:45 +05:30
if @associations_to_preload.present? && @collection.respond_to?(:each_batch)
@collection.each_batch(order_hint: :created_at) do |relation|
relation.preload(@associations_to_preload).order(:id).each(&block) # rubocop:disable CodeReuse/ActiveRecord
end
else
@collection.find_each(&block) # rubocop: disable CodeReuse/ActiveRecord
end
2020-05-24 23:13:21 +05:30
end
2020-04-22 19:07:51 +05:30
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-24 23:13:21 +05:30
each do |object|
2020-04-22 19:07:51 +05:30
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-04-17 20:07:23 +05:30
return line unless line.is_a?(String) && line.match?(PREFIX_REGEX)
2020-04-22 19:07:51 +05:30
2021-04-17 20:07:23 +05:30
["'", line].join
2020-04-22 19:07:51 +05:30
end
end