wvanbergen-scoped_search 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,12 @@
1
+ scoped_search 0.3.0
2
+ ===================
3
+ - Detection of column types so they can be handled properly
4
+ - Date based queries supported on date and time fields
5
+
6
+ scoped_search 0.2.0
7
+ ===================
8
+ - OR keyword supported in query language
9
+
10
+ scoped_search 0.1.0
11
+ ===================
12
+ - Initial version
data/TODO CHANGED
@@ -5,15 +5,12 @@ Contact willem AT vanbergen DOT org if you want to help out
5
5
 
6
6
  New features:
7
7
  - Search fields of associations as well
8
- - Allow other search operators than %LIKE%
8
+ - Allow smart searches. Example: "90 days ago", "Yesterday", "10 days from now"
9
9
 
10
10
  Refactoring:
11
- - Create a separate class to build the actual SQL queries.
12
11
  - For searchable_on(*fields) make it so that instead of fields it accepts options (a hash) where
13
12
  :only and :except can be values. That way it is possible for all fields to be loaded except
14
13
  the ones specified with :except.
15
- - Add checks for field types because the latest version of PostgreSQL (version 8.3.3) is more
16
- strict about searching for strings in columns that are not string types.
17
14
 
18
15
  Documentation & testing:
19
16
  - Put something useful in the wiki
@@ -0,0 +1,158 @@
1
+ module ScopedSearch
2
+
3
+ class QueryConditionsBuilder
4
+
5
+ # Build the query
6
+ def self.build_query(search_conditions, query_fields)
7
+ self.new.build(search_conditions, query_fields)
8
+ end
9
+
10
+ def initialize
11
+ @query_fields = nil
12
+ @query_params = {}
13
+ end
14
+
15
+
16
+ # Build the query
17
+ #
18
+ # Hash query_options : A hash of fields and field types.
19
+ #
20
+ # Exampe:
21
+ # search_conditions = [["Wes", :like], ["Hays", :not], ["Hello World", :like], ["Goodnight Moon", :not],
22
+ # ["Bob OR Wes", :or], ["Happy cow OR Sad Frog", :or], ["Man made OR Dogs", :or],
23
+ # ["Cows OR Frog Toys", :or], ['9/28/1980, :datetime]]
24
+ # query_fields = {:first_name => :string, :created_at => :datetime}
25
+ #
26
+ # Exceptons :
27
+ # 1) If urlParams does not contain a :controller key.
28
+ def build(search_conditions, query_fields)
29
+ raise 'search_conditions must be a hash' unless search_conditions.class.to_s == 'Array'
30
+ raise 'query_fields must be a hash' unless query_fields.class.to_s == 'Hash'
31
+ @query_fields = query_fields
32
+
33
+ conditions = []
34
+
35
+ search_conditions.each_with_index do |search_condition, index|
36
+ keyword_name = "keyword_#{index}".to_sym
37
+ conditions << case search_condition.last
38
+ #when :integer: integer_conditions(keyword_name, search_condition.first)
39
+
40
+ when :like: like_condition(keyword_name, search_condition.first)
41
+ when :not: not_like_condition(keyword_name, search_condition.first)
42
+
43
+ when :or: or_condition(keyword_name, search_condition.first)
44
+
45
+ when :less_than_date: less_than_date(keyword_name, search_condition.first)
46
+ when :less_than_or_equal_to_date: less_than_or_equal_to_date(keyword_name, search_condition.first)
47
+ when :as_of_date: as_of_date(keyword_name, search_condition.first)
48
+ when :greater_than_date: greater_than_date(keyword_name, search_condition.first)
49
+ when :greater_than_or_equal_to_date: greater_than_or_equal_to_date(keyword_name, search_condition.first)
50
+
51
+ when :between_dates: between_dates(keyword_name, search_condition.first)
52
+ end
53
+ end
54
+
55
+ [conditions.compact.join(' AND '), @query_params]
56
+ end
57
+
58
+
59
+ private
60
+
61
+ # def integer_condition(keyword_name, value)
62
+ # end
63
+
64
+ def like_condition(keyword_name, value)
65
+ @query_params[keyword_name] = "%#{value}%"
66
+ retVal = []
67
+ @query_fields.each do |field, field_type| #|key,value|
68
+ if field_type == :string or field_type == :text
69
+ retVal << "#{field} LIKE :#{keyword_name.to_s}"
70
+ end
71
+ end
72
+ "(#{retVal.join(' OR ')})"
73
+ end
74
+
75
+ def not_like_condition(keyword_name, value)
76
+ @query_params[keyword_name] = "%#{value}%"
77
+ retVal = []
78
+ @query_fields.each do |field, field_type| #|key,value|
79
+ if field_type == :string or field_type == :text
80
+ retVal << "(#{field} NOT LIKE :#{keyword_name.to_s} OR #{field} IS NULL)"
81
+ end
82
+ end
83
+ "(#{retVal.join(' AND ')})"
84
+ end
85
+
86
+ def or_condition(keyword_name, value)
87
+ retVal = []
88
+ word1, word2 = value.split(' OR ')
89
+ keyword_name_a = "#{keyword_name.to_s}a".to_sym
90
+ keyword_name_b = "#{keyword_name.to_s}b".to_sym
91
+ @query_params[keyword_name_a] = "%#{word1}%"
92
+ @query_params[keyword_name_b] = "%#{word2}%"
93
+ @query_fields.each do |field, field_type| #|key,value|
94
+ if field_type == :string or field_type == :text
95
+ retVal << "(#{field} LIKE :#{keyword_name_a.to_s} OR #{field} LIKE :#{keyword_name_b.to_s})"
96
+ end
97
+ end
98
+ "(#{retVal.join(' OR ')})"
99
+ end
100
+
101
+ def less_than_date(keyword_name, value)
102
+ helper_date_operation('<', keyword_name, value)
103
+ end
104
+
105
+ def less_than_or_equal_to_date(keyword_name, value)
106
+ helper_date_operation('<=', keyword_name, value)
107
+ end
108
+
109
+ def as_of_date(keyword_name, value)
110
+ helper_date_operation('=', keyword_name, value)
111
+ end
112
+
113
+ def greater_than_date(keyword_name, value)
114
+ helper_date_operation('>', keyword_name, value)
115
+ end
116
+
117
+ def greater_than_or_equal_to_date(keyword_name, value)
118
+ helper_date_operation('>=', keyword_name, value)
119
+ end
120
+
121
+ def between_dates(keyword_name, value)
122
+ date1, date2 = value.split(' TO ')
123
+ dt1 = Date.parse(date1) # This will throw an exception if it is not valid
124
+ dt2 = Date.parse(date2) # This will throw an exception if it is not valid
125
+ keyword_name_a = "#{keyword_name.to_s}a".to_sym
126
+ keyword_name_b = "#{keyword_name.to_s}b".to_sym
127
+ @query_params[keyword_name_a] = dt1.to_s
128
+ @query_params[keyword_name_b] = dt2.to_s
129
+
130
+ retVal = []
131
+ @query_fields.each do |field, field_type| #|key,value|
132
+ if field_type == :date or field_type == :datetime or field_type == :timestamp
133
+ retVal << "(#{field} BETWEEN :#{keyword_name_a.to_s} AND :#{keyword_name_b.to_s})"
134
+ end
135
+ end
136
+ "(#{retVal.join(' OR ')})"
137
+ rescue
138
+ # The date is not valid so just ignore it
139
+ return nil
140
+ end
141
+
142
+
143
+ def helper_date_operation(operator, keyword_name, value)
144
+ dt = Date.parse(value) # This will throw an exception if it is not valid
145
+ @query_params[keyword_name] = dt.to_s
146
+ retVal = []
147
+ @query_fields.each do |field, field_type| #|key,value|
148
+ if field_type == :date or field_type == :datetime or field_type == :timestamp
149
+ retVal << "#{field} #{operator} :#{keyword_name.to_s}"
150
+ end
151
+ end
152
+ "(#{retVal.join(' OR ')})"
153
+ rescue
154
+ # The date is not valid so just ignore it
155
+ return nil
156
+ end
157
+ end
158
+ end
@@ -23,38 +23,65 @@ module ScopedSearch
23
23
  else
24
24
  if /^.+[ ]OR[ ].+$/ =~ item
25
25
  conditions_tree << [item, :or]
