rad_core_rails 0.5.3 → 0.7.3

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: 542155bff8ff325428b30760ef13fda51209abd8d3f828fe6f4e8d6ebaa27c79
4
- data.tar.gz: 31a9ee127dd3dd4639df4f8504083521f6587f1d67d9ed64498ef9b38e163627
3
+ metadata.gz: c5230b7df1cd2ab97fe6564245ecf4e2db25cc3a94347c671ce7ba0130ff97a9
4
+ data.tar.gz: aec5f63b9beaf6bb4d944b48bae11f4013c234c9b19f65ac94117df0f99e3c13
5
5
  SHA512:
6
- metadata.gz: 5d0404693b4bad560238af7f4c459fd3cda9075ac720163b818d38e8d12e49bcc1cf14a503dc4f09a361eec550f652b8936ad476b3da36e980073a8a67c2eae0
7
- data.tar.gz: e9a5a0ba37595e5a3ab1326b9af740bab64305801867660275e63b3e98cebf0a7d5d2d3191b3fe8abebb336382262342dcc95c82ae2ecb4af64018a8917d9f05
6
+ metadata.gz: 8499c89d340f431ea160f9aa49bd82bef51ad59ba9468e3503f4e398234e770ffdc23f3c17e0475f0e90a44bf5a42c0b1d0c44ee06385968c83ef562f8da33a5
7
+ data.tar.gz: d674bb003a850988e2896aa67ed0d8cf991c9359e9d3878d21ac8bd2e908b98d6552c2a2ab214d3dead033282201157cace3e3e6e0d4c60902f4bd5c889a9c44
data/.gitignore CHANGED
@@ -8,9 +8,10 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  Gemfile.lock
11
- bin
11
+ /bin/
12
12
  *.gem
13
13
  .rspec_status
14
14
  .ruby-gemset
15
15
  .ruby-version
16
16
  /.byebug_history
17
+ .robocop.yml
@@ -0,0 +1,4 @@
1
+ AllCops:
2
+ SuggestExtensions: false
3
+ Style/FrozenStringLiteralComment:
4
+ Enabled: false
data/README.md CHANGED
@@ -22,7 +22,67 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ For basic functionality just include `RadCoreRails` to your `ActiveRecord` model.
26
+
27
+ ```ruby
28
+ class Job < ActiveRecord::Base
29
+ include RadCoreRails
30
+ ...
31
+ end
32
+ ```
33
+
34
+ For using Zip Codes by Radius filter follow the steps below:
35
+
36
+ Step 1 - generate migration:
37
+
38
+ ```shell script
39
+ rails generate rad_core_rails:zip_codes_migration
40
+ ```
41
+
42
+ Step 2 - migrate DB:
43
+
44
+ ```shell script
45
+ rails db:migrate
46
+ ```
47
+
48
+ Step 3 - import all US zip-codes to your DB:
49
+ > this gem has built in JSON file with all US zip-codes
50
+
51
+ ```shell script
52
+ rails rad_core_rails:import_zip_codes
53
+ ```
54
+
55
+ Step 4 - add new filter to `filter_manifest` method like on example:
56
+
57
+ > OLD Syntax
58
+ ```ruby
59
+ class Job < ActiveRecord::Base
60
+ include RadCoreRails
61
+
62
+ def self.filter_manifest
63
+ {
64
+ zip_codes: ->(filter) { generate_zip_codes_clauses('addresses.zip', filter) }
65
+ }.with_indifferent_access
66
+ end
67
+ end
68
+ ```
69
+
70
+ > NEW Syntax starting from v0.7.0
71
+
72
+ ```ruby
73
+ class Job < ActiveRecord::Base
74
+ include RadCoreRails
75
+
76
+ filterable zip_codes: [:zip_codes, 'addresses.zip']
77
+ end
78
+ ```
79
+
80
+ Step 5 - filter params example
81
+
82
+ ```ruby
83
+ { key: 'zip_codes', option: '15', values: ['85001'] }
84
+ ```
85
+
26
86
 
27
87
  ## Development
28
88
 
data/Rakefile CHANGED
@@ -1,6 +1,11 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require './support/active_record_rake_tasks'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
7
  task :default => :spec
