ghazel-acts-as-taggable-on 2.0.6.1

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 +10 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +221 -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 +30 -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 +139 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +262 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +105 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +69 -0
  16. data/lib/acts_as_taggable_on/acts_as_tagger.rb +67 -0
  17. data/lib/acts_as_taggable_on/compatibility/Gemfile +8 -0
  18. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +21 -0
  19. data/lib/acts_as_taggable_on/tag.rb +84 -0
  20. data/lib/acts_as_taggable_on/tag_list.rb +96 -0
  21. data/lib/acts_as_taggable_on/tagging.rb +24 -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 +32 -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 +268 -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 +333 -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/database.yml.sample +17 -0
  36. data/spec/models.rb +31 -0
  37. data/spec/schema.rb +43 -0
  38. data/spec/spec_helper.rb +60 -0
  39. metadata +114 -0
@@ -0,0 +1,105 @@
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
+ if owner.nil?
34
+ base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
35
+ else
36
+ base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
39
+ end
40
+ end
41
+
42
+ def cached_owned_tag_list_on(context)
43
+ variable_name = "@owned_#{context}_list"
44
+ cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
45
+ end
46
+
47
+ def owner_tag_list_on(owner, context)
48
+ add_custom_context(context)
49
+
50
+ cache = cached_owned_tag_list_on(context)
51
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
52
+
53
+ cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
54
+ end
55
+
56
+ def set_owner_tag_list_on(owner, context, new_list)
57
+ add_custom_context(context)
58
+
59
+ cache = cached_owned_tag_list_on(context)
60
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
61
+
62
+ cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
63
+ end
64
+
65
+ def reload(*args)
66
+ self.class.tag_types.each do |context|
67
+ instance_variable_set("@owned_#{context}_list", nil)
68
+ end
69
+
70
+ super(*args)
71
+ end
72
+
73
+ def save_owned_tags
74
+ tagging_contexts.each do |context|
75
+ cached_owned_tag_list_on(context).each do |owner, tag_list|
76
+ # Find existing tags or create non-existing tags:
77
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
78
+
79
+ owned_tags = owner_tags_on(owner, context)
80
+ old_tags = owned_tags - tag_list
81
+ new_tags = tag_list - owned_tags
82
+
83
+ # Find all taggings that belong to the taggable (self), are owned by the owner,
84
+ # have the correct context, and are removed from the list.
85
+ old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
86
+ :tagger_type => owner.class.to_s, :tagger_id => owner.id,
87
+ :tag_id => old_tags, :context => context).all
88
+
89
+ if old_taggings.present?
90
+ # Destroy old taggings:
91
+ ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
92
+ end
93
+
94
+ # Create new taggings:
95
+ new_tags.each do |tag|
96
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
97
+ end
98
+ end
99
+ end
100
+
101
+ true
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,69 @@
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
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
46
+
47
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
48
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
49
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
50
+ :group => group_columns,
51
+ :order => "count DESC" }.update(options))
52
+ end
53
+
54
+ def related_tags_for(context, klass, options = {})
55
+ tags_to_find = tags_on(context).collect { |t| t.name }
56
+
57
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
58
+
59
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
60
+
61
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
62
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
63
+ :conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
64
+ :group => group_columns,
65
+ :order => "count DESC" }.update(options))
66
+ end
67
+ end
68
+ end
69
+ 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 => "ActsAsTaggableOn::Tagging")
20
+ has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true, :class_name => "ActsAsTaggableOn::Tag"
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,8 @@
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'
7
+ gem 'mysql'
8
+ gem 'pg'
@@ -0,0 +1,21 @@
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
+
14
+ def self.to_sql
15
+ construct_finder_sql({})
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,84 @@
1
+ module ActsAsTaggableOn
2
+ class Tag < ::ActiveRecord::Base
3
+ include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
4
+
5
+ attr_accessible :name
6
+
7
+ ### ASSOCIATIONS:
8
+
9
+ has_many :taggings, :dependent => :destroy, :class_name => 'ActsAsTaggableOn::Tagging'
10
+
11
+ ### VALIDATIONS:
12
+
13
+ validates_presence_of :name
14
+ validates_uniqueness_of :name
15
+
16
+ ### SCOPES:
17
+
18
+ def self.using_postgresql?
19
+ connection.adapter_name == 'PostgreSQL'
20
+ end
21
+
22
+ def self.named(name)
23
+ where(["name #{like_operator} ?", name])
24
+ end
25
+
26
+ def self.named_any(list)
27
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", tag.to_s]) }.join(" OR "))
28
+ end
29
+
30
+ def self.named_like(name)
31
+ where(["name #{like_operator} ?", "%#{name}%"])
32
+ end
33
+
34
+ def self.named_like_any(list)
35
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{tag.to_s}%"]) }.join(" OR "))
36
+ end
37
+
38
+ ### CLASS METHODS:
39
+
40
+ def self.find_or_create_with_like_by_name(name)
41
+ named_like(name).first || create(:name => name)
42
+ end
43
+
44
+ def self.find_or_create_all_with_like_by_name(*list)
45
+ list = [list].flatten
46
+
47
+ return [] if list.empty?
48
+
49
+ existing_tags = Tag.named_any(list).all
50
+ new_tag_names = list.reject do |name|
51
+ name = comparable_name(name)
52
+ existing_tags.any? { |tag| comparable_name(tag.name) == name }
53
+ end
54
+ created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
55
+
56
+ existing_tags + created_tags
57
+ end
58
+
59
+ ### INSTANCE METHODS:
60
+
61
+ def ==(object)
62
+ super || (object.is_a?(Tag) && name == object.name)
63
+ end
64
+
65
+ def to_s
66
+ name
67
+ end
68
+
69
+ def count
70
+ read_attribute(:count).to_i
71
+ end
72
+
73
+ class << self
74
+ private
75
+ def like_operator
76
+ using_postgresql? ? 'ILIKE' : 'LIKE'
77
+ end
78
+
79
+ def comparable_name(str)
80
+ RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,96 @@
1
+ module ActsAsTaggableOn
2
+ class TagList < Array
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
+ glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
20
+ string = string.join(glue) if string.respond_to?(:join)
21
+
22
+ new.tap do |tag_list|
23
+ string = string.to_s.dup
24
+
25
+ # Parse the quoted tags
26
+ string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
27
+ string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
28
+
29
+ tag_list.add(string.split(delimiter))
30
+ end
31
+ end
32
+
33
+ ##
34
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
35
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
36
+ #
37
+ # Example:
38
+ # tag_list.add("Fun", "Happy")
39
+ # tag_list.add("Fun, Happy", :parse => true)
40
+ def add(*names)
41
+ extract_and_apply_options!(names)
42
+ concat(names)
43
+ clean!
44
+ self
45
+ end
46
+
47
+ ##
48
+ # Remove specific tags from the tag_list.
49
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
50
+ #
51
+ # Example:
52
+ # tag_list.remove("Sad", "Lonely")
53
+ # tag_list.remove("Sad, Lonely", :parse => true)
54
+ def remove(*names)
55
+ extract_and_apply_options!(names)
56
+ delete_if { |name| names.include?(name) }
57
+ self
58
+ end
59
+
60
+ ##
61
+ # Transform the tag_list into a tag string suitable for edting in a form.
62
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
63
+ #
64
+ # Example:
65
+ # tag_list = TagList.new("Round", "Square,Cube")
66
+ # tag_list.to_s # 'Round, "Square,Cube"'
67
+ def to_s
68
+ tags = frozen? ? self.dup : self
69
+ tags.send(:clean!)
70
+
71
+ tags.map do |name|
72
+ name.include?(delimiter) ? "\"#{name}\"" : name
73
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
74
+ end
75
+
76
+ private
77
+
78
+ # Remove whitespace, duplicates, and blanks.
79
+ def clean!
80
+ reject!(&:blank?)
81
+ map!(&:strip)
82
+ uniq!
83
+ end
84
+
85
+ def extract_and_apply_options!(args)
86
+ options = args.last.is_a?(Hash) ? args.pop : {}
87
+ options.assert_valid_keys :parse
88
+
89
+ if options[:parse]
90
+ args.map! { |a| self.class.from(a) }
91
+ end
92
+
93
+ args.flatten!
94
+ end
95
+ end
96
+ end