rexport 1.2.1 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cf8cb2fe8850f491ab8eb8a92d0034d1447babd6a4a79457acea80fcbec94fc
4
- data.tar.gz: 2e646631a4c3673359f31bd222c43c32ed6fcb20813f7f6ac34d633005899628
3
+ metadata.gz: 645738a1ea2afb2feee13d71a5c4e5e01ce10fe6e872f1bb5f01077e6db05638
4
+ data.tar.gz: 37c36f6d85dd9a877e8e8de45642c3e8536aab04c71ea1aeb231abf750ed24f5
5
5
  SHA512:
6
- metadata.gz: '08d2fd739cf9775d22295a57ba4ffd934da63079339f0b697ff923ecba958b2bf5c007987a5ade00756b56a758672d37b23482dc9151dbb610157be181761521'
7
- data.tar.gz: '0359913d6d17b4303518284a54a5da8428a1c0b3ce871766fa117a37c063f5d739aa812a08e495689867502ca63be23e719405211eb2ea35b2c046dae006c6e7'
6
+ metadata.gz: 9f9b3fcc9a4823a79ea2341aa22d918acd1fbad669d6eae62fec6fcc386923f49616d977745bfd81a179b92c3b9fdc65b058c53bf8cb1d9f4cfe6e66e9f41546
7
+ data.tar.gz: 19f28b327e48fcf962f17bd91c3100dd6bd912872ed822d6cb8ce2224ca4985d36d448a5af0895dcc0bceef803fd1719310deab6e402af2e9d87415adfa25f12
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Aaron Baldwin, WWIDEA INC.
1
+ Copyright 2011-2023 Aaron Baldwin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Rexport
2
+ Ruby on Rails gem to manage exports
3
+
4
+ ## Installation
5
+ 1. install gem
6
+ 2. copy migration into application db/migrate folder and run
7
+ 3. create models and mix in corresponding module
8
+ - **export** - export_methods
9
+ - **export_item** - export_item_methods
10
+ - **export_filter** - export_filter_methods
11
+
12
+ ## License
13
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,10 +1,5 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ # frozen_string_literal: true
3
2
 
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
8
- end
3
+ require "bundler/setup"
9
4
 
10
- task default: :test
5
+ require "bundler/gem_tasks"
@@ -1,28 +1,50 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExportsHelper
4
+ BOOLEAN_OPTIONS = [nil, ["true", 1], ["false", 0]].freeze
5
+ FORM_TAG_CLASS = "form-control"
6
+
2
7
  def filter_value_field(rexport_model, field)
8
+ ActiveSupport::Deprecation.warn(
9
+ "Calling #filter_value_field is deprecated. Use #rexport_filter_form_tag instead"
10
+ )
11
+ rexport_filter_tag(@export, rexport_model, field) # rubocop:disable Rails/HelperInstanceVariable
12
+ end
13
+
14
+ def rexport_filter_tag(export, rexport_model, field) # rubocop:disable Metrics/MethodLength
3
15
  filter_field = rexport_model.field_path(rexport_model.filter_column(field))
4
- tag_name = "export[export_filter_attributes][#{filter_field.to_s}]"
5
- value = @export.filter_value(filter_field)
16
+ tag_name = "export[export_filter_attributes][#{filter_field}]"
17
+ value = export.filter_value(filter_field)
6
18
 
7
19
  case field.type
8
- when :boolean
9
- select_tag(tag_name, options_for_select([nil, ['true', 1], ['false', 0]], (value.to_i unless value.nil?)), class: 'form-control')
10
20
  when :association
