acts-as-taggable-on 1.0.13 → 2.0.6

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 (43) hide show
  1. data/CHANGELOG +5 -2
  2. data/Gemfile +10 -0
  3. data/README.rdoc +56 -26
  4. data/Rakefile +46 -16
  5. data/VERSION +1 -1
  6. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  7. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  8. data/lib/acts-as-taggable-on.rb +30 -7
  9. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +132 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +241 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +65 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +43 -373
  15. data/lib/acts_as_taggable_on/acts_as_tagger.rb +60 -45
  16. data/lib/acts_as_taggable_on/compatibility/Gemfile +8 -0
  17. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
  18. data/lib/acts_as_taggable_on/compatibility/postgresql.rb +44 -0
  19. data/lib/acts_as_taggable_on/tag.rb +70 -22
  20. data/lib/acts_as_taggable_on/tag_list.rb +81 -80
  21. data/lib/acts_as_taggable_on/tagging.rb +23 -7
  22. data/lib/acts_as_taggable_on/tags_helper.rb +14 -8
  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 -7
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +102 -55
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +48 -6
  28. data/spec/acts_as_taggable_on/tag_list_spec.rb +21 -3
  29. data/spec/acts_as_taggable_on/tag_spec.rb +77 -24
  30. data/spec/acts_as_taggable_on/taggable_spec.rb +173 -76
  31. data/spec/acts_as_taggable_on/tagger_spec.rb +74 -6
  32. data/spec/acts_as_taggable_on/tagging_spec.rb +22 -7
  33. data/spec/acts_as_taggable_on/tags_helper_spec.rb +4 -6
  34. data/spec/bm.rb +52 -0
  35. data/spec/database.yml +17 -0
  36. data/spec/database.yml.sample +17 -0
  37. data/spec/models.rb +31 -0
  38. data/spec/schema.rb +13 -2
  39. data/spec/spec_helper.rb +52 -40
  40. metadata +31 -9
  41. data/lib/acts_as_taggable_on/group_helper.rb +0 -12
  42. data/spec/acts_as_taggable_on/group_helper_spec.rb +0 -18
  43. data/spec/spec.opts +0 -3
@@ -0,0 +1,241 @@
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}.tagger_id IS NULL 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
+ #
63
+ # Example:
64
+ # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
65
+ # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
66
+ # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
67
+ # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
68
+ def tagged_with(tags, options = {})
69
+ tag_list = ActsAsTaggableOn::TagList.from(tags)
70
+
71
+ return {} if tag_list.empty?
72
+
73
+ joins = []
74
+ conditions = []
75
+
76
+ context = options.delete(:on)
77
+
78
+ if options.delete(:exclude)
79
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
80
+ 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}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
81
+
82
+ elsif options.delete(:any)
83
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
84
+ conditions << "#{table_name}.#{primary_key} 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}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
85
+
86
+ else
87
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
88
+ return scoped(:conditions => "1 = 0") unless tags.length == tag_list.length
89
+
90
+ tags.each do |tag|
91
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
92
+ prefix = "#{safe_tag}_#{rand(1024)}"
93
+
94
+ taggings_alias = "#{table_name}_taggings_#{prefix}"
95
+
96
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
97
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
98
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
99
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
100
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
101
+
102
+ joins << tagging_join
103
+ end
104
+ end
105
+
106
+ taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
107
+
108
+ if options.delete(:match_all)
109
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
110
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
111
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
112
+
113
+ group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
114
+ end
115
+
116
+
117
+ scoped(:joins => joins.join(" "),
118
+ :group => group,
119
+ :conditions => conditions.join(" AND "),
120
+ :order => options[:order],
121
+ :readonly => false)
122
+ end
123
+
124
+ def is_taggable?
125
+ true
126
+ end
127
+ end
128
+
129
+ module InstanceMethods
130
+ # all column names are necessary for PostgreSQL group clause
131
+ def grouped_column_names_for(object)
132
+ self.class.grouped_column_names_for(object)
133
+ end
134
+
135
+ def custom_contexts
136
+ @custom_contexts ||= []
137
+ end
138
+
139
+ def is_taggable?
140
+ self.class.is_taggable?
141
+ end
142
+
143
+ def add_custom_context(value)
144
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
145
+ end
146
+
147
+ def cached_tag_list_on(context)
148
+ self["cached_#{context.to_s.singularize}_list"]
149
+ end
150
+
151
+ def tag_list_cache_set_on(context)
152
+ variable_name = "@#{context.to_s.singularize}_list"
153
+ !instance_variable_get(variable_name).nil?
154
+ end
155
+
156
+ def tag_list_cache_on(context)
157
+ variable_name = "@#{context.to_s.singularize}_list"
158
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
159
+ end
160
+
161
+ def tag_list_on(context)
162
+ add_custom_context(context)
163
+ tag_list_cache_on(context)
164
+ end
165
+
166
+ def all_tags_list_on(context)
167
+ variable_name = "@all_#{context.to_s.singularize}_list"
168
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
169
+
170
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
171
+ end
172
+
173
+ ##
174
+ # Returns all tags of a given context
175
+ def all_tags_on(context)
176
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
177
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
178
+
179
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
180
+ base_tags.where(opts).order("max(#{tagging_table_name}.created_at)").group("#{tag_table_name}.id, #{tag_table_name}.name").all
181
+ end
182
+
183
+ ##
184
+ # Returns all tags that are not owned of a given context
185
+ def tags_on(context)
186
+ base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
187
+ end
188
+
189
+ def set_tag_list_on(context, new_list)
190
+ add_custom_context(context)
191
+
192
+ variable_name = "@#{context.to_s.singularize}_list"
193
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
194
+ end
195
+
196
+ def tagging_contexts
197
+ custom_contexts + self.class.tag_types.map(&:to_s)
198
+ end
199
+
200
+ def reload(*args)
201
+ self.class.tag_types.each do |context|
202
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
203
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
204
+ end
205
+
206
+ super(*args)
207
+ end
208
+
209
+ def save_tags
210
+ tagging_contexts.each do |context|
211
+ next unless tag_list_cache_set_on(context)
212
+
213
+ tag_list = tag_list_cache_on(context).uniq
214
+
215
+ # Find existing tags or create non-existing tags:
216
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
217
+
218
+ current_tags = tags_on(context)
219
+ old_tags = current_tags - tag_list
220
+ new_tags = tag_list - current_tags
221
+
222
+ # Find taggings to remove:
223
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
224
+ :context => context.to_s, :tag_id => old_tags).all
225
+
226
+ if old_taggings.present?
227
+ # Destroy old taggings:
228
+ ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
229
+ end
230
+
231
+ # Create new taggings:
232
+ new_tags.each do |tag|
233
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
234
+ end
235
+ end
236
+
237
+ true
238
+ end
239
+ end
240
+ end
241
+ end
@@ -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([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
34
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
35
+ #{ActsAsTaggableOn::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] ||= ActsAsTaggableOn::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] = ActsAsTaggableOn::TagList.from(new_list)
59
+ end
60
+
61
+ def reload(*args)
62
+ self.class.tag_types.each do |context|
63
+ instance_variable_set("@owned_#{context}_list", nil)
64
+ end
65
+
66
+ super(*args)
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 = ActsAsTaggableOn::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 = ActsAsTaggableOn::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
+ ActsAsTaggableOn::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(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
46
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
47
+ :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],
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(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
58
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
59
+ :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],
60
+ :group => grouped_column_names_for(klass),
61
+ :order => "count DESC" }.update(options))
62
+ end
63
+ end
64
+ end
65
+ end