diff --git a/README.md b/README.md new file mode 100644 index 0000000..9dc2b51 --- /dev/null +++ b/README.md @@ -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) \ No newline at end of file diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 8091a1e..0000000 --- a/README.rdoc +++ /dev/null @@ -1,5 +0,0 @@ -= Ransack - -Don't use me. - -Seriously, I'm not anywhere close to ready for public consumption, yet. diff --git a/lib/ransack/configuration.rb b/lib/ransack/configuration.rb index b392c40..7db492b 100644 --- a/lib/ransack/configuration.rb +++ b/lib/ransack/configuration.rb @@ -7,10 +7,6 @@ module Ransack mattr_accessor :predicates self.predicates = {} - def predicate_keys - predicates.keys.sort {|a,b| b.length <=> a.length} - end - def configure yield self end diff --git a/lib/ransack/helpers/form_builder.rb b/lib/ransack/helpers/form_builder.rb index 4bf6ae0..8ba901a 100644 --- a/lib/ransack/helpers/form_builder.rb +++ b/lib/ransack/helpers/form_builder.rb @@ -111,7 +111,7 @@ module Ransack def predicate_select(options = {}, html_options = {}) 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.respond_to? :call keys = keys.select {|k| only.call(k)} diff --git a/lib/ransack/nodes/condition.rb b/lib/ransack/nodes/condition.rb index 216436d..1cfdc07 100644 --- a/lib/ransack/nodes/condition.rb +++ b/lib/ransack/nodes/condition.rb @@ -28,7 +28,7 @@ module Ransack def extract_attributes_and_predicate(key) str = key.dup - name = Ransack.predicate_keys.detect {|p| str.sub!(/_#{p}$/, '')} + name = Predicate.detect_and_strip_from_string!(str) predicate = Predicate.named(name) raise ArgumentError, "No valid predicate for #{key}" unless predicate attributes = str.split(/_and_|_or_/) @@ -194,7 +194,7 @@ module Ransack def formatted_values_for_attribute(attr) casted_values_for_attribute(attr).map do |val| 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 end end diff --git a/lib/ransack/nodes/grouping.rb b/lib/ransack/nodes/grouping.rb index b4b981f..6d15aa0 100644 --- a/lib/ransack/nodes/grouping.rb +++ b/lib/ransack/nodes/grouping.rb @@ -173,7 +173,7 @@ module Ransack def strip_predicate_and_index(str) string = str.split(/\(/).first - Ransack.predicate_keys.detect {|p| string.sub!(/_#{p}$/, '')} + Predicate.detect_and_strip_from_string!(string) string end diff --git a/lib/ransack/predicate.rb b/lib/ransack/predicate.rb index 01ab23b..6651457 100644 --- a/lib/ransack/predicate.rb +++ b/lib/ransack/predicate.rb @@ -3,13 +3,35 @@ module Ransack attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound 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) Ransack.predicates[name.to_s] end - def for_attribute_name(attribute_name) - self.named(Ransack.predicate_keys.detect {|p| attribute_name.to_s.match(/_#{p}$/)}) + def detect_and_strip_from_string!(str) + names_by_decreasing_length.detect {|p| str.sub!(/_#{p}$/, '')} 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 def initialize(opts = {}) @@ -31,11 +53,19 @@ module Ransack name.hash end + def format(val) + if formatter + formatter.call(val) + else + val + end + end + def validate(vals) if validator vals.select {|v| validator.call(v.value)}.any? else - vals.select {|v| !v.blank?}.any? + vals.select {|v| v.present?}.any? end end diff --git a/lib/ransack/translate.rb b/lib/ransack/translate.rb index 2a94f54..78939a6 100644 --- a/lib/ransack/translate.rb +++ b/lib/ransack/translate.rb @@ -18,7 +18,7 @@ module Ransack original_name = key.to_s base_class = context.klass 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}$/, '') attribute_names = attributes_str.split(/_and_|_or_/) combinator = attributes_str.match(/_and_/) ? :and : :or diff --git a/ransack.gemspec b/ransack.gemspec index 8893e8c..cd0c96e 100644 --- a/ransack.gemspec +++ b/ransack.gemspec @@ -9,8 +9,8 @@ Gem::Specification.new do |s| s.authors = ["Ernie Miller"] s.email = ["ernie@metautonomo.us"] s.homepage = "http://metautonomo.us/projects/ransack" - s.summary = %q{Object-based searching. Like MetaSearch, but this time, with a better name.} - s.description = %q{Not yet ready for public consumption.} + s.summary = %q{Object-based searching for ActiveRecord (currently).} + 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" diff --git a/spec/ransack/helpers/form_builder_spec.rb b/spec/ransack/helpers/form_builder_spec.rb index 19d9c65..1a0beb5 100644 --- a/spec/ransack/helpers/form_builder_spec.rb +++ b/spec/ransack/helpers/form_builder_spec.rb @@ -88,28 +88,28 @@ module Ransack it 'returns predicates with predicate_select' do html = @f.predicate_select - Ransack.predicate_keys.each do |key| + Predicate.names.each do |key| html.should match /