8
+
9
+ # Stub the :environment task for tasks like db:migrate & db:seed. Since this is a Gem we've explicitly required all
10
+ # dependent files in the needed places and we don't have to load the entire environment.
11
+ task :environment
@@ -0,0 +1,17 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :zip_codes do |t|
4
+ t.string :city
5
+ t.string :state
6
+ t.string :zip
7
+ t.decimal :latitude, precision: 10, scale: 6
8
+ t.decimal :longitude, precision: 10, scale: 6
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :zip_codes, :zip, unique: true
14
+ add_index :zip_codes, :latitude
15
+ add_index :zip_codes, :longitude
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module RadCoreRails
4
+ module Generators
5
+ class ZipCodesMigrationGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ def copy_migration
10
+ migration_template "migration.rb", "db/migrate/create_zip_codes.rb", migration_version: migration_version
11
+ end
12
+
13
+ def migration_version
14
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,15 +1,18 @@
1
- require "rad_core_rails/version"
2
- require "active_support/concern"
3
- require "active_support/core_ext"
1
+ # Need some refactoring: https://ankane.org/gem-patterns
4
2
  require "active_record"
5
- require 'rad_core_rails/query_generator'
6
- require 'rad_core_rails/search_terms'
7
- require 'rad_core_rails/sortable'
3
+
4
+ require "rad_core_rails/railtie" if defined?(Rails)
5
+ require "rad_core_rails/version"
6
+ require "rad_core_rails/query_generator"
7
+ require "rad_core_rails/search_terms"
8
+ require "rad_core_rails/sortable"
8
9
 
9
10
  module RadCoreRails
11
+ autoload :ZipCode, 'rad_core_rails/zip_code'
12
+
10
13
  def self.included(receiver)
11
14
  receiver.send :include, Sortable, SearchTerms, QueryGenerator
12
15
  end
13
16
 
14
17
  class Error < StandardError; end
15
- end
18
+ end
@@ -11,13 +11,17 @@ module RadCoreRails
11
11
  @searchable_columns = handle_columns(columns)
12
12
  end
13
13
 
14
+ def filterable(filters_data = [])
15
+ @filter_manifest = handle_filterable(filters_data)
16
+ end
17
+
14
18
  def joins_clause
15
19
  <<-SQL
16
20
  SQL
17
21
  end
18
22
 
19
23
  def filter_manifest
20
- {}.with_indifferent_access
24
+ @filter_manifest || {}.with_indifferent_access
21
25
  end
22
26
 
23
27
  def create_filters(search, filters)
@@ -29,33 +33,102 @@ module RadCoreRails
29
33
  args << arg
30
34
  end
31
35
  filters.map do |filter|
32
- clause, arguments = filter_manifest[filter[:key]].call(filter)
33
- query << clause
34
- arguments.each do |arg|
35
- args << arg
36
+ begin
37
+ clause, arguments = filter_manifest[filter[:key]].call(filter)
38
+ query << clause
39
+ arguments.each do |arg|
40
+ args << arg
41
+ end
42
+ rescue NoMethodError
43
+ raise NoMethodError, "Filter with the name `#{filter[:key]}` doesn't exist."
36
44
  end
37
45
  end
38
46
  [query.reject(&:blank?).join(' AND '), args]
39
47
  end
40
48
 
49
+ # def generate_search_clause(search)
50
+ # and_args = []
51
+ # or_args = []
52
+ # if search.present? && searchable_columns.is_a?(Array)
53
+ # # search_term_size = search.split(' ').length
54
+ # and_terms = search.split(' ').select { |term| !term.include?('+') }
55
+ # or_terms = search.split(' ').select { |term| term.include?('+') }
56
+ # and_columns = []
57
+ # or_columns = []
58
+ # # just_ors = search_term_size == or_term_size
59
+ # # operand = just_ors ? 'OR' : 'AND'
60
+ # # search_terms = just_ors == true ? search.split(' ') : search.split(' ').select { |term| !term.include?('+') }
61
+ # and_terms.each do |term|
62
+ # columns = []
63
+ # and_terms.each do |col|
64
+ # and_columns.push("(LOWER(#{col}) ILIKE ?)")
65
+ # and_args.push '%' + term.downcase.strip + '%'
66
+ # end
67
+ # or_terms.each do |col|
68
+ # columns.push("(LOWER(#{col}) ILIKE ?)")
69
+ # or_args.push '%' + term[1, term.length].downcase.strip + '%'
70
+ # end
71
+ # clause = if or_columns.empty?
72
+ # '(' + and_columns.join(' OR ') + ')'
73
+ # else
74
+ # '(' + '(' + and_columns.join(' OR ') + ')' + 'AND' + '(' + or_columns.join(' OR ') + ')' + ')'
75
+ # end
76
+ # end
77
+ # [clause, and_args + or_args]
78
+ # else
79
+ # ['', []]
80
+ # end
81
+ # end
82
+
83
+ # @searchable_columns, is an array of column names that you can compare to your terms.
84
+ #
41
85
  def generate_search_clause(search)
