xport 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []