attr_searchable 0.0.3 → 0.0.4

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