42
- args = []
43
86
  if search.present? && searchable_columns.is_a?(Array)
44
- search_term_size = search.split(' ').length
45
- or_term_size = search.split(' ').select { |term| term.include?('+') }.length
46
- just_ors = search_term_size == or_term_size
47
- operand = just_ors ? 'OR' : 'AND'
48
- search_terms = just_ors == true ? search.split(' ') : search.split(' ').select { |term| !term.include?('+') }
49
- clause = []
50
- search_terms.each do |term|
87
+ # holds all of the sql from sanitized_and_terms
88
+ and_args = []
89
+ # holds all the table columns sql for all the AND terms
90
+ and_clause = []
91
+ # holds all of the sql from sanitized_or_terms
92
+ or_args = []
93
+ # holds all the table columns sql for all the OR terms
94
+ or_clause = []
95
+ # extract and terms, remove casing, and add ILIKE '%' comparisions
96
+ sanitized_and_terms = search.split(' ')
97
+ .reject { |term| term.include?('+') }
98
+ .map { |term| '%' + term.downcase.strip + '%' }
99
+ # extract or terms, remove casing, and add ILIKE '%' comparisions
100
+ sanitized_or_terms = search.split(' ')
101
+ .select { |term| term.include?('+') }
102
+ .map { |term| '%' + term[1, term.length].downcase.strip + '%' }
103
+ # loop through sanitized_and_terms to find all possible columns.
104
+ sanitized_and_terms.each do |sanitized_term|
51
105
  columns = []
106
+ # all possible columns where this should be searched
52
107
  searchable_columns.each do |col|
53
108
  columns.push("(LOWER(#{col}) ILIKE ?)")
54
- args.push '%' + (operand == 'OR' ? term[1, term.length].downcase.strip : term.downcase.strip) + '%'
109
+ and_args.push sanitized_term
55
110
  end
56
- clause.push '(' + columns.join(' OR ') + ')'
111
+ and_clause.push '(' + columns.join(' OR ') + ')'
112
+ end
113
+ # loop through sanitized_or_terms to find all possible columns.
114
+ sanitized_or_terms.each do |sanitized_term|
115
+ columns = []
116
+ # all possible columns where this should be searched
117
+ searchable_columns.each do |col|
118
+ columns.push("(LOWER(#{col}) ILIKE ?)")
119
+ or_args.push sanitized_term
120
+ end
121
+ or_clause.push '(' + columns.join(' OR ') + ')'
122
+ end
123
+ if or_clause.empty? && and_clause.empty?
124
+ ['', []]
125
+ elsif or_clause.empty?
126
+ ['(' + and_clause.join(' AND ') + ')', and_args]
127
+ elsif and_clause.empty?
128
+ ['(' + or_clause.join(' OR ') + ')', or_args]
129
+ else
130
+ ['((' + and_clause.join(' AND ') + ') AND (' + or_clause.join(' OR ') + '))', and_args + or_args]
57
131
  end
58
- ['(' + clause.join(" #{operand} ") + ')', args]
59
132
  else
60
133
  ['', []]
61
134
  end
@@ -74,15 +147,24 @@ module RadCoreRails
74
147
  str = if optional_exclusion_clause.present? && filter[:option] == '!='
75
148
  optional_exclusion_clause
76
149
  else
77
- "(#{column_name} #{filter[:option]} ANY(ARRAY[?]))"
150
+ # probably should fix this.
151
+ # somtimes we arent sending a filter option, so just default it to =
152
+ "(#{column_name} #{filter[:option] || '='} ANY(ARRAY[?]))"
78
153
  end
79
154
  args = [filter[:values]]
80
155
 
81
156
  [str, args]
82
157
  end
83
158
 
159
+ def generate_zip_codes_clause(column_name, filter)
160
+ str = "(#{column_name} = ANY(#{zip_code_class.distance_query}))"
161
+ args = zip_code_class.distance_args(filter)
162
+
163
+ [str, args]
164
+ end
165
+
84
166
  def generate_array_clause(column_name, filter)
85
- str = "(#{column_name} && ARRAY[?])"
167
+ str = "(#{column_name}::text[] && ARRAY[?])"
86
168
  args = [filter[:values]]
87
169
 
88
170
  [str, args]
@@ -169,6 +251,29 @@ module RadCoreRails
169
251
  end
170
252
  end
171
253
  end
254
+
255
+ def handle_filterable(filters_data)
256
+ return nil if filters_data.empty?
257
+
258
+ @zip_code_class = filters_data.delete(:zip_code_class)
259
+
260
+ result = {}
261
+
262
+ filters_data.each do |filter_data|
263
+ filter_type, filter_column = filter_data.last
264
+ result[filter_data.first] = lambda { |filter|
265
+ public_send("generate_#{filter_type}_clause", filter_column, filter)
266
+ }
267
+ end
268
+
269
+ result.with_indifferent_access
270
+ end
271
+
272
+ def zip_code_class
273
+ return RadCoreRails::ZipCode unless @zip_code_class
274
+
275
+ @zip_code_class.to_s.constantize
276
+ end
172
277
  end
173
278
  end
174
279
  end
@@ -0,0 +1,7 @@
1
+ module RadCoreRails
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/rad_core_rails.rake"
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RadCoreRails
4
- VERSION = '0.5.3'
4
+ VERSION = '0.7.3'
5
5
  end
@@ -0,0 +1,54 @@
1
+ module RadCoreRails
2
+ class ZipCode < ActiveRecord::Base
3
+ self.table_name = 'zip_codes'
4
+
5
+ validates_presence_of :city, :state, :zip, :latitude, :longitude
6
+
7
+ class << self
8
+ # Query for returning a one latitude for given zip-code
9
+ def lat_sub_query
10
+ <<-SQL
11
+ SELECT zip_codes.latitude
12
+ FROM zip_codes
13
+ WHERE zip = ?
14
+ LIMIT 1
15
+ SQL
16
+ end
17
+
18
+ # Query for returning a one longitude value for given zip-code
19
+ def lng_sub_query
20
+ <<-SQL
21
+ SELECT zip_codes.longitude
22
+ FROM zip_codes
23
+ WHERE zip = ?
24
+ LIMIT 1
25
+ SQL
26
+ end
27
+
28
+ # Query for returning an array of zip codes around the zip-code sent to this function with a given radius in miles.
29
+ def distance_query
30
+ <<-SQL
31
+ WITH t AS (SELECT
32
+ zip, (
33
+ 3959 * acos (
34
+ cos(RADIANS((#{lat_sub_query})))
35
+ * cos(RADIANS(zip_codes.latitude))
36
+ * cos(RADIANS(zip_codes.longitude) - RADIANS((#{lng_sub_query})))
37
+ + sin(RADIANS((#{lat_sub_query})))
38
+ * sin(RADIANS(zip_codes.latitude))
39
+ )
40
+ ) AS distance
41
+ FROM zip_codes) SELECT t.zip FROM t WHERE t.distance < ?
42
+ SQL
43
+ end
44
+
45
+ # Query arguments for the `distance_query` based on passed filter params.
46
+ def distance_args(filter)
47
+ zip_value = filter[:values].try(:first) || ''
48
+ distance_miles = filter[:option] || 0
49
+
50
+ [zip_value, zip_value, zip_value, distance_miles]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,43 @@
1
+ require "rad_core_rails/zip_code"
2
+
3
+ module RadCoreRails
4
+ class ZipImporter
5
+ attr_reader :file_path, :zip_codes_data
6
+
7
+ def initialize(file_path)
8
+ @file_path = file_path
9
+ end
10
+
11
+ def call
12
+ puts "Parsing file..."
13
+ parse_data
14
+ puts "Parsing completed."
15
+
16
+ puts "Importing data..."
17
+ import_data!
18
+ puts "Successfully imported!"
19
+
20
+ true
21
+ end
22
+
23
+ private
24
+
25
+ def import_data!
26
+ zip_codes_data.each do |zip_code_data|
27
+ fields = zip_code_data["fields"]
28
+
29
+ RadCoreRails::ZipCode.find_or_create_by!(
30
+ city: fields["city"].to_s,
31
+ state: fields["state"].to_s,
32
+ zip: fields["zip"].to_s,
33
+ latitude: fields["latitude"].to_s,
34
+ longitude: fields["longitude"].to_s
35
+ )
36
+ end
37
+ end
38
+
39
+ def parse_data
40
+ @zip_codes_data = JSON.parse(IO.read(file_path))
41
+ end
42
+ end
43
+ end