attr_searchable 0.0.3 → 0.0.4

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.
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![Dependency Status](https://gemnasium.com/mrkamel/attr_searchable.png?travis)](https://gemnasium.com/mrkamel/attr_searchable)
6
6
  [![Gem Version](https://badge.fury.io/rb/attr_searchable.svg)](http://badge.fury.io/rb/attr_searchable)
7
7
 
8
+ ![attr_searchable](https://raw.githubusercontent.com/mrkamel/attr_searchable_logo/master/attr_searchable.png)
9
+
8
10
  AttrSearchable extends your ActiveRecord models to support fulltext search
9
11
  engine like queries via simple query strings and hash-based queries. Assume you
10
12
  have a `Book` model having various attributes like `title`, `author`, `stock`,
@@ -218,6 +220,28 @@ from from the usual non-fulltext indices. Thus, you should add a usual index on
218
220
  every column you expose to search queries plus a fulltext index for every
219
221
  fulltext attribute.
220
222
 
223
+ In case you can't use fulltext indices, because you're e.g. still on MySQL 5.5
224
+ while using InnoDB or another RDBMS without fulltext support, you can make your
225
+ RDBMS use usual non-fulltext indices for string columns if you don't need the
226
+ left wildcard within `LIKE` queries. Simply supply the following option:
227
+
228
+ ```ruby
229
+ class User < ActiveRecord::Base
230
+ include AttrSearchable
231
+
232
+ attr_searchable :username
233
+ attr_searchable_options :username, :left_wildcard => false
234
+
235
+ # ...
236
+ ```
237
+
238
+ such that AttrSearchable will omit the left most wildcard.
239
+
240
+ ```ruby
241
+ User.search("admin")
242
+ # ... WHERE users.username LIKE 'admin%'
243
+ ```
244
+
221
245
  ## Associations
222
246
 
223
247
  If you specify searchable attributes from another model, like
@@ -267,6 +291,44 @@ end
267
291
  AttrSearchable will then skip any association auto loading and will use
268
292
  the `search_scope` instead.
269
293
 
294
+ ## Custom table names and associations
295
+
296
+ AttrSearchable tries to infer a model's class name and SQL alias from the
297
+ specified attributes to autodetect datatype definitions, etc. This usually
298
+ works quite fine. In case you're using custom table names via `self.table_name
299
+ = ...` or if a model is associated multiple times, AttrSearchable however can't
300
+ infer the class and alias names, e.g.
301
+
302
+ ```ruby
303
+ class Book < ActiveRecord::Base
304
+ # ...
305
+
306
+ has_many :users, :through => :comments
307
+ belongs_to :user
308
+
309
+ attr_searchable :user => ["users.username", "users_books.username"]
310
+
311
+ # ...
312
+ end
313
+ ```
314
+
315
+ Here, for queries to work you have to use `users_books.username`, because
316
+ ActiveRecord assigns a different SQL alias for users within its SQL queries,
317
+ because the user model is associated multiple times. However, as AttrSearchable
318
+ now can't infer the `User` model from `users_books`, you have to add:
319
+
320
+ ```ruby
321
+ class Book < ActiveRecord::Base
322
+ # ...
323
+
324
+ attr_searchable_alias :users_books => :user
325
+
326
+ # ...
327
+ end
328
+ ```
329
+
330
+ to tell AttrSearchable about the custom SQL alias and mapping.
331
+
270
332
  ## Supported operators
271
333
 
272
334
  Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
@@ -360,6 +422,12 @@ Book.unsafe_search("stock: None") # => raise AttrSearchable::IncompatibleDatatyp
360
422
 
361
423
  ## Changelog
362
424
 
425
+ Version 0.0.4:
426
+
427
+ * Fixed date attributes
428
+ * Fail softly for mixed datatype attributes
429
+ * Support custom table, class and alias names via attr_searchable_alias
430
+
363
431
  Version 0.0.3:
364
432
 
365
433
  * belongs_to association fixes
@@ -18,8 +18,8 @@ module AttrSearchable
18
18
  class ParseError < RuntimeError; end
19
19
 
20
20
  module Parser
21
- def self.parse(arg, model)
22
- arg.is_a?(Hash) ? parse_hash(arg, model) : parse_string(arg, model)
21
+ def self.parse(query, model)
22
+ query.is_a?(Hash) ? parse_hash(query, model) : parse_string(query, model)
23
23
  end
24
24
 
25
25
  def self.parse_hash(hash, model)
@@ -40,6 +40,9 @@ module AttrSearchable
40
40
  base.class_attribute :searchable_attribute_options
41
41
  base.searchable_attribute_options = {}
42
42
 
43
+ base.class_attribute :searchable_attribute_aliases
44
+ base.searchable_attribute_aliases = {}
45
+
43
46
  base.extend ClassMethods
44
47
  end
45
48
 
@@ -61,22 +64,30 @@ module AttrSearchable
61
64
  end
62
65
 
63
66
  def attr_searchable_options(key, options = {})
64
- self.searchable_attribute_options[key.to_s] = (self.searchable_attribute_options[key.to_s] || {}).merge(options)
67
+ self.searchable_attribute_options[key.to_s] = (searchable_attribute_options[key.to_s] || {}).merge(options)
68
+ end
69
+
70
+ def attr_searchable_alias(hash)
71
+ hash.each do |key, value|
72
+ self.searchable_attribute_aliases[key.to_s] = value.respond_to?(:table_name) ? value.table_name : value.to_s
73
+ end
65
74
  end
66
75
 
67
- def search(*args)
68
- unsafe_search *args
76
+ def search(query)
77
+ unsafe_search query
69
78
  rescue AttrSearchable::RuntimeError
70
79
  respond_to?(:none) ? none : where("1 = 0")
71
80
  end
72
81
 
73
- def unsafe_search(arg)
74
- return respond_to?(:scoped) ? scoped : all if arg.blank?
82
+ def unsafe_search(query)
83
+ return respond_to?(:scoped) ? scoped : all if query.blank?
84
+
85
+ associations = searchable_attributes.values.flatten.uniq.collect { |column| column.split(".").first }.collect { |column| searchable_attribute_aliases[column] || column.to_sym }
75
86
 
76
87
  scope = respond_to?(:search_scope) ? search_scope : nil
77
- scope ||= eager_load(searchable_attributes.values.flatten.uniq.collect { |column| column.split(".").first.to_sym } - [name.tableize.to_sym])
88
+ scope ||= eager_load(associations - [name.tableize.to_sym])
78
89
 
79
- scope.where AttrSearchable::Parser.parse(arg, self).optimize!.to_sql(self)
90
+ scope.where AttrSearchable::Parser.parse(query, self).optimize!.to_sql(self)
80
91
  end
81
92
  end
82
93
  end
@@ -1,3 +1,3 @@
1
1
  module AttrSearchable
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -87,9 +87,9 @@ module AttrSearchableGrammar
87
87
  keys = model.searchable_attribute_options.select { |key, value| value[:default] == true }.keys
88
88
  keys = model.searchable_attributes.keys if keys.empty?
89
89
 
90
- queries = keys.collect { |key| collection_for key }.select { |attribute| attribute.compatible? text_value }.collect { |attribute| attribute.matches text_value }
90
+ queries = keys.collect { |key| collection_for key }.select { |collection| collection.compatible? text_value }.collect { |collection| collection.matches text_value }
91
91
 
92
- raise AttrSearchable::NoSearchableAttributes unless model.searchable_attributes
92
+ raise AttrSearchable::NoSearchableAttributes if queries.empty?
93
93
 
94
94
  queries.flatten.inject(:or)
95
95
  end
@@ -55,13 +55,24 @@ module AttrSearchableGrammar
55
55
  @attributes ||= model.searchable_attributes[key].collect { |attribute_definition| attribute_for attribute_definition }
56
56
  end
57
57
 
58
+ def klass_for(table)
59
+ klass = model.searchable_attribute_aliases[table]
60
+ klass ||= table
61
+
62
+ klass.classify.constantize
63
+ end
64
+
65
+ def alias_for(table)
66
+ (model.searchable_attribute_aliases[table] && table) || klass_for(table).table_name
67
+ end
68
+
58
69
  def attribute_for(attribute_definition)
59
70
  table, column = attribute_definition.split(".")
60
- klass = table.classify.constantize
71
+ klass = klass_for(table)
61
72
 
62
73
  raise(AttrSearchable::UnknownAttribute, "Unknown attribute #{attribute_definition}") unless klass.columns_hash[column]
63
74
 
64
- Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(klass.table_name)[column], klass, options)
75
+ Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(alias_for(table))[column], klass, options)
65
76
  end
66
77
  end
67
78
 
@@ -148,16 +159,16 @@ module AttrSearchableGrammar
148
159
  ::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
149
160
  elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
150
161
  ::Time.new($3, $1, 15).beginning_of_month .. ::Time.new($3, $1, 15).end_of_month
151
- elsif value !~ /:/
162
+ elsif value !~ /:/
152
163
  time = ::Time.parse(value)
153
164
  time.beginning_of_day .. time.end_of_day
154
165
  else
155
166
  time = ::Time.parse(value)
156
167
  time .. time
157
- end
168
+ end
158
169
  rescue ArgumentError
159
170
  raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
160
- end
171
+ end
161
172
 
162
173
  def map(value)
163
174
  parse(value).first
@@ -180,8 +191,20 @@ module AttrSearchableGrammar
180
191
 
181
192
  class Date < Datetime
182
193
  def parse(value)
183
- dates = super(value).collect { |time| ::Time.parse(time).to_date }
184
- dates.first .. dates.last
194
+ return value .. value unless value.is_a?(::String)
195
+
196
+ if value =~ /^[0-9]{4,}$/
197
+ ::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
198
+ elsif value =~ /^([0-9]{4,})(\.|-|\/)([0-9]{1,2})$/
199
+ ::Date.new($1.to_i, $3.to_i, 15).beginning_of_month .. ::Date.new($1.to_i, $3.to_i, 15).end_of_month
200
+ elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
201
+ ::Date.new($3.to_i, $1.to_i, 15).beginning_of_month .. ::Date.new($3.to_i, $1.to_i, 15).end_of_month
202
+ else
203
+ date = ::Date.parse(value)
204
+ date .. date
205
+ end
206
+ rescue ArgumentError
207
+ raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
185
208
  end
186
209
  end
187
210
 
@@ -193,7 +216,7 @@ module AttrSearchableGrammar
193
216
  return false if value.to_s =~ /^(0|false|no)$/i
194
217
 
195
218
  raise AttrSearchable::IncompatibleDatatype, "Incompatible datatype for #{value}"
196
- end
219
+ end
197
220
  end
198
221
  end
199
222
  end
data/test/date_test.rb ADDED
@@ -0,0 +1,75 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class DateTest < AttrSearchable::TestCase
5
+ def test_mapping
6
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
7
+
8
+ assert_includes Product.search("created_on: 2014"), product
9
+ assert_includes Product.search("created_on: 2014-05"), product
10
+ assert_includes Product.search("created_on: 2014-05-01"), product
11
+ end
12
+
13
+ def test_anywhere
14
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
15
+
16
+ assert_includes Product.search("2014-05-01"), product
17
+ refute_includes Product.search("2014-05-02"), product
18
+ end
19
+
20
+ def test_includes
21
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
22
+
23
+ assert_includes Product.search("created_on: 2014-05-01"), product
24
+ refute_includes Product.search("created_on: 2014-05-02"), product
25
+ end
26
+
27
+ def test_equals
28
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
29
+
30
+ assert_includes Product.search("created_on = 2014-05-01"), product
31
+ refute_includes Product.search("created_on = 2014-05-02"), product
32
+ end
33
+
34
+ def test_equals_not
35
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
36
+
37
+ assert_includes Product.search("created_on != 2014-05-02"), product
38
+ refute_includes Product.search("created_on != 2014-05-01"), product
39
+ end
40
+
41
+ def test_greater
42
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
43
+
44
+ assert_includes Product.search("created_on < 2014-05-02"), product
45
+ refute_includes Product.search("created_on < 2014-05-01"), product
46
+ end
47
+
48
+ def test_greater_equals
49
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
50
+
51
+ assert_includes Product.search("created_on >= 2014-05-01"), product
52
+ refute_includes Product.search("created_on >= 2014-05-02"), product
53
+ end
54
+
55
+ def test_less
56
+ product = create(:product, :created_on => Date.parse("2014-05-01"))
57
+
58
+ assert_includes Product.search("created_on < 2014-05-02"), product
59
+ refute_includes Product.search("created_on < 2014-05-01"), product
60
+ end
61
+
62
+ def test_less_equals
63
+ product = create(:product, :created_on => Date.parse("2014-05-02"))
64
+
65
+ assert_includes Product.search("created_on <= 2014-05-02"), product
66
+ refute_includes Product.search("created_on <= 2014-05-01"), product
67
+ end
68
+
69
+ def test_incompatible_datatype
70
+ assert_raises AttrSearchable::IncompatibleDatatype do
71
+ Product.unsafe_search "created_on: Value"
72
+ end
73
+ end
74
+ end
75
+
data/test/test_helper.rb CHANGED
@@ -34,11 +34,12 @@ end
34
34
  class Product < ActiveRecord::Base
35
35
  include AttrSearchable
36
36
 
37
- attr_searchable :title, :description, :brand, :stock, :price, :created_at, :available
38
- attr_searchable :comment => ["comments.title", "comments.message"], :user => "users.username"
39
-
37
+ attr_searchable :title, :description, :brand, :stock, :price, :created_at, :created_on, :available
38
+ attr_searchable :comment => ["comments.title", "comments.message"], :user => ["users.username", "users_products.username"]
40
39
  attr_searchable :primary => [:title, :description]
41
40
 
41
+ attr_searchable_alias :users_products => :user
42
+
42
43
  if DATABASE != "sqlite"
43
44
  attr_searchable_options :title, :type => :fulltext
44
45
  attr_searchable_options :description, :type => :fulltext
@@ -51,6 +52,8 @@ class Product < ActiveRecord::Base
51
52
 
52
53
  has_many :comments
53
54
  has_many :users, :through => :comments
55
+
56
+ belongs_to :user
54
57
  end
55
58
 
56
59
  FactoryGirl.define do
@@ -69,11 +72,13 @@ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS comments"
69
72
  ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS users"
70
73
 
71
74
  ActiveRecord::Base.connection.create_table :products do |t|
75
+ t.references :user
72
76
  t.string :title
73
77
  t.text :description
74
78
  t.integer :stock
75
79
  t.float :price
76
80
  t.datetime :created_at
81
+ t.date :created_on
77
82
  t.boolean :available
78
83
  t.string :brand
79
84
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr_searchable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-06-26 00:00:00.000000000 Z
12
+ date: 2014-07-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: treetop
@@ -154,6 +154,7 @@ files:
154
154
  - test/attr_searchable_test.rb
155
155
  - test/boolean_test.rb
156
156
  - test/database.yml
157
+ - test/date_test.rb
157
158
  - test/datetime_test.rb
158
159
  - test/error_test.rb
159
160
  - test/float_test.rb
@@ -195,6 +196,7 @@ test_files:
195
196
  - test/attr_searchable_test.rb
196
197
  - test/boolean_test.rb
197
198
  - test/database.yml
199
+ - test/date_test.rb
198
200
  - test/datetime_test.rb
199
201
  - test/error_test.rb
200
202
  - test/float_test.rb