Permalink
Browse files

initial commit (extraction from xtt)

  • Loading branch information...
0 parents commit 60aa6fcf2d1807875dac7cd08c012cc663efa59a @technoweenie committed Apr 5, 2008
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Rick Olson
+
+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,43 @@
+CanSearch
+=========
+
+Allows you create common named_scopes and chain them together with #search and
+#search_for.
+
+ class Topic
+ belongs_to :forum
+
+ can_search do
+ scoped_by :forums
+ scoped_by :created, :scope => :date_range
+ end
+
+ # creates these two named scopes
+ # named_scope :by_forums, lambda { |f| {:conditions => {:forum_id => f}} }
+ # named_scope :created, lambda { |range| {:conditions => ...} }
+ end
+
+ Topic.search(:forum => 1) # Topic.by_forums(1)
+ Topic.search(:forums => [1,2]) # Topic.by_forums([1,2])
+ Topic.search(:created => (time1..time2)) # Topic.created(time1..time2)
+ Topic.search(:created => \
+ {:period => :daily, :start => Time.now}) # Topic.created(Time.now, Time.now + 1.day)
+
+You can automatically paginate:
+
+ Topic.search :forum => 1, :page => params[:page]
+
+You can also access the named_scope directly for custom #find or #calculate calls.
+
+ Topic.search_for(:forum => 1).sum(:hits)
+
+Oh, and you can combine scopes:
+
+ Topic.search :forum => 1, :forums => [2, 3], :created => (time1..time2)
+
+ def can_search(&block)
+ self.search_scopes = CanSearch::SearchScopes.new(self, &block)
+ end
+end
+
+Copyright (c) 2008-* Rick Olson, released under the MIT license
@@ -0,0 +1,53 @@
+require 'rake'
+require "rake/rdoctask"
+require 'rake/gempackagetask'
+require File.join(File.dirname(__FILE__), 'spec', 'spec_helper')
+require 'spec/rake/spectask'
+require 'spec/rake/verify_rcov'
+
+rdoc_files = FileList["{bin,lib,example_configs}/**/*"].to_a
+extra_rdoc_files = %w(README COPYRIGHT RELEASES CHANGELOG)
+
+Rake::RDocTask.new do |rd|
+ rd.main = "README"
+ rd.rdoc_files.include(rdoc_files, extra_rdoc_files)
+ rd.rdoc_dir = "doc/rdoc/"
+end
+
+desc "Run all examples with RCov"
+Spec::Rake::SpecTask.new(:rcov) do |t|
+ t.spec_files = FileList['spec/**/*.rb']
+ t.rcov = true
+ t.rcov_opts = ['--exclude', 'spec']
+ t.rcov_dir = "doc/rcov"
+end
+
+desc "Run all specs"
+Spec::Rake::SpecTask.new(:spec) do |t|
+ t.spec_files = FileList['spec/**/*.rb']
+ t.rcov = false
+end
+
+desc "Generate RSpec Report"
+task :rspec_report => [:clobber_rspec_report] do
+ files = FileList["spec/**/*.rb"].to_s
+ %x(spec #{files} --format html:doc/rspec_report.html)
+end
+
+task :clobber_rspec_report do
+ %x(rm -rf doc/rspec_report.html)
+end
+
+desc "Generate all documentation"
+task :generate_documentation => [:clobber_documentation, :rdoc, :rcov, :rspec_report]
+
+desc "Remove all documentation"
+task :clobber_documentation => [:clobber_rdoc, :clobber_rcov, :clobber_rspec_report]
+
+desc "Build Release"
+task :build_release => [:pre_commit, :generate_documentation, :repackage] do
+ %x(mv pkg gem)
+end
+
+desc "Run this before commiting"
+task :pre_commit => [:verify_rcov]
@@ -0,0 +1,39 @@
+class << ActiveRecord::Base
+ # Allows you create common named_scopes and chain them together with #search and
+ # #search_for.
+ #
+ # class Topic
+ # belongs_to :forum
+ #
+ # can_search do
+ # scoped_by :forums
+ # scoped_by :created, :scope => :date_range
+ # end
+ #
+ # # creates these two named scopes
+ # # named_scope :by_forums, lambda { |f| {:conditions => {:forum_id => f}} }
+ # # named_scope :created, lambda { |range| {:conditions => ...} }
+ # end
+ #
+ # Topic.search(:forum => 1) # Topic.by_forums(1)
+ # Topic.search(:forums => [1,2]) # Topic.by_forums([1,2])
+ # Topic.search(:created => (time1..time2)) # Topic.created(time1..time2)
+ # Topic.search(:created => \
+ # {:period => :daily, :start => Time.now}) # Topic.created(Time.now, Time.now + 1.day)
+ #
+ # You can automatically paginate:
+ #
+ # Topic.search :forum => 1, :page => params[:page]
+ #
+ # You can also access the named_scope directly for custom #find or #calculate calls.
+ #
+ # Topic.search_for(:forum => 1).sum(:hits)
+ #
+ # Oh, and you can combine scopes:
+ #
+ # Topic.search :forum => 1, :forums => [2, 3], :created => (time1..time2)
+ #
+ def can_search(&block)
+ self.search_scopes = CanSearch::SearchScopes.new(self, &block)
+ end
+end
@@ -0,0 +1,19 @@
+module CanSearch
+ def self.extended(base)
+ class << base
+ attr_accessor :search_scopes
+ end
+ end
+
+ # Calls either #paginate or #all on the returned scoped from #search_for.
+ def search(options = {})
+ options = options.dup
+ search_for(options).send(options.key?(:page) ? :paginate : :all, options)
+ end
+
+ # Strips search scope keys from options and builds a scoped finder object. This
+ # returns the model if no search scopes are in use.
+ def search_for(options = {})
+ search_scopes.search_for(options)
+ end
+end
@@ -0,0 +1,93 @@
+module CanSearch
+ # Generates a named scope for searching by time ranges. You can either specify
+ # your own time range, or specify a single time and use one of the periods to determine
+ # the range.
+ #
+ # class Topic
+ # can_search do
+ # scoped_by :created, :scope => :date_range
+ # end
+ # end
+ #
+ # Topic.search(:created => (time1..time2)) # Topic.created(time1..time2)
+ # Topic.search(:created => \
+ # {:period => :daily, :start => Time.now}) # Topic.created(Time.now, Time.now + 1.day)
+ #
+ class DateRangeScope < BaseScope
+ # Default collection of all date range periods. A period is simply a proc
+ # that returns a time range calculated from the given time.
+ def self.periods() @periods ||= {} end
+ periods.update \
+ :daily => lambda { |now|
+ today = now.midnight
+ (today..today + 1.day - 1.second)
+ },
+ :weekly => lambda { |now|
+ mon = now.beginning_of_week
+ (mon..mon + 1.week - 1.second)
+ },
+ :'bi-weekly' => lambda { |now|
+ today = now.midnight
+ today.day >= 15 ? (today.change(:day => 15)..today.end_of_month) : (today.beginning_of_month..today.change(:day => 15) - 1.second)
+ },
+ :monthly => lambda { |now|
+ (now.beginning_of_month..now.end_of_month)
+ }
+
+ # The attribute adds a '_at' suffix to the scope name (:created => :created_at).
+ # The named_scope uses the scope name by default.
+ def initialize(model, name, options = {})
+ super
+ @attribute = options[:attribute] || begin
+ name_str = name.to_s
+ name_str =~ /_at$/ ? name : (name_str << "_at").to_sym
+ end
+ @named_scope = options[:named_scope] || @name
+ @model.named_scope @named_scope, lambda { |range|
+ if range.respond_to?(:[])
+ range = range[:period] && @model.date_range_for(range[:period], range[:start])
+ end
+ if range
+ {:conditions => "#{@model.table_name}.#{@attribute} #{range.to_s :db}"}
+ else
+ {}
+ end
+ }
+ end
+
+ def scope_for(finder, options = {})
+ if value = options.delete(@name)
+ finder.send(@named_scope, value)
+ else
+ finder
+ end
+ end
+ end
+
+ # Shortcut to CanSearch::DateRangeScope.periods
+ def date_periods() @date_periods ||= CanSearch::DateRangeScope.periods end
+
+ # Returns a range for the given time using the date period.
+ def date_range_for(period_name, time = nil)
+ if period = date_periods[period_name.to_sym]
+ period.call(parse_filtered_time(time))
+ else
+ raise "Invalid period: #{period_name.inspect}"
+ end
+ end
+
+protected
+ # Parses the given time. Strings are parsed with the current time zone, times are
+ # converted to the current time zone, and a nil value assumes you want Time.zone.now.
+ def parse_filtered_time(time = nil)
+ case time
+ when String then Time.zone.parse(time)
+ when nil then Time.zone.now
+ when Time, ActiveSupport::TimeWithZone then time.in_time_zone
+ else raise "Invalid time: #{time.inspect}"
+ end
+ end
+
+ # Add this scope type
+ SearchScopes.scope_types[:date_range] = DateRangeScope
+end
@@ -0,0 +1,109 @@
+module CanSearch
+ # Tracks the search scopes for a given model.
+ class SearchScopes
+ # Registered scope_types using their symbolized names as keys.
+ def self.scope_types() @scope_types ||= {} end
+
+ attr_reader :model, :scopes
+
+ def initialize(model, &block)
+ @scopes = {}
+ @model = model
+ @model.extend CanSearch
+ instance_eval(&block) if block
+ end
+
+ # Adds a new scope for the given model. It works by looking up the scope class
+ # in the scope_types hash and instantiating it with the given arguments.
+ def scoped_by(name, options = {})
+ options[:scope] ||= :reference
+ if scope_class = self.class.scope_types[options[:scope]]
+ @scopes[name] = scope_class.new(@model, name, options)
+ end
+ end
+
+ # Builds a combined scoped finder object, starting with the model itself.
+ def search_for(options = {})
+ @scopes.values.inject(@model) { |finder, scope| scope.scope_for(finder, options) }
+ end
+
+ def [](name)
+ @scopes[name]
+ end
+ end
+
+ # The base class for all scope classes. Scope classes know how to take the
+ # given arguments, generate a proper named_scope for the model, and perform
+ # searches on it.
+ class BaseScope
+ # This is the key the scope looks for to create the finder.
+ attr_reader :name
+
+ # This is the main attribute that is being used in the search.
+ attr_reader :attribute
+
+ # The name of the named_scope that is used.
+ attr_reader :named_scope
+
+ # a reference to the ActiveRecord model that this scope is attached to.
+ attr_reader :model
+
+ def initialize(model, name, options = {})
+ @model, @name = model, name
+ end
+
+ # strip out any scoped keys from options and return a chained finder.
+ def scope_for(finder, options = {})
+ finder
+ end
+
+ def ==(other)
+ self.class == other.class && other.name == @name && other.attribute == @attribute && other.named_scope == @named_scope
+ end
+ end
+
+ # Generates named_scope for belongs_to associations. ReferenceScopes actually look for both a singular
+ # and plural key. Singular keys should be the id value or the model instance.
+ #
+ # class Topic
+ # belongs_to :forum
+ #
+ # can_search do
+ # scoped_by :forums
+ # end
+ # end
+ #
+ # Topic.search(:forum => 1) # Topic.by_forums(1)
+ # Topic.search(:forums => [1,2]) # Topic.by_forums([1,2])
+ #
+ class ReferenceScope < BaseScope
+ attr_reader :singular_name
+
+ # By default, the singular_name is generated with the #singularize (:forums => :forum) inflection.
+ # The attribute is taken from the #foreign_key (:forum => :forum_id) inflection of the singular name.
+ # The named_scope adds a "by_" prefix to the scope name (:forums => :by_forums).
+ def initialize(model, name, options = {})
+ super
+ single = name.to_s.singularize
+ @singular_name = options[:singular] || single.to_sym
+ @attribute = options[:attribute] || single.foreign_key.to_sym
+ @named_scope = options[:named_scope] || "by_#{name}".to_sym
+ @model.named_scope @named_scope, lambda { |records| {:conditions => {@attribute => records}} }
+ end
+
+ def scope_for(finder, options = {})
+ value, values = options.delete(@singular_name), options.delete(@name) || []
+ values << value if value
+ return finder if values.empty?
+ finder.send(@named_scope, values.size == 1 ? values.first : values)
+ end
+
+ def ==(other)
+ super && other.singular_name == @singular_name
+ end
+ end
+
+ SearchScopes.scope_types[:reference] = ReferenceScope
+end
+
+send respond_to?(:require_dependency) ? :require_dependency : :require, 'can_search/date_range_scope'
Oops, something went wrong.

0 comments on commit 60aa6fc

Please sign in to comment.