spotlight_search 0.1.8 → 0.1.9
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 +4 -4
- data/README.md +3 -1
- data/app/jobs/spotlight_search/export_job.rb +35 -4
- data/lib/spotlight_search.rb +9 -0
- data/lib/spotlight_search/exceptions.rb +1 -1
- data/lib/spotlight_search/exportable_columns.rb +24 -29
- data/lib/spotlight_search/exportable_columns_v2.rb +102 -0
- data/lib/spotlight_search/helpers.rb +23 -3
- data/lib/spotlight_search/utils.rb +69 -0
- data/lib/spotlight_search/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d8c71e34af7024e41f5d6767ce6b9cc6a4466aed6882997d97b4f45e57c2500
|
|
4
|
+
data.tar.gz: bd7d756c813e0a0572dd4973e8f795ce5195740811460bd91fa595aafa556080
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e83a3b01980d9d3fa0fea02ee9f9f226ec882dfbaaff6b13bf69748b9e37ef8e7cc6b8487cba6a27a4d2d1681864ae06d490628cf01d4746a6fce1c63cc9bf4
|
|
7
|
+
data.tar.gz: 1fca5cef1d8ced80eb3c9a040152277002758b592b34af18674ba205737fecba884de3f397ac4a4edf74b4104178dcdd1f75b4ae219f52b9b10e0a776adc8907
|
data/README.md
CHANGED
|
@@ -20,6 +20,8 @@ Or install it manually:
|
|
|
20
20
|
|
|
21
21
|
$ gem install spotlight_search
|
|
22
22
|
|
|
23
|
+
Include the spotlight_search javascript by adding the line `//= require spotlight_search` to your `app/assets/javascripts/application.js`
|
|
24
|
+
|
|
23
25
|
## Usage
|
|
24
26
|
|
|
25
27
|
1. [Filtering, Sorting and Pagination](#filtering-sorting-and-pagination)
|
|
@@ -169,7 +171,7 @@ Add `exportable email, model_object` in your view to display the export button.
|
|
|
169
171
|
</td>
|
|
170
172
|
</table>
|
|
171
173
|
|
|
172
|
-
<%= exportable(current_user.email,
|
|
174
|
+
<%= exportable(current_user.email, Person) %>
|
|
173
175
|
```
|
|
174
176
|
|
|
175
177
|
This will first show a popup where an option to select the export enabled columns will be listed. This will also apply any filters that has been selected along with a sorting if applied. It then pushes the export to a background job which will send an excel file of the contents to the specified email. You can edit the style of the button using the class `export-to-file-btn`.
|
|
@@ -5,7 +5,13 @@ module SpotlightSearch
|
|
|
5
5
|
def perform(email, klass, columns = [], filters = {})
|
|
6
6
|
klass = klass.constantize
|
|
7
7
|
records = get_records(klass, filters, columns)
|
|
8
|
-
file_path =
|
|
8
|
+
file_path =
|
|
9
|
+
case SpotlightSearch.exportable_columns_version
|
|
10
|
+
when :v1
|
|
11
|
+
create_excel(records, klass.name, columns)
|
|
12
|
+
when :v2
|
|
13
|
+
create_excel_v2(records, klass.name)
|
|
14
|
+
end
|
|
9
15
|
subject = "#{klass.name} export at #{Time.now}"
|
|
10
16
|
ExportMailer.send_excel_file(email, file_path, subject).deliver_now
|
|
11
17
|
File.delete(file_path)
|
|
@@ -13,7 +19,7 @@ module SpotlightSearch
|
|
|
13
19
|
|
|
14
20
|
def get_records(klass, filters, columns)
|
|
15
21
|
records = klass
|
|
16
|
-
if filters
|
|
22
|
+
if !filters.empty?
|
|
17
23
|
if filters['filters'].present?
|
|
18
24
|
filters['filters'].each do |scope, scope_args|
|
|
19
25
|
if scope_args.is_a?(Array)
|
|
@@ -26,9 +32,16 @@ module SpotlightSearch
|
|
|
26
32
|
if filters['sort'].present?
|
|
27
33
|
records = records.order("#{filters['sort']['sort_column']} #{filters['sort']['sort_direction']}")
|
|
28
34
|
end
|
|
35
|
+
else
|
|
36
|
+
records = records.all
|
|
37
|
+
end
|
|
38
|
+
case SpotlightSearch.exportable_columns_version
|
|
39
|
+
when :v1
|
|
40
|
+
columns = columns.map(&:to_sym)
|
|
41
|
+
records.select(*columns)
|
|
42
|
+
when :v2
|
|
43
|
+
records.as_json(SpotlightSearch::Utils.deserialize_csv_columns(columns, :as_json_params))
|
|
29
44
|
end
|
|
30
|
-
columns = columns.map(&:to_sym)
|
|
31
|
-
records.select(*columns)
|
|
32
45
|
end
|
|
33
46
|
|
|
34
47
|
# Creating excel with the passed records
|
|
@@ -48,5 +61,23 @@ module SpotlightSearch
|
|
|
48
61
|
xl.serialize(file_location)
|
|
49
62
|
file_location
|
|
50
63
|
end
|
|
64
|
+
|
|
65
|
+
def create_excel_v2(records, class_name)
|
|
66
|
+
flattened_records = records.map { |record| SpotlightSearch::Utils.flatten_hash(record) }
|
|
67
|
+
columns = flattened_records[0].keys
|
|
68
|
+
size_arr = []
|
|
69
|
+
columns.size.times { size_arr << 22 }
|
|
70
|
+
xl = Axlsx::Package.new
|
|
71
|
+
xl.workbook.add_worksheet do |sheet|
|
|
72
|
+
sheet.add_row columns, b: true
|
|
73
|
+
flattened_records.each do |record|
|
|
74
|
+
sheet.add_row(columns.map { |column| record[column] })
|
|
75
|
+
end
|
|
76
|
+
sheet.column_widths(*size_arr)
|
|
77
|
+
end
|
|
78
|
+
file_location = "#{Rails.root}/public/export_#{class_name}_#{Time.now.to_s}.xls"
|
|
79
|
+
xl.serialize(file_location)
|
|
80
|
+
file_location
|
|
81
|
+
end
|
|
51
82
|
end
|
|
52
83
|
end
|
data/lib/spotlight_search.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require 'spotlight_search/engine'
|
|
2
2
|
require 'spotlight_search/version'
|
|
3
3
|
require 'spotlight_search/exportable_columns'
|
|
4
|
+
require 'spotlight_search/exportable_columns_v2'
|
|
5
|
+
require 'spotlight_search/utils'
|
|
4
6
|
require 'spotlight_search/railtie' if defined?(Rails)
|
|
5
7
|
require 'active_support'
|
|
6
8
|
require 'active_support/rails'
|
|
@@ -10,6 +12,13 @@ module SpotlightSearch
|
|
|
10
12
|
|
|
11
13
|
autoload :Exceptions, 'spotlight_search/exceptions'
|
|
12
14
|
|
|
15
|
+
def self.setup
|
|
16
|
+
yield self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
mattr_accessor :exportable_columns_version
|
|
20
|
+
@@exportable_columns_version = :v1
|
|
21
|
+
|
|
13
22
|
module ClassMethods
|
|
14
23
|
def filter_by(page, filter_params = {}, sort_params = {})
|
|
15
24
|
filtered_result = OpenStruct.new
|
|
@@ -28,35 +28,30 @@ module SpotlightSearch
|
|
|
28
28
|
# export_columns enabled: true, except: [:created_at, :updated_at]
|
|
29
29
|
# end
|
|
30
30
|
#
|
|
31
|
-
def export_columns(enabled: false, only: nil, except: nil)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
self.enabled_columns = self.enabled_columns - except
|
|
52
|
-
end
|
|
53
|
-
else
|
|
54
|
-
self.export_enabled = false
|
|
55
|
-
self.enabled_columns = nil
|
|
56
|
-
end
|
|
31
|
+
def export_columns(enabled: false, only: nil, except: nil, associated: nil)
|
|
32
|
+
ActiveRecord::Base.connection.migration_context.needs_migration? && return
|
|
33
|
+
return unless enabled
|
|
34
|
+
|
|
35
|
+
self.export_enabled = true
|
|
36
|
+
all_columns = self.column_names.map(&:to_sym)
|
|
37
|
+
if only.present?
|
|
38
|
+
unless (valid_columns = only & all_columns).size == only.size
|
|
39
|
+
invalid_columns = only - valid_columns
|
|
40
|
+
raise SpotlightSearch::Exceptions::InvalidColumns, invalid_columns
|
|
41
|
+
end
|
|
42
|
+
self.enabled_columns = only
|
|
43
|
+
else
|
|
44
|
+
self.enabled_columns = all_columns
|
|
45
|
+
end
|
|
46
|
+
if except.present?
|
|
47
|
+
unless (valid_columns = except & all_columns).size == except.size
|
|
48
|
+
invalid_columns = except - valid_columns
|
|
49
|
+
raise SpotlightSearch::Exceptions::InvalidColumns, invalid_columns
|
|
57
50
|
end
|
|
58
|
-
|
|
51
|
+
self.enabled_columns = self.enabled_columns - except
|
|
59
52
|
end
|
|
53
|
+
rescue ActiveRecord::NoDatabaseError
|
|
54
|
+
Rails.logger.info("No database error")
|
|
60
55
|
end
|
|
61
56
|
|
|
62
57
|
# Validates whether the selected columns are allowed for export
|
|
@@ -72,8 +67,8 @@ module SpotlightSearch
|
|
|
72
67
|
end
|
|
73
68
|
|
|
74
69
|
included do
|
|
75
|
-
class_attribute :enabled_columns, instance_accessor: false
|
|
76
|
-
class_attribute :export_enabled, instance_accessor: false
|
|
70
|
+
class_attribute :enabled_columns, instance_accessor: false, default: nil
|
|
71
|
+
class_attribute :export_enabled, instance_accessor: false, default: false
|
|
77
72
|
end
|
|
78
73
|
end
|
|
79
74
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module SpotlightSearch
|
|
2
|
+
module ExportableColumnsV2
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
module ClassMethods
|
|
6
|
+
# Enables or disables export and specifies which all columns can be
|
|
7
|
+
# exported. For enabling export for all columns in all models
|
|
8
|
+
#
|
|
9
|
+
# class ApplicationRecord < ActiveRecord::Base
|
|
10
|
+
# export_columns enabled: true
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# For disabling export for only specific models
|
|
14
|
+
#
|
|
15
|
+
# class Person < ActiveRecord::Base
|
|
16
|
+
# export_columns enabled: false
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# For allowing export for only specific columns in a model
|
|
20
|
+
#
|
|
21
|
+
# class Person < ActiveRecord::Base
|
|
22
|
+
# export_columns enabled: true, only: [:created_at, :updated_at]
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# For excluding only specific columns and allowing all others
|
|
26
|
+
#
|
|
27
|
+
# class Person < ActiveRecord::Base
|
|
28
|
+
# export_columns enabled: true, except: [:created_at, :updated_at]
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
def export_columns(*record_fields, **associated_fields)
|
|
32
|
+
ActiveRecord::Base.connection.migration_context.needs_migration? && return
|
|
33
|
+
|
|
34
|
+
# Check that all record fields are valid accessible. Error if it doesn't.
|
|
35
|
+
# for each association, check that if its a valid association, and take the recursive step with that association
|
|
36
|
+
# End result is setting up in self, enabled columns and enabled associated columns
|
|
37
|
+
columns_hash = _model_exportable_columns(self, *record_fields, **associated_fields)
|
|
38
|
+
|
|
39
|
+
raise SpotlightSearch::Exceptions::InvalidColumns, columns_hash[:invalid_columns] if columns_hash[:invalid_columns].size.positive?
|
|
40
|
+
self.enabled_columns = [*record_fields, **associated_fields]
|
|
41
|
+
rescue ActiveRecord::NoDatabaseError
|
|
42
|
+
Rails.logger.info("No database error")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def _model_exportable_columns(klass, *record_fields, **associated_fields)
|
|
46
|
+
# Gets all the valid columns of a model
|
|
47
|
+
# If any column is invalid, it also returns it
|
|
48
|
+
raise SpotlightSearch::Exceptions::InvalidValue, "Expected ActiveRecord::Base, Received #{klass}" unless klass < ActiveRecord::Base
|
|
49
|
+
|
|
50
|
+
valid_columns = []
|
|
51
|
+
invalid_columns = []
|
|
52
|
+
|
|
53
|
+
# Base case: verify all the columns that belong to this record
|
|
54
|
+
record_fields.each do |field|
|
|
55
|
+
klass.new.respond_to?(field) ? valid_columns << field : invalid_columns << field
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Recursive case: check all associations and verify that they are all valid too
|
|
59
|
+
associated_fields.each do |association, association_record_fields|
|
|
60
|
+
reflection = klass.reflect_on_association(association)
|
|
61
|
+
invalid_columns << association && next unless reflection # Add whole association to invalid columns if it doesn't exist
|
|
62
|
+
|
|
63
|
+
case reflection
|
|
64
|
+
when ActiveRecord::Reflection::BelongsToReflection, ActiveRecord::Reflection::HasOneReflection
|
|
65
|
+
if reflection.polymorphic?
|
|
66
|
+
# We cannot process them further, so we'll assume it works and call it a day
|
|
67
|
+
valid_columns << { association => association_record_fields }
|
|
68
|
+
else
|
|
69
|
+
columns_hash = _model_exportable_columns(reflection.klass, *association_record_fields)
|
|
70
|
+
valid_columns << { association => columns_hash[:valid_columns] }
|
|
71
|
+
invalid_columns << { association => columns_hash[:invalid_columns] } if columns_hash[:invalid_columns].size.positive?
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
# one to many relationshops cannot be supported
|
|
75
|
+
invalid_columns << association
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# return all the valid and invalid columns in a hash
|
|
81
|
+
{
|
|
82
|
+
valid_columns: valid_columns,
|
|
83
|
+
invalid_columns: invalid_columns
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Validates whether the selected columns are allowed for export
|
|
88
|
+
def validate_exportable_columns(columns)
|
|
89
|
+
unless columns.is_a?(Array)
|
|
90
|
+
raise SpotlightSearch::Exceptions::InvalidValue, 'Expected Array. Invalid type received'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
included do
|
|
98
|
+
class_attribute :enabled_columns, instance_accessor: false, default: nil
|
|
99
|
+
class_attribute :enabled_associated_columns, instance_accessor: false, default: nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -48,9 +48,14 @@ module SpotlightSearch
|
|
|
48
48
|
tag.div class: "modal-body" do
|
|
49
49
|
form_tag '/spotlight_search/export_to_file', id: 'export-to-file-form', style: "width: 100%;" do
|
|
50
50
|
concat hidden_field_tag 'email', email, id: 'export-to-file-email'
|
|
51
|
-
concat hidden_field_tag 'filters', nil, id: 'export-to-file-filters'
|
|
51
|
+
concat hidden_field_tag 'filters', nil, id: 'export-to-file-filters' # Filters are not being sent
|
|
52
52
|
concat hidden_field_tag 'klass', klass.to_s, id: 'export-to-file-klass'
|
|
53
|
-
|
|
53
|
+
case SpotlightSearch.exportable_columns_version
|
|
54
|
+
when :v1
|
|
55
|
+
concat checkbox_row(klass)
|
|
56
|
+
when :v2
|
|
57
|
+
concat checkbox_row_v2(klass)
|
|
58
|
+
end
|
|
54
59
|
concat submit_tag 'Export as excel', class: 'btn btn-bordered export-to-file-btn'
|
|
55
60
|
end
|
|
56
61
|
end
|
|
@@ -67,7 +72,22 @@ module SpotlightSearch
|
|
|
67
72
|
def create_checkbox(column_name)
|
|
68
73
|
tag.div class: "col-md-4" do
|
|
69
74
|
concat check_box_tag "columns[]", column_name.to_s
|
|
70
|
-
concat column_name.to_s
|
|
75
|
+
concat column_name.to_s.humanize
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def checkbox_row_v2(klass)
|
|
80
|
+
tag.div class: "row" do
|
|
81
|
+
SpotlightSearch::Utils.serialize_csv_columns(*klass.enabled_columns).each do |column_path|
|
|
82
|
+
concat create_checkbox_v2(column_path)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_checkbox_v2(column_path)
|
|
88
|
+
tag.div class: "col-md-4" do
|
|
89
|
+
concat check_box_tag "columns[]", column_path
|
|
90
|
+
concat column_path.to_s.split('/').join('_').humanize
|
|
71
91
|
end
|
|
72
92
|
end
|
|
73
93
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
module SpotlightSearch::Utils
|
|
4
|
+
class << self
|
|
5
|
+
def serialize_csv_columns(*columns, **hashes)
|
|
6
|
+
# Turns an arbitrary list of args and kwargs into a list of params to be used in a form
|
|
7
|
+
# For example, turns SpotlightSearch::Utils.serialize_csv_columns(:a, :b, c: [:d, e: :h], f: :g)
|
|
8
|
+
# into [:a, :b, "c/d", "c/e/h", "f/g"]
|
|
9
|
+
columns + hashes.map do |key, value|
|
|
10
|
+
serialize_csv_columns(*value).map do |column|
|
|
11
|
+
"#{key}/#{column}"
|
|
12
|
+
end
|
|
13
|
+
end.reduce([], :+)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def deserialize_csv_columns(list, method)
|
|
17
|
+
# Does the opposite operation of the above
|
|
18
|
+
list.reduce(recursive_hash) do |acc, item|
|
|
19
|
+
tokens = item.to_s.split('/')
|
|
20
|
+
send(method, acc, tokens.shift, tokens)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def base(hash, key, tokens) # recursive
|
|
25
|
+
hash.empty? && hash = { columns: [], associations: recursive_hash }
|
|
26
|
+
if tokens.empty?
|
|
27
|
+
# base case
|
|
28
|
+
hash[:columns] << key
|
|
29
|
+
else
|
|
30
|
+
# recursive case
|
|
31
|
+
# hash[:associations] ||= {}
|
|
32
|
+
hash[:associations][key] = base(hash[:associations][key], tokens.shift, tokens)
|
|
33
|
+
end
|
|
34
|
+
hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def as_json_params(hash, key, tokens)
|
|
38
|
+
hash.empty? && hash = { only: [], methods: [], include: recursive_hash }
|
|
39
|
+
if tokens.empty?
|
|
40
|
+
# base case
|
|
41
|
+
hash[:methods] << key
|
|
42
|
+
else
|
|
43
|
+
# recursive case
|
|
44
|
+
# hash[:associations] ||= {}
|
|
45
|
+
hash[:include][key] = as_json_params(hash[:include][key], tokens.shift, tokens)
|
|
46
|
+
end
|
|
47
|
+
hash
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def recursive_hash
|
|
51
|
+
func = ->(h, k) { h[k] = Hash.new(&func) }
|
|
52
|
+
# This hash creates a new hash, infinitely deep, whenever a value is not found
|
|
53
|
+
# recursive_hash[:a][:b][:c][:d] will never fail
|
|
54
|
+
Hash.new(&func)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def flatten_hash(hash, prefix="", separator="_")
|
|
58
|
+
hash.reduce({}) do |acc, item|
|
|
59
|
+
case item[1]
|
|
60
|
+
when Hash
|
|
61
|
+
acc.merge(flatten_hash(item[1], "#{prefix}#{item[0]}#{separator}"))
|
|
62
|
+
else
|
|
63
|
+
acc.merge("#{prefix}#{item[0]}" => item[1])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spotlight_search
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anbazhagan Palani
|
|
@@ -123,8 +123,10 @@ files:
|
|
|
123
123
|
- lib/spotlight_search/engine.rb
|
|
124
124
|
- lib/spotlight_search/exceptions.rb
|
|
125
125
|
- lib/spotlight_search/exportable_columns.rb
|
|
126
|
+
- lib/spotlight_search/exportable_columns_v2.rb
|
|
126
127
|
- lib/spotlight_search/helpers.rb
|
|
127
128
|
- lib/spotlight_search/railtie.rb
|
|
129
|
+
- lib/spotlight_search/utils.rb
|
|
128
130
|
- lib/spotlight_search/version.rb
|
|
129
131
|
- spotlight_search.gemspec
|
|
130
132
|
homepage: https://github.com/commutatus/spotlight-search
|