litmus-acts-as-taggable-on 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +25 -0
- data/Gemfile +6 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +227 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
- data/lib/acts-as-taggable-on.rb +32 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +53 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +130 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +237 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +65 -0
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +67 -0
- data/lib/acts_as_taggable_on/compatibility/Gemfile +6 -0
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
- data/lib/acts_as_taggable_on/tag.rb +67 -0
- data/lib/acts_as_taggable_on/tag_list.rb +95 -0
- data/lib/acts_as_taggable_on/tagging.rb +23 -0
- data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
- data/rails/init.rb +1 -0
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +266 -0
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +70 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +115 -0
- data/spec/acts_as_taggable_on/taggable_spec.rb +306 -0
- data/spec/acts_as_taggable_on/tagger_spec.rb +91 -0
- data/spec/acts_as_taggable_on/tagging_spec.rb +31 -0
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +28 -0
- data/spec/bm.rb +52 -0
- data/spec/models.rb +31 -0
- data/spec/schema.rb +43 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +53 -0
- metadata +115 -0
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            module ActsAsTaggableOn::Taggable
         | 
| 2 | 
            +
              module Related
         | 
| 3 | 
            +
                def self.included(base)
         | 
| 4 | 
            +
                  base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
         | 
| 5 | 
            +
                  base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
         | 
| 6 | 
            +
                  base.initialize_acts_as_taggable_on_related
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                module ClassMethods
         | 
| 10 | 
            +
                  def initialize_acts_as_taggable_on_related
         | 
| 11 | 
            +
                    tag_types.map(&:to_s).each do |tag_type|
         | 
| 12 | 
            +
                      class_eval %(
         | 
| 13 | 
            +
                        def find_related_#{tag_type}(options = {})
         | 
| 14 | 
            +
                          related_tags_for('#{tag_type}', self.class, options)
         | 
| 15 | 
            +
                        end
         | 
| 16 | 
            +
                        alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                        def find_related_#{tag_type}_for(klass, options = {})
         | 
| 19 | 
            +
                          related_tags_for('#{tag_type}', klass, options)
         | 
| 20 | 
            +
                        end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                        def find_matching_contexts(search_context, result_context, options = {})
         | 
| 23 | 
            +
                          matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
         | 
| 24 | 
            +
                        end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                        def find_matching_contexts_for(klass, search_context, result_context, options = {})
         | 
| 27 | 
            +
                          matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
         | 
| 28 | 
            +
                        end
         | 
| 29 | 
            +
                      )
         | 
| 30 | 
            +
                    end        
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                  
         | 
| 33 | 
            +
                  def acts_as_taggable_on(*args)
         | 
| 34 | 
            +
                    super(*args)
         | 
| 35 | 
            +
                    initialize_acts_as_taggable_on_related
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
                
         | 
| 39 | 
            +
                module InstanceMethods
         | 
| 40 | 
            +
                  def matching_contexts_for(search_context, result_context, klass, options = {})
         | 
| 41 | 
            +
                    tags_to_find = tags_on(search_context).collect { |t| t.name }
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
         | 
| 44 | 
            +
                    
         | 
| 45 | 
            +
                    klass.scoped({ :select     => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
         | 
| 46 | 
            +
                                   :from       => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
         | 
| 47 | 
            +
                                   :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?) AND #{Tagging.table_name}.context = ?", tags_to_find, result_context],
         | 
| 48 | 
            +
                                   :group      => grouped_column_names_for(klass),
         | 
| 49 | 
            +
                                   :order      => "count DESC" }.update(options))
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  
         | 
| 52 | 
            +
                  def related_tags_for(context, klass, options = {})
         | 
| 53 | 
            +
                    tags_to_find = tags_on(context).collect { |t| t.name }
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    klass.scoped({ :select     => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
         | 
| 58 | 
            +
                                   :from       => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
         | 
| 59 | 
            +
                                   :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
         | 
| 60 | 
            +
                                   :group      => grouped_column_names_for(klass),
         | 
| 61 | 
            +
                                   :order      => "count DESC" }.update(options))
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            module ActsAsTaggableOn
         | 
| 2 | 
            +
              module Tagger
         | 
| 3 | 
            +
                def self.included(base)
         | 
| 4 | 
            +
                  base.extend ClassMethods
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                module ClassMethods
         | 
| 8 | 
            +
                  ##
         | 
| 9 | 
            +
                  # Make a model a tagger. This allows an instance of a model to claim ownership
         | 
| 10 | 
            +
                  # of tags.
         | 
| 11 | 
            +
                  #
         | 
| 12 | 
            +
                  # Example:
         | 
| 13 | 
            +
                  #   class User < ActiveRecord::Base
         | 
| 14 | 
            +
                  #     acts_as_tagger
         | 
| 15 | 
            +
                  #   end
         | 
| 16 | 
            +
                  def acts_as_tagger(opts={})
         | 
| 17 | 
            +
                    class_eval do
         | 
| 18 | 
            +
                      has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
         | 
| 19 | 
            +
                                                           :include => :tag, :class_name => "Tagging")
         | 
| 20 | 
            +
                      has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    include ActsAsTaggableOn::Tagger::InstanceMethods
         | 
| 24 | 
            +
                    extend ActsAsTaggableOn::Tagger::SingletonMethods
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def is_tagger?
         | 
| 28 | 
            +
                    false
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                module InstanceMethods
         | 
| 33 | 
            +
                  ##
         | 
| 34 | 
            +
                  # Tag a taggable model with tags that are owned by the tagger. 
         | 
| 35 | 
            +
                  #
         | 
| 36 | 
            +
                  # @param taggable The object that will be tagged
         | 
| 37 | 
            +
                  # @param [Hash] options An hash with options. Available options are:
         | 
| 38 | 
            +
                  #               * <tt>:with</tt> - The tags that you want to
         | 
| 39 | 
            +
                  #               * <tt>:on</tt>   - The context on which you want to tag
         | 
| 40 | 
            +
                  #
         | 
| 41 | 
            +
                  # Example:
         | 
| 42 | 
            +
                  #   @user.tag(@photo, :with => "paris, normandy", :on => :locations)
         | 
| 43 | 
            +
                  def tag(taggable, opts={})
         | 
| 44 | 
            +
                    opts.reverse_merge!(:force => true)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    raise "You need to specify a tag context using :on"                unless opts.has_key?(:on)
         | 
| 49 | 
            +
                    raise "You need to specify some tags using :with"                  unless opts.has_key?(:with)
         | 
| 50 | 
            +
                    raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
         | 
| 53 | 
            +
                    taggable.save
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def is_tagger?
         | 
| 57 | 
            +
                    self.class.is_tagger?
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                module SingletonMethods
         | 
| 62 | 
            +
                  def is_tagger?
         | 
| 63 | 
            +
                    true
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module ActsAsTaggableOn
         | 
| 2 | 
            +
              module ActiveRecord
         | 
| 3 | 
            +
                module Backports
         | 
| 4 | 
            +
                  def self.included(base)
         | 
| 5 | 
            +
                    base.class_eval do
         | 
| 6 | 
            +
                      named_scope :where,    lambda { |conditions| { :conditions => conditions } }  
         | 
| 7 | 
            +
                      named_scope :joins,    lambda { |joins|      { :joins => joins } }
         | 
| 8 | 
            +
                      named_scope :group,    lambda { |group|      { :group => group } }
         | 
| 9 | 
            +
                      named_scope :order,    lambda { |order|      { :order => order } }
         | 
| 10 | 
            +
                      named_scope :select,   lambda { |select|     { :select => select } }
         | 
| 11 | 
            +
                      named_scope :limit,    lambda { |limit|      { :limit => limit } }
         | 
| 12 | 
            +
                      named_scope :readonly, lambda { |readonly|   { :readonly => readonly } }   
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            class Tag < ActiveRecord::Base
         | 
| 2 | 
            +
              include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
         | 
| 3 | 
            +
              include ::TagValidations
         | 
| 4 | 
            +
              include ::TagCustomisations
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              attr_accessible :name
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              ### ASSOCIATIONS:
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              has_many :taggings, :dependent => :destroy
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              ### VALIDATIONS:
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              validates_presence_of :name
         | 
| 15 | 
            +
              validates_uniqueness_of :name
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              ### SCOPES:
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def self.named(name)
         | 
| 20 | 
            +
                where(["name LIKE ?", name])
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
              
         | 
| 23 | 
            +
              def self.named_any(list)
         | 
| 24 | 
            +
                where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR "))
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
              
         | 
| 27 | 
            +
              def self.named_like(name)
         | 
| 28 | 
            +
                where(["name LIKE ?", "%#{name}%"])
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def self.named_like_any(list)
         | 
| 32 | 
            +
                where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR "))
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              ### CLASS METHODS:
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def self.find_or_create_with_like_by_name(name)
         | 
| 38 | 
            +
                named_like(name).first || create(:name => name)
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def self.find_or_create_all_with_like_by_name(*list)
         | 
| 42 | 
            +
                list = [list].flatten
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                return [] if list.empty?
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                existing_tags = Tag.named_any(list).all
         | 
| 47 | 
            +
                new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
         | 
| 48 | 
            +
                created_tags  = new_tag_names.map { |name| Tag.create(:name => name) }
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                existing_tags + created_tags
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
              ### INSTANCE METHODS:
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def ==(object)
         | 
| 56 | 
            +
                super || (object.is_a?(Tag) && name == object.name)
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              def to_s
         | 
| 60 | 
            +
                name
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              def count
         | 
| 64 | 
            +
                read_attribute(:count).to_i
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            end
         | 
| @@ -0,0 +1,95 @@ | |
| 1 | 
            +
            class TagList < Array
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              cattr_accessor :delimiter
         | 
| 4 | 
            +
              self.delimiter = ','
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              attr_accessor :owner
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              def initialize(*args)
         | 
| 9 | 
            +
                add(*args)
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
              
         | 
| 12 | 
            +
              ##
         | 
| 13 | 
            +
              # Returns a new TagList using the given tag string.
         | 
| 14 | 
            +
              #
         | 
| 15 | 
            +
              # Example:
         | 
| 16 | 
            +
              #   tag_list = TagList.from("One , Two,  Three")
         | 
| 17 | 
            +
              #   tag_list # ["One", "Two", "Three"]
         | 
| 18 | 
            +
              def self.from(string)
         | 
| 19 | 
            +
                string = string.join(", ") if string.respond_to?(:join)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                new.tap do |tag_list|
         | 
| 22 | 
            +
                  string = string.to_s.dup
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  # Parse the quoted tags
         | 
| 25 | 
            +
                  string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
         | 
| 26 | 
            +
                  string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  tag_list.add(string.split(delimiter))
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              ##
         | 
| 33 | 
            +
              # Add tags to the tag_list. Duplicate or blank tags will be ignored.
         | 
| 34 | 
            +
              # Use the <tt>:parse</tt> option to add an unparsed tag string.
         | 
| 35 | 
            +
              #
         | 
| 36 | 
            +
              # Example:
         | 
| 37 | 
            +
              #   tag_list.add("Fun", "Happy")
         | 
| 38 | 
            +
              #   tag_list.add("Fun, Happy", :parse => true)
         | 
| 39 | 
            +
              def add(*names)
         | 
| 40 | 
            +
                extract_and_apply_options!(names)
         | 
| 41 | 
            +
                concat(names)
         | 
| 42 | 
            +
                clean!
         | 
| 43 | 
            +
                self
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              ##
         | 
| 47 | 
            +
              # Remove specific tags from the tag_list.
         | 
| 48 | 
            +
              # Use the <tt>:parse</tt> option to add an unparsed tag string.
         | 
| 49 | 
            +
              #
         | 
| 50 | 
            +
              # Example:
         | 
| 51 | 
            +
              #   tag_list.remove("Sad", "Lonely")
         | 
| 52 | 
            +
              #   tag_list.remove("Sad, Lonely", :parse => true)
         | 
| 53 | 
            +
              def remove(*names)
         | 
| 54 | 
            +
                extract_and_apply_options!(names)
         | 
| 55 | 
            +
                delete_if { |name| names.include?(name) }
         | 
| 56 | 
            +
                self
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              ##
         | 
| 60 | 
            +
              # Transform the tag_list into a tag string suitable for edting in a form.
         | 
| 61 | 
            +
              # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
         | 
| 62 | 
            +
              #
         | 
| 63 | 
            +
              # Example:
         | 
| 64 | 
            +
              #   tag_list = TagList.new("Round", "Square,Cube")
         | 
| 65 | 
            +
              #   tag_list.to_s # 'Round, "Square,Cube"'
         | 
| 66 | 
            +
              def to_s
         | 
| 67 | 
            +
                tags = frozen? ? self.dup : self
         | 
| 68 | 
            +
                tags.send(:clean!)
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                tags.map do |name|
         | 
| 71 | 
            +
                  name.include?(delimiter) ? "\"#{name}\"" : name
         | 
| 72 | 
            +
                end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
              private
         | 
| 76 | 
            +
              
         | 
| 77 | 
            +
              # Remove whitespace, duplicates, and blanks.
         | 
| 78 | 
            +
              def clean!
         | 
| 79 | 
            +
                reject!(&:blank?)
         | 
| 80 | 
            +
                map!(&:strip)
         | 
| 81 | 
            +
                uniq!
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              def extract_and_apply_options!(args)
         | 
| 85 | 
            +
                options = args.last.is_a?(Hash) ? args.pop : {}
         | 
| 86 | 
            +
                options.assert_valid_keys :parse
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                if options[:parse]
         | 
| 89 | 
            +
                  args.map! { |a| self.class.from(a) }
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                args.flatten!
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            class Tagging < ActiveRecord::Base #:nodoc:
         | 
| 2 | 
            +
              include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              attr_accessible :tag,
         | 
| 5 | 
            +
                              :tag_id,
         | 
| 6 | 
            +
                              :context,
         | 
| 7 | 
            +
                              :taggable,
         | 
| 8 | 
            +
                              :taggable_type,
         | 
| 9 | 
            +
                              :taggable_id,
         | 
| 10 | 
            +
                              :tagger,
         | 
| 11 | 
            +
                              :tagger_type,
         | 
| 12 | 
            +
                              :tagger_id
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              belongs_to :tag
         | 
| 15 | 
            +
              belongs_to :taggable, :polymorphic => true
         | 
| 16 | 
            +
              belongs_to :tagger,   :polymorphic => true
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              validates_presence_of :context
         | 
| 19 | 
            +
              validates_presence_of :tag_id
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module TagsHelper
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # See the README for an example using tag_cloud.
         | 
| 4 | 
            +
              def tag_cloud(tags, classes)
         | 
| 5 | 
            +
                tags = tags.all if tags.respond_to?(:all)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                return [] if tags.empty?
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                max_count = tags.sort_by(&:count).last.count.to_f
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                tags.each do |tag|
         | 
| 12 | 
            +
                  index = ((tag.count / max_count) * (classes.size - 1)).round
         | 
| 13 | 
            +
                  yield tag, classes[index]
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            require 'rails/generators/migration'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ActsAsTaggableOn
         | 
| 4 | 
            +
              class MigrationGenerator < Rails::Generators::Base
         | 
| 5 | 
            +
                include Rails::Generators::Migration
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                desc "Generates migration for Tag and Tagging models"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def self.orm
         | 
| 10 | 
            +
                  Rails::Generators.options[:rails][:orm]
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def self.source_root
         | 
| 14 | 
            +
                  File.join(File.dirname(__FILE__), 'templates', orm)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def self.orm_has_migration?
         | 
| 18 | 
            +
                  [:active_record].include? orm
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def self.next_migration_number(path)
         | 
| 22 | 
            +
                  Time.now.utc.strftime("%Y%m%d%H%M%S")
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def create_migration_file
         | 
| 26 | 
            +
                  if self.class.orm_has_migration?
         | 
| 27 | 
            +
                    migration_template 'migration.rb', 'db/migrate/acts_as_taggable_on_migration'
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            class ActsAsTaggableOnMigration < ActiveRecord::Migration
         | 
| 2 | 
            +
              def self.up
         | 
| 3 | 
            +
                create_table :tags do |t|
         | 
| 4 | 
            +
                  t.string :name
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                create_table :taggings do |t|
         | 
| 8 | 
            +
                  t.references :tag
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  # You should make sure that the column created is
         | 
| 11 | 
            +
                  # long enough to store the required class names.
         | 
| 12 | 
            +
                  t.references :taggable, :polymorphic => true
         | 
| 13 | 
            +
                  t.references :tagger, :polymorphic => true
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  t.string :context
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  t.datetime :created_at
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                add_index :taggings, :tag_id
         | 
| 21 | 
            +
                add_index :taggings, [:taggable_id, :taggable_type, :context]
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              def self.down
         | 
| 25 | 
            +
                drop_table :taggings
         | 
| 26 | 
            +
                drop_table :tags
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
    
        data/rails/init.rb
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            require 'acts-as-taggable-on'
         | 
| @@ -0,0 +1,266 @@ | |
| 1 | 
            +
            require File.dirname(__FILE__) + '/../spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe "Acts As Taggable On" do
         | 
| 4 | 
            +
              before(:each) do
         | 
| 5 | 
            +
                clean_database!
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              it "should provide a class method 'taggable?' that is false for untaggable models" do
         | 
| 9 | 
            +
                UntaggableModel.should_not be_taggable
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              describe "Taggable Method Generation" do
         | 
| 13 | 
            +
                before(:each) do
         | 
| 14 | 
            +
                  [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
         | 
| 15 | 
            +
                  @taggable = TaggableModel.new(:name => "Bob Jones")
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                it "should respond 'true' to taggable?" do
         | 
| 19 | 
            +
                  @taggable.class.should be_taggable
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                it "should create a class attribute for tag types" do
         | 
| 23 | 
            +
                  @taggable.class.should respond_to(:tag_types)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                it "should create an instance attribute for tag types" do
         | 
| 27 | 
            +
                  @taggable.should respond_to(:tag_types)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
                
         | 
| 30 | 
            +
                it "should have all tag types" do
         | 
| 31 | 
            +
                  @taggable.tag_types.should == [:tags, :languages, :skills, :needs, :offerings]
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                it "should generate an association for each tag type" do
         | 
| 35 | 
            +
                  @taggable.should respond_to(:tags, :skills, :languages)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                it "should add tagged_with and tag_counts to singleton" do
         | 
| 39 | 
            +
                  TaggableModel.should respond_to(:tagged_with, :tag_counts)
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                it "should generate a tag_list accessor/setter for each tag type" do
         | 
| 43 | 
            +
                  @taggable.should respond_to(:tag_list, :skill_list, :language_list)
         | 
| 44 | 
            +
                  @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
                
         | 
| 47 | 
            +
                it "should generate a tag_list accessor, that includes owned tags, for each tag type" do
         | 
| 48 | 
            +
                  @taggable.should respond_to(:all_tags_list, :all_skills_list, :all_languages_list)
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              describe "Single Table Inheritance" do
         | 
| 53 | 
            +
                before do
         | 
| 54 | 
            +
                  @taggable = TaggableModel.new(:name => "taggable")
         | 
| 55 | 
            +
                  @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
         | 
| 56 | 
            +
                  @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              
         | 
| 59 | 
            +
                it "should pass on tag contexts to STI-inherited models" do
         | 
| 60 | 
            +
                  @inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
         | 
| 61 | 
            +
                  @inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              
         | 
| 64 | 
            +
                it "should have tag contexts added in altered STI models" do
         | 
| 65 | 
            +
                  @inherited_different.should respond_to(:part_list)
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
              
         | 
| 69 | 
            +
              describe "Reloading" do
         | 
| 70 | 
            +
                it "should save a model instantiated by Model.find" do
         | 
| 71 | 
            +
                  taggable = TaggableModel.create!(:name => "Taggable")
         | 
| 72 | 
            +
                  found_taggable = TaggableModel.find(taggable.id)
         | 
| 73 | 
            +
                  found_taggable.save
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              describe "Related Objects" do
         | 
| 78 | 
            +
                it "should find related objects based on tag names on context" do
         | 
| 79 | 
            +
                  taggable1 = TaggableModel.create!(:name => "Taggable 1")
         | 
| 80 | 
            +
                  taggable2 = TaggableModel.create!(:name => "Taggable 2")
         | 
| 81 | 
            +
                  taggable3 = TaggableModel.create!(:name => "Taggable 3")
         | 
| 82 | 
            +
              
         | 
| 83 | 
            +
                  taggable1.tag_list = "one, two"
         | 
| 84 | 
            +
                  taggable1.save
         | 
| 85 | 
            +
              
         | 
| 86 | 
            +
                  taggable2.tag_list = "three, four"
         | 
| 87 | 
            +
                  taggable2.save
         | 
| 88 | 
            +
              
         | 
| 89 | 
            +
                  taggable3.tag_list = "one, four"
         | 
| 90 | 
            +
                  taggable3.save
         | 
| 91 | 
            +
              
         | 
| 92 | 
            +
                  taggable1.find_related_tags.should include(taggable3)
         | 
| 93 | 
            +
                  taggable1.find_related_tags.should_not include(taggable2)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
              
         | 
| 96 | 
            +
                it "should find other related objects based on tag names on context" do
         | 
| 97 | 
            +
                  taggable1 = TaggableModel.create!(:name => "Taggable 1")
         | 
| 98 | 
            +
                  taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
         | 
| 99 | 
            +
                  taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
         | 
| 100 | 
            +
              
         | 
| 101 | 
            +
                  taggable1.tag_list = "one, two"
         | 
| 102 | 
            +
                  taggable1.save
         | 
| 103 | 
            +
              
         | 
| 104 | 
            +
                  taggable2.tag_list = "three, four"
         | 
| 105 | 
            +
                  taggable2.save
         | 
| 106 | 
            +
              
         | 
| 107 | 
            +
                  taggable3.tag_list = "one, four"
         | 
| 108 | 
            +
                  taggable3.save
         | 
| 109 | 
            +
              
         | 
| 110 | 
            +
                  taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
         | 
| 111 | 
            +
                  taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
              
         | 
| 114 | 
            +
                it "should not include the object itself in the list of related objects" do
         | 
| 115 | 
            +
                  taggable1 = TaggableModel.create!(:name => "Taggable 1")
         | 
| 116 | 
            +
                  taggable2 = TaggableModel.create!(:name => "Taggable 2")
         | 
| 117 | 
            +
              
         | 
| 118 | 
            +
                  taggable1.tag_list = "one"
         | 
| 119 | 
            +
                  taggable1.save
         | 
| 120 | 
            +
              
         | 
| 121 | 
            +
                  taggable2.tag_list = "one, two"
         | 
| 122 | 
            +
                  taggable2.save
         | 
| 123 | 
            +
              
         | 
| 124 | 
            +
                  taggable1.find_related_tags.should include(taggable2)
         | 
| 125 | 
            +
                  taggable1.find_related_tags.should_not include(taggable1)
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              describe "Matching Contexts" do
         | 
| 130 | 
            +
                it "should find objects with tags of matching contexts" do
         | 
| 131 | 
            +
                  taggable1 = TaggableModel.create!(:name => "Taggable 1")
         | 
| 132 | 
            +
                  taggable2 = TaggableModel.create!(:name => "Taggable 2")
         | 
| 133 | 
            +
                  taggable3 = TaggableModel.create!(:name => "Taggable 3")
         | 
| 134 | 
            +
              
         | 
| 135 | 
            +
                  taggable1.offering_list = "one, two"
         | 
| 136 | 
            +
                  taggable1.save!
         | 
| 137 | 
            +
              
         | 
| 138 | 
            +
                  taggable2.need_list = "one, two"
         | 
| 139 | 
            +
                  taggable2.save!
         | 
| 140 | 
            +
              
         | 
| 141 | 
            +
                  taggable3.offering_list = "one, two"
         | 
| 142 | 
            +
                  taggable3.save!
         | 
| 143 | 
            +
              
         | 
| 144 | 
            +
                  taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
         | 
| 145 | 
            +
                  taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
              
         | 
| 148 | 
            +
                it "should find other related objects with tags of matching contexts" do
         | 
| 149 | 
            +
                  taggable1 = TaggableModel.create!(:name => "Taggable 1")
         | 
| 150 | 
            +
                  taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
         | 
| 151 | 
            +
                  taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
         | 
| 152 | 
            +
              
         | 
| 153 | 
            +
                  taggable1.offering_list = "one, two"
         | 
| 154 | 
            +
                  taggable1.save
         | 
| 155 | 
            +
              
         | 
| 156 | 
            +
                  taggable2.need_list = "one, two"
         | 
| 157 | 
            +
                  taggable2.save
         | 
| 158 | 
            +
              
         | 
| 159 | 
            +
                  taggable3.offering_list = "one, two"
         | 
| 160 | 
            +
                  taggable3.save
         | 
| 161 | 
            +
              
         | 
| 162 | 
            +
                  taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
         | 
| 163 | 
            +
                  taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
              
         | 
| 166 | 
            +
                it "should not include the object itself in the list of related objects" do
         | 
| 167 | 
            +
                  taggable1 = TaggableModel.create!(:name => "Taggable 1")
         | 
| 168 | 
            +
                  taggable2 = TaggableModel.create!(:name => "Taggable 2")
         | 
| 169 | 
            +
              
         | 
| 170 | 
            +
                  taggable1.tag_list = "one"
         | 
| 171 | 
            +
                  taggable1.save
         | 
| 172 | 
            +
              
         | 
| 173 | 
            +
                  taggable2.tag_list = "one, two"
         | 
| 174 | 
            +
                  taggable2.save
         | 
| 175 | 
            +
              
         | 
| 176 | 
            +
                  taggable1.find_related_tags.should include(taggable2)
         | 
| 177 | 
            +
                  taggable1.find_related_tags.should_not include(taggable1)
         | 
| 178 | 
            +
                end
         | 
| 179 | 
            +
              end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
              describe 'Tagging Contexts' do
         | 
| 182 | 
            +
                it 'should eliminate duplicate tagging contexts ' do
         | 
| 183 | 
            +
                  TaggableModel.acts_as_taggable_on(:skills, :skills)
         | 
| 184 | 
            +
                  TaggableModel.tag_types.freq[:skills].should_not == 3
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                it "should not contain embedded/nested arrays" do
         | 
| 188 | 
            +
                  TaggableModel.acts_as_taggable_on([:array], [:array])
         | 
| 189 | 
            +
                  TaggableModel.tag_types.freq[[:array]].should == 0
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                it "should _flatten_ the content of arrays" do
         | 
| 193 | 
            +
                  TaggableModel.acts_as_taggable_on([:array], [:array])
         | 
| 194 | 
            +
                  TaggableModel.tag_types.freq[:array].should == 1
         | 
| 195 | 
            +
                end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                it "should not raise an error when passed nil" do
         | 
| 198 | 
            +
                  lambda {
         | 
| 199 | 
            +
                    TaggableModel.acts_as_taggable_on()
         | 
| 200 | 
            +
                  }.should_not raise_error
         | 
| 201 | 
            +
                end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                it "should not raise an error when passed [nil]" do
         | 
| 204 | 
            +
                  lambda {
         | 
| 205 | 
            +
                    TaggableModel.acts_as_taggable_on([nil])
         | 
| 206 | 
            +
                  }.should_not raise_error
         | 
| 207 | 
            +
                end
         | 
| 208 | 
            +
              end
         | 
| 209 | 
            +
              
         | 
| 210 | 
            +
              describe 'Caching' do
         | 
| 211 | 
            +
                before(:each) do
         | 
| 212 | 
            +
                  @taggable = CachedModel.new(:name => "Bob Jones")  
         | 
| 213 | 
            +
                end
         | 
| 214 | 
            +
                
         | 
| 215 | 
            +
                it "should add saving of tag lists and cached tag lists to the instance" do
         | 
| 216 | 
            +
                  @taggable.should respond_to(:save_cached_tag_list)
         | 
| 217 | 
            +
                  @taggable.should respond_to(:save_tags)
         | 
| 218 | 
            +
                end  
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                it "should generate a cached column checker for each tag type" do
         | 
| 221 | 
            +
                  CachedModel.should respond_to(:caching_tag_list?)
         | 
| 222 | 
            +
                end  
         | 
| 223 | 
            +
                
         | 
| 224 | 
            +
                it 'should not have cached tags' do
         | 
| 225 | 
            +
                  @taggable.cached_tag_list.should be_blank  
         | 
| 226 | 
            +
                end
         | 
| 227 | 
            +
                
         | 
| 228 | 
            +
                it 'should cache tags' do
         | 
| 229 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome, epic')
         | 
| 230 | 
            +
                  @taggable.cached_tag_list.should == 'awesome, epic'
         | 
| 231 | 
            +
                end
         | 
| 232 | 
            +
                
         | 
| 233 | 
            +
                it 'should keep the cache' do
         | 
| 234 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome, epic')
         | 
| 235 | 
            +
                  @taggable = CachedModel.find(@taggable)  
         | 
| 236 | 
            +
                  @taggable.save!
         | 
| 237 | 
            +
                  @taggable.cached_tag_list.should == 'awesome, epic'   
         | 
| 238 | 
            +
                end
         | 
| 239 | 
            +
                
         | 
| 240 | 
            +
                it 'should update the cache' do
         | 
| 241 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome, epic')
         | 
| 242 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome')
         | 
| 243 | 
            +
                  @taggable.cached_tag_list.should == 'awesome'      
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
                
         | 
| 246 | 
            +
                it 'should remove the cache' do
         | 
| 247 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome, epic')
         | 
| 248 | 
            +
                  @taggable.update_attributes(:tag_list => '')
         | 
| 249 | 
            +
                  @taggable.cached_tag_list.should be_blank  
         | 
| 250 | 
            +
                end
         | 
| 251 | 
            +
                
         | 
| 252 | 
            +
                it 'should have a tag list' do
         | 
| 253 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome, epic')
         | 
| 254 | 
            +
                  @taggable = CachedModel.find(@taggable.id)
         | 
| 255 | 
            +
                  @taggable.tag_list.sort.should == %w(awesome epic).sort
         | 
| 256 | 
            +
                end
         | 
| 257 | 
            +
                
         | 
| 258 | 
            +
                it 'should keep the tag list' do
         | 
| 259 | 
            +
                  @taggable.update_attributes(:tag_list => 'awesome, epic')
         | 
| 260 | 
            +
                  @taggable = CachedModel.find(@taggable.id)
         | 
| 261 | 
            +
                  @taggable.save!
         | 
| 262 | 
            +
                  @taggable.tag_list.sort.should == %w(awesome epic).sort
         | 
| 263 | 
            +
                end
         | 
| 264 | 
            +
              end
         | 
| 265 | 
            +
             | 
| 266 | 
            +
            end
         |