26
+ elsif /^#{RegTokens::BetweenDateFormatMMDDYYYY}$/ =~ item or
27
+ /^#{RegTokens::BetweenDateFormatYYYYMMDD}$/ =~ item or
28
+ /^#{RegTokens::BetweenDatabaseFormat}$/ =~ item
29
+ conditions_tree << [item, :between_dates]
30
+ elsif /^#{RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
31
+ /^#{RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
32
+ /^#{RegTokens::GreaterThanOrEqualToDatabaseFormat}$/ =~ item
33
+ conditions_tree << [item, :greater_than_or_equal_to_date]
34
+ elsif /^#{RegTokens::LessThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
35
+ /^#{RegTokens::LessThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
36
+ /^#{RegTokens::LessThanOrEqualToDatabaseFormat}$/ =~ item
37
+ conditions_tree << [item, :less_than_or_equal_to_date]
38
+ elsif /^#{RegTokens::GreaterThanDateFormatMMDDYYYY}$/ =~ item or
39
+ /^#{RegTokens::GreaterThanDateFormatYYYYMMDD}$/ =~ item or
40
+ /^#{RegTokens::GreaterThanDatabaseFormat}$/ =~ item
41
+ conditions_tree << [item, :greater_than_date]
42
+ elsif /^#{RegTokens::LessThanDateFormatMMDDYYYY}$/ =~ item or
43
+ /^#{RegTokens::LessThanDateFormatYYYYMMDD}$/ =~ item or
44
+ /^#{RegTokens::LessThanDatabaseFormat}$/ =~ item
45
+ conditions_tree << [item, :less_than_date]
46
+ elsif /^#{RegTokens::DateFormatMMDDYYYY}$/ =~ item or
47
+ /^#{RegTokens::DateFormatYYYYMMDD}$/ =~ item or
48
+ /^#{RegTokens::DatabaseFormat}$/ =~ item
49
+ conditions_tree << [item, :as_of_date]
26
50
  else
27
51
  conditions_tree << (negate ? [item, :not] : [item, :like])
28
52
  negate = false
29
53
  end
30
54
  end
31
55
  end
56
+
32
57
  return conditions_tree
33
58
  end
34
-
35
- # **Patterns**
36
- # Each pattern is sperated by a "|". With regular expressions the order of the expression does matter.
37
- #
38
- # ([\w]+[ ]OR[ ][\w]+)
39
- # ([\w]+[ ]OR[ ]["][\w ]+["])
40
- # (["][\w ]+["][ ]OR[ ][\w]+)
41
- # (["][\w ]+["][ ]OR[ ]["][\w ]+["])
42
- # Any two combinations of letters, numbers and underscores that are seperated by " OR " (a single space must
43
- # be on each side of the "OR").
44
- # THESE COULD BE COMBINED BUT BECAUSE OF THE WAY PARSING WORKS THIS IS NOT DONE ON PURPOSE!!
45
- #
46
- # ([-]?[\w]+)
47
- # Any combination of letters, numbers and underscores that may or may not have a dash in front.
48
- #
49
- # ([-]?["][\w ]+["])
50
- # Any combination of letters, numbers, underscores and spaces within double quotes that may or may not have a dash in front.
59
+
51
60
  def tokenize(query)
52
- pattern = ['([\w]+[ ]OR[ ][\w]+)',
53
- '([\w]+[ ]OR[ ]["][\w ]+["])',
54
- '(["][\w ]+["][ ]OR[ ][\w]+)',
55
- '(["][\w ]+["][ ]OR[ ]["][\w ]+["])',
56
- '([-]?[\w]+)',
57
- '([-]?["][\w ]+["])']
61
+ pattern = [RegTokens::BetweenDateFormatMMDDYYYY,
62
+ RegTokens::BetweenDateFormatYYYYMMDD,
63
+ RegTokens::BetweenDatabaseFormat,
64
+ RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY,
65
+ RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD,
66
+ RegTokens::GreaterThanOrEqualToDatabaseFormat,
67
+ RegTokens::LessThanOrEqualToDateFormatMMDDYYYY,
68
+ RegTokens::LessThanOrEqualToDateFormatYYYYMMDD,
69
+ RegTokens::LessThanOrEqualToDatabaseFormat,
70
+ RegTokens::GreaterThanDateFormatMMDDYYYY,
71
+ RegTokens::GreaterThanDateFormatYYYYMMDD,
72
+ RegTokens::GreaterThanDatabaseFormat,
73
+ RegTokens::LessThanDateFormatMMDDYYYY,
74
+ RegTokens::LessThanDateFormatYYYYMMDD,
75
+ RegTokens::LessThanDatabaseFormat,
76
+ RegTokens::DateFormatMMDDYYYY,
77
+ RegTokens::DateFormatYYYYMMDD,
78
+ RegTokens::DatabaseFormat,
79
+ RegTokens::WordOrWord,
80
+ RegTokens::WordOrString,
81
+ RegTokens::StringOrWord,
82
+ RegTokens::StringOrString,
83
+ RegTokens::PossiblyNegatedWord,
84
+ RegTokens::PossiblyNegatedString]
58
85
  pattern = Regexp.new(pattern.join('|'))
