rylwin-acts-as-taggable-on 2.1.1a

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +10 -0
  4. data/CHANGELOG +35 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +5 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.rdoc +222 -0
  9. data/Rakefile +13 -0
  10. data/acts-as-taggable-on.gemspec +28 -0
  11. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  12. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  13. data/lib/acts-as-taggable-on/version.rb +4 -0
  14. data/lib/acts-as-taggable-on.rb +36 -0
  15. data/lib/acts_as_taggable_on/acts_as_tagger.rb +67 -0
  16. data/lib/acts_as_taggable_on/compatibility/Gemfile +8 -0
  17. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +21 -0
  18. data/lib/acts_as_taggable_on/tag.rb +81 -0
  19. data/lib/acts_as_taggable_on/tag_list.rb +96 -0
  20. data/lib/acts_as_taggable_on/taggable/acts_as_taggable_on/cache.rb +53 -0
  21. data/lib/acts_as_taggable_on/taggable/acts_as_taggable_on/collection.rb +139 -0
  22. data/lib/acts_as_taggable_on/taggable/acts_as_taggable_on/core.rb +284 -0
  23. data/lib/acts_as_taggable_on/taggable/acts_as_taggable_on/ownership.rb +105 -0
  24. data/lib/acts_as_taggable_on/taggable/acts_as_taggable_on/related.rb +73 -0
  25. data/lib/acts_as_taggable_on/taggable/acts_as_tagger.rb +67 -0
  26. data/lib/acts_as_taggable_on/taggable/cache.rb +53 -0
  27. data/lib/acts_as_taggable_on/taggable/collection.rb +139 -0
  28. data/lib/acts_as_taggable_on/taggable/compatibility/Gemfile +8 -0
  29. data/lib/acts_as_taggable_on/taggable/compatibility/active_record_backports.rb +21 -0
  30. data/lib/acts_as_taggable_on/taggable/core.rb +284 -0
  31. data/lib/acts_as_taggable_on/taggable/ownership.rb +105 -0
  32. data/lib/acts_as_taggable_on/taggable/related.rb +73 -0
  33. data/lib/acts_as_taggable_on/taggable/tag.rb +81 -0
  34. data/lib/acts_as_taggable_on/taggable/tag_list.rb +96 -0
  35. data/lib/acts_as_taggable_on/taggable/tagging.rb +24 -0
  36. data/lib/acts_as_taggable_on/taggable/tags_helper.rb +17 -0
  37. data/lib/acts_as_taggable_on/taggable/utils.rb +31 -0
  38. data/lib/acts_as_taggable_on/taggable.rb +63 -0
  39. data/lib/acts_as_taggable_on/tagging.rb +24 -0
  40. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  41. data/lib/acts_as_taggable_on/utils.rb +31 -0
  42. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +39 -0
  43. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  44. data/rails/init.rb +1 -0
  45. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +366 -0
  46. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  47. data/spec/acts_as_taggable_on/tag_list_spec.rb +74 -0
  48. data/spec/acts_as_taggable_on/tag_spec.rb +136 -0
  49. data/spec/acts_as_taggable_on/taggable_spec.rb +410 -0
  50. data/spec/acts_as_taggable_on/tagger_spec.rb +105 -0
  51. data/spec/acts_as_taggable_on/tagging_spec.rb +31 -0
  52. data/spec/acts_as_taggable_on/tags_helper_spec.rb +28 -0
  53. data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
  54. data/spec/bm.rb +52 -0
  55. data/spec/database.yml.sample +19 -0
  56. data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
  57. data/spec/models.rb +44 -0
  58. data/spec/schema.rb +56 -0
  59. data/spec/spec_helper.rb +81 -0
  60. data/uninstall.rb +1 -0
  61. metadata +245 -0
@@ -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
@@ -0,0 +1,53 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Cache
3
+ def self.included(base)
4
+ # Skip adding caching capabilities if table not exists or no cache columns exist
5
+ return unless base.table_exists? && base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
6
+
7
+ base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8
+ base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
9
+
10
+ base.class_eval do
11
+ before_save :save_cached_tag_list
12
+ end
13
+
14
+ base.initialize_acts_as_taggable_on_cache
15
+ end
16
+
17
+ module ClassMethods
18
+ def initialize_acts_as_taggable_on_cache
19
+ tag_types.map(&:to_s).each do |tag_type|
20
+ class_eval %(
21
+ def self.caching_#{tag_type.singularize}_list?
22
+ caching_tag_list_on?("#{tag_type}")
23
+ end
24
+ )
25
+ end
26
+ end
27
+
28
+ def acts_as_taggable_on(*args)
29
+ super(*args)
30
+ initialize_acts_as_taggable_on_cache
31
+ end
32
+
33
+ def caching_tag_list_on?(context)
34
+ column_names.include?("cached_#{context.to_s.singularize}_list")
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ def save_cached_tag_list
40
+ tag_types.map(&:to_s).each do |tag_type|
41
+ if self.class.send("caching_#{tag_type.singularize}_list?")
42
+ if tag_list_cache_set_on(tag_type)
43
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
44
+ self["cached_#{tag_type.singularize}_list"] = list
45
+ end
46
+ end
47
+ end
48
+
49
+ true
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,139 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Collection
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Collection::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
6
+ base.initialize_acts_as_taggable_on_collection
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_collection
11
+ tag_types.map(&:to_s).each do |tag_type|
12
+ class_eval %(
13
+ def self.#{tag_type.singularize}_counts(options={})
14
+ tag_counts_on('#{tag_type}', options)
15
+ end
16
+
17
+ def #{tag_type.singularize}_counts(options = {})
18
+ tag_counts_on('#{tag_type}', options)
19
+ end
20
+
21
+ def top_#{tag_type}(limit = 10)
22
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
23
+ end
24
+
25
+ def self.top_#{tag_type}(limit = 10)
26
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
27
+ end
28
+ )
29
+ end
30
+ end
31
+
32
+ def acts_as_taggable_on(*args)
33
+ super(*args)
34
+ initialize_acts_as_taggable_on_collection
35
+ end
36
+
37
+ def tag_counts_on(context, options = {})
38
+ all_tag_counts(options.merge({:on => context.to_s}))
39
+ end
40
+
41
+ ##
42
+ # Calculate the tag counts for all tags.
43
+ #
44
+ # @param [Hash] options Options:
45
+ # * :start_at - Restrict the tags to those created after a certain time
46
+ # * :end_at - Restrict the tags to those created before a certain time
47
+ # * :conditions - A piece of SQL conditions to add to the query
48
+ # * :limit - The maximum number of tags to return
49
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
50
+ # * :at_least - Exclude tags with a frequency less than the given value
51
+ # * :at_most - Exclude tags with a frequency greater than the given value
52
+ # * :on - Scope the find to only include a certain context
53
+ def all_tag_counts(options = {})
54
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
55
+
56
+ scope = if ActiveRecord::VERSION::MAJOR >= 3
57
+ {}
58
+ else
59
+ scope(:find) || {}
60
+ end
61
+
62
+ ## Generate conditions:
63
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
64
+
65
+ start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
66
+ end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
67
+
68
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
69
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
70
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
71
+
72
+ tagging_conditions = [
73
+ taggable_conditions,
74
+ scope[:conditions],
75
+ start_at_conditions,
76
+ end_at_conditions
77
+ ].compact.reverse
78
+
79
+ tag_conditions = [
80
+ options[:conditions]
81
+ ].compact.reverse
82
+
83
+ ## Generate joins:
84
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
85
+ taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
86
+
87
+ tagging_joins = [
88
+ taggable_join,
89
+ scope[:joins]
90
+ ].compact
91
+
92
+ tag_joins = [
93
+ ].compact
94
+
95
+ [tagging_joins, tag_joins].each(&:reverse!) if ActiveRecord::VERSION::MAJOR < 3
96
+
97
+ ## Generate scope:
98
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
99
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
100
+
101
+ # Joins and conditions
102
+ tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
103
+ tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
104
+
105
+ tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
106
+ tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
107
+
108
+ # GROUP BY and HAVING clauses:
109
+ at_least = sanitize_sql(['tags_count >= ?', options.delete(:at_least)]) if options[:at_least]
110
+ at_most = sanitize_sql(['tags_count <= ?', options.delete(:at_most)]) if options[:at_most]
111
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
112
+
113
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
114
+
115
+ if ActiveRecord::VERSION::MAJOR >= 3
116
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
117
+ scoped_select = "#{table_name}.#{primary_key}"
118
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{select(scoped_select).to_sql})").
119
+ group(group_columns).
120
+ having(having)
121
+ else
122
+ # Having is not available in 2.3.x:
123
+ group_by = "#{group_columns} HAVING COUNT(*) > 0"
124
+ group_by << " AND #{having}" unless having.blank?
125
+ tagging_scope = tagging_scope.group(group_by)
126
+ end
127
+
128
+ tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS taggings ON taggings.tag_id = tags.id")
129
+ tag_scope
130
+ end
131
+ end
132
+
133
+ module InstanceMethods
134
+ def tag_counts_on(context, options={})
135
+ self.class.tag_counts_on(context, options.merge(:id => id))
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,284 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Core
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
6
+
7
+ base.class_eval do
8
+ attr_writer :custom_contexts
9
+ after_save :save_tags
10
+ end
11
+
12
+ base.initialize_acts_as_taggable_on_core
13
+ end
14
+
15
+ module ClassMethods
16
+ def initialize_acts_as_taggable_on_core
17
+ tag_types.map(&:to_s).each do |tags_type|
18
+ tag_type = tags_type.to_s.singularize
19
+ context_taggings = "#{tag_type}_taggings".to_sym
20
+ context_tags = tags_type.to_sym
21
+
22
+ class_eval do
23
+ has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
+ has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
26
+ end
27
+
28
+ class_eval %(
29
+ def #{tag_type}_list
30
+ tag_list_on('#{tags_type}')
31
+ end
32
+
33
+ def #{tag_type}_list=(new_tags)
34
+ set_tag_list_on('#{tags_type}', new_tags)
35
+ end
36
+
37
+ def all_#{tags_type}_list
38
+ all_tags_list_on('#{tags_type}')
39
+ end
40
+ )
41
+ end
42
+ end
43
+
44
+ def acts_as_taggable_on(*args)
45
+ super(*args)
46
+ initialize_acts_as_taggable_on_core
47
+ end
48
+
49
+ # all column names are necessary for PostgreSQL group clause
50
+ def grouped_column_names_for(object)
51
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
52
+ end
53
+
54
+ ##
55
+ # Return a scope of objects that are tagged with the specified tags.
56
+ #
57
+ # @param tags The tags that we want to query for
58
+ # @param [Hash] options A hash of options to alter you query:
59
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
60
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
61
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
62
+ # * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
63
+ #
64
+ # Example:
65
+ # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
66
+ # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
67
+ # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
68
+ # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
69
+ # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
70
+ def tagged_with(tags, options = {})
71
+ tag_list = ActsAsTaggableOn::TagList.from(tags)
72
+ empty_result = scoped(:conditions => "1 = 0")
73
+
74
+ return empty_result if tag_list.empty?
75
+
76
+ joins = []
77
+ conditions = []
78
+
79
+ context = options.delete(:on)
80
+ owned_by = options.delete(:owned_by)
81
+ alias_base_name = undecorated_table_name.gsub('.','_')
82
+
83
+ if options.delete(:exclude)
84
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
85
+ conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
86
+
87
+ elsif options.delete(:any)
88
+ # get tags, drop out if nothing returned (we need at least one)
89
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
90
+ return scoped(:conditions => "1 = 0") unless tags.length > 0
91
+
92
+ # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
93
+ # avoid ambiguous column name
94
+ taggings_context = context ? "_#{context}" : ''
95
+
96
+ #TODO: fix alias to be smaller
97
+ taggings_alias = "#{alias_base_name}#{taggings_context}_taggings_#{tags.map(&:safe_name).join('_')}_#{rand(1024)}"
98
+
99
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
100
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
101
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
102
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
103
+
104
+ # don't need to sanitize sql, map all ids and join with OR logic
105
+ conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
106
+ select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
107
+
108
+ joins << tagging_join
109
+
110
+ else
111
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
112
+ return empty_result unless tags.length == tag_list.length
113
+
114
+ tags.each do |tag|
115
+ prefix = "#{tag.safe_name}_#{rand(1024)}"
116
+
117
+ taggings_alias = "#{alias_base_name}_taggings_#{prefix}"
118
+
119
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
120
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
121
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
122
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
123
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
124
+
125
+ if owned_by
126
+ tagging_join << " AND " +
127
+ sanitize_sql([
128
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
129
+ owned_by.id,
130
+ owned_by.class.to_s
131
+ ])
132
+ end
133
+
134
+ joins << tagging_join
135
+ end
136
+ end
137
+
138
+ taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
139
+
140
+ if options.delete(:match_all)
141
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
142
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
143
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
144
+
145
+
146
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
147
+ group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
148
+ end
149
+
150
+ scoped(:select => select_clause,
151
+ :joins => joins.join(" "),
152
+ :group => group,
153
+ :conditions => conditions.join(" AND "),
154
+ :order => options[:order],
155
+ :readonly => false)
156
+ end
157
+
158
+ def is_taggable?
159
+ true
160
+ end
161
+ end
162
+
163
+ module InstanceMethods
164
+ # all column names are necessary for PostgreSQL group clause
165
+ def grouped_column_names_for(object)
166
+ self.class.grouped_column_names_for(object)
167
+ end
168
+
169
+ def custom_contexts
170
+ @custom_contexts ||= []
171
+ end
172
+
173
+ def is_taggable?
174
+ self.class.is_taggable?
175
+ end
176
+
177
+ def add_custom_context(value)
178
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
179
+ end
180
+
181
+ def cached_tag_list_on(context)
182
+ self["cached_#{context.to_s.singularize}_list"]
183
+ end
184
+
185
+ def tag_list_cache_set_on(context)
186
+ variable_name = "@#{context.to_s.singularize}_list"
187
+ !instance_variable_get(variable_name).nil?
188
+ end
189
+
190
+ def tag_list_cache_on(context)
191
+ variable_name = "@#{context.to_s.singularize}_list"
192
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
193
+ end
194
+
195
+ def tag_list_on(context)
196
+ add_custom_context(context)
197
+ tag_list_cache_on(context)
198
+ end
199
+
200
+ def all_tags_list_on(context)
201
+ variable_name = "@all_#{context.to_s.singularize}_list"
202
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
203
+
204
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
205
+ end
206
+
207
+ ##
208
+ # Returns all tags of a given context
209
+ def all_tags_on(context)
210
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
211
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
212
+
213
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
214
+ scope = base_tags.where(opts)
215
+
216
+ if ActsAsTaggableOn::Tag.using_postgresql?
217
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
218
+ scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
219
+ else
220
+ scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
221
+ end
222
+
223
+ scope.all
224
+ end
225
+
226
+ ##
227
+ # Returns all tags that are not owned of a given context
228
+ def tags_on(context)
229
+ base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
230
+ end
231
+
232
+ def set_tag_list_on(context, new_list)
233
+ add_custom_context(context)
234
+
235
+ variable_name = "@#{context.to_s.singularize}_list"
236
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
237
+ end
238
+
239
+ def tagging_contexts
240
+ custom_contexts + self.class.tag_types.map(&:to_s)
241
+ end
242
+
243
+ def reload(*args)
244
+ self.class.tag_types.each do |context|
245
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
246
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
247
+ end
248
+
249
+ super(*args)
250
+ end
251
+
252
+ def save_tags
253
+ tagging_contexts.each do |context|
254
+ next unless tag_list_cache_set_on(context)
255
+
256
+ tag_list = tag_list_cache_on(context).uniq
257
+
258
+ # Find existing tags or create non-existing tags:
259
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
260
+
261
+ current_tags = tags_on(context)
262
+ old_tags = current_tags - tag_list
263
+ new_tags = tag_list - current_tags
264
+
265
+ # Find taggings to remove:
266
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
267
+ :context => context.to_s, :tag_id => old_tags).all
268
+
269
+ if old_taggings.present?
270
+ # Destroy old taggings:
271
+ ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
272
+ end
273
+
274
+ # Create new taggings:
275
+ new_tags.each do |tag|
276
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
277
+ end
278
+ end
279
+
280
+ true
281
+ end
282
+ end
283
+ end
284
+ end
@@ -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