DRY up predicate detection, add some starter docs
This commit is contained in:
parent
da6d4aa4aa
commit
e5c7d8be2b
|
@ -0,0 +1,129 @@
|
||||||
|
# Ransack
|
||||||
|
|
||||||
|
Ransack is a rewrite of [MetaSearch](http://metautonomo.us/projects/metasearch). While it
|
||||||
|
supports many of the same features as MetaSearch, its underlying implementation differs
|
||||||
|
greatly from MetaSearch, and _backwards compatibility is not a design goal._
|
||||||
|
|
||||||
|
Ransack enables the creation of both simple and advanced search forms against your
|
||||||
|
application's models. If you're looking for something that simplifies query generation
|
||||||
|
at the model or controller layer, you're probably not looking for Ransack (or MetaSearch,
|
||||||
|
for that matter). Try [Squeel](http://metautonomo.us/projects/squeel) instead.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
In your Gemfile:
|
||||||
|
|
||||||
|
gem "ransack" # Last officially released gem
|
||||||
|
# gem "ransack", :git => "git://github.com/ernie/ransack.git" # Track git repo
|
||||||
|
|
||||||
|
If you'd like to add your own custom Ransack predicates:
|
||||||
|
|
||||||
|
Ransack.configure do |config|
|
||||||
|
config.add_predicate 'equals_diddly', # Name your predicate
|
||||||
|
# What non-compound ARel predicate will it use? (eq, matches, etc)
|
||||||
|
:arel_predicate => 'eq',
|
||||||
|
# Format incoming values as you see fit. (Default: Don't do formatting)
|
||||||
|
:formatter => proc {|v| "#{v}-diddly"},
|
||||||
|
# Validate a value. An "invalid" value won't be used in a search.
|
||||||
|
# Below is default.
|
||||||
|
:validator => proc {|v| v.present?}
|
||||||
|
# Should compounds be created? Will use the compound (any/all) version
|
||||||
|
# of the arel_predicate to create a corresponding any/all version of
|
||||||
|
# your predicate. (Default: true)
|
||||||
|
:compounds => true,
|
||||||
|
# Force a specific column type for type-casting of supplied values.
|
||||||
|
# (Default: use type from DB column)
|
||||||
|
:type => :string
|
||||||
|
end
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Ransack can be used in one of two modes, simple or advanced.
|
||||||
|
|
||||||
|
### Simple Mode
|
||||||
|
|
||||||
|
This mode works much like MetaSearch, for those of you who are familiar with it, and
|
||||||
|
requires very little setup effort.
|
||||||
|
|
||||||
|
If you're coming from MetaSearch, things to note:
|
||||||
|
|
||||||
|
1. The default param key for search params is now `:q`, instead of `:search`. This is
|
||||||
|
primarily to shorten query strings, though advanced queries (below) will still
|
||||||
|
run afoul of URL length limits in most browsers and require a switch to HTTP
|
||||||
|
POST requests.
|
||||||
|
2. `form_for` is now `search_form_for`, and validates that a Ransack::Search object
|
||||||
|
is passed to it.
|
||||||
|
3. Common ActiveRecord::Relation methods are no longer delegated by the search object.
|
||||||
|
Instead, you will get your search results (an ActiveRecord::Relation in the case of
|
||||||
|
the ActiveRecord adapter) via a call to `Search#result`. If passed `:distinct => true`,
|
||||||
|
`result` will generate a `SELECT DISTINCT` to avoid returning duplicate rows, even if
|
||||||
|
conditions on a join would otherwise result in some.
|
||||||
|
|
||||||
|
In your controller:
|
||||||
|
|
||||||
|
def index
|
||||||
|
@q = Person.search(params[:q])
|
||||||
|
@people = @q.result(:distinct => true)
|
||||||
|
end
|
||||||
|
|
||||||
|
In your view:
|
||||||
|
|
||||||
|
<%= search_form_for @q do |f| %>
|
||||||
|
<%= f.label :name_cont %>
|
||||||
|
<%= f.text_field :name_cont %>
|
||||||
|
<%= f.label :articles_title_start %>
|
||||||
|
<%= f.text_field :articles_title_start %>
|
||||||
|
<%= f.submit %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
`cont` (contains) and `start` (starts with) are just two of the available search predicates.
|
||||||
|
See Constants for a full list.
|
||||||
|
|
||||||
|
### Advanced Mode
|
||||||
|
|
||||||
|
"Advanced" searches (ab)use Rails' nested attributes functionality in order to generate
|
||||||
|
complex queries with nested AND/OR groupings, etc. This takes a bit more work but can
|
||||||
|
generate some pretty cool search interfaces that put a lot of power in the hands of
|
||||||
|
your users. A notable drawback with these searches is that the increased size of the
|
||||||
|
parameter string will typically force you to use the HTTP POST method instead of GET. :(
|
||||||
|
|
||||||
|
This means you'll need to tweak your routes...
|
||||||
|
|
||||||
|
resources :people do
|
||||||
|
collection do
|
||||||
|
match 'search' => 'people#search', :via => [:get, :post], :as => :search
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
... and add another controller action ...
|
||||||
|
|
||||||
|
def search
|
||||||
|
index
|
||||||
|
render :index
|
||||||
|
end
|
||||||
|
|
||||||
|
... and update your `search_form_for` line in the view ...
|
||||||
|
|
||||||
|
<%= search_form_for @q, :url => search_people_path,
|
||||||
|
:html => {:method => :post} do |f| %>
|
||||||
|
|
||||||
|
Once you've done so, you can make use of the helpers in Ransack::Helpers::FormBuilder to
|
||||||
|
construct much more complex search forms.
|
||||||
|
|
||||||
|
**more docs to come**
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
If you'd like to support the continued development of Ransack, please consider
|
||||||
|
[making a donation](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=48Q9HY64L3TWA).
|
||||||
|
|
||||||
|
To support the project in other ways:
|
||||||
|
|
||||||
|
* Use Ransack in your apps, and let me know if you encounter anything that's broken or missing.
|
||||||
|
A failing spec is awesome. A pull request is even better!
|
||||||
|
* Spread the word on Twitter, Facebook, and elsewhere if Ransack's been useful to you. The more
|
||||||
|
people who are using the project, the quicker we can find and fix bugs!
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Copyright © 2011 [Ernie Miller](http://twitter.com/erniemiller)
|
|
@ -1,5 +0,0 @@
|
||||||
= Ransack
|
|
||||||
|
|
||||||
Don't use me.
|
|
||||||
|
|
||||||
Seriously, I'm not anywhere close to ready for public consumption, yet.
|
|
|
@ -7,10 +7,6 @@ module Ransack
|
||||||
mattr_accessor :predicates
|
mattr_accessor :predicates
|
||||||
self.predicates = {}
|
self.predicates = {}
|
||||||
|
|
||||||
def predicate_keys
|
|
||||||
predicates.keys.sort {|a,b| b.length <=> a.length}
|
|
||||||
end
|
|
||||||
|
|
||||||
def configure
|
def configure
|
||||||
yield self
|
yield self
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,7 +111,7 @@ module Ransack
|
||||||
|
|
||||||
def predicate_select(options = {}, html_options = {})
|
def predicate_select(options = {}, html_options = {})
|
||||||
options[:compounds] = true if options[:compounds].nil?
|
options[:compounds] = true if options[:compounds].nil?
|
||||||
keys = options[:compounds] ? Ransack.predicate_keys : Ransack.predicate_keys.reject {|k| k.match(/_(any|all)$/)}
|
keys = options[:compounds] ? Predicate.names : Predicate.names.reject {|k| k.match(/_(any|all)$/)}
|
||||||
if only = options[:only]
|
if only = options[:only]
|
||||||
if only.respond_to? :call
|
if only.respond_to? :call
|
||||||
keys = keys.select {|k| only.call(k)}
|
keys = keys.select {|k| only.call(k)}
|
||||||
|
|
|
@ -28,7 +28,7 @@ module Ransack
|
||||||
|
|
||||||
def extract_attributes_and_predicate(key)
|
def extract_attributes_and_predicate(key)
|
||||||
str = key.dup
|
str = key.dup
|
||||||
name = Ransack.predicate_keys.detect {|p| str.sub!(/_#{p}$/, '')}
|
name = Predicate.detect_and_strip_from_string!(str)
|
||||||
predicate = Predicate.named(name)
|
predicate = Predicate.named(name)
|
||||||
raise ArgumentError, "No valid predicate for #{key}" unless predicate
|
raise ArgumentError, "No valid predicate for #{key}" unless predicate
|
||||||
attributes = str.split(/_and_|_or_/)
|
attributes = str.split(/_and_|_or_/)
|
||||||
|
@ -194,7 +194,7 @@ module Ransack
|
||||||
def formatted_values_for_attribute(attr)
|
def formatted_values_for_attribute(attr)
|
||||||
casted_values_for_attribute(attr).map do |val|
|
casted_values_for_attribute(attr).map do |val|
|
||||||
val = attr.ransacker.formatter.call(val) if attr.ransacker && attr.ransacker.formatter
|
val = attr.ransacker.formatter.call(val) if attr.ransacker && attr.ransacker.formatter
|
||||||
val = predicate.formatter.call(val) if predicate.formatter
|
val = predicate.format(val)
|
||||||
val
|
val
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -173,7 +173,7 @@ module Ransack
|
||||||
|
|
||||||
def strip_predicate_and_index(str)
|
def strip_predicate_and_index(str)
|
||||||
string = str.split(/\(/).first
|
string = str.split(/\(/).first
|
||||||
Ransack.predicate_keys.detect {|p| string.sub!(/_#{p}$/, '')}
|
Predicate.detect_and_strip_from_string!(string)
|
||||||
string
|
string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,35 @@ module Ransack
|
||||||
attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound
|
attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
|
||||||
|
def names
|
||||||
|
Ransack.predicates.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def names_by_decreasing_length
|
||||||
|
names.sort {|a,b| b.length <=> a.length}
|
||||||
|
end
|
||||||
|
|
||||||
def named(name)
|
def named(name)
|
||||||
Ransack.predicates[name.to_s]
|
Ransack.predicates[name.to_s]
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_attribute_name(attribute_name)
|
def detect_and_strip_from_string!(str)
|
||||||
self.named(Ransack.predicate_keys.detect {|p| attribute_name.to_s.match(/_#{p}$/)})
|
names_by_decreasing_length.detect {|p| str.sub!(/_#{p}$/, '')}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def detect_from_string(str)
|
||||||
|
names_by_decreasing_length.detect {|p| str.match(/_#{p}$/)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def name_from_attribute_name(attribute_name)
|
||||||
|
names_by_decreasing_length.detect {|p| attribute_name.to_s.match(/_#{p}$/)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_attribute_name(attribute_name)
|
||||||
|
self.named(detect_from_string(attribute_name.to_s))
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(opts = {})
|
def initialize(opts = {})
|
||||||
|
@ -31,11 +53,19 @@ module Ransack
|
||||||
name.hash
|
name.hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format(val)
|
||||||
|
if formatter
|
||||||
|
formatter.call(val)
|
||||||
|
else
|
||||||
|
val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate(vals)
|
def validate(vals)
|
||||||
if validator
|
if validator
|
||||||
vals.select {|v| validator.call(v.value)}.any?
|
vals.select {|v| validator.call(v.value)}.any?
|
||||||
else
|
else
|
||||||
vals.select {|v| !v.blank?}.any?
|
vals.select {|v| v.present?}.any?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ module Ransack
|
||||||
original_name = key.to_s
|
original_name = key.to_s
|
||||||
base_class = context.klass
|
base_class = context.klass
|
||||||
base_ancestors = base_class.ancestors.select { |x| x.respond_to?(:model_name) }
|
base_ancestors = base_class.ancestors.select { |x| x.respond_to?(:model_name) }
|
||||||
predicate = Ransack.predicate_keys.detect {|p| original_name.match(/_#{p}$/)}
|
predicate = Predicate.detect_from_string(original_name)
|
||||||
attributes_str = original_name.sub(/_#{predicate}$/, '')
|
attributes_str = original_name.sub(/_#{predicate}$/, '')
|
||||||
attribute_names = attributes_str.split(/_and_|_or_/)
|
attribute_names = attributes_str.split(/_and_|_or_/)
|
||||||
combinator = attributes_str.match(/_and_/) ? :and : :or
|
combinator = attributes_str.match(/_and_/) ? :and : :or
|
||||||
|
|
|
@ -9,8 +9,8 @@ Gem::Specification.new do |s|
|
||||||
s.authors = ["Ernie Miller"]
|
s.authors = ["Ernie Miller"]
|
||||||
s.email = ["ernie@metautonomo.us"]
|
s.email = ["ernie@metautonomo.us"]
|
||||||
s.homepage = "http://metautonomo.us/projects/ransack"
|
s.homepage = "http://metautonomo.us/projects/ransack"
|
||||||
s.summary = %q{Object-based searching. Like MetaSearch, but this time, with a better name.}
|
s.summary = %q{Object-based searching for ActiveRecord (currently).}
|
||||||
s.description = %q{Not yet ready for public consumption.}
|
s.description = %q{Ransack is the successor to the MetaSearch gem. It improves and expands upon MetaSearch's functionality, but does not have a 100%-compatible API.}
|
||||||
|
|
||||||
s.rubyforge_project = "ransack"
|
s.rubyforge_project = "ransack"
|
||||||
|
|
||||||
|
|
|
@ -88,28 +88,28 @@ module Ransack
|
||||||
|
|
||||||
it 'returns predicates with predicate_select' do
|
it 'returns predicates with predicate_select' do
|
||||||
html = @f.predicate_select
|
html = @f.predicate_select
|
||||||
Ransack.predicate_keys.each do |key|
|
Predicate.names.each do |key|
|
||||||
html.should match /<option value="#{key}">/
|
html.should match /<option value="#{key}">/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters predicates with single-value :only' do
|
it 'filters predicates with single-value :only' do
|
||||||
html = @f.predicate_select :only => 'eq'
|
html = @f.predicate_select :only => 'eq'
|
||||||
Ransack.predicate_keys.reject {|k| k =~ /^eq/}.each do |key|
|
Predicate.names.reject {|k| k =~ /^eq/}.each do |key|
|
||||||
html.should_not match /<option value="#{key}">/
|
html.should_not match /<option value="#{key}">/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters predicates with multi-value :only' do
|
it 'filters predicates with multi-value :only' do
|
||||||
html = @f.predicate_select :only => [:eq, :lt]
|
html = @f.predicate_select :only => [:eq, :lt]
|
||||||
Ransack.predicate_keys.reject {|k| k =~ /^(eq|lt)/}.each do |key|
|
Predicate.names.reject {|k| k =~ /^(eq|lt)/}.each do |key|
|
||||||
html.should_not match /<option value="#{key}">/
|
html.should_not match /<option value="#{key}">/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'excludes compounds when :compounds => false' do
|
it 'excludes compounds when :compounds => false' do
|
||||||
html = @f.predicate_select :compounds => false
|
html = @f.predicate_select :compounds => false
|
||||||
Ransack.predicate_keys.select {|k| k =~ /_(any|all)$/}.each do |key|
|
Predicate.names.select {|k| k =~ /_(any|all)$/}.each do |key|
|
||||||
html.should_not match /<option value="#{key}">/
|
html.should_not match /<option value="#{key}">/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue