xport 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6e0b568866d5b2f9c8680d14fe7266ec09b5eb32
4
+ data.tar.gz: 5051b3e1619135362ec775465583ba1b700621b1
5
+ SHA512:
6
+ metadata.gz: ef2450eb7859f679931eb58b9eaf05fa7d97cd90ca16f850b8190885be4d099dd2cd15b2c0c525df6058d21fb2a6cdd451ebb3ef49959e07a529ac2e755818ab
7
+ data.tar.gz: 01022f1cea1427d8faf3b512759c13a52a86910be75cc64cf44b38b822b8cc0535db9f3726041d038ddcb6453b50366aca1ba1a5d735a3803cbcb169a39b72cb
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def self.next_migration_number(dirname)
11
+ if ActiveRecord::Base.timestamped_migrations
12
+ Time.new.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ format("%.3d", current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def create_migrations
19
+ migration_template 'migration.rb', 'db/migrate/create_downloads.rb'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDownloads < ActiveRecord::Migration
4
+ def up
5
+ create_table :downloads do |t|
6
+ t.string :user_id
7
+ t.string :file_filename
8
+ t.string :filename
9
+ t.string :job_id
10
+ t.string :export_klass_name
11
+ t.string :export_model_name
12
+ t.text :query
13
+ t.integer :records_count
14
+ t.datetime :exported_at
15
+ t.string :export_additional_columns, array: true, default: []
16
+ t.timestamps
17
+ end
18
+ end
19
+
20
+ def down
21
+ drop_table :downloads
22
+ end
23
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ module Axlsx
5
+ def to_xlsx(&block)
6
+ formatter = Xport::Axlsx::Formatter.new(self)
7
+ to_file(formatter, &block)
8
+ end
9
+
10
+ class Formatter
11
+ attr_reader :export, :workbook, :worksheet
12
+
13
+ delegate :builder, to: :export
14
+
15
+ def initialize(export)
16
+ @export = export
17
+ @package = ::Axlsx::Package.new
18
+ @workbook = @package.workbook
19
+ @worksheet = @workbook.add_worksheet
20
+
21
+ # Support Numbers and multiline strings in Excel Mac 2011
22
+ # https://github.com/randym/axlsx/issues/252
23
+ @package.use_shared_strings = true
24
+ end
25
+
26
+ def to_file
27
+ @package.to_stream
28
+ end
29
+
30
+ def add_header_row(row)
31
+ worksheet.add_row row, style: header_style
32
+ end
33
+
34
+ def add_row(row)
35
+ values = row.map { |v| v.is_a?(Xport::Cell) ? v.value : v }
36
+ axlsx_row = worksheet.add_row(values, style: styles, types: builder.types)
37
+ row.each.with_index do |cell, i|
38
+ next unless cell.is_a?(Xport::Cell)
39
+ axlsx_cell = axlsx_row.cells[i]
40
+ axlsx_cell.color = cell.color
41
+ if cell.comment
42
+ worksheet.add_comment(
43
+ ref: axlsx_cell.reference(false),
44
+ author: 'Conditions',
45
+ text: cell.comment,
46
+ visible: false
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ def merge_header_cells(range)
53
+ worksheet.merge_cells worksheet.rows.first.cells[range]
54
+ end
55
+
56
+ def column_widths(*widths)
57
+ worksheet.column_widths(*widths)
58
+ end
59
+
60
+ private
61
+
62
+ def styles
63
+ @styles ||= builder.styles.map do |options|
64
+ workbook.styles.add_style(options) if options
65
+ end
66
+ end
67
+
68
+ def header_style
69
+ @header_style ||= workbook.styles.add_style b: true
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/xport/cell.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ class Cell
5
+ attr_accessor :value, :color, :comment
6
+ end
7
+ end
data/lib/xport/csv.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ module CSV
5
+ def to_csv(&block)
6
+ formatter = Xport::CSV::Formatter.new(self)
7
+ to_file(formatter, &block)
8
+ end
9
+
10
+ class Formatter
11
+ def initialize(export)
12
+ @io = StringIO.new
13
+ @csv = ::CSV.new(@io)
14
+ end
15
+
16
+ def to_file
17
+ @io.rewind
18
+ @io
19
+ end
20
+
21
+ def add_row(row)
22
+ values = row.map { |v| v.is_a?(Xport::Cell) ? v.value : v }
23
+ @csv << values
24
+ end
25
+ alias_method :add_header_row, :add_row
26
+
27
+ def merge_header_cells(range); end
28
+ def column_widths(*widths); end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ class DownloadPresenter
5
+ attr_reader :h
6
+
7
+ def initialize(download, h)
8
+ @download = download
9
+ @h = h
10
+ end
11
+
12
+ def progress_bar(job_id)
13
+ return unless job_id
14
+ job = Resque::Plugins::Status::Hash.get(job_id)
15
+ return unless job
16
+
17
+ case job.status
18
+ when Resque::Plugins::Status::STATUS_COMPLETED
19
+ nil
20
+ when Resque::Plugins::Status::STATUS_QUEUED
21
+ progress_bar_pending
22
+ else
23
+ h.content_tag(:div, class: 'progress', data: { component: 'Future.ProgressBar' }) do
24
+ h.content_tag(:div, class: 'progress-bar', style: "width: #{job.pct_complete}%") do
25
+ "#{job.pct_complete}% #{job.message}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def progress_bar_pending
32
+ message = I18n.translate(
33
+ 'helpers.progress_bar.please_wait_pending',
34
+ count: Resque.info[:pending]
35
+ )
36
+
37
+ h.content_tag(:div, class: 'progress', data: { component: 'Future.ProgressBar' }) do
38
+ h.content_tag(:div, class: 'progress-bar progress-bar-pending', style: 'width: 100%') do
39
+ h.content_tag(:span, message)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ module DownloadsControllerMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :set_download, only: %i(show update destroy)
9
+ before_action :set_downloads, only: :index
10
+ end
11
+
12
+ def index; end
13
+
14
+ def show
15
+ respond_to do |format|
16
+ format.html do
17
+ if !request.xhr? && @download.file?
18
+ redirect_to format: @download.type
19
+ else
20
+ render 'show'
21
+ end
22
+ end
23
+
24
+ format.any(:xlsx, :csv) do
25
+ send_data @download.file.read, filename: @download.filename
26
+ end
27
+ end
28
+ end
29
+
30
+ def update
31
+ @download.schedule_export!
32
+ redirect_to @download
33
+ end
34
+
35
+ def destroy
36
+ @download.destroy
37
+ redirect_to action: 'index'
38
+ end
39
+
40
+ private
41
+
42
+ def resource_class
43
+ controller_path.classify.constantize
44
+ end
45
+
46
+ def set_download
47
+ @download = resource_class.find(params[:id])
48
+ end
49
+
50
+ def set_downloads
51
+ @downloads = resource_class.page(params[:page])
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ class Export
5
+ attr_reader :workbook, :objects, :builder
6
+
7
+ class_attribute :builder_block
8
+
9
+ def self.columns(&block)
10
+ self.builder_block = block
11
+ end
12
+
13
+ def initialize(objects = [])
14
+ @objects = objects
15
+ @builder = Xport::ExportBuilder.new(self, &builder_block)
16
+ end
17
+
18
+ def to_file(formatter = nil, &block)
19
+ preload!
20
+ # TODO: There shouldn't be a default formatter
21
+ formatter ||= Xport::Axlsx::Formatter.new(self)
22
+ write_contents(formatter, &block)
23
+ formatter.to_file
24
+ end
25
+
26
+ def object_class
27
+ self.class.name.sub(/Export/, '').singularize.constantize
28
+ end
29
+
30
+ # TODO: Extract to xport-downloads?
31
+ def download(filename:, user: nil)
32
+ return unless objects.respond_to?(:to_sql)
33
+ download = download_class.new
34
+ download.user = user
35
+ download.export_klass_name = self.class.name
36
+
37
+ if objects.respond_to?(:model)
38
+ download.export_model_name = objects.model.name
39
+ end
40
+ if respond_to?(:additional_columns)
41
+ download.export_additional_columns = additional_columns
42
+ end
43
+ # TODO: Remove `unprepared_statement` call when `to_sql` is fixed
44
+ # https://github.com/rails/rails/issues/18379
45
+ object_class.connection.unprepared_statement do
46
+ download.query = objects.to_sql
47
+ end
48
+ download.filename = filename
49
+ download.save!
50
+ download.schedule_export!
51
+ download
52
+ end
53
+
54
+ def download_class
55
+ module_name = self.class.to_s.deconstantize
56
+ if module_name.present?
57
+ "#{module_name}::Download".constantize
58
+ else
59
+ Download
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def preload!; end
66
+
67
+ def write_contents(formatter, &block)
68
+ write_header(formatter)
69
+ write_body(formatter, &block)
70
+ write_widths(formatter)
71
+ end
72
+
73
+ def write_header(formatter)
74
+ if builder.grouped?
75
+ group_row = []
76
+ builder.groups_with_offset_and_colspan.each do |group, offset, colspan|
77
+ group_row << human_attribute_name(group)
78
+ (colspan - 1).times do
79
+ group_row << nil
80
+ end
81
+ end
82
+ formatter.add_header_row group_row
83
+
84
+ builder.groups_with_offset_and_colspan.each do |group, offset, colspan|
85
+ colspan -= 1
86
+ formatter.merge_header_cells(offset..offset + colspan)
87
+ end
88
+ end
89
+ formatter.add_header_row header_row
90
+ end
91
+
92
+ def write_body(formatter)
93
+ each_object do |object, rownum|
94
+ yield rownum, objects.size if block_given?
95
+ formatter.add_row object_row(object)
96
+ end
97
+ end
98
+
99
+ def write_widths(formatter)
100
+ formatter.column_widths(*builder.widths)
101
+ end
102
+
103
+ def find_in_batches?
104
+ false
105
+ end
106
+
107
+ def each_object(&block)
108
+ if find_in_batches?
109
+ objects.find_each.with_index(&block)
110
+ else
111
+ objects.each_with_index(&block)
112
+ end
113
+ end
114
+
115
+ def header_row
116
+ builder.headers.map do |name|
117
+ human_attribute_name(name)
118
+ end
119
+ end
120
+
121
+ def human_attribute_name(name)
122
+ return if name.to_s.start_with?('empty')
123
+
124
+ klass = object_class
125
+ case name
126
+ when String
127
+ name
128
+ when Symbol
129
+ klass.human_attribute_name(name)
130
+ end
131
+ end
132
+
133
+ def object_row(object)
134
+ builder.columns.map.with_index do |name, index|
135
+ block = builder.blocks[index]
136
+ if block
137
+ value = instance_exec(object, &block)
138
+ elsif respond_to?(name, true)
139
+ value = send(name, object)
140
+ else
141
+ value = object.send(name)
142
+ if respond_to?("convert_#{name}", true)
143
+ value = send("convert_#{name}", value)
144
+ end
145
+ end
146
+ value
147
+ end
148
+ end
149
+
150
+ def helper
151
+ ApplicationController.helpers
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ class ExportBuilder
5
+ attr_reader :export, :columns, :headers, :groups, :styles, :types, :widths, :blocks
6
+
7
+ def initialize(export, &block)
8
+ @export = export
9
+ @columns = []
10
+ @headers = []
11
+ @groups = []
12
+ @styles = []
13
+ @types = []
14
+ @widths = []
15
+ @blocks = []
16
+ instance_exec(export, &block)
17
+ end
18
+
19
+ def column(name, type: nil, style: nil, width: nil, header: nil, group: nil, &block)
20
+ columns << name
21
+ headers << (header || name)
22
+ groups << group
23
+ types << type
24
+ styles << style
25
+ widths << width
26
+ blocks << block
27
+ end
28
+
29
+ def grouped?
30
+ groups.any?
31
+ end
32
+
33
+ def groups_with_offset_and_colspan
34
+ offset = 0
35
+ colspan = 1
36
+ [].tap do |result|
37
+ groups.each do |group|
38
+ last = result.last
39
+ # check if current group is same as last group
40
+ if last && last[0] == group
41
+ # if group is the same, update colspan
42
+ last[2] += 1
43
+ else
44
+ result << [group, offset, colspan]
45
+ end
46
+ offset += 1
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xport
4
+ module ExportControllerMethods
5
+ def xport_export(export, filename:)
6
+ download = export.download(filename: filename, user: current_user)
7
+ if download
8
+ redirect_to download
9
+ else
10
+ send_data export.to_file.read, filename: filename
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Xport
2
+ VERSION = "0.1.0"
3
+ end
data/lib/xport.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/class/attribute'
4
+ require 'active_support/core_ext/module/delegation'
5
+ require 'active_support/concern'
6
+
7
+ require 'xport/version'
8
+ require 'xport/export'
9
+ require 'xport/export_builder'
10
+ require 'xport/cell'
11
+ require 'xport/downloads_controller_methods'
12
+ require 'xport/export_controller_methods'
13
+ require 'xport/download_presenter'
14
+ require 'xport/axlsx'
15
+ require 'xport/csv'
16
+
17
+ module Xport
18
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xport
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Janis Vitols
8
+ - Edgars Beigarts
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2017-08-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.5'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.5'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.1'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.1'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.6'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.6'
70
+ - !ruby/object:Gem::Dependency
71
+ name: simplecov
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.8'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.8'
84
+ - !ruby/object:Gem::Dependency
85
+ name: axlsx
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: saxlsx
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ description:
113
+ email:
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/generators/xport/install/install_generator.rb
119
+ - lib/generators/xport/install/templates/migration.rb
120
+ - lib/xport.rb
121
+ - lib/xport/axlsx.rb
122
+ - lib/xport/cell.rb
123
+ - lib/xport/csv.rb
124
+ - lib/xport/download_presenter.rb
125
+ - lib/xport/downloads_controller_methods.rb
126
+ - lib/xport/export.rb
127
+ - lib/xport/export_builder.rb
128
+ - lib/xport/export_controller_methods.rb
129
+ - lib/xport/version.rb
130
+ homepage:
131
+ licenses: []
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.6.11
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: CSV/Excel exports with own DSL and background jobs
153
+ test_files: []