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.
Files changed (39) hide show
  1. data/CHANGELOG +25 -0
  2. data/Gemfile +6 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +227 -0
  5. data/Rakefile +59 -0
  6. data/VERSION +1 -0
  7. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  8. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  9. data/lib/acts-as-taggable-on.rb +32 -0
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +53 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +130 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +237 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +65 -0
  16. data/lib/acts_as_taggable_on/acts_as_tagger.rb +67 -0
  17. data/lib/acts_as_taggable_on/compatibility/Gemfile +6 -0
  18. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
  19. data/lib/acts_as_taggable_on/tag.rb +67 -0
  20. data/lib/acts_as_taggable_on/tag_list.rb +95 -0
  21. data/lib/acts_as_taggable_on/tagging.rb +23 -0
  22. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  23. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
  24. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  25. data/rails/init.rb +1 -0
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +266 -0
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  28. data/spec/acts_as_taggable_on/tag_list_spec.rb +70 -0
  29. data/spec/acts_as_taggable_on/tag_spec.rb +115 -0
  30. data/spec/acts_as_taggable_on/taggable_spec.rb +306 -0
  31. data/spec/acts_as_taggable_on/tagger_spec.rb +91 -0
  32. data/spec/acts_as_taggable_on/tagging_spec.rb +31 -0
  33. data/spec/acts_as_taggable_on/tags_helper_spec.rb +28 -0
  34. data/spec/bm.rb +52 -0
  35. data/spec/models.rb +31 -0
  36. data/spec/schema.rb +43 -0
  37. data/spec/spec.opts +2 -0
  38. data/spec/spec_helper.rb +53 -0
  39. 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,6 @@
1
+ source :gemcutter
2
+
3
+ # Rails 2.3
4
+ gem 'rails', '2.3.5'
5
+ gem 'rspec', '1.3.0', :require => 'spec'
6
+ gem 'sqlite3-ruby', '1.2.5', :require => 'sqlite3'
@@ -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
@@ -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