11
- association, text_method = field.method.split('.')
12
- select_tag(tag_name,
13
- ('<option value=""></option>' +
14
- options_from_collection_for_select(
21
+ association, text_method = field.method.split(".")
22
+ association_filter_select_tag(
23
+ tag_name,
24
+ association_filter_options(
15
25
  rexport_model.collection_from_association(association),
16
- :id,
17
26
  text_method,
18
- value.to_i
19
- )).html_safe,
20
- class: 'form-control'
27
+ value
28
+ )
21
29
  )
22
- when :datetime, nil
23
- '&nbsp;'.html_safe
24
- else
25
- text_field_tag(tag_name, value, class: 'form-control')
30
+ when :boolean
31
+ boolean_filter_select_tag(tag_name, value)
32
+ when :date, :integer, :string
33
+ text_field_tag(tag_name, value, class: FORM_TAG_CLASS)
26
34
  end
27
35
  end
36
+
37
+ private
38
+
39
+ def boolean_filter_select_tag(tag_name, value)
40
+ select_tag(tag_name, options_for_select(BOOLEAN_OPTIONS, value&.to_i), class: FORM_TAG_CLASS)
41
+ end
42
+
43
+ def association_filter_options(collection, text_method, value)
44
+ options_from_collection_for_select(collection, :id, text_method, value&.to_i)
45
+ end
46
+
47
+ def association_filter_select_tag(tag_name, options)
48
+ select_tag(tag_name, options, include_blank: true, class: FORM_TAG_CLASS)
49
+ end
28
50
  end
@@ -7,10 +7,10 @@
7
7
  </tr>
8
8
  <% for field in rexport_model.rexport_fields_array %>
9
9
  <tr class="<%= cycle('odd','even') %>">
10
- <td><%= check_box_tag "export[rexport_fields][#{rexport_model.field_path(field.name)}]", 1, @export.has_rexport_field?(rexport_model.field_path(field.name)) %></td>
10
+ <td><%= check_box_tag "export[rexport_fields][#{rexport_model.field_path(field.name)}]", 1, @export.rexport_field?(rexport_model.field_path(field.name)) %></td>
11
11
  <td class="row_title"><%= field.name.titleize %></td>
12
12
  <td class='row_title'>
13
- <%= filter_value_field(rexport_model, field) %>
13
+ <%= rexport_filter_tag(@export, rexport_model, field) %>
14
14
  </td>
15
15
  </tr>
16
16
  <% end %>
data/config/routes.rb CHANGED
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Rails.application.routes.draw do
2
4
  # singleton resources
3
5
  resource :export_item_sorting, only: :update
4
6
 
5
7
  # collection resources
6
- resources :export_items, only: %i(edit update destroy)
7
- resources :export_filters, only: %i(edit update destroy)
8
+ resources :export_items, only: %i[edit update destroy]
9
+ resources :export_filters, only: %i[edit update destroy]
8
10
  resources :exports do
9
11
  resources :export_filters, only: :new
10
12
  end
@@ -1,17 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rexport
2
4
  class DataField
3
5
  include Comparable
4
- attr_accessor :name, :method, :type
6
+ attr_reader :name, :method, :type
5
7
 
6
8
  # Stores the name and method of the export data item
7
9
  def initialize(name, options = {})
8
- self.name = name.to_s
9
- self.method = options[:method].blank? ? self.name : options[:method].to_s
10
- self.type = options[:type]
10
+ @name = name.to_s
11
+ @method = options[:method].blank? ? self.name : options[:method].to_s
12
+ @type = options[:type]
13
+ end
14
+
15
+ # Sort by name
16
+ def <=>(other)
17
+ name <=> other.name
11
18
  end
12
19
 
13
- def <=>(rf)
14
- self.name <=> rf.name
20
+ # Returns the first association name from a method chain string. If the string does not contain
21
+ # the dot operator a nil is returned.
22
+ #
23
+ # Examples:
24
+ #
25
+ # "assocation.method" # => "association"
26
+ # "assocation_one.assocation_two.method" # => "assocation_one"
27
+ # "method" # => nil
28
+ def association_name
29
+ method[0..(first_dot_index - 1)] if first_dot_index.present?
30
+ end
31
+
32
+ private
33
+
34
+ def first_dot_index
35
+ @first_dot_index ||= method.index(".")
15
36
  end
16
37
  end
17
38
  end
@@ -1,4 +1,6 @@
1
- module Rexport #:nodoc:
1
+ # frozen_string_literal: true
2
+
3
+ module Rexport # :nodoc:
2
4
  module DataFields
3
5
  extend ActiveSupport::Concern
4
6
 
@@ -7,6 +9,7 @@ module Rexport #:nodoc:
7
9
  def get_klass_from_associations(*associations)
8
10
  associations.flatten!
9
11
  return self if associations.empty?
12
+
10
13
  reflect_on_association(associations.shift.to_sym).klass.get_klass_from_associations(associations)
11
14
  end
12
15
  end
@@ -14,21 +17,15 @@ module Rexport #:nodoc:
14
17
  # Return an array of formatted export values for the passed methods
15
18
  def export(*methods)
16
19
  methods.flatten.map do |method|
17
- case value = (eval("self.#{method}", binding) rescue nil)
18
- when Date, Time
19
- value.strftime("%m/%d/%y")
20
- when TrueClass
21
- 'Y'
22
- when FalseClass
23
- 'N'
24
- else value.to_s
25
- end
20
+ Rexport::Formatter.convert(instance_eval(method))
21
+ rescue NameError
22
+ ""
26
23
  end
27
24
  end
28
25
 
29
26
  # Returns string indicating this field is undefined
30
27
  def undefined_rexport_field
31
- 'UNDEFINED EXPORT FIELD'
28
+ "UNDEFINED EXPORT FIELD"
32
29
  end
33
30
  end
34
31
  end
@@ -1,4 +1,6 @@
1
- module Rexport #:nodoc:
1
+ # frozen_string_literal: true
2
+
3
+ module Rexport # :nodoc:
2
4
  module ExportFilterMethods
3
5
  extend ActiveSupport::Concern
4
6
 
@@ -12,23 +14,24 @@ module Rexport #:nodoc:
12
14
  end
13
15
 
14
16
  def attributes_for_copy
15
- attributes.slice('filter_field', 'value')
17
+ attributes.slice("filter_field", "value")
16
18
  end
17
19
 
18
20
  private
19
21
 
20
22
  def associated_object_value
21
- return 'UNDEFINED ASSOCIATION' unless filter_association
23
+ return "UNDEFINED ASSOCIATION" unless filter_association
24
+
22
25
  begin
23
26
  object = filter_association.klass.find(value)
24
- return object.respond_to?(:name) ? object.name : object.to_s
27
+ object.respond_to?(:name) ? object.name : object.to_s
25
28
  rescue ActiveRecord::RecordNotFound
26
- return 'ASSOCIATED OBJECT NOT FOUND'
29
+ "ASSOCIATED OBJECT NOT FOUND"
27
30
  end
28
31
  end
29
32
 
30
33
  def filter_association
31
- @filter_on_assocation ||= find_filter_association
34
+ @filter_association ||= find_filter_association
32
35
  end
33
36
 
34
37
  def find_filter_association
@@ -42,11 +45,11 @@ module Rexport #:nodoc:
42
45
  end
43
46
 
44
47
  def filter_path
45
- filter_field.split('.')[0..-2]
48
+ filter_field.split(".")[0..-2]
46
49
  end
47
50
 
48
51
  def filter_foreign_key
49
- filter_field.split('.').last
52
+ filter_field.split(".").last
50
53
  end
51
54
 
52
55
  def filter_on_associated_object?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rexport
2
4
  module ExportFiltersControllerMethods
3
5
  def destroy
@@ -1,4 +1,6 @@
1
- module Rexport #:nodoc:
1
+ # frozen_string_literal: true
2
+
3
+ module Rexport # :nodoc:
2
4
  module ExportItemMethods
3
5
  extend ActiveSupport::Concern
4
6
 
@@ -17,14 +19,14 @@ module Rexport #:nodoc:
17
19
  def resort(export_item_ids)
18
20
  transaction do
19
21
  export_item_ids.each_with_index do |id, index|
20
- find(id.gsub(/[^0-9]/, '')).update_attribute(:position, index + 1)
22
+ find(id.gsub(/[^0-9]/, "")).update_attribute(:position, index + 1)
21
23
  end
22
24
  end
23
25
  end
24
26
  end
25
27
 
26
28
  def attributes_for_copy
27
- attributes.slice('position', 'name', 'rexport_field')
29
+ attributes.slice("position", "name", "rexport_field")
28
30
  end
29
31
 
30
32
  private
@@ -34,7 +36,7 @@ module Rexport #:nodoc:
34
36
  end
35
37
 
36
38
  def generate_name_from_rexport_field
37
- rexport_field.split('.').last(2).map(&:titleize).join(' - ')
39
+ rexport_field.split(".").last(2).map(&:titleize).join(" - ")
38
40
  end
39
41
  end
40
42
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rexport
2
4
  module ExportItemSortingsControllerMethods
3
5
  def update
4
6
  ExportItem.resort(params[:sorted_items])
5
-
7
+
6
8
  respond_to do |format|
7
9
  format.js do
8
10
  head :ok
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rexport
2
4
  module ExportItemsControllerMethods
3
5
  def edit
@@ -6,7 +8,7 @@ module Rexport
6
8
 
7
9
  def update
8
10
  if export_item.update(export_item_params)
9
- redirect_to export_path(export_item.export), notice: 'ExportItem was successfully updated.'
11
+ redirect_to export_path(export_item.export), notice: "ExportItem was successfully updated."
10
12
  else
11
13
  render :edit
12
14
  end
@@ -1,6 +1,8 @@
1
- require 'csv'
1
+ # frozen_string_literal: true
2
2
 
3
- module Rexport #:nodoc:
3
+ require "csv"
4
+
5
+ module Rexport # :nodoc:
4
6
  module ExportMethods
5
7
  extend ActiveSupport::Concern
6
8
 
@@ -18,7 +20,7 @@ module Rexport #:nodoc:
18
20
 
19
21
  module ClassMethods
20
22
  def models
21
- %w(override_this_method)
23
+ %w[override_this_method]
22
24
  end
23
25
  end
24
26
 
@@ -28,12 +30,7 @@ module Rexport #:nodoc:
28
30
 
29
31
  # Returns a string with the export data
30
32
  def to_s
31
- String.new.tap do |result|
32
- result << header * '|' << "\n"
33
- records.each do |record|
34
- result << record * '|' << "\n"
35
- end
36
- end
33
+ records.unshift(header).map { |line| line.join("|") }.join("\n")
37
34
  end
38
35
 
39
36
  # Returns a csv string with the export data
@@ -80,33 +77,35 @@ module Rexport #:nodoc:
80
77
  # Returns a class based on a path array
81
78
  def get_klass_from_path(path, klass = export_model)
82
79
  return klass unless (association_name = path.shift)
80
+
83
81
  get_klass_from_path(path, klass.reflect_on_association(association_name.to_sym).klass)
84
82
  end
85
83
 
86
- def has_rexport_field?(rexport_field)
84
+ def has_rexport_field?(rexport_field) # rubocop:disable Naming/PredicateName
85
+ ActiveSupport::Deprecation.warn("Calling #has_rexport_field? is deprecated. Use #rexport_field? instead")
86
+ rexport_field?(rexport_field)
87
+ end
88
+
89
+ def rexport_field?(rexport_field)
87
90
  rexport_fields.include?(rexport_field)
88
91
  end
89
92
 
90
- def rexport_fields=(rexport_fields)
91
- @rexport_fields = if rexport_fields.respond_to?(:keys)
92
- @set_position = false
93
- rexport_fields.keys.map(&:to_s)
94
- else
95
- @set_position = true
96
- rexport_fields.map(&:to_s)
97
- end
93
+ # Stores rexport_field names to update the export_items association after save
94
+ # Expects fields to be a hash with field names as the keys or an array of field names:
95
+ # { "field_one" => "1", "field_two" => "1" }
96
+ # ["field_one", "field_two"]
97
+ def rexport_fields=(fields)
98
+ @rexport_fields = extract_rexport_fields(fields).map(&:to_s)
98
99
  end
99
100
 
100
101
  def export_filter_attributes=(attributes)
101
102
  attributes.each do |field, value|
102
103
  if value.blank?
103
- filter = export_filters.find_by(filter_field: field)
104
- filter.destroy if filter
104
+ export_filters.find_by(filter_field: field)&.destroy
105
105
  elsif new_record?
106
106
  export_filters.build(filter_field: field, value: value)
107
107
  else
108
- filter = export_filters.find_or_create_by(filter_field: field)
109
- filter.update_attribute(:value, value)
108
+ export_filters.find_or_create_by(filter_field: field).update_attribute(:value, value)
110
109
  end
111
110
  end
112
111
  end
@@ -140,31 +139,33 @@ module Rexport #:nodoc:
140
139
 
141
140
  def get_rexport_models(model, results = [], path = nil)
142
141
  return unless model.include?(Rexport::DataFields)
142
+
143
143
  results << RexportModel.new(model, path: path)
144
144
  get_associations(model).each do |associated_model|
145
145
  # prevent infinite loop by checking if this class is already in the results set
146
146
  next if results.detect { |result| result.klass == associated_model.klass }
147
- get_rexport_models(associated_model.klass, results, [path, associated_model.name].compact * '.')
147
+
148
+ get_rexport_models(associated_model.klass, results, [path, associated_model.name].compact * ".")
148
149
  end
149
- return results
150
+ results
150
151
  end
151
152
 
152
153
  def get_associations(model)
153
- %i(belongs_to has_one).map do |type|
154
+ %i[belongs_to has_one].map do |type|
154
155
  model.reflect_on_all_associations(type)
155
156
  end.flatten.reject(&:polymorphic?)
156
157
  end
157
158
 
158
159
  def build_include
159
- root = Rexport::TreeNode.new('root')
160
- (rexport_methods + filter_fields).select {|m| m.include?('.')}.each do |method|
161
- root.add_child(method.split('.').values_at(0..-2))
160
+ root = Rexport::TreeNode.new("root")
161
+ (rexport_methods + filter_fields).select { |m| m.include?(".") }.each do |method|
162
+ root.add_child(method.split(".").values_at(0..-2))
162
163
  end
163
164
  root.to_include
164
165
  end
165
166
 
166
167
  def build_conditions
167
- Hash.new.tap do |conditions|
168
+ {}.tap do |conditions|
168
169
  export_filters.each do |filter|
169
170
  conditions[get_database_field(filter.filter_field)] = filter.value
170
171
  end
@@ -172,7 +173,7 @@ module Rexport #:nodoc:
172
173
  end
173
174
 
174
175
  def get_database_field(field)
175
- path = field.split('.')
176
+ path = field.split(".")
176
177
  field = path.pop
177
178
  "#{get_klass_from_path(path).table_name}.#{field}"
178
179
  end
@@ -194,27 +195,38 @@ module Rexport #:nodoc:
194
195
  end
195
196
 
196
197
  def save_export_items
197
- export_items.each do |export_item|
198
- unless rexport_fields.include?(export_item.rexport_field)
199
- export_item.destroy
200
- end
201
- end
198
+ export_items.where.not(rexport_field: rexport_fields).destroy_all
202
199
 
203
200
  rexport_fields.each.with_index(1) do |rexport_field, position|
204
- export_item = export_items.detect { |i| i.rexport_field == rexport_field } || export_items.create(rexport_field: rexport_field)
205
- export_item.update_attribute(:position, position) if set_position
201
+ find_or_create_export_item(rexport_field).tap do |export_item|
202
+ export_item.update_attribute(:position, position) if set_position
203
+ end
206
204
  end
207
205
 
208
- return true
206
+ true
207
+ end
208
+
209
+ # Uses array find to search in memory export_items assocation instead of performing a SQL query on every iteration
210
+ def find_or_create_export_item(rexport_field)
211
+ export_items.find { |export_item| export_item.rexport_field == rexport_field } || export_items.create(rexport_field: rexport_field)
209
212
  end
210
213
 
211
214
  def attributes_for_copy
212
- attributes.slice('model_class_name', 'description').merge(name: find_unique_name(name))
215
+ attributes.slice("model_class_name", "description").merge(name: find_unique_name(name))
213
216
  end
214
217
 
215
218
  def find_unique_name(original_name, suffix = 0)
216
- new_name = suffix == 0 ? "#{original_name} Copy" : "#{original_name} Copy [#{suffix}]"
217
- self.class.find_by(name: new_name) ? find_unique_name(original_name, suffix += 1) : new_name
219
+ new_name = suffix.zero? ? "#{original_name} Copy" : "#{original_name} Copy [#{suffix}]"
220
+ self.class.find_by(name: new_name) ? find_unique_name(original_name, suffix + 1) : new_name
221
+ end
222
+
223
+ def extract_rexport_fields(fields)
224
+ # When fields is a hash return the keys and do not update export_item positions on save
225
+ return fields.keys if fields.respond_to?(:keys)
226
+
227
+ # When fields is an array update export item positions on save
228
+ @set_position = true
229
+ fields
218
230
  end
219
231
 
220
232
  def set_position
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rexport
2
4
  module ExportsControllerMethods
3
5
  def index
@@ -9,7 +11,7 @@ module Rexport
9
11
 
10
12
  respond_to do |format|
11
13
  format.html # show.html.erb
12
- format.csv { send_data(export.to_csv, type: export_content_type, filename: filename) }
14
+ format.csv { send_data(export.to_csv, filename: filename) }
13
15
  end
14
16
  end
15
17
 
@@ -25,7 +27,7 @@ module Rexport
25
27
  @export = params[:original_export_id] ? Export.find(params[:original_export_id]).copy : Export.new(export_params)
26
28
 
27
29
  if @export.save
28
- redirect_to @export, notice: 'Export was successfully created.'
30
+ redirect_to @export, notice: "Export was successfully created."
29
31
  else
30
32
  render :new
31
33
  end
@@ -33,7 +35,7 @@ module Rexport
33
35
 
34
36
  def update
35
37
  if export.update(export_params)
36
- redirect_to export, notice: 'Export was successfully updated.'
38
+ redirect_to export, notice: "Export was successfully updated."
37
39
  else
38
40
  render :edit
39
41
  end
@@ -60,17 +62,15 @@ module Rexport
60
62
  :name,
61
63
  :model_class_name,
62
64
  :description,
63
- rexport_fields: {},
64
- export_filter_attributes: {}
65
+ {
66
+ rexport_fields: {},
67
+ export_filter_attributes: {}
68
+ }
65
69
  ]
66
70
  end
67
71
 
68
- def export_content_type
69
- request.user_agent =~ /windows/i ? 'application/vnd.ms-excel' : 'text/csv'
70
- end
71
-
72
72
  def filename
73
- "#{export.model_class_name}_#{export.name.gsub(/ /, '_')}_#{Time.now.strftime('%Y%m%d')}.csv"
73
+ "#{export.model_class_name}_#{export.name.tr(' ', '_')}_#{Time.current.strftime('%Y%m%d')}.csv"
74
74
  end
75
75
  end
76
76
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rexport
4
+ module Formatter
5
+ def self.convert(value)
6
+ case value
7
+ when Date, Time
8
+ value.strftime("%m/%d/%y")
9
+ when TrueClass
10
+ "Y"
11
+ when FalseClass
12
+ "N"
13
+ else
14
+ value.to_s
15
+ end
16
+ end
17
+ end
18
+ end