scoped_search 2.2.1 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/.infinity_test +8 -0
- data/Gemfile +15 -0
- data/lib/scoped_search/auto_complete_builder.rb +251 -0
- data/lib/scoped_search/definition.rb +90 -10
- data/lib/scoped_search/query_builder.rb +209 -45
- data/lib/scoped_search/query_language/parser.rb +4 -2
- data/lib/scoped_search/query_language/tokenizer.rb +3 -3
- data/lib/scoped_search/rails_helper.rb +210 -0
- data/lib/scoped_search.rb +9 -1
- data/scoped_search.gemspec +7 -5
- data/spec/database.yml +13 -6
- data/spec/integration/api_spec.rb +1 -1
- data/spec/integration/auto_complete_spec.rb +140 -0
- data/spec/integration/key_value_querying_spec.rb +87 -0
- data/spec/integration/ordinal_querying_spec.rb +105 -10
- data/spec/integration/profile_querying_spec.rb +1 -1
- data/spec/integration/relation_querying_spec.rb +186 -194
- data/spec/integration/set_query_spec.rb +76 -0
- data/spec/integration/string_querying_spec.rb +19 -2
- data/spec/lib/matchers.rb +10 -0
- data/spec/spec_helper.rb +2 -4
- data/spec/unit/ast_spec.rb +1 -1
- data/spec/unit/auto_complete_builder_spec.rb +20 -0
- data/spec/unit/definition_spec.rb +1 -1
- data/spec/unit/parser_spec.rb +1 -1
- data/spec/unit/query_builder_spec.rb +2 -1
- data/spec/unit/tokenizer_spec.rb +1 -1
- data/tasks/github-gem.rake +18 -14
- metadata +33 -7
@@ -0,0 +1,210 @@
|
|
1
|
+
module ScopedSearch
|
2
|
+
module RailsHelper
|
3
|
+
# Creates a link that alternates between ascending and descending.
|
4
|
+
#
|
5
|
+
# Examples:
|
6
|
+
#
|
7
|
+
# sort @search, :by => :username
|
8
|
+
# sort @search, :by => :created_at, :as => "Created"
|
9
|
+
#
|
10
|
+
# This helper accepts the following options:
|
11
|
+
#
|
12
|
+
# * <tt>:by</tt> - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
|
13
|
+
# * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to :by
|
14
|
+
def sort(field, options = {}, html_options = {})
|
15
|
+
|
16
|
+
unless options[:as]
|
17
|
+
id = field.to_s.downcase == "id"
|
18
|
+
options[:as] = id ? field.to_s.upcase : field.to_s.humanize
|
19
|
+
end
|
20
|
+
|
21
|
+
ascend = "#{field} ASC"
|
22
|
+
descend = "#{field} DESC"
|
23
|
+
|
24
|
+
ascending = params[:order] == ascend
|
25
|
+
new_sort = ascending ? descend : ascend
|
26
|
+
selected = [ascend, descend].include?(params[:order])
|
27
|
+
|
28
|
+
if selected
|
29
|
+
css_classes = html_options[:class] ? html_options[:class].split(" ") : []
|
30
|
+
if ascending
|
31
|
+
options[:as] = "▲ #{options[:as]}"
|
32
|
+
css_classes << "ascending"
|
33
|
+
else
|
34
|
+
options[:as] = "▼ #{options[:as]}"
|
35
|
+
css_classes << "descending"
|
36
|
+
end
|
37
|
+
html_options[:class] = css_classes.join(" ")
|
38
|
+
end
|
39
|
+
|
40
|
+
url_options = params.merge(:order => new_sort)
|
41
|
+
|
42
|
+
options[:as] = raw(options[:as]) if defined?(RailsXss)
|
43
|
+
|
44
|
+
a_link(options[:as], html_escape(url_for(url_options)),html_options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Adds AJAX auto complete functionality to the text input field with the
|
48
|
+
# DOM ID specified by +field_id+.
|
49
|
+
#
|
50
|
+
# Required +options+ is:
|
51
|
+
# <tt>:url</tt>:: URL to call for auto completion results
|
52
|
+
# in url_for format.
|
53
|
+
#
|
54
|
+
# Additional +options+ are:
|
55
|
+
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
|
56
|
+
# innerHTML should be updated with the auto complete
|
57
|
+
# entries returned by the AJAX request.
|
58
|
+
# Defaults to <tt>field_id</tt> + '_auto_complete'
|
59
|
+
# <tt>:with</tt>:: A JavaScript expression specifying the
|
60
|
+
# parameters for the XMLHttpRequest. This defaults
|
61
|
+
# to 'fieldname=value'.
|
62
|
+
# <tt>:frequency</tt>:: Determines the time to wait after the last keystroke
|
63
|
+
# for the AJAX request to be initiated.
|
64
|
+
# <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be
|
65
|
+
# displayed while auto complete is running.
|
66
|
+
# <tt>:tokens</tt>:: A string or an array of strings containing
|
67
|
+
# separator tokens for tokenized incremental
|
68
|
+
# auto completion. Example: <tt>:tokens => ','</tt> would
|
69
|
+
# allow multiple auto completion entries, separated
|
70
|
+
# by commas.
|
71
|
+
# <tt>:min_chars</tt>:: The minimum number of characters that should be
|
72
|
+
# in the input field before an Ajax call is made
|
73
|
+
# to the server.
|
74
|
+
# <tt>:on_hide</tt>:: A Javascript expression that is called when the
|
75
|
+
# auto completion div is hidden. The expression
|
76
|
+
# should take two variables: element and update.
|
77
|
+
# Element is a DOM element for the field, update
|
78
|
+
# is a DOM element for the div from which the
|
79
|
+
# innerHTML is replaced.
|
80
|
+
# <tt>:on_show</tt>:: Like on_hide, only now the expression is called
|
81
|
+
# then the div is shown.
|
82
|
+
# <tt>:after_update_element</tt>:: A Javascript expression that is called when the
|
83
|
+
# user has selected one of the proposed values.
|
84
|
+
# The expression should take two variables: element and value.
|
85
|
+
# Element is a DOM element for the field, value
|
86
|
+
# is the value selected by the user.
|
87
|
+
# <tt>:select</tt>:: Pick the class of the element from which the value for
|
88
|
+
# insertion should be extracted. If this is not specified,
|
89
|
+
# the entire element is used.
|
90
|
+
# <tt>:method</tt>:: Specifies the HTTP verb to use when the auto completion
|
91
|
+
# request is made. Defaults to POST.
|
92
|
+
def auto_complete_field(field_id, options = {})
|
93
|
+
function = "var #{field_id}_auto_completer = new Ajax.Autocompleter("
|
94
|
+
function << "'#{field_id}', "
|
95
|
+
function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', "
|
96
|
+
function << "'#{url_for(options[:url])}'"
|
97
|
+
|
98
|
+
js_options = {}
|
99
|
+
js_options[:tokens] = array_or_string_for_javascript(options[:tokens]) if options[:tokens]
|
100
|
+
js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
|
101
|
+
js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
|
102
|
+
js_options[:select] = "'#{options[:select]}'" if options[:select]
|
103
|
+
js_options[:paramName] = "'#{options[:param_name]}'" if options[:param_name]
|
104
|
+
js_options[:frequency] = "#{options[:frequency]}" if options[:frequency]
|
105
|
+
js_options[:method] = "'#{options[:method].to_s}'" if options[:method]
|
106
|
+
|
107
|
+
{ :after_update_element => :afterUpdateElement,
|
108
|
+
:on_show => :onShow, :on_hide => :onHide, :min_chars => :minChars }.each do |k,v|
|
109
|
+
js_options[v] = options[k] if options[k]
|
110
|
+
end
|
111
|
+
|
112
|
+
function << (', ' + options_for_javascript(js_options) + ')')
|
113
|
+
|
114
|
+
javascript_tag(function)
|
115
|
+
end
|
116
|
+
|
117
|
+
def auto_complete_field_jquery(method, url, options = {})
|
118
|
+
function = <<-EOF
|
119
|
+
$.widget( "custom.catcomplete", $.ui.autocomplete, {
|
120
|
+
_renderMenu: function( ul, items ) {
|
121
|
+
var self = this,
|
122
|
+
currentCategory = "";
|
123
|
+
$.each( items, function( index, item ) {
|
124
|
+
if ( item.category != undefined && item.category != currentCategory ) {
|
125
|
+
ul.append( "<li class='ui-autocomplete-category'>" + item.category + "</li>" );
|
126
|
+
currentCategory = item.category;
|
127
|
+
}
|
128
|
+
if ( item.error != undefined ) {
|
129
|
+
ul.append( "<li class='ui-autocomplete-error'>" + item.error + "</li>" );
|
130
|
+
}
|
131
|
+
if( item.completed != undefined ) {
|
132
|
+
$( "<li></li>" ).data( "item.autocomplete", item )
|
133
|
+
.append( "<a>" + "<strong class='ui-autocomplete-completed'>" + item.completed + "</strong>" + item.part + "</a>" )
|
134
|
+
.appendTo( ul );
|
135
|
+
} else {
|
136
|
+
self._renderItem( ul, item );
|
137
|
+
}
|
138
|
+
});
|
139
|
+
}
|
140
|
+
});
|
141
|
+
|
142
|
+
$("##{method}")
|
143
|
+
.catcomplete({
|
144
|
+
source: function( request, response ) { $.getJSON( "#{url}", { #{method}: request.term }, response ); },
|
145
|
+
minLength: #{options[:min_length] || 0},
|
146
|
+
delay: #{options[:delay] || 200},
|
147
|
+
select: function(event, ui) { $( this ).catcomplete( "search" , ui.item.value); },
|
148
|
+
search: function(event, ui) { $(".auto_complete_clear").hide(); },
|
149
|
+
open: function(event, ui) { $(".auto_complete_clear").show(); }
|
150
|
+
});
|
151
|
+
|
152
|
+
$("##{method}").bind( "focus", function( event ) {
|
153
|
+
if( $( this )[0].value == "" ) {
|
154
|
+
$( this ).catcomplete( "search" );
|
155
|
+
}
|
156
|
+
});
|
157
|
+
|
158
|
+
EOF
|
159
|
+
|
160
|
+
|
161
|
+
javascript_tag(function)
|
162
|
+
end
|
163
|
+
|
164
|
+
def auto_complete_clear_value_button(field_id)
|
165
|
+
html_options = {:tabindex => '-1',:class=>"auto_complete_clear",:title =>'Clear Search', :onclick=>"document.getElementById('#{field_id}').value = '';"}
|
166
|
+
a_link("", "#", html_options)
|
167
|
+
end
|
168
|
+
|
169
|
+
def a_link(name, href, html_options)
|
170
|
+
tag_options = tag_options(html_options)
|
171
|
+
link = "<a href=\"#{href}\"#{tag_options}>#{name}</a>"
|
172
|
+
return link.respond_to?(:html_safe) ? link.html_safe : link
|
173
|
+
end
|
174
|
+
|
175
|
+
# Use this method in your view to generate a return for the AJAX auto complete requests.
|
176
|
+
#
|
177
|
+
# The auto_complete_result can of course also be called from a view belonging to the
|
178
|
+
# auto_complete action if you need to decorate it further.
|
179
|
+
def auto_complete_result(entries, phrase = nil)
|
180
|
+
return unless entries
|
181
|
+
items = entries.map { |entry| content_tag("li", phrase ? highlight(entry, phrase) : h(entry)) }
|
182
|
+
content_tag("ul", items)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Wrapper for text_field with added AJAX auto completion functionality.
|
186
|
+
#
|
187
|
+
# In your controller, you'll need to define an action called
|
188
|
+
# auto_complete_method to respond the AJAX calls,
|
189
|
+
def auto_complete_field_tag(method, val,tag_options = {}, completion_options = {})
|
190
|
+
auto_completer_options = { :url => { :action => "auto_complete_#{method}" } }.update(completion_options)
|
191
|
+
|
192
|
+
text_field_tag(method, val,tag_options.merge(:class => "auto_complete_input")) +
|
193
|
+
auto_complete_clear_value_button(method) +
|
194
|
+
content_tag("div", "", :id => "#{method}_auto_complete", :class => "auto_complete") +
|
195
|
+
auto_complete_field(method, auto_completer_options)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Wrapper for text_field with added JQuery auto completion functionality.
|
199
|
+
#
|
200
|
+
# In your controller, you'll need to define an action called
|
201
|
+
# auto_complete_method to respond the JQuery calls,
|
202
|
+
def auto_complete_field_tag_jquery(method, val,tag_options = {}, completion_options = {})
|
203
|
+
url = url_for(:action => "auto_complete_#{method}")
|
204
|
+
options = tag_options.merge(:class => "auto_complete_input")
|
205
|
+
text_field_tag(method, val, options) + auto_complete_clear_value_button(method) +
|
206
|
+
auto_complete_field_jquery(method, url, completion_options)
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
data/lib/scoped_search.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
1
3
|
# ScopedSearch is the base module for the scoped_search plugin. This file
|
2
4
|
# defines some modules and exception classes, loads the necessary files, and
|
3
5
|
# installs itself in ActiveRecord.
|
@@ -12,7 +14,7 @@ module ScopedSearch
|
|
12
14
|
|
13
15
|
# The current scoped_search version. Do not change thisvalue by hand,
|
14
16
|
# because it will be updated automatically by the gem release script.
|
15
|
-
VERSION = "2.
|
17
|
+
VERSION = "2.3.0"
|
16
18
|
|
17
19
|
# The ClassMethods module will be included into the ActiveRecord::Base class
|
18
20
|
# to add the <tt>ActiveRecord::Base.scoped_search</tt> method and the
|
@@ -85,7 +87,13 @@ end
|
|
85
87
|
require 'scoped_search/definition'
|
86
88
|
require 'scoped_search/query_language'
|
87
89
|
require 'scoped_search/query_builder'
|
90
|
+
require 'scoped_search/auto_complete_builder'
|
88
91
|
|
89
92
|
# Import the search_on method in the ActiveReocrd::Base class
|
90
93
|
ActiveRecord::Base.send(:extend, ScopedSearch::ClassMethods)
|
91
94
|
ActiveRecord::Base.send(:extend, ScopedSearch::BackwardsCompatibility)
|
95
|
+
|
96
|
+
if defined?(ActionController)
|
97
|
+
require "scoped_search/rails_helper"
|
98
|
+
ActionController::Base.helper(ScopedSearch::RailsHelper)
|
99
|
+
end
|
data/scoped_search.gemspec
CHANGED
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
|
|
3
3
|
|
4
4
|
# Do not change the version and date fields by hand. This will be done
|
5
5
|
# automatically by the gem release script.
|
6
|
-
s.version = "2.
|
7
|
-
s.date = "
|
6
|
+
s.version = "2.3.0"
|
7
|
+
s.date = "2011-05-16"
|
8
8
|
|
9
9
|
s.summary = "Easily search you ActiveRecord models with a simple query language using a named scope."
|
10
10
|
s.description = <<-EOS
|
@@ -26,12 +26,14 @@ Gem::Specification.new do |s|
|
|
26
26
|
|
27
27
|
s.add_runtime_dependency('activerecord', '>= 2.1.0')
|
28
28
|
s.add_development_dependency('rspec', '~> 2.0')
|
29
|
+
|
30
|
+
s.add_development_dependency('sqlite3-ruby')
|
29
31
|
|
30
32
|
s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
|
31
33
|
s.extra_rdoc_files = ['README.rdoc']
|
32
34
|
|
33
35
|
# Do not change the files and test_files fields by hand. This will be done
|
34
36
|
# automatically by the gem release script.
|
35
|
-
s.files = %w(.gitignore LICENSE README.rdoc Rakefile init.rb lib/scoped_search.rb lib/scoped_search/definition.rb lib/scoped_search/query_builder.rb lib/scoped_search/query_language.rb lib/scoped_search/query_language/ast.rb lib/scoped_search/query_language/parser.rb lib/scoped_search/query_language/tokenizer.rb scoped_search.gemspec spec/database.yml spec/integration/api_spec.rb spec/integration/ordinal_querying_spec.rb spec/integration/profile_querying_spec.rb spec/integration/relation_querying_spec.rb spec/integration/string_querying_spec.rb spec/lib/database.rb spec/lib/matchers.rb spec/lib/mocks.rb spec/spec_helper.rb spec/unit/ast_spec.rb spec/unit/definition_spec.rb spec/unit/parser_spec.rb spec/unit/query_builder_spec.rb spec/unit/tokenizer_spec.rb tasks/github-gem.rake)
|
36
|
-
s.test_files = %w(spec/integration/api_spec.rb spec/integration/ordinal_querying_spec.rb spec/integration/profile_querying_spec.rb spec/integration/relation_querying_spec.rb spec/integration/string_querying_spec.rb spec/unit/ast_spec.rb spec/unit/definition_spec.rb spec/unit/parser_spec.rb spec/unit/query_builder_spec.rb spec/unit/tokenizer_spec.rb)
|
37
|
-
end
|
37
|
+
s.files = %w(.gitignore .infinity_test Gemfile LICENSE README.rdoc Rakefile init.rb lib/scoped_search.rb lib/scoped_search/auto_complete_builder.rb lib/scoped_search/definition.rb lib/scoped_search/query_builder.rb lib/scoped_search/query_language.rb lib/scoped_search/query_language/ast.rb lib/scoped_search/query_language/parser.rb lib/scoped_search/query_language/tokenizer.rb lib/scoped_search/rails_helper.rb scoped_search.gemspec spec/database.yml spec/integration/api_spec.rb spec/integration/auto_complete_spec.rb spec/integration/key_value_querying_spec.rb spec/integration/ordinal_querying_spec.rb spec/integration/profile_querying_spec.rb spec/integration/relation_querying_spec.rb spec/integration/set_query_spec.rb spec/integration/string_querying_spec.rb spec/lib/database.rb spec/lib/matchers.rb spec/lib/mocks.rb spec/spec_helper.rb spec/unit/ast_spec.rb spec/unit/auto_complete_builder_spec.rb spec/unit/definition_spec.rb spec/unit/parser_spec.rb spec/unit/query_builder_spec.rb spec/unit/tokenizer_spec.rb tasks/github-gem.rake)
|
38
|
+
s.test_files = %w(spec/integration/api_spec.rb spec/integration/auto_complete_spec.rb spec/integration/key_value_querying_spec.rb spec/integration/ordinal_querying_spec.rb spec/integration/profile_querying_spec.rb spec/integration/relation_querying_spec.rb spec/integration/set_query_spec.rb spec/integration/string_querying_spec.rb spec/unit/ast_spec.rb spec/unit/auto_complete_builder_spec.rb spec/unit/definition_spec.rb spec/unit/parser_spec.rb spec/unit/query_builder_spec.rb spec/unit/tokenizer_spec.rb)
|
39
|
+
end
|
data/spec/database.yml
CHANGED
@@ -7,13 +7,20 @@
|
|
7
7
|
# to you.
|
8
8
|
|
9
9
|
|
10
|
-
<% if Gem.available?('sqlite3-ruby') %>
|
11
|
-
sqlite3:
|
12
|
-
|
13
|
-
|
14
|
-
<% end %>
|
10
|
+
# <% if Gem.available?('sqlite3-ruby') %>
|
11
|
+
# sqlite3:
|
12
|
+
# adapter: "sqlite3"
|
13
|
+
# database: ":memory:"
|
14
|
+
# <% end %>
|
15
15
|
|
16
|
-
<% if Gem.available?('
|
16
|
+
<% if Gem.available?('mysql2') %>
|
17
|
+
mysql2:
|
18
|
+
adapter: "mysql2"
|
19
|
+
host: "localhost"
|
20
|
+
username: "root"
|
21
|
+
password:
|
22
|
+
database: "scoped_search_test"
|
23
|
+
<% elsif Gem.available?('mysql') %>
|
17
24
|
mysql:
|
18
25
|
adapter: "mysql"
|
19
26
|
host: "localhost"
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
|
+
|
3
|
+
# These specs will run on all databases that are defined in the spec/database.yml file.
|
4
|
+
# Comment out any databases that you do not have available for testing purposes if needed.
|
5
|
+
ScopedSearch::RSpec::Database.test_databases.each do |db|
|
6
|
+
|
7
|
+
describe ScopedSearch, "using a #{db} database" do
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
ScopedSearch::RSpec::Database.establish_named_connection(db)
|
11
|
+
|
12
|
+
## The related class
|
13
|
+
ActiveRecord::Migration.drop_table(:bars) rescue nil
|
14
|
+
ActiveRecord::Migration.create_table(:bars) { |t| t.string :related; t.integer :foo_id }
|
15
|
+
class ::Bar < ActiveRecord::Base; belongs_to :foo; end
|
16
|
+
|
17
|
+
::Foo = ScopedSearch::RSpec::Database.create_model(:string => :string, :another => :string, :explicit => :string, :int => :integer, :date => :date, :unindexed => :integer) do |klass|
|
18
|
+
klass.has_many :bars
|
19
|
+
|
20
|
+
klass.scoped_search :on => [:string, :int, :date]
|
21
|
+
klass.scoped_search :on => :another, :default_operator => :eq, :alias => :alias
|
22
|
+
klass.scoped_search :on => :explicit, :only_explicit => true, :complete_value => true
|
23
|
+
klass.scoped_search :on => :related, :in => :bars, :rename => 'bars.related'.to_sym
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
@foo_1 = Foo.create!(:string => 'foo', :another => 'temp 1', :explicit => 'baz', :int => 9 , :date => 'February 8, 20011' , :unindexed => 10)
|
28
|
+
Foo.create!(:string => 'bar', :another => 'temp 2', :explicit => 'baz', :int => 9 , :date => 'February 10, 20011', :unindexed => 10)
|
29
|
+
Foo.create!(:string => 'baz', :another => nil, :explicit => nil , :int => nil, :date => nil , :unindexed => nil)
|
30
|
+
|
31
|
+
Bar.create!(:related => 'lala', :foo => @foo_1)
|
32
|
+
Bar.create!(:related => 'another lala', :foo => @foo_1)
|
33
|
+
end
|
34
|
+
|
35
|
+
after(:all) do
|
36
|
+
ScopedSearch::RSpec::Database.drop_model(Foo)
|
37
|
+
ScopedSearch::RSpec::Database.drop_model(Bar)
|
38
|
+
ScopedSearch::RSpec::Database.close_connection
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'basic auto completer' do
|
42
|
+
it "should complete the field name" do
|
43
|
+
Foo.complete_for('str').should =~ ([' string '])
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should not complete the logical operators at the beginning" do
|
47
|
+
Foo.complete_for('a').should_not contain(['and'])
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should complete the string comparators" do
|
51
|
+
Foo.complete_for('string ').should =~ (["string != ", "string !~ ", "string = ", "string ~ "])
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should complete the numerical comparators" do
|
55
|
+
Foo.complete_for('int ').should =~ (["int != ", "int < ", "int <= ", "int = ", "int > ", "int >= "])
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should complete the temporal (date) comparators" do
|
59
|
+
Foo.complete_for('date ').should =~ (["date = ", "date < ", "date > "])
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should raise error for unindexed field" do
|
63
|
+
lambda { Foo.complete_for('unindexed = 10 ')}.should raise_error(ScopedSearch::QueryNotSupported)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should raise error for unknown field" do
|
67
|
+
lambda {Foo.complete_for('unknown = 10 ')}.should raise_error(ScopedSearch::QueryNotSupported)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should complete logical comparators" do
|
71
|
+
Foo.complete_for('string ~ fo ').should contain("string ~ fo and", "string ~ fo or")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should complete prefix operators" do
|
75
|
+
Foo.complete_for(' ').should contain(" has", " not")
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should not complete logical infix operators" do
|
79
|
+
Foo.complete_for(' ').should_not contain(" and", " or")
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should not repeat logical operators" do
|
83
|
+
Foo.complete_for('string = foo and ').should_not contain("string = foo and and", "string = foo and or")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'using an aliased field' do
|
88
|
+
it "should complete an explicit match using its alias" do
|
89
|
+
Foo.complete_for('al').should contain(' alias ')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'value auto complete' do
|
94
|
+
it "should complete values list of values " do
|
95
|
+
Foo.complete_for('explicit = ').should have(1).item
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should complete values should contain baz" do
|
99
|
+
Foo.complete_for('explicit = ').should contain('explicit = baz')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'auto complete relations' do
|
104
|
+
it "should complete related object name" do
|
105
|
+
Foo.complete_for('ba').should contain(' bars.related ')
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should complete related object name with field name" do
|
109
|
+
Foo.complete_for('bars.').should contain(' bars.related ')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'using null prefix operators queries' do
|
114
|
+
|
115
|
+
it "should complete has operator" do
|
116
|
+
Foo.complete_for('has strin').should eql(['has string '])
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should complete null? operator" do
|
120
|
+
Foo.complete_for('null? st').should eql(['null? string '])
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should complete set? operator" do
|
124
|
+
Foo.complete_for('set? exp').should eql(['set? explicit '])
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should complete null? operator for explicit field" do
|
128
|
+
Foo.complete_for('null? explici').should eql(['null? explicit '])
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should not complete comparators after prefix statement" do
|
132
|
+
Foo.complete_for('has string ').should_not contain(['has string = '])
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should not complete infix operator" do
|
136
|
+
Foo.complete_for('has string ').should_not contain('has string = ')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
|
+
|
3
|
+
# These specs will run on all databases that are defined in the spec/database.yml file.
|
4
|
+
# Comment out any databases that you do not have available for testing purposes if needed.
|
5
|
+
ScopedSearch::RSpec::Database.test_databases.each do |db|
|
6
|
+
|
7
|
+
describe ScopedSearch, "using a #{db} database" do
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
ScopedSearch::RSpec::Database.establish_named_connection(db)
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
ScopedSearch::RSpec::Database.close_connection
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'querying a key-value schema' do
|
18
|
+
|
19
|
+
before(:all) do
|
20
|
+
ActiveRecord::Migration.create_table(:keys) { |t| t.string :name }
|
21
|
+
class ::Key < ActiveRecord::Base; has_many :facts; end
|
22
|
+
|
23
|
+
ActiveRecord::Migration.create_table(:facts) { |t| t.string :value; t.integer :key_id; t.integer :bar_id }
|
24
|
+
class ::Fact < ActiveRecord::Base; belongs_to :key; belongs_to :bar; end
|
25
|
+
|
26
|
+
# The class that will run the queries
|
27
|
+
::Bar = ScopedSearch::RSpec::Database.create_model(:name => :string) do |klass|
|
28
|
+
klass.has_many :facts
|
29
|
+
klass.has_many :keys, :through => :facts
|
30
|
+
klass.scoped_search :in => :facts, :on => :value, :rename => :facts, :in_key => :keys, :on_key => :name, :complete_value => true
|
31
|
+
end
|
32
|
+
|
33
|
+
@key1 = Key.create!(:name => 'color')
|
34
|
+
@key2 = Key.create!(:name => 'size')
|
35
|
+
|
36
|
+
|
37
|
+
@bar1 = Bar.create!(:name => 'bar')
|
38
|
+
@bar2 = Bar.create!(:name => 'barbary')
|
39
|
+
|
40
|
+
Fact.create!(:value => 'green', :key => @key1, :bar => @bar1)
|
41
|
+
Fact.create!(:value => 'gold' , :key => @key1, :bar => @bar2)
|
42
|
+
Fact.create!(:value => '5' , :key => @key2, :bar => @bar1)
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
after(:all) do
|
47
|
+
ScopedSearch::RSpec::Database.drop_model(Bar)
|
48
|
+
ScopedSearch::RSpec::Database.drop_model(Fact)
|
49
|
+
ScopedSearch::RSpec::Database.drop_model(Key)
|
50
|
+
Object.send :remove_const, :Fact
|
51
|
+
Object.send :remove_const, :Key
|
52
|
+
Object.send :remove_const, :Bar
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should find all bars with a fact name color and fact value green" do
|
56
|
+
Bar.search_for('facts.color = green').should have(1).items
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should find all bars with a fact name size and fact value 5" do
|
60
|
+
Bar.search_for('facts.size = 5').should have(1).items
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should find all bars with a fact color green and fact size 5" do
|
64
|
+
Bar.search_for('facts.color = green and facts.size = 5').should have(1).items
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should find all bars that has size value" do
|
68
|
+
Bar.search_for('has facts.size').should have(1).items
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should find all bars that has color value" do
|
72
|
+
Bar.search_for('has facts.color').should have(2).items
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should complete facts names" do
|
76
|
+
Bar.complete_for('facts.').should have(2).items
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should complete values for fact name = color" do
|
80
|
+
Bar.complete_for('facts.color = ').should have(2).items
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|