59
86
 
60
87
  tokens = []
@@ -0,0 +1,51 @@
1
+ # Regular expression tokens to be used for parsing.
2
+ module RegTokens
3
+
4
+ WORD = '[\w]+'
5
+ SPACE = '[ ]'
6
+ STRING = '["][\w ]+["]'
7
+ OR = 'OR'
8
+ POSSIBLY_NEGATED = '[-]?'
9
+ MONTH = '[\d]{1,2}'
10
+ DAY = '[\d]{1,2}'
11
+ FULL_YEAR = '[\d]{4}'
12
+ LESS_THAN = '[<][ ]'
13
+ GREATER_THAN = '[>][ ]'
14
+ LESS_THAN_OR_EQUAL_TO = '[<][=][ ]'
15
+ GREATER_THAN_OR_EQUAL_TO = '[>][=][ ]'
16
+ TO = 'TO'
17
+
18
+ WordOrWord = "(#{WORD}#{SPACE}#{OR}#{SPACE}#{WORD})"
19
+ WordOrString = "(#{WORD}#{SPACE}#{OR}#{SPACE}#{STRING})"
20
+ StringOrWord = "(#{STRING}#{SPACE}#{OR}#{SPACE}#{WORD})"
21
+ StringOrString = "(#{STRING}#{SPACE}#{OR}#{SPACE}#{STRING})"
22
+ PossiblyNegatedWord = "(#{POSSIBLY_NEGATED}#{WORD})"
23
+ PossiblyNegatedString = "(#{POSSIBLY_NEGATED}#{STRING})"
24
+
25
+ DateFormatMMDDYYYY = "(#{MONTH}/#{DAY}/#{FULL_YEAR})" # This would be the same for DD/MM/YYYY
26
+ DateFormatYYYYMMDD = "(#{FULL_YEAR}/#{MONTH}/#{DAY})"
27
+ DatabaseFormat = "(#{FULL_YEAR}-#{MONTH}-#{DAY})"
28
+
29
+ LessThanDateFormatMMDDYYYY = "(#{LESS_THAN}#{MONTH}/#{DAY}/#{FULL_YEAR})"
30
+ LessThanDateFormatYYYYMMDD = "(#{LESS_THAN}#{FULL_YEAR}/#{MONTH}/#{DAY})"
31
+ LessThanDatabaseFormat = "(#{LESS_THAN}#{FULL_YEAR}-#{MONTH}-#{DAY})"
32
+
33
+ GreaterThanDateFormatMMDDYYYY = "(#{GREATER_THAN}#{MONTH}/#{DAY}/#{FULL_YEAR})"
34
+ GreaterThanDateFormatYYYYMMDD = "(#{GREATER_THAN}#{FULL_YEAR}/#{MONTH}/#{DAY})"
35
+ GreaterThanDatabaseFormat = "(#{GREATER_THAN}#{FULL_YEAR}-#{MONTH}-#{DAY})"
36
+
37
+ LessThanOrEqualToDateFormatMMDDYYYY = "(#{LESS_THAN_OR_EQUAL_TO}#{MONTH}/#{DAY}/#{FULL_YEAR})"
38
+ LessThanOrEqualToDateFormatYYYYMMDD = "(#{LESS_THAN_OR_EQUAL_TO}#{FULL_YEAR}/#{MONTH}/#{DAY})"
39
+ LessThanOrEqualToDatabaseFormat = "(#{LESS_THAN_OR_EQUAL_TO}#{FULL_YEAR}-#{MONTH}-#{DAY})"
40
+
41
+ GreaterThanOrEqualToDateFormatMMDDYYYY = "(#{GREATER_THAN_OR_EQUAL_TO}#{MONTH}/#{DAY}/#{FULL_YEAR})"
42
+ GreaterThanOrEqualToDateFormatYYYYMMDD = "(#{GREATER_THAN_OR_EQUAL_TO}#{FULL_YEAR}/#{MONTH}/#{DAY})"
43
+ GreaterThanOrEqualToDatabaseFormat = "(#{GREATER_THAN_OR_EQUAL_TO}#{FULL_YEAR}-#{MONTH}-#{DAY})"
44
+
45
+ BetweenDateFormatMMDDYYYY = "(#{MONTH}/#{DAY}/#{FULL_YEAR}#{SPACE}#{TO}#{SPACE}#{MONTH}/#{DAY}/#{FULL_YEAR})"
46
+ BetweenDateFormatYYYYMMDD = "(#{FULL_YEAR}/#{MONTH}/#{DAY}#{SPACE}#{TO}#{SPACE}#{FULL_YEAR}/#{MONTH}/#{DAY})"
47
+ BetweenDatabaseFormat = "(#{FULL_YEAR}-#{MONTH}-#{DAY}#{SPACE}#{TO}#{SPACE}#{FULL_YEAR}-#{MONTH}-#{DAY})"
48
+ end
49
+
50
+
51
+
data/lib/scoped_search.rb CHANGED
@@ -3,7 +3,9 @@ module ScopedSearch
3
3
  module ClassMethods
