rexport 1.2.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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