Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

« Newer Snippets
Older Snippets »
Showing 1-4 of 4 total  RSS 

Rails Array#add_condition Method

Lets you add a condition to a set of ActiveRecord conditions easily like this:

  conditions = ['active = ? and type = ?', true, 2]
  conditions.add_condition ['person_id = ?', 345]


class Array
  def add_condition(condition, conjunction='and')
    if condition.is_a? Array
      if self.empty?
        (self << condition).flatten!
      else
        self[0] += " #{conjunction} " + condition.shift
        (self << condition).flatten!
      end
    elsif condition.is_a? String
      self[0] += " #{conjunction} " + condition
    else
      raise "don't know how to handle this condition type"
    end
    self
  end
end

Extending acts_as_taggable to take scope into account

The acts_as_taggable plugin is great and so useful. But it discards scope, and it doesn't work like any other model attribute in your controller views [using normal @model.update(params[:model]) calls].

I have changed it to:
* add an alias tag_list= for tag_with (found in the "rails wiki":http://wiki.rubyonrails.com/rails/pages/ActsAsTaggablePluginHowto),
* updated find_tagged_with and count_tagged_with to use scope (after reading an article from Jamis on "ActiveRecord::Base.find":http://weblog.jamisbuck.org/2006/11/20/under-the-hood-activerecord-base-find-part-2)

Scope is used when doing this for instance:

  Message.with_scope :find => {:conditions => ["project_id = ?", @project]} do
    @messages = Message.find_tagged_with(tag.name)
  end


Then only the messages tagged with tag.name that belong to project @project.


*acts_as_taggable.rb*

module ActiveRecord
  module Acts #:nodoc:
    module Taggable #:nodoc:
      def self.included(base)
        base.extend(ClassMethods)  
      end
      
      module ClassMethods
        def acts_as_taggable(options = {})
          write_inheritable_attribute(:acts_as_taggable_options, {
            :taggable_type => ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s,
            :from => options[:from]
          })
          
          class_inheritable_reader :acts_as_taggable_options

          has_many :taggings, :as => :taggable, :dependent => true
          has_many :tags, :through => :taggings

          include ActiveRecord::Acts::Taggable::InstanceMethods
          extend ActiveRecord::Acts::Taggable::SingletonMethods          
        end
      end
      
      module SingletonMethods
        def find_tagged_with(list)
          tagged_with list
        end

        def count_tagged_with(list)
          tagged_with list, :count
        end

        # DRY sql query and handle scope
        protected
        def tagged_with(list, type = :find)
          if type == :count
            sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key}) AS cnt "
          else
            sql = "SELECT DISTINCT #{table_name}.* "
          end
          sql << "FROM #{table_name}, tags, taggings "

          conditions = [
            "#{table_name}.#{primary_key} = taggings.taggable_id " +
            "AND taggings.taggable_type = ? " +
            "AND taggings.tag_id = tags.id AND tags.name IN (?) ",
            acts_as_taggable_options[:taggable_type], list
            ]
          add_conditions!(sql, conditions)

          result = find_by_sql(sql)
          (type == :count) ? result.first.cnt.to_i : result
        end

      end
      
      module InstanceMethods
        def tag_with(list)
          Tag.transaction do
            taggings.destroy_all

            Tag.parse(list).each do |name|
              if acts_as_taggable_options[:from]
                send(acts_as_taggable_options[:from]).tags.find_or_create_by_name(name).on(self)
              else
                Tag.find_or_create_by_name(name).on(self)
              end
            end
          end
        end

        # allow using active record update_attributes() and others by using tag_list to set
        # and read the tag list
        alias tag_list= tag_with

        def tag_list
          tags.collect { |tag| tag.name.include?(" ") ? "'#{tag.name}'" : tag.name }.join(" ")
        end
      end
    end
  end
end

Extending acts_as_taggable for real-world requirements

acts_as_taggable is cool, but it doesn't easily support limits, offsets, tag intersections, arbitrary conditions, etc.. so I created some unit tests and started to extend it. This is where I'm at so far:

module ActiveRecord
  module Acts #:nodoc:
    module Taggable #:nodoc:
      def self.included(base)
        base.extend(ClassMethods)  
      end
      
      module ClassMethods
        def acts_as_taggable(options = {})
          write_inheritable_attribute(:acts_as_taggable_options, {
            :taggable_type => ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s,
            :from => options[:from]
          })
          
          class_inheritable_reader :acts_as_taggable_options

          has_many :taggings, :as => :taggable, :dependent => true
          has_many :tags, :through => :taggings

          include ActiveRecord::Acts::Taggable::InstanceMethods
          extend ActiveRecord::Acts::Taggable::SingletonMethods          
        end
      end
      
      module SingletonMethods
        def find_tagged_with(list, options = {})
          local_options = { :limit => 1000, :offset => 0 }.merge(options)
          find_by_sql([
            "SELECT DISTINCT #{table_name}.* FROM #{table_name}, tags, taggings " +
            "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +
            "AND taggings.taggable_type = ? " +
            "AND taggings.tag_id = tags.id AND tags.name IN (?) #{"AND (#{local_options[:conditions]})" if local_options[:conditions]} LIMIT ? OFFSET ?",
            acts_as_taggable_options[:taggable_type], list, local_options[:limit], local_options[:offset]
          ])
        end
        
        def count_tagged_with(list, options = {})
          local_options = {}.merge(options)
          find_by_sql([
            "SELECT COUNT(DISTINCT #{table_name}.#{primary_key}) AS cnt FROM #{table_name}, tags, taggings " +
            "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +
            "AND taggings.taggable_type = ? " +
            "AND taggings.tag_id = tags.id AND tags.name IN (?) #{"AND (#{local_options[:conditions]}) " if local_options[:conditions]}",
            acts_as_taggable_options[:taggable_type], list
          ]).first.cnt.to_i
        end

        def find_tagged_with_intersecting(list, options = {})
          local_options = { :limit => 1000, :offset => 0 }.merge(options)
          find_by_sql([
            "SELECT DISTINCT #{table_name}.* FROM #{table_name}, tags, taggings " +
            "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +
            "AND taggings.taggable_type = ? " +
            "AND taggings.tag_id = tags.id AND tags.name IN (?) #{"AND (#{local_options[:conditions]}) " if local_options[:conditions]} GROUP BY #{table_name}.id HAVING COUNT(#{table_name}.id) = #{list.size} LIMIT ? OFFSET ?",
            acts_as_taggable_options[:taggable_type], list, local_options[:limit], local_options[:offset]
          ])
        end

        def count_tagged_with_intersecting(list, options = {})
          local_options = {}.merge(options)
          find_by_sql([
            "SELECT COUNT(*) AS cnt FROM (SELECT #{table_name}.#{primary_key} AS cnt FROM #{table_name}, tags, taggings " +
            "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +
            "AND taggings.taggable_type = ? " +
            "AND taggings.tag_id = tags.id AND tags.name IN (?) " +
            "#{"AND (#{local_options[:conditions]})" if local_options[:conditions]} " +
            "GROUP BY taggings.taggable_id HAVING COUNT(taggings.taggable_id) = #{list.size}) AS x",
            acts_as_taggable_options[:taggable_type], list
          ]).first.cnt.to_i
        end               
      end
      
      module InstanceMethods
        def tag_with(list)
          Tag.transaction do
            taggings.destroy_all

            Tag.parse(list).each do |name|
              if acts_as_taggable_options[:from]
                send(acts_as_taggable_options[:from]).tags.find_or_create_by_name(name).on(self)
              else
                Tag.find_or_create_by_name(name).on(self)
              end
            end
          end
        end

        def tag_list
          tags.collect { |tag| tag.name.include?(" ") ? "'#{tag.name}'" : tag.name }.join(" ")
        end
      end
    end
  end
end


These are the sorts of tests I'm then doing:

assert_equal 2, Post.find_tagged_with_intersecting(%w{TagOne TagTwo}).size
assert_equal 2, Post.count_tagged_with_intersecting(%w{TagOne TagTwo})
assert_equal 1, Post.find_tagged_with_intersecting(%w{TagOne TagTwo}, :conditions => 'status = 1').size
assert_equal 1, Post.count_tagged_with_intersecting(%w{TagOne TagTwo}, :conditions => 'status = 1')
p = Post.find_tagged_with("TagOne", :conditions => 'status = 1')

conditioner for ActiveRecord-friendly conditions from a collection

I frequently have a collection of values that I want to match in an ActiveRecord query, but it would be nice if I could let ActiveRecord handle checking the data and escaping it properly. So, I wrote this method to return ActiveRecord-friendly conditions, such as:
["user_id=? AND job_id=?", 3, 4]
based on the 'raw' conditions you feed to it, such as:
[['user_id', 3], ['job_id', 4]]


# Returns ActiveRecord-friendly conditions based on the given
# raw conditions; handles grouping based on like field names;
# allows different boolean operators in raw conditions;
# allows different comparison operators in raw conditions;
# raw conditions setup:
# [[field name, desired value, bool. op., comp. op.], ...]
# name = condition[0]
# value = condition[1]
# bool_type = condition[2]
# comparison = condition[3]
# raw conditions example:
# [['type_id', '4', 'OR'], ['created_on', Date.new, 'AND', '<=']]
def conditioner( raw_conditions )
  return nil if raw_conditions.nil? || raw_conditions.empty?
  
  conditions = ['(']
  count = 0
  prev_name = raw_conditions[0][0]
  raw_conditions.each do |condition|
    name = condition[0]
      
    conditions[0] << ') AND ' if prev_name != name
    conditions[0] << ' ' << ( condition[2] || 'OR' ) << ' ' unless count == 0 || prev_name != name
    conditions[0] << '(' if prev_name != name
    conditions[0] << name + ' ' << ( condition[3] || '=' ) + ' ?'
      
    conditions << condition[1]
      
    prev_name = name
    count += 1
  end
  conditions[0] << ')'

  conditions
end


This way, you can do something like the following:
model_ids = Model.find( :all ).map( &:id )
raw_conditions = model_ids.collect { |id| ['model_id', id] }
conditions = conditioner( raw_conditions )
desired_collection = OtherModel.find( :all, :conditions => conditions )


If your query needs to depend on more than one factor, you might do something like the following:
if test
  raw_conditions = [['user_id', 3]]
else
  raw_conditions = [['user_id', 4], ['groups.name', 'dev']]
end

team_ids.each { |id| raw_conditions << ['team_id', id, 'AND'] }
« Newer Snippets
Older Snippets »
Showing 1-4 of 4 total  RSS