4
4
 
5
5
  def self.extended(base)
6
+ require 'scoped_search/reg_tokens'
6
7
  require 'scoped_search/query_language_parser'
8
+ require 'scoped_search/query_conditions_builder'
7
9
  end
8
10
 
9
11
  # Creates a named scope in the class it was called upon
@@ -16,49 +18,19 @@ module ScopedSearch
16
18
  # Build a hash that is used for the named_scope search_for.
17
19
  # This function will split the search_string into keywords, and search for all the keywords
18
20
  # in the fields that were provided to searchable_on
19
- def build_scoped_search_conditions(search_string)
21
+ def build_scoped_search_conditions(search_string)
20
22
  if search_string.nil? || search_string.strip.blank?
21
- return { :conditions => nil }
22
- else
23
- conditions = []
24
- query_params = {}
25
-
26
- QueryLanguageParser.parse(search_string).each_with_index do |search_condition, index|
27
- keyword_name = "keyword_#{index}".to_sym
28
- query_params[keyword_name] = "%#{search_condition.first}%"
29
-
30
- # a keyword may be found in any of the provided fields, so join the conitions with OR
31
- if search_condition.length == 2 && search_condition.last == :not
32
- keyword_conditions = self.scoped_search_fields.map do |field|
33
- field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
34
- "(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
35
- end
36
- conditions << "(#{keyword_conditions.join(' AND ')})"
37
- elsif search_condition.length == 2 && search_condition.last == :or
38
- word1, word2 = query_params[keyword_name].split(' OR ')
39
-
40
- query_params.delete(keyword_name)
41
- keyword_name_a = "#{keyword_name.to_s}a".to_sym
42
- keyword_name_b = "#{keyword_name.to_s}b".to_sym
43
- query_params[keyword_name_a] = word1
44
- query_params[keyword_name_b] = word2
45
-
46
- keyword_conditions = self.scoped_search_fields.map do |field|
47
- field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
48
- "(#{field_name} LIKE :#{keyword_name_a.to_s} OR #{field_name} LIKE :#{keyword_name_b.to_s})"
49
- end
50
- conditions << "(#{keyword_conditions.join(' OR ')})"
51
- else
52
- keyword_conditions = self.scoped_search_fields.map do |field|
53
- field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
54
- "#{field_name} LIKE :#{keyword_name.to_s}"
55
- end
56
- conditions << "(#{keyword_conditions.join(' OR ')})"
57
- end
23
+ return {:conditions => nil}
24
+ else
25
+ query_fields = {}
26
+ self.scoped_search_fields.each do |field|
27
+ field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
28
+ query_fields[field_name] = self.columns_hash[field.to_s].type
58
29
  end
59
-
60
- # all keywords must be matched, so join the conditions with AND
61
- return { :conditions => [conditions.join(' AND '), query_params] }
30
+
31
+ search_conditions = QueryLanguageParser.parse(search_string)
32
+ conditions = QueryConditionsBuilder.build_query(search_conditions, query_fields)
33
+ return {:conditions => conditions}
62
34
  end
63
35
  end
64
36
  end
@@ -0,0 +1,114 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class QueryConditionsBuilderTest < Test::Unit::TestCase
4
+
5
+ # change this function if you switch to another query language parser
6
+ def build_query(search_conditions, query_fields)
7
+ ScopedSearch::QueryConditionsBuilder.build_query(search_conditions, query_fields)
8
+ end
9
+
10
+ # ** Single query search tests **
11
+ def test_like_search_condition
12
+ search_conditions = [["Wes", :like]]
13
+ query_fields = {'some_table.first_name' => :string}
14
+ conditions = build_query(search_conditions, query_fields)
15
+
16
+ assert_equal '(some_table.first_name LIKE :keyword_0)', conditions.first
17
+ assert_equal '%Wes%', conditions.last[:keyword_0]
18
+ end
19
+
20
+ def test_not_like_search_condition
21
+ search_conditions = [["Wes", :not]]
22
+ query_fields = {'some_table.first_name' => :string}
23
+ conditions = build_query(search_conditions, query_fields)
24
+
25
+ assert_equal '((some_table.first_name NOT LIKE :keyword_0 OR some_table.first_name IS NULL))', conditions.first
26
+ assert_equal '%Wes%', conditions.last[:keyword_0]
27
+ end
28
+
29
+ def test_or_search_condition
30
+ search_conditions = [["Wes OR Hays", :or]]
31
+ query_fields = {'some_table.first_name' => :string}
32
+ conditions = build_query(search_conditions, query_fields)
33
+ regExs = build_regex_for_or(['first_name'], 'keyword_0')
34
+ assert_match /^#{regExs}$/, conditions.first
35
+ assert_equal '%Wes%', conditions.last[:keyword_0a]
36
+ assert_equal '%Hays%', conditions.last[:keyword_0b]
37
+ end
38
+
39
+ # def test_date_search_condition
40
+ # search_conditions = [["09/27/1980", :as_of_date]]
41
+ # query_fields = {'some_table.event_date' => :datetime}
42
+ # conditions = build_query(search_conditions, query_fields)
43
+ # regExs = build_regex_for_date(['event_date'], 'keyword_0')
44
+ # assert_match /^#{regExs}$/, conditions.first
45
+ # assert_equal '09/27/1980', conditions.last[:keyword_0a]
46
+ # end
47
+
48
+
49
+ # ** Multi query search tests **
50
+ def test_like_two_search_condition
51
+ search_conditions = [["Wes", :like],["Hays", :like]]
52
+ query_fields = {'some_table.first_name' => :string,'some_table.last_name' => :string}
53
+ conditions = build_query(search_conditions, query_fields)
54
+
55
+ fields = ['first_name','last_name']
56
+ regExs = [build_regex_for_like(fields,'keyword_0'),
57
+ build_regex_for_like(fields,'keyword_1')].join('[ ]AND[ ]')
58
+
59
+ assert_match /^#{regExs}$/, conditions.first
60
+ assert_equal '%Wes%', conditions.last[:keyword_0]
61
+ assert_equal '%Hays%', conditions.last[:keyword_1]
62
+ end
63
+
64
+ def test_like_two_search_conditions_with_one_not
65
+ search_conditions = [["Wes", :like],["Hays", :not]]
66
+ query_fields = {'some_table.first_name' => :string,'some_table.last_name' => :string}
67
+ conditions = build_query(search_conditions, query_fields)
68
+
69
+ fields = ['first_name','last_name']
70
+ regExs = [build_regex_for_like(fields,'keyword_0'),
71
+ build_regex_for_not_like(fields,'keyword_1')].join('[ ]AND[ ]')
72
+
73
+ assert_match /^#{regExs}$/, conditions.first
74
+ assert_equal '%Wes%', conditions.last[:keyword_0]
75
+ assert_equal '%Hays%', conditions.last[:keyword_1]
76
+ end
77
+
78
+
79
+
80
+ # ** Helper methods **
81
+ def build_regex_for_like(fields,keyword)
82
+ orFields = fields.join('|')
83
+ regParts = fields.collect { |field|
84
+ "some_table.(#{orFields}) LIKE :#{keyword}"
85
+ }.join('[ ]OR[ ]')
86
+ "[\(]#{regParts}[\)]"
87
+ end
88
+
89
+ def build_regex_for_not_like(fields,keyword)
90
+ orFields = fields.join('|')
91
+ regParts = fields.collect { |field|
92
+ "[\(]some_table.(#{orFields}) NOT LIKE :#{keyword} OR some_table.(#{orFields}) IS NULL[\)]"
93
+ }.join('[ ]AND[ ]')
94
+
95
+ "[\(]#{regParts}[\)]"
96
+ end
97
+
98
+ def build_regex_for_or(fields,keyword)
99
+ orFields = fields.join('|')
100
+ regParts = fields.collect { |field|
101
+ "[\(]some_table.(#{orFields}) LIKE :#{keyword}a OR some_table.(#{orFields}) LIKE :#{keyword}b[\)]"
102
+ }.join('[ ]OR[ ]')
103
+
104
+ "[\(]#{regParts}[\)]"
105
+ end
106
+
107
+ def build_regex_for_date(fields,keyword)
108
+ orFields = fields.join('|')
109
+ regParts = fields.collect { |field|
110
+ "some_table.(#{orFields}) = :#{keyword}"
111
+ }.join('[ ]OR[ ]')
112
+ "[\(]#{regParts}[\)]"
113
+ end
114
+ end
@@ -51,7 +51,7 @@ class QueryLanguageTest < Test::Unit::TestCase
51
51
  assert_equal 2, parsed.length
