Skip to content

Instantly share code, notes, and snippets.

@joeljunstrom
Created July 2, 2020 08:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joeljunstrom/3b70337c4f38a2a7de104ef2d5d67ce9 to your computer and use it in GitHub Desktop.
Save joeljunstrom/3b70337c4f38a2a7de104ef2d5d67ce9 to your computer and use it in GitHub Desktop.
Exporting customers in the background example
# frozen_string_literal: true
class Exporters::Customers
attr_reader :file_export
def initialize(file_export)
@file_export = file_export
end
def call
Admin::CustomersCsv.new(query).generate
end
def filename
"customers-#{Time.current.to_s(:filename)}.csv"
end
private
def query
Customer.all
end
end
# frozen_string_literal: true
require "csv"
class Admin::CustomersCsv
HEADERS =
["id", "First name", "Last name", "Email", "Street", "Postal code", "Locality", "Country"].freeze
def initialize(scope)
@scope = scope
end
def generate(destination = Tempfile.new)
CSV.open(destination, "w", headers: HEADERS, write_headers: true) do |csv|
customer_query.find_each do |row|
csv << [
row.id,
row.first_name,
row.last_name,
row.email_address,
row.street_name,
row.postal_code,
row.locality,
CountryName.new(row.country_code).value
]
end
end
destination.rewind
destination
end
private
def customer_query
identity_table = Identity.arel_table
address_table = Address.arel_table
email_table = Email.arel_table
scope
.left_outer_joins(:identity, :address)
.joins(user: :email)
.select(
:id,
:user_id,
:identity_id,
:address_id,
email_table[:address].as("email_address"),
identity_table[:first_name],
identity_table[:last_name],
address_table[:street_name],
address_table[:postal_code],
address_table[:locality],
address_table[:country].as("country_code")
)
end
attr_reader :scope
end
# frozen_string_literal: true
class Exporters
# Receives a file export instance and looks up which exporter to use
# and calls it
class << self
def call(file_export)
for_file_export(file_export).call
end
def for_file_export(file_export)
klass =
case file_export.data_type
when "customers"
Exporters::Customers
when "product_subscriptions"
Exporters::ProductSubscriptions
else
raise ArgumentError, "No exporter class found for #{file_export.data_type}"
end
new(exporter: klass.new(file_export))
end
end
attr_reader :exporter
def initialize(exporter:)
@exporter = exporter
end
# Run the export and attach the resulting csv on the file export
def call
file = exporter.call
FileExport.transaction do
exporter
.file_export
.file
.attach(io: File.open(file.path), filename: exporter.filename)
exporter.file_export.update_column(:generated_at, Time.current)
end
exporter
ensure
file&.close
file&.unlink
end
end
# frozen_string_literal: true
class FileExport < ApplicationRecord
AVAILABLE_FOR = 1.day
has_one_attached :file
belongs_to :product, optional: true
class << self
def prepare!
FileExport.transaction do
FileExport.insert(
{data_type: "customers"},
returning: false,
unique_by: :index_file_exports_on_data_type
)
subscription_exports_attrs =
Product.pluck(:id).map { |id|
{data_type: "product_subscriptions", product_id: id}
}
if subscription_exports_attrs.any?
FileExport.insert_all(
subscription_exports_attrs,
returning: false,
unique_by: :index_file_exports_on_data_type_and_product_id
)
end
end
end
end
def available_until
(generated_at + AVAILABLE_FOR) if generated_at
end
def generating!
update_columns(generating: true, generated_at: nil)
end
def finished!
update_columns(generating: false)
end
end
# frozen_string_literal: true
class FileExportJob < ApplicationJob
sidekiq_options retry: 3
def perform(file_export_id)
file_export = FileExport.find_by(id: file_export_id)
return unless file_export
Exporters.call(file_export)
file_export.finished!
rescue
file_export.finished!
raise
end
end
class Admin::FileExportsController < Admin::AdminController
menu :exports
def index
FileExport.prepare!
exports =
FileExport
.includes(:product, :file_attachment, :file_blob)
.order(id: :asc)
.load
render locals: {exports: exports}
end
def generate
file_export = FileExport.find(params[:id])
file_export.generating!
FileExportJob.perform_later(file_export.id)
redirect_to admin_file_exports_path
end
end
# frozen_string_literal: true
class Exporters::ProductSubscriptions
delegate :product, to: :file_export
attr_reader :file_export
def initialize(file_export)
@file_export = file_export
end
def call
Admin::CustomersCsv.new(query).generate
end
def filename
"#{product.name.parameterize}-#{Time.current.to_s(:filename)}.csv"
end
private
def query
Customer
.joins(subscriptions: [:products])
.where(
subscriptions: {starts_at: (..Time.current), ends_at: (Time.current..)},
package_products: {product_id: product.id}
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment