axtro-acts-as-taggable-on 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +25 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +95 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +221 -0
  6. data/Rakefile +59 -0
  7. data/VERSION +1 -0
  8. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  9. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  10. data/lib/acts-as-taggable-on.rb +30 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +53 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +130 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +237 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +65 -0
  17. data/lib/acts_as_taggable_on/acts_as_tagger.rb +67 -0
  18. data/lib/acts_as_taggable_on/compatibility/Gemfile +6 -0
  19. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
  20. data/lib/acts_as_taggable_on/tag.rb +65 -0
  21. data/lib/acts_as_taggable_on/tag_list.rb +95 -0
  22. data/lib/acts_as_taggable_on/tagging.rb +23 -0
  23. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  24. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
  25. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  26. data/rails/init.rb +1 -0
  27. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +266 -0
  28. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  29. data/spec/acts_as_taggable_on/tag_list_spec.rb +70 -0
  30. data/spec/acts_as_taggable_on/tag_spec.rb +115 -0
  31. data/spec/acts_as_taggable_on/taggable_spec.rb +311 -0
  32. data/spec/acts_as_taggable_on/tagger_spec.rb +91 -0
  33. data/spec/acts_as_taggable_on/tagging_spec.rb +31 -0
  34. data/spec/acts_as_taggable_on/tags_helper_spec.rb +28 -0
  35. data/spec/bm.rb +52 -0
  36. data/spec/models.rb +31 -0
  37. data/spec/schema.rb +43 -0
  38. data/spec/spec.opts +2 -0
  39. data/spec/spec_helper.rb +53 -0
  40. metadata +169 -0
@@ -0,0 +1,101 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Ownership
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Ownership::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods
6
+
7
+ base.class_eval do
8
+ after_save :save_owned_tags
9
+ end
10
+
11
+ base.initialize_acts_as_taggable_on_ownership
12
+ end
13
+
14
+ module ClassMethods
15
+ def acts_as_taggable_on(*args)
16
+ initialize_acts_as_taggable_on_ownership
17
+ super(*args)
18
+ end
19
+
20
+ def initialize_acts_as_taggable_on_ownership
21
+ tag_types.map(&:to_s).each do |tag_type|
22
+ class_eval %(
23
+ def #{tag_type}_from(owner)
24
+ owner_tag_list_on(owner, '#{tag_type}')
25
+ end
26
+ )
27
+ end
28
+ end
29
+ end
30
+
31
+ module InstanceMethods
32
+ def owner_tags_on(owner, context)
33
+ base_tags.where([%(#{Tagging.table_name}.context = ? AND
34
+ #{Tagging.table_name}.tagger_id = ? AND
35
+ #{Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
36
+ end
37
+
38
+ def cached_owned_tag_list_on(context)
39
+ variable_name = "@owned_#{context}_list"
40
+ cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
41
+ end
42
+
43
+ def owner_tag_list_on(owner, context)
44
+ add_custom_context(context)
45
+
46
+ cache = cached_owned_tag_list_on(context)
47
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
48
+
49
+ cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
50
+ end
51
+
52
+ def set_owner_tag_list_on(owner, context, new_list)
53
+ add_custom_context(context)
54
+
55
+ cache = cached_owned_tag_list_on(context)
56
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
57
+
58
+ cache[owner] = TagList.from(new_list)
59
+ end
60
+
61
+ def reload
62
+ self.class.tag_types.each do |context|
63
+ instance_variable_set("@owned_#{context}_list", nil)
64
+ end
65
+
66
+ super
67
+ end
68
+
69
+ def save_owned_tags
70
+ tagging_contexts.each do |context|
71
+ cached_owned_tag_list_on(context).each do |owner, tag_list|
72
+ # Find existing tags or create non-existing tags:
73
+ tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
74
+
75
+ owned_tags = owner_tags_on(owner, context)
76
+ old_tags = owned_tags - tag_list
77
+ new_tags = tag_list - owned_tags
78
+
79
+ # Find all taggings that belong to the taggable (self), are owned by the owner,
80
+ # have the correct context, and are removed from the list.
81
+ old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
82
+ :tagger_type => owner.class.to_s, :tagger_id => owner.id,
83
+ :tag_id => old_tags, :context => context).all
84
+
85
+ if old_taggings.present?
86
+ # Destroy old taggings:
87
+ Tagging.destroy_all(:id => old_taggings.map(&:id))
88
+ end
89
+
90
+ # Create new taggings:
91
+ new_tags.each do |tag|
92
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
93
+ end
94
+ end
95
+ end
96
+
97
+ true
98
+ end
99
+ end
100
+ end
101
+ end
@@ -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,65 @@
1
+ class Tag < ActiveRecord::Base
2
+ include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
3
+
4
+ attr_accessible :name
5
+
6
+ ### ASSOCIATIONS:
7
+
8
+ has_many :taggings, :dependent => :destroy
9
+
10
+ ### VALIDATIONS:
11
+
12
+ validates_presence_of :name
13
+ validates_uniqueness_of :name, :scope => :store_id
14
+
15
+ ### SCOPES:
16
+
17
+ def self.named(name)
18
+ where(["name LIKE ?", name])
19
+ end
20
+
21
+ def self.named_any(list)
22
+ where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR "))
23
+ end
24
+
25
+ def self.named_like(name)
26
+ where(["name LIKE ?", "%#{name}%"])
27
+ end
28
+
29
+ def self.named_like_any(list)
30
+ where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR "))
31
+ end
32
+
33
+ ### CLASS METHODS:
34
+
35
+ def self.find_or_create_with_like_by_name(name)
36
+ named_like(name).first || create(:name => name)
37
+ end
38
+
39
+ def self.find_or_create_all_with_like_by_name(*list)
40
+ list = [list].flatten
41
+
42
+ return [] if list.empty?
43
+
44
+ existing_tags = Tag.named_any(list).all
45
+ new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
46
+ created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
47
+
48
+ existing_tags + created_tags
49
+ end
50
+
51
+ ### INSTANCE METHODS:
52
+
53
+ def ==(object)
54
+ super || (object.is_a?(Tag) && name == object.name)
55
+ end
56
+
57
+ def to_s
58
+ name
59
+ end
60
+
61
+ def count
62
+ read_attribute(:count).to_i
63
+ end
64
+
65
+ 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