Initial commit.
This commit is contained in:
commit
294015309b
|
@ -0,0 +1,4 @@
|
||||||
|
*.gem
|
||||||
|
.bundle
|
||||||
|
Gemfile.lock
|
||||||
|
pkg/*
|
|
@ -0,0 +1,11 @@
|
||||||
|
source "http://rubygems.org"
|
||||||
|
gemspec
|
||||||
|
|
||||||
|
gem 'arel', :git => 'git://github.com/rails/arel.git'
|
||||||
|
gem 'rack', :git => 'git://github.com/rack/rack.git'
|
||||||
|
|
||||||
|
git 'git://github.com/rails/rails.git' do
|
||||||
|
gem 'activesupport'
|
||||||
|
gem 'activerecord'
|
||||||
|
gem 'actionpack'
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (c) 2010 Ernie Miller
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,5 @@
|
||||||
|
= Ransack
|
||||||
|
|
||||||
|
Don't use me.
|
||||||
|
|
||||||
|
Seriously, I'm not anywhere close to ready for public consumption, yet.
|
|
@ -0,0 +1,19 @@
|
||||||
|
require 'bundler'
|
||||||
|
require 'rspec/core/rake_task'
|
||||||
|
|
||||||
|
Bundler::GemHelper.install_tasks
|
||||||
|
|
||||||
|
RSpec::Core::RakeTask.new(:spec) do |rspec|
|
||||||
|
rspec.rspec_opts = ['--backtrace']
|
||||||
|
end
|
||||||
|
|
||||||
|
task :default => :spec
|
||||||
|
|
||||||
|
desc "Open an irb session with Ransack and the sample data used in specs"
|
||||||
|
task :console do
|
||||||
|
require 'irb'
|
||||||
|
require 'irb/completion'
|
||||||
|
require 'console'
|
||||||
|
ARGV.clear
|
||||||
|
IRB.start
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
require 'ransack/configuration'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
extend Configuration
|
||||||
|
end
|
||||||
|
|
||||||
|
Ransack.configure do |config|
|
||||||
|
Ransack::Constants::AREL_PREDICATES.each do |name|
|
||||||
|
config.add_predicate name, :arel_predicate => name
|
||||||
|
end
|
||||||
|
|
||||||
|
Ransack::Constants::DERIVED_PREDICATES.each do |args|
|
||||||
|
config.add_predicate *args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'ransack/translate'
|
||||||
|
require 'ransack/search'
|
||||||
|
require 'ransack/adapters/active_record'
|
||||||
|
require 'ransack/helpers'
|
||||||
|
require 'action_controller'
|
||||||
|
|
||||||
|
ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base
|
||||||
|
ActionController::Base.helper Ransack::Helpers::FormHelper
|
|
@ -0,0 +1,2 @@
|
||||||
|
require 'ransack/adapters/active_record/base'
|
||||||
|
require 'ransack/adapters/active_record/context'
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Ransack
|
||||||
|
module Adapters
|
||||||
|
module ActiveRecord
|
||||||
|
module Base
|
||||||
|
|
||||||
|
def self.extended(base)
|
||||||
|
alias :search :ransack unless base.method_defined? :search
|
||||||
|
end
|
||||||
|
|
||||||
|
def ransack(params = {})
|
||||||
|
Search.new(self, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,153 @@
|
||||||
|
require 'ransack/context'
|
||||||
|
require 'active_record'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Adapters
|
||||||
|
module ActiveRecord
|
||||||
|
class Context < ::Ransack::Context
|
||||||
|
# Because the AR::Associations namespace is insane
|
||||||
|
JoinDependency = ::ActiveRecord::Associations::JoinDependency
|
||||||
|
JoinPart = JoinDependency::JoinPart
|
||||||
|
JoinAssociation = JoinDependency::JoinAssociation
|
||||||
|
|
||||||
|
def evaluate(search, opts = {})
|
||||||
|
relation = @object.where(accept(search.base)).order(accept(search.sorts))
|
||||||
|
opts[:distinct] ? relation.group(@klass.arel_table[@klass.primary_key]) : relation
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_method?(str, klass = @klass)
|
||||||
|
exists = false
|
||||||
|
|
||||||
|
if column = get_column(str, klass)
|
||||||
|
exists = true
|
||||||
|
elsif (segments = str.split(/_/)).size > 1
|
||||||
|
remainder = []
|
||||||
|
found_assoc = nil
|
||||||
|
while !found_assoc && remainder.unshift(segments.pop) && segments.size > 0 do
|
||||||
|
if found_assoc = get_association(segments.join('_'), klass)
|
||||||
|
exists = attribute_method?(remainder.join('_'), found_assoc.klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
exists
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_for(attr)
|
||||||
|
return nil unless attr
|
||||||
|
name = attr.name.to_s
|
||||||
|
table = attr.relation.table_name
|
||||||
|
|
||||||
|
unless @engine.connection_pool.table_exists?(table)
|
||||||
|
raise "No table named #{table} exists"
|
||||||
|
end
|
||||||
|
|
||||||
|
@engine.connection_pool.columns_hash[table][name].type
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def klassify(obj)
|
||||||
|
if Class === obj && ::ActiveRecord::Base > obj
|
||||||
|
obj
|
||||||
|
elsif obj.respond_to? :klass
|
||||||
|
obj.klass
|
||||||
|
elsif obj.respond_to? :active_record
|
||||||
|
obj.active_record
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Don't know how to klassify #{obj}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_attribute(str, parent = @base)
|
||||||
|
attribute = nil
|
||||||
|
|
||||||
|
if column = get_column(str, parent)
|
||||||
|
attribute = parent.table[str]
|
||||||
|
elsif (segments = str.split(/_/)).size > 1
|
||||||
|
remainder = []
|
||||||
|
found_assoc = nil
|
||||||
|
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
||||||
|
if found_assoc = get_association(segments.join('_'), parent)
|
||||||
|
join = build_or_find_association(found_assoc.name, parent)
|
||||||
|
attribute = get_attribute(remainder.join('_'), join)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attribute
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_column(str, parent = @base)
|
||||||
|
klassify(parent).columns_hash[str]
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_association(str, parent = @base)
|
||||||
|
klassify(parent).reflect_on_all_associations.detect {|a| a.name.to_s == str}
|
||||||
|
end
|
||||||
|
|
||||||
|
def join_dependency(relation)
|
||||||
|
if relation.respond_to?(:join_dependency) # MetaWhere will enable this
|
||||||
|
relation.join_dependency
|
||||||
|
else
|
||||||
|
build_join_dependency(relation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_join_dependency(relation)
|
||||||
|
buckets = relation.joins_values.group_by do |join|
|
||||||
|
case join
|
||||||
|
when String
|
||||||
|
'string_join'
|
||||||
|
when Hash, Symbol, Array
|
||||||
|
'association_join'
|
||||||
|
when ActiveRecord::Associations::JoinDependency::JoinAssociation
|
||||||
|
'stashed_join'
|
||||||
|
when Arel::Nodes::Join
|
||||||
|
'join_node'
|
||||||
|
else
|
||||||
|
raise 'unknown class: %s' % join.class.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
association_joins = buckets['association_join'] || []
|
||||||
|
stashed_association_joins = buckets['stashed_join'] || []
|
||||||
|
join_nodes = buckets['join_node'] || []
|
||||||
|
string_joins = (buckets['string_join'] || []).map { |x|
|
||||||
|
x.strip
|
||||||
|
}.uniq
|
||||||
|
|
||||||
|
join_list = relation.send :custom_join_ast, relation.table.from(relation.table), string_joins
|
||||||
|
|
||||||
|
join_dependency = JoinDependency.new(
|
||||||
|
relation.klass,
|
||||||
|
association_joins,
|
||||||
|
join_list
|
||||||
|
)
|
||||||
|
|
||||||
|
join_nodes.each do |join|
|
||||||
|
join_dependency.table_aliases[join.left.name.downcase] = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
join_dependency.graft(*stashed_association_joins)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_or_find_association(name, parent = @base)
|
||||||
|
found_association = @join_dependency.join_associations.detect do |assoc|
|
||||||
|
assoc.reflection.name == name &&
|
||||||
|
assoc.parent == parent
|
||||||
|
end
|
||||||
|
unless found_association
|
||||||
|
@join_dependency.send(:build, name.to_sym, parent, Arel::Nodes::OuterJoin)
|
||||||
|
found_association = @join_dependency.join_associations.last
|
||||||
|
# Leverage the stashed association functionality in AR
|
||||||
|
@object = @object.joins(found_association)
|
||||||
|
end
|
||||||
|
|
||||||
|
found_association
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
require 'ransack/constants'
|
||||||
|
require 'ransack/predicate'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Configuration
|
||||||
|
|
||||||
|
mattr_accessor :predicates
|
||||||
|
self.predicates = {}
|
||||||
|
|
||||||
|
def self.predicate_keys
|
||||||
|
predicates.keys.sort {|a,b| b.length <=> a.length}
|
||||||
|
end
|
||||||
|
|
||||||
|
def configure
|
||||||
|
yield self
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_predicate(name, opts = {})
|
||||||
|
name = name.to_s
|
||||||
|
opts[:name] = name
|
||||||
|
compounds = opts.delete(:compounds)
|
||||||
|
compounds = true if compounds.nil?
|
||||||
|
opts[:arel_predicate] = opts[:arel_predicate].to_s
|
||||||
|
|
||||||
|
self.predicates[name] = Predicate.new(opts)
|
||||||
|
|
||||||
|
['_any', '_all'].each do |suffix|
|
||||||
|
self.predicates[name + suffix] = Predicate.new(
|
||||||
|
opts.merge(
|
||||||
|
:name => name + suffix,
|
||||||
|
:arel_predicate => opts[:arel_predicate] + suffix,
|
||||||
|
:compound => true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end if compounds
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
module Ransack
|
||||||
|
module Constants
|
||||||
|
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
||||||
|
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
|
||||||
|
|
||||||
|
AREL_PREDICATES = %w(eq not_eq matches does_not_match lt lteq gt gteq in not_in)
|
||||||
|
|
||||||
|
DERIVED_PREDICATES = [
|
||||||
|
['cont', {:arel_predicate => 'matches', :formatter => proc {|v| "%#{v}%"}}],
|
||||||
|
['not_cont', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "%#{v}%"}}],
|
||||||
|
['start', {:arel_predicate => 'matches', :formatter => proc {|v| "#{v}%"}}],
|
||||||
|
['not_start', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "#{v}%"}}],
|
||||||
|
['end', {:arel_predicate => 'matches', :formatter => proc {|v| "%#{v}"}}],
|
||||||
|
['not_end', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "%#{v}"}}],
|
||||||
|
['true', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}}],
|
||||||
|
['false', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| !v}}],
|
||||||
|
['present', {:arel_predicate => 'not_eq_all', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| [nil, '']}}],
|
||||||
|
['blank', {:arel_predicate => 'eq_any', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| [nil, '']}}],
|
||||||
|
['null', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| nil}}],
|
||||||
|
['not_null', {:arel_predicate => 'not_eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| nil}}]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,152 @@
|
||||||
|
module Ransack
|
||||||
|
class Context
|
||||||
|
attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
|
||||||
|
|
||||||
|
class << self
|
||||||
|
|
||||||
|
def for(object)
|
||||||
|
context = Class === object ? for_class(object) : for_object(object)
|
||||||
|
context or raise ArgumentError, "Don't know what context to use for #{object}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_class(klass)
|
||||||
|
if klass < ActiveRecord::Base
|
||||||
|
Adapters::ActiveRecord::Context.new(klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_object(object)
|
||||||
|
case object
|
||||||
|
when ActiveRecord::Relation
|
||||||
|
Adapters::ActiveRecord::Context.new(object.klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_accept?(object)
|
||||||
|
method_defined? DISPATCH[object.class]
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(object)
|
||||||
|
@object = object.scoped
|
||||||
|
@klass = @object.klass
|
||||||
|
@join_dependency = join_dependency(@object)
|
||||||
|
@base = @join_dependency.join_base
|
||||||
|
@engine = @base.arel_engine
|
||||||
|
@arel_visitor = Arel::Visitors.visitor_for @engine
|
||||||
|
@default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
|
||||||
|
@attributes = Hash.new do |hash, key|
|
||||||
|
if attribute = get_attribute(key.to_s)
|
||||||
|
hash[key] = attribute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Convert a string representing a chain of associations and an attribute
|
||||||
|
# into the attribute itself
|
||||||
|
def contextualize(str)
|
||||||
|
@attributes[str]
|
||||||
|
end
|
||||||
|
|
||||||
|
def traverse(str, base = @base)
|
||||||
|
str ||= ''
|
||||||
|
|
||||||
|
if (segments = str.split(/_/)).size > 0
|
||||||
|
association_parts = []
|
||||||
|
found_assoc = nil
|
||||||
|
while !found_assoc && segments.size > 0 && association_parts << segments.shift do
|
||||||
|
if found_assoc = get_association(association_parts.join('_'), base)
|
||||||
|
base = traverse(segments.join('_'), found_assoc.klass)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
raise ArgumentError, "No association matches #{str}" unless found_assoc
|
||||||
|
end
|
||||||
|
|
||||||
|
klassify(base)
|
||||||
|
end
|
||||||
|
|
||||||
|
def association_path(str, base = @base)
|
||||||
|
base = klassify(base)
|
||||||
|
str ||= ''
|
||||||
|
path = []
|
||||||
|
segments = str.split(/_/)
|
||||||
|
association_parts = []
|
||||||
|
if (segments = str.split(/_/)).size > 0
|
||||||
|
while segments.size > 0 && !base.columns_hash[segments.join('_')] && association_parts << segments.shift do
|
||||||
|
if found_assoc = get_association(association_parts.join('_'), base)
|
||||||
|
path += association_parts
|
||||||
|
association_parts = []
|
||||||
|
base = klassify(found_assoc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
path.join('_')
|
||||||
|
end
|
||||||
|
|
||||||
|
def searchable_columns(str = '')
|
||||||
|
traverse(str).column_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept(object)
|
||||||
|
visit(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_accept?(object)
|
||||||
|
respond_to? DISPATCH[object.class]
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_Array(object)
|
||||||
|
object.map {|o| accept(o)}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_Ransack_Nodes_Condition(object)
|
||||||
|
object.apply_predicate if object.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_Ransack_Nodes_And(object)
|
||||||
|
nodes = object.values.map {|o| accept(o)}.compact
|
||||||
|
return nil unless nodes.size > 0
|
||||||
|
|
||||||
|
if nodes.size > 1
|
||||||
|
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(nodes))
|
||||||
|
else
|
||||||
|
nodes.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_Ransack_Nodes_Sort(object)
|
||||||
|
object.attr.send(object.dir) if object.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_Ransack_Nodes_Or(object)
|
||||||
|
nodes = object.values.map {|o| accept(o)}.compact
|
||||||
|
return nil unless nodes.size > 0
|
||||||
|
|
||||||
|
if nodes.size > 1
|
||||||
|
nodes.inject(&:or)
|
||||||
|
else
|
||||||
|
nodes.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def quoted?(object)
|
||||||
|
case object
|
||||||
|
when Arel::Nodes::SqlLiteral, Bignum, Fixnum
|
||||||
|
false
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit(object)
|
||||||
|
send(DISPATCH[object.class], object)
|
||||||
|
end
|
||||||
|
|
||||||
|
DISPATCH = Hash.new do |hash, klass|
|
||||||
|
hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
require 'ransack/helpers/form_builder'
|
||||||
|
require 'ransack/helpers/form_helper'
|
|
@ -0,0 +1,172 @@
|
||||||
|
require 'action_view'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Helpers
|
||||||
|
class FormBuilder < ::ActionView::Helpers::FormBuilder
|
||||||
|
def label(method, *args, &block)
|
||||||
|
options = args.extract_options!
|
||||||
|
text = args.first
|
||||||
|
i18n = options[:i18n] || {}
|
||||||
|
text ||= object.translate(method, i18n.reverse_merge(:include_associations => true)) if object.respond_to? :translate
|
||||||
|
super(method, text, options, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_select(options = {}, html_options = {})
|
||||||
|
raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
|
||||||
|
options[:include_blank] = true unless options.has_key?(:include_blank)
|
||||||
|
bases = [''] + association_array(options[:associations])
|
||||||
|
if bases.size > 1
|
||||||
|
collection = bases.map do |base|
|
||||||
|
[
|
||||||
|
Translate.association(base, :context => object.context),
|
||||||
|
object.context.searchable_columns(base).map do |c|
|
||||||
|
[
|
||||||
|
attr_from_base_and_column(base, c),
|
||||||
|
Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
]
|
||||||
|
end
|
||||||
|
@template.grouped_collection_select(
|
||||||
|
@object_name, :name, collection, :last, :first, :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
collection = object.context.searchable_columns(bases.first).map do |c|
|
||||||
|
[
|
||||||
|
attr_from_base_and_column(bases.first, c),
|
||||||
|
Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
@template.collection_select(
|
||||||
|
@object_name, :name, collection, :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_select(options = {}, html_options = {})
|
||||||
|
raise ArgumentError, "sort_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
|
||||||
|
options[:include_blank] = true unless options.has_key?(:include_blank)
|
||||||
|
bases = [''] + association_array(options[:associations])
|
||||||
|
if bases.any?
|
||||||
|
collection = bases.map do |base|
|
||||||
|
[
|
||||||
|
Translate.association(base, :context => object.context),
|
||||||
|
object.context.searchable_columns(base).map do |c|
|
||||||
|
[
|
||||||
|
attr_from_base_and_column(base, c),
|
||||||
|
Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
]
|
||||||
|
end
|
||||||
|
@template.grouped_collection_select(
|
||||||
|
@object_name, :name, collection, :last, :first, :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
) + @template.collection_select(
|
||||||
|
@object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
collection = object.context.searchable_columns(bases.first).map do |c|
|
||||||
|
[
|
||||||
|
attr_from_base_and_column(bases.first, c),
|
||||||
|
Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
@template.collection_select(
|
||||||
|
@object_name, :name, collection, :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
) + @template.collection_select(
|
||||||
|
@object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_fields(*args, &block)
|
||||||
|
search_fields(:s, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def condition_fields(*args, &block)
|
||||||
|
search_fields(:c, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def and_fields(*args, &block)
|
||||||
|
search_fields(:n, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def or_fields(*args, &block)
|
||||||
|
search_fields(:o, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_fields(*args, &block)
|
||||||
|
search_fields(:a, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def predicate_fields(*args, &block)
|
||||||
|
search_fields(:p, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_fields(*args, &block)
|
||||||
|
search_fields(:v, args, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_fields(name, args, block)
|
||||||
|
args << {} unless args.last.is_a?(Hash)
|
||||||
|
args.last[:builder] ||= options[:builder]
|
||||||
|
args.last[:parent_builder] = self
|
||||||
|
options = args.extract_options!
|
||||||
|
objects = args.shift
|
||||||
|
objects ||= @object.send(name)
|
||||||
|
objects = [objects] unless Array === objects
|
||||||
|
name = "#{options[:object_name] || object_name}[#{name}]"
|
||||||
|
output = ActiveSupport::SafeBuffer.new
|
||||||
|
objects.each do |child|
|
||||||
|
output << @template.fields_for("#{name}[#{options[:child_index] || nested_child_index(name)}]", child, options, &block)
|
||||||
|
end
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
def predicate_select(options = {}, html_options = {})
|
||||||
|
@template.collection_select(
|
||||||
|
@object_name, :p, Predicate.collection, :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def combinator_select(options = {}, html_options = {})
|
||||||
|
@template.collection_select(
|
||||||
|
@object_name, :m, [['or', Translate.word(:or)], ['and', Translate.word(:and)]], :first, :last,
|
||||||
|
objectify_options(options), @default_options.merge(html_options)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def association_array(obj, prefix = nil)
|
||||||
|
([prefix] + case obj
|
||||||
|
when Array
|
||||||
|
obj
|
||||||
|
when Hash
|
||||||
|
obj.map do |key, value|
|
||||||
|
case value
|
||||||
|
when Array, Hash
|
||||||
|
bases_array(value, key.to_s)
|
||||||
|
else
|
||||||
|
[key.to_s, [key, value].join('_')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
[obj]
|
||||||
|
end).compact.flatten.map {|v| [prefix, v].compact.join('_')}
|
||||||
|
end
|
||||||
|
|
||||||
|
def attr_from_base_and_column(base, column)
|
||||||
|
[base, column].reject {|v| v.blank?}.join('_')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
module Ransack
|
||||||
|
module Helpers
|
||||||
|
module FormHelper
|
||||||
|
def search_form_for(record, options = {}, &proc)
|
||||||
|
if record.is_a?(Ransack::Search)
|
||||||
|
search = record
|
||||||
|
options[:url] ||= polymorphic_path(search.klass)
|
||||||
|
elsif record.is_a?(Array) && (search = record.detect {|o| o.is_a?(Ransack::Search)})
|
||||||
|
options[:url] ||= polymorphic_path(record.map {|o| o.is_a?(Ransack::Search) ? o.klass : o})
|
||||||
|
else
|
||||||
|
raise ArgumentError, "No Ransack::Search object was provided to search_form_for!"
|
||||||
|
end
|
||||||
|
options[:html] ||= {}
|
||||||
|
html_options = {
|
||||||
|
:class => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
|
||||||
|
:id => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
|
||||||
|
:method => :get
|
||||||
|
}
|
||||||
|
options[:html].reverse_merge!(html_options)
|
||||||
|
options[:builder] ||= FormBuilder
|
||||||
|
|
||||||
|
form_for(record, options, &proc)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,67 @@
|
||||||
|
en:
|
||||||
|
ransack:
|
||||||
|
predicate: "predicate"
|
||||||
|
and: "and"
|
||||||
|
or: "or"
|
||||||
|
combinator: "combinator"
|
||||||
|
attribute: "attribute"
|
||||||
|
value: "value"
|
||||||
|
condition: "condition"
|
||||||
|
sort: "sort"
|
||||||
|
asc: "ascending"
|
||||||
|
desc: "descending"
|
||||||
|
predicates:
|
||||||
|
eq: "equals"
|
||||||
|
eq_any: "equals any"
|
||||||
|
eq_all: "equals all"
|
||||||
|
not_eq: "not equal to"
|
||||||
|
not_eq_any: "not equal to any"
|
||||||
|
not_eq_all: "not equal to all"
|
||||||
|
matches: "matches"
|
||||||
|
matches_any: "matches_any"
|
||||||
|
matches_all: "matches all"
|
||||||
|
does_not_match: "doesn't match"
|
||||||
|
does_not_match_any: "doesn't match any"
|
||||||
|
does_not_match_all: "doesn't match all"
|
||||||
|
lt: "less than"
|
||||||
|
lt_any: "less than any"
|
||||||
|
lt_all: "less than all"
|
||||||
|
lteq: "less than or equal to"
|
||||||
|
lteq_any: "less than or equal to any"
|
||||||
|
lteq_all: "less than or equal to all"
|
||||||
|
gt: "greater than"
|
||||||
|
gt_any: "greater than any"
|
||||||
|
gt_all: "greater than all"
|
||||||
|
gteq: "greater than or equal to"
|
||||||
|
gteq_any: "greater than or equal to any"
|
||||||
|
gteq_all: "greater than or equal to all"
|
||||||
|
in: "in"
|
||||||
|
in_any: "in any"
|
||||||
|
in_all: "in all"
|
||||||
|
not_in: "not in"
|
||||||
|
not_in_any: "not in any"
|
||||||
|
not_in_all: "not in all"
|
||||||
|
cont: "contains"
|
||||||
|
cont_any: "contains any"
|
||||||
|
cont_all: "contains all"
|
||||||
|
not_cont: "doesn't contain"
|
||||||
|
not_cont_any: "doesn't contain any"
|
||||||
|
not_cont_all: "doesn't contain all"
|
||||||
|
start: "starts with"
|
||||||
|
start_any: "starts with any"
|
||||||
|
start_all: "starts with all"
|
||||||
|
not_start: "doesn't start with"
|
||||||
|
not_start_any: "doesn't start with any"
|
||||||
|
not_start_all: "doesn't start with all"
|
||||||
|
end: "ends with"
|
||||||
|
end_any: "ends with any"
|
||||||
|
end_all: "ends with all"
|
||||||
|
not_end: "doesn't end with"
|
||||||
|
not_end_any: "doesn't end with any"
|
||||||
|
not_end_all: "doesn't end with all"
|
||||||
|
true: "is true"
|
||||||
|
false: "is false"
|
||||||
|
present: "is present"
|
||||||
|
blank: "is blank"
|
||||||
|
null: "is null"
|
||||||
|
not_null: "is not null"
|
|
@ -0,0 +1,53 @@
|
||||||
|
module Ransack
|
||||||
|
module Naming
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.extend ClassMethods
|
||||||
|
end
|
||||||
|
|
||||||
|
def persisted?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_key
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_model
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Name < String
|
||||||
|
attr_reader :singular, :plural, :element, :collection, :partial_path, :human, :param_key, :route_key, :i18n_key
|
||||||
|
alias_method :cache_key, :collection
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
super("Search")
|
||||||
|
@singular = "search".freeze
|
||||||
|
@plural = "searches".freeze
|
||||||
|
@element = "search".freeze
|
||||||
|
@human = "Search".freeze
|
||||||
|
@collection = "ransack/searches".freeze
|
||||||
|
@partial_path = "#{@collection}/#{@element}".freeze
|
||||||
|
@param_key = "q".freeze
|
||||||
|
@route_key = "searches".freeze
|
||||||
|
@i18n_key = :ransack
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def model_name
|
||||||
|
@_model_name ||= Name.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def i18n_scope
|
||||||
|
:ransack
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
require 'ransack/nodes/node'
|
||||||
|
require 'ransack/nodes/attribute'
|
||||||
|
require 'ransack/nodes/value'
|
||||||
|
require 'ransack/nodes/condition'
|
||||||
|
require 'ransack/nodes/sort'
|
||||||
|
require 'ransack/nodes/and'
|
||||||
|
require 'ransack/nodes/or'
|
|
@ -0,0 +1,8 @@
|
||||||
|
require 'ransack/nodes/grouping'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class And < Grouping
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Attribute < Node
|
||||||
|
attr_reader :name, :attr
|
||||||
|
delegate :blank?, :==, :to => :name
|
||||||
|
|
||||||
|
def initialize(context, name = nil)
|
||||||
|
super(context)
|
||||||
|
self.name = name unless name.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def name=(name)
|
||||||
|
@name = name
|
||||||
|
@attr = contextualize(name) unless name.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
@attr
|
||||||
|
end
|
||||||
|
|
||||||
|
def eql?(other)
|
||||||
|
self.class == other.class &&
|
||||||
|
self.name == other.name
|
||||||
|
end
|
||||||
|
alias :== :eql?
|
||||||
|
|
||||||
|
def hash
|
||||||
|
self.name.hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def persisted?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,209 @@
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Condition < Node
|
||||||
|
i18n_word :attribute, :predicate, :combinator, :value
|
||||||
|
i18n_alias :a => :attribute, :p => :predicate, :m => :combinator, :v => :value
|
||||||
|
|
||||||
|
attr_reader :predicate
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def extract(context, key, values)
|
||||||
|
attributes, predicate = extract_attributes_and_predicate(key)
|
||||||
|
if attributes.size > 0
|
||||||
|
combinator = key.match(/_(or|and)_/) ? $1 : nil
|
||||||
|
condition = self.new(context)
|
||||||
|
condition.build(
|
||||||
|
:a => attributes,
|
||||||
|
:p => predicate.name,
|
||||||
|
:m => combinator,
|
||||||
|
:v => [values]
|
||||||
|
)
|
||||||
|
predicate.validate(condition.values) ? condition : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def extract_attributes_and_predicate(key)
|
||||||
|
str = key.dup
|
||||||
|
name = Ransack::Configuration.predicate_keys.detect {|p| str.sub!(/_#{p}$/, '')}
|
||||||
|
predicate = Predicate.named(name)
|
||||||
|
raise ArgumentError, "No valid predicate for #{key}" unless predicate
|
||||||
|
attributes = str.split(/_and_|_or_/)
|
||||||
|
[attributes, predicate]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
attributes.detect(&:valid?) && predicate && valid_arity? && predicate.validate(values) && valid_combinator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_arity?
|
||||||
|
values.size <= 1 || predicate.compound || %w(in not_in).include?(predicate.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes
|
||||||
|
@attributes ||= []
|
||||||
|
end
|
||||||
|
alias :a :attributes
|
||||||
|
|
||||||
|
def attributes=(args)
|
||||||
|
case args
|
||||||
|
when Array
|
||||||
|
args.each do |attr|
|
||||||
|
attr = Attribute.new(@context, attr)
|
||||||
|
self.attributes << attr if attr.valid?
|
||||||
|
end
|
||||||
|
when Hash
|
||||||
|
args.each do |index, attrs|
|
||||||
|
attr = Attribute.new(@context, attrs[:name])
|
||||||
|
self.attributes << attr if attr.valid?
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Invalid argument (#{args.class}) supplied to attributes="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias :a= :attributes=
|
||||||
|
|
||||||
|
def values
|
||||||
|
@values ||= []
|
||||||
|
end
|
||||||
|
alias :v :values
|
||||||
|
|
||||||
|
def values=(args)
|
||||||
|
case args
|
||||||
|
when Array
|
||||||
|
args.each do |val|
|
||||||
|
val = Value.new(@context, val, current_type)
|
||||||
|
self.values << val
|
||||||
|
end
|
||||||
|
when Hash
|
||||||
|
args.each do |index, attrs|
|
||||||
|
val = Value.new(@context, attrs[:value], current_type)
|
||||||
|
self.values << val
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Invalid argument (#{args.class}) supplied to values="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias :v= :values=
|
||||||
|
|
||||||
|
def combinator
|
||||||
|
@attributes.size > 1 ? @combinator : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def combinator=(val)
|
||||||
|
@combinator = ['and', 'or'].detect {|v| v == val.to_s} || nil
|
||||||
|
end
|
||||||
|
alias :m= :combinator=
|
||||||
|
alias :m :combinator
|
||||||
|
|
||||||
|
def build_attribute(name = nil)
|
||||||
|
Attribute.new(@context, name).tap do |attribute|
|
||||||
|
self.attributes << attribute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_value(val = nil)
|
||||||
|
Value.new(@context, val, current_type).tap do |value|
|
||||||
|
self.values << value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
predicate.compound ? values.map(&:value) : values.first.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(params)
|
||||||
|
params.with_indifferent_access.each do |key, value|
|
||||||
|
if key.match(/^(a|v|p|m)$/)
|
||||||
|
self.send("#{key}=", value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set_value_types!
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def persisted?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def key
|
||||||
|
@key ||= attributes.map(&:name).join("_#{combinator}_") + "_#{predicate.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def eql?(other)
|
||||||
|
self.class == other.class &&
|
||||||
|
self.attributes == other.attributes &&
|
||||||
|
self.predicate == other.predicate &&
|
||||||
|
self.values == other.values &&
|
||||||
|
self.combinator == other.combinator
|
||||||
|
end
|
||||||
|
alias :== :eql?
|
||||||
|
|
||||||
|
def hash
|
||||||
|
[attributes, predicate, values, combinator].hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def predicate_name=(name)
|
||||||
|
self.predicate = Predicate.named(name)
|
||||||
|
end
|
||||||
|
alias :p= :predicate_name=
|
||||||
|
|
||||||
|
def predicate=(predicate)
|
||||||
|
@predicate = predicate
|
||||||
|
predicate
|
||||||
|
end
|
||||||
|
|
||||||
|
def predicate_name
|
||||||
|
predicate.name if predicate
|
||||||
|
end
|
||||||
|
alias :p :predicate_name
|
||||||
|
|
||||||
|
def apply_predicate
|
||||||
|
attributes = arel_attributes.compact
|
||||||
|
|
||||||
|
if attributes.size > 1
|
||||||
|
case combinator
|
||||||
|
when 'and'
|
||||||
|
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(
|
||||||
|
attributes.map {|a| a.send(predicate.arel_predicate, predicate.format(values))}
|
||||||
|
))
|
||||||
|
when 'or'
|
||||||
|
attributes.inject(attributes.shift.send(predicate.arel_predicate, predicate.format(values))) do |memo, a|
|
||||||
|
memo.or(a.send(predicate.arel_predicate, predicate.format(values)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
attributes.first.send(predicate.arel_predicate, predicate.format(values))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_value_types!
|
||||||
|
self.values.each {|v| v.type = current_type}
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_type
|
||||||
|
if predicate && predicate.type
|
||||||
|
predicate.type
|
||||||
|
elsif attributes.size > 0
|
||||||
|
@context.type_for(attributes.first.attr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_combinator?
|
||||||
|
attributes.size < 2 ||
|
||||||
|
['and', 'or'].include?(combinator)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arel_attributes
|
||||||
|
attributes.map(&:attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,207 @@
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Grouping < Node
|
||||||
|
attr_reader :conditions
|
||||||
|
i18n_word :condition, :and, :or
|
||||||
|
i18n_alias :c => :condition, :n => :and, :o => :or
|
||||||
|
|
||||||
|
delegate :each, :to => :values
|
||||||
|
|
||||||
|
def persisted?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def translate(key, options = {})
|
||||||
|
super or Translate.attribute(key.to_s, options.merge(:context => context))
|
||||||
|
end
|
||||||
|
|
||||||
|
def conditions
|
||||||
|
@conditions ||= []
|
||||||
|
end
|
||||||
|
alias :c :conditions
|
||||||
|
|
||||||
|
def conditions=(conditions)
|
||||||
|
case conditions
|
||||||
|
when Array
|
||||||
|
conditions.each do |attrs|
|
||||||
|
condition = Condition.new(@context).build(attrs)
|
||||||
|
self.conditions << condition if condition.valid?
|
||||||
|
end
|
||||||
|
when Hash
|
||||||
|
conditions.each do |index, attrs|
|
||||||
|
condition = Condition.new(@context).build(attrs)
|
||||||
|
self.conditions << condition if condition.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.conditions.uniq!
|
||||||
|
end
|
||||||
|
alias :c= :conditions=
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
if condition = conditions.detect {|c| c.key == key.to_s}
|
||||||
|
condition
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def []=(key, value)
|
||||||
|
conditions.reject! {|c| c.key == key.to_s}
|
||||||
|
self.conditions << value
|
||||||
|
end
|
||||||
|
|
||||||
|
def values
|
||||||
|
conditions + ors + ands
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to?(method_id)
|
||||||
|
super or begin
|
||||||
|
method_name = method_id.to_s
|
||||||
|
writer = method_name.sub!(/\=$/, '')
|
||||||
|
attribute_method?(method_name) ? true : false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_condition(opts = {})
|
||||||
|
new_condition(opts).tap do |condition|
|
||||||
|
self.conditions << condition
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_condition(opts = {})
|
||||||
|
attrs = opts[:attributes] || 1
|
||||||
|
vals = opts[:values] || 1
|
||||||
|
condition = Condition.new(@context)
|
||||||
|
condition.predicate = Predicate.named('eq')
|
||||||
|
attrs.times { condition.build_attribute }
|
||||||
|
vals.times { condition.build_value }
|
||||||
|
condition
|
||||||
|
end
|
||||||
|
|
||||||
|
def ands
|
||||||
|
@ands ||= []
|
||||||
|
end
|
||||||
|
alias :n :ands
|
||||||
|
|
||||||
|
def ands=(ands)
|
||||||
|
case ands
|
||||||
|
when Array
|
||||||
|
ands.each do |attrs|
|
||||||
|
and_object = And.new(@context).build(attrs)
|
||||||
|
self.ands << and_object if and_object.values.any?
|
||||||
|
end
|
||||||
|
when Hash
|
||||||
|
ands.each do |index, attrs|
|
||||||
|
and_object = And.new(@context).build(attrs)
|
||||||
|
self.ands << and_object if and_object.values.any?
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Invalid argument (#{ands.class}) supplied to ands="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias :n= :ands=
|
||||||
|
|
||||||
|
def ors
|
||||||
|
@ors ||= []
|
||||||
|
end
|
||||||
|
alias :o :ors
|
||||||
|
|
||||||
|
def ors=(ors)
|
||||||
|
case ors
|
||||||
|
when Array
|
||||||
|
ors.each do |attrs|
|
||||||
|
or_object = Or.new(@context).build(attrs)
|
||||||
|
self.ors << or_object if or_object.values.any?
|
||||||
|
end
|
||||||
|
when Hash
|
||||||
|
ors.each do |index, attrs|
|
||||||
|
or_object = Or.new(@context).build(attrs)
|
||||||
|
self.ors << or_object if or_object.values.any?
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Invalid argument (#{ors.class}) supplied to ors="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias :o= :ors=
|
||||||
|
|
||||||
|
def method_missing(method_id, *args)
|
||||||
|
method_name = method_id.to_s
|
||||||
|
writer = method_name.sub!(/\=$/, '')
|
||||||
|
if attribute_method?(method_name)
|
||||||
|
writer ? write_attribute(method_name, *args) : read_attribute(method_name)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_method?(name)
|
||||||
|
name = strip_predicate_and_index(name)
|
||||||
|
case name
|
||||||
|
when /^(n|o|c|ands|ors|conditions)=?$/
|
||||||
|
true
|
||||||
|
else
|
||||||
|
name.split(/_and_|_or_/).select {|n| !@context.attribute_method?(n)}.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_and(params = {})
|
||||||
|
params ||= {}
|
||||||
|
new_and(params).tap do |new_and|
|
||||||
|
self.ands << new_and
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_and(params = {})
|
||||||
|
And.new(@context).build(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_or(params = {})
|
||||||
|
params ||= {}
|
||||||
|
new_or(params).tap do |new_or|
|
||||||
|
self.ors << new_or
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_or(params = {})
|
||||||
|
Or.new(@context).build(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(params)
|
||||||
|
params.with_indifferent_access.each do |key, value|
|
||||||
|
case key
|
||||||
|
when /^(n|o|c)$/
|
||||||
|
self.send("#{key}=", value)
|
||||||
|
else
|
||||||
|
write_attribute(key.to_s, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def write_attribute(name, val)
|
||||||
|
# TODO: Methods
|
||||||
|
if condition = Condition.extract(@context, name, val)
|
||||||
|
self[name] = condition
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_attribute(name)
|
||||||
|
if self[name].respond_to?(:value)
|
||||||
|
self[name].value
|
||||||
|
else
|
||||||
|
self[name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def strip_predicate_and_index(str)
|
||||||
|
string = str.split(/\(/).first
|
||||||
|
Ransack::Configuration.predicate_keys.detect {|p| string.sub!(/_#{p}$/, '')}
|
||||||
|
string
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Node
|
||||||
|
attr_reader :context
|
||||||
|
delegate :contextualize, :to => :context
|
||||||
|
class_attribute :i18n_words
|
||||||
|
class_attribute :i18n_aliases
|
||||||
|
self.i18n_words = []
|
||||||
|
self.i18n_aliases = {}
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def i18n_word(*args)
|
||||||
|
self.i18n_words += args.map(&:to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def i18n_alias(opts = {})
|
||||||
|
self.i18n_aliases.merge! Hash[opts.map {|k, v| [k.to_s, v.to_s]}]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(context)
|
||||||
|
@context = context
|
||||||
|
end
|
||||||
|
|
||||||
|
def translate(key, options = {})
|
||||||
|
key = i18n_aliases[key.to_s] if i18n_aliases.has_key?(key.to_s)
|
||||||
|
if i18n_words.include?(key.to_s)
|
||||||
|
Translate.word(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
require 'ransack/nodes/grouping'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Or < Grouping
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Sort < Node
|
||||||
|
attr_reader :name, :attr, :dir
|
||||||
|
i18n_word :asc, :desc
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def extract(context, str)
|
||||||
|
attr, direction = str.split(/\s+/,2)
|
||||||
|
self.new(context).build(:name => attr, :dir => direction)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(params)
|
||||||
|
params.with_indifferent_access.each do |key, value|
|
||||||
|
if key.match(/^(name|dir)$/)
|
||||||
|
self.send("#{key}=", value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
@attr
|
||||||
|
end
|
||||||
|
|
||||||
|
def name=(name)
|
||||||
|
@name = name
|
||||||
|
@attr = contextualize(name) unless name.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def dir=(dir)
|
||||||
|
@dir = %w(asc desc).include?(dir) ? dir : 'asc'
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,120 @@
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
class Value < Node
|
||||||
|
attr_reader :value_before_cast, :type
|
||||||
|
delegate :blank?, :to => :value_before_cast
|
||||||
|
|
||||||
|
def initialize(context, value = nil, type = nil)
|
||||||
|
super(context)
|
||||||
|
@value_before_cast = value
|
||||||
|
self.type = type if type
|
||||||
|
end
|
||||||
|
|
||||||
|
def value=(val)
|
||||||
|
@value_before_cast = value
|
||||||
|
@value = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
@value ||= cast_to_type(@value_before_cast, @type)
|
||||||
|
end
|
||||||
|
|
||||||
|
def persisted?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def eql?(other)
|
||||||
|
self.class == other.class &&
|
||||||
|
self.value_before_cast == other.value_before_cast
|
||||||
|
end
|
||||||
|
alias :== :eql?
|
||||||
|
|
||||||
|
def hash
|
||||||
|
value_before_cast.hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def type=(type)
|
||||||
|
@value = nil
|
||||||
|
@type = type
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_type(val, type)
|
||||||
|
case type
|
||||||
|
when :date
|
||||||
|
cast_to_date(val)
|
||||||
|
when :datetime, :timestamp, :time
|
||||||
|
cast_to_time(val)
|
||||||
|
when :boolean
|
||||||
|
cast_to_boolean(val)
|
||||||
|
when :integer
|
||||||
|
cast_to_integer(val)
|
||||||
|
when :float
|
||||||
|
cast_to_float(val)
|
||||||
|
when :decimal
|
||||||
|
cast_to_decimal(val)
|
||||||
|
else
|
||||||
|
cast_to_string(val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_date(val)
|
||||||
|
if val.respond_to?(:to_date)
|
||||||
|
val.to_date rescue nil
|
||||||
|
else
|
||||||
|
y, m, d = *[val].flatten
|
||||||
|
m ||= 1
|
||||||
|
d ||= 1
|
||||||
|
Date.new(y,m,d) rescue nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXME: doesn't seem to be casting, even with Time.zone.local
|
||||||
|
def cast_to_time(val)
|
||||||
|
if val.is_a?(Array)
|
||||||
|
Time.zone.local(*val) rescue nil
|
||||||
|
else
|
||||||
|
unless val.acts_like?(:time)
|
||||||
|
val = val.is_a?(String) ? Time.zone.parse(val) : val.to_time rescue val
|
||||||
|
end
|
||||||
|
val.in_time_zone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_boolean(val)
|
||||||
|
if val.is_a?(String) && val.blank?
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
Constants::TRUE_VALUES.include?(val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_string(val)
|
||||||
|
val.respond_to?(:to_s) ? val.to_s : String.new(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_integer(val)
|
||||||
|
val.blank? ? nil : val.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_float(val)
|
||||||
|
val.blank? ? nil : val.to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_to_decimal(val)
|
||||||
|
if val.blank?
|
||||||
|
nil
|
||||||
|
elsif val.class == BigDecimal
|
||||||
|
val
|
||||||
|
elsif val.respond_to?(:to_d)
|
||||||
|
val.to_d
|
||||||
|
else
|
||||||
|
val.to_s.to_d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def array_of_arrays?(val)
|
||||||
|
Array === val && Array === val.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,57 @@
|
||||||
|
module Ransack
|
||||||
|
class Predicate
|
||||||
|
attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def named(name)
|
||||||
|
Configuration.predicates[name.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_attribute_name(attribute_name)
|
||||||
|
self.named(Configuration.predicate_keys.detect {|p| attribute_name.to_s.match(/_#{p}$/)})
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection
|
||||||
|
Configuration.predicates.map {|k, v| [k, Translate.predicate(k)]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(opts = {})
|
||||||
|
@name = opts[:name]
|
||||||
|
@arel_predicate = opts[:arel_predicate]
|
||||||
|
@type = opts[:type]
|
||||||
|
@formatter = opts[:formatter]
|
||||||
|
@validator = opts[:validator]
|
||||||
|
@compound = opts[:compound]
|
||||||
|
end
|
||||||
|
|
||||||
|
def format(vals)
|
||||||
|
if formatter
|
||||||
|
vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
|
||||||
|
map {|v| formatter.call(v.value)}
|
||||||
|
else
|
||||||
|
vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
|
||||||
|
map {|v| v.value}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def eql?(other)
|
||||||
|
self.class == other.class &&
|
||||||
|
self.name == other.name
|
||||||
|
end
|
||||||
|
alias :== :eql?
|
||||||
|
|
||||||
|
def hash
|
||||||
|
name.hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate(vals)
|
||||||
|
if validator
|
||||||
|
vals.select {|v| validator.call(v.value_before_cast)}.any?
|
||||||
|
else
|
||||||
|
vals.select {|v| !v.blank?}.any?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,114 @@
|
||||||
|
require 'ransack/nodes'
|
||||||
|
require 'ransack/context'
|
||||||
|
require 'ransack/naming'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
class Search
|
||||||
|
include Naming
|
||||||
|
|
||||||
|
attr_reader :base, :context
|
||||||
|
|
||||||
|
delegate :object, :klass, :to => :context
|
||||||
|
delegate :new_and, :new_or, :new_condition,
|
||||||
|
:build_and, :build_or, :build_condition,
|
||||||
|
:translate, :to => :base
|
||||||
|
|
||||||
|
def initialize(object, params = {})
|
||||||
|
params ||= {}
|
||||||
|
@context = Context.for(object)
|
||||||
|
@base = Nodes::And.new(@context)
|
||||||
|
build(params.with_indifferent_access)
|
||||||
|
end
|
||||||
|
|
||||||
|
def result(opts = {})
|
||||||
|
@result ||= @context.evaluate(self, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(params)
|
||||||
|
collapse_multiparameter_attributes!(params).each do |key, value|
|
||||||
|
case key
|
||||||
|
when 's', 'sorts'
|
||||||
|
send("#{key}=", value)
|
||||||
|
else
|
||||||
|
base.send("#{key}=", value) if base.attribute_method?(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def sorts=(args)
|
||||||
|
case args
|
||||||
|
when Array
|
||||||
|
args.each do |sort|
|
||||||
|
sort = Nodes::Sort.extract(@context, sort)
|
||||||
|
self.sorts << sort
|
||||||
|
end
|
||||||
|
when Hash
|
||||||
|
args.each do |index, attrs|
|
||||||
|
sort = Nodes::Sort.new(@context).build(attrs)
|
||||||
|
self.sorts << sort
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.sorts = [args]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias :s= :sorts=
|
||||||
|
|
||||||
|
def sorts
|
||||||
|
@sorts ||= []
|
||||||
|
end
|
||||||
|
alias :s :sorts
|
||||||
|
|
||||||
|
def build_sort(opts = {})
|
||||||
|
new_sort(opts).tap do |sort|
|
||||||
|
self.sorts << sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_sort(opts = {})
|
||||||
|
Nodes::Sort.new(@context).build(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to?(method_id)
|
||||||
|
super or begin
|
||||||
|
method_name = method_id.to_s
|
||||||
|
writer = method_name.sub!(/\=$/, '')
|
||||||
|
base.attribute_method?(method_name) ? true : false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_missing(method_id, *args)
|
||||||
|
method_name = method_id.to_s
|
||||||
|
writer = method_name.sub!(/\=$/, '')
|
||||||
|
if base.attribute_method?(method_name)
|
||||||
|
base.send(method_id, *args)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collapse_multiparameter_attributes!(attrs)
|
||||||
|
attrs.keys.each do |k|
|
||||||
|
if k.include?("(")
|
||||||
|
real_attribute, position = k.split(/\(|\)/)
|
||||||
|
cast = %w(a s i).include?(position.last) ? position.last : nil
|
||||||
|
position = position.to_i - 1
|
||||||
|
value = attrs.delete(k)
|
||||||
|
attrs[real_attribute] ||= []
|
||||||
|
attrs[real_attribute][position] = if cast
|
||||||
|
(value.blank? && cast == 'i') ? nil : value.send("to_#{cast}")
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
elsif Hash === attrs[k]
|
||||||
|
collapse_multiparameter_attributes!(attrs[k])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,92 @@
|
||||||
|
I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'locale', '*.yml')]
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Translate
|
||||||
|
def self.word(key, options = {})
|
||||||
|
I18n.translate(:"ransack.#{key}", :default => key.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.predicate(key, options = {})
|
||||||
|
I18n.translate(:"ransack.predicates.#{key}", :default => key.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.attribute(key, options = {})
|
||||||
|
unless context = options.delete(:context)
|
||||||
|
raise ArgumentError, "A context is required to translate associations"
|
||||||
|
end
|
||||||
|
|
||||||
|
original_name = key.to_s
|
||||||
|
base_class = context.klass
|
||||||
|
base_ancestors = base_class.ancestors.select { |x| x.respond_to?(:model_name) }
|
||||||
|
predicate = Ransack::Configuration.predicate_keys.detect {|p| original_name.match(/_#{p}$/)}
|
||||||
|
attributes_str = original_name.sub(/_#{predicate}$/, '')
|
||||||
|
attribute_names = attributes_str.split(/_and_|_or_/)
|
||||||
|
combinator = attributes_str.match(/_and_/) ? :and : :or
|
||||||
|
defaults = base_ancestors.map do |klass|
|
||||||
|
:"ransack.attributes.#{klass.model_name.underscore}.#{original_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
translated_names = attribute_names.map do |attr|
|
||||||
|
attribute_name(context, attr, options[:include_associations])
|
||||||
|
end
|
||||||
|
|
||||||
|
interpolations = {}
|
||||||
|
interpolations[:attributes] = translated_names.join(" #{Translate.word(combinator)} ")
|
||||||
|
|
||||||
|
if predicate
|
||||||
|
defaults << "%{attributes} %{predicate}"
|
||||||
|
interpolations[:predicate] = Translate.predicate(predicate)
|
||||||
|
else
|
||||||
|
defaults << "%{attributes}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defaults << options.delete(:default) if options[:default]
|
||||||
|
options.reverse_merge! :count => 1, :default => defaults
|
||||||
|
I18n.translate(defaults.shift, options.merge(interpolations))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.association(key, options = {})
|
||||||
|
unless context = options.delete(:context)
|
||||||
|
raise ArgumentError, "A context is required to translate associations"
|
||||||
|
end
|
||||||
|
|
||||||
|
defaults = key.blank? ? [:"#{context.klass.i18n_scope}.models.#{context.klass.model_name.underscore}"] : [:"ransack.associations.#{context.klass.model_name.underscore}.#{key}"]
|
||||||
|
defaults << context.traverse(key).model_name.human
|
||||||
|
options = {:count => 1, :default => defaults}
|
||||||
|
I18n.translate(defaults.shift, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.attribute_name(context, name, include_associations = nil)
|
||||||
|
assoc_path = context.association_path(name)
|
||||||
|
associated_class = context.traverse(assoc_path) if assoc_path.present?
|
||||||
|
attr_name = name.sub(/^#{assoc_path}_/, '')
|
||||||
|
interpolations = {}
|
||||||
|
interpolations[:attr_fallback_name] = I18n.translate(
|
||||||
|
(associated_class ?
|
||||||
|
:"ransack.attributes.#{associated_class.model_name.underscore}.#{attr_name}" :
|
||||||
|
:"ransack.attributes.#{context.klass.model_name.underscore}.#{attr_name}"
|
||||||
|
),
|
||||||
|
:default => [
|
||||||
|
(associated_class ?
|
||||||
|
:"#{associated_class.i18n_scope}.attributes.#{associated_class.model_name.underscore}.#{attr_name}" :
|
||||||
|
:"#{context.klass.i18n_scope}.attributes.#{context.klass.model_name.underscore}.#{attr_name}"
|
||||||
|
),
|
||||||
|
attr_name.humanize
|
||||||
|
]
|
||||||
|
)
|
||||||
|
defaults = [
|
||||||
|
:"ransack.attributes.#{context.klass.model_name.underscore}.#{name}"
|
||||||
|
]
|
||||||
|
if include_associations && associated_class
|
||||||
|
defaults << '%{association_name} %{attr_fallback_name}'
|
||||||
|
interpolations[:association_name] = association(assoc_path, :context => context)
|
||||||
|
else
|
||||||
|
defaults << '%{attr_fallback_name}'
|
||||||
|
end
|
||||||
|
options = {:count => 1, :default => defaults}
|
||||||
|
I18n.translate(defaults.shift, options.merge(interpolations))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
module Ransack
|
||||||
|
VERSION = "0.1.0"
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
$:.push File.expand_path("../lib", __FILE__)
|
||||||
|
require "ransack/version"
|
||||||
|
|
||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = "ransack"
|
||||||
|
s.version = Ransack::VERSION
|
||||||
|
s.platform = Gem::Platform::RUBY
|
||||||
|
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.rubyforge_project = "ransack"
|
||||||
|
|
||||||
|
s.add_dependency 'activerecord', '~> 3.1.0.alpha'
|
||||||
|
s.add_dependency 'activesupport', '~> 3.1.0.alpha'
|
||||||
|
s.add_dependency 'actionpack', '~> 3.1.0.alpha'
|
||||||
|
s.add_development_dependency 'rspec', '~> 2.5.0'
|
||||||
|
s.add_development_dependency 'machinist', '~> 1.0.6'
|
||||||
|
s.add_development_dependency 'faker', '~> 0.9.5'
|
||||||
|
s.add_development_dependency 'sqlite3', '~> 1.3.3'
|
||||||
|
|
||||||
|
s.files = `git ls-files`.split("\n")
|
||||||
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
||||||
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
||||||
|
s.require_paths = ["lib"]
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
Article.blueprint do
|
||||||
|
person
|
||||||
|
title
|
||||||
|
body
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
Comment.blueprint do
|
||||||
|
article
|
||||||
|
person
|
||||||
|
body
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
Note.blueprint do
|
||||||
|
note
|
||||||
|
end
|
|
@ -0,0 +1,4 @@
|
||||||
|
Person.blueprint do
|
||||||
|
name
|
||||||
|
salary
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
Tag.blueprint do
|
||||||
|
name { Sham.tag_name }
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
Bundler.setup
|
||||||
|
require 'machinist/active_record'
|
||||||
|
require 'sham'
|
||||||
|
require 'faker'
|
||||||
|
|
||||||
|
Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
|
||||||
|
require f
|
||||||
|
end
|
||||||
|
|
||||||
|
Sham.define do
|
||||||
|
name { Faker::Name.name }
|
||||||
|
title { Faker::Lorem.sentence }
|
||||||
|
body { Faker::Lorem.paragraph }
|
||||||
|
salary {|index| 30000 + (index * 1000)}
|
||||||
|
tag_name { Faker::Lorem.words(3).join(' ') }
|
||||||
|
note { Faker::Lorem.words(7).join(' ') }
|
||||||
|
end
|
||||||
|
|
||||||
|
Schema.create
|
||||||
|
|
||||||
|
require 'ransack'
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
module RansackHelper
|
||||||
|
end
|
|
@ -0,0 +1,37 @@
|
||||||
|
$VERBOSE = false
|
||||||
|
require 'bundler'
|
||||||
|
Bundler.setup
|
||||||
|
require 'machinist/active_record'
|
||||||
|
require 'sham'
|
||||||
|
require 'faker'
|
||||||
|
|
||||||
|
Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
|
||||||
|
require f
|
||||||
|
end
|
||||||
|
|
||||||
|
Sham.define do
|
||||||
|
name { Faker::Name.name }
|
||||||
|
title { Faker::Lorem.sentence }
|
||||||
|
body { Faker::Lorem.paragraph }
|
||||||
|
salary {|index| 30000 + (index * 1000)}
|
||||||
|
tag_name { Faker::Lorem.words(3).join(' ') }
|
||||||
|
note { Faker::Lorem.words(7).join(' ') }
|
||||||
|
end
|
||||||
|
|
||||||
|
Schema.create
|
||||||
|
|
||||||
|
require 'ransack'
|
||||||
|
|
||||||
|
Article.joins{person.comments}.where{person.comments.body =~ '%hello%'}.to_sql
|
||||||
|
# => "SELECT \"articles\".* FROM \"articles\" INNER JOIN \"people\" ON \"people\".\"id\" = \"articles\".\"person_id\" INNER JOIN \"comments\" ON \"comments\".\"person_id\" = \"people\".\"id\" WHERE \"comments\".\"body\" LIKE '%hello%'"
|
||||||
|
|
||||||
|
Person.where{(id + 1) == 2}.first
|
||||||
|
# => #<Person id: 1, parent_id: nil, name: "Aric Smith", salary: 31000>
|
||||||
|
|
||||||
|
Person.where{(salary - 40000) < 0}.to_sql
|
||||||
|
# => "SELECT \"people\".* FROM \"people\" WHERE \"people\".\"salary\" - 40000 < 0"
|
||||||
|
|
||||||
|
p = Person.select{[id, name, salary, (salary + 1000).as('salary_after_increase')]}.first
|
||||||
|
# => #<Person id: 1, name: "Aric Smith", salary: 31000>
|
||||||
|
|
||||||
|
p.salary_after_increase # =>
|
|
@ -0,0 +1,30 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Adapters
|
||||||
|
module ActiveRecord
|
||||||
|
describe Base do
|
||||||
|
|
||||||
|
it 'adds a ransack method to ActiveRecord::Base' do
|
||||||
|
::ActiveRecord::Base.should respond_to :ransack
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'aliases the method to search if available' do
|
||||||
|
::ActiveRecord::Base.should respond_to :search
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#search' do
|
||||||
|
before do
|
||||||
|
@s = Person.search
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a search with Relation as its object' do
|
||||||
|
@s.should be_a Search
|
||||||
|
@s.object.should be_an ::ActiveRecord::Relation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Adapters
|
||||||
|
module ActiveRecord
|
||||||
|
describe Context do
|
||||||
|
before do
|
||||||
|
@c = Context.new(Person)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contextualizes strings to attributes' do
|
||||||
|
attribute = @c.contextualize 'children_children_parent_name'
|
||||||
|
attribute.should be_a Arel::Attributes::Attribute
|
||||||
|
attribute.name.should eq 'name'
|
||||||
|
attribute.relation.table_alias.should eq 'parents_people'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'builds new associations if not yet built' do
|
||||||
|
attribute = @c.contextualize 'children_articles_title'
|
||||||
|
attribute.should be_a Arel::Attributes::Attribute
|
||||||
|
attribute.name.should eq 'title'
|
||||||
|
attribute.relation.name.should eq 'articles'
|
||||||
|
attribute.relation.table_alias.should be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,11 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
describe Configuration do
|
||||||
|
it 'yields self on configure' do
|
||||||
|
Ransack.configure do
|
||||||
|
self.should eq Ransack::Configuration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Helpers
|
||||||
|
describe FormBuilder do
|
||||||
|
|
||||||
|
router = ActionDispatch::Routing::RouteSet.new
|
||||||
|
router.draw do
|
||||||
|
resources :people
|
||||||
|
match ':controller(/:action(/:id(.:format)))'
|
||||||
|
end
|
||||||
|
|
||||||
|
include router.url_helpers
|
||||||
|
|
||||||
|
# FIXME: figure out a cleaner way to get this behavior
|
||||||
|
before do
|
||||||
|
@controller = ActionView::TestCase::TestController.new
|
||||||
|
@controller.instance_variable_set(:@_routes, router)
|
||||||
|
@controller.class_eval do
|
||||||
|
include router.url_helpers
|
||||||
|
end
|
||||||
|
|
||||||
|
@s = Person.search
|
||||||
|
@controller.view_context.search_form_for @s do |f|
|
||||||
|
@f = f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'selects previously-entered time values with datetime_select' do
|
||||||
|
@s.created_at_eq = [2011, 1, 2, 3, 4, 5]
|
||||||
|
html = @f.datetime_select :created_at_eq
|
||||||
|
[2011, 1, 2, 3, 4, 5].each do |val|
|
||||||
|
html.should match /<option selected="selected" value="#{val}">#{val}<\/option>/o
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
module Nodes
|
||||||
|
describe Grouping do
|
||||||
|
before do
|
||||||
|
@g = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
describe Predicate do
|
||||||
|
|
||||||
|
before do
|
||||||
|
@s = Search.new(Person)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'cont' do
|
||||||
|
it 'generates a LIKE query with value surrounded by %' do
|
||||||
|
@s.name_cont = 'ric'
|
||||||
|
@s.result.to_sql.should match /"people"."name" LIKE '%ric%'/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'not_cont' do
|
||||||
|
it 'generates a NOT LIKE query with value surrounded by %' do
|
||||||
|
@s.name_not_cont = 'ric'
|
||||||
|
@s.result.to_sql.should match /"people"."name" NOT LIKE '%ric%'/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,182 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Ransack
|
||||||
|
describe Search do
|
||||||
|
|
||||||
|
describe '#build' do
|
||||||
|
it 'creates Conditions for top-level attributes' do
|
||||||
|
search = Search.new(Person, :name_eq => 'Ernie')
|
||||||
|
condition = search.base[:name_eq]
|
||||||
|
condition.should be_a Nodes::Condition
|
||||||
|
condition.predicate.name.should eq 'eq'
|
||||||
|
condition.attributes.first.name.should eq 'name'
|
||||||
|
condition.value.should eq 'Ernie'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates Conditions for association attributes' do
|
||||||
|
search = Search.new(Person, :children_name_eq => 'Ernie')
|
||||||
|
condition = search.base[:children_name_eq]
|
||||||
|
condition.should be_a Nodes::Condition
|
||||||
|
condition.predicate.name.should eq 'eq'
|
||||||
|
condition.attributes.first.name.should eq 'children_name'
|
||||||
|
condition.value.should eq 'Ernie'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'discards empty conditions' do
|
||||||
|
search = Search.new(Person, :children_name_eq => '')
|
||||||
|
condition = search.base[:children_name_eq]
|
||||||
|
condition.should be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts arrays of groupings' do
|
||||||
|
search = Search.new(Person,
|
||||||
|
:o => [
|
||||||
|
{:name_eq => 'Ernie', :children_name_eq => 'Ernie'},
|
||||||
|
{:name_eq => 'Bert', :children_name_eq => 'Bert'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
ors = search.ors
|
||||||
|
ors.should have(2).items
|
||||||
|
or1, or2 = ors
|
||||||
|
or1.should be_a Nodes::Or
|
||||||
|
or2.should be_a Nodes::Or
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts "attributes" hashes for groupings' do
|
||||||
|
search = Search.new(Person,
|
||||||
|
:o => {
|
||||||
|
'0' => {:name_eq => 'Ernie', :children_name_eq => 'Ernie'},
|
||||||
|
'1' => {:name_eq => 'Bert', :children_name_eq => 'Bert'},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ors = search.ors
|
||||||
|
ors.should have(2).items
|
||||||
|
or1, or2 = ors
|
||||||
|
or1.should be_a Nodes::Or
|
||||||
|
or2.should be_a Nodes::Or
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts "attributes" hashes for conditions' do
|
||||||
|
search = Search.new(Person,
|
||||||
|
:c => {
|
||||||
|
'0' => {:a => ['name'], :p => 'eq', :v => ['Ernie']},
|
||||||
|
'1' => {:a => ['children_name', 'parent_name'], :p => 'eq', :v => ['Ernie'], :m => 'or'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
conditions = search.base.conditions
|
||||||
|
conditions.should have(2).items
|
||||||
|
conditions.map {|c| c.class}.should eq [Nodes::Condition, Nodes::Condition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#result' do
|
||||||
|
it 'evaluates conditions contextually' do
|
||||||
|
search = Search.new(Person, :children_name_eq => 'Ernie')
|
||||||
|
search.result.should be_an ActiveRecord::Relation
|
||||||
|
where = search.result.where_values.first
|
||||||
|
where.to_sql.should match /"children_people"\."name" = 'Ernie'/
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates compound conditions contextually' do
|
||||||
|
search = Search.new(Person, :children_name_or_name_eq => 'Ernie')
|
||||||
|
search.result.should be_an ActiveRecord::Relation
|
||||||
|
where = search.result.where_values.first
|
||||||
|
where.to_sql.should match /"children_people"\."name" = 'Ernie' OR "people"\."name" = 'Ernie'/
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates nested conditions' do
|
||||||
|
search = Search.new(Person, :children_name_eq => 'Ernie',
|
||||||
|
:o => [{
|
||||||
|
:name_eq => 'Ernie',
|
||||||
|
:children_children_name_eq => 'Ernie'
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
search.result.should be_an ActiveRecord::Relation
|
||||||
|
where = search.result.where_values.first
|
||||||
|
where.to_sql.should match /\("children_people"."name" = 'Ernie' AND \("people"."name" = 'Ernie' OR "children_people_2"."name" = 'Ernie'\)\)/
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'evaluates arrays of groupings' do
|
||||||
|
search = Search.new(Person,
|
||||||
|
:o => [
|
||||||
|
{:name_eq => 'Ernie', :children_name_eq => 'Ernie'},
|
||||||
|
{:name_eq => 'Bert', :children_name_eq => 'Bert'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
search.result.should be_an ActiveRecord::Relation
|
||||||
|
where = search.result.where_values.first
|
||||||
|
where.to_sql.should match /\(\("people"."name" = 'Ernie' OR "children_people"."name" = 'Ernie'\) AND \("people"."name" = 'Bert' OR "children_people"."name" = 'Bert'\)\)/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sorts=' do
|
||||||
|
before do
|
||||||
|
@s = Search.new(Person)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates sorts based on a single attribute/direction' do
|
||||||
|
@s.sorts = 'id desc'
|
||||||
|
@s.sorts.should have(1).item
|
||||||
|
sort = @s.sorts.first
|
||||||
|
sort.should be_a Nodes::Sort
|
||||||
|
sort.name.should eq 'id'
|
||||||
|
sort.dir.should eq 'desc'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates sorts based on multiple attributes/directions in array format' do
|
||||||
|
@s.sorts = ['id desc', 'name asc']
|
||||||
|
@s.sorts.should have(2).items
|
||||||
|
sort1, sort2 = @s.sorts
|
||||||
|
sort1.should be_a Nodes::Sort
|
||||||
|
sort1.name.should eq 'id'
|
||||||
|
sort1.dir.should eq 'desc'
|
||||||
|
sort2.should be_a Nodes::Sort
|
||||||
|
sort2.name.should eq 'name'
|
||||||
|
sort2.dir.should eq 'asc'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates sorts based on multiple attributes/directions in hash format' do
|
||||||
|
@s.sorts = {
|
||||||
|
'0' => {
|
||||||
|
:name => 'id',
|
||||||
|
:dir => 'desc'
|
||||||
|
},
|
||||||
|
'1' => {
|
||||||
|
:name => 'name',
|
||||||
|
:dir => 'asc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@s.sorts.should have(2).items
|
||||||
|
sort1, sort2 = @s.sorts
|
||||||
|
sort1.should be_a Nodes::Sort
|
||||||
|
sort1.name.should eq 'id'
|
||||||
|
sort1.dir.should eq 'desc'
|
||||||
|
sort2.should be_a Nodes::Sort
|
||||||
|
sort2.name.should eq 'name'
|
||||||
|
sort2.dir.should eq 'asc'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#method_missing' do
|
||||||
|
before do
|
||||||
|
@s = Search.new(Person)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises NoMethodError when sent an invalid attribute' do
|
||||||
|
expect {@s.blah}.to raise_error NoMethodError
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets condition attributes when sent valid attributes' do
|
||||||
|
@s.name_eq = 'Ernie'
|
||||||
|
@s.name_eq.should eq 'Ernie'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows chaining to access nested conditions' do
|
||||||
|
@s.ors = [{:name_eq => 'Ernie', :children_name_eq => 'Ernie'}]
|
||||||
|
@s.ors.first.name_eq.should eq 'Ernie'
|
||||||
|
@s.ors.first.children_name_eq.should eq 'Ernie'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
require 'machinist/active_record'
|
||||||
|
require 'sham'
|
||||||
|
require 'faker'
|
||||||
|
|
||||||
|
Time.zone = 'Eastern Time (US & Canada)'
|
||||||
|
|
||||||
|
Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
|
||||||
|
require f
|
||||||
|
end
|
||||||
|
|
||||||
|
Sham.define do
|
||||||
|
name { Faker::Name.name }
|
||||||
|
title { Faker::Lorem.sentence }
|
||||||
|
body { Faker::Lorem.paragraph }
|
||||||
|
salary {|index| 30000 + (index * 1000)}
|
||||||
|
tag_name { Faker::Lorem.words(3).join(' ') }
|
||||||
|
note { Faker::Lorem.words(7).join(' ') }
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.before(:suite) { Schema.create }
|
||||||
|
config.before(:all) { Sham.reset(:before_all) }
|
||||||
|
config.before(:each) { Sham.reset(:before_each) }
|
||||||
|
|
||||||
|
config.include RansackHelper
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'ransack'
|
|
@ -0,0 +1,102 @@
|
||||||
|
require 'active_record'
|
||||||
|
|
||||||
|
ActiveRecord::Base.establish_connection(
|
||||||
|
:adapter => 'sqlite3',
|
||||||
|
:database => ':memory:'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Person < ActiveRecord::Base
|
||||||
|
belongs_to :parent, :class_name => 'Person', :foreign_key => :parent_id
|
||||||
|
has_many :children, :class_name => 'Person', :foreign_key => :parent_id
|
||||||
|
has_many :articles
|
||||||
|
has_many :comments
|
||||||
|
has_many :authored_article_comments, :through => :articles,
|
||||||
|
:class_name => 'Comment', :foreign_key => :person_id
|
||||||
|
has_many :notes, :as => :notable
|
||||||
|
end
|
||||||
|
|
||||||
|
class Article < ActiveRecord::Base
|
||||||
|
belongs_to :person
|
||||||
|
has_many :comments
|
||||||
|
has_and_belongs_to_many :tags
|
||||||
|
has_many :notes, :as => :notable
|
||||||
|
end
|
||||||
|
|
||||||
|
class Comment < ActiveRecord::Base
|
||||||
|
belongs_to :article
|
||||||
|
belongs_to :person
|
||||||
|
end
|
||||||
|
|
||||||
|
class Tag < ActiveRecord::Base
|
||||||
|
has_and_belongs_to_many :articles
|
||||||
|
end
|
||||||
|
|
||||||
|
class Note < ActiveRecord::Base
|
||||||
|
belongs_to :notable, :polymorphic => true
|
||||||
|
end
|
||||||
|
|
||||||
|
module Schema
|
||||||
|
def self.create
|
||||||
|
ActiveRecord::Base.silence do
|
||||||
|
ActiveRecord::Migration.verbose = false
|
||||||
|
|
||||||
|
ActiveRecord::Schema.define do
|
||||||
|
create_table :people, :force => true do |t|
|
||||||
|
t.integer :parent_id
|
||||||
|
t.string :name
|
||||||
|
t.integer :salary
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :articles, :force => true do |t|
|
||||||
|
t.integer :person_id
|
||||||
|
t.string :title
|
||||||
|
t.text :body
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :comments, :force => true do |t|
|
||||||
|
t.integer :article_id
|
||||||
|
t.integer :person_id
|
||||||
|
t.text :body
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :tags, :force => true do |t|
|
||||||
|
t.string :name
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :articles_tags, :force => true, :id => false do |t|
|
||||||
|
t.integer :article_id
|
||||||
|
t.integer :tag_id
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :notes, :force => true do |t|
|
||||||
|
t.integer :notable_id
|
||||||
|
t.string :notable_type
|
||||||
|
t.string :note
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
10.times do
|
||||||
|
person = Person.make
|
||||||
|
Note.make(:notable => person)
|
||||||
|
3.times do
|
||||||
|
article = Article.make(:person => person)
|
||||||
|
3.times do
|
||||||
|
article.tags = [Tag.make, Tag.make, Tag.make]
|
||||||
|
end
|
||||||
|
Note.make(:notable => article)
|
||||||
|
10.times do
|
||||||
|
Comment.make(:article => article)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
2.times do
|
||||||
|
Comment.make(:person => person)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Comment.make(:body => 'First post!', :article => Article.make(:title => 'Hello, world!'))
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue