require "csv" module Dunlop::CsvBuilder extend ActiveSupport::Concern included do attr_reader :resource, :options, :csv_options, :controller class_attribute :default_csv_options self.default_csv_options = {} end def initialize(resource, options = {}) @resource, @options = resource, options @csv_options = default_csv_options.merge(options.slice(:col_sep, :row_sep, :encoding, :field_size_limit, :quote_char, :force_quotes)) @controller = options[:controller] end def file_name options[:file_name] || "#{Rails.application.config.application_name}-#{date_number}-data.csv" end def headers [] end def for_each_serialized_row(&block) iterate_over = iteration_resource iterate_over = Array.wrap(iterate_over) unless iterate_over.respond_to?(:find_each_ordered) || iterate_over.respond_to?(:each) # allow resource to be a record using the index's csv builder iteration_method = iterate_over.respond_to?(:find_each_ordered) ? :find_each_ordered : :each iterate_over.public_send(iteration_method) do |*args| block.call serialize_row(*args) end end def serialize_row(record) [record.id] end def data # use #to_csv for base_class builders if present # in custom class implement" # def data # resource.to_csv # end # since Arrays implement #to_csv, but we do not want to use it in this case we explicitly exclude it return resource.to_csv if self.class == CsvBuilder and resource.present? and resource.respond_to?(:to_csv) and !resource.is_a?(Array) # try iteration otherwise str = CSV.generate **csv_options do |csv| csv << headers if headers.present? for_each_serialized_row do |serialized_row| csv << serialized_row end end str end def date_number Date.current.to_s(:number) end # This method can be overridden in order to use another iteration source than the resource eg: # resouce = batch # def iteration_resource # resource.workflow_instances # end def iteration_resource resource end #TODO: this is a separate include, not part of the core CsvBuilder object def workflow_instances_in_batch resource.workflow_instances.for_filtered_export end # Do a lookup on a record of an attribute and map the value to the # csv required version if appliccable. At the moment booleans are # converted to zeros and ones def record_attribute(record, attribute_name) result = record.public_send(attribute_name) case result when TrueClass then 1 when FalseClass then 0 when String then result.strip.gsub(/\r\n?|\n/, " ") when DayTimeBase then result.number else result end end # Make some options available as methods %i[action_name controller_path].each do |option_method| define_method option_method do options[option_method] end end module ClassMethods # find the csv builder for the given route spec def find_for_route(route_spec) find_for_route_lookup_order(route_spec).lazy.map(&:safe_constantize).find(&:itself) end # separate method since this is the important method to test, so extracted from the main method def find_for_route_lookup_order(route_spec) controller_path, action_name = route_spec.split('#') controller_parts = controller_path.split('/') if controller_parts.size < 2 parts = controller_parts.map(&:classify) else parts = controller_parts[0..-2].map(&:camelize) parts << controller_parts.last.classify end # Create a lookup tree that uses all the namespaces, followed by the actoin name # and ending with CsvBuilder for consitancy. Aka: if the route is: # workflow_instances/scenario1s#show # the lookup_order should be: # ["WorkflowInstanceBatch::Scenario1::ShowCsvBuilder", "WorkflowInstanceBatch::ShowCsvBuilder"] action_postfix = "::#{action_name.camelize}CsvBuilder" parts.map.with_index{|p, i| parts[0..(i - 1)].join('::') + action_postfix} end end end