52
52
  assert_equal 'hallo', parsed[0].first
53
53
  assert_equal 'willem', parsed[1].first
54
-
54
+
55
55
  parsed = parse_query(' "hallo wi"llem"')
56
56
  assert_equal 2, parsed.length
57
57
  assert_equal 'hallo wi', parsed[0].first
@@ -108,6 +108,28 @@ class QueryLanguageTest < Test::Unit::TestCase
108
108
  assert_equal :or, parsed[0][1]
109
109
  end
110
110
 
111
+ def test_as_of_date
112
+ # parsed = parse_query('9/27/1980')
113
+ # assert_equal 1, parsed.length
114
+ # assert_equal '9/27/1980', parsed[0][0]
115
+ # assert_equal :as_of, parsed[0][1]
116
+
117
+ # parsed = parse_query('"Man made" OR Dogs')
118
+ # assert_equal 1, parsed.length
119
+ # assert_equal 'Man made OR Dogs', parsed[0][0]
120
+ # assert_equal :or, parsed[0][1]
121
+ #
122
+ # parsed = parse_query('Cows OR "Frog Toys"')
123
+ # assert_equal 1, parsed.length
124
+ # assert_equal 'Cows OR Frog Toys', parsed[0][0]
125
+ # assert_equal :or, parsed[0][1]
126
+ #
127
+ # parsed = parse_query('"Happy cow" OR "Sad Frog"')
128
+ # assert_equal 1, parsed.length
129
+ # assert_equal 'Happy cow OR Sad Frog', parsed[0][0]
130
+ # assert_equal :or, parsed[0][1]
131
+ end
132
+
111
133
  def test_long_string
112
134
  str = 'Wes -Hays "Hello World" -"Goodnight Moon" Bob OR Wes "Happy cow" OR "Sad Frog" "Man made" OR Dogs Cows OR "Frog Toys"'
113
135
  parsed = parse_query(str)
@@ -11,31 +11,55 @@ class ScopedSearchTest < Test::Unit::TestCase
11
11
  teardown_db
12
12
  end
13
13
 
14
- def test_enabling
15
- assert !SearchTestModel.respond_to?(:search_for)
16
- SearchTestModel.searchable_on :string_field, :text_field
17
- assert SearchTestModel.respond_to?(:search_for)
18
-
19
- assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
20
-
21
- end
22
-
14
+ # def test_enabling
15
+ # assert !SearchTestModel.respond_to?(:search_for)
16
+ # SearchTestModel.searchable_on :string_field, :text_field, :date_field
17
+ # assert SearchTestModel.respond_to?(:search_for)
18
+ #
19
+ # assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
20
+ # end
21
+ #
23
22
  def test_search
24
- SearchTestModel.searchable_on :string_field, :text_field
23
+ SearchTestModel.searchable_on :string_field, :text_field, :date_field
25
24
 
26
- assert_equal 15, SearchTestModel.search_for('').count
25
+ assert_equal 16, SearchTestModel.search_for('').count
27
26
  assert_equal 0, SearchTestModel.search_for('456').count
28
27
  assert_equal 2, SearchTestModel.search_for('hays').count
29
28
  assert_equal 1, SearchTestModel.search_for('hay ob').count
30
- assert_equal 13, SearchTestModel.search_for('o').count
29
+ assert_equal 14, SearchTestModel.search_for('o').count
31
30
  assert_equal 2, SearchTestModel.search_for('-o').count
32
- assert_equal 13, SearchTestModel.search_for('-Jim').count
31
+ assert_equal 14, SearchTestModel.search_for('-Jim').count
33
32
  assert_equal 1, SearchTestModel.search_for('Jim -Bush').count
34
33
  assert_equal 1, SearchTestModel.search_for('"Hello World" -"Goodnight Moon"').count
35
34
  assert_equal 2, SearchTestModel.search_for('Wes OR Bob').count
36
35
  assert_equal 3, SearchTestModel.search_for('"Happy cow" OR "Sad Frog"').count
37
36
  assert_equal 3, SearchTestModel.search_for('"Man made" OR Dogs').count
38
37
  assert_equal 2, SearchTestModel.search_for('Cows OR "Frog Toys"').count
