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

About this user

Peter Cooperx http://www.petercooper.co.uk/

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

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')

Turn a date range into SQL conditions

From here on Project.ioni.st.

(6.months.ago.to_date..1.year.ago.to_date).to_s(:db)
=> "BETWEEN '2005-07-27' AND '2005-01-22'"

Rebuild indexes on a table with MySQL

REPAIR TABLE tbl_name QUICK;

How Snippets gets general post counts for tags

This is in models/tag.rb

def find_all_with_count
    self.find_by_sql("SELECT t.*, COUNT(pt.post_id) AS count FROM tags t, posts_tags pt WHERE pt.tag_id = t.id GROUP BY t.id ORDER BY count DESC;")
end

This is a basic non-specific overview of all tags, as you would see on Snippets' front page. So:

@tag_info = Tag.find_all_with_count
@tag_info.each { |t| .. you now have t.name, t.count, etc .. }

Find items with similar (or as many as possible) relationships - for a 'related posts' box etc

If we have the id for a post in postid and a limit of num and we want to find posts which share as many tags as possible with postid's post, the following SQL will get you there.

SELECT p.*, COUNT(pt2.post_id) AS count FROM posts p, posts_tags pt, tags t, posts_tags pt2 WHERE pt.post_id=#{postid} AND t.id = pt.tag_id AND pt2.post_id != pt.post_id AND pt2.tag_id=pt.tag_id AND p.id = pt2.post_id GROUP BY pt2.post_id ORDER BY count DESC LIMIT #{num};")

Find all many-to-many relationships which are tied to an arbitrary number of other many-to-many relationships

Yet more code from Snippets itself. When you narrow down posts by tags, I only want you to see tags which are shared by other posts which also have the same current tags. This is more difficult than it sounds, and requires use of a subselect:

SELECT *, COUNT(pt.post_id) AS count FROM posts_tags pt, tags t WHERE pt.post_id IN (SELECT pt.post_id FROM posts_tags pt, tags t WHERE pt.tag_id = t.id AND (t.name IN ('rails', 'ruby')) GROUP BY pt.post_id HAVING COUNT(pt.post_id)=2) AND t.id = pt.tag_id GROUP BY pt.tag_id ORDER BY count DESC;

The first query (inside the subselect) finds all post IDs for posts which definitely contain any of the current tags. The outer query finds all OTHER tags associated with these posts, therefore we find all (and the quantity of) the tags related to the input tags.

Return items with data intersections over a many-to-many join

If you have three tables, 'posts', 'tags', and a join table 'posts_tags', and you wish to find all posts which are all associated with a number of different tags, you can use this SQL trick:

SELECT p.* FROM posts_tags pt, posts p, tags t WHERE pt.tag_id = t.id AND (t.name IN ('tag1', 'tag2', 'tag3')) AND p.id=pt.post_id GROUP BY p.id HAVING COUNT(p.id) = 3;

.. where 3 is the number of tags in total. In Ruby/Rails, if you have an array called 'tags' containing the tags, you could use this code:

@posts = Post.find_by_sql ("SELECT p.* FROM posts_tags pt, posts p, tags t WHERE pt.tag_id = t.id AND (t.name = '" + tags.uniq.join ('\' OR t.name=\'') + "') AND p.id=pt.post_id GROUP BY p.id HAVING COUNT(p.id) = " + tags.uniq.length.to_s)


This is how Snippets itself works. You can also add a p.user_id check to the HAVING operator to only find posts with certain tags by a certain user.

paginate on custom sql queries in rails

Someone in #rubyonrails wrote this. Forget who!

def paginate_from_sql(model, sql, total, per_page)
  @object_pages = Paginator.new self, total, per_page, @params['page']
  @objects = model.find_by_sql(sql + " LIMIT #{per_page} " +
                               "OFFSET #{@object_pages.current.to_sql[1]}")
end
« Newer Snippets
Older Snippets »
Showing 1-8 of 8 total  RSS