38
+
39
+ # ** DATES **
40
+ #
41
+ # The next two dates are invalid therefore it will be ignored.
42
+ # Thus it would be the same as searching for an empty string
43
+ assert_equal 16, SearchTestModel.search_for('2/30/1980').count
44
+ assert_equal 16, SearchTestModel.search_for('99/99/9999').count
45
+
46
+ assert_equal 1, SearchTestModel.search_for('9/27/1980').count
47
+ assert_equal 1, SearchTestModel.search_for('hays 9/27/1980').count
48
+ assert_equal 2, SearchTestModel.search_for('hays 2/30/1980').count
49
+ assert_equal 1, SearchTestModel.search_for('2006/07/15').count
50
+
51
+ assert_equal 1, SearchTestModel.search_for('< 12/01/1980').count
52
+ assert_equal 5, SearchTestModel.search_for('> 1/1/2006').count
53
+
54
+ assert_equal 5, SearchTestModel.search_for('< 12/26/2002').count
55
+ assert_equal 6, SearchTestModel.search_for('<= 12/26/2002').count
56
+
57
+ assert_equal 5, SearchTestModel.search_for('> 2/5/2005').count
58
+ assert_equal 6, SearchTestModel.search_for('>= 2/5/2005').count
59
+
60
+ assert_equal 3, SearchTestModel.search_for('1/1/2005 TO 1/1/2007').count
61
+
62
+ assert_equal 2, SearchTestModel.search_for('Happy 1/1/2005 TO 1/1/2007').count
39
63
  end
40
64
 
41
65
  end
data/test/test_helper.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'test/unit'
2
2
  require 'rubygems'
3
3
  require 'active_record'
4
+ require 'ruby-debug'
4
5
 
5
6
  require "#{File.dirname(__FILE__)}/../lib/scoped_search"
6
7
 
@@ -11,6 +12,7 @@ def setup_db
11
12
  t.string :string_field
12
13
  t.text :text_field
13
14
  t.string :ignored_field
15
+ t.date :date_field
14
16
  t.timestamps
15
17
  end
16
18
  end
@@ -22,20 +24,21 @@ end
22
24
 
23
25
  class SearchTestModel < ActiveRecord::Base
24
26
  def self.create_corpus!
25
- create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456")
26
- create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a")
27
- create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b")
28
- create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c")
29
- create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d")
30
- create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e")
31
- create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f")
32
- create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g")
33
- create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h")
34
- create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i")
35
- create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j")
36
- create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k")
37
- create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l")
38
- create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m")
39
- create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n")
27
+ create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456", :date_field => '2000-01-01')
28
+ create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a", :date_field => '2001-04-15')
29
+ create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b", :date_field => '2001-04-17')
30
+ create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c", :date_field => '1980-09-27')
31
+ create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d", :date_field => '2002-11-09')
32
+ create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e", :date_field => '2002-12-26')
33
+ create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f", :date_field => '2003-03-19')
34
+ create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g", :date_field => '2004-05-01')
35
+ create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h", :date_field => '2004-07-11')
36
+ create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i", :date_field => '2004-09-12')
37
+ create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j", :date_field => '2005-02-05')
38
+ create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k", :date_field => '2006-03-09')
39
+ create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l", :date_field => '2006-07-15')
40
+ create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m", :date_field => '2007-06-13')
41
+ create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n", :date_field => '2008-03-04')
42
+ create!(:string_field => "Happy Toys", :text_field => "Sad Toys", :ignored_field => "123456n", :date_field => '2008-05-12')
40
43
  end
41
44
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wvanbergen-scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2008-09-13 00:00:00 -07:00
13
+ date: 2008-09-21 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -25,6 +25,7 @@ extensions: []
25
25
  extra_rdoc_files: []
26
26
 
27
27
  files:
28
+ - CHANGELOG
28
29
  - LICENSE
29
30
  - README.rdoc
30
31
  - Rakefile
@@ -33,8 +34,11 @@ files:
33
34
  - lib
34
35
  - lib/scoped_search
35
36
  - lib/scoped_search.rb
37
+ - lib/scoped_search/query_conditions_builder.rb
36
38
  - lib/scoped_search/query_language_parser.rb
39
+ - lib/scoped_search/reg_tokens.rb
37
40
  - test
41
+ - test/query_conditions_builder_test.rb
38
42
  - test/query_language_test.rb
39
43
  - test/search_for_test.rb
40
44
  - test/tasks.rake
@@ -66,5 +70,6 @@ signing_key:
66
70
  specification_version: 2
67
71
  summary: A Rails plugin to search your models using a named_scope
68
72
  test_files:
73
+ - test/query_conditions_builder_test.rb
69
74
  - test/query_language_test.rb
70
75
  - test/